如果你一天没有真正理解它,你就应该继续学习它。
曾经我去找工作面试的时候,我最讨厌别人问我闭包,因为我说不清楚。现在我面试别人了,却又最爱问闭包,因为闭包真的能直接的检验你对JS的理解深度。可能够回答上来的人真的很少。
两年以来我面试过估计200多人,其中技术能力最强的是阿里P6的一个胖胖的哥们儿,这里简称PP。PP的JS基础很扎实,对React的理解比较深刻,其他问题上我们聊得很开心。可即使是这样的高手,在闭包的问题上也有些犯难,没有第一时间回答出来我想要的答案。
因此,如果有这么一篇两篇文章,能够帮助大家将闭包吃透,我觉得是一件非常了不起的事。在JS基础进阶系列中,我已经将闭包的基础,定义,特点,以及如何在chrome浏览器中观察闭包都一一跟大家分享了,这一篇就着眼于实践继续学习。
就以我和PP同学在面试过程中的对话为引子,对话内容大概如下:
我:能聊聊你对闭包的理解吗 PP:函数执行时访问上层作用域的变量,就能形成闭包,闭包可以持久化保持变量。
我:还有其他的吗? PP:没了
我:我如果说闭包在我们的实践中几乎无处不在,你认同这样的说法吗? PP(有点犹豫):认同
我:那哪些场景有涉及到呢? PP:一时想不起来。
我(不太甘心,继续引导):模块化你应该知道吧,你认为模块和闭包有没有可能存在什么联系? PP:没有
我:确定吗? PP:确定没有!
OK,到这里,如果你是面试官,你觉得PP同学的回答怎么样?达到你的要求了吗?
当然,买过我书并且认真看过的同学应该知道,回答得并不让人满意。这里,我们结合React Hooks的实际情况,接着聊聊这个话题。
也许有的同学会比较奇怪,这系列文章明明就是介绍React Hooks,跟闭包有半毛钱的关系?
事实却相反,闭包,是React Hooks的核心。不理解闭包,React Hooks的使用就无法达到炉火纯青的地步。如果只是基于表面的去使用,看官方文档就可以了,这也不是我们这系列文章的目的。
在接着聊闭包与模块之间的联系之前,我们先来回顾几个的概念。
闭包是一个特殊的对象
它由两部分组成,执行上下文A以及在A中创建的函数B。
当B执行时,如果访问了A中的变量对象,那么闭包就会产生。
在大多数理解中,包括许多著名的书籍、文章里都以函数B的名字代指这里生成的闭包。而在chrome中,则以执行上下文A的函数名代指闭包。
许多地方喜欢用词法环境,或者词法作用域来定义闭包的概念,但是闭包是代码执行过程中才会产生的特殊对象,因此我认为使用执行上下文更为准确。当然,这并不影响闭包的理解与使用。
还有另外一个重要的知识点:
本质上,JavaScript中并没有自己的模块概念,我们只能使用函数/自执行函数来模拟模块。
现在的前端工程中(ES6的模块语法规范),使用的模块,本质上都是函数或者自执行函数。
webpack等打包工具会帮助我们将其打包成为函数
思考一下,定义一个React组件,并且在其他模块中使用,这和闭包有关系吗?来个简单的例子分析试试看。
在模块Counter.jsx中定义一个Counter组件
代码语言:javascript复制// Counter.jsx
export default function Counter() {}
然后在App模块中使用Counter组件
代码语言:javascript复制// App.jsx
import Counter from './Counter';
export default function App() {
// todo
return (
<Counter />
)
}
结合上面的几个知识点,基础扎实的同学到这里应该能够知道答案了,如果还没想明白,没关系,更详细一步。
上面的代码我们可以手动转换成伪代码
代码语言:javascript复制const CounterModule = (function() {
return function Counter() {}
})()
const AppModule = (function() {
const Counter = CounterModule;
return function App() {
return Counter();
}
})()
我们将上面闭包定义的A,B用本例中的名称替换一下:
自执行函数AppModule以及在AppModule中创建的函数App。
当App在render中执行时,访问了AppModule中的变量对象(定义了变量Counter),那么闭包就会产生。
所以,闭包跟模块之间的关系,到这里,就非常清晰了。根据闭包的生成条件与实践场景,我们会发现,模块中,非常容易生成闭包。每一个JS模块都可以认为是一个独立的作用域,当代码执行时,该词法作用域创建执行上下文,如果在模块内部,创建了可供外部引用访问的函数时,就为闭包的产生提供了条件,只要该函数在外部执行访问了模块内部的其他变量,闭包就会产生。
再来一个例子。
定义一个名为State的模块,代码如下:
代码语言:javascript复制// state.js
let state = null;
export const useState = (value: number) => {
// 第一次调用时没有初始值,因此使用传入的初始值赋值
state = state || value;
function dispatch(newValue) {
state = newValue;
// 假设此方法能触发页面渲染
render();
}
return [state, dispatch];
}
在其他模块中引入并使用。
代码语言:javascript复制import React from 'react';
import {useState} from './state';
function Demo() {
// 使用数组解构的方式,定义变量
const [counter, setCounter] = useState(0);
return (
<div onClick={() => setCounter(counter 1)}>hello world, {counter}</div>
)
}
export default Demo();
执行上下文state(模块state)以及在state中创建的函数useState
当useState在Demo中执行时,访问了state中的变量对象,那么闭包就会产生。
思考题:setCounter的执行会产生闭包吗?
根据闭包的特性,state模块中的state变量,会持久存在。因此当Demo函数再次执行时,我们也能获取到上一次Demo函数执行结束时state的值。
这就是React Hooks能够让函数组件拥有内部状态的基本原理。
此处案例中的useState的实现原理与用法,与React Hooks基本一致。但是真正的源码实现肯定不会这么简单粗暴。
我们来简单分析一下React Hooks源码是如何实现的。
需要注意的是,我们这里分析源码的重点,是感悟闭包在React Hooks中扮演的角色。如果要更进步要了解Fiber的原理,以后再跟大家分享。
另外一个值得大家重视的点是,要有意识的总结我在阅读源码过程中的思路,这会对大家想要阅读别人的代码时帮助很大。我就不把方法直接写出来了,具体以后再分享
通过断点调试,发现React Hooks的各种逻辑处理都在ReactCurrentDispatcher[1]这个模块。
这个文件共有两千多行,是一个非常复杂的模块。
第一步,要搞清楚这个模块的作用。
具体的方法是观察模块返回了什么内容。搜索export。export表示这个模块会对外抛出的接口,这是模块与外部沟通的唯一方式。
搜索结果发现大多数export都是type类型声明,我们这里不关注。经过简单的分析,所有的核心逻辑都写在renderWithHooks
中。通过断点调试也能定位到这个方法。
快速分析一个函数的作用,一个思路是看它返回了什么,二个思路是看它改变了什么。
分析结果发现,该函数修改了外层作用域中的变量,这就是我们想要的重要讯息。
之前从ReactHooks.js
模块中发现useState的实现非常简单,如下
export function useState<S>(initialState: (() => S) | S) {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
继续查看resolveDispatcher的实现
代码语言:javascript复制function resolveDispatcher() {
const dispatcher = ReactCurrentDispatcher.current;
return dispatcher;
}
到这里,其实基本上就对上号了。当然具体原理还要结合Fiber调度来理解,这里不继续深入。我们本文关注的重点仍然在闭包。
从上图中知道,在某种条件下(更新时),ReactCurrentDispatcher.current
就是HooksDispatcherOnUpdateInDEV
,这个方法在ReactFiberHooks
模块中声明。
继续阅读源码,发现HooksDispatcherOnUpdateInDEV
是在该模块中定义的一个变量。
这个时候,我们就应该很自然的想到,奥,这里利用了闭包。
继续通过关键字,发现该变量被赋予了具体值。这些,就全是ReactHooks支持的api。如图
我们暂时只关注useState,去看看它是如何实现的。
代码语言:javascript复制useState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
currentHookNameInDev = 'useState';
updateHookTypesDev();
const prevDispatcher = ReactCurrentDispatcher.current;
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
try {
return updateState(initialState);
} finally {
ReactCurrentDispatcher.current = prevDispatcher;
}
},
这里的关键是updateState(initialState)
function updateState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
return updateReducer(basicStateReducer, (initialState: any));
}
继续找到updateReducer
,updateReducer的逻辑比较复杂。不过我们基于上面提到过的两个思路,看他修改了什么,与返回了什么,就能很快理清它。
function updateReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
const hook = updateWorkInProgressHook();
const queue = hook.queue;
// ...
queue.lastRenderedReducer = reducer;
if (numberOfReRenders > 0) {
// This is a re-render. Apply the new render phase updates to the previous
// work-in-progress hook.
const dispatch: Dispatch<A> = (queue.dispatch: any);
if (renderPhaseUpdates !== null) {
// ...
return [hook.memoizedState, dispatch];
}
// The last update in the entire queue
const last = queue.last;
// The last update that is part of the base state.
const baseUpdate = hook.baseUpdate;
const baseState = hook.baseState;
// Find the first unprocessed update.
let first;
if (baseUpdate !== null) {
if (last !== null) {
// For the first update, the queue is a circular linked list where
// `queue.last.next = queue.first`. Once the first update commits, and
// the `baseUpdate` is no longer empty, we can unravel the list.
last.next = null;
}
first = baseUpdate.next;
} else {
first = last !== null ? last.next : null;
}
if (first !== null) {
// ...
hook.memoizedState = newState;
hook.baseUpdate = newBaseUpdate;
hook.baseState = newBaseState;
queue.lastRenderedState = newState;
}
const dispatch: Dispatch<A> = (queue.dispatch: any);
return [hook.memoizedState, dispatch];
}
简化一下源代码,发现逻辑虽然复杂,但是核心的两个东西,还是在于修改了一个叫做hook
的变量,以及返回了[hook.memoizedState, dispatch]
。
这个hook是什么呢?在updateWorkInProgressHook
方法中发现,hook是包含了memoizedState, baseState, queue, baseUpdate, next
属性的一个对象。
function updateWorkInProgressHook(): Hook {
if (nextWorkInProgressHook !== null) {
workInProgressHook = nextWorkInProgressHook;
nextWorkInProgressHook = workInProgressHook.next;
currentHook = nextCurrentHook;
nextCurrentHook = currentHook !== null ? currentHook.next : null;
} else {
invariant(
nextCurrentHook !== null,
'Rendered more hooks than during the previous render.',
);
currentHook = nextCurrentHook;
const newHook: Hook = {
memoizedState: currentHook.memoizedState,
baseState: currentHook.baseState,
queue: currentHook.queue,
baseUpdate: currentHook.baseUpdate,
next: null,
};
if (workInProgressHook === null) {
workInProgressHook = firstWorkInProgressHook = newHook;
} else {
workInProgressHook = workInProgressHook.next = newHook;
}
nextCurrentHook = currentHook.next;
}
return workInProgressHook;
}
updateReducer
返回的数组中,第一个值就是memoizedState
。
因此可以得出结论,其实我们的状态,就缓存在hook.memoizedState
这个值里。
继续观察updateWorkInProgressHook
方法,发现该方法在内部修改了很多外部的变量,workInProgressHook,nextWorkInProgressHook,currentHook
等。而memoizedState: currentHook.memoizedState
。
因此,最终我们的状态,在update时,其实就是存在于currentHook
。这也是利用了闭包。
OK,按照这个思路,React Hooks的源码逻辑很快就能分析出来,不过我们这里的重点是关注闭包在React Hooks中是如何扮演角色的。如果你已经体会到了闭包的作用,本文的目的就基本达到了。
需要注意的是,在更新时,调用的是updateReducer
,但是在初始化时,调用的方法却不一样,如图。
闭包无处不在,你要体会到这句话的真正含义。
源码阅读并非学习的必要过程,如果JS基础还不够扎实,不用着急纠结于自己读不懂怎么办。慢慢来就可以了。
最后,给大家留一个思考题。著名的状态管理器redux,或者vue中的vuex,他们的实现有没有利用闭包呢?
本系列文章的所有案例,都可以在下面的地址中查看
https://github.com/advance-course/react-hooks
本系列文章为原创,请勿私自转载,转载请务必私信我
References
[1]
ReactCurrentDispatcher: https://github.com/facebook/react/blob/master/packages/react/src/ReactCurrentDispatcher.js