从源码角度来聊聊React的生命周期(下)

713 阅读17分钟

接上篇

从源码角度来聊聊React的生命周期(上)遗留了更新阶段和卸载阶段没有提到,本篇继续来分析。对于 React 而言,卸载阶段所做的工作其实更加复杂。

在进入源码之前还是提出一些问题,带着这些问题去看源码:

  • React 中有哪些方法可以重新 render?
  • React 的更新流程是怎样的?
  • 更新阶段的生命周期执行情况?
  • 父组件更新的时候,子组件是否会更新?如何避免子组件更新?

首先第一个问题,用过 React 的同学肯定都很熟悉了,主要是有下面这几种方法:

  • ReactDOM.render
  • props变化
  • setState
  • forceUpdate
  • useState
  • useReducer

第一个方法就是上篇提到的挂载阶段的入口方法,后面几种也分为了两大类,一个就是 class 组件,另外就是 function 组件。本篇也是围绕这几种方法的更新流程来分析。

更新阶段

先来回忆一下我们的生命周期流程图

react-lifecyle

还是那个例子,先来写一个 App 的根组件

class App extends Component {
  constructor(props) {
    super(props);
    console.log('father constructor');
    this.state = { count: 0, name: 'qiugu' };
  }

  shouldComponentUpdate() {
    console.log('father shouldComponentUpdate');
    return true;
  }

  getSnapshotBeforeUpdate() {
    console.log('father getSnapshotBeforeUpdate');
    return null;
  }
  
  static getDerivedStateFromProps() {
    console.log('father getDerivedStateFromProps');
    return {};
  }

  setCount = () => {
    this.setState({ count: this.state.count + 1 });
  };

  componentDidMount() {
    console.log('father componentDidMount');
  }

  componentDidUpdate() {
    console.log('father componentDidUpdate');
  }

  render() {
    console.log('father render');
    return (
      <div>
         <img src={logo} className="App-logo" alt="logo" />
         <Child name={this.state.name}/>
         <div>
          <button onClick={this.setCount}>
            count is: {this.state.count}
          </button>
         </div>
      </div>
    )
  }
}

我们把断点打在了 setCount 这里,来看下调用栈:

image.png

然后再把断点打在 render 方法中,再次看下调用栈:

image.png

好像还是看不明白,这次把断点加在 dispatchDiscreteEvent 方法上,结果发现:

image.png

只有一个调用栈,说明这里是 setCount 触发的起点了,在往上已经没有其他的调用栈了。回过头来看看这个方法名,大致可以猜测这是一个触发事件的方法,结合 React 的自定义事件系统,恍然大悟!这里点击了按钮,所以触发了 React 的事件系统,因此这里就是进行更新的起点。

关于 React 的事件系统不是今天的主题,暂且略过他,主要来看看 React 是如何调度更新的。

setState 更新

根据上面的 class 组件,首先点击 setCount 来触发 setState 更新。setState 方法是组件原型上面的方法,其实主要是调用了组件实例上的 updater 上存储的方法。上篇也分析过,如果是在 constructor 中调用 setState,此时组件实例还未生成,此时的 updater 上的方法并不是真正的更新方法。

先来看下 setState 方法内部是什么样的:

// 这里很熟悉,第一个参数就是传递的状态,第二个参数就是更新后的回调方法
Component.prototype.setState = function (partialState, callback) {
  // 这里对state做了一个验证类型的操作
  if (!(typeof partialState === 'object' || typeof partialState === 'function' || partialState == null)) {
    {
      throw Error( "setState(...): takes an object of state variables to update or a function which returns an object of state variables." );
    }
  }

  // 调用enqueueSetState方法
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
};

setState 做的事很简单,关键在于这个 enqueueSetState 方法,那么就看下这个方法做了什么:

function enqueueSetState (inst, payload, callback) {
    // 从组件实例上获取fiber节点
    var fiber = get(inst);
    var eventTime = requestEventTime();
    // 获取优先级
    var lane = requestUpdateLane(fiber);
    // 创建update
    var update = createUpdate(eventTime, lane);
    // 给update绑定state
    update.payload = payload;

    if (callback !== undefined && callback !== null) {
      {
        warnOnInvalidCallback(callback, 'setState');
      }

      update.callback = callback;
    }

    enqueueUpdate(fiber, update);
    scheduleUpdateOnFiber(fiber, lane, eventTime);
  },

这个方法中重点需要关注 update 的创建,update 其实就是一个对象,它是这样的:

var update = {
    eventTime: eventTime, // 任务调度相关字段,暂时忽略
    lane: lane, // 优先级
    tag: UpdateState, // 更新的类型
    payload: null, // 就是setState的第一个参数
    callback: null, // 回调函数,暂时不需要关注
    next: null // 与下一个update连接组成链表结构
};

带着这个 update,进入了 enqueueUpdate 方法中,这个方法中有一个 updateQueue,这个是在挂载阶段或者是上次更新时生成的更新队列,他的结构是这样的:

var updateQueue = {
    baseState: { count: 0, name: 'qiugu' }, // 此次更新前的状态
    effects: [], // 保存上次更新的回调
    firstBaseUpdate: null, // 更新的类型
    firstBaseUpdate: null, // 就是setState的第一个参数
    // 这里会保存当前正在更新的update
    shared: {
        pending: null
    }
};

这个 updateQueue 对象是 fiber 上的属性,最后更新也是基于此来更新的,所以这里主要了解一下 React 是如何去更新 updateQueue 中的状态的。

function enqueueUpdate(fiber, update) {
  // 取出fiber上的更新队列
  var updateQueue = fiber.updateQueue;

  if (updateQueue === null) {
    // 只会发生在fiber被卸载的情况
    return;
  }
  
  // 拿到上次准备更新的队列,如果是第一次的话,则为null
  var sharedQueue = updateQueue.shared;
  var pending = sharedQueue.pending;

  if (pending === null) {
    // 这是第一次更新,创建了一个循环链表
    update.next = update;
  } else {
    update.next = pending.next;
    pending.next = update;
  }
  
  // 把此次更新的update挂到更新队列上去
  sharedQueue.pending = update;
}

总结一下,其实 enqueueUpdate 方法就是将创建的 update 对象挂到了 fiber 节点的 updateQueue 上。如果是首次更新的话,让 update 指向了自己,形成了一个循环列表,如果上次的 fiber 中存在了更新队列,那么就让此次更新的 update 的 next 指向了 上次更新的 next,并且上次更新的 next 指向了 此次的 update。这么说有点绕,画个图来理解一下:

image.png

所谓存在了更新队列,实际上就是多次 setState 就会保留了上次的更新,从而进入了 else 的分支。不过最终都是会把更新连接起来形成了一个循环链表,这个循环链表在后面 render 阶段会被剪开。

好了,创建好了循环队列以后,就开始进行调度更新了,也就是 scheduleUpdateOnFiber 方法所做的事了。

function scheduleUpdateOnFiber(fiber, lane, eventTime) {
  // 去除了一些无关逻辑
  // 检测更新次数的,默认超过50次就会报错
  checkForNestedUpdates();
  // 这里就是从触发更新的fiber节点遍历到根节点
  var root = markUpdateLaneFromFiberToRoot(fiber, lane);

  // 获取优先级
  var priorityLevel = getCurrentPriorityLevel();
  // 如果是同步执行下面的,否则执行else
  // 我们的例子执行的是if条件下的语句
  if (lane === SyncLane) {
    if ((executionContext & LegacyUnbatchedContext) !== NoContext && 
    (executionContext & (RenderContext | CommitContext)) === NoContext) {
      performSyncWorkOnRoot(root);
    } else {
      // 这里就是回到render阶段的关键
      ensureRootIsScheduled(root, eventTime);
      schedulePendingInteractions(root, lane);
    }
  } else {
    if ((executionContext & DiscreteEventContext) !== NoContext && (
    priorityLevel === UserBlockingPriority$2 || priorityLevel === ImmediatePriority$1)) {
      if (rootsWithPendingDiscreteUpdates === null) {
        rootsWithPendingDiscreteUpdates = new Set([root]);
      } else {
        rootsWithPendingDiscreteUpdates.add(root);
      }
    }
    ensureRootIsScheduled(root, eventTime);
    schedulePendingInteractions(root, lane);
  }
}

这里就不在继续深入分析 React 是如何调度执行 render 阶段的入口方法的。暂时只需要了解在进入 ensureRootIsScheduled 方法后,里面有这么一句来调度入口方法的:

scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));

从这个方法可以联想到 setState 是同步调用还是异步调用。一般情况下 setState 就是异步调用的,这里 render 入口方法被当做回调函数传入了 schedule 方法中。但是当 setState 被放入了 setTimeout 或者是原生的事件回调函数中,则又变成了同步。这是因为这些回调打破了 schedule 的调度,变成了同步执行,至于其中具体细节有机会再去分析。

更新的整体流程就是如此

react-update-process.png

看完了整体的更新流程,接下来进入更新阶段的生命周期来看看。

function updateClassInstance(
  current: Fiber,
  workInProgress: Fiber,
  ctor: any,
  newProps: any,
  renderLanes: Lanes,
): boolean {
  // 获取更新组件实例
  const instance = workInProgress.stateNode;
  // 从current树种克隆一个Update更新队列
  cloneUpdateQueue(current, workInProgress);

  const unresolvedOldProps = workInProgress.memoizedProps;
  const oldProps =
    workInProgress.type === workInProgress.elementType
      ? unresolvedOldProps
      : resolveDefaultProps(workInProgress.type, unresolvedOldProps);
  instance.props = oldProps;
  const unresolvedNewProps = workInProgress.pendingProps;

  const getDerivedStateFromProps = ctor.getDerivedStateFromProps;
  // 判断是否存在新的生命周期方法,也就是getDerivedStateFromProps和getSnapshotBeforeUpdate
  const hasNewLifecycles =
    typeof getDerivedStateFromProps === 'function' ||
    typeof instance.getSnapshotBeforeUpdate === 'function';

  // 如果没有新的生命周期方法,就调用componentWillReceiveProps方法
  if (
    !hasNewLifecycles &&
    (typeof instance.UNSAFE_componentWillReceiveProps === 'function' ||
      typeof instance.componentWillReceiveProps === 'function')
  ) {
    callComponentWillReceiveProps(
        workInProgress,
        instance,
        newProps,
        nextContext,
      );
  }

  resetHasForceUpdateBeforeProcessing();

  const oldState = workInProgress.memoizedState;
  let newState = (instance.state = oldState);
  // 这里是执行state和props更新的地方
  processUpdateQueue(workInProgress, newProps, instance, renderLanes);
  newState = workInProgress.memoizedState;

  // 调用getDerivedStateFromProps方法
  if (typeof getDerivedStateFromProps === 'function') {
    applyDerivedStateFromProps(
      workInProgress,
      ctor,
      getDerivedStateFromProps,
      newProps,
    );
    newState = workInProgress.memoizedState;
  }

  // 调用shouldComponentUpdate
  const shouldUpdate =
    checkHasForceUpdateAfterProcessing() ||
    checkShouldComponentUpdate(
      workInProgress,
      ctor,
      oldProps,
      newProps,
      oldState,
      newState,
      nextContext,
    );

  if (shouldUpdate) {
    // 这里也是调用老的生命周期的方法componentWillUpdate和UNSAFE_componentWillUpdate
    if (
      !hasNewLifecycles &&
      (typeof instance.UNSAFE_componentWillUpdate === 'function' ||
        typeof instance.componentWillUpdate === 'function')
    ) {
      if (typeof instance.componentWillUpdate === 'function') {
        instance.componentWillUpdate(newProps, newState, nextContext);
      }
      if (typeof instance.UNSAFE_componentWillUpdate === 'function') {
        instance.UNSAFE_componentWillUpdate(newProps, newState, nextContext);
      }
    }
  } else {
    workInProgress.memoizedProps = newProps;
    workInProgress.memoizedState = newState;
  }

  // 即使shouldComponentUpdate方法返回false,这里也会重新赋值
  instance.props = newProps;
  instance.state = newState;
  instance.context = nextContext;

  return shouldUpdate;
}

updateClassInstance 方法只有在更新阶段才会被调用,而判断是否在更新阶段,根据 React 的双缓存架构,其实就是判断 current 指针是否为空,为空则表示第一次挂载,否则就是更新阶段。并且这个方法也包含了更新阶段的 render 过程中的生命周期方法。

function applyDerivedStateFromProps(workInProgress, ctor, getDerivedStateFromProps, nextProps) {
  var prevState = workInProgress.memoizedState;

  {
    if ( workInProgress.mode & StrictMode) {
      disableLogs();

      try {
        // 这里这个方法是被调用了两次的,第一次是react用来检测副作用的
        getDerivedStateFromProps(nextProps, prevState);
      } finally {
        // 这里log不会被打印出来
        reenableLogs();
      }
    }
  }

  var partialState = getDerivedStateFromProps(nextProps, prevState);

  // 合并了state
  var memoizedState = partialState === null || partialState === undefined ? prevState : _assign({}, prevState, partialState);
  workInProgress.memoizedState = memoizedState;

  if (workInProgress.lanes === NoLanes) {
    var updateQueue = workInProgress.updateQueue;
    updateQueue.baseState = memoizedState;
  }
}

可以看到 getDerivedStateFromProps 执行中做的工作很简单,就是合并 state,然后将合并的 state 挂到 update 队列上。

紧接着可以看到,后面执行了 shouldComponentUpdate 的判断,那究竟是如何做的呢?先进入这个 checkShouldComponentUpdate 方法中去看看:

function checkShouldComponentUpdate(
  workInProgress,
  ctor,
  oldProps,
  newProps,
  oldState,
  newState,
  nextContext,
) {
  const instance = workInProgress.stateNode;
  if (typeof instance.shouldComponentUpdate === 'function') {
    if (
        debugRenderPhaseSideEffectsForStrictMode &&
        workInProgress.mode & StrictMode
      ) {
        disableLogs();
        try {
          // 多调用了一次检测副作用
          instance.shouldComponentUpdate(newProps, newState, nextContext);
        } finally {
          reenableLogs();
        }
    }
    // 执行了shouldComponentUpdate
    const shouldUpdate = instance.shouldComponentUpdate(
      newProps,
      newState,
      nextContext,
    );

    return shouldUpdate;
  }

  // 如果组件是PureComponent,那么就浅比较props和state来决定返回true还是false
  if (ctor.prototype && ctor.prototype.isPureReactComponent) {
    return (
      !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
    );
  }

  return true;
}

代码也很容易理解,先是判断 shouldComponentUpdate 是否是一个函数,是的话就执行了 shouldComponentUpdate 方法返回执行结果就可以了。其次判断当前组件实例原型是否是 PureComponent,如果是话,进行新旧 props 和新旧 state 的浅比较,返回比较后的结果。最后其他情况下,一律返回 true。

这里需要注意的是,在检测完是否需要更新后,如果需要更新,后面还会执行 componentWillUpdate 的方法

if (typeof instance.componentWillUpdate === 'function') {
    instance.componentWillUpdate(newProps, newState, nextContext);
}
if (typeof instance.UNSAFE_componentWillUpdate === 'function') {
    instance.UNSAFE_componentWillUpdate(newProps, newState, nextContext);
}

到这里就做完了当前的组件的更新工作,接下来就返回下一个工作单元,也就是其子节点,去执行下一个工作单元中的更新。需要注意的是不同的组件类型会执行不同的更新方法,我们的例子中最开始的是一个 App 的 class 组件,App 下面有三个子节点,分别是:

  • img节点
  • Child子组件
  • div节点

img 和 div 节点会走 updateHostComponent 方法,而 Child 组件则是会走和 App 一样的 updateClassComponent。最终两种类型的组件都是会进入 reconcileChildren 方法中,进行 diff 比较,更新 workInProgress 内存中的 Fiber 树,后面就进入了 commit 阶段了。上篇提到的 commit 阶段的执行过程,在beforeMutation 阶段执行了 getSnapshotBeforeUpdate 方法,在 commit 阶段 执行了 componentDidMount 方法。

先从 render 方法看起。这里的 render 执行时机是在执行完 shouldComponentUpdate 方法以后,执行了 finishClassComponent 来获取当前 fiber 的子节点:

// 省略无关逻辑
function finishClassComponent(current, workInProgress, Component, shouldUpdate, hasContext, renderLanes) {
  var didCaptureError = (workInProgress.flags & DidCapture) !== NoFlags;

  // 如果shouldComponentUpdate返回false,并且没有错误,那么会跳过当前fiber的更新,去找子节点是否有更新
  if (!shouldUpdate && !didCaptureError) {
    return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
  }
  
  // 组件实例
  var instance = workInProgress.stateNode;

  var nextChildren;

  if (didCaptureError && typeof Component.getDerivedStateFromError !== 'function') {
    // 这里如果捕获到错误,并且没有定义getDerivedStateFromError方法的话,就放弃组件的更新
    nextChildren = null;
  } else {
    {
      // 执行class组件的render方法
      nextChildren = instance.render();

      if ( workInProgress.mode & StrictMode) {
        disableLogs();

        try {
          instance.render();
        } finally {
          reenableLogs();
        }
      }
    }
  }

  if (current !== null && didCaptureError) {
    forceUnmountCurrentAndReconcile(current, workInProgress, nextChildren, renderLanes);
  } else {
    // diff算法的核心方法,比较同层级的节点
    reconcileChildren(current, workInProgress, nextChildren, renderLanes);
  }

  return workInProgress.child;
}

render 更新逻辑如上所示,值得一提的是在这个方法中会进行著名的 diff 算法,这里我们没有展开,先做一个了解。

上篇我们知道 render 方法结束以后,其实就进入了 commit 阶段,剩下的两个生命周期都是在 commit 阶段执行的。先来一一看看它们做了什么工作。

function commitBeforeMutationLifeCycles(current, finishedWork) {
  switch (finishedWork.tag) {
    case FunctionComponent:
    case ForwardRef:
    case SimpleMemoComponent:
    case Block:
      {
        return;
      }

    case ClassComponent:
      {
        if (finishedWork.flags & Snapshot) {
          if (current !== null) {
            var prevProps = current.memoizedProps;
            var prevState = current.memoizedState;
            var instance = finishedWork.stateNode;

            var snapshot = instance.getSnapshotBeforeUpdate(finishedWork.elementType === finishedWork.type ? prevProps : resolveDefaultProps(finishedWork.type, prevProps), prevState);
            
            // getSnapshotBeforeUpdate必须要有一个返回值
            if (snapshot === undefined) {
                error('%s.getSnapshotBeforeUpdate(): A snapshot value (or null) ' + 'must be returned. You have returned undefined.', getComponentName(finishedWork.type));
              }
            // 把结果赋值给了一个内部变量,后面componentDidUpdate会用到
            instance.__reactInternalSnapshotBeforeUpdate = snapshot;
          }
        }

        return;
      }
  }
}
// 省略无关逻辑
function commitLifeCycles(finishedRoot, current, finishedWork, committedLanes) {
  switch (finishedWork.tag) {
    case FunctionComponent:
    case ForwardRef:
    case SimpleMemoComponent:
    case Block: 
      {
        // 函数式等组件更新的逻辑入口,稍后会提到
        {
          commitHookEffectListMount(Layout | HasEffect, finishedWork);
        }

        schedulePassiveEffects(finishedWork);
        return;
      }

    case ClassComponent:
      {
        var instance = finishedWork.stateNode;

        if (finishedWork.flags & Update) {
          if (current === null) {
            // 挂载阶段
            instance.componentDidMount();
          } else {
            var prevProps = finishedWork.elementType === finishedWork.type ? current.memoizedProps : resolveDefaultProps(finishedWork.type, current.memoizedProps);
            var prevState = current.memoizedState;
            // 更新阶段会调用,传入的第三个参数就是上面getSnapshotBeforeUpdate的返回值
            instance.componentDidUpdate(prevProps, prevState, instance.__reactInternalSnapshotBeforeUpdate);
          }
        } 

        var updateQueue = finishedWork.updateQueue;

        if (updateQueue !== null) {
          commitUpdateQueue(finishedWork, updateQueue, instance);
        }

        return;
      }
  }
}

这里生命周期的逻辑比较简单,而且这里也是下面说到的函数式组件更新逻辑的入口。可以看到执行了 getSnapshotBeforeUpdate 后将返回值保存在一个变量中,然后在 componentDidUpdate 中作为第三个参数传入进去。另外 commitLifeCycles 方法从根开始,所以先执行完 Child 组件的生命周期方法,然后再去执行父组件的方法,完整的更新流程顺序可以看下图:

image.png

到这里可以发现,React 的更新流程是自顶向下的,如果子组件没有做过诸如 shouldComponentUpdate 的判断,无论子组件的 state 和 props 是否有变化,子组件都会重新去 render ,但是这其实没有必要的,因此在实际项目开发中就需要注意子组件是否需要 render,就可以利用 shouldComponentUpdate 去判断是否需要更新,或者是使用 PureComponent 去浅比较 state 和 props 是否更新,来避免多余的 render。

forceUpdate 更新

说完了 setState 的流程,来看看 forceUpdate 的更新。实际上 forceUpdate 的更新是和 setState 基本一样的,可以看到其实现是这样的:

enqueueForceUpdate: function (inst, callback) {
    var fiber = get(inst);
    var eventTime = requestEventTime();
    var lane = requestUpdateLane(fiber);
    var update = createUpdate(eventTime, lane);
    // setState是update.payload,这里update.tag
    // ForceUpdate是一个全局变量,代表更新类型
    // react还有以下几种更新类型UpdateState、ReplaceState、CaptureUpdate
    update.tag = ForceUpdate;

    if (callback !== undefined && callback !== null) {
      {
        warnOnInvalidCallback(callback, 'forceUpdate');
      }

      update.callback = callback;
    }

    enqueueUpdate(fiber, update);
    scheduleUpdateOnFiber(fiber, lane, eventTime);
 }

这里的区别就在于 update 上挂了一个 tag 标签,哪里会用到这个字段呢?其实就在上面提到的 processUpdateQueue 方法中会调用 getStateFromUpdate 方法

function getStateFromUpdate(workInProgress, queue, update, prevState, nextProps, instance) {
  switch (update.tag) {
    // 省略了其他逻辑
    case ForceUpdate:
      {
        hasForceUpdate = true;
        return prevState;
      }
  }

  return prevState;
}

这里这个 hasForceUpdate 变量又是做什么的呢?不知道是否还记得上面执行 shouldComponentUpdate 的方法,里面有这么一句:

var shouldUpdate = checkHasForceUpdateAfterProcessing() || checkShouldComponentUpdate(workInProgress, ctor, oldProps, newProps, oldState, newState, nextContext);

这个 checkHasForceUpdateAfterProcessing 方法实际就是返回上面的 hasForceUpdate 的值。好了,到这里就接上了 setState 的更新流程了,接下来就和 setState 更新是一样的了。

useReducer 更新

首先大家应该都知道,useState 其实就是 useReducer 的一层包装,所以这里就以 useReducer 为例,来说明 hooks 的更新流程。

虽然我们经常类比 hooks 和 class component 的生命周期,但是从概念上说,两者并没有关联,也就是说不是一个东西。生命周期更像是介入整个 React 工作流程的一种插件机制,可以想象成 webpack 的 plugins。但是 hooks 更像是实现 plugins 机制的 tapable,属于更加底层的一种抽象。

这里大家可以参考大佬的解释

好了,现在把上面的 class 组件改写成函数组件:

function reducer(state, action) {
  switch (action.type) {
    case 'ADD':
      return state + 1;
    case 'MINUS':
      return state - 1;
    default:
      return state;
  }
}

function App() {
  const [state, dispatch] = useReducer(reducer, 0);
  const [name, setName] = useState('qiugu');

  const clickHandler = () => {
    dispatch({
      type: 'ADD'
    });
    setName('qgggggggggg');
  };

  return (
    <div className="App">
      <img src={logo} className="App-logo" alt="logo" />
      <Child name={name}/>
      <div>
        <button onClick={clickHandler}>
          count is: {state}
        </button>
      </div>
    </div>
  )
}

改造完成以后,来看看函数组件是什么时候被执行的,可以在函数组件内打个断点来看下调用栈情况:

image.png

可以看到是 renderWithHooks 这个方法中调用了函数组件,然后就来看看 hooks 是如何调用的,再次打个断点在useReducer 上:

// 执行钩子
function useReducer(reducer, initialArg, init) {
  // 这里会检测hooks是否在函数组件外面调用了,如果是的话,则会抛出错误
  var dispatcher = resolveDispatcher();
  // 接着执行dispatcher下的useReducer,所有的hooks都在dispatcher对象下面
  return dispatcher.useReducer(reducer, initialArg, init);
}

dispatcher.useReducer = function(reducer, initialArg, init) {
   currentHookNameInDev = 'useReducer';
   mountHookTypesDev();
   var prevDispatcher = ReactCurrentDispatcher$1.current;
   ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnMountInDEV;

   try {
      return mountReducer(reducer, initialArg, init);
   } finally {
      ReactCurrentDispatcher$1.current = prevDispatcher;
   }
}

function mountReducer(reducer, initialArg, init) {
  var hook = mountWorkInProgressHook();
  var initialState;

  if (init !== undefined) {
    initialState = init(initialArg);
  } else {
    initialState = initialArg;
  }

  hook.memoizedState = hook.baseState = initialState;
  var queue = hook.queue = {
    pending: null,
    dispatch: null,
    lastRenderedReducer: reducer,
    lastRenderedState: initialState
  };
  // 这里绑定了更新的方法
  var dispatch = queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber$1, queue);
  return [hook.memoizedState, dispatch];
}

function mountWorkInProgressHook() {
  // hook的数据结构
  var hook = {
    memoizedState: null,
    baseState: null,
    baseQueue: null,
    queue: null,
    next: null
  };
  
  // 这里用了一个全局变量workInProgressHook来保存内存中当前执行的hook
  if (workInProgressHook === null) {
    // 这是链表中的第一个钩子
    currentlyRenderingFiber$1.memoizedState = workInProgressHook = hook;
  } else {
    // 不为空的话,则把当前的hooks加到链表末尾
    workInProgressHook = workInProgressHook.next = hook;
  }

  return workInProgressHook;
}

上面代码的注释已经说明了执行过程,这里还是再简单说一下,执行 hooks 的时候,实际所有的 hooks 存在了一个 ReactCurrentDispatcher 的对象上,初始的时候这个对象长这样:

const ReactCurrentDispatcher = {
  current: null,
};

所以如果在函数外面调用 hooks 的时候,得到的结果为 null,检测出你在函数外面使用 hooks 的情况。而在函数内调用 hooks 的时候,会赋值给这个对象的 current 属性,这样调用 resolveDispatcher 方法得到的结果就是一个所有 hooks 的对象。

然后执行 mountReducer,里面的 mountWorkInProgressHook 方法会将 hooks 的连接起来形成一个链表结构,最后就返回我们看到的[state, setXXX]这样的结构,当点击setXXX的时候,实际就是调用的 dispatchAction 这个方法,也是 hooks 的状态更新流程。

function dispatchAction(fiber, queue, action) {
  var eventTime = requestEventTime();
  var lane = requestUpdateLane(fiber);
  // update结构,类比上面的setState的结构
  var update = {
    lane: lane,
    action: action,
    eagerReducer: null,
    eagerState: null,
    next: null
  };

  var pending = queue.pending;

  if (pending === null) {
    // 第一次更新,创建了一个循环链表
    update.next = update;
  } else {
    update.next = pending.next;
    pending.next = update;
  }

  queue.pending = update;
  var alternate = fiber.alternate;

  if (fiber === currentlyRenderingFiber$1 || alternate !== null && alternate === currentlyRenderingFiber$1) {
    // 省略掉
  } else {
    if (fiber.lanes === NoLanes && (alternate === null || alternate.lanes === NoLanes)) {
      var lastRenderedReducer = queue.lastRenderedReducer;

      if (lastRenderedReducer !== null) {
        var prevDispatcher;

        {
          prevDispatcher = ReactCurrentDispatcher$1.current;
          ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
        }

        try {
          // 拿到初始状态,demo中给的状态是0
          var currentState = queue.lastRenderedState;
          // lastRenderedReducer就是我们定义热reducer方法
          var eagerState = lastRenderedReducer(currentState, action);

          update.eagerReducer = lastRenderedReducer;
          update.eagerState = eagerState;
          
          // 这里就是使用Object.is来判断前后状态是否相等,如果不变的话,则不进行更新
          if (objectIs(eagerState, currentState)) {
            return;
          }
        } catch (error) {
        } finally {
          {
            ReactCurrentDispatcher$1.current = prevDispatcher;
          }
        }
      }
    }

    // 这里和setState更新时进入的方法是一样的
    scheduleUpdateOnFiber(fiber, lane, eventTime);
  }
}

这里 dispatchAction 的更新其实也和 setState 是类似的,也同样构造了一个循环链表的结构。这里有一点就是如果更新的状态前后没有变化的话,直接就 return 掉了,并不会进行接下来的更新,因此效率也是会更高一点。

到这里 hooks 的更新流程就说完了,还有 useEffect 没有提到,因为其涉及到了 React 的调度模块,关于 React 如何调度的,这里我自己也没有完全明白,所以点到为止。看完了这些以后,对比一下 class 组件,很容易就可以得到下面这些结论:

  • 函数组件没有实例,所以不需要保存组件实例,节省了内存空间
  • 函数组件如 useReducer、useState 之类的更新,如果前后状态不变,是不会继续比较 render。但是 class 组件则不会,无论状态是否改变,只要调用了 setState 就会重新去 render。

卸载阶段

最后看下卸载阶段的生命周期方法的执行,我们来修改下 class 组件的 render 方法:

  render() {
    console.log('father render');
    return (
      <div className="App">
        <img src={logo} className="App-logo" alt="logo" />
        { this.state.count % 2 === 0 && <Child name={this.state.name}/> }
        <div>
          <button onClick={this.setCount}>
            count is: {this.state.count}
          </button>
        </div>
      </div>
    )
  }

当 count 的 state 为奇数时移除子组件,然后来观察生命周期的执行情况

image.png

点击按钮的时候,触发了子组件的 componentWillUnmount 方法,这个时候在子组件的方法中打个断点,看看子组件是如何触发卸载的:

image.png

看过上篇的同学应该知道,点击按钮发生更新,先进行 render 阶段对比好差异以后,进行 commit 提交,上面的 commitMutationEffect 就是提交改变阶段。当 React 遍历 effect 副作用链表时,发现子组件被移除了,于是触发 commitDeletion 来删除子组件。并且 commitDeletion 还将 current 树和 workInProgress 树中的一些副作用属性属性全部删除了:

  fiber.alternate = null;
  fiber.child = null;
  fiber.dependencies = null;
  fiber.firstEffect = null;
  fiber.lastEffect = null;
  fiber.memoizedProps = null;
  fiber.memoizedState = null;
  fiber.pendingProps = null;
  fiber.return = null;
  fiber.updateQueue = null;

然后继续删除 ref 的引用,最后则是调用 componentWillUnmount 的回调方法:

var callComponentWillUnmountWithTimer = function (current, instance) {
  instance.props = current.memoizedProps;
  instance.state = current.memoizedState;

  {
    instance.componentWillUnmount();
  }
};

一点想法

到此,整个生命周期阶段都过了一遍,还有很多没有细说的地方,留着自己去分析了。最后给我的感受就是,生命周期的方法确实非常多,而且名字也很难记,不如 hooks 来的清爽,还有什么 this 指向问题啦,逻辑无法复用啊,等等问题,好像都在表明 class 组件不是个好“东西”,生命周期更是一堆坑。

但是回过头来想想,学习 React 的时候,我们正是从生命周期开始学习,知道了 React 先是构造函数,然后再去 render,更新的时候,先是 render 然后 componentDidMount。卸载的时候执行了 componentWillUnmount。正是这些让我们对 React 组件的一生有了一个直观的认知。hooks 也确实开启了一种全新的使用方式,但是生命周期总是贯穿其中,正是应了那句生命周期虐我千百遍,我依然待之如初恋

最后不论是 React ,或者是其他框架,甚至是任何软件,都是有其生命周期的,学好生命周期也是我们深入认识其本质的第一步。