最近在看 React, 发现一篇深度好文, 忍不住就翻译了.

React 是一个用于构建用户界面的库, 它的核心是跟踪组件状态变化并将它们更新到页面上. 在 React 中, 我们称这个过程为 reconciliation. 当调用 setState 方法的时候, react 会检查 state 和 props 是否发生了变化, 并重新渲染组件.

本文中, 我们将会深入理解一些重要的概念和与算法相关的数据结构.

背景知识

下面是一个简单的应用, 页面上有一个按钮, 每次点击都会增加数字并渲染在页面上.

下面是代码:

class ClickCounter extends React.Component {constructor(props) {super(props)this.state = {count: 0}this.handleClick = this.handleClick.bind(this)}handleClick() {this.setState(() => {return {count: state.count + 1}})}render() {return [<button key="1" onClick={this.handleClick}>Update counter</button>,<span key="2">{this.state.count}</span>]}
}

上面是一个简单的组件, 在 render 方法里返回两个子元素 buttonspan, 点击按钮的时候, 组件的 state 会被内部的 handleClick 方法更新.

React 在执行 reconciliation 的时候会有很多不同的活动, 拿上面的例子来说, 第一次渲染和状态更新的时候会执行以下操作:

  • 更新 ClickCounter 的 state 中的 count 属性
  • 检索和比较 ClickCounter 的子元素和他们的 props
  • 更新 span 元素的props

在 reconciliation 期间还有很多其他的活动, 像调用生命周期函数, 更新 refs. 在 Fiber 的架构中, 这些活动都统一的称为 "work". work 的类型通常根据 React element 的类型决定. 例如, 对于类组件会创建实例, 但却不会对函数组件作同样的事. 在 React 中有非常多种类的元素, class components, function components, host components(DOM nodes), portal 等等. React element 的类型一般在 createElement 函数的第一个参数定义. 这个函数用来创造一个元素,通常在 render 方法中调用.

从 React Element 到 Fiber nodes

React 中的每个组件都代表了一个 UI. 例如下面的 ClickCounter 组件所展示的模板.

<button key="1" onClick={this.onClick}>Update counter</button>
<span key="2">{this.state.count}</span>

React Elements

当模板经过 JSX 编译后, 我们会得到一堆的 React elements, 他们是真正从 render 方法里返回的东西, 不是HTML. 因为我们不要求使用 JSX, 所以 ClickCounter 组件 render 方法里的返回可以被重写为:

 class ClickCounter {...render() {return [React.createElement('button',{key: '1',onClick: this.onClick},'Update counter'),React.createElement('span',{key: '2'},this.state.count)]}
}

在 render 方法里调用的 React.createElement 方法会创建两个像下面的数据结构:

[{$$typeof: Symbol(react.element),type: 'button',key: "1",props: {children: 'Update counter',onClick: () => { ... }}},{$$typeof: Symbol(react.element),type: 'span',key: "2",props: {children: 0}}
]

在这个对象中 React 添加了一个 $$typeof 属性作为当前 React element 的唯一标识. 而 type, key, props, 则描述了当前 element 的样子, 他们的值则是在调用 React.createElement 时传入的. 要注意当 span 和 button 节点的子元素是文本时 React 是怎样处理的, 以及点击回调如何作为 button 元素的 props 中的一部分.

Fiber nodes

在 reconciliation 期间, 每个从 render 方法中返回的 React element 数据都会被合并进 fiber 节点树, 每个 React element 都会关联到一个 fiber node. fiber node 是一种包含组件状态和 DOM 的可变数据结构, 不会像 React element 一样每次渲染都会重新创建.

前面提到过, 不同类型的 React element 会执行不同的活动. 在 ClickCounter 组件里会调用生命周期函数和 render 方法, 对于 span 元素对应的类型标记为 host component (DOM node) ,会执行 DOM 的修改. 所以, React element 会根据相应类型转化为 fiber 节点用来描述需要完成的工作.

你可以把 fiber当做是代表某些工作要做的数据结构, 一种工作单元. Fiber 架构提供了一种便捷的方式去跟踪, 调度, 停止和中断工作.

当 React element 第一次转换为 fiber node,React 在 createFiberFromTypeAndProps 函数中使用元素的数据去创建一个 fiber. 在随后的更新中, React 会重用这 fiber, 根据相对应的 React element 数据更新必要的属性. React 会根据 key 属性在层级结构中移动会删除这个节点当 render 方法中不再返回 React element 的时候.

源码的 ChildReconciler 函数中包含了对所有已存在的 fiber 节点可以执行的活动和对应函数的列表.

因为 React elements 是树状的, 所以 fiber nodes 也会被构建为相对应的树. 下面是 ClickCounter 的 fiber nodes 树结构.

所有的 fiber node 都会用 child, sibling, return 三个属性构建一个链表连接在一起.

Current & work in progress trees

第一次渲染之后, React 会得到一个 fiber 树, 它映射着程序的状态, 并渲染到界面上. 这个树被称为 current. 当 React 开始更新, 会重新构建一棵树, 称为 workInProgress, 所有的状态更新都会新被应用到这棵树上,完成之后刷新到界面上.

所有的 "work" 都是在 workInProgress 树上进行的. 当 React 开始遍历 current 树, 会对每个 fiber 节点创建一个备份 (alternate) 来构成 workInProgress 树. 当所有的更新和相关的 "work" 完成, 这个备份树就会被刷新到界面上, workInProgress 树就会变为 current 树.

React 的核心原则是一致性. 它总是一次性更新 DOM,不会每步都显示效果. workInProgress 就是一个用户不可见的 "草稿", React 在它上面处理所有组件, 处理完成后将它的更改再刷新到界面上.

在源码里, 你会看见非常多的函数将 current 和 workInProgress 作为参数, 下面就是这样的函数:

function updateHostComponent(current, workInProgress, renderExpirationTime) {...}

每一个 fiber 节点的 alternate 字段都持有其它树相对应它的节点引用. current 的节点会指向 workInrogress 的节点, 反之亦然.

在 React 中我们可以把组件当做是一个根据 state 和 props 来计算 UI 的函数. 每个像修改 DOM, 调用生命周期函数都被认为以 side-effect 或一种简单的影响. effects 在这篇文档里也被提及了.

你之前可能在 React 组件中已经通过数据抓取, 订阅或手动修改 DOM. 我们把这些操作称为 "side effects" (或者简称为 "effects") 因为它们会影响其他的组件而且在渲染阶段无法完成.

大多数的 state 和 props 更新都会导致 side-effects. 应用 effects 就是某种类型的 work, fiber node 是种的方便机制, 可以跟踪除了更新之外的 effects. 每个 fiber node 都有与之相关联的 effects. 它们被编码在 effectTag 字段里. 因此, Fiber 中的 effects 基本上定义了处理更新后需要为实例完成的工作. 对于 host component (DOM) 元素, work 包括 添加, 更新和删除元素. 对于类组件, 则可能需要更新 ref , 调用 componentDidUpdate 和 componentDidMount 生命周期方法, 还有其它类型相对应的它 effects.

Effects list

React 处理更新非常的快, 为了达到更好的性能使用了一些非常有趣的技术. 其中几个是将带有 effects 的 fiber 节点构建为线性表可以快速的迭代. 迭代线性表远远快于树而且不需要在没有 side-effects 的 fiber 节点上浪费时间.

这个链表的目的是标记出具有 DOM 更新或其它与之关联的 effects, 是 finishedWork 树的子集. 在 current 和 workInProgress 树中使用 nextEffect 属性连接在一起.

Dan Abramov 为此提供了一个形象的比喻. 他喜欢将它想象成一棵圣诞树, 通过"圣诞灯"将所有的有效节点绑定在一起. 让我们想象下面的 fiber nodes, 其中橙色的部分表示有工作要做. c2将会被插入到 DOM 中, d2和 c1 将会修改属性, b2 会触发生命周期函数. effect 链表会将它们连接在一起以便 React 可以跳过其他的节点.

我们可以清楚的看到带有 effects 的节点时如何连接在一起的. 当遍历这些节点时, React 用 firstEffect 指针指出链表从哪里开始, 所以上图可以像下面这样显示:

React 按照从子元素到父元素的顺序处理这些效果.

Root of the fiber tree

每个 React 应用都有一个或多个 DOM 元素充当容器, 在我们的例子中, 是带有 id container 的 div元素.

const domContainer = document.querySelector('#container');
ReactDOM.render(React.createElement(ClickCounter), domContainer);

React 为每一个容器都创建了 fiber root 对象, 可以通过 DOM 元素的引用访问到它.

const fiberRoot = query('#container')._reactRootContainer._internalRoot

fiber root 对象是 React 保存 fiber 树引用的地方. 它被储存在 fiber root 对象的 current 属性里.

const hostRootFiberNode = fiberRoot.current

fiber 树中的第一个节点有特殊的类型, 叫做HostRoot(容器元素).它在内部创建并作为顶部组件的父级.在 HostRoot 中可以通过 stateNode 属性访问到 FiberRoot.

fiberRoot.current.stateNode === fiberRoot; // true

你可以通过 HostRoot 探索到整个 fiber 树, 也可以通过组件实例获得单个 fiber 节点

compInstance._reactInternalFiber

Fiber node structure

让我们看下 ClickCounter 组件的 fiber 节点结构

{stateNode: new ClickCounter,type: ClickCounter,alternate: null,key: null,updateQueue: null,memoizedState: {count: 0},pendingProps: {},memoizedProps: {},tag: 1,effectTag: 0,nextEffect: null
}

span DOM 元素的:

{stateNode: new HTMLSpanElement,type: 'span',alternate: null,key:'2',updateQueue: null,memoizedState: null,pendingProps: {children: 0},memoizedProps: {children: 0},tag: 5,effectTag: 0,nextEffect: null
}

fiber node 上有很多的字段, alternate, effectTag, nextEffect 前面已经解释过了, 剩下的将在下面阐释.

stateNode

持有对组件类实例的引用, DOM 节点或其他与 fiber 节点相关联的 React element 类型.一般来说, 我们可以说这个属性被用于保存与当前 fiber 相关的本地状态.

type

定义相关联的 fiber 类型, 函数或是类. 对于 class components, 它指向构造函数, 对于 DOM 元素则指定为 HTML 标记. 我经常用这个字段来判断这个 fiber 节点与哪个元素相关.

tag

定义 fiber 的类型. 它在 reconciliation 算法中用来确定需要完成的工作. 像前面提到的, work 的种类取决于 React element 的类型. createFiberFromTypeAndProps 函数将React element 映射到f相关的 fiber 节点类型. 在我们的应用中, ClickComponent 组件的 tag 属性是 1 标明为是个 ClassComponent. span 元素是 5 标明它是 HostComponent.

updateQueue

state 更新, 回调和 DOM 更新的队列.

memoizedState

用于创建输出的 fiber state. 在处理更新的时候, 他映射的是当前界面上呈现的state.(更新时,存储的之前的state)

memoizedProps

上一次渲染过程中用来创建的输出的 fiber props.

pendingProps

已从 React elements 的新数据中更新的 props并且需要应用于子组件和 DOM 元素.

key

在一组子元素中的唯一标识, 帮助 React 指出哪些已被更改, 添加或删除.

***更详细的可以去看源码里的注释文件***

算法概览

React 执行 work 的时候分两个阶段: rendercommit.

在第一个 render 阶段, React 将更新应用于通过 setState 和 React.render 调度的组件, 指出在 UI 上需要更新什么. 第一次初始化渲染, React 会通过 render 方法为每一个元素都创建一个新的 fiber.在随后的更新中, 已存在的 fiber 会被重用和更新. 这个阶段会构建一个被 side-effects 标记的 fiber 节点树. effects 描述了在随后的 commit 阶段需要完成的工作.这个阶段带有 effects 的节点都会被应用到它们的实例上, 然后遍历 effects 链表执行 DOM 更新并在界面上呈现.

重要的一点是, 渲染阶段的 work 可以异步执行.React 根据可用时间处理一个或多个 fiber 节点, 当某些重要的事件发生时, 就停下来处理这些事件, 处理完成后再回来继续. 有时候它会丢弃已经完成的工作, 并从顶部重新开始.因为在此阶段对用户是不可见的, 所以才使得暂停才变成可能. 随后的 commit 阶段是同步的, 它会产生用户可见的变化, 例如 DOM 的修改. 这就是 React 需要一次性完成它们的原因.

调用生命周期函数是 React 执行的工作之一. 一些在 render 阶段调用, 一些在 commit 阶段调用.

在 render 阶段执行的方法:

  • [UNSAFE_]componentWillMount ( 弃用 )
  • [UNSAFE_]componentWillReceiveProps ( 弃用 )
  • getDerivedStateFromProps
  • shouldComponentUpdate
  • [UNSAFE_]componentWillUpdate ( 弃用 )
  • render

上面有一些 render 阶段的方法在 16.3 中被标记为 UNSAFE, 它们也许会在将来的某个 16.x 中被弃用. 你可以在这里了解到更多.

之所以被标记为 UNSAFE, 是因为开发者经常误用, 在那些方法里执行产生 side-effect 的函数, 可能会导致新的异步渲染方法出现问题.

commit 阶段执行的方法:

  • getSnapshotBeforeUpdate
  • componentDidMount
  • componentDidUpdate
  • componentWillUnmount

Render 阶段

reconciliation 算法总是使用 renderRoot 方法从最顶端的 HostRoot 节点开始, 跳过已经处理过的节点直到带有未完成 work 的节点. 例如, 当在组件树深处调用 setState 方法的时候, React 会从顶部开始快速的跳过所有父级节点直接到调用 setState 方法的节点.

work loop 的主要步骤

所有的 fiber 节点都会在 workLoop 方法中被处理, 下面是代码的同步实现部分:

function workLoop(isYield){if(!isYield){while(nextUnitOfWork !== null){nextUnitOfWork = performUnitOfWork(nextUnitOfWork)}} else {...}
} 

在上面的代码中. nextUnitOfWork 保存了 workInProgress 树中一个有工作要处理的 fiber 节点. 当 React 遍历所有节点时, 这个变量用来判断是否尤其它的带有未完成工作的 fiber 节点. 当前节点处理完成之后, 保存的就是下一个节点或 null. 这种情况下, React 退出 work loop 并准备到 commit 阶段.

下面是四个用来初始化和完成工作的主要方法:

  • performUnitOfWork
  • beginWork
  • completeUnitOfWork
  • completeWork

下面的动画更直观的展示了处理过程. React 每次都是处理完字 fiber 节点再到 父 fiber 节点.

垂直线表示兄弟节点, 弯曲线表示子节点.

这里是视频, 可以暂停仔细看看. 概念上来讲, 可以把 'begin' 理解为走入一个组件, 'complete' 为走出一个组件.

下面是一个简单实现的示例:

function performUnitOfWork(workInProgress) {let next = beginWork(workInProgress);if (next === null) {next = completeUnitOfWork(workInProgress);}return next;
}function beginWork(workInProgress) {console.log('work performed for ' + workInProgress.name);return workInProgress.child;
}

performUnitOfWork 接收一个来自 workInProgress 树的 fiber 节点然后调用 beginWork 开始处理它的 work. 为了方便展示, 只是简单的打印了已完成工作 fiber 节点的 name 属性. beginWork 函数总是返回下一个子节点的指针或者是 null.

如果存在下一个子节点, 它会被分配到 nextUnitOfWork 变量. 如果没有节点, React就会知道已经到了分支的结尾, 就会完成当前节点的工作. 工作完成, React 会执行它兄弟节点的工作再回溯到它的父节点. 下面是 completeUnitOfWork 函数:

function completeUnitOfWork(workInProgress) {while (true) {let returnFiber = workInProgress.return;let siblingFiber = workInProgress.sibling;nextUnitOfWork = completeWork(workInProgress);if (siblingFiber !== null) {// If there is a sibling, return it// to perform work for this siblingreturn siblingFiber;} else if (returnFiber !== null) {// If there's no more work in this returnFiber,// continue the loop to complete the parent.workInProgress = returnFiber;continue;} else {// We've reached the root.return null;}}
}function completeWork(workInProgress) {console.log('work completed for ' + workInProgress.name);return n

只有处理完子节点所有分支之后, 才会回溯到父节点. 完整示例地址

Commit 阶段

这个阶段从 completeRoot 函数开始. 这里是 React 更新 DOM, 调用前后异变生命周期函数的地方.

到了这个阶段, 存在着两个 fiber 树和 effect 链表. 一棵树代表着当前渲染在界面上的状态(current). 另一个代表着将来要映射到界面上的状态(workInProgress). 两个树有着同样的数据结构.

还有 effects 链表 ----- 一个通过 nextEffect 引用的 workInProgress 树的节点子集. effects 链表使运行 render 阶段的结果. 整个渲染就是确定哪些节点需要插入, 更新或者删除, 哪些组件需要调用生命周期函数.

For debugging purposes, the current tree can be accessed through the currentproperty of the fiber root. The finishedWork tree can be accessed through the alternate property of the HostFiber node in the current tree.

commit 阶段主要的函数是 commitRoot, 基本上执行以下操作:

  • 在有 Snapshot 效果标记的节点上调用 getSnapshotBeforeUpdate 生命周期方法
  • 在有 Deletion 效果标记的节点上调用 componentWillUnmount 生命周期方法
  • 执行所有的 DOM 插入, 更新, 删除
  • finishedWork 设置为 current 树
  • 在有 Placement 效果标记的节点上调用 componentDidMount 生命周期方法
  • 在有 Update 效果标记的节点上调用 componentDidUpdate 生命周期方法

调用 getSnapshotBeforeUpdate 方法后, React commit 树内的所有 side-effects. 它分为两步. 第一步执行所有的 DOM 插入,更新,删除和 ref 卸载. 然后 React 将 finishedWork 树赋值给 FiberRoot , workInProgress 树标记为 current 树. 第二步 React 调用其他的生命周期函数和 ref 回调. 这写方法都会单独执行, 因此已经执行了整个树中的所有插入, 更新和删除.

function commitRoot(root, finishedWork) {commitBeforeMutationLifecycles()commitAllHostEffects();root.current = finishedWork;commitAllLifeCycles();
}

Pre-mutation lifecycle methods

代码遍历 effects 树, 检查节点上是否有 Snapshot effect 标记

function commitBeforeMutationLifecycles() {while (nextEffect !== null) {const effectTag = nextEffect.effectTag;if (effectTag & Snapshot) {const current = nextEffect.alternate;commitBeforeMutationLifeCycles(current, nextEffect);}nextEffect = nextEffect.nextEffect;}
}

对于 class 组件来说, 就是调用 getSnapshotBeforeUpdate

DOM updates

commitAllHostEffects 函数是 React 执行 DOM 更新的地方

function commitAllHostEffects() {switch (primaryEffectTag) {case Placement: {commitPlacement(nextEffect);...}case PlacementAndUpdate: {commitPlacement(nextEffect);commitWork(current, nextEffect);...}case Update: {commitWork(current, nextEffect);...}case Deletion: {commitDeletion(nextEffect);...}}
}

React 会在 commitDeletion 中调用 componentWillUnmount 方法

Post-mutation lifecycle methods

commitAllLifecycles 函数会调用剩下的生命周期函数 componentDidUpdatecomponentDidMount.

原文地址: https://medium.com/react-in-depth/inside-fiber-in-depth-overview-of-the-new-reconciliation-algorithm-in-react-e1c04700ef6e

react 返回一个页面_Fiber 内部: 深入理解 React 的新 reconciliation 算法相关推荐

  1. react 返回一个页面_react-navigation goBack返回指定页面

    重新看了下文档 屏幕快照 2018-09-12 下午2.33.39.png 所以只需要this.props.navigation.navigate('xxx')这样就可以了,下边方法没用了 1.问题 ...

  2. react 返回一个页面_react项目中实现返回不刷新

    说明 打开京东手机web版,细心的你会发现,当你从首页list页跳到详情页在返回(不论你点的是app的返回还是安卓返回健)的时候列表页位置还是静静的在哪里, 在我们的react项目里如何实现呢?首先我 ...

  3. react 返回一个页面_react-router-dom 怎么让第二个页面返回到第一个页面使得第一个页面不重新加载...

    RT 用了 react-router-dom v4 从列表页( List )点击一项进入详情页( Detail ),在 Detail 里点击返回,返回到 List,这里 List 会重新加载,也就是说 ...

  4. js请求返回一个页面html页面跳转页面,JS cookie操作 解决页面跳转返回

    在HTML中,页面跳转后,js和HTML都会刷新,这样数据就会初始化,如果要解决页面跳转互传数据,可以考虑cookie. 对于页面跳转,然后页面返回等一系列操作,cookie中数据模拟堆栈出栈可以实现 ...

  5. # js如何返回上一个页面

    HTML通过内置js如何返回上一个页面 HTML通过内置js如何返回上一个页面 这里列举三个js返回上一页的方法 1.通过 window.history.back()方法返回: 例如: <but ...

  6. div内嵌网页ajax,Div里面载入另一个页面的实现(取代框架)(AJax)(转)

    随着框架越来越不火了,HTML5就不对框架支持了,iframe也只有url了,Div就担当了此大任 DIV+CSS在页面部局确实也很让人满意,使用也更方便 今天突然遇到一个问题,那就是需要导入另一个页 ...

  7. 输入一个url到呈现一个页面,到底发生了啥?

    1. 输入阶段 输入第一个字符开始,浏览器会检索历史记录,显示匹配的地址(如果有的话) 输入完毕之后,浏览器会根据URL的规则判断输入的内容是搜索内容还是URL,如果是搜索内容,则回车之后通过默认的搜 ...

  8. 解决springmvc在单纯返回一个字符串对象时所出现的乱码情况(极速版)

    使用springmvc框架开发了这么长时间,之前都是直接返回jsp页面,乱码情况都是通过配置和手动编解码来解决,但是今天突然返回一段单纯的字符串时,发现中文乱码情况解决不了了,下面就给各位分享一下如何 ...

  9. 条款23: 必须返回一个对象时不要试图返回一个引用

    据说爱因斯坦曾提出过这样的建议:尽可能地让事情简单,但不要过于简单.在C++语言中相似的说法应该是:尽可能地使程序高效,但不要过于高效. 一旦程序员抓住了"传值"在效率上的把柄(参 ...

最新文章

  1. 「Jenkins+Git+Maven+Shell+Tomcat持续集成」经典教程
  2. Android中文API (110) —— CursorTreeAdapter
  3. 拆解一个舵机组成的机器人
  4. dede扩展数据类型_数据类型,扩展
  5. java 小波去噪原理_小波去噪的基本知识
  6. javaScript基本功001
  7. 如何用 Redis 实现延迟队列?
  8. 托管数据中心之间的PUE比较(下)
  9. nfs文件服务器以及客户端基本配置
  10. 对某机构为“转移内部矛盾”而嫁祸于我们的事件之真相大起底
  11. Java面向对象的三大特征(封装,继承,多态)
  12. 多线程设计模式(二):Future模式
  13. LGOJP1941 飞扬的小鸟
  14. 正确的座机号码格式_简历里的手机号及座机号的标准写法是什么?正确书写才更可能求职成功!...
  15. 各银行支付/各种支付平台/php对接支付接口心得/php h5支付接口对接
  16. 微软Kinect是怎么做到的
  17. python大小写转换_Python字母大小写的转换(两种方法)
  18. 20. Learning to Perturb Word Embeddings for Out-of-distribution QA 阅读笔记
  19. 数字孪生|成熟度评价
  20. 无线覆盖范围 测试软件,无线覆盖验收标准

热门文章

  1. 找出1个小时前更新的文件并进行拷贝
  2. 【基础概念】 Redis简介和面试常见问题
  3. JS进阶篇--ckplayer.js视频播放插件
  4. 编写音乐播放器的一些感想
  5. IOS开发----生成静态库(.a)
  6. 【转】矮个子女生夏天穿衣法则
  7. OAuth 2 实现单点登录,通俗易懂!
  8. 通俗理解 Kubernetes 中的服务,搞懂后真“有趣”
  9. 很遗憾,我们正在逐渐丧失专注阅读的能力
  10. 某程序员求助:喜欢上漂亮的产品经理却不敢追,追不上太尴尬,公司也不允许办公室恋情!网友:别怂!...