xiaoxiaosaohuo/Note

React深入分析 (2)更新过程

Opened this issue · 0 comments

主要分析一下React处理更新和构建effect list

demo code

class App extends React.Component {
    constructor(props) {
        super(props);
        this.state = {count: 0};
        this.handleClick = this.handleClick.bind(this);
    }

    handleClick() {
        this.setState((state) => {
            return {count: state.count + 1};
        });
         this.setState((state) => {
            return {count: state.count + 2};
        });
    }
    componentDidUpdate() {}


    render() {
        return (<React.Fragment>
            <button key="1" onClick={this.handleClick}>点我</button>
            <span key="2">{this.state.count}</span>
            </React.Fragment>
        )
    }
}

整个Reconcliation分为render 和 commit两个阶段

render阶段

  • 更新App的state对象的count属性
  • 调用render方法,生成children,对children进行reconcilation.
  • 更新span的属性

commit阶段

  • 更新span 的textContent属性
  • 调用componentDidUpdate生命周期

render 阶段

当我们点击按钮时click事件触发,会调用事件回调函数,本例子中调用两次setState,会进入批量处理。

每一个React Class Component都有一个updater,默认情况下,update对象是ReactNoopUpdateQueue,一个抽象的API表示,并没有什么功能。

function Component(props, context, updater) {
  this.props = props;
  this.context = context;
  // If a component has string refs, we will assign a different object later.
  this.refs = emptyObject;
  // We initialize the default updater but the real one gets injected by the
  // renderer.
  this.updater = updater || ReactNoopUpdateQueue;
}
Component.prototype.setState = function (partialState, callback) {
  !(typeof partialState === 'object' || typeof partialState === 'function' || partialState == null) ? invariant(false, 'setState(...): takes an object of state variables to update or a function which returns an object of state variables.') : void 0;
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
};

当Class组件实例化时,实例的updater会赋予具体的实现,这种做法使得在ReactDOM, React Native, SSR ,testRender等环境有不同的setState实现机制。

在ReactDOM中,创建组件实例时会设置实例的uptater为classComponentUpdater,它主要负责处理更新队列和调度工作。

function adoptClassInstance(workInProgress, instance) {
  instance.updater = classComponentUpdater;
  workInProgress.stateNode = instance;
}

var classComponentUpdater={
    isMounted: isMounted,
    enqueueSetState: function (inst, payload, callback) {},
    enqueueReplaceState: function (inst, payload, callback) {},
    enqueueForceUpdate: function (inst, callback) {}
    
}

对于本demo而言,调用setState时会调用this.updater.enqueueSetState方法。

enqueueSetState: function (inst, payload, callback) {
    var fiber = get(inst); //获取实例的fibernode
    var currentTime = requestCurrentTime();
    var expirationTime = computeExpirationForFiber(currentTime, fiber);
    //创建更新队列
    var update = createUpdate(expirationTime);
    update.payload = payload;
    if (callback !== undefined && callback !== null) {
      {
        warnOnInvalidCallback$1(callback, 'setState');
      }
      update.callback = callback;
    }

    flushPassiveEffects();
    enqueueUpdate(fiber, update);
    scheduleWork(fiber, expirationTime);
  },

首先获取当前组件的fiber node,创建更新队列update,用payload更新update.payload,payload是setState方法的参数。可以是函数或者对象。
本例子中是一个函数。

update={
    callback: null
    expirationTime: 1
    next: null
    nextEffect: null
    payload: 
    tag: 0
}
update.payload = payload;

接下来调用enqueueUpdate,传入fiber, update对象,这一步是处理更新队列,将更新添加到fiber 的updateQueue中,第一次调用setState后updateQueue的结构如下

{
    stateNode:App, //实例
    type:ƒ App(props),//构造函数
    updateQueue:{
        baseState: {count: 0},
        firstUpdate: {
        expirationTime: 1,
        tag: 0, 
        payload: ƒ (state),
        callback: null, 
        next: null, 
        },
        lastUpdate:{
        expirationTime: 1,
        tag: 0, 
        payload: ƒ (state),//(state) => { return {count: state.count + 1} }
        callback: null, 
        next: null, 
        },
    }
}

可以看到updateQueue.firstUpdate.payload就是setStae传入的回调函数。

由于进入了批量处理,接着会调用第二个setState方法。

{
    stateNode:App, //实例
    type:ƒ App(props),//构造函数
    updateQueue:{
        baseState: {count: 0},
        firstUpdate: {
        expirationTime: 1,
        tag: 0, 
        payload: ƒ (state),
        callback: null, 
        next: {
            callback: null
            expirationTime: 1
            next: null
            nextEffect: null,
            ////(state) => { return {count: state.count + 2} }
            payload: ƒ (state)
        }, 
        },
        lastUpdate:{
        expirationTime: 1,
        tag: 0, 
        payload: ƒ (state),//(state) => { return {count: state.count + 2} }
        callback: null, 
        next: null, 
        },
    }
}

处理完更新队列之后就开始scheduleWork了。

从根节点HostRoot Fiber node开始调用renderRoot方法 ,

根据root.current创建nextUnitOfWork,然后开始工作,所有的工作都在fiber node的alertnate上进行,如果alertnate不存在,React会调用createWorkInProgress 创建,然后处理更新。

需要注意的是它会跳过(跳过)已经处理过的fiber节点,直到它找到一个未完成工作的节点,本示例中是App fiber node.

如果当前节点没有要完成的工作,会直接克隆子节点并返回,作为下一个工作单元。

function bailoutOnAlreadyFinishedWork(current$$1, workInProgress, renderExpirationTime) {
     cloneChildFibers(current$$1, workInProgress);
    return workInProgress.child;
}

beginWork

beginWork方法是一个有很多case的switch方法,

function beginWork(current$$1, workInProgress, renderExpirationTime) {
    switch (workInProgress.tag) {
    case IndeterminateComponent:
    ...
    case LazyComponent:
    ...
    case FunctionComponent:
    ...
    case ClassComponent:
    return updateClassComponent(current$$1, workInProgress, _Component2, _resolvedProps, renderExpirationTime);
     case HostRoot:
     ...
     case HostComponent:
     ...
     case HostText:
     ...
    
}

显然App是ClassComponent,会调用ClassComponent对应的处理方法。

updateClassComponent 处理App组件的更新

移除部分无关代码。

function updateClassComponent(current$$1, workInProgress, Component, nextProps, renderExpirationTime) {
    var instance = workInProgress.stateNode;
    var shouldUpdate = void 0;
 
    shouldUpdate = updateClassInstance(current$$1, workInProgress, Component, nextProps, renderExpirationTime);
    return finishClassComponent(current$$1, workInProgress, Component, shouldUpdate, hasContext, renderExpirationTime);
}

function updateClassInstance(current, workInProgress, ctor, newProps, renderExpirationTime) {
//获取实例
  var instance = workInProgress.stateNode;

  var oldProps = workInProgress.memoizedProps;
  instance.props = oldProps;
  
  var getDerivedStateFromProps = ctor.getDerivedStateFromProps;
  var oldState = workInProgress.memoizedState;
  var newState = instance.state = oldState;
  var updateQueue = workInProgress.updateQueue;
  //处理updateQueue
  if (updateQueue !== null) {
    processUpdateQueue(workInProgress, updateQueue, newProps, instance, renderExpirationTime);
    newState = workInProgress.memoizedState;
  }
  //调用getDerivedStateFromProps
  if (typeof getDerivedStateFromProps === 'function') {
    applyDerivedStateFromProps(workInProgress, ctor, getDerivedStateFromProps, newProps);
    newState = workInProgress.memoizedState;
  }
  //设置shouldUpdate,会调用shoulComponentUpdate方法
  var shouldUpdate = checkHasForceUpdateAfterProcessing() || checkShouldComponentUpdate(workInProgress, ctor, oldProps, newProps, oldState, newState, nextContext);
  //标记effectTag
   if (shouldUpdate) {
        instance.componentWillUpdate(newProps, newState, nextContext);
        workInProgress.effectTag |= Update;
        workInProgress.effectTag |= Snapshot;
    }
    //更新实例的props,state,context
    instance.props = newProps;
  instance.state = newState;
  instance.context = nextContext;

  return shouldUpdate;
  }
  

这里重点看一下processUpdateQueue 方法,如何处理更新队列的。

在processUpdateQueue内部通过循环遍历firstUpdate进行处理,getStateFromUpdate方法处理单个update

function getStateFromUpdate(workInProgress, queue, update, prevState, nextProps, instance) {
     switch (update.tag) {
    case ReplaceState:
    ....
    case CaptureUpdate:
    ...
     case UpdateState:
      {
        var _payload2 = update.payload;
        var partialState = void 0;
        if (typeof _payload2 === 'function') {
          // Updater function
          
          partialState = _payload2.call(instance, prevState, nextProps);
        } else {
          // Partial state object
          partialState = _payload2;
        }
        if (partialState === null || partialState === undefined) {
          // Null and undefined are treated as no-ops.
          return prevState;
        }
        // Merge the partial state and the previous state.
        return _assign({}, prevState, partialState);
      }
    case ForceUpdate:
      {
        hasForceUpdate = true;
        return prevState;
      }
  }
  return prevState;
}

当处理完成之后

{
    stateNode:App, //实例
    type:ƒ App(props),//构造函数
    memoizedState: {count: 3},
    effectTag: 4,
    updateQueue:{
        baseState: {count: 3},
        firstUpdate:null,
        lastUpdate:null
    }
}

更新处理完成之后,实例的state,props,context就被更新了,接下来调用finishClassComponent方法进一步处理。

reconlication children

finishClassComponent 方法会调用组件的render方法生成children,并返回第一个child作为nextUnitOfWork,并用render方法中的数据更新子节点的属性。

由于button节点没有任何更新,我们略过,看span节点

这是reconcile App fiber 的children之前的状态

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

这是render方法调用之后返回的span element ,

{
    $$typeof: Symbol(react.element)
    key: "2"
    props: {children: 3}
    ref: null
    type: "span"
}

可以看出我们需要的更新数据在render方法返回的element中,在createWorkInProgress方法中,React会拷贝React element的属性去更新fiber node。

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

之后React在span Fiber node上工作时,会将pengingProps拷贝给memoizedProps,然后标记effect,以便将来更新DOM.

nextUnitOfWork= span fiber node

当nextUnitOfWork指向span Fiber node时,将进入下一个workloop。

由于span是HostComponent,beginWork会调用updateHostComponent ,这个方法会对span的子节点进行reconlication,由于span并不存在child,在beginWork工作完成之后,
更新memoizedProps。

function performUnitOfWork(workInProgress) {
    ...
    next = beginWork(current$$1, workInProgress, nextRenderExpirationTime);
    workInProgress.memoizedProps = workInProgress.pendingProps;
    ...
}

然后进入completeWork

completeWork on span

如同beginWork一样,这里也是一个大switch;

switch (workInProgress.tag) {
    case IndeterminateComponent:
      break;
    case LazyComponent:
      break;
    case SimpleMemoComponent:
    case FunctionComponent:
      break;
    case ClassComponent:
        break
    case HostComponent:
     updateHostComponent$1(current, workInProgress, type, newProps, rootContainerInstance);
     ....
     }

可以看到函数式组件,class组件直接跳过了,因为这一步的工作是处理DOM更新的。

对于我们的span 会做这些工作

  • prepareUpdate 处理更新,生成updatePayload,主要是diffProperties
  • 添加updateQueue到fiber
  • 添加effect

处理后span fiber相关的属性如下

{
    stateNode: new HTMLSpanElement,
    type: "span",
    effectTag: 4,
    updateQueue: ["children", "3"],
    ...
}

现在span fiber node上的工作完成了,开始往父级fiber上收集effect。

Effects list

由于只有App 和span 这两个fiber node 有side effects,React会将span fiber node 添加到其returnFiber.firstEffect

if (effectTag > PerformedWork) {
  if (returnFiber.lastEffect !== null) {
    returnFiber.lastEffect.nextEffect = workInProgress;
  } else {
    returnFiber.firstEffect = workInProgress;
  }
  returnFiber.lastEffect = workInProgress;
}

最后的effectList如下:

2018-12-15 19 30 11

Commit阶段

这个阶段从completeRoot方法开始,一旦开始工作,就不能被打断,直到工作完成。
这个阶段主要工作有两个

  1. 更新DOM
  2. 调用生命周期 componentDidUpdate

应用更新

三个大循环在前几篇文章中有提到过,简化一下就是下面这样

function commitRoot(root, finishedWork) {
    //getSnapshotBeforeUpdate
    commitBeforeMutationLifecycles()
    //应用更新
    commitAllHostEffects();
    root.current = finishedWork;
    //调用生命周期
    commitAllLifeCycles();
}

这里着重看一下后两个阶段

DOM 更新

这个阶段会调用commitAllHostEffects,这是React将span的text从0改变为3的过程,

function commitAllHostEffects() {
    while (nextEffect !== null) {
        var primaryEffectTag = effectTag & (Placement | Update | Deletion);
        switch (primaryEffectTag) {
          case Placement:
          {
              commitPlacement(nextEffect);
               nextEffect.effectTag &= ~Placement;
              break;
            }
        case PlacementAndUpdate:
        ....
        
        case Update:
            {
              var _current2 = nextEffect.alternate;
              commitWork(_current2, nextEffect);
              break;
            }
          case Deletion:
            {
              commitDeletion(nextEffect);
              break;
            }
        }
        nextEffect = nextEffect.nextEffect;
    }
    
}

对于span fiber node 会进入Update case,而commitWork接着调用updateDOMProperties更新属性

function updateDOMProperties(domElement, updatePayload, ...) {
  for (let i = 0; i < updatePayload.length; i += 2) {
    const propKey = updatePayload[i];
    const propValue = updatePayload[i + 1];
    if (propKey === STYLE) { ...} 
    else if (propKey === DANGEROUSLY_SET_INNER_HTML) {...} 
    else if (propKey === CHILDREN) {
      setTextContent(domElement, propValue);
    } else {...}
  }
}

DOM更新之后,设置root.current 指向finishedWork,也就是当前工作树。

root.current = finishedWork;

调用生命周期

最后一个方法是调用生命周期钩子。由于在render阶段给App添加了Update effect,将在这个方法里调用App的componentDidUpdate钩子

function commitAllLifeCycles(finishedRoot, ...) {
    while (nextEffect !== null) {
        const effectTag = nextEffect.effectTag;

        if (effectTag & (Update | Callback)) {
            const current = nextEffect.alternate;
            commitLifeCycles(finishedRoot, current, nextEffect, ...);
        }
        
        if (effectTag & Ref) {
            commitAttachRef(nextEffect);
        }
        
        nextEffect = nextEffect.nextEffect;
    }
}

最终会调用commitLifeCycles

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

可以看到,当current不存在时会调用componentDidMount,
存在是会调用componentDidUpdate。

end

进行收尾清理

比如

root.expirationTime = expirationTime;
root.finishedWork = null;
isRendering = false;

以上就是更新的基本过程。

后期会研究一下context和hooks原理