超性感的React Hooks(二)再谈闭包

6,060 阅读10分钟

再谈闭包

如果你一天没有真正理解它,你就应该继续学习它。

曾经我去找工作面试的时候,我最讨厌别人问我闭包,因为我说不清楚。现在我面试别人了,却又最爱问闭包,因为闭包真的能直接的检验你对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组件

// Counter.jsx
export default function Counter() {}

然后在App模块中使用Counter组件

// App.jsx
import Counter from './Counter';
export default function App() {
 // todo
   return (
    <Counter />
  )
}

结合上面的几个知识点,基础扎实的同学到这里应该能够知道答案了,如果还没想明白,没关系,更详细一步。

上面的代码我们可以手动转换成伪代码

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的模块,代码如下:

// state.js
let state = null;

export const useState = (value: number) => {
  // 第一次调用时没有初始值,因此使用传入的初始值赋值
  state = state || value;

  function dispatch(newValue) {
    state = newValue;
    // 假设此方法能触发页面渲染
    render();
  }

  return [state, dispatch];
}

在其他模块中引入并使用。

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这个模块。

这个文件共有两千多行,是一个非常复杂的模块。

第一步,要搞清楚这个模块的作用。

具体的方法是观察模块返回了什么内容。搜索export。export表示这个模块会对外抛出的接口,这是模块与外部沟通的唯一方式。

搜索export

搜索结果发现大多数export都是type类型声明,我们这里不关注。经过简单的分析,所有的核心逻辑都写在renderWithHooks中。通过断点调试也能定位到这个方法。

快速分析一个函数的作用,一个思路是看它返回了什么,二个思路是看它改变了什么

分析结果发现,该函数修改了外层作用域中的变量,这就是我们想要的重要讯息。

image.png

之前从ReactHooks.js模块中发现useState的实现非常简单,如下

export function useState<S>(initialState: (() => S) | S) {
  const dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}

继续查看resolveDispatcher的实现

function resolveDispatcher() {
  const dispatcher = ReactCurrentDispatcher.current;
  return dispatcher;
}

到这里,其实基本上就对上号了。当然具体原理还要结合Fiber调度来理解,这里不继续深入。我们本文关注的重点仍然在闭包。

从上图中知道,在某种条件下(更新时),ReactCurrentDispatcher.current就是HooksDispatcherOnUpdateInDEV,这个方法在ReactFiberHooks模块中声明。

继续阅读源码,发现HooksDispatcherOnUpdateInDEV是在该模块中定义的一个变量。

image.png

这个时候,我们就应该很自然的想到,奥,这里利用了闭包。

继续通过关键字,发现该变量被赋予了具体值。这些,就全是ReactHooks支持的api。如图

image.png

我们暂时只关注useState,去看看它是如何实现的。

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,但是在初始化时,调用的方法却不一样,如图。

image.png

闭包无处不在,你要体会到这句话的真正含义。

源码阅读并非学习过程中的必要过程,如果JS基础还不够扎实,不用着急纠结于自己读不懂怎么办。慢慢来就可以了。

最后,给大家留一个思考题。著名的状态管理器redux,或者vue中的vuex,他们的实现有没有利用闭包呢?如果有,是如何做到的?他们实现的方式与React中的state有什么异同呢?

本系列文章的所有案例,都可以在下面的地址中查看

github.com/advance-cou…

本系列文章为原创,请勿私自转载,转载请务必私信我