React深入分析useEffect源码

目录
  • 热身准备
  • 初始化 mount
  • 更新 update
    • updateEffect
  • 执行副作用
  • 总结

热身准备

这里不再讲useLayoutEffect,它和useEffect的代码是一样的,区别主要是:

  • 执行时机不同;
  • useEffect是异步, useLayoutEffect是同步,会阻塞渲染;

初始化 mount

mountEffect

在所有hook初始化时都会通过下面这行代码实现hook结构的初始化和存储,这里不再讲mountWorkInProgressHook方法

var hook = mountWorkInProgressHook();

mountEffect方法中,只有这几行代码。先来解读下几个参数:

  • fiberFlags:有副作用的更新标记,用来标记hook所在的fiber
  • hookFlags:副作用标记;
  • create:使用者传入的回调函数;
  • deps:使用者传入的数组依赖;
function mountEffectImpl(fiberFlags, hookFlags, create, deps) {
  // hook初始化
  var hook = mountWorkInProgressHook();
  // 判断是否有传入deps,如果有会作为下次更新的deps
  var nextDeps = deps === undefined ? null : deps;
  // 给hook所在的fiber打上有副作用的更新的标记
  currentlyRenderingFiber$1.flags |= fiberFlags;
  // 将副作用操作存放到fiber.memoizedState.hook.memoizedState中
  hook.memoizedState = pushEffect(HasEffect | hookFlags, create, undefined, nextDeps);
}

上面代码中都有注释,接下来我们看看React是如何存放副作用更新操作的,主要就是pushEffect方法

function pushEffect(tag, create, destroy, deps) {
  // 初始化副作用结构,
  var effect = {
    tag: tag,
    create: create,   // 回调函数
    destroy: destroy,  // 回调函数里的return(mount时是undefined)
    deps: deps,    // 依赖数组
    // 闭环链表
    next: null
  };
  // 下面的一大段代码看着复杂,但是有没有很熟悉的感觉?
  var componentUpdateQueue = currentlyRenderingFiber$1.updateQueue;
  if (componentUpdateQueue === null) {
    componentUpdateQueue = createFunctionComponentUpdateQueue();
    currentlyRenderingFiber$1.updateQueue = componentUpdateQueue;
    // effect.next = effect形成环形链表
    componentUpdateQueue.lastEffect = effect.next = effect;
  } else {
    var lastEffect = componentUpdateQueue.lastEffect;
    if (lastEffect === null) {
      componentUpdateQueue.lastEffect = effect.next = effect;
    } else {
      var firstEffect = lastEffect.next;
      lastEffect.next = effect;
      effect.next = firstEffect;
      componentUpdateQueue.lastEffect = effect;
    }
  }
  return effect;
}

上面这段代码除了初始化副作用的结构代码外,都是我们前面讲过的操作闭环链表,向链表末尾添加新的effect,该effect.next指向fisrtEffect,并且链表当前的指针指向最新添加的effect

useEffect的初始化就这么简单,简单总结一下:给hook所在的fiber打上副作用更新标记,并且fiber.memoizedState.hook.memoizedStatefiber.updateQueue存储了相关的副作用,这些副作用通过闭环链表的结构存储。

相关参考视频讲解:传送门

更新 update

updateEffect

updateWorkInProgressHook在上篇文章也已讲过,不再详述,主要功能就是创建一个带有回调函数的newHook去覆盖之前的hook

function updateEffectImpl(fiberFlags, hookFlags, create, deps) {
  var hook = updateWorkInProgressHook();
  var nextDeps = deps === undefined ? null : deps;
  var destroy = undefined;
  if (currentHook !== null) {
    var prevEffect = currentHook.memoizedState;
    destroy = prevEffect.destroy;
    if (nextDeps !== null) {
      var prevDeps = prevEffect.deps;
      // 比较两次依赖数组中的值是否有变化
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        // 和之前初始化时一样
        pushEffect(hookFlags, create, destroy, nextDeps);
        return;
      }
    }
  }
  // 和之前初始化时一样
  currentlyRenderingFiber$1.flags |= fiberFlags;
  hook.memoizedState = pushEffect(HasEffect | hookFlags, create, destroy, nextDeps);
}

相信眼眼尖的看官已经注意到上面代码中有两个pushEffect,一个没有赋值给hook.memoizedState,一个赋值了,这两者有什么区别呢?

先保留着这个疑问,先来了解下下面这行代码都做了些什么,因为它造就了两个pushEffect

if (areHookInputsEqual(nextDeps, prevDeps)){...}

function areHookInputsEqual(nextDeps, prevDeps) {
  // 没有传deps的情况返回false
  if (prevDeps === null) {
    return false;
  }
  // deps不是[],且其中的值有变动才会返回false
  for (var i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
    if (objectIs(nextDeps[i], prevDeps[i])) {
      continue;
    }
    return false;
  }
  // deps = [],或者deps里面的值没有变化会返回true
  return true;
}

它会判断两次依赖数组中的值是否有变化以及deps是否是空数组来决定返回truefalse,返回true表明这次不需要调用回调函数。

现在我们明白了两次pushEffect的异同,if内部的pushEffect是不需要调用的回调函数, 外面的pushEffect是需要调用的。再来仔细看下这两行代码:

// if内部的,第一个参数是hookFlags = 4
pushEffect(hookFlags, create, destroy, nextDeps);
// if外部的,第一个参数是HasEffect | hookFlags = 5
hook.memoizedState = pushEffect(HasEffect | hookFlags, create, destroy, nextDeps);

这两行代码的区别是传入的第一个参数不同,而第一个参数就是effect.tag的值,effect.tag = 4不会添加到副作用执行队列,而effect.tag = 5可以。没有添加到副作用执行队列的effect就不会执行。这样就巧妙的实现了useEffect基于deps来判断是否需要执行回调函数。

到这里, 我们搞明白了,不管useEffect里的deps有没有变化都会为回调函数创建effect并添加到effect链表和fiber.updateQueue中,但是React会根据effect.tag来决定该effect是否要添加到副作用执行队列中去执行。

执行副作用

我们现在知道了,useEffect是异步执行的。那么这个回调函数副作用会在什么时候执行呢?useEffect回调函数会在layout阶段之后执行。现在我们来了解下具体调用执行的流程。

我画了一个简单的流程图,大致描述了下调用流程。首先在mutation之前阶段,基于副作用创建任务并放到taskQueue中,同时会执行requestHostCallback,这个方法就涉及到了异步了,它首先考虑使用MessageChannel实现异步,其次会考虑使用setTimeout实现。使用MessageChannel时,requestHostCallback会马上执行port.postMessage(null);,这样就可以在异步的第一时间执行workLoopworkLoop会遍历taskQueue,执行任务,如果是useEffecteffect任务,会调用flusnPassiveEffects

Q:可能有人会疑惑为什么优先考虑MessageChannel

A: 首先我们要明白React调度更新的目的是为了时间分片,意思是每隔一段时间就把主线程还给浏览器,避免长时间占用主线程导致页面卡顿。使用MessageChannelSetTimeout的目的都是为了创建宏任务,因为宏任务会在当前微任务都执行完后,等到浏览器主线程空闲后才会执行。不优先考虑setTimeout的原因是,setTimeout执行时间不准确,会造成时间浪费,即使是setTimeout(fn, 0),感兴趣的可以去自己了解下,本文不做赘述了。

schedulePassiveEffects中,会决定是否执行effect链表中的effect,判断的依据就是每个effect上的effect.tag:

function schedulePassiveEffects(finishedWork) {
  var updateQueue = finishedWork.updateQueue;
  var lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
  if (lastEffect !== null) {
    var firstEffect = lastEffect.next;
    var effect = firstEffect;
    // 遍历effect链表
    do {
      var _effect = effect,
          next = _effect.next,
          tag = _effect.tag;
      // 基于effect.tag决定是否添加到副作用执行队列
      if ((tag & Passive$1) !== NoFlags$1 && (tag & HasEffect) !== NoFlags$1) {
        enqueuePendingPassiveHookEffectUnmount(finishedWork, effect);
        enqueuePendingPassiveHookEffectMount(finishedWork, effect);
      }
      effect = next;
    } while (effect !== firstEffect);
  }
}

flushPassiveEffects中,会先执行上次更新动作的销毁函数,然后再执行本次更新动作的回调函数,并且会把回调函数的return作为下次更新动作的销毁函数。

function flushPassiveEffectsImpl() {
  // 执行上次更新动作的销毁函数
  var unmountEffects = pendingPassiveHookEffectsUnmount;
  pendingPassiveHookEffectsUnmount = [];
  for (var i = 0; i < unmountEffects.length; i += 2) {
    ...destroy()
  }
  // 执行本次更新动作的回调函数
  var mountEffects = pendingPassiveHookEffectsMount;
  pendingPassiveHookEffectsMount = [];
  for (var _i = 0; _i < mountEffects.length; _i += 2) {
    ...create()
  }
}

上面代码中的这两行就是来自副作用执行队列,已经过滤掉了不需要执行的effect,只执行该队列上的副作用函数

var unmountEffects = pendingPassiveHookEffectsUnmount;
var mountEffects = pendingPassiveHookEffectsMount;

总结

看完这篇文章, 我们可以弄明白下面这几个问题:

  • useEffectuseLayoutEffect的区别?
  • useEffect是怎么判断回调函数是否需要执行的?
  • useEffect是同步还是异步?
  • useEffect是通过什么实现异步的?
  • useEffect为什么要要优先选用MessageChannel实现异步?

到此这篇关于React深入分析useEffect源码的文章就介绍到这了,更多相关React useEffect内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • React前端开发createElement源码解读

    目录 React 与 Babel 元素标签转译 组件转译 子元素转译 createElement 源码 函数入参 第一段代码 __self 和 __source 第二段代码 props 对象 第三段代码 children 第四段代码 defaultProps 第五段代码 owner ReactElement 源码 REACT_ELEMENT_TYPE 回顾 React 与 Babel 元素标签转译 用过 React 的同学都知道,当我们这样写时: <div id="foo">

  • React超详细分析useState与useReducer源码

    目录 热身准备 为什么会有hooks hooks执行时机 两套hooks hooks存储 初始化 mount useState mountWorkInProgressHook 更新update updateState updateReducer updateWorkInProgressHook 总结 热身准备 在正式讲useState,我们先热热身,了解下必备知识. 为什么会有hooks 大家都知道hooks是在函数组件的产物.之前class组件为什么没有出现hooks这种东西呢? 答案很简单,

  • React commit源码分析详解

    目录 总览 commitBeforeMutationEffects commitMutationEffects 插入 dom 节点 获取父节点及插入位置 判断当前节点是否为单节点 在对应位置插入节点 更新 dom 节点 更新 HostComponent 更新 HostText 删除 dom 节点 unmountHostComponents commitNestedUnmounts commitUnmount commitLayoutEffects 执行生命周期 处理回调 总结 总览 commit

  • React Fiber源码深入分析

    目录 前言 React架构前世今生 React@15及之前 React@16及之后 Fiber Fiber简单理解 Fiber结构 Fiber工作原理 mount update 前言 本次React源码参考版本为17.0.3. React架构前世今生 查阅文档了解到, React@16.x是个分水岭. React@15及之前 在16之前,React架构大致可以分为两层: Reconciler: 主要职责是对比查找更新前后的变化的组件: Renderer: 主要职责是基于变化渲染页面: 但是Rea

  • React深入分析useEffect源码

    目录 热身准备 初始化 mount 更新 update updateEffect 执行副作用 总结 热身准备 这里不再讲useLayoutEffect,它和useEffect的代码是一样的,区别主要是: 执行时机不同: useEffect是异步, useLayoutEffect是同步,会阻塞渲染: 初始化 mount mountEffect 在所有hook初始化时都会通过下面这行代码实现hook结构的初始化和存储,这里不再讲mountWorkInProgressHook方法 var hook =

  • React之echarts-for-react源码解读

    目录 前言 从与原生初始化对比开始 陷阱-默认值height为300px 主逻辑源码剖析 挂载渲染过程 更新渲染过程 卸载过程 项目依赖 后续 前言 在当前工业4.0和智能制造的产业升级浪潮当中,智慧大屏无疑是展示企业IT成果的最有效方式之一.然而其背后怎么能缺少ECharts的身影呢?对于React应用而言,直接使用ECharts并不是最高效且优雅的方式,而echarts-for-react则是针对React应用对ECharts进行轻量封装和增强的工具库. echarts-for-react的

  • react diff算法源码解析

    React中Diff算法又称为调和算法,对应函数名为reconcileChildren,它的主要作用是标记更新过程中那些元素发生了变化,这些变化包括新增.移动.删除.过程发生在beginWork阶段,只有非初次渲染才会Diff. 以前看过一些文章将Diff算法表述为两颗Fiber树的比较,这是不正确的,实际的Diff过程是一组现有的Fiber节点和新的由JSX生成的ReactElement的比较,然后生成新的Fiber节点的过程,这个过程中也会尝试复用现有Fiber节点. 节点Diff又分为两种

  • React事件机制源码解析

    React v17里事件机制有了比较大的改动,想来和v16差别还是比较大的. 本文浅析的React版本为17.0.1,使用ReactDOM.render创建应用,不含优先级相关. 原理简述 React中事件分为委托事件(DelegatedEvent)和不需要委托事件(NonDelegatedEvent),委托事件在fiberRoot创建的时候,就会在root节点的DOM元素上绑定几乎所有事件的处理函数,而不需要委托事件只会将处理函数绑定在DOM元素本身. 同时,React将事件分为3种类型--d

  • ahooks整体架构及React工具库源码解读

    目录 引言 React hooks utils 库 ahooks 简介 特点 hooks 种类 ahooks 整体架构 项目启动 整体结构 hooks 总结 引言 本文是深入浅出 ahooks 源码系列文章的第一篇,这个系列的目标主要有以下几点: 加深对 React hooks 的理解. 学习如何抽象自定义 hooks.构建属于自己的 React hooks 工具库. 培养阅读学习源码的习惯,工具库是一个对源码阅读不错的选择. 注:本系列对 ahooks 的源码解析是基于 v3.3.13.自己

  • 深入研究React中setState源码

    React作为一门前端框架,虽然只是focus在MVVM中的View部分,但还是实现了View和model的绑定.修改数据的同时,可以实现View的刷新.这大大简化了我们的逻辑,只用关心数据流的变化,同时减少了代码量,使得后期维护也更加方便.这个特性则要归功于setState()方法.React中利用队列机制来管理state,避免了很多重复的View刷新.下面我们来从源码角度探寻下setState机制. 1 还是先声明一个组件,从最开始一步步来寻源: class App extends Comp

  • React 源码调试方式

    目录 正文 断点调试 搜索定位 Chrome Devtools 调试 sourcemap npm 下载react包 插件注释 调试 React 最初源码 关联 react 源码项目 总结 正文 什么?调试 React 源码还有优雅和不优雅之分? 别着急,我们先来听个故事: 东东是一名前端工程师,主要用 React 技术栈,用了多年之后想深入一下,所以最近开始看 React 源码. 断点调试 他把 react 和 react-dom 包下载了下来,在项目里引入,开发服务跑起来后,打开 Chrome

  • React Refs 的使用forwardRef 源码示例解析

    目录 三种使用方式 1. String Refs 2. 回调 Refs 3. createRef 两种使用目的 Refs 转发 createRef 源码 forwardRef 源码 三种使用方式 React 提供了 Refs,帮助我们访问 DOM 节点或在 render 方法中创建的 React 元素. React 提供了三种使用 Ref 的方式: 1. String Refs class App extends React.Component { constructor(props) { su

  • React实现合成事件的源码分析

    目录 事件绑定 事件触发 结尾 今天尝试学习 React 事件的源码实现. React 版本为 18.2.0 React 中的事件,是对原生事件的封装,叫做合成事件.抽象出一层合成事件,是为了做兼容,抹平不同浏览器之间的差异. 下面会从两个方面进行源码的解读: 事件绑定 事件触发 事件绑定 首先是 React 项目过程启动时,调用 listenToAllSupportedEvents 方法,做合成事件的绑定. // 对应方法 `ReactDOM.createRoot() function cre

随机推荐