caasih.net

Alt 裡的 Promise

使用 Alt 這套 Flux framework 時,我知道 Store 的 this.waitFor(anotherStore) 是 sync 的,於是 async 的操作,都得放到 actions 中。

TL;DR

把 Promise 記起來,避免重複操作,並讓 action 寫起來跟原本的一樣。但該怎麼記,我還沒有個好方法。

@tomchentw 建議該在 fetch 時解決一切,還提到可以用 prfun 的 Promise.guard ,其他補充在後面。

一開始...

按 Alt 的教學,會寫出這樣的 action :

fetchLocations() {
  // we dispatch an event here so we can have "loading" state.
  this.dispatch();

  LocationsFetcher.fetch()
    .then((locations) => {
      // we can access other actions within our action through `this.actions`
      this.actions.updateLocations(locations);
    })
    .catch((errorMessage) => {
      this.actions.locationsFailed(errorMessage);
    });
}

locationsFailed(errorMessage) {
  this.dispatch(errorMessage);
}

其中第一個 this.dispatch() 表示「這件事情將要發生了」,然後 this.actions.updateLocations(locations) 表示「成功」, this.actions.locationsFailed(errorMessage) 表示失敗。並配上這樣的 store :

handleUpdateLocations(locations) {
  this.locations = locations;
  this.errorMessage = null;
}

handleFetchLocations() {
  // reset the array while we're fetching new locations so React can
  // be smart and render a spinner for us since the data is empty.
  this.locations = [];
}

handleLocationsFailed(errorMessage) {
  this.errorMessage = errorMessage;
}

為了知道現在是不是正在抓資料,也許會加上:

handleUpdateLocations(locations) {
  this.fetching = false;
  // ...
}

handleFetchLocations() {
  this.fetching = true;
  // ...
}

handleLocationsFailed(errorMessage) {
  this.fetching = false;
  // ...
}

如果我希望今天一個操作只會發生一次,我該怎麼做?最傻的方法是在 handleFetchLocations 裡面拋個 error 出來:

handleFetchLocations() {
  if(this.fetching) {
    throw new Error('in progress');
  }
  this.fetching = true;
  // ...
}

但是這麼一來, action 就會變成:

fetchLocations() {
  try {
    this.dispatch();

  	LocationsFetcher.fetch()
      .then((locations) => {
        this.actions.updateLocations(locations);
      })
      .catch((errorMessage) => {
        this.actions.locationsFailed(errorMessage);
      });
  } catch(error) {
    this.actions.locationsFailed(error);
  }
}

呃,兩套處理錯誤的方法寫在一起。

全部交給 Promise

現在想到的解法是,寫個 function 包在 async 動作之外,記下這個動作是不是正在執行:

var doOnce = function doOnce(action) {
  var p = null;
  var release = () => p = null;
  return function() {
    if(p) {
      return { p, isNew: false };
    }
    p = action.apply(this, arguments);
    if(!p.then) {
      p = Promise.resolve(p);
    }
    p.then(release, release);
    return { p, isNew: true };
  }
};

LocationFetcherdoOnce 包起來:

var LocationsFetcher = {
  fetch: doOnce(function () {
    return new Promise(function (resolve, reject) {
      setTimeout(function () {

        // resolve with some mock data
        resolve(mockData);
      }, 250);
    });
  })
};

然後 action 就可以寫成:

fetchLocations() {
  this.dispatch();

  var { p, isNew } = LocationsFetcher.fetch();
  
  if(isNew) {
    p
      .then((locations) => {
        this.actions.updateLocations(locations);
      })
      .catch((errorMessage) => {
        this.actions.locationsFailed(errorMessage);
      });
  }
}

因為 Alt 會 bind 正確的 this ,於是可以寫成:

fetchLocations() {
  this.dispatch();

  var { p, isNew } = LocationsFetcher.fetch();
  
  if(isNew) {
    p
      .then(this.actions.updateLocations)
      .catch(this.actions.locationsFailed);
  }
}

要是不在意 then 和 catch 可能被觸發多次(像是你的 store 不會再發出別的 action ,也很放心讓 React 幫你檢查 DOM 是否需要更新),稍微修改 doOnce 後,甚至可以寫成:

fetchLocations() {
  this.dispatch();

  LocationsFetcher.fetch();
    .then(this.actions.updateLocations)
    .catch(this.actions.locationsFailed);
}

跟本來的 action 幾乎一樣 :D

更多

@tomchentw 在 comment 中提到了個好方法,只要改寫 doOnce ,就可以做到類似的事:

var doOnce = function doOnce(action) {
  var p = null;
  var release = () => p = null;
  return function() {
    if(p) {
      return Promise.reject(new Error('in progress'));
    }
    p = Promise.resolve(action.apply(this, arguments));
    p.then(release, release);
    return p;
  }
};
Isaac Huang 發佈於