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

简介: 曾经我去找工作面试的时候,我最讨厌别人问我闭包,因为我说不清楚。现在我面试别人了,却又最爱问闭包,因为闭包真的能直接的检验你对JS的理解深度。可能够回答上来的人真的很少。两年以来我面试过估计200多人,其中技术能力最强的是阿里P6的一个胖胖的哥们儿,这里简称PP。PP的JS基础很扎实,对React的理解比较深刻,其他问题上我们聊得很开心。可即使是这样的高手,在闭包的问题上也有些犯难,没有第一时间回答出来我想要的答案。

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


微信图片_20220509194119.png


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


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


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


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


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


微信图片_20220509194549.gif


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


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

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


微信图片_20220509194554.jpg


之前从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是在该模块中定义的一个变量。


微信图片_20220509194558.jpg


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


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


微信图片_20220509194602.jpg


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


微信图片_20220509194607.jpg


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

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


最后,给大家留一个思考题。著名的状态管理器redux,或者vue中的vuex,他们的实现有没有利用闭包呢?

相关文章
|
2月前
|
前端开发 JavaScript
React Hooks 全面解析
【10月更文挑战第11天】React Hooks 是 React 16.8 引入的新特性,允许在函数组件中使用状态和其他 React 特性,简化了状态管理和生命周期管理。本文从基础概念入手,详细介绍了 `useState` 和 `useEffect` 的用法,探讨了常见问题和易错点,并提供了代码示例。通过学习本文,你将更好地理解和使用 Hooks,提升开发效率。
74 4
|
21天前
|
前端开发 JavaScript API
探究 React Hooks:如何利用全新 API 优化组件逻辑复用与状态管理
本文深入探讨React Hooks的使用方法,通过全新API优化组件逻辑复用和状态管理,提升开发效率和代码可维护性。
|
24天前
|
前端开发
深入探索React Hooks:从useState到useEffect
深入探索React Hooks:从useState到useEffect
21 3
|
29天前
|
前端开发 JavaScript
深入探索React Hooks:从useState到useEffect
深入探索React Hooks:从useState到useEffect
|
1月前
|
前端开发 JavaScript 开发者
“揭秘React Hooks的神秘面纱:如何掌握这些改变游戏规则的超能力以打造无敌前端应用”
【10月更文挑战第25天】React Hooks 自 2018 年推出以来,已成为 React 功能组件的重要组成部分。本文全面解析了 React Hooks 的核心概念,包括 `useState` 和 `useEffect` 的使用方法,并提供了最佳实践,如避免过度使用 Hooks、保持 Hooks 调用顺序一致、使用 `useReducer` 管理复杂状态逻辑、自定义 Hooks 封装复用逻辑等,帮助开发者更高效地使用 Hooks,构建健壮且易于维护的 React 应用。
34 2
|
2月前
|
前端开发 开发者
React 提供的其他重要 Hooks
【10月更文挑战第20天】React 提供了一系列强大的 Hooks,除了 `useRef` 之外,还有许多其他重要的 Hooks,它们共同构成了函数式组件开发的基础。
37 6
|
18天前
|
前端开发 JavaScript
React Hooks 深入解析
React Hooks 深入解析
20 0
|
18天前
|
前端开发
React Hooks:从基础到进阶的深入理解
React Hooks:从基础到进阶的深入理解
25 0
|
21天前
|
缓存 前端开发 开发者
深入理解React Hooks,打造高效响应式UI
深入理解React Hooks,打造高效响应式UI
29 0
|
2月前
|
前端开发 JavaScript 开发者
React Hooks
10月更文挑战第13天
37 1