UNDERCOVERj / tech-blog

个人博客☺️
39 stars 1 forks source link

React16.2源码解析-页面渲染流程 #12

Open UNDERCOVERj opened 6 years ago

UNDERCOVERj commented 6 years ago

前言

本文中

workInProgress 指的是正在工作的 fiber

"更新队列"指 workInProgress.updateQueue

流程

jsx => element tree => fiber tree => html dom

React 渲染页面

  1. schedule work

执行虚拟 DOMfiber 树)的更新,有 scheduleWorkrequestWork , performWork 是三部曲。

  1. reconcile work

    • 调度阶段( render/reconciliation ):在这个阶段 React 会更新数据生成新的 Virtual DOM,然后通过 Diff 算法,快速找出需要更新的元素,放到更新队列中去,得到新的更新队列。(改变pendingState, pendingProps,最后构建新的fiber tree)。在初始时则是生成子 fiber ,连接 fibersiblingFiber
    • 渲染阶段(commit):这个阶段 React 会遍历更新队列,将其所有的变更一次性更新到DOM上。并触发响应生命周期函数钩子。

流程图

reactdom render

什么是JSX

JSXJavaScript 的一种语法扩展,它很像一种模板语言

JSX 应用实例:

  1. JSX 作为表达式
function getElement () {
    if (flag) {
        return <p>true</p>
    }

    return <p>false</p>

}
const element = getElement();
  1. JSX指定属性
const element = <div className="element">element</div>

生命周期

image

image

componentWillReceiveProps在props改变时执行

JSX的编译方式

react 中,通过 babel 可以将 JSX 编译成 JavaScript 代码

比如 JSX 表达式

const element = (
    <div className="app">
        hi
    </div>
)

编译成 JavaScript

const element = React.createElement('div', {
    className: 'app'
}, 'hi')

child 以参数形式一个个传入 React.createElement(而不是以数组形式)

编译得到 element的流程如下:

流程

在得到首个 element 后,便能执行 ReactDOM.render 函数,并将 element,container,callback 传入

1. schedule work阶段

schedule work

1.1 renderSubtreeIntoContainer

渲染一个元素到 DOM,将 container 中原有的子元素删除。比如下列代码,会将原有的 p 元素删除,而用 <div>child</div> 代替

// html

<div id="app">
    <p>child</p>
</div>

ReactDOM.render(<div>child</div>, document.querySelector('#app'))

待新的 root 构建好后,将之与 container 关联: root = container._reactRootContainer = newRoot

1.2 createContainer

创建 fiber root

var root = {
    current: {
        alternate: null,
        tag,
        key,
        type,
        stateNode: root,
        child,
        return,
        index,
        ref,
        pendingProps,
        memoizedProps,
        updateQueue,
        memoizedState,
        effectTag,
        nextEffect
        firstEffect,
        lastEffect,
        expirationTime
    },
    containerInfo: containerInfo,
    pendingChildren: null,
    remainingExpirationTime: NoWork,
    isReadyForCommit: false,
    finishedWork: null,
    context: null,
    pendingContext: null,
    hydrate: hydrate,
    nextScheduledRoot: null
};

1.3 scheduleTopLevelUpdate

创建 update 对象,初始的 fiber rootupdateQueue 中,partialStateelement 组成的对象

var update = {
    expirationTime: expirationTime,
    partialState: { element: element },
    callback: callback,
    isReplace: false,
    isForced: false,
    nextCallback: null,
    next: null
}

1.4 insertUpdateIntoFiber

update 对象植入 current fiber 中。挂载在 firstlast 属性上

root.current.updateQueue = {
    baseState: null,
    expirationTime: NoWork,
    first: { // update对象
        expirationTime: expirationTime,
        partialState: { element: element },
        callback: callback,
        isReplace: false,
        isForced: false,
        nextCallback: null,
        next: null
    },
    last: {  // update对象
        expirationTime: expirationTime,
        partialState: { element: element },
        callback: callback,
        isReplace: false,
        isForced: false,
        nextCallback: null,
        next: null
    },
    callbackList: null,
    hasForceUpdate: false,
    isInitialized: false,
    isProcessing: false
}

1.5 scheduleWorkImpl

用一个 while 循环沿着 parent 位置向上遍历 fiber ,直到 root.current ,并且更新每个 fiberexpirationTime

然后再 requestWork

1.6 requestWork

当调度器有 update 工作的时候,就会调用 requestWork 。在其中改变 firstScheduledRootlastScheduledRoot 等被调度的 FiberRoot

函数中需要判断的 isBatchingUpdates,在事件触发后会改变。以下代码是事件触发后会执行,所以在 requestWork 时,isBatchingUpdatestrue

事件绑定机制

function batchedUpdates(fn, a) {
    var previousIsBatchingUpdates = isBatchingUpdates; // 批量处理嘛
    isBatchingUpdates = true;
    try {
        return fn(a); // // 此过程中可能改变state所以需要再performWork
    } finally {
        isBatchingUpdates = previousIsBatchingUpdates; 
        if (!isBatchingUpdates && !isRendering) {
            performWork(Sync, null);
        }
    }
}

1.7 performWork

找到优先级最高的 work ,并激活它,此后开始构建 fiber

1.8 performWorkOnRoot

开始渲染,isRendering = true 区分同步渲染还是异步渲染

performWorkOnRoot 方法中有两个重要的步骤。1. renderRoot 。2. commitRoot

2. reconcile work 阶段

详情见:Reconciliation

2.1 renderRoot

开始 work ,设置 workloop 函数中所用到的 nextUnitOfWork(复制 nextRoot.current 得到) ,然后执行 workloop 循环函数,得到更新后的 fiber 树。

如下图:创建 workInProgress(复制 current fiber(这也是 current.alternatecurrent 不全等的原因)),并将 workInProgressalternate 指向 current fiber,在之后使用 workInProgress 的时候,都是用它的替代品( current.alternate )来作文章

nextUnitOfWork = createWorkInProgress(nextRoot.current, null, expirationTime);

// 结果如下

var current = root.current;
nextUnitOfWork = {
    alternate: current,
    stateNode: root,
    tag,
    key,
    child,
    sibling,
    return,
    effectTag
    ....
}

所以在 reconcileChildren 阶段,改变了 workInProgress.child ,但是 current.child 不会改变。也就是 workInProgresscurrent 的复制品,所以 workInProgress 的属性改变并不会导致 current 属性改变。

经过 renderRoot 后,得到 root.current.alternate

var finishedWork = root.current.alternate = {
    alternate: current,
    child: {
        alternate: null,
        child: {
            alternate: null,
            child: null
        },
        sibling: {
            alternate: null,
            child: {
                alternate: null,
                child: null,
                sibling: null
            }
        }
    }
}

2.1.1 workloop

以深度遍历方式,激活所有期望 work ,对每个 work 都执行 performUnitOfWork

function workLopp () {
    while (nextUnitOfWork !== null) { // 激活所有期望任务
        nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    }
}

实现深度遍历:

lsls

遍历逻辑如下:

dd

对1中提到的 workInProgress 或者说 nextUnitOfWork 开始工作,对下面层级的子 fiber 进行更新。

2.1.2 performUnitOfWork

performUnitOfWork 中有两个重要函数,beginWorkcompleteUnitOfWork

var next = beginWork(current, workInProgress, nextRenderExpirationTime);

if (next === null) { // 表示workInProgress为叶节点,无next了,子节点只是text了。
    // If this doesn't spawn new work, complete the current work.
    next = completeUnitOfWork(workInProgress);
}

return next;

2.1.2.1 beginWork

期间执行生命周期函数componentWillMountcomponentWillReceivePropscomponentWillUpdate。在 updateClassComponent 中执行 render

根据 workInProgresstagcomponent 类型)来做出相应的更新。将更新后得到的 child(可为 null)返回,置为 child 属性,此时 alternate 仍未 null

bailoutOnAlreadyFinishedWork 函数中,可以改变 workInProgress.childworkInProgress.child.alternate

childFiber.alternate 改变是在 cloneChildFibers 过程,将本来为 nullalternate 改为指向 childFiber

有如下 update 操作:

  1. mountIndeterminateComponent
  2. updateFunctionalComponent
  3. updateClassComponent

ds

  1. 通过判断 workInProgress.alternate 是否为 null ,来判断是否是第一次处理 该 workInProgress。否就执行2,是则跳到3。待2、3执行完后跳到5
  2. 执行 updateClassInstance 来更新 instance 实例。如果不能渲染,则不改变实例。在此函数中还需要处理 workInProgress.updateQueue。另外需要调用更新前的生命周期函数:componentWillReceivePropscomponentWillUpdate 。另外需要替换新旧 propsstate,在更新完后,通过在此步中设置的 effectTag 来调用生命周期函数:componentDidUpdate。最后更新 instance 上的 props,state,refs 等属性
  3. 执行 constructClassInstance ,更具工作任务的 type 来创建 instance 实例(在 fiber 架构中有描述)。并给 instance 对象属性赋值,并关联 instanceworkInProgress
  4. 执行 mountClassInstance ,给 instance 添加 props,state,refs 等属性。调用生命周期函数 componentWillMount ,并在钩子函数执行完后检查 updateQueue。改变 effectTag ,在 mount 后执行 componentDidMount 钩子。
  5. 执行 finishClassComponent,调用 instance.render() ,得到 element ,并将此 element和workInProgress 传入 reconcileChildFibers 中进行调和,如果 element 不为 null ,根据 $$typeof 类型创建 childFiber ,将 childFiber 挂载在 workInProgresschild 属性上,最后返回 child
  1. updateHostRoot

如果 workInProgress 更新队列(如1.4中的updateQueue)不为 null,则处理更新队列。在处理过程中将 callback 回调函数推进更新队列的 callbackList,并将 update 对象链的 partialState 合并。

  1. 从需要更新的对象开始。var update = updateQueue.first
  2. update = update.next 走,直到 update.nextnull
  3. 将更新对象 update 经过 getStateFromUpdate 处理,返回 partialState
  4. partialState 与上一个 state 进行 assign 合并
  5. 处理完后将 workInProgress.updateQueue 重置为 null

设置 workInProgress.effectTag ,标记 commit 阶段的操作(插入、更新、插入并更新、删除)

workInProgress 挂载 child fiber ,并返回该 child。在处理 child 的过程中,需要创建 fiber,有这几种方式 createFiberFromFragment,createFiberFromFragment,useFiber

如果workInProgress 更新队列(如1.4中的updateQueue)为,则返回 null。则返回 workInProgress.child

  1. updateHostComponent

此方法为更新形如 div、p 标签之类的 html 组件。故在 reconcileChildren 时传入的是 props.children。如果 props.children 是文本字符串而不是 element,则改变 workInProgresseffectTag 以便在 commit 阶段重置内容,并且传入 reconcileChildFibers的newChildnull ,并且最后返回 null

如果 props.children 为数组,则在 reconcileChildrenArray 中按序给每个元素进行调和,sibling 将每个 fiber 连接起来。最后返回第一个 child

在更新时 performWork 的过程中,若新旧 props 没有改变,则直接返回 workInProgress.child

  1. updateHostText
  2. updateCallComponent
  3. updatePortalComponent
  4. updateFragment

2.1.2.2 completeUnitOfWork

如果 beginWork 返回的 childnull ,则证明 workInProgress 已没有孩子 fiber 了,这时就需要去找 workInProgress 的下一个可遍历 fiber 节点。可以是返回 sibling fiber,没有的话是找父级的 sibling fiber并返回。

在此函数的 while 循环中,如果 workInProgress.tag 大于 PerformedWork 所对应的值,设置 returnFibereffect 串,在下一次循环时,将此 fibereffect 串又传递给returnFiber

2.2 commitRoot

finishedWork = renderRoot(root, expirationTime);

commitRoot(finishedWork);

renderRoot 后构建了一个新的 fiber 树,但此树只是 root.current 的替代品,还没有真正的应用到 root.current 上。

但是当 commitRoot 后,将 root.current 改变为 finishedWork 。这时 root.current.alternate === finishedWork.alternate 。为最初的 currentFiber 对象.

commitRoot 中,将已经连接起来的 effect 串从头开始执行,对于 effect fiber ,根据其 effectTag ,来标记其操作方式。

当渲染 dom 时,由组件为单位,从里到外渲染。

2.2.1 commitAllHostEffects

详情见: 视图改变

parent 插入真实 dom 节点

2.2.2 commitAllLifeCycles

  1. commitLifeCycles

根据 effect fibereffectTagalternate 来决定钩子函数如何执行。可执行 componentDidMountcomponentDidUpdate

var finishedWork = nextEffect;
if (finishedWork.effectTag & Update) {
    if (current === null) {
        instance.props = finishedWork.memoizedProps;
        instance.state = finishedWork.memoizedState;
        instance.componentDidMount(); // 执行componentDidMount钩子函数
    } else {
        var prevProps = current.memoizedProps;
        var prevState = current.memoizedState;
        instance.props = finishedWork.memoizedProps;
        instance.state = finishedWork.memoizedState;
        instance.componentDidUpdate(prevProps, prevState);
    }
}

关于 currentFiber.alternate 的改变

下列示范代码,详细请看源码的 createWorkInProgress

var current = {
    alternate: null,
    b: 1,
    c: 2
}

var workInProgress = { // 复制current,创建workInProgress
    alternate: null,
    b: 1,
    c: 2
}

workInProgress.alternate = current;
current.alternate = workInProgress;

current = {
    alternate: { // workInProgress
        alternate: current,
        b: 1,
        c: 2
    },
    b: 1,
    c: 2
}

return workInProgress
luke93h commented 6 years ago

能问下,思维导图用的是什么软件吗?

UNDERCOVERj commented 6 years ago

@luke93h xmind

luke93h commented 6 years ago

好的,谢谢啊~

qingtianiii commented 5 years ago

想问一下,那个 fiber 的时间分片是怎么体现在源码中的啊?源码中大量的 expirationTime、updateTime、next..Time 之类的时间用处在哪?还有各种的 startTimer、stopTimer 也不太理解是干嘛用的哎

zsjun commented 5 years ago

image 箭头反了