Reflux系列01:異步操作經驗小結


寫在前面

在實際項目中,應用往往充斥着大量的異步操作,如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,單向數據流被打破,應用的行為相對變得難以預測,同時單元測試的難度也會有所增加。

enter image description here

ps:在大部分情況下,將異步操作放在store里,簡單粗暴有效,反而可以節省不少代碼,看着也直觀。究竟放在actions、store里,筆者是傾向於放在actions里的,讀者可自行斟酌。

畢竟,社區對這個事情也還在吵個不停。。。

Async 操作:在actions中處理

還是前面的例子,稍作改造,將異步的邏輯挪到actions里,二話不說上代碼。

reflux是比較接地氣的flux實現,充分考慮到了異步操作的場景。定義action時,通過asyncResult: true標識:

  1. 操作是異步的。
  2. 異步操作是分狀態(生命周期)的,默認的有completedfailed。可以通過children參數自定義請求狀態。
  3. 在store里通過類似onAddTodoonAddTodoCompletedonAddTodoFailed對請求的不同的狀態進行處理。
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,也可以有多種實現方式,具體全看判斷取舍。

最后,歡迎交流。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM