Reflux系列01:异步操作经验小结

jopen 8年前

写在前面

在实际项目中,应用往往充斥着大量的异步操作,如ajax请求,定时器等。一旦应用涉及异步操作,代码便会变得复杂起来。在flux体系中,让人困惑的往往有几点:

  1. 异步操作应该在 actions 还是 store 中进行?
  2. 异步操作的多个状态,如pending(处理中)、completed(成功)、failed(失败),该如何拆解维护?
  3. 请求参数校验:应该在 actions 还是 store 中进行校验?校验的逻辑如何跟业务逻辑本身进行分离?

本文从简单的同步请求讲起,逐个对上面3个问题进行回答。一家之言并非定则,读者可自行判别。

本文适合对reflux有一定了解的读者,如尚无了解,可先行查看 官方文档 。本文所涉及的代码示例,可在 此处 下载。

Sync Action:同步操作

同步操作比较简单,没什么好讲的,直接上代码可能更直观。

var Reflux = require('reflux');    var TodoActions = Reflux.createActions({      addTodo: {sync: true}  });    var state = [];  var TodoStore = Reflux.createStore({      listenables: [TodoActions],      onAddTodo: function(text){          state.push(text);          this.trigger(state);      },      getState: function(){          return state;      }  });    TodoStore.listen(function(state){      console.log('state is: ' + state);      });  TodoActions.addTodo('起床');  TodoActions.addTodo('吃早餐');  TodoActions.addTodo('上班');

看下运行结果

➜  examples git:(master) ✗ node 01-sync-actions.js  state is: 起床  state is: 起床,吃早餐  state is: 起床,吃早餐,上班

Async Action:在store中处理

下面是个简单的异步操作的例子。这里通过 addToServer 这个方法来模拟异步请求,并通过 isSucc 字段来控制请求的状态为 成功 还是 失败

可以看到,这里对前面例子中的 state 进行了一定的改造,通过 state.status 来保存请求的状态,包括:

  • pending:请求处理中
  • completed:请求处理成功
  • failed:请求处理失败
var Reflux = require('reflux');    /**   * @param {String} options.text    * @param {Boolean} options.isSucc 是否成功   * @param {Function} options.callback 异步回调   * @param {Number} options.delay 异步延迟的时间   */  var addToServer = function(options){      var ret = {code: 0, text: options.text, msg: '添加成功 :)'};        if(!options.isSucc){          ret = {code: -1, msg: '添加失败!'};      }        setTimeout(function(){          options.callback && options.callback(ret);      }, options.delay);  };      var TodoActions = Reflux.createActions(['addTodo']);    var state = {      items: [],      status: ''  };    var TodoStore = Reflux.createStore({        init: function(){          state.items.push('睡觉');      },        listenables: [TodoActions],        onAddTodo: function(text, isSucc){          var that = this;            state.status = 'pending';          that.trigger(state);            addToServer({              text: text,              isSucc: isSucc,              delay: 500,              callback: function(ret){                  if(ret.code===0){                      state.status = 'success';                      state.items.push(text);                  }else{                      state.status = 'error';                  }                  that.trigger(state);              }          });      },      getState: function(){          return state;      }  });    TodoStore.listen(function(state){      console.log('status is: ' + state.status + ', current todos is: ' + state.items);  });    TodoActions.addTodo('起床', true);  TodoActions.addTodo('吃早餐', false);  TodoActions.addTodo('上班', true);

看下运行结果:

➜  examples git:(master) ✗ node 02-async-actions-in-store.js   status is: pending, current todos is: 睡觉  status is: pending, current todos is: 睡觉  status is: pending, current todos is: 睡觉  status is: success, current todos is: 睡觉,起床  status is: error, current todos is: 睡觉,起床  status is: success, current todos is: 睡觉,起床,上班

Async Action:在store中处理 潜在的问题

首先,祭出官方flux架构示意图,相信大家对这张图已经很熟悉了。flux架构最大的特点就是 单向数据流 ,它的好处在于 可预测易测试

一旦将异步逻辑引入store,单向数据流被打破,应用的行为相对变得难以预测,同时单元测试的难度也会有所增加。

ps:在大部分情况下,将异步操作放在store里,简单粗暴有效,反而可以节省不少代码,看着也直观。究竟放在actions、store里,笔者是倾向于放在 actions 里的,读者可自行斟酌。

毕竟,社区对这个事情也还在吵个不停。。。

Async 操作:在actions中处理

还是前面的例子,稍作改造,将异步的逻辑挪到 actions 里,二话不说上代码。

reflux是比较接地气的flux实现,充分考虑到了异步操作的场景。定义action时,通过 asyncResult: true 标识:

  1. 操作是异步的。
  2. 异步操作是分状态(生命周期)的,默认的有 completed 、 failed 。可以通过 children 参数自定义请求状态。
  3. 在store里通过类似 onAddTodo 、 onAddTodoCompleted 、 onAddTodoFailed 对请求的不同的状态进行处理。
var Reflux = require('reflux');    /**   * @param {String} options.text    * @param {Boolean} options.isSucc 是否成功   * @param {Function} options.callback 异步回调   * @param {Number} options.delay 异步延迟的时间   */  var addToServer = function(options){      var ret = {code: 0, text: options.text, msg: '添加成功 :)'};        if(!options.isSucc){          ret = {code: -1, msg: '添加失败!'};      }        setTimeout(function(){          options.callback && options.callback(ret);      }, options.delay);  };      var TodoActions = Reflux.createActions({      addTodo: {asyncResult: true}  });    TodoActions.addTodo.listen(function(text, isSucc){      var that = this;      addToServer({          text: text,          isSucc: isSucc,          delay: 500,          callback: function(ret){              if(ret.code===0){                  that.completed(ret);              }else{                  that.failed(ret);              }          }      });  });      var state = {      items: [],      status: ''  };    var TodoStore = Reflux.createStore({        init: function(){          state.items.push('睡觉');      },        listenables: [TodoActions],        onAddTodo: function(text, isSucc){          var that = this;            state.status = 'pending';          this.trigger(state);      },        onAddTodoCompleted: function(ret){          state.status = 'success';          state.items.push(ret.text);          this.trigger(state);      },        onAddTodoFailed: function(ret){          state.status = 'error';          this.trigger(state);      },        getState: function(){          return state;      }  });    TodoStore.listen(function(state){      console.log('status is: ' + state.status + ', current todos is: ' + state.items);  });    TodoActions.addTodo('起床', true);  TodoActions.addTodo('吃早餐', false);  TodoActions.addTodo('上班', true);

运行,看程序输出

➜  examples git:(master) ✗ node 03-async-actions-in-action.js   status is: pending, current todos is: 睡觉  status is: pending, current todos is: 睡觉  status is: pending, current todos is: 睡觉  status is: success, current todos is: 睡觉,起床  status is: error, current todos is: 睡觉,起床  status is: success, current todos is: 睡觉,起床,上班

Async Action:参数校验

前面已经示范了如何在actions里进行异步请求,接下来简单演示下异步请求的前置步骤:参数校验。

预期中的流程是:

流程1:参数校验 --> 校验通过 --> 请求处理中 --> 请求处理成功(失败) 流程2:参数校验 --> 校验不通过 --> 请求处理失败

预期之外:store.onAddTodo 触发

直接对上一小节的代码进行调整。首先判断传入的 text 参数是否是字符串,如果不是,直接进入错误处理。

var Reflux = require('reflux');    /**   * @param {String} options.text    * @param {Boolean} options.isSucc 是否成功   * @param {Function} options.callback 异步回调   * @param {Number} options.delay 异步延迟的时间   */  var addToServer = function(options){      var ret = {code: 0, text: options.text, msg: '添加成功 :)'};        if(!options.isSucc){          ret = {code: -1, msg: '添加失败!'};      }        setTimeout(function(){          options.callback && options.callback(ret);      }, options.delay);  };      var TodoActions = Reflux.createActions({      addTodo: {asyncResult: true}  });    TodoActions.addTodo.listen(function(text, isSucc){      var that = this;        if(typeof text !== 'string'){          that.failed({ret: 999, text: text, msg: '非法参数!'});          return;      }        addToServer({          text: text,          isSucc: isSucc,          delay: 500,          callback: function(ret){              if(ret.code===0){                  that.completed(ret);              }else{                  that.failed(ret);              }          }      });  });      var state = {      items: [],      status: ''  };    var TodoStore = Reflux.createStore({        init: function(){          state.items.push('睡觉');      },        listenables: [TodoActions],        onAddTodo: function(text, isSucc){          var that = this;            state.status = 'pending';          this.trigger(state);      },        onAddTodoCompleted: function(ret){          state.status = 'success';          state.items.push(ret.text);          this.trigger(state);      },        onAddTodoFailed: function(ret){          state.status = 'error';          this.trigger(state);      },        getState: function(){          return state;      }  });    TodoStore.listen(function(state){      console.log('status is: ' + state.status + ', current todos is: ' + state.items);  });    // 非法参数  TodoActions.addTodo(true, true);

运行看看效果。这里发现一个问题,尽管参数校验不通过,但 store.onAddTodo 还是被触发了,于是打印出了 status is: pending, current todos is: 睡觉 。

而按照我们的预期, store.onAddTodo 是不应该触发的。

➜  examples git:(master) ✗ node 04-invalid-params.js   status is: pending, current todos is: 睡觉  status is: error, current todos is: 睡觉

shouldEmit 阻止store.onAddTodo触发

好在reflux里也考虑到了这样的场景,于是我们可以通过 shouldEmit 来阻止 store.onAddTodo 被触发。关于这个配置参数的使用,可参考 文档

看修改后的代码

var Reflux = require('reflux');    /**   * @param {String} options.text    * @param {Boolean} options.isSucc 是否成功   * @param {Function} options.callback 异步回调   * @param {Number} options.delay 异步延迟的时间   */  var addToServer = function(options){      var ret = {code: 0, text: options.text, msg: '添加成功 :)'};        if(!options.isSucc){          ret = {code: -1, msg: '添加失败!'};      }        setTimeout(function(){          options.callback && options.callback(ret);      }, options.delay);  };      var TodoActions = Reflux.createActions({      addTodo: {asyncResult: true}  });    TodoActions.addTodo.shouldEmit = function(text, isSucc){      if(typeof text !== 'string'){          this.failed({ret: 999, text: text, msg: '非法参数!'});          return false;      }      return true;  };    TodoActions.addTodo.listen(function(text, isSucc){      var that = this;        addToServer({          text: text,          isSucc: isSucc,          delay: 500,          callback: function(ret){              if(ret.code===0){                  that.completed(ret);              }else{                  that.failed(ret);              }          }      });  });      var state = {      items: [],      status: ''  };    var TodoStore = Reflux.createStore({        init: function(){          state.items.push('睡觉');      },        listenables: [TodoActions],        onAddTodo: function(text, isSucc){          var that = this;            state.status = 'pending';          this.trigger(state);      },        onAddTodoCompleted: function(ret){          state.status = 'success';          state.items.push(ret.text);          this.trigger(state);      },        onAddTodoFailed: function(ret){          state.status = 'error';          this.trigger(state);      },        getState: function(){          return state;      }  });    TodoStore.listen(function(state){      console.log('status is: ' + state.status + ', current todos is: ' + state.items);  });    // 非法参数  TodoActions.addTodo(true, true);  setTimeout(function(){      TodoActions.addTodo('起床', true);  }, 100)

再次运行看看效果。通过对比可以看到,当 shouldEmit 返回 false ,就达到了之前预期的效果。

➜  examples git:(master) ✗ node 05-invalid-params-shouldEmit.js   status is: error, current todos is: 睡觉  status is: pending, current todos is: 睡觉  status is: success, current todos is: 睡觉,起床

写在后面

flux的实现细节存在不少争议,而针对文中例子,reflux的设计比较灵活,同样是使用reflux,也可以有多种实现方式,具体全看判断取舍。

最后,欢迎交流。

</div>

来自: http://imweb.io/topic/568b62464c44bcc56092e418