type
Post
status
Published
date
Apr 6, 2018
slug
summary
偶然间发现 redux-thunk 的源码仅有 11 行
tags
ReactJS
Web dev
category
技术分享
icon
password
偶然间发现 redux-thunk 的源码仅有 11 行:
function createThunkMiddleware(extraArgument) { return ({ dispatch, getState }) => next => action => { if (typeof action === "function") { return action(dispatch, getState, extraArgument); } return next(action); }; } const thunk = createThunkMiddleware(); thunk.withExtraArgument = createThunkMiddleware; export default thunk;
虽然我现在工作中使用的是 dva,而 dva 是基于 redux-saga 的,但这短短 11 行代码就能收获 8k+的 star 就足以让每一个 react 开发者去学习理解。

1.什么是“thunk”

Thunk 函数诞生于上个世纪 60 年代,那个时候计算机科学家们还在研究怎么把编译器写好。其中一个争论的焦点就是求值策略,他们分为两派,传值调用传名调用,我们看下面的例子:
var x = 1; function f(m) { return m * 2; } f(x + 5);
传值调用:在进入函数体之前,就计算 x+5 的值(等于 6),再将这个值传入函数 f。C 语言就采用这种策略。
f(x + 5); // 传值调用时,等同于 f(6);
传名调用:即直接将表达式 x+5 传入函数体,只在用到它的时候求值。Hskell 语言采用这种策略。
f(x + 5)( // 传名调用时,等同于 x + 5 ) * 2;
传值调用和传名调用,各有利弊。传值调用比较简单,但是对参数求值的时候,实际上还没用到这个参数,有可能造成性能损失,比如下面的例子:
function f(a, b) { return b; } f(3 * x * x - 2 * x - 1, x);
函数体内根本没用到参数 a,却对它求值了。
编译器“传名调用”的实现,往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体。这个临时函数就叫做 Thunk 函数。
function f(m) { return m * 2; } f(x + 5); // 等同于 var thunk = function() { return x + 5; }; function f(thunk) { return thunk() * 2; }
Wikipedia:
In computer programming, a thunk is a subroutine used to inject an additional calculation into another subroutine. Thunks are primarily used to delay a calculation until its result is needed, or to insert operations at the beginning or end of the other subroutine. They have a variety of other applications in compiler code generation and modular programming.
在计算机编程中,thunk 是一个子程序,用于将另外的计算注入到另一个子程序中。Thunk 主要用于延迟计算直到需要结果,或者在另一个子程序的开始或结束处插入操作。它在编译器代码生成和模块化编程中有各种其他应用。

2.JavaScript 中的 Thunk

JavaScript 语言是传值调用,它的 Thunk 函数含义有所不同。在 JavaScript 语言中,Thunk 函数替换的不是表达式,而是多参数函数,将其替换成单参数的版本,且只接受回调函数作为参数。看下面的例子:
// 正常版本的readFile(多参数版本) fs.readFile(fileName, callback);
// Thunk版本的readFile(单参数版本)var readFileThunk = Thunk(fileName);readFileThunk(callback);var Thunk = function(fileName) { return function(callback) { return fs.readFile(fileName, callback); };};
下面是一个简单的 Thunk 函数转换器。
// Thunk版本的readFile(单参数版本) var readFileThunk = Thunk(fileName); readFileThunk(callback); var Thunk = function(fileName) { return function(callback) { return fs.readFile(fileName, callback); }; };
这样说来,thunk 和 cps 还有点联系。
怎样理解 Continuation-passing style? - 伍成然的回答 - 知乎 https://www.zhihu.com/question/20259086/answer/339409254
乍一看 thunk 和 cps 恰好相反(thunk 将一个多参数函数转为多个单参数函数,cps 将多个单参数函数转为一个多参数函数),但其实他们的内在原理是一样的,那就是:
当 A 函数的执行依赖于 B 函数的执行结果时,就把 B 函数和 A 函数放入同一个调用栈中,并让 A 先入栈,即在 A 函数中调用 B 函数。
其实,thunk 是基于 cps,当一个函数可以写成 cps 时,我们可以使用 thunk 对该函数进行改造,使其看上去更像同步的写法,而不是回调的写法。

3.Redux-Thunk

我们还是扯回来,再来看看 redux-thunk 的源码,或许很多人一开始就直接被一堆箭头函数打懵了,你可以用 babel 转成 ES5 慢慢看,不过我建议你先去看看 redux 的Middleware,看完你就知道 middleware 的外层写法都一样() => store => next => action =>,这里作者只是将 store 解构了。
所以它的核心代码在于:
if (typeof action === "function") { return action(dispatch, getState, extraArgument); } return next(action);
这里的意思是当 action 是一个函数时,返回它的执行结果。如果本身传入的函数是一个异步函数,我们完全可以在函数调用结束后,获取必要的数据再次触发 dispatch 由此实现异步效果。 下面是我们的 action.js
export const ADD = "ADD"; export const add = payload => ({ type: ADD, payload }); export const addAsync = payload => dispatch => setTimeout(() => dispatch(add(payload)), 1000);
我们看看没引入 redux-thunk 时,一个有效的 action 应该是这样,一个 plain object:
{ type, payload }
当我们在没引入 redux-thunk,又想把 action 写成函数时,redux 会报错:
notion image
当我们引入 redux-thunk 时,action 就可以是一个函数了,它的参数有 dispatch, getState, extraArgument,这里我们只用了 dispatch
dispatch => setTimeout(() => dispatch(add(payload)), 1000);

总结

redux-thunk 的源码很简单,简单得我在想为什么 redux 不内置进去。而当我完整分析完,或许你就会明白,redux-thunk 就是 redux 的 middleware 的最佳实践之一,我们完全可以在此基础上做更多的扩展。比如你想将 action 写成一个 Promise,你可以使用下面的 middleware:
const promiseAction = store => next => action => { if (typeof action.then !== "function") { return next(action); } return Promise.resolve(action).then(store.dispatch); };
所以,redux 的 middleware 还是需要多多关注啊!
2018-52017-8