超性感的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,他们的实现有没有利用闭包呢?

相关文章
|
4天前
|
设计模式 存储 前端开发
React开发设计模式及原则概念问题之自定义Hooks的作用是什么,自定义Hooks设计时要遵循什么原则呢
React开发设计模式及原则概念问题之自定义Hooks的作用是什么,自定义Hooks设计时要遵循什么原则呢
|
1月前
|
前端开发 JavaScript 安全
TypeScript在React Hooks中的应用:提升React开发的类型安全与可维护性
【7月更文挑战第17天】TypeScript在React Hooks中的应用极大地提升了React应用的类型安全性和可维护性。通过为状态、依赖项和自定义Hooks指定明确的类型,开发者可以编写更加健壮、易于理解和维护的代码。随着React和TypeScript的不断发展,结合两者的优势将成为构建现代Web应用的标准做法。
|
13天前
|
前端开发 开发者
彻底颠覆!React Hooks带来前端开发的革命,你准备好了吗?
【8月更文挑战第6天】在现代Web开发中,React作为顶级前端框架,以高效性能和丰富生态著称。React Hooks自16.8版本引入,赋予函数组件使用状态和生命周期的能力,使代码更简洁、模块化,易于维护。常用Hooks如`useState`、`useEffect`等简化了状态管理和副作用操作。Hooks不仅增强了组件能力,提高了代码可读性和可维护性,还在各种应用场景中展现出强大功能,尤其是在中大型项目中优化了代码结构和提升了开发效率。总之,React Hooks为前端开发注入了新活力,推动了更高效便捷的开发实践。
26 1
|
1月前
|
前端开发
React Hooks实战技巧:提升你的组件开发效率
【7月更文挑战第16天】React Hooks为React开发带来了革命性的变化,使得函数式组件更加强大和灵活。通过掌握上述实战技巧,你可以更高效地编写清晰、可维护和可复用的React组件。希望这些技巧能帮助你在React开发之路上走得更远。
|
10天前
|
JavaScript 前端开发 API
浅谈:为啥 Vue 和 React 都选择了 Hooks?
浅谈:为啥 Vue 和 React 都选择了 Hooks?
|
1月前
|
前端开发 JavaScript 开发者
前端框架与库 - React生命周期与Hooks
【7月更文挑战第13天】React 框架革新UI构建,引入Hooks简化组件状态管理和副作用处理。组件生命周期涉及挂载、更新、卸载,对应不同方法,如`componentDidMount`、`shouldComponentUpdate`等,但现在推荐使用`useState`和`useEffect` Hooks。`useEffect`处理副作用,需注意清理和依赖数组。避免问题的关键在于正确使用Hooks和理解其工作模式,以构建高效应用。
|
1月前
|
前端开发 JavaScript 数据格式
react18【系列实用教程】Hooks (useState,useReducer,useRef,useEffect,useContext,useMemo,useCallback,自定义 Hook )
react18【系列实用教程】Hooks (useState,useReducer,useRef,useEffect,useContext,useMemo,useCallback,自定义 Hook )
40 1
|
1月前
|
缓存 前端开发 JavaScript
React Hooks(实例及详解)
【7月更文挑战第2天】React Hooks(实例及详解)
33 3
|
1月前
|
存储 前端开发 JavaScript
react hooks 学习进阶
【7月更文挑战第12天】 React Hooks(自16.8版起)让函数组件能处理状态和副作用。useState用于添加状态管理,useEffect处理副作用,useContext共享数据,useReducer处理复杂状态逻辑,useRef获取引用。进阶技巧涉及性能优化,如useMemo和useCallback,以及遵循规则避免在不适当位置调用Hooks。理解异步更新机制和结合Redux等库提升应用复杂性管理。持续学习新技巧是关键。
30 0
|
1月前
|
前端开发
react18【系列实用教程】Hooks 闭包陷阱 (2024最新版)含useState 闭包陷阱,useEffect 闭包陷阱,useCallback 闭包陷阱
react18【系列实用教程】Hooks 闭包陷阱 (2024最新版)含useState 闭包陷阱,useEffect 闭包陷阱,useCallback 闭包陷阱
27 0

热门文章

最新文章