React源码学习-初始化及更新流程

准备工作

这篇文章本来准备作为一篇笔记,不准备发到博客中来。但简单写完后想起自己初次接触react源码时像无头苍蝇一样到处乱撞,中间绕了很大的圈子才渐渐找到感觉,这里把自己近期看到的好文章及心得总结出来,希望能让像我一样的react源码“初学者”少走一些弯路。

精品文章

  1. react-in-depth系列,这个是目前为止我看到的关于react 源码写的最好的几篇文章,值得反复阅读。原文需要翻墙才能打开,知乎里也有人翻译了这几篇文章。

    The how and why on React’s usage of linked list in Fiber to walk the component’s treeInside Fiber: in-depth overview of the new reconciliation algorithm in ReactIn-depth explanation of state and props update in React

  2. 知乎文章,暂时推荐一篇,也可以搜索其他文章看一看。

    深入剖析 React Concurrent

  3. Dan Abramov的博客,这个人不用说是谁了吧。

    Dan Abramov的博客How Does setState Know What to Do?,等等都值得反复读

  4. 其他。

    前端进阶之道,不错的一个入门解析

阅读本篇

阅读本篇文章之前,最好先阅读以上推荐文章,先对FiberExpirationTimeworkLoop等等概念有个大致的认识。

调试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
//html
<div id="root"></div>

//App组件
class App extends Component {
constructor(props) {
super(props);
this.state = {
count: 1,
};
}

handleClick = () => {
this.setState({
count: 2,
});
};

static getDerivedStateFromProps(props, state) {
return null;
}

getSnapshotBeforeUpdate(prevProps, prevState) {
console.log(prevState);
}

componentDidMount() {
console.log("this");
}

componentDidUpdate(prevProps, prevState, snapshot) {
console.log(prevState);
}

componentWillUnmount() {
console.log("haha");
}

render() {
return (
<div className="container">
<button key="1" onClick={this.handleClick}>
点击
</button>
<span key="2">{this.state.count}</span>
</div>
);
}
}

//渲染入口
ReactDOM.render(<App />, document.getElementById("root"));

初始化流程

render阶段

  1. ReactDOM.render(<App />, 'root')最终返回了HostRootFiber下的第一个子节点的实例,即App组件instance。但在返回实例之前,react先创建了一个基础数据结构,挂载在了root DOM节点上。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    root._internalRootContainer = {
    _internalRoot: {
    finishedWork: null,
    current: HostRootFiber,
    containerInfo: div#root,
    }
    };
    ReactNode._internalRoot = FiberRoot;
    FiberRoot.current = HostRootFiber;
    HostRootFiber.stateNode = FiberRoot;
  2. 结构有了,需要updateContainer(),把fiber树建立起来。在进入workLoop()之前,都需要从当前操作的fiber节点回溯到FiberRoot,沿途更新父fiberchildExpirationTime,标记子fiber是否需要更新。(ExpirationTimer越大,优先级越高)。

    1
    2
    const root = markUpdateTimeFromFiberToRoot(fiber, expirationTime);
    prepareFreshStack(root, expirationTime);

    同时,还需要通过prepareFreshStack(root, expirationTime)方法,将workInProgress置为HostRootFiber,所以,不论初始化还是更新,workLoop()都是从HostRootFiber开始。

  3. 第一次循环,workInProgress (又称unitOfWork)HostRootFiber,从performUnitOfWork()进入beginWork(),当前workInProgress.tagHostRoot,走updateHostRoot()方法。这里,最重要的是调用了reconcilChildren(current, workInProgress, nextChildren)nextChildren是代表<App />的一个ReactElement,就是一个普通的对象,内部的props属性为空。

    1
    2
    3
    4
    5
    6
    7
    nextChildren = {
    $$typeof: Symbol(react.element)
    key: null
    props: {}
    ref: null
    type: ƒn App(props)
    }

    reconcilChildren做了一件事,使用传入的ReactElement生成一个fiberNode,并与workInProgress-fiber链接起来。

    1
    2
    workInProgress.child = fiber;
    fiber.return = workInProgress;

    最后,本次循环beginWork返回了workInProgress.child,即App-fiber。进入下一个循环。

  4. 此时,workInProgress指向了App-fiber。这里要明白一点,此时我们并没有div.container、button、spanfiber节点,App-fiberchildnull。循环从performUnitOfWork()进入beginWork()

    当前workInProgress.tagClassComponent,走updateClassComponent()方法。初始化时,Class App还没有实例,要先constructormount组件类。

    a. constructClassInstance(),执行类的constructor()创建实例instance,同时super(props)调用父类Component的构造函数,之后将this.updater指向classComponentUpdater对象,内有关键的enqueueSetState方法,用来之后更新组件。将App-fiberstateNode指向instance

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    const instance = new ctor(props, context);  //new App(props, context)

    instance.updater = classComponentUpdater;
    workInProgress.stateNode = instance;

    const classComponentUpdater = {
    enqueueSetState(inst, payload, callback) {
    enqueueUpdate(fiber, update);
    scheduleWork(fiber, expirationTime);
    }
    };

    //实例调用this.setState(payload, callback),实际上调用了Component.prototype.setState,这个原型方法内部直接调用了 this.updater.enqueueSetState(),也就是实例上的updater提供的方法。

    b. mountClassInstance(),为instance初始化props、state、refs、context等属性、执行getDerivedStateFromProps()、为componentDidMount标记effectTag

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    const instance = workInProgress.stateNode;
    instance.props = newProps;
    instance.state = workInProgress.memoizedState; //{ count: 1 }
    instance.refs = emptyRefsObject;
    ...
    const getDerivedStateFromProps = ctor.getDerivedStateFromProps; //注意这里,类的static方法
    if (typeof getDerivedStateFromProps === 'function') {
    applyDerivedStateFromProps(
    workInProgress,
    ctor,
    getDerivedStateFromProps,
    newProps,
    );
    instance.state = workInProgress.memoizedState;
    }
    ...
    if (typeof instance.componentDidMount === 'function') {
    workInProgress.effectTag |= Update;
    }

    c. finishClassComponent(),执行instance.render(),返回嵌套的ReactElement,调用reconcileChildren()方法,链接App-fiber和它的子节点。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    const nextUnitOfWork = finishClassComponent();
    ...
    //finishClassComponent
    const nextChildren = instance.render(); //调用App定义的render()方法,返回嵌套ReactElement
    let nextChildren = {
    $$typeof: Symbol(react.element),
    key: null,
    props: {
    children: [
    {
    $$typeof: Symbol(react.element),
    key: "1",
    props: {
    children: "点击",
    onClick: fn()
    },
    ref: null,
    type: "button",
    },
    {
    $$typeof: Symbol(react.element),
    key: "2",
    props: {
    children: 1
    },
    ref: null,
    type: "span",
    }
    ]
    },
    ref: null,
    type: "div",
    }

    //reconcileChildren内部,使用nextChildren生成div.container-fiber,将这个fiber与App-fiber链接起来,注意,此时内部的button和span依然是ReactElement,组成了一个数组挂载在div-fiber的pendingProps.children内。
    reconcileChildren(current, workInProgress, nextChildren, renderExpirationTime);

    最后,本次循环beginWork返回了workInProgress.child,即div-fiber。进入下一个循环。

  5. 此时,workInProgress指向了div-fiber。循环从performUnitOfWork()进入beginWork(),当前workInProgress.tagHostComponent,走updateHostComponent()。没有什么特别的处理,直接调用reconcileChildren()进行fiber的创建和链接。

    这次的reconcileChildren有些不同,因为nextChildren是一个数组,里面是buttonspanReactElement形式。react会将数组内的所有元素都创建为fiber节点,然后通过sibling链接起来,同时他们有共同的父fiber节点。

    1
    2
    reconcileChildren(current, workInProgress, nextChildren, renderExpirationTime);
    //nextChildren为数组[{ $$typeof: ... }, { $$typeof: ... }]

    最后,beginWork返回了div-fiber的第一个child,即button-fiber

  6. workInProgress指向button-fiber,循环从performUnitOfWork()进入beginWork(),当前workInProgress.tagHostComponent,走updateHostComponent()。这里,button-fiber没有child要链接,reconcileChildren()不做任何事情,beginWork()完成。

    接下来进入completeUnitOfWork(),这个函数是一个中转函数,负责处理循环的走向(走向sibling还是return),button-fiber先进入completeWork()

    completeWork()内也会根据workInProgress.tag进行不同的处理。对于HostComponent,react会使用原生的document.createElement()fiber创建真正的DOM元素。

    1
    2
    3
    4
    5
    //type为button, rootContainerInstance为rootDOM
    let instance = createInstance(type, newProps, rootContainerInstance, currentHostContext, workInProgress);
    //内部调用react-dom/ReactDOMComponent.js,使用document.createElement()创建DOM

    workInProgress.stateNode = instance;

    button-fiber的整个workLoop()走完。workInProgress指向其sibling,即span-fiber

  7. span-fiber进入beginWork()。与button-fiber的流程一样,在completeWork()内创建了真正的DOM元素,但span-fibersiblingnull,故返回了return节点,即div-fiberspan-fiberworkLoop()循环完成。

  8. div-fiber进入completeUnitOfWork(),随后进入completeWork()。同属于HostComponent(),通过createInstance()创建了真正的DOM节点,没有sibling,返回return节点,即App-fiberdiv-fiberworkLoop()循环完成。

  9. App-fibercompleteUnitOfWork()completeWork()没有什么特别的操作(类组件,无需创建DOM),没有sibling,返回return节点,即HostRootFiberApp-fiberworkLoop()完成。

  10. HostRootFiber同样在completeUnitOfWork()completeWork()没有什么特别的操作,workLoop()完成。

至此,初始化的render phase完成。

commit阶段

接下来进入commit阶段。render阶段完成时,react会生成一条effect-list链表,记录着需要处理的side-effect(这个链表的生成过程另文总结)。由commitRoot()进入到commitRootImpl()

1
2
3
4
5
6
function commitRoot(root, finishedWork) {
commitBeforeMutationEffects();
commitMutationEffects();
root.current = finishedWork;
commitAllLifeCycles();
}
  1. 第一阶段称为before mutation phase,这时可以读取DOM,但DOM还未更新,也不可执行side-effect,这个阶段主要用来执行getSnapshotBeforeUpdate()生命周期。需要注意的是,commit阶段的每个子步骤,react都会去遍历整个effect-list,将整个list上所有对应的work完成后再进入下个步骤,继续从头开始遍历。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function commitBeforeMutationEffects() {
    while (nextEffect !== null) {
    switch(workInProgress.tag) {
    case ClassComponent: {
    instance.getSnapshotBeforeUpdate();
    return;
    }
    case HostComponent: { ... }
    }
    nextEffect = nextEffect.nextEffect;
    }
    }
  2. 第二阶段称为mutation phase,主要是用来进行DOM的更新,注意内部的commitWork()方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    function commitMutationEffects() {
    while (nextEffect !== null) {
    case Placement: {
    commitPlacement(nextEffect);
    nextEffect.effectTag &= ~Placement;
    break;
    }
    case PlacementAndUpdate: { ... }
    case Update: {
    const current = nextEffect.alternate;
    commitWork(current, nextEffect);
    break;
    }
    case Deletion: { ... }
    nextEffect = nextEffect.nextEffect;
    }
    }
  3. 第三阶段我们需要将workInProgress Tree替换为current Tree

    1
    root.current = finishedWork;
  4. 最后一个阶段,我们要执行具有side-effect的生命周期函数。这里,指的就是componentDidMountcomponentDidUpdate,依据的是effect-list fiber内的effectTag属性。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    function commitLayoutEffects() {
    while (nextEffect !== null) {
    const effectTag = nextEffect.effectTag;

    if (effectTag & (Update | Callback)) {
    const current = nextEffect.alternate;
    commitLayoutEffectOnFiber(
    root,
    current,
    nextEffect,
    committedExpirationTime,
    );
    }

    if (effectTag & Ref) {
    recordEffect();
    commitAttachRef(nextEffect);
    }
    nextEffect = nextEffect.nextEffect;
    }
    }

    function commitLayoutEffectOnFiber() {
    switch (finishedWork.tag) {
    case ClassComponent: {
    const instance = finishedWork.stateNode;
    if (finishedWork.effectTag & Update) {
    if (current === null) {
    instance.componentDidMount();
    } else {
    ...
    instance.componentDidUpdate();
    }
    }
    }
    case HostComponent: { ... },
    ...
    }
    }

至此,commit phase完成。

更新流程

更新过程大体上与初始化过程一致,比如fiber node依然会走workLoop()循环。但更新过程是由instance.setState()调用触发,对于Class组件来说需要执行不同的生命周期函数,还需要去diffing algorithm标记节点的变化,另外还需要标记effectTagFiber Tree的遍历也会跳过没有“变更”的节点,直达需要处理的节点。

render阶段

  1. 初始化阶段reactDOM为实例绑定了updater属性,在实例调用setState()方法时,就会执行到ReactDOM提供的enqueueSetState方法,继而触发scheduleWork过程。同理,在执行workLoop()之前,react会从当前节点(App组件)回溯到root,沿途更新parent nodechildExpirationTime,同时会将workInProgress指向HostRootFiber。不过,更新时react会跳过bails out那些没有任何work的节点,直接达到目标节点,即我们的App-fiber。我们假定的例子中,HostRootFiber下即为App-fiber,所以只跳过了HostRootFiber。这时,App-fiber进入了workLoop()

  2. workInProgress指向App-fiber,在beginWork中依据wrokInProgress.tag执行updateClassComponent(),此时我们已经有了App组件的实例,继而进入updateClassInstance()方法。

    a. 依据setState(payload, callback)计算新的state

    b. 执行getDerivedStateFromProps()shouldComponentUpdate()生命周期函数。

    c. 为getSnapshotBeforeUpdatecomponentDidUpdate标记effectTag

    d. 更新instancestatepropscontext属性。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    function updateClassInstance(current, workInProgress, ctor, newProps, ...) {
    const instance = workInProgress.stateNode;
    //第一步,计算新的state
    const oldState = workInProgress.memoizedState; //{ count: 1 }
    let newState = (instance.state = oldState); //{ count: 1 }
    let updateQueue = workInProgress.updateQueue; //{baseState: 1, firstUpdate:{ payload: {count: 2}, callback, }, lastUpdate:{ payload: {count: 2}, callback,}}
    if (updateQueue !== null) {
    processUpdateQueue(
    workInProgress,
    updateQueue,
    newProps,
    instance,
    renderExpirationTime,
    );
    newState = workInProgress.memoizedState; // { count: 2 }
    }

    //第二步,执行getDerivedStateFromProps
    if (typeof getDerivedStateFromProps === 'function') {
    applyDerivedStateFromProps(
    workInProgress,
    ctor,
    getDerivedStateFromProps,
    newProps,
    );
    newState = workInProgress.memoizedState;
    }

    //第三步,标记componentDidUpdate和getSnapshotBeforeUpdate
    if (typeof instance.componentDidUpdate === 'function') {
    workInProgress.effectTag |= Update;
    }
    if (typeof instance.getSnapshotBeforeUpdate === 'function') {
    workInProgress.effectTag |= Snapshot;
    }

    //第四步,更新props、state、context属性
    instance.props = newProps;
    instance.state = newState;
    instance.context = nextContext;

    return shouldUpdate;
    }

    接下来进入finishClassComponent()方法。这里,先执行instance.render()获取新的嵌套ReactElement,注意,此时render()内的JSX使用的都是最新的propsstate,但DOM还没有更新,页面还没有展示出变化。拿到新的nextChildren后,同样调用reconcileChilder()方法,进行节点的diffing algorithm,实际上通过比较已有的fiber node和新生成的ReactElement,依据keyparent node的类型等属性,决定节点的删除、移动、更新等。这里,我们的span节点进行了内容的变更,前后节点变化为:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    {
    stateNode: new HTMLSpanElement,
    type: "span",
    key: "2",
    memoizedProps: {children: 1},
    pendingProps: {children: 1},
    ...
    }

    {
    stateNode: new HTMLSpanElement,
    type: "span",
    key: "2",
    memoizedProps: {children: 1},
    pendingProps: {children: 2},
    ...
    }

    这之后,返回了App-fiber的子节点,进入div-fiberworkLoop()

  3. 接下来的div-fiberbutton-fiberworkLoop()过程没有什么特别的地方。不过,button-fibercompleteWork()阶段存疑,初始化时创建了DOM,更新时又会如何处理?这里存疑,还需深入研究。我们先跳过,直接进入span-fiberworkLoop()

  4. 进入span-fiber的循环后,beginWorkcompleteUnitOfWork都没什么特别要处理的,直接进入completeWork()。在这里,需要为span-fiber准备好DOM的更新,标记好effectTag,同时,span-fibermemoizedProps也会被更新为pendingProps

  5. 整个更新流程的render phase就完成了,这次的render phase,我们为App组件标记了getSnapshotBeforeUpdatecomponentDidUpdate两个effectTag(位运算,叠加),为span-fiber标记了DOM更新的effectTag。而且这两个节点的更新,我们使用firstEffectnextEffect属性与HostRootFiber进行了链表链接,在接下来的commit phase阶段去处理。

commit阶段

这次的commit阶段与初始化时的commit阶段基本一致,可能在DOM更新等地方有所不同,还需深入研究。

总结

react的源码非常复杂,读起来非常的吃力。上面写了这么多,也只是浅尝即止。内部各任务如何调度、expirationTime如何设置和处理、side-effect链的生成,还有Hooksrefcontext的融合,DOM元素的生成等等都能单独拿出来研究很久。不过大致的流程过了一遍,也算打开了一条门缝,可借以窥探内部的种种奥秘。接下来会去单独研究一下effect-listexpirationTime的相关知识点。