sl1673495 / daily-plan

34 stars 0 forks source link

2020-04-28日计划: Fiber、pipeline #17

Open sl1673495 opened 4 years ago

sl1673495 commented 4 years ago

https://indepth.dev/inside-fiber-in-depth-overview-of-the-new-reconciliation-algorithm-in-react/

对于这种 React.Element 节点,会在 render 阶段被合并成一个 Fiber 节点。

{
    $$typeof: Symbol(react.element),
    key: null,
    props: {},
    ref: null,
    type: ClickCounter
}

对于不同类型的节点,React 做的事情不同:

  1. 对于 Class Component 创建和更新的时候需要调用声明周期。
  2. 对于 DOM 节点则是调用原生方法。

所以需要做不同的标记

Fiber 是一个最小的工作单元,并且它的结构便于跟踪、调度、中断和恢复。

这里 可以看到 Fiber 的创建过程。

Fiber 结构图: image

sl1673495 commented 4 years ago

Current and work in progress trees

current 指向当前的 fiber 树 React 在开始更新的时候,会构建一颗 workInProgress 树。对于已经存在的 Fiber Node,React 会创建一个 alternate node(替身)放到 workInProgress 树上。

当更新完成后,workInProgress 就变成了 current。

React 有一个原则就是一致性,更新 DOM 的操作永远是一次性的。 workInProgress 可以理解为是一份对用户不可见的“草稿”。

每个 alternate node 都会保存一份对 current 树上自己对应的节点的引用,反之亦然。

sl1673495 commented 4 years ago

Effects list

React 为了使协调过程变得更快,采取了一些比较有趣的策略。比如他们构建了一个链表用来快速迭代带有 effect 的Fiber Node。

这把原本的迭代一棵树的问题转化成了迭代一个链表,快多了!

这个 副作用链表是 fiber树的子集,它记录了一些需要做DOM更新或者其他副作用的节点,用 nextEffect 属性指向下一个副作用节点。

Dan Abramov 给了一个好玩的比喻,副作用链表之于整个Fiber Tree,就像是圣诞树上挂了一些圣诞灯饰。

比如这个图上,我们的更新导致将c2插入DOM,d2和c1更改属性,b2触发生命周期方法。 副作用链表会将它们链接在一起,以便React可以跳过其他节点:

image

sl1673495 commented 4 years ago

Root of the fiber tree

DOM 容器的元素上可以访问到 Root Fiber

const domContainer = document.querySelector('#container');

ReactDOM.render(React.createElement(ClickCounter), domContainer);

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

const hostRootFiberNode = fiberRoot.current

// current 可以回头查找 fiberRoot
fiberRoot.current.stateNode === fiberRoot;

// 组件实例上也可以找到
compInstance._reactInternalFiber
sl1673495 commented 4 years ago

Fiber 的结构

ClickCounter 组件

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

span 元素

{
    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
}

stateNode

在类组件中,这是组件实例;在 DOM 元素的 Fiber 中,这是元素实例。

type

Fiber 相关的类或函数,如果是 DOM 元素的话,那就是 HTML 节点的字符串。

tag

这里 有定义,标识 Fiber 节点的类型,如 FunctionComponentHostComponent

updateQueue

状态更新、回调、DOM 更新的队列。

memoizedState

用来记录 Fiber 的状态,在更新时,它反映了当前应当渲染到屏幕上的状态。

memoizedProps

上一次渲染时的 props

pendingProps

本次更新后的 props

key

唯一标识符,用来让 React 知道这个节点和上一次渲染的时候是不是同一个,列表 diff 很重要。

child、sibling and return

是 Fiber 可中断、可重新开始,可回溯的关键结构属性。

expirationTime, childExpirationTime and mode

调度相关。

sl1673495 commented 4 years ago

https://indepth.dev/inside-fiber-in-depth-overview-of-the-new-reconciliation-algorithm-in-react/#general-algorithm

General algorithm

React 在两个主要阶段执行工作:render 和 commit。

render 阶段构建出一颗带有副作用的 Fiber 树,在 commit 阶段遍历副作用链表,更新 DOM 元素和其他副作用变更。

render 阶段是可以异步执行的,它可以在完成了一个或多个 Fiber 的工作后,把执行权交给优先级更高的事件,再恢复到暂停时的 Fiber,所以 Commit 阶段一定要和这个阶段分开来,因为你不可以把一部分状态绘制到屏幕上,不符合一致性原则。

commit 阶段一定是同步的,一口气执行完毕的。

render 阶段的生命周期:

  1. [UNSAFE_]componentWillMount (deprecated)
  2. [UNSAFE_]componentWillReceiveProps (deprecated)
  3. getDerivedStateFromProps
  4. shouldComponentUpdate
  5. [UNSAFE_]componentWillUpdate (deprecated)
  6. render

这些阶段不要去写副作用。

commit 阶段的生命周期:

  1. getSnapshotBeforeUpdate
  2. componentDidMount
  3. componentDidUpdate
  4. componentWillUnmount

这些阶段可以安全的操作 dom。

Render 阶段

reconciliation 算法始终最顶层的 HostRoot Fiber 节点开始。 但是,React 会从已处理的 Fiber 节点中跳过,直到找到工作未完成的节点为止。

例如,如果您在组件树的深处调用 setState,则 React 将从顶部开始,但会快速跳过父级,直到它到达调用了 setState 方法的组件。


## Commit阶段

```js
function commitRoot(root, finishedWork) {
    commitBeforeMutationLifecycles()
    // DOM 更新
    commitAllHostEffects();
    // current 指向当前树
    root.current = finishedWork;
    // componentDidUpdate 和 componentDidMount.
    commitAllLifeCycles();
}

这里的每一个 commit 子方法,都会遍历 effectList,根据对应的 tag 去调用对应的方法。

commitBeforeMutationLifecycles

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

对于类组件,这个 effect 意味着调用 getSnapshotBeforeUpdate 生命周期方法。

commitAllHostEffects

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 方法。

sl1673495 commented 4 years ago

https://github.com/tc39/proposal-pipeline-operator

未来的 pipeline 语法。

add(7, ?) 也是一个新的柯里化提案,等于提前为 add 方法固定一个参数,返回一个新的方法。

function add (x, y) {
  return x + y
}

const addSeven = add(7, ?) 

// 等价于
const addSeven = (arg) => add(7, arg)

再配合上新的 pipeline 提案,甚至可以轻松的组合「形参个数不同的函数」。

let person = { score: 25 };

let newScore = person.score
  |> double
  |> add(7, ?)
  |> boundScore(0, 100, ?);

newScore //=> 57