剖析Saga底层原理: 手写Saga核心
Saga简介: (来自Saga文档介绍)redux-saga 是一个用于管理应用程序 Side Effect(副作用,例如异步获取数据,访问浏览器缓存等)的 library,它的目标是让副作用管理更容易,执行更高效,测试更简单,在处理故障时更容易。
可以想像为,一个 saga 就像是应用程序中一个单独的线程,它独自负责处理副作用。 redux-saga 是一个 redux 中间件,意味着这个线程可以通过正常的 redux action 从主应用程序启动,暂停和取消,它能访问完整的 redux state,也可以 dispatch redux action。
redux-saga 使用了 ES6 的 Generator 功能,让异步的流程更易于读取,写入和测试。(如果你还不熟悉的话,这里有一些介绍性的链接) 通过这样的方式,这些异步的流程看起来就像是标准同步的 Javascript 代码。(有点像 async/await,但 Generator 还有一些更棒而且我们也需要的功能)。
- 如上简介所示,在深究Saga之前,希望大家有Es6 Generator函数的基础,否则Saga的原理可能会让你看起来很疑惑~~
- 在此之前,如果对Saga还不够了解可以前往Saga的文档进行基本的Saga使用了解: https://redux-saga-in-chinese.js.org/
- 首先,saga作为redux的一个中间件,所以需要具有redux中间件的基本结构(也就是多层嵌套的函数~~这样说可能比较唐突)
- Saga的实现主要是对Saga中内定的部分核心Effect副作用进行实现,这些Effect来自Saga下的一个effects包
- 从需求出发,实现Saga核心(也就是先根据原本的Saga库写个例子,然后根据这个例子开始实现相应的内容)
// store/index.js
// 定义redux store,添加saga中间件
import { applyMiddleware, createStore } from "redux"
import reducer from "./reducer"
import createSagaMiddleware from "../redux-saga"
import rootSaga from "./rootSaga"
let sagaMiddleware = createSagaMiddleware();
let store = applyMiddleware(sagaMiddleware)(createStore)(reducer);
sagaMiddleware.run(rootSaga)
export default store;
// store/reducer.js
// 定义redux reducer处理
import * as types from "./types"
export default function (state = {number: 0}, action) { // 这里主要实现对number 加一操作
switch(action.type){
case types.INCREMENT:
return {number: state.number + 1};
default:
return state;
}
}
// store/types.js
// 定义actionType类型文件
export const INCREMENT = 'INCREMENT';
export const ASYNC_INCREMENT = "ASYNC_INCREMENT";
// store/action.js
// 定义action
import * as types from "./types"
export default {
asyncIncrement(){
return { type: types.ASYNC_INCREMENT } // 发起异步加一操作的action
}
}
// store/rootSaga/index.js
// saga的根处理文件,最所有的saga处理进行整合
import * as types from "../types"
import { take, put, delay} from "../../redux-saga/effects"
export default function* rootSaga(){
for(let i = 0; i < 3; i++){
yield take(types.ASYNC_INCREMENT);
delay(1000)
yield put({ type: types.INCREMENT });
}
}
实现Saga核心: (这里为了减少篇幅,之间写出最终的核心代码,里面附上详细注释,大家根据注释进行解读)
// redux-saga 核心部分:
// 这个库用于进行一些事件的绑定监听,例如take等Effect的处理
let EventEmitter = require("events")
// 作为redux的中间件, 应该具有的基本结构
export default function createSagaMiddleware(){
let events = new EventEmitter()
function sagaMiddleware({ getState, dispatch }){
function run(generator){
// 使run方法同时支持迭代器和generator参数:
// 如果是一个generator函数则需要执行才能得到的iterator对象
// 如果非函数相当于直接就是迭代器,可以直接进行迭代
let it = typeof generator === "function" ? generator() : generator;
function next(action){
// 这里 next()执行之后得到的是generator中yield执行saga effect得到的结果值(基本都是些内置的Action, 和下面的switch中可以分别匹配)
// 如执行take得到一个take相关的action, put执行得到一个put相关的action
// 这里将action传递给next,将会作为 yield 的返回值,使得在yield之后可以得到异步执行的结果, 例如异步请求的数据等等
let { value: effect, done } = it.next(action);
if(!done){
// 支持itarator处理: (关于Es6迭代器的内容这里不做介绍)
if(typeof effect[Symbol.iterator] == "function"){ // 包含Symbol.iaerator属性且是一个函数,则说明是一个迭代器
run(effect); // 调用run方法进行递归
next();
} else if(effect.then){ // 如果effect是一个Promise, 对Promise进行支持
effect.then(next); // 等待promise的成功后执行next
} else {
switch(effect.type){ // 关于这里swatch匹配后effect中的属性内容大家可以结合后面的effects查看
case 'TAKE':
events.once(effect.actionType, next); // 使用events注册事件,若是take到对应的actionType来到执行next继续往下
break;
case 'PUT':
dispatch(effect.action); // put就直接派发action
next(); // 递归,继续往下进行操作搜集
break;
case 'CALL':
let {fn, args, context} = effect.payload;
// 这里得到的是从delay函数中解析出的payload
// 其中得到的fn函数返回的是一个promise
fn.apply(context, args).then(next);
break;
case 'FORK':
run(effect.task);
next(); // 这里为了不阻塞进程的运行,立即调用next()
break;
case "CPS":
// cps中执行异步的回调函数是这里的next, 在异步执行结束之后继续往下执行,使得整个流程串联起来
effect.fn(...effect.args, (error, value) => {
if(error){ // 如果error有值,则说明发生了错误,直接结束了当前的saga
it.return('发生错位..')
}
next(value)
});
break;
default:
break;
}
}
}
}
next();
}
sagaMiddleware.run = run; // 将sagaMiddleware上挂载run方法,这个方法在定义Saga的时候调用,传入generator
return function(next){ // 这里是redux中间件基本结构
return function(action){
events.emit(action.type, action) // 这里监听actionType, 去出发相应的events
next(action)
}
}
}
// 最终导出sagaMiddleware函数
return sagaMiddleware;
}
effects相关:
// 定义saga相关的effect
export function take(actionType){
return {
type: "TAKE",
actionType
}
}
// put: 将会直接派发一个action
export function put(action) {
return {
type: 'PUT',
action
}
}
// call方法的第一个参数可以传递一个数组,数组的第一个元素为函数执行的上下文: context
// 第二个位置的内容为function
export function call(fn, ...args){
let context = null;
if(Array.isArray(fn)){
context = fn[0];
fn = fn[1];
}
return {
type: "CALL",
payload: { fn, context, args: args }
}
}
export function delay(...args){
return {
type: "CALL",
payload: {fn: delayP, context: null, args: args}
}
}
// 这里得到的就是delay中args中的参数数组,通过apply方法执行传递过来
// args数组中的参数第一个元素就是延时的时间,之后可以传递其他的参数
function delayP(ms, val){
return new Promise(function(resolve){
setTimeout(function(){ //这里使用定时器达到延时效果
resolve(val)
}, ms)
})
}
// 这里fork得到的task任务也是一个generator
export function fork(task){
return {
type: 'FORK',
task
}
}
// 这里类似于开启了一个新的进程,然后在进程里面进行死循环 然后执行take itarator (具体的实现是generator的嵌套执行)
// 这个方法执行后会得到一个generator的itarator的迭代器
export function* takeEvery(actionType, task){
// 这里相当于 yield fork(saga), 也就是Generator嵌套Generator
yield fork(function*(){
while(true){
yield take(actionType);
yield task();
}
})
}
export function cps(fn, ...args){
return {
type: "CPS",
fn,
args
}
}
- 注: 这里为了简便就直接给出了最终写出的核心代码,大家可以将代码拷贝到IDE中,放到项目中进行测试解读,效果应该会更好哈~~