IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> JavaScript知识库 -> 浅谈react——状态更新之后发生了什么 -> 正文阅读

[JavaScript知识库]浅谈react——状态更新之后发生了什么

学习react底层的过程,和学习其他原理一样,抓住一些关健的点,也就是关键的函数(往往代表了一些阶段),可以对源码的把握以及 图像化的流程更加清晰和易懂。

?

我们知道facebook团队在react16之后就对react底层有一些重大的重构,一句大白话来解释就是,让react可以实现异步可中断的更新。

?至于怎么实现的,是react引入了Scheduler调度器,会分配给js线程一个初始的执行时间,源码里面yieldInterval=5ms,如果预留的时间不够浏览器渲染的话,那么react就会将控制权交给浏览器,等到下一帧再进行渲染。具体原理本篇不细讲。

但是在更新过程不是所有阶段都是异步的,这会造成一些预料之外的

回到状态更新上来,可以改变状态大概有这些:

  • ReactDOM.render

  • this.setState

  • this.forceUpdate

  • useState

  • useReducer

很显然,react开发着肯定会想到接入一套状态更新体制当中,怎么实现呢?答案就是,每次创建一个保存状态更新的对象,在render阶段根据该对象来计算新的state。然而在render阶段,是从rootFiber开始遍历,产生update对象的可能是fiber树中的一个节点,所有得有一种方式能够让react从节点回到顶点。这个关键的过程就是markUpdateLaneFromFiberToRoot函数。

function markUpdateLaneFromFiberToRoot(
  sourceFiber: Fiber,
  lane: Lane,
): FiberRoot | null {
  // Update the source fiber's lanes
  sourceFiber.lanes = mergeLanes(sourceFiber.lanes, lane);
  let alternate = sourceFiber.alternate;
  if (alternate !== null) {
    alternate.lanes = mergeLanes(alternate.lanes, lane);
  }
    let node = sourceFiber;
  let parent = sourceFiber.return;
  while (parent !== null) {
    parent.childLanes = mergeLanes(parent.childLanes, lane);
    alternate = parent.alternate;
    if (alternate !== null) {
      alternate.childLanes = mergeLanes(alternate.childLanes, lane);
    }
    }
    node = parent;
    parent = parent.return;
  }
  if (node.tag === HostRoot) {
    const root: FiberRoot = node.stateNode;
    return root;
  } else {
    return null;
  }
}

这就是源码,可以看出它不仅接收一个fiber还接收一个优先级,这也对应着update不仅仅有一维的状态保存,还有优先级。逻辑上来看,就是不断通过return指针来往上一层查找,直到找到最上层。

现在拥有了rootfiber立即更新的话肯定有个问题,那就是更新都是同步的,但是react是异步的,那就得调度更新了。核心的函数就是ensureRootIsScheduled。

核心代码如下:

if (newCallbackPriority === SyncLanePriority) {
  //同步高优先级任务
  newCallbackNode = scheduleSyncCallback(
  //同步执行render阶段
    performSyncWorkOnRoot.bind(null, root)
  );
} else {
  // 异步低优先级任务
  var schedulerPriorityLevel = lanePriorityToSchedulerPriority(
    newCallbackPriority
  );
  newCallbackNode = scheduleCallback(
    schedulerPriorityLevel,
   异步执行render阶段
    performConcurrentWorkOnRoot.bind(null, root)
  );
}

?这样就让render阶段成为一个既可以异步也可以同步也有优先级的了,接下来可以光明正大的更新了。?

Render阶段? ? ??

注意,render阶段的开始于performSyncWorkOnRoot或performConcurrentWorkOnRoot方法的调用。

// performSyncWorkOnRoot会调用该方法--同步更新
function workLoopSync() {
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

// performConcurrentWorkOnRoot会调用该方法--异步更新
function workLoopConcurrent() {
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}

?注意到异步更新里面有一个关键的函数shouldYield(),这个函数会监控浏览器当前帧是否有剩余时间,没有的话就是终止更新,等到下一帧继续执行。

workInProgress代表正在构建的fiber树,而performUnitOfWork会创建下一个fiber节点并连接上之前的节点,形成fiber树。

在整个render阶段,大概也可以分为两个重要的时期,对应fiber被遍历两遍的过程。第一个阶段就是beginWork()从rootFiber节点开始,向下进行遍历。当遍历到叶子节点时,就会执行completeWork()函数,从叶子节点一步一步到rootfiber。

下面来详细说一下beginWork,这个函数作用很简单,就是根据传入的fiber节点,创建子fiber节点以及通过对比新旧节点给节点打上一些effectTag标签,给commit阶段处理。总体上时分为两个阶段:

1.mount时,因为此时的current树还是空的,所有值会根据不同的fiber.tag来生成对应的fiber节点。

 switch (workInProgress.tag) {
    case IndeterminateComponent: 
      // ...省略
    case LazyComponent: 
      // ...省略
    case FunctionComponent: 
      // ...省略
    case ClassComponent: 
      // ...省略
    case HostRoot:
      // ...省略
     .
     .
     .
}

2.update时情况就会比较复杂,会在这个过程这个过程中通过调用reconcileChildren方法来进行diff(不深究),尝试最大可能的复用当前节点。?

//reconcileChildren部分逻辑
if (current === null) {
    // 对于mount的组件
    workInProgress.child = mountChildFibers(
      workInProgress,
      null,
      nextChildren,
      renderLanes,
    );
  } else {
    // 对于update的组件
    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,
      nextChildren,
      renderLanes,
    );
  }

实际上,mount和update时都会调用reconcileChildren。但是为什么要调用两个不同的函数呢?

因为mount阶段fiber节点都是首次创建,如果都打上effectTag标签的话,会影响性能。因此,mount的时候,mountchildFibers只会给rootFiber节点打上一个placement的标记,一次性加入。

接下来就是completeWork(current,workInprogress)阶段,这个阶段的作用总结来说就是,mount时

?根据fiber创建DOM节点,并且初始化DOM节点的props。update阶段就是处理diff之后的收尾工作,依据打完effectTag标签的fiber,形成一个需要更新的props组成的一个数组。

mount阶段和update阶段重要的函数:

const currentHostContext = getHostContext();
// 为fiber创建对应DOM节点
const instance = createInstance(
    type,
    newProps,
    rootContainerInstance,
    currentHostContext,
    workInProgress,
  );

// 将子孙DOM节点插入刚生成的DOM节点中
appendAllChildren(instance, workInProgress, false, false);

// DOM节点赋值给fiber.stateNode
workInProgress.stateNode = instance;

mount阶段也会初始化DOM节点上的props。

 // update的情况
  updateHostComponent(
    current,
    workInProgress,
    type,
    newProps,
    rootContainerInstance,
  );
//在updateHostComponent内部
workInProgress.updateQueue = (updatePayload: any);

其中updatePayload就是需要更新的props数组。

最后在completeWork阶段还有一个小细节,为了让commit阶段更加快速的操作DOM,在completeWork这个回宿阶段会格外的生成一个effectList数组,里面包含的是下一阶段需要更新的DOM。

至此,render阶段才算完成,这个过程可能不是一帆风顺的,会由于高优先级以及浏览器的渲染时间而终止以及恢复。现在currentFiber还没有变化,真正改变DOM的就是下一阶段,也是同步的,commit阶段。

commit的入口函数是commitRoot(root)

这个阶段大致分为三个阶段:

before mutation阶段----mutation阶段----layout阶段

1.befor mutation阶段。

主要就是遍历effectList,以及调用主函数commitBeforeMutationEffects,这个函数里面会调用getSnapshotBeforeUpdate这个钩子开始,这也看出来是在同步阶段执行,并且在componentDidMount之前执行。其次就是调度useEffect,这是个重头戏。

// 调度useEffect
if ((effectTag & Passive) !== NoEffect) {
  if (!rootDoesHavePassiveEffects) {
    rootDoesHavePassiveEffects = true;
    scheduleCallback(NormalSchedulerPriority, () => {
      // 触发useEffect
      flushPassiveEffects();
      return null;
    });
  }
}

?这里额外聊一下,useEffect的调度过程,useEffect在这个阶段是通过flushPassiveEffects调度useEffect,但是不会立马执行,因为这是一个异步的过程,flushPassiveEffects依赖的参数rootWithPendingPassiveEffects此时为null,只有等到layout阶段,rootWithPendingPassiveEffects才会被赋值为effectList,执行回调,这也是useEffect异步回调的原理。

而且因为这个原因,useEffect的执行要比?componentDidMount、componentDidUpdate 执行要靠后,而且视图已经更新,不会阻塞页面渲染。

2.mutation阶段。

真正执行DOM操作的过程。这个阶段也是遍历effectList,主函数是commitMutationEffects。接收一个fiberRoot和更新级作为参数。

function commitMutationEffects(root: FiberRoot, renderPriorityLevel) {
  // 遍历effectList
  while (nextEffect !== null) {

    const effectTag = nextEffect.effectTag;
    .
    .
    .
}
  • 充值文字节点,在react里面文字节点是被单独分开处理的
  • 更新ref
  • 根据不同的effectTag执行不同的操作,包括Placement,update,deletion,以及Hydrating。

?下面来聊聊具体的effectTag,Placement tag调用了commitPlacement函数,获取父fiber节点,再进行insertBefore或着appendChild的DOM操作插入DOM节点。而当fiber中包含Update effectTag会调用commitUpdate,执行函数组件的useLayoutEffect的销毁函数,对于原生DOM组件,会将在completeWork阶段附着在fiber节点上的updateQuene对应的内容渲染到页面上。至于Deletion effectTag,就是删除fiber节点以及解绑ref,执行compoentWillUnmount。并且会执行useEffect的销毁函数。

3.layout阶段。

和之前的阶段一样,这个阶段也是会遍历effectList。这个阶段的DOM结构已经更新渲染完成。这个阶段的主函数就是commitLayoutEffects。主要的逻辑代码如下:

function commitLayoutEffects(root: FiberRoot, committedLanes: Lanes) {
  while (nextEffect !== null) {
    const effectTag = nextEffect.effectTag;

    // 调用生命周期钩子和hook
    if (effectTag & (Update | Callback)) {
      const current = nextEffect.alternate;
      commitLayoutEffectOnFiber(root, current, nextEffect, committedLanes);
    }

    // 赋值ref
    if (effectTag & Ref) {
      commitAttachRef(nextEffect);
    }

    nextEffect = nextEffect.nextEffect;
  }
}

?1.commitLayoutEffectOnFiber(之前的版本叫commitLifeCycles),对于类组件,会根据current==null来判断执行componentDidmount以及componentDidupdate函数。而且此时setState的第二个参数回调函数会在此执行。对于函数组件,他会调用useLayoutEffect的回调函数以及调度useEffect的销毁和回调函数。注意一点,此时useEffect并没有执行,等到layout阶段之后再执行。

2.commitAttachRef,这个函数做的事情比较简单,获取实例DOM实例,然后再更新ref,如果ref是个回到函数,也是会执行,是对象实例则直接赋值。

至此整个流程差不多结束,但是内存里面的两颗fiber树是啥时候切换的呢?

答案是:在mutation和layout阶段之间。

总结一下从状态更新,到页面显示的全过程:

  • 调用不同的方法状态更新
  • 创建Update对象
  • 调用markUpdateLaneFromFiberToRoot回归到root节点
  • 调用ensureRootIsScheduled,进行调度更新
  • render阶段
  • commit阶段

这只是从底层原理和流程的角度去看整个更新过程,react在这个给过程中也有许多的地方值得深入学习,比如采用位运算以及位标识,用一个数组的奇偶数来保存相反的回调函数...不过应该也算是讲明白了一些流程。学习路上进无止境,一起加油!

如果有不对的地方,烦请读者指出,共同进步!!!

  JavaScript知识库 最新文章
ES6的相关知识点
react 函数式组件 & react其他一些总结
Vue基础超详细
前端JS也可以连点成线(Vue中运用 AntVG6)
Vue事件处理的基本使用
Vue后台项目的记录 (一)
前后端分离vue跨域,devServer配置proxy代理
TypeScript
初识vuex
vue项目安装包指令收集
上一篇文章      下一篇文章      查看所有文章
加:2021-09-12 13:04:57  更:2021-09-12 13:05:13 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 17:07:28-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码