前言
高质量的学习需要的是耐心【1】和一些技巧【2】
我不是一个按套路出牌的人,我看过一些文章和资料,不乏写的很不错的,但是觉得太教条主义,而我要做的是传授,我想让程序的世界更生动...
这篇文章,更适用于初级,在这里,不仅仅分享的是技术,还有我的学习理解
正文部分
学习思路
面对一个新的事物,不限于汽车,玩具,游戏,家电。我们总是要了解它的用途和功能以及如何使用它。我觉得这种思想在软件世界里一样通用,只不过一切都变得抽象了,不再是眼睛看到的,而是要通过思考来理解。
就拿redux-saga为例,一个libary库?redux中间件?解决了异步问题? 没错,其实也可以换个方式思考,它是个产物,我们可以把redux-saga 等同于一个 汽车,家电这种。
尝试这种思考模式,我们继续
使用方法和用途
学习redux-saga前需掌握generator,如果你还不熟悉可以去看看 阮一峰的文章 前置条件【3】
redux-saga的使用方法和api这里就不讲解,可以参考官方文档 中文 英文 前置条件
如果你已经掌握了redux-saga的用途和使用方法,那我们继续聊,否则会影响接下来的阅读和理解
需求分析
相信你对redux-saga已经掌握很熟练,这里我们再简短介绍下
概述: redux-saga 是⼀个⽤于管理应⽤程序 Side Effect(副作⽤,例如异步获取数据,访问浏览器缓存等)的 library,它的⽬标是让副作⽤管理更容易,执⾏更⾼效,测试更简单,在处理故障时更容易。
接下来,梳理下重点需求:
1.既然是redux的中间件,createSagaMiddleware的返回产物一定符合中间件定义标准,支持run方法 2.Side Effect 副作用如何处理? 3.take、put、call、fork、all... 方法
逐个击破
effect 和 take、put、call、fork、all
源码:
import { IO } from "./symbols";
import * as effectTypes from "./effectTypes";
const makeEffect = (type, payload) => ({ [IO]: IO, type, payload });
export function take(pattern) {
return makeEffect(effectTypes.TAKE, { pattern });
}
export function put(action) {
return makeEffect(effectTypes.PUT, { action });
}
export function call(fn, ...args) {
return makeEffect(effectTypes.CALL, { fn, args });
}
export function fork(fn, ...args) {
return makeEffect(effectTypes.FORK, { fn, args });
}
export function all(effects) {
return makeEffect(effectTypes.ALL, effects);
}
讲解:
IO 只是一个symbol。
effectTypes 副作用类型常量。
makeEffect 副总用对象生成器,类似于redux的action,不同type来区分行为。
take put等方法最终都是生成副作用对象 { [IO]: IO, type, payload }。
createSagaMiddleware
import runSaga from "./runSaga";
import { stdChannel } from "./channel";
export default function createSagaMiddleware() {
let boundRunSaga;
let channel = stdChannel();
function sagaMiddleWare({ getState, dispatch }) {
boundRunSaga = runSaga.bind(null, {
channel,
getState,
dispatch,
});
return (next) => (action) => {
let result = next(action);
channel.put(action);
return result;
};
}
sagaMiddleWare.run = (...args) => boundRunSaga(...args);
return sagaMiddleWare;
}
讲解:
createSagaMiddleware:创建一个符合redux规范的中间件,同时支持run方法。
这里通过闭包返回sagaMiddleWare,创造了单独的作用域空间,避免污染全局。
runSaga 处理run接收的的函数【generator函数】。
boundRunSaga runSaga以及后面的处理需要getState,dispatch,channel,通过runSaga.bind实现。
stdChannel,channel 一种订阅发布空间,后面会讲。
runSaga
import proc from "./proc";
export default function runSaga({channel, dispatch, getState}, saga, ...args) {
const iterator = saga(...args);
const env = {
dispatch,
getState,
channel
}
proc(env, iterator);
}
讲解:
通过const iterator = saga(...args);执行generator函数,得到一个遍历器对象
调用 proc 【带有effect generator函数过程处理】
proc
import { IO } from "./symbols";
import effectRunnerMap from "./effectRunnermap";
export default function proc(env, iterator, cb) {
next();
function next(arg, isErr) {
let result;
if(isErr) {
result = iterator.throw(arg);
} else {
result = iterator.next(arg)
}
if (!result.done) {
// 遍历没有结束
digestEffect(result.value, next)
} else {
if (typeof cb === 'function') {
cb(result.value)
}
}
}
function runEffect(effect, currCb) {
if (effect && effect[IO]) {
const effectRunner = effectRunnerMap[effect.type];
effectRunner(env, effect.payload, currCb)
} else {
currCb();
}
}
function digestEffect(effect, cb) {
let effectSettled;
function currCb(res, isErr) {
if(effectSettled) return;
effectSettled = true;
cb(res, isErr);
}
runEffect(effect, currCb)
}
}
讲解:
proc 带有effect的generator函数处理,从我的表述,这个函数是一个独立的功能,在redux-saga里多出调用,包括:runCallEffect,runForkEffect,runAllEffect。
iterator generator函数生成的遍历器对象,需要调用next方法来进行下一步。
next 对iterator.next进行了封装,通过digestEffect->runEffect处理next返回的efffect result = iterator.next(arg)得到的是take,put的返回对象,所以这里判断哪一个来具体执行。const effectRunner = effectRunnerMap[effect.type]。
effectRunnermap
import * as effectTypes from './effectTypes';
import proc from './proc';
import * as is from './is';
function runTakeEffect(env, {channel = env.channel, pattern}, cb) {
const matcher = (input) => input.type === pattern;
channel.take(cb, matcher);
}
function runPutEffect(env, {action}, cb) {
console.log('runPutEffect ');
const result = env.dispatch(action);
cb(result);
}
function runCallEffect(env, {fn, args}, cb) {
console.log('runCallEffect ');
const result = fn.apply(null, args);
if(is.promise(result)) {
result.then((data) => {
cb(data);
}).catch((err) => {
cb(err, true);
})
return;
}
if(is.iterator(result)) {
proc(env, result, cb);
return;
}
cb(result);
}
function runForkEffect(env, {fn, args}, cb) {
console.log('runForkEffect');
const taskIterator = fn.apply(null, args);
proc(env, taskIterator);
cb();
}
function runAllEffect(env, effects, cb) {
console.log('runAllEffect');
const len = effects.length;
for (let i = 0; i < len; i++) {
proc(env, effects[i]);
}
cb();
}
const effectRunnerMap = {
[effectTypes.TAKE]: runTakeEffect,
[effectTypes.PUT]: runPutEffect,
[effectTypes.CALL]: runCallEffect,
[effectTypes.FORK]: runForkEffect,
[effectTypes.ALL]: runAllEffect,
}
export default effectRunnerMap;
讲解:
通过策略模式的方式,处理不同的内置effect副作用
channel 订阅发布管理器,take注册监听事件
runCallEffect,runForkEffect,runAllEffect支持generator函数回调,内部也是通过proc来处理,此处体现了proc这个函数的抽象独立功能
channel
import { MATCH } from "./symbols";
export function stdChannel() {
const currentTakers = [];
function take(cb, matcher) {
cb[MATCH] = matcher;
currentTakers.push(cb);
}
function put(input) {
const takers = currentTakers;
for(let i = 0, len=takers.length; i<len ; i++) {
const taker = takers[i];
if(taker[MATCH](input)) {
taker(input);
}
}
}
return {
take,
put,
}
}
讲解:
MATCH 通过type来判断是否是注册的监听函数
take 在订阅监听的函数上加上MATCH
put 接收action,通过action.type判断,触发对应函数,在createSagaMiddleware代码中channel.put(action) 调用了put
总结
通过以上的原理讲解,相信你已经对redux-saga的原理有了更多的理解。
对于一些初学者,经验少的工程师,理解源码原理不算是一件容易的事情,只要你有耐心,不断地反思来提示自己的学习技巧,一定会有质的变化
如果你对redux-sage的源码原理足够掌握,这时候你可以和很多知识体系结合起来,把一些经典的技术手段梳理起来,结合实践融会贯通【4】
例如:封闭的作用域,订阅发布功能,策略模式,bind的应用,所以当你学习基础和细节很重要,多结合实践,多看看高质量代码,你才能更快的成长。
Git代码
注释
1.耐心:必须要有做事持之以恒的精神
2.技巧:学习不是单纯的消耗时间,要在学习过程不断思考,形成一套有效的方法,学习效率会呈指数提升
3.前置条件: 前置条件标记技能是对于接下来的学习必须掌握的,避免阅读后续内容懵圈
4.融会贯通: 把以前学习的知识和现有的结合起来,形成自己的知识网和体系