likes
comments
collection
share

一文带你入门最流行的react框架源码与架构

作者站长头像
站长
· 阅读数 12

练习demo:build your own react

pomb.us/build-your-…

相关文档

react.docschina.org/docs/introd…

reactjs.org/blog/2015/1…

musicfe.dev/react-fiber…

react.iamkasong.com/

github.com/facebook/re…

React代码为什么要这么写?

相信下面的代码,我们用过react的人,都会很熟悉,但是否有思考过,我们为什么要经常去import React from 'react', 不写就会报错,并且为什么在JSX文件里面写HTML标签,它就能渲染出来呢

import ReactDOM from 'react-dom'; 
import React from 'react'; 
function App(){ 
  return <div>app text</div> 
} 
ReactDOM.render(<App title='app title' />, document.getElementById('#root')) 

我们平时写的代码,叫做JSX,JSX会被babel 编译后才可以运行

"use strict"; 
 
var _reactDom = _interopRequireDefault(require("react-dom")); 
 
var _react = _interopRequireDefault(require("react")); 
 
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 
 
function App() { 
  return /*#__PURE__*/_react.default.createElement("div", null, "app text"); 
} 
 
_reactDom.default.render( /*#__PURE__*/_react.default.createElement(App, { 
  title: "app title" 
}), document.getElementById('#root')); 

我们的JSX代码,最终被转换为 React.createElement函数创建的element,也就是常说的 ReactElement对象

这时候,我们能回答为什么在写JSX的时候,要写import React from 'react',因为用到了React的方法,所以我们要先把React给引入进来,没有import React from 'react'; 会报错未找到变量。

React.createElement

  1. React.createElement(type, config, children) 的入参只有三个
    1. type: 对于普通标签,那么会传入该标签的tagName,对于函数组件跟class组件则是该函数
    2. config: 传给标签或者组件的 props属性
    3. children: 普通文本(string/number)或者使用React.createElement创建的ReactElement对象
  1. React.createElement 最终调用了ReactElement方法,返回一个 $$typeof属性为 REACT_ELEMENT_TYPE(常量)的对象,我们也称为reactElement对象, 传入的children会保存为props.children,其中 key这个属性,会被作为react保留的属性,后面被专门用来判断是否复用fiber的, 还有 ref 用来保存react实现的相关引用: class instance, dom, hook ref
const ReactElement = function(type, key, ref, self, source, owner, props) { 
  const element = { 
    // This tag allows us to uniquely identify this as a React Element 
    $$typeof: REACT_ELEMENT_TYPE, 
    // Built-in properties that belong on the element 
    type: type, 
    key: key, 
    ref: ref, 
    props: props, 
    // Record the component responsible for creating this element. 
    _owner: owner, 
  }; 
 
  return element; 
}; 

React17的架构

  • Scheduler(调度器)—— 调度任务的优先级,高优任务优先进入Reconciler, 这一块是单独的库,模拟了requestIdleCallback 的实现,从而实现Reconciler的时间片调度。 这部分也抽离了一个单独的包,叫做 scheduler, 目前正在逐渐独立出来,避免跟react代码耦合一起,未来可以单独作为一个时间片调度的npm包。
  • Reconciler(协调器)—— 这一处理阶段称为render阶段,基于fiber架构的reconciler,负责构建fiber树并追踪副作用,这一步是在内存中进行的,不会影响到实际上的页面,因此实现了异步可中断的更新。 这一阶段被抽离出来一个单独的包: react-reconciler, 可以应用于各个平台【web ,RN ,node】,只需要renderer 提供对应的渲染,更新,创建视图等符合规范的接口
  • Renderer(渲染器)—— 这一处理阶段称为commit阶段,负责将变化的组件渲染到页面上(对应不同的宿主环境,如RN,web,node )跟执行钩子函数。

一文带你入门最流行的react框架源码与架构

Fiber是什么

    1. 从架构来讲,如果说进程是资源分配的最小单位,线程是CPU调度的最小单位,那么Fiber是 React 进行更新的 最小单位,整个Reconciler的工作,都是围绕Fiber的构建与应用更新展开
    2. 作为静态的数据结构来说,每个Fiber节点对应一个React element,保存了该组件的类型(函数组件/类组件/原生组件...)、对应的DOM节点等信息。
    3. 作为动态的工作单元来说,每个Fiber节点保存了本次更新中该组件改变的状态、要执行的工作(需要被删除/被插入页面中/被更新...)
    4. Fiber的数据结构以及其含义
function FiberNode( 
  tag: WorkTag, 
  pendingProps: mixed, 
  key: null | string, 
  mode: TypeOfMode, 
) { 
  // Instance 
  this.tag = tag; // 表明是ClassComponent fiber ,FunctionComponent fiber ,HostComponent fiber 
  this.key = key; // reactElement的key属性 
  this.elementType = null; // 保存reactElement.type 
  this.type = null;  // 一般情况下跟elementType相等,在某些热更新处理环境可能不等 
  this.stateNode = null;  
  // 对于ClassComponent fiber 来说,保存的是instance 
  // 对于Host类型的节点,保存的是所挂载的dom 
 
  this.return = null; // 父fiber 
  this.child = null; // 子fiber 
  this.sibling = null; // 相邻fiber 
  this.index = 0; // 在兄弟fiber中的位置 
 
  this.ref = null;  
 
  this.pendingProps = pendingProps;// 更新时reactElement的props 
  this.memoizedProps = null; // 更新后的props,用来与更新时的props做对比 
  this.updateQueue = null; // 更新队列 
  this.memoizedState = null; // 更新后的state, 新的state基于state进行update计算产生 
  this.dependencies = null; 
 
  this.mode = mode; 
 
  // Effects 
  this.flags = NoFlags; // 该fiber的副作用 
  this.subtreeFlags = NoFlags; // 子fiber的副作用 
  this.deletions = null; // 删除的子fiber 
 
  this.lanes = NoLanes; // 更新优先级 
  this.childLanes = NoLanes; // 子fiber更新优先级 
  this.alternate = null; // alternate fiber节点,用于构建双缓存fiber树 
 
} 

React fiber架构的双缓存机制

1. 什么是双缓存?

显示器在绘制的时候,是连续的,没有卡顿中断的,也是因为应用了类似双缓存机制,如果是直接清除当前帧,然后在当前的画面进行下一帧的绘画,从清除当前页面到下一帧画面绘制完成,肯定会有延迟感,因此通过在内存中,提前绘制下一帧的画面,然后直接替换当前一帧,达到无缝衔接的目的。

2. Fiber的双缓存是什么?

React 在构建的整个应用的时候,会生成两棵Fiber树,两棵Fiber树是可以互换的,当前应用的根节点,渲染出当前页面视图的树是current Fiber树,FiberRootNode 的current指向它, 正在内存中构建下一次渲染视图的,是workInProgress Fiber树。

当页面触发更新,会在内存中构建一棵新的Fiber树,渲染完成这棵fiber树到视图之后,FiberRootNode的current 会指向这棵树

3. Fiber 双缓存树是如何构建跟工作的?

React组件初次构建/触发更新产生新的reactElement,reconciler将其构建成新的fiber树,找出对比需要更新的fiber链表,被称为effectList, 结合renderer完成视图更新与渲染

Reconciler 与 Renderer

此处为语雀内容卡片,点击链接查看:www.yuque.com/guang-nzhrr…

React如何触发更新

关于react的fiber架构,基本上讲解完毕,那么如何跟我们日常使用串起来呢,回忆我们最经常使用的触发更新或者渲染视图的代码

// 第一次视图渲染 
ReactDOM.render(<App/>,document.getElementById('root')) 
// class component 触发更新 
this.setState(payload,callback); 
this.forceUpdate(); 
// function component 触发更新 
const [state,setState] = useState(); 
const [state, dispatch] = useReducer(reducer, initialState); 

这些代码在react实现底层,都会创建 update对象,调用enqueueUpdate将update对象加入updateQueue链表, 执行scheduleUpdateOnFiber调度更新 ,会调用markUpdateLaneFromFiberToRoot 从触发更新的fiber节点一路向上标记childRenderLanes,并找到其rootFiber 并返回, 同时根据不同的模式,会进入同步调用还是优先级调度 performSyncWorkOnRoot/ performConcurrentWorkOnRoot ,从而决定怎么处理我们的render阶段(同步不可中断,还是异步可中断地构建fiber)

function workLoopSync() { 
  // Already timed out, so perform work without checking if we need to yield. 
  while (workInProgress !== null) { 
    performUnitOfWork(workInProgress); 
  } 
} 
 
function workLoopConcurrent() { 
  // Perform work until Scheduler asks us to yield 
  while (workInProgress !== null && !shouldYield()) { 
    performUnitOfWork(workInProgress); 
  } 
} 

render阶段

  • beginWork会创建fiber,调用processUpdateQueue计算state;
  • completeWork会初始化dom实例,并追加副作用,将其添加到effectList

commit阶段,遍历effectList渲染视图,切换fiber树,执行副作用。

触发更新的数据结构

update对象

  1. ClassComponent 跟 ReactDOM.render 使用的update数据结构:
const update: Update<*> = { 
  eventTime, 
  lane, 
  suspenseConfig, 
  tag: UpdateState|ReplaceState|ForceUpdate|CaptureUpdate, 
  payload: null, 
  callback: null, 
  next: null, 
}; 

update.payload 是该更新对象会进行更新的内容

在classComponent 中,payload是this.setState(payload,callback)的第一个参数

在 reactDOM.render 中,payload 是 { element: App组件的reactElement对象 }

updateQueue的数据结构:

fiberRootNode.updateQueue = { 
 baseState: null, // 上一次的state 
 effects: null, // 带有callback的fiber list 
 firstBaseUpdate: null, // 上一次被中断的第一个update   
 lastBaseUpdate: null, // 上一次被中断的updte的最后一个 
 // 假设 A->B->C->D 有这样一个update list, 然后B被中断了, 
 // 那么firsetbaseUpdate就是B,lastBaseUpdate就是D 
 shared: { pending:  update } 
} 
fiber.updateQueue.pending = update
update.payload = { element: { type: App, $$typeof: Symbol('ReactElement') 

effects保存的是 带有callback的 update, 这里对应于 ReactDOM.render的第三个参数:传入的函数

这里调用了enqueueUpdate(fiber: Fiber, update: Update) 将update加入到updateQueue,updateQueue.shared.pending保存的是当前fiber产生的update,这是一个环状链表。

ReactDOM.render/setState/this.setState是可以被多次调用的,假设ReactDOM.render先产生了updateA,后产生 updateB, shared.pending永远指向的是最后一个update, 那么 updateQueue.shared.pending = updateB, updateB -> updateA, updateA -> updateB , 这么做的原因是,想拿到最先开始的update 进行处理,就会很方便,只需要访问 fiber.updateQueue.shared.pending.next

ReactDOM.render

对于我们最最最熟悉的一段代码,其实做了很多事情

function App() { 
  return <div onClick={plus}>this is app text </div>; 
} 
ReactDOM.render(<App />, document.getElementById("root")); 

一文带你入门最流行的react框架源码与架构

  1. ReactDOM.render 调用并返回legacyRenderSubtreeIntoContainer的结果
  2. legacyRenderSubtreeIntoContainer,创建了应用根节点fiberRootNode跟rootFiber,完成初始化工作,并调用 updateContainer
// 设置了unbatchUpdateContext 
unbatchedUpdates(() => { 
  updateContainer(children, fiberRoot, parentComponent, callback); 
}); 

fiberRootNode.current = rootFiber 
rootFiber.stateNode = fiberRootNode 
fiberRootNode.containerInfo = container【document.getElementById("root")】 
  1. updateContainer(children, fiberRootNode, parentComponent, callback);

创建了update,初始化updateQueue,enqueueUpdate,scheduleUpdateOnFiber 调度更新,markUpdateLaneFromFiberToRoot 找到当前触发更新fiber的fiberRootNode节点,

  1. 根据当前传递的const lane = requestUpdateLane(current);, 决定进入render阶段的入口

ReactDOM.render 是legacy模式, 因此创建的 rootFiber,通过requestUpdateLane获得的是 SyncLane的优先级

一文带你入门最流行的react框架源码与架构

因此最终进入的是performSyncWorkOnRoot(fiberRootNode) -> workLoopSync递归调用performUnitOfWork -> performUnitOfWork构建fiber树 -> commitRoot渲染更新视图

function workLoopSync() { 
  // Already timed out, so perform work without checking if we need to yield. 
  while (workInProgress !== null) { 
    performUnitOfWork(workInProgress); 
  } 
} 

Class Component instance setState

对于class组件

  1. 使用this.setState 进行状态更新, 实际调用的是this.updater.enqueueSetState -> 创建update -> enqueueUpdate -> scheduleUpdateOnFiber 调度更新
  2. 使用this.forceUpdate 强制更新,实际调用的是 this.updater.enqueueForceUpdate(this, callback, 'forceUpdate');
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.forceUpdate = function(callback) { 
  this.updater.enqueueForceUpdate(this, callback, 'forceUpdate'); 
}; 
 
Component.prototype.setState = function(partialState, callback) { 
  this.updater.enqueueSetState(this, partialState, callback, 'setState'); 
}; 

updater在初始化的时候, 被设置为默认值, 在开发环境,如果updater没有被加载就被调用了,那么会控制台报错,例如你这么做了:

class ClassChild extends React.Component { 
  constructor() { 
    super(); 
    this.setState({ a: 1 }); 
  } 
 
  render() { 
    return <header>ClassChild</header>; 
  } 
} 

一文带你入门最流行的react框架源码与架构

这个是默认的ReactNoopUpdateQueue

const didWarnStateUpdateForUnmountedComponent = {}; 
function warnNoop(publicInstance, callerName) { 
  if (__DEV__) { 
    const constructor = publicInstance.constructor; 
    const componentName = 
      (constructor && (constructor.displayName || constructor.name)) || 
      'ReactClass'; 
    const warningKey = `${componentName}.${callerName}`; 
    if (didWarnStateUpdateForUnmountedComponent[warningKey]) { 
      return; 
    } 
    console.error( 
      "Can't call %s on a component that is not yet mounted. " + 
        'This is a no-op, but it might indicate a bug in your application. ' + 
        'Instead, assign to `this.state` directly or define a `state = {};` ' + 
        'class property with the desired state in the %s component.', 
      callerName, 
      componentName, 
    ); 
    didWarnStateUpdateForUnmountedComponent[warningKey] = true; 
  } 
} 
 
/** 
 * This is the abstract API for an update queue. 
 */ 
const ReactNoopUpdateQueue = { 
  isMounted: function(publicInstance) { 
    return false; 
  }, 
 
  enqueueForceUpdate: function(publicInstance, callback, callerName) { 
    warnNoop(publicInstance, 'forceUpdate'); 
  }, 
 
  enqueueReplaceState: function( 
    publicInstance, 
    completeState, 
    callback, 
    callerName, 
  ) { 
    warnNoop(publicInstance, 'replaceState'); 
  }, 
 
  enqueueSetState: function( 
    publicInstance, 
    partialState, 
    callback, 
    callerName, 
  ) { 
    warnNoop(publicInstance, 'setState'); 
  }, 
}; 
 
export default ReactNoopUpdateQueue; 

真正的updater是在render阶段才被设置的

在 ReactDOM.render执行的时候,beginWork 会对第一次初始化的classComponent fiber节点调用constructClassInstance 构建实例, 并且设置真正的updater: classComponentUpdater

const classComponentUpdater = { 
  isMounted, 
  enqueueSetState(inst, payload, callback) { 
    const fiber = getInstance(inst); 
    const eventTime = requestEventTime(); 
    const lane = requestUpdateLane(fiber); 
 
    const update = createUpdate(eventTime, lane); 
    update.payload = payload; 
    if (callback !== undefined && callback !== null) { 
      update.callback = callback; 
    } 
 
    enqueueUpdate(fiber, update); 
    scheduleUpdateOnFiber(fiber, lane, eventTime); 
     
  }, 
  enqueueReplaceState(inst, payload, callback) { 
    const fiber = getInstance(inst); 
    const eventTime = requestEventTime(); 
    const lane = requestUpdateLane(fiber); 
 
    const update = createUpdate(eventTime, lane); 
    update.tag = ReplaceState; 
    update.payload = payload; 
 
    if (callback !== undefined && callback !== null) { 
      update.callback = callback; 
    } 
 
    enqueueUpdate(fiber, update); 
    scheduleUpdateOnFiber(fiber, lane, eventTime); 
 
  }, 
   
  enqueueForceUpdate(inst, callback) { 
    const fiber = getInstance(inst); 
    const eventTime = requestEventTime(); 
    const lane = requestUpdateLane(fiber); 
 
    const update = createUpdate(eventTime, lane); 
    update.tag = ForceUpdate; 
 
    if (callback !== undefined && callback !== null) { 
      update.callback = callback; 
    } 
 
    enqueueUpdate(fiber, update); 
    scheduleUpdateOnFiber(fiber, lane, eventTime); 
  }, 
}; 

Function Component useState & useReducer

对于 Function Component,触发状态更新有两种hooks: useState跟useReducer

export function useState<S>( 
  initialState: (() => S) | S, 
): [S, Dispatch<BasicStateAction<S>>] { 
  const dispatcher = resolveDispatcher(); 
  return dispatcher.useState(initialState); 
} 
 
export function useReducer<S, I, A>( 
  reducer: (S, A) => S, 
  initialArg: I, 
  init?: I => S, 
): [S, Dispatch<A>] { 
  const dispatcher = resolveDispatcher(); 
  return dispatcher.useReducer(reducer, initialArg, init); 
} 

我们直接看react的代码,无论是useState还是useReducer,都只能看到是调用了 dispatcher对象的方法,而resolveDispatcher 也只是从ReactCurrentDispatcher 获取当前的current对象。

对于useState 跟 useReducer 这两个Hook,有两个阶段会计算state

声明阶段:即在render阶段App被调用时,会依次执行useReducer与useState方法, 根据update 重新计算 state

调用阶段: 即点击按钮后,dispatch或updateNum被调用时,调度更新,重新触发声明阶段,根据update计算 state

不同阶段的dispacher

function resolveDispatcher() { 
  const dispatcher = ReactCurrentDispatcher.current; 
  return dispatcher; 
} 

因此我们肯定断定的是在某个时刻ReactCurrentDispatcher.current 被设置为真正的dispatcher,这点跟class component 实例 延迟updater设置一样。

useReducer在mount跟update时候执行的hook,是两个不同的函数,因为在不同的上下文中 ReactCurrentDispatcher 设置的dispatcher是不一样的

那么dispacher是在什么时候才被真正设置的呢

以App组件为例, 代码如下:

const reducer = (state, action) => { 
  switch (action) { 
    case "add": 
      return state + 1; 
    case "minus": 
      return state - 1; 
    default: 
      throw new Error(); 
  } 
}; 
function App() { 
  const [num, setNum] = useState(0); 
  const [num2, dispatch] = useReducer(reducer, 1); 
 
  const plus = useCallback(() => { 
    setNum(num + 1); 
    dispatch("add"); 
  }, [num, dispatch]); 
 
  return ( 
    <div onClick={plus}> 
      this is app text {num}-{num2} 
    </div> 
  ); 
} 
ReactDOM.render(<App />, document.getElementById("root")); 

debugger断点,看到主要走到这里:renderWithHooks

一文带你入门最流行的react框架源码与架构

在 render阶段,beginWork(递)的时候,这个时候会根据fiber.tag 是IndeterminateComponent(所有function component fiber在第一次被创建的时候,tag 都是为IndeterminateComponent),调用renderWithHooks,最终会执行 App 这个function component

renderWithHooks具体的代码如下:

export function renderWithHooks<Props, SecondArg>( 
  current: Fiber | null, 
  workInProgress: Fiber, 
  Component: (p: Props, arg: SecondArg) => any, 
  props: Props, 
  secondArg: SecondArg, 
  nextRenderLanes: Lanes, 
): any { 
  renderLanes = nextRenderLanes; 
  // 设置当前renderingFiber 为传入的workInProgress, 后面的函数会用到这个变量进行判断 
  currentlyRenderingFiber = workInProgress; 
  // memoizedState 存的就是hooks的链表 
  workInProgress.memoizedState = null; 
  workInProgress.updateQueue = null; 
  workInProgress.lanes = NoLanes; 
 
  // Using memoizedState to differentiate between mount/update only works if at least one stateful hook is used. 
  // Non-stateful hooks (e.g. context) don't get added to memoizedState, 
  // so memoizedState would be null during updates and mounts. 
    ReactCurrentDispatcher.current = 
      current === null || current.memoizedState === null 
        ? HooksDispatcherOnMount 
        : HooksDispatcherOnUpdate; 
 
  let children = Component(props, secondArg); 
 
  // Check if there was a render phase update 
  if (didScheduleRenderPhaseUpdateDuringThisPass) { 
    // Keep rendering in a loop for as long as render phase updates continue to 
    // be scheduled. Use a counter to prevent infinite loops. 
    let numberOfReRenders: number = 0; 
    do { 
      didScheduleRenderPhaseUpdateDuringThisPass = false; 
 
      numberOfReRenders += 1; 
 
      // Start over from the beginning of the list 
      currentHook = null; 
      workInProgressHook = null; 
 
      workInProgress.updateQueue = null; 
 
      ReactCurrentDispatcher.current = HooksDispatcherOnRerender; 
 
      children = Component(props, secondArg); 
    } while (didScheduleRenderPhaseUpdateDuringThisPass); 
  } 
 
  // We can assume the previous dispatcher is always this one, since we set it 
  // at the beginning of the render phase and there's no re-entrancy. 
  ReactCurrentDispatcher.current = ContextOnlyDispatcher; 
 
  // This check uses currentHook so that it works the same in DEV and prod bundles. 
  // hookTypesDev could catch more cases (e.g. context) but only in DEV bundles. 
  const didRenderTooFewHooks = 
    currentHook !== null && currentHook.next !== null; 
 
  renderLanes = NoLanes; 
  currentlyRenderingFiber = (null: any); 
 
  currentHook = null; 
  workInProgressHook = null; 
 
  didScheduleRenderPhaseUpdate = false; 
 
  return children; 
} 

在执行App之前,会根据workInProgress Fiber对应的current Fiber或者 current Fiber.memoizedState 是否存在,判断处于mount还是update,从而设置不同的dispatcher:HooksDispatcherOnMount 跟HooksDispatcherOnUpdate

useState在不同的dispatcher实现方式是不一样的:

const HooksDispatcherOnMount = { 
  readContext, 
  useCallback: mountCallback, 
  useContext: readContext, 
  useEffect: mountEffect, 
  useImperativeHandle: mountImperativeHandle, 
  useLayoutEffect: mountLayoutEffect, 
  useMemo: mountMemo, 
  useReducer: mountReducer, 
  useRef: mountRef, 
  useState: mountState, 
  useDebugValue: mountDebugValue, 
  useDeferredValue: mountDeferredValue, 
  useTransition: mountTransition, 
  useMutableSource: mountMutableSource, 
  useOpaqueIdentifier: mountOpaqueIdentifier, 
 
  unstable_isNewReconciler: enableNewReconciler, 
};   
const HooksDispatcherOnUpdate = { 
  readContext, 
  useCallback: updateCallback, 
  useContext: readContext, 
  useEffect: updateEffect, 
  useImperativeHandle: updateImperativeHandle, 
  useLayoutEffect: updateLayoutEffect, 
  useMemo: updateMemo, 
  useReducer: updateReducer, 
  useRef: updateRef, 
  useState: updateState, 
  useDebugValue: updateDebugValue, 
  useDeferredValue: updateDeferredValue, 
  useTransition: updateTransition, 
  useMutableSource: updateMutableSource, 
  useOpaqueIdentifier: updateOpaqueIdentifier, 
  unstable_isNewReconciler: enableNewReconciler, 
}; 

除了 useContext,其他 hook方法都会在mount时,会调用mountWorkInProgressHook,创建hook节点,生成链表,使用 workInProgressHook 来指向当前处理的hook

function mountWorkInProgressHook(): Hook { 
  const hook: Hook = { 
    memoizedState: null, 
    baseState: null, 
    baseQueue: null, 
    queue: null, 
 
    next: null, 
  }; 
 
  if (workInProgressHook === null) { 
    // This is the first hook in the list 
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook; 
  } else { 
    // Append to the end of the list 
    workInProgressHook = workInProgressHook.next = hook; 
  } 
  return workInProgressHook; 
} 

Hook 跟 Fiber 数据结构说明:

对于FunctionComponent Fiber, 其fiber.memoizedState保存的是hook链表

而hook 的数据结构是这样的:

  const hook: Hook = { 
    memoizedState: null, // 更新后的state, 对于useState跟useReducer 则是effect节点 
    baseState: null, // 上一次更新完的state, update基于这个baseState进行计算 
    baseQueue: null, // 上一次没有完成的update 
    queue: null, // 用来保存该hook最新产生的update 
    next: null, // 链接下一个hook 
  }; 

hook也存在一个memoizedState, 用来保存当前hook的值,不同类型的hook保存的值是不一样的

const [state, dispatch] = useReducer(reducer,initialArg,init?)

对于useReducer,memorizedState保存的是最新state的值

const [state, setState] = useState(initState)

对于useState, memoizedState保存的是最新state的值

const callback = useCallback(fn, deps)

对于 useCallback, memoizedState 保存的是[fn, deps]

useEffect(fn, deps)useLayoutEffect(fn,deps)

对于useEffect / useLayoutEffect,hook.memoizedState保存的是 对应的effect 节点(下面Hook章节介绍)

useState

mount 阶段的 useState

function mountState<S>( 
  initialState: (() => S) | S, 
): [S, Dispatch<BasicStateAction<S>>] { 
  // 创建hook节点,并生成hook链表 
  const hook = mountWorkInProgressHook(); 
  if (typeof initialState === 'function') { 
    // $FlowFixMe: Flow doesn't like mixed types 
    initialState = initialState(); 
  } 
  hook.memoizedState = hook.baseState = initialState; 
  // 同时给当前的hook创建updateQueue 
  const queue = (hook.queue = { 
    pending: null, // 保存触发的update 
    dispatch: null, // 绑定了fiber,queue的 disaptchAction函数 
    // 上一次更新时调用的reducer函数,newState = reducer(oldState,action)) 
    lastRenderedReducer: basicStateReducer,  
    // 上一次更新的state 
    lastRenderedState: (initialState: any),  
  }); 
  const dispatch: Dispatch< 
    BasicStateAction<S>, 
  > = (queue.dispatch = (dispatchAction.bind( 
    null, 
    currentlyRenderingFiber, 
    queue, 
  ): any)); 
  return [hook.memoizedState, dispatch]; 
} 

在beginwork中,renderWithHooks 会执行我们的Function component,首次被执行的时候处于mount, useState会调用 mountDispatcher.useState, 创建了hook节点并添加到workInProgressHook链表,同时初始化updateQueue,返回绑定当前fiber,updateQueue参数的dispatchAction函数

function dispatchAction<S, A>( 
  fiber: Fiber, 
  queue: UpdateQueue<S, A>, 
  action: A, 
) { 
  const eventTime = requestEventTime(); 
  const lane = requestUpdateLane(fiber); 
  const update: Update<S, A> = { 
    lane, 
    action, 
    eagerReducer: null, 
    eagerState: null, 
    next: (null: any), 
 }; 
  
 // Append the update to the end of the list. 
  const pending = queue.pending; 
  if (pending === null) { 
    // This is the first update. Create a circular list. 
    update.next = update; 
  } else { 
    update.next = pending.next; 
    pending.next = update; 
  } 
  queue.pending = update; 
 
  const alternate = fiber.alternate; 
  if ( 
    fiber === currentlyRenderingFiber || 
    (alternate !== null && alternate === currentlyRenderingFiber) 
  ) { 
    // This is a render phase update. Stash it in a lazily-created map of 
    // queue -> linked list of updates. After this render pass, we'll restart 
    // and apply the stashed updates on top of the work-in-progress hook. 
    didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true; 
  } else { 
    if ( 
      fiber.lanes === NoLanes && 
      (alternate === null || alternate.lanes === NoLanes) 
    ) { 
      // The queue is currently empty, which means we can eagerly compute the 
      // next state before entering the render phase. If the new state is the 
      // same as the current state, we may be able to bail out entirely. 
      const lastRenderedReducer = queue.lastRenderedReducer; 
      if (lastRenderedReducer !== null) { 
        try { 
          const currentState: S = (queue.lastRenderedState: any); 
          const eagerState = lastRenderedReducer(currentState, action); 
          // Stash the eagerly computed state, and the reducer used to compute 
          // it, on the update object. If the reducer hasn't changed by the 
          // time we enter the render phase, then the eager state can be used 
          // without calling the reducer again. 
          update.eagerReducer = lastRenderedReducer; 
          update.eagerState = eagerState; 
          if (is(eagerState, currentState)) { 
            // Fast path. We can bail out without scheduling React to re-render. 
            // It's still possible that we'll need to rebase this update later, 
            // if the component re-renders for a different reason and by that 
            // time the reducer has changed. 
            return; 
          } 
        } catch (error) { 
          // Suppress the error. It will throw again in the render phase. 
        }  
      } 
    } 
 
    scheduleUpdateOnFiber(fiber, lane, eventTime); 
  } 
} 

useState返回的setState(action)函数, 其实是 dispatchAction(action)函数

dispatchAction 函数根据传递的action创建 update, 并添加到hook.queue.pending, 然后调度scheduleUpdateOnFiber 进行更新, 最终进入render阶段的renderWithHooks, 重新执行Function Component进入useState的声明阶段,重新计算state

在dispatchAction里面有一个优化路径,当前触发 update的fiber没有存在更新的话,并且其将处于render阶段的fiber也没有存在更新。

fiber.lanes === NoLanes && (alternate === null || alternate.lanes === NoLanes),

那么意味着当前创建的update会是update queue上面的第一个update,这一步进行优化,提前计算state,赋值到eargerState, 而不必等到声明阶段去计算state.

同时判断 eagerState 跟 currentState 一致,可以不触发更新

// 在后面renderWithHooks, 会直接使用这次计算的state 
if (update.eagerReducer === reducer) { 
      // If this update was processed eagerly, and its reducer matches the 
      // current reducer, we can use the eagerly computed state. 
      newState = update.eagerState; 
 } 

fiber === currentlyRenderingFiber || (alternate !== null && alternate === currentlyRenderingFiber)

currentlyRenderingFiber即处于render阶段的fiber

触发更新时通过bind预先保存的fiber与currentlyRenderingFiber相等,代表本次更新发生于FunctionComponent对应fiber的render阶段。

例如这种代码:

function App(){ 
  const [num,setNum] = useState(0); 
  if(num < 10){ 
      setNum(num+1); 
  } 
  return <div>{num}</div> 
} 

所以这是一个render阶段触发的更新,需要标记变量didScheduleRenderPhaseUpdate,后续单独处理。

update 阶段的 useState

function updateState<S>( 
  initialState: (() => S) | S, 
): [S, Dispatch<BasicStateAction<S>>] { 
  return updateReducer(basicStateReducer, (initialState: any)); 
} 
 
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S { 
  // $FlowFixMe: Flow doesn't like mixed types 
  return typeof action === 'function' ? action(state) : action; 
} 

调用dispatchAction 重新进入render阶段,调用renderWithHooks,重新执行App,此时使用的dispatcher为 updateDispatcher,

因此 useState 调用的是updateState函数,,其实是调用了 内置 basicStateReducer的updateReducer

function updateReducer<S, I, A>( 
  reducer: (S, A) => S, 
  initialArg: I, 
  init?: I => S, 
): [S, Dispatch<A>] { 
  // 获取到当前fiber节点的memoizedState, 也就是当前的hook链表 
  const hook = updateWorkInProgressHook(); 
  const queue = hook.queue; 
  invariant( 
    queue !== null, 
    'Should have a queue. This is likely a bug in React. Please file an issue.', 
  ); 
  queue.lastRenderedReducer = reducer; 
 
  const current: Hook = (currentHook: any); 
 
  // The last rebase update that is NOT part of the base state. 
  let baseQueue = current.baseQueue; 
 
  // The last pending update that hasn't been processed yet. 
  const pendingQueue = queue.pending; 
  if (pendingQueue !== null) { 
    // We have new updates that haven't been processed yet. 
    // We'll add them to the base queue. 
    if (baseQueue !== null) { 
      // Merge the pending queue and the base queue. 
      const baseFirst = baseQueue.next; 
      const pendingFirst = pendingQueue.next; 
      baseQueue.next = pendingFirst; 
      pendingQueue.next = baseFirst; 
    } 
 
    current.baseQueue = baseQueue = pendingQueue; 
    queue.pending = null; 
  } 
  if (baseQueue !== null) { 
    // We have a queue to process. 
    const first = baseQueue.next; 
    let newState = current.baseState; 
 
    let newBaseState = null; 
    let newBaseQueueFirst = null; 
    let newBaseQueueLast = null; 
    let update = first; 
    do { 
      const updateLane = update.lane; 
      if (!isSubsetOfLanes(renderLanes, updateLane)) { 
        // Priority is insufficient. Skip this update. If this is the first 
        // skipped update, the previous update/state is the new base 
        // update/state. 
        const clone: Update<S, A> = { 
          lane: updateLane, 
          action: update.action, 
          eagerReducer: update.eagerReducer, 
          eagerState: update.eagerState, 
          next: (null: any), 
        }; 
        if (newBaseQueueLast === null) { 
          newBaseQueueFirst = newBaseQueueLast = clone; 
          newBaseState = newState; 
        } else { 
          newBaseQueueLast = newBaseQueueLast.next = clone; 
        } 
        // Update the remaining priority in the queue. 
        // TODO: Don't need to accumulate this. Instead, we can remove 
        // renderLanes from the original lanes. 
        currentlyRenderingFiber.lanes = mergeLanes( 
          currentlyRenderingFiber.lanes, 
          updateLane, 
        ); 
        markSkippedUpdateLanes(updateLane); 
      } else { 
        // This update does have sufficient priority. 
 
        if (newBaseQueueLast !== null) { 
          const clone: Update<S, A> = { 
            // This update is going to be committed so we never want uncommit 
            // it. Using NoLane works because 0 is a subset of all bitmasks, so 
            // this will never be skipped by the check above. 
            lane: NoLane, 
            action: update.action, 
            eagerReducer: update.eagerReducer, 
            eagerState: update.eagerState, 
            next: (null: any), 
          }; 
          newBaseQueueLast = newBaseQueueLast.next = clone; 
        } 
 
        // Process this update. 
        if (update.eagerReducer === reducer) { 
          // If this update was processed eagerly, and its reducer matches the 
          // current reducer, we can use the eagerly computed state. 
          newState = ((update.eagerState: any): S); 
        } else { 
         // useState使用basicStateReducer计算更新 
          const action = update.action; 
          newState = reducer(newState, action); 
        } 
      } 
      update = update.next; 
    } while (update !== null && update !== first); 
 
    if (newBaseQueueLast === null) { 
      newBaseState = newState; 
    } else { 
      newBaseQueueLast.next = (newBaseQueueFirst: any); 
    } 
 
    // Mark that the fiber performed work, but only if the new state is 
    // different from the current state. 
    if (!is(newState, hook.memoizedState)) { 
      markWorkInProgressReceivedUpdate(); 
    } 
 
    hook.memoizedState = newState; 
    hook.baseState = newBaseState; 
    hook.baseQueue = newBaseQueueLast; 
 
    queue.lastRenderedState = newState; 
  } 
 
  const dispatch: Dispatch<A> = (queue.dispatch: any); 
  return [hook.memoizedState, dispatch]; 
} 

useReducer

mount阶段的useReducer

function mountReducer<S, I, A>( 
  reducer: (S, A) => S, 
  initialArg: I, 
  init?: I => S, 
): [S, Dispatch<A>] { 
  const hook = mountWorkInProgressHook(); 
  let initialState; 
  if (init !== undefined) { 
    initialState = init(initialArg); 
  } else { 
    initialState = ((initialArg: any): S); 
  } 
  hook.memoizedState = hook.baseState = initialState; 
  const queue = (hook.queue = { 
    pending: null, 
    dispatch: null, 
    lastRenderedReducer: reducer, 
    lastRenderedState: (initialState: any), 
  }); 
  const dispatch: Dispatch<A> = (queue.dispatch = (dispatchAction.bind( 
    null, 
    currentlyRenderingFiber, 
    queue, 
  ): any)); 
  return [hook.memoizedState, dispatch]; 
} 

执行逻辑基本跟useState 差不多

update阶段的useReducer

update 阶段的useReducer 其实调用的是 updateReducer, 逻辑跟上面updateState一致,只是使用的reducer是我们 useReducer(reducerFn,initArg); 传入的reducerFn函数

而updateState 是内置了 basicUpdateStateReducer 的 useReducer

Hook

useEffect 跟 useLayoutEffect

数据结构

在mount阶段,useEffect 跟 useLayoutEffect调用的都是 mountEffectImpl, 只是传递的fiberFlags 跟hookFlags 不一致而已

function mountEffect( 
  create: () => (() => void) | void, 
  deps: Array<mixed> | void | null, 
): void { 
  return mountEffectImpl( 
    UpdateEffect | PassiveEffect, 
    HookPassive, 
    create, 
    deps, 
  ); 
} 
 
function mountLayoutEffect( 
  create: () => (() => void) | void, 
  deps: Array<mixed> | void | null, 
): void { 
  return mountEffectImpl(UpdateEffect, HookLayout, create, deps); 
} 
 
function mountEffectImpl(fiberFlags, hookFlags, create, deps): void { 
  const hook = mountWorkInProgressHook(); 
  const nextDeps = deps === undefined ? null : deps; 
  currentlyRenderingFiber.flags |= fiberFlags; 
  hook.memoizedState = pushEffect( 
    HookHasEffect | hookFlags, 
    create, 
    undefined, 
    nextDeps, 
  ); 
} 
function pushEffect(tag, create, destroy, deps) { 
  const effect: Effect = { 
    tag, 
    create, 
    destroy, 
    deps, 
    // Circular 
    next: (null: any), 
  }; 
  let componentUpdateQueue: null | FunctionComponentUpdateQueue = (currentlyRenderingFiber.updateQueue: any); 
  if (componentUpdateQueue === null) { 
    componentUpdateQueue = createFunctionComponentUpdateQueue(); 
    currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any); 
    componentUpdateQueue.lastEffect = effect.next = effect; 
  } else { 
    const lastEffect = componentUpdateQueue.lastEffect; 
    if (lastEffect === null) { 
      componentUpdateQueue.lastEffect = effect.next = effect; 
    } else { 
      const firstEffect = lastEffect.next; 
      lastEffect.next = effect; 
      effect.next = firstEffect; 
      componentUpdateQueue.lastEffect = effect; 
    } 
  } 
  return effect; 
} 
  const effect: Effect = { 
    tag, // Passive 表示 useEffect , Layout 表示 useLayoutEffect 
    create, // 传入的fn 
    destroy, // fn() 返回的销毁函数 
    deps, // 依赖项 
    // Circular 
    next: (null: any), 
  }; 
  
export const NoFlags = /*  */ 0b000; 
// Represents whether effect should fire. 
export const HasEffect = /* */ 0b001; 
// Represents the phase in which the effect (not the clean-up) fires. 
export const Layout = /*    */ 0b010; 
export const Passive = /*   */ 0b100; 
update时候的 useEffect 跟 useLayoutEffect 
function updateEffect( 
  create: () => (() => void) | void, 
  deps: Array<mixed> | void | null, 
): void { 
  return updateEffectImpl( 
    UpdateEffect | PassiveEffect, 
    HookPassive, 
    create, 
    deps, 
  ); 
} 
 
function updateLayoutEffect( 
  create: () => (() => void) | void, 
  deps: Array<mixed> | void | null, 
): void { 
  return updateEffectImpl(UpdateEffect, HookLayout, create, deps); 
} 
 
function updateEffectImpl(fiberFlags, hookFlags, create, deps): void { 
  const hook = updateWorkInProgressHook(); 
  const nextDeps = deps === undefined ? null : deps; 
  let destroy = undefined; 
 
  if (currentHook !== null) { 
    const prevEffect = currentHook.memoizedState; 
    destroy = prevEffect.destroy; 
    if (nextDeps !== null) { 
      const prevDeps = prevEffect.deps; 
      if (areHookInputsEqual(nextDeps, prevDeps)) { 
        pushEffect(hookFlags, create, destroy, nextDeps); 
        return; 
      } 
    } 
  } 
 
  currentlyRenderingFiber.flags |= fiberFlags; 
 
  hook.memoizedState = pushEffect( 
    HookHasEffect | hookFlags, 
    create, 
    destroy, 
    nextDeps, 
  ); 
} 
 
function areHookInputsEqual( 
  nextDeps: Array<mixed>, 
  prevDeps: Array<mixed> | null, 
) { 
  for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) { 
  // 调用的是Object.is方法,如果不支持则会使用polyfill进行兼容  
  // https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/is 
    if (is(nextDeps[i], prevDeps[i])) { 
      continue; 
    } 
    return false; 
  } 
  return true; 
} 

每个useEffect/useLayoutEffect 在mount/update时候,都会调用pushEffect创建一个effect节点, 追加在当前fiber.updateQueue.lastEffect 环状链表中, lastEffect指向的是最后一个effect节点,lastEffect.next 指向第一个effect节点

mount阶段

使用了useEffect 的fiber会被增加UpdateEffect|PassiveEffect的flags ,创建的effect节点 会被赋予 HasEffect| Passive 的tag

使用了useLayoutEffect的fiber会被增加 UpdateEffect 的flags, 创建的effect节点 会被赋予 HasEffect|Layout 的tag

useLayoutEffect 跟 useEffect 的create函数跟 destroy 函数都保存在effect节点,等待被执行

effect节点,被赋予HookHasEffect 表示应该该effect应该被触发

update阶段

通过对比前后的依赖,判断是否相等,从而决定fiber是否被增加UpdateEffect|PassiveEffect的flags, effect节点是否会被赋予 HasEffect| Passive 的tag

执行时机

useLayoutEffect 的销毁函数,是在mutation阶段同步执行的

useLayoutEffect 的创建函数,是在layout阶段同步执行

function commitMutationEffects( 
  root: FiberRoot, 
  renderPriorityLevel: ReactPriorityLevel, 
) { 
  while (nextEffect !== null) { 
    const flags = nextEffect.flags; 
    const primaryFlags = flags & (Placement | Update | Deletion | Hydrating); 
    switch (primaryFlags) { 
      case PlacementAndUpdate: { 
        // Placement 
        commitPlacement(nextEffect); 
        nextEffect.flags &= ~Placement; 
        // Update 
        const current = nextEffect.alternate; 
        commitWork(current, nextEffect); 
        break; 
      } 
      case Update: { 
        const current = nextEffect.alternate; 
        commitWork(current, nextEffect); 
        break; 
      } 
    } 
    nextEffect = nextEffect.nextEffect; 
  } 
} 

前面提到,使用了这两个hook的FunctionComponent fiber会被赋予Update 的flags

在mutation阶段,也就是commitMutationEffects函数调用过程中,就会进入 commitWork

function commitWork(current: Fiber | null, finishedWork: Fiber): void { 
  switch (finishedWork.tag) { 
    case FunctionComponent: 
    case ForwardRef: 
    case MemoComponent: 
    case SimpleMemoComponent: 
    case Block: { 
      commitHookEffectListUnmount(HookLayout | HookHasEffect, finishedWork); 
      return; 
    } 
    case ClassComponent: { 
      return; 
    } 
    // 与本节无关内容省略 
} 

commitWork 根据tag 是FunctionComponent, 因此会调用 commitHookEffectListUnmount

function commitHookEffectListUnmount(tag: number, finishedWork: Fiber) { 
  const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any); 
  const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null; 
  if (lastEffect !== null) { 
    const firstEffect = lastEffect.next; 
    let effect = firstEffect; 
    do { 
      if ((effect.tag & tag) === tag) { 
        // Unmount 
        const destroy = effect.destroy; 
        effect.destroy = undefined; 
        if (destroy !== undefined) { 
          destroy(); 
        } 
      } 
      effect = effect.next; 
    } while (effect !== firstEffect); 
  } 
} 

commitHookEffectListUnmount(HookLayout | HookHasEffect,finishedWork);

遍历我们的fiber.updateQueue.lasteEffect ,也就是useLayoutEffect/ useEffect 创建的effect 节点,遍历useLayout 的创建的节点,即 tag 有HookLayout并且需要触发HookHasEffect ,立即执行该effect节点的destory函数

在commitLayoutEffects中,会遍历effectList fiber,会调用 commitLayoutEffectOnFiber(root, current, nextEffect, committedLanes);

function commitLayoutEffects(root: FiberRoot, committedLanes: Lanes) { 
  while (nextEffect !== null) { 
 
    const flags = nextEffect.flags; 
 
    if (flags & (Update | Callback)) { 
      const current = nextEffect.alternate; 
      commitLayoutEffectOnFiber(root, current, nextEffect, committedLanes); 
    } 
     
    nextEffect = nextEffect.nextEffect; 
  } 
} 

commitLayoutEffectOnFiber 其实是 commitLifeCycles 函数的别名,会调用commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork)

function commitLifeCycles( 
  finishedRoot: FiberRoot, 
  current: Fiber | null, 
  finishedWork: Fiber, 
  committedLanes: Lanes, 
): void { 
  switch (finishedWork.tag) { 
    case FunctionComponent: 
    case ForwardRef: 
    case SimpleMemoComponent: 
    case Block: { 
      commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork); 
      schedulePassiveEffects(finishedWork); 
      return; 
    } 
    //  
} 

由于传递的是 HookLayout 跟 HookHasEffect 的tag, 因此会遍历useLayoutEffect的 effect节点,执行他们的create函数,并且将返回的结果赋值给 destroy 函数,保存在effect节点上

function commitHookEffectListMount(tag: number, finishedWork: Fiber) { 
  const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any); 
  const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null; 
  if (lastEffect !== null) { 
    const firstEffect = lastEffect.next; 
    let effect = firstEffect; 
    do { 
      if ((effect.tag & tag) === tag) { 
        // Mount 
        const create = effect.create; 
        effect.destroy = create(); 
 
      } 
      effect = effect.next; 
    } while (effect !== firstEffect); 
  } 
} 

useEffect 的create 跟 destroy 函数的执行时机,会比useLayout 慢,而且是异步执行的

在beforeMutation阶段,也就是 commitBeforeMutationEffects, 会调度 useEffect 的执行

function commitBeforeMutationEffects() { 
  while (nextEffect !== null) { 
    const current = nextEffect.alternate; 
    const flags = nextEffect.flags; 
    if ((flags & Snapshot) !== NoFlags) { 
    // 这里执行的是getSnapshotBeforeUpdate 
      commitBeforeMutationEffectOnFiber(current, nextEffect); 
    } 
    // 判断有useEffect 节点的,调度执行flushPassiveEffects 
    if ((flags & Passive) !== NoFlags) { 
      // If there are passive effects, schedule a callback to flush at 
      // the earliest opportunity. 
      if (!rootDoesHavePassiveEffects) { 
        rootDoesHavePassiveEffects = true; 
        scheduleCallback(NormalSchedulerPriority, () => { 
          flushPassiveEffects(); 
          return null; 
        }); 
      } 
    } 
    nextEffect = nextEffect.nextEffect; 
  } 
} 

使用了useEffect 的fiber会被赋予Passive 的flags, 以NormalSchedulerPriority 的优先级,异步调度flushPassiveEffects的执行,这个调度由于是异步的,需要等到 layout阶段结束了才执行回调函数,所以 当useLayoutEffect 的destory 跟create 函数都执行完了,才进行 useEffect 的create 跟destory 函数的执行

同时这里 rootDoesHavePassiveEffects = true ,避免重复调度的同时,后面的判断逻辑需要用到

前面介绍到了使用useLayoutEffect或者useEffect的FunctionComponent Fiber 在layout阶段会进入commitLifeCycles的逻辑,然后调用commitHookEffectListMount 跟schedulePassiveEffects

function schedulePassiveEffects(finishedWork: Fiber) { 
  const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any); 
  const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null; 
  if (lastEffect !== null) { 
    const firstEffect = lastEffect.next; 
    let effect = firstEffect; 
    do { 
      const {next, tag} = effect; 
      if ( 
        (tag & HookPassive) !== NoHookEffect && 
        (tag & HookHasEffect) !== NoHookEffect 
      ) { 
        enqueuePendingPassiveHookEffectUnmount(finishedWork, effect); 
        enqueuePendingPassiveHookEffectMount(finishedWork, effect); 
      } 
      effect = next; 
    } while (effect !== firstEffect); 
  } 
} 
function enqueuePendingPassiveHookEffectUnmount( 
  fiber: Fiber, 
  effect: HookEffect, 
): void { 
  pendingPassiveHookEffectsUnmount.push(effect, fiber); 
  if (!rootDoesHavePassiveEffects) { 
    rootDoesHavePassiveEffects = true; 
    scheduleCallback(NormalSchedulerPriority, () => { 
      flushPassiveEffects(); 
      return null; 
    }); 
  } 
} 
function enqueuePendingPassiveHookEffectMount(fiber, effect) { 
  pendingPassiveHookEffectsMount.push(effect, fiber); 
 
  if (!rootDoesHavePassiveEffects) { 
    rootDoesHavePassiveEffects = true; 
    scheduleCallback(NormalPriority$1, function () { 
      flushPassiveEffects(); 
      return null; 
    }); 
  } 
} 

schedulePassiveEffects函数遍历 effect 链表, 将符合 Passive | HookHasEffect tag 的effect节点,也就是 useEffect 的 effect 节点,添加到 pendingPassiveHookEffectsUnmount 跟 pendingPassiveHookEffectsMount 数组 , 数组的第 i 项为effect节点,第 i+1 项 为effect节点对应的fiber节点。

  if (rootDoesHavePassiveEffects) { 
    // This commit has passive effects. Stash a reference to them. But don't 
    // schedule a callback until after flushing layout work. 
    rootDoesHavePassiveEffects = false; 
    rootWithPendingPassiveEffects = root; 
    pendingPassiveEffectsLanes = lanes; 
    pendingPassiveEffectsRenderPriority = renderPriorityLevel; 
  } 

在完成layout阶段之后, commitRoot 在最后会判断 rootDoesHavePassiveEffects是否为true,会赋值 rootWithPendingPassiveEffects = root ,pendingPassiveEffectsRenderPriority= renderPriorityLevel 等相关变量。

之后等待异步调用,flushPassiveEffects 被执行, 内部调用了 flushPassiveEffectsImpl

function flushPassiveEffectsImpl() { 
  if (rootWithPendingPassiveEffects === null) { 
    return false; 
  } 
 
  const root = rootWithPendingPassiveEffects; 
  const lanes = pendingPassiveEffectsLanes; 
  rootWithPendingPassiveEffects = null; 
  pendingPassiveEffectsLanes = NoLanes; 
 
  const prevExecutionContext = executionContext; 
  executionContext |= CommitContext; 
  const prevInteractions = pushInteractions(root); 
 
  // First pass: Destroy stale passive effects. 
  const unmountEffects = pendingPassiveHookEffectsUnmount; 
  pendingPassiveHookEffectsUnmount = []; 
  for (let i = 0; i < unmountEffects.length; i += 2) { 
    const effect = ((unmountEffects[i]: any): HookEffect); 
    const fiber = ((unmountEffects[i + 1]: any): Fiber); 
    const destroy = effect.destroy; 
    effect.destroy = undefined; 
 
    if (typeof destroy === 'function') { 
         destroy(); 
      } 
    } 
  } 
  // Second pass: Create new passive effects. 
  const mountEffects = pendingPassiveHookEffectsMount; 
  pendingPassiveHookEffectsMount = []; 
  for (let i = 0; i < mountEffects.length; i += 2) { 
    const effect = ((mountEffects[i]: any): HookEffect); 
    const fiber = ((mountEffects[i + 1]: any): Fiber); 
    const create = effect.create; 
    effect.destroy = create(); 
  } 
 
  executionContext = prevExecutionContext; 
 
   
  return true; 
} 

flushPassiveEffectsImpl 此时判断rootWithPendingPassiveEffects 已经存在,因此进入下面的逻辑, 遍历pendingPassiveHookEffectsUnmount跟pendingPassiveHookEffectsMount,批量完成useEffect 销毁函数的执行跟 创建函数的执行

useRef

function mountRef<T>(initialValue: T): {|current: T|} { 
  const hook = mountWorkInProgressHook(); 
  const ref = {current: initialValue}; 
  hook.memoizedState = ref; 
  return ref; 
} 
 
function updateRef<T>(initialValue: T): {|current: T|} { 
  const hook = updateWorkInProgressHook(); 
  return hook.memoizedState; 
} 

useRef 只会在mount 时赋值 hook.memoizedState = {current: initialValue}, 之后便维持不变,之后memoizedState的值不变,改变的是current 属性的引用

使用场景

在HostComponent 跟 ClassComponent 可以赋值 ref 属性

对于HostComponent ref赋值了对应的dom属性

对于ClassComponent ref赋值了对应的instance实例

FunctionComponent 不能赋值ref属性,除非使用React.forwardRef() 进行透传 ref

ref引用类型

ref 是引用[reference]的缩写,表示对某个值的引用,包括但不限于dom

ref类型

在reactElement中,ref 的类型有两种:

  • useRef产生值为{current: any}的对象ref
  • ref={(instance) => {}} 的function ref
function App() { 
  const hookRef = useRef(null); 
  const funcionHookRef = useRef(null); 
  return (
  <div> 
  <div ref={(instance) => {funcionHookRef.current = instance}}>function ref</div> 
  <div ref={hookRef}>useRef</div> 
 </div>) 
} 

执行时机

对于ref的更新,也是依赖于副作用,需要更新ref的fiber会被赋予Ref的flags

在render阶段中,对于HostComponent 跟 ClassComponent 会调用markRef决定是否更新ref,如果被赋值了Ref 的flags,那么该fiber节点也会加入到effectList中

function markRef(current: Fiber | null, workInProgress: Fiber) { 
  const ref = workInProgress.ref; 
  if ( 
  // mount阶段并且有ref属性 
    (current === null && ref !== null) || 
  // uodate阶段ref属性不相等 
    (current !== null && current.ref !== ref) 
  ) { 
    // Schedule a Ref effect 
    workInProgress.flags |= Ref; 
  } 
} 

在commit阶段,commitMutationEffects流程中, 如果fiber有 Ref 的flags,则移除ref属性引用, 同时对于被卸载的fiber节点及其子孙fiber节点也会调用 safelyDetachRef 移除ref引用

function commitMutationEffects() { 
    if (flags & Ref) { 
      const current = nextEffect.alternate; 
      if (current !== null) { 
        commitDetachRef(current); 
      } 
    } 
}  
function commitDetachRef(current: Fiber) { 
  const currentRef = current.ref; 
  if (currentRef !== null) { 
    if (typeof currentRef === 'function') { 
      currentRef(null); 
    } else { 
      currentRef.current = null; 
    } 
  } 
}  
function safelyDetachRef(current: Fiber) { 
  const ref = current.ref; 
  if (ref !== null) { 
    if (typeof ref === 'function') { 
      try { 
        ref(null); 
      } catch (refError) { 
        captureCommitPhaseError(current, refError); 
      } 
    } else { 
      ref.current = null; 
    } 
  } 
} 

在commitLayoutEffects中, 有Ref flags的fiber会被重新赋值ref属性

function commitLayoutEffects() { 
 
  while (nextEffect !== null) { 
   
    const flags = nextEffect.flags; 
 
    if (flags & (Update | Callback)) { 
      const current = nextEffect.alternate; 
      commitLayoutEffectOnFiber(root, current, nextEffect, committedLanes); 
    } 
     
    if (flags & Ref) { 
        commitAttachRef(nextEffect); 
    } 
 
    nextEffect = nextEffect.nextEffect; 
  } 
} 
function commitAttachRef(finishedWork: Fiber) { 
  const ref = finishedWork.ref; 
  if (ref !== null) { 
    const instance = finishedWork.stateNode; 
    let instanceToUse; 
    switch (finishedWork.tag) { 
      case HostComponent: 
        instanceToUse = getPublicInstance(instance); 
        break; 
      default: 
        instanceToUse = instance; 
    } 
 
    if (typeof ref === 'function') { 
      ref(instanceToUse); 
    } else { 
      ref.current = instanceToUse; 
    } 
  } 
} 

useCallback

使用场景:

仅在 Function component 中 可使用useCallback

当依赖数组没改变的时候,可复用之前的callback函数,当传递给子组件的时候,便于子组件配合 React.memo做渲染优化

执行时机:

render阶段的 beginWork中, renderWithHooks方法,会执行Function Component, 从而调用到了dispatch.useCallback

在mount阶段 调用mountCallback,hook.memoizedState保存了callback函数跟对应的依赖数组

在update阶段调用updateCallback, 比较前后的依赖数组是否有更改,然后决定是否返回新的callback并更新memoizedState,还是返回之前memoizedState 缓存的callback。

function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T { 
  const hook = mountWorkInProgressHook(); 
  const nextDeps = deps === undefined ? null : deps; 
  hook.memoizedState = [callback, nextDeps]; 
  return callback; 
} 
function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T { 
  const hook = updateWorkInProgressHook(); 
  const nextDeps = deps === undefined ? null : deps; 
  const prevState = hook.memoizedState; 
  if (prevState !== null) { 
    if (nextDeps !== null) { 
      const prevDeps: Array<mixed> | null = prevState[1]; 
      if (areHookInputsEqual(nextDeps, prevDeps)) { 
        return prevState[0]; 
      } 
    } 
  } 
  hook.memoizedState = [callback, nextDeps]; 
  return callback; 
} 

useMemo

使用场景:

仅在 Function component 中 可使用useMemo

当依赖数组没改变的时候,可复用之前的callback执行返回值,当传递给子组件的时候,便于子组件配合 React.memo做渲染优化

执行时机:

render阶段的 beginWork中, renderWithHooks方法,会执行Function Component, 从而调用到了dispatch.useMemo

在mount阶段 调用mountMemo,hook.memoizedState保存了callback函数执行的返回值 跟对应的依赖数组

在update阶段调用updateMemo, 比较前后的依赖数组是否有更改,然后决定是否返回之前memoizedState 缓存的返回值,还是重新执行最新的callbac返回结果,并重新赋值 hook.memoizedState = [nextValue, nextDeps];

function mountMemo<T>( 
  nextCreate: () => T, 
  deps: Array<mixed> | void | null, 
): T { 
  const hook = mountWorkInProgressHook(); 
  const nextDeps = deps === undefined ? null : deps; 
  const nextValue = nextCreate(); 
  hook.memoizedState = [nextValue, nextDeps]; 
  return nextValue; 
} 
function updateMemo<T>( 
  nextCreate: () => T, 
  deps: Array<mixed> | void | null, 
): T { 
  const hook = updateWorkInProgressHook(); 
  const nextDeps = deps === undefined ? null : deps; 
  const prevState = hook.memoizedState; 
  if (prevState !== null) { 
    // Assume these are defined. If they're not, areHookInputsEqual will warn. 
    if (nextDeps !== null) { 
      const prevDeps: Array<mixed> | null = prevState[1]; 
      if (areHookInputsEqual(nextDeps, prevDeps)) { 
        return prevState[0]; 
      } 
    } 
  } 
  const nextValue = nextCreate(); 
  hook.memoizedState = [nextValue, nextDeps]; 
  return nextValue; 
} 

Diff算法实现

执行时机

在 render阶段中, workInProgress fiber的构建前面已经提到,是通过深度优先递归调用reconcileChildFibers来生成的,主要利用的是 current Fiber 的child fiber,即将渲染的ReactElement对象/ReactElement对象数组,在内存中构建workInProgress fiber

export function reconcileChildren( 
  current: Fiber | null, 
  workInProgress: Fiber, 
  nextChildren: any, 
  renderLanes: Lanes, 
) { 
  if (current === null) { 
    // If this is a fresh new component that hasn't been rendered yet, we 
    // won't update its child set by applying minimal side-effects. Instead, 
    // we will add them all to the child before it gets rendered. That means 
    // we can optimize this reconciliation pass by not tracking side-effects. 
    workInProgress.child = mountChildFibers( 
      workInProgress, 
      null, 
      nextChildren, 
      renderLanes, 
    ); 
  } else { 
    // If the current child is the same as the work in progress, it means that 
    // we haven't yet started any work on these children. Therefore, we use 
    // the clone algorithm to create a copy of all the current children. 
 
    // If we had any progressed work already, that is invalid at this point so 
    // let's throw it out. 
    workInProgress.child = reconcileChildFibers( 
      workInProgress, 
      current.child, 
      nextChildren, 
      renderLanes, 
    ); 
  } 
} 

reconcileChildFibers 得到的child fiber 是 新建/卸载/复用,是通过diff算法来实现的

function reconcileChildFibers( 
    returnFiber: Fiber, 
    currentFirstChild: Fiber | null, 
    newChild: any, 
    lanes: Lanes, 
  ): Fiber | null { 
 
    // Handle top level unkeyed fragments as if they were arrays. 
    // This leads to an ambiguity between <>{[...]}</> and <>...</>. 
    // We treat the ambiguous cases above the same. 
    const isUnkeyedTopLevelFragment = 
      typeof newChild === 'object' && 
      newChild !== null && 
      newChild.type === REACT_FRAGMENT_TYPE && 
      newChild.key === null; 
   
    if (isUnkeyedTopLevelFragment) { 
      newChild = newChild.props.children; 
    } 
 
    // Handle object types 
    const isObject = typeof newChild === 'object' && newChild !== null; 
 
 
    if (isObject) { 
      switch (newChild.$$typeof) { 
        case REACT_ELEMENT_TYPE: 
          return placeSingleChild( 
            reconcileSingleElement( 
              returnFiber, 
              currentFirstChild, 
              newChild, 
              lanes, 
            ), 
          ); 
      } 
    } 
     
    if (isArray(newChild)) { 
      return reconcileChildrenArray( 
        returnFiber, 
        currentFirstChild, 
        newChild, 
        lanes, 
      ); 
    } 
 
    // Remaining cases are all treated as empty. 
    return deleteRemainingChildren(returnFiber, currentFirstChild); 
  } 

遵循原则

为了降低对比的复杂度为O(N)

react的diff算法遵循下面几个原则:

  1. 只对比同层级的fiber节点
  2. 尝试通过key相等的fiber来进行fiber节点复用

假设我们更新前有

qq login
wx login

更新后变成了

wx login
qq login

我们会复用原来的两个fiber,而不是销毁重建

  1. 如果key相等,则依赖elementType判断,如果elementType不相等,则直接废弃之前的fiber节点,根据新的element新建fiber节点

单节点的diff

单节点的diff分为三种情况

多变单: 原本同级多个fiber,变为一个fiber

单变单: 原本同级一个fiber,变为一个fiber

无变单: 原本同级没有fiber节点,变为有一个fiber节点

这三种在更新时候,reconcileChildFibers 传入的newChild 会是 拥有$$typeof属性 的ReactElement对象

最终会调用reconcileSingleElement 返回 fiber节点

我们需要关注

returnFiber: diff fiber的父级fiber,也就是当前workInProgress fiber

currentFirstChild: 视图存在的fiber节点,其兄弟节点以sibling连接

element : 即将更新的reactElement

 function reconcileSingleElement( 
    returnFiber: Fiber, 
    currentFirstChild: Fiber | null, 
    element: ReactElement, 
    lanes: Lanes, 
  ): Fiber { 
    const key = element.key; 
    let child = currentFirstChild; 
    while (child !== null) { 
      // the first item in the list. 
      if (child.key === key) { 
       // 匹配到fiber节点,可以进行复用 
        if ( 
          child.elementType === element.type 
        ) { 
          // 由于即将更新的只有一个fiber节点,那么其他旧的fiber节点应该被删除掉 
          deleteRemainingChildren(returnFiber, child.sibling); 
          const existing = useFiber(child, element.props); 
          existing.ref = coerceRef(returnFiber, child, element); 
          existing.return = returnFiber; 
          return existing; 
        } 
        // Didn't match. 
        deleteRemainingChildren(returnFiber, child); 
        break; 
      } else { 
        deleteChild(returnFiber, child); 
      } 
      child = child.sibling; 
    } 
     // 上面的fiber diff 没有匹配到可复用的fiber 
      const created = createFiberFromElement(element, returnFiber.mode, lanes); 
      created.ref = coerceRef(returnFiber, currentFirstChild, element); 
      created.return = returnFiber; 
      return created; 
  } 
  1. 判断上一次更新对应位置是否存在fiber节点
  2. 若不存在fiber节点则根据ReactElement新建fiber节点并返回
  3. 若存在fiber节点,判断两者的key是否相等,若key相等并且elementType相等,则进行fiber节点复用,其他兄弟fiber节点则标记删除
  4. 若key不相等,则标记该fiber节点删除,继续进行遍历兄弟fiber节点,尝试找到可以复用的fiber节点
  5. 若key相等,但是elementType不相等,则标记该fiber节点及其兄弟fiber节点删除
  6. 最后还是没找到可复用节点fiber,则进入2流程,根据ReactElement新建fiber节点并返回

多节点diff

reconcileChildFibers 传入的newChild 为一个数组时,则会进入多节点diff的逻辑reconcileChildrenArray(returnFiber, currentFirstChild, newChildren, lanes)

如下面的例子 : 假设我们有这样一个react组件, 原来App的子fiber是 li fiber,key 为one

function App() { 
  return <li key="one">1</li> 
} 

现在该组件发生更新,生成 三个reactElement数组

function App() { 
  return (<> 
    <li key="one">1</li> 
    <li key="two">1</li> 
    <li key="three">1</li> 
   </>) 
} 

我们依然关注三个参数:

returnFiber: diff fiber的父级fiber,也就是当前workInProgress fiber

currentFirstChild: 视图存在的fiber节点,其兄弟节点以sibling连接

newChildren : 即将更新的reactElement 数组

oldFiber.index : 旧fiber节点在其兄弟fiber节点的位置

newFiber.index: 新fiber节点在其构建完兄弟fiber节点的位置

lastPlaceIndex: 代表复用fiber在原本oldFiber中的最右位置

function reconcileChildrenArray( 
    returnFiber: Fiber, 
    currentFirstChild: Fiber | null, 
    newChildren: Array<*>, 
    lanes: Lanes, 
  ): Fiber | null { 
 
    let resultingFirstChild: Fiber | null = null; 
    let previousNewFiber: Fiber | null = null; 
 
    let oldFiber = currentFirstChild; 
    let lastPlacedIndex = 0; 
    let newIdx = 0; 
    let nextOldFiber = null; 
    for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) { 
      if (oldFiber.index > newIdx) { 
        nextOldFiber = oldFiber; 
        oldFiber = null; 
      } else { 
        nextOldFiber = oldFiber.sibling; 
      } 
      const newFiber = updateSlot( 
        returnFiber, 
        oldFiber, 
        newChildren[newIdx], 
        lanes, 
      ); 
      // key 不相等,跳出这一轮遍历 
      if (newFiber === null) { 
        if (oldFiber === null) { 
          oldFiber = nextOldFiber; 
        } 
        break; 
      } 
      if (shouldTrackSideEffects) { 
      // key相等,但是elementType不相等,不能复用,fiber是直接新建的 
        if (oldFiber && newFiber.alternate === null) { 
          // We matched the slot, but we didn't reuse the existing fiber, so we 
          // need to delete the existing child. 
          // 标记该fiber节点删除 
          deleteChild(returnFiber, oldFiber); 
        } 
      } 
      lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); 
      if (previousNewFiber === null) { 
        // 拿到更新后第一个fiber需要时直接返回 
        resultingFirstChild = newFiber; 
      } else { 
       // 把更新后的fiber通过sibling 串起来 
        previousNewFiber.sibling = newFiber; 
      } 
      previousNewFiber = newFiber; 
      oldFiber = nextOldFiber; 
    } 
 
    // newChildren 遍历完了,直接返回之前保存的第一个newFiber,并且标记剩下的fiber节点删除 
    if (newIdx === newChildren.length) { 
      // We've reached the end of the new children. We can delete the rest. 
      deleteRemainingChildren(returnFiber, oldFiber); 
      return resultingFirstChild; 
    } 
     
 
    // oldFiber 提前遍历完了,那么针对剩下的element新建fiber 
    if (oldFiber === null) { 
      // If we don't have any more existing children we can choose a fast path 
      // since the rest will all be insertions. 
      for (; newIdx < newChildren.length; newIdx++) { 
        const newFiber = createChild(returnFiber, newChildren[newIdx], lanes); 
        if (newFiber === null) { 
          continue; 
        } 
        lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); 
        if (previousNewFiber === null) { 
          resultingFirstChild = newFiber; 
        } else { 
          previousNewFiber.sibling = newFiber; 
        } 
        previousNewFiber = newFiber; 
      } 
      return resultingFirstChild; 
    } 
     
    // 前面newChildren 跟 oldFiber 没遍历完,证明出现key不相等的情况, 
    // oldFiber节点在更新后发生了移动 
 
    // Add all children to a key map for quick lookups. 
    const existingChildren = mapRemainingChildren(returnFiber, oldFiber); 
 
    // Keep scanning and use the map to restore deleted items as moves. 
    for (; newIdx < newChildren.length; newIdx++) { 
      const newFiber = updateFromMap( 
        existingChildren, 
        returnFiber, 
        newIdx, 
        newChildren[newIdx], 
        lanes, 
      ); 
      if (newFiber !== null) { 
        if (shouldTrackSideEffects) { 
          if (newFiber.alternate !== null) { 
            // The new fiber is a work in progress, but if there exists a 
            // current, that means that we reused the fiber. We need to delete 
            // it from the child list so that we don't add it to the deletion 
            // list. 
            existingChildren.delete( 
              newFiber.key === null ? newIdx : newFiber.key, 
            ); 
          } 
        } 
        lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); 
        if (previousNewFiber === null) { 
          resultingFirstChild = newFiber; 
        } else { 
          previousNewFiber.sibling = newFiber; 
        } 
        previousNewFiber = newFiber; 
      } 
    } 
 
    if (shouldTrackSideEffects) { 
      // Any existing children that weren't consumed above were deleted. We need 
      // to add them to the deletion list. 
      existingChildren.forEach(child => deleteChild(returnFiber, child)); 
    } 
 
    return resultingFirstChild; 
  } 

按照索引方式遍历newChildren数组,同时按照链表方式遍历对比currentChild fiber节点,进行遍历

以下三种情况会导致遍历发生中断,进入不同的流程:

  1. old fiber提前遍历完了,剩下的newChildren 直接根据element 新建fiber
  2. newChildren 提前遍历完了,剩下的oldFiber 标记删除
  3. old fiber 跟 nextChildren 都没遍历完,发生在key不相等的时候, 会进入另一轮遍历
function updateSlot( 
    returnFiber: Fiber, 
    oldFiber: Fiber | null, 
    newChild: any, 
    lanes: Lanes, 
  ): Fiber | null { 
    // Update the fiber if the keys match, otherwise return null. 
    const key = oldFiber !== null ? oldFiber.key : null; 
 
    if (typeof newChild === 'object' && newChild !== null) { 
      switch (newChild.$$typeof) { 
        case REACT_ELEMENT_TYPE: { 
          if (newChild.key === key) { 
            return updateElement(returnFiber, oldFiber, newChild, lanes); 
          } else { 
            return null; 
          } 
        } 
      } 
 
      throwOnInvalidObjectType(returnFiber, newChild); 
    } 
 
    return null; 
  } 

第一轮遍历,根据updateSlot(returnFiber,oldFiber,newChildren[newIdx],lanes) 返回的结果,决定是否继续第一次遍历,updateSlot 会尝试进行fiber的复用,updateSlot会判断old Fiber 跟 newChild 的key 是否相等, 相等的话会进入updateElement的逻辑

function updateElement( 
    returnFiber: Fiber, 
    current: Fiber | null, 
    element: ReactElement, 
    lanes: Lanes, 
  ): Fiber { 
    if (current !== null) { 
      if ( 
        current.elementType === element.type  
      ) { 
        // Move based on index 
        const existing = useFiber(current, element.props); 
        existing.ref = coerceRef(returnFiber, current, element); 
        existing.return = returnFiber; 
        return existing; 
      }  
    // Insert 
    const created = createFiberFromElement(element, returnFiber.mode, lanes); 
    created.ref = coerceRef(returnFiber, current, element); 
    created.return = returnFiber; 
    return created; 
  } 

如果oldFiber 跟 newChild 的elementType相等,则进行fiber复用,因为是update阶段,那么复用后的fiber.alternate属性是存在的(指向的是当前视图的fiber),而elementType不相等的话,会直接根据element新建fiber,alternate指向节点为null,同时oldFiber会被标志清除

出现1情况,会根据剩下element新建fiber,然后返回new first fiber 链表

假设更新后的fiber 有 a,b,c 那么他们是长这样的 a -> b -> c, 箭头代表sibling

出现2情况,会标志剩下的oldFiber删除,并返回new first fiber链表

出现3情况,那么oldFiber跟newChild都没遍历完,会基于剩下的oldFiber 构建key -> oldFiber的map,如果key不存在,那么用oldFiber.index作为key

然后进入第二轮遍历,遍历剩下的newChild, 通过updateFromMap方法尝试找到可复用的fiber节点

 function updateFromMap( 
    existingChildren: Map<string | number, Fiber>, 
    returnFiber: Fiber, 
    newIdx: number, 
    newChild: any, 
    lanes: Lanes, 
  ): Fiber | null { 
  
    if (typeof newChild === 'object' && newChild !== null) { 
      switch (newChild.$$typeof) { 
        case REACT_ELEMENT_TYPE: { 
          const matchedFiber = 
            existingChildren.get( 
              newChild.key === null ? newIdx : newChild.key, 
            ) || null; 
          return updateElement(returnFiber, matchedFiber, newChild, lanes); 
        } 
      } 
 
    return null; 
  } 

根据updateFromMap 判断复用返回的fiber.alternate 判断是复用了,然后将它从map删除掉(避免后面被标记删除),遍历完newChildren之后,对old Fiber map剩下的fiber标记删除

在第一轮遍历跟第二轮遍历,我们发现 当复用/新建 fiber之后,会调用lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);更新lastPlacedIndex,这是为了处理旧fiber节点在更新之后,可能发生移动的情况,我们要进行优化,而不是直接根据key不相同就删除旧fiber,新建fiber

function placeChild( 
    newFiber: Fiber, 
    lastPlacedIndex: number, 
    newIndex: number, 
  ): number { 
    newFiber.index = newIndex; 
    if (!shouldTrackSideEffects) { 
      // Noop. 
      return lastPlacedIndex; 
    } 
    const current = newFiber.alternate; 
    if (current !== null) { 
      const oldIndex = current.index; 
      if (oldIndex < lastPlacedIndex) { 
        // This is a move. 
        newFiber.flags = Placement; 
        return lastPlacedIndex; 
      } else { 
        // This item can stay in place. 
        return oldIndex; 
      } 
    } else { 
      // This is an insertion. 
      newFiber.flags = Placement; 
      return lastPlacedIndex; 
    } 
  } 

placeChild会给newFiber赋值在新fiber中的index(位置)

如果newFiber.alternate存在,证明是复用的fiber,判断oldFiber.index 跟 lastPlacedIndex 大小,如果oldIndex < lastPlacedIndex, 代表当前最新的复用fiber在oldFiber中的位置, 但是在更新后发生了位置的移动, 因为要标志Placement的flags,表示移动

如果alternate不存在,证明是新建的fiber,因此要增加Placement 的flags副作用,并且返回旧的lastPlacedIndex

看起来比1,2情况难理解,我们来举例子说明

以下用a,b,c,d 代表四个不同的fiber节点,他们的key分别是a,b,c,d

现在视图【view】: abcd -> 即将更新【update】: adcb

  • 第一轮遍历,开始比较,此时 lastPlacedIndex = 0, newIndex = 0
  • 比较 view a 跟 update a, 可以进行复用,oldIndex跟lastPlacedIndex 都是0, newIndex++比较下一个newChild
  • 比较 view b 跟update d, key 不相等,退出第一轮遍历, 此时oldFiber剩下bcd, newChildren 剩下dcb
  • 第二轮遍历,遍历剩下的newChildren 也就是dcb,跟key-fiber map中取到的fiber做比较,尝试复用fiber
  • 比较update d 跟view d(从map取到), oldIndex = c.index = 3, lastPlacedIndex = 0; 此时 oldIndex > lastPlacedIndex,placeChild返回oldIndex, 更新lastPlaceIndex = 3
  • 比较update b 跟view b(从map取到), oldIndex = b.index = 1,lastPlacedIndex =3, 此时oldIndex < lastPlacedIndex, 虽然是复用的fiber节点,但是要标记b fiber Placement 的flags 追踪副作用,后面加入effectList, placeChild返回lastPlaceIndex,lastPlaceIndex保持不变,也就是d的位置保持不变
  • 比较 update c 跟 view c(从map取到),oldIndex = b.index = 2, lastPlacedIndex = 3,此时oldIndex < lastPlaceIndex,标记b fiber Placement 的flags 副作用,placeChild返回lastPlaceIndex,lastPlaceIndex保持不变,也就是d的位置保持不变
  • 本次diff之后的结果,看似是d挪到了bc前面,其实被标记副作用是的bc fiber,是bc挪到了d后面, 这是react的更新原则决定的
    • 前面分析的render阶段,我们知道fiber的构建是深度优先遍历的,最先完成completeWork的fiber 分别是 a, d, b, c
    • 根据fiber.flags追踪副作用构建我们的effectList, a->b->c->d 到 a->d->b->c, effectList为 b->c
    • 在commit阶段中的mutaion中,以HostComponent为例子,原本dom结构: abcd , 我们要把dom结构变为adbc, 遍历effectList: b->c调用 commitPlacement
    • 先处理b fiber, const before = getHostSibling(finishedWork);,在新fiber结构中,后面最近的dom节点对应的fiber,因为c fiber有副作用 Placement, 我们只能拿到null, 因此before = null,调用insertOrAppendPlacementNode(finishedWork, before, parent) 其实是parentStateNode.appendChild(c fiber.stateNode)
    • 后处理c fiber,此时在新的fiber结构中,并没有位于c fiber后面的host类型兄弟fiber, 因此, const before = null, 调用insertOrAppendPlacementNode(finishedWork, before, parent) 其实是parentStateNode.appendChild(c fiber.stateNode)

与vue 框架diff算法的差异

正常的vdom diff算法是通过双索引遍历的方式进行优化的,但是由于fiber本身的结构,其兄弟节点是链表结构,不适合用双向索引的方式

Scheduler

简介

scheduler本质是模拟了requestIdleCallback,利用宏任务API: MessageChannel【在非浏览器环境会降级为settimout 0】来实现 时间片的分割, 因为messageChannel产生的宏任务比settimtout产生的宏任务更快被调用

原理:

浏览器主线程中,一个宏任务做了哪些事情:

同步js -> 微任务队列【promise】 -> requestAnimationFrame回调 -> 重排/重绘 -> requestidleCallback

以下对用到的变量进行解释说明,方便后面理解如何被使用:

// Max 31 bit integer. The max integer size in V8 for 32-bit systems. 
// Math.pow(2, 30) - 1 
// 0b111111111111111111111111111111 
var maxSigned31BitInt = 1073741823; 
// Times out immediately 立刻执行优先级,过期时间为-1ms 
var IMMEDIATE_PRIORITY_TIMEOUT = -1; 
// Eventually times out 用户行为优先级,过期时间为250ms 
var USER_BLOCKING_PRIORITY_TIMEOUT = 250; 
// 正常优先级,过期时间5000ms 
var NORMAL_PRIORITY_TIMEOUT = 5000; 
// 低优先级,过期时间10000ms 
var LOW_PRIORITY_TIMEOUT = 10000; 
// Never times out 永远不会过期,代表可能一直被delay不执行 
var IDLE_PRIORITY_TIMEOUT = maxSigned31BitInt; 
 
// Tasks are stored on a min heap 
// 立即执行 任务队列 
var taskQueue = []; 
// 延迟执行 任务队列 
var timerQueue = []; 
 
// Incrementing id counter. Used to maintain insertion order. 
// 自增的任务id 
var taskIdCounter = 1; 
 
// Pausing the scheduler is useful for debugging. 
var isSchedulerPaused = false; 
 
// 当前正在执行的任务 
var currentTask = null; 
// 当前的优先级 
var currentPriorityLevel = NormalPriority; 
 
// This is set while performing work, to prevent re-entrancy. 
// 当任务被执行的时候,标志为true,避免任务被重复执行 
var isPerformingWork = false; 
 
// 当及时任务被调度的时候,标志为true,避免重复调度 
var isHostCallbackScheduled = false; 
// 当延迟任务被调度的时候,标志为true,避免重复调度 
var isHostTimeoutScheduled = false; 
Scheduler 对外提供几个API以供调用: 
unstable_runWithPriority(priorityLevel, eventHandler): 设置scheduler 调度器的优先级,执行 eventHandler
// 之前的历史代码是用于提前执行调度优先级,然后调用调度器调度任务 
// 现在 unstable_scheduleCallback 直接支持传入优先级进行调度了 
// 因此 17.0.1 代码也不会这么用了 
unstable_runWithPriority(priority, () => { 
  unstable_scheduleCallback(() => { 
    // 要做的事情... 
  }); 
}); 
function unstable_runWithPriority(priorityLevel, eventHandler) { 
  switch (priorityLevel) { 
    case ImmediatePriority: 
    case UserBlockingPriority: 
    case NormalPriority: 
    case LowPriority: 
    case IdlePriority: 
      break; 
    default: 
      priorityLevel = NormalPriority; 
  } 
 
  var previousPriorityLevel = currentPriorityLevel; 
  currentPriorityLevel = priorityLevel; 
 
  try { 
    return eventHandler(); 
  } finally { 
    currentPriorityLevel = previousPriorityLevel; 
  } 
} 

getCurrentTime: 获取当前页面开始渲染到现在的时间戳

getCurrentTime默认使用的performance.now,记录从浏览器渲染到现在经过的时间戳

const hasPerformanceNow = 
  typeof performance === 'object' && typeof performance.now === 'function'; 
 
if (hasPerformanceNow) { 
  const localPerformance = performance; 
  getCurrentTime = () => localPerformance.now(); 
} else { 
  const localDate = Date; 
  const initialTime = localDate.now(); 
  getCurrentTime = () => localDate.now() - initialTime; 
} 

unstable_scheduleCallback(priorityLevel, callback, options):以priorityLevel的优先级,调度callback函数的执行。options可选传递延迟执行时间

function unstable_scheduleCallback(priorityLevel, callback, options) { 
  var currentTime = getCurrentTime(); 
 
  var startTime; 
  if (typeof options === 'object' && options !== null) { 
    var delay = options.delay; 
    if (typeof delay === 'number' && delay > 0) { 
      startTime = currentTime + delay; 
    } else { 
      startTime = currentTime; 
    } 
  } else { 
    startTime = currentTime; 
  } 
 
  var timeout; 
  switch (priorityLevel) { 
    case ImmediatePriority: 
      timeout = IMMEDIATE_PRIORITY_TIMEOUT; 
      break; 
    case UserBlockingPriority: 
      timeout = USER_BLOCKING_PRIORITY_TIMEOUT; 
      break; 
    case IdlePriority: 
      timeout = IDLE_PRIORITY_TIMEOUT; 
      break; 
    case LowPriority: 
      timeout = LOW_PRIORITY_TIMEOUT; 
      break; 
    case NormalPriority: 
    default: 
      timeout = NORMAL_PRIORITY_TIMEOUT; 
      break; 
  } 
 
  var expirationTime = startTime + timeout; // 根据优先级计算任务的过期时间 
 
  // 为该callback创建一个任务 
  var newTask = { 
    id: taskIdCounter++, 
    callback, 
    priorityLevel, 
    startTime, 
    expirationTime, 
    sortIndex: -1, 
  }; 
 
// 目前17.0.1并没有 Scheduler_scheduleCallback 传递options参数的,因此不会用到延迟任务 
  if (startTime > currentTime) { 
    // This is a delayed task. 
    newTask.sortIndex = startTime; 
    push(timerQueue, newTask); 
    if (peek(taskQueue) === null && newTask === peek(timerQueue)) { 
      // All tasks are delayed, and this is the task with the earliest delay. 
      if (isHostTimeoutScheduled) { 
        // Cancel an existing timeout. 
        cancelHostTimeout(); 
      } else { 
        isHostTimeoutScheduled = true; 
      } 
      // Schedule a timeout. 
      requestHostTimeout(handleTimeout, startTime - currentTime); 
    } 
  } else { 
    newTask.sortIndex = expirationTime; 
    // 入堆,根据优先级生成最小堆 
    push(taskQueue, newTask); 
    // Schedule a host callback, if needed. If we're already performing work, 
    // wait until the next time we yield. 
    if (!isHostCallbackScheduled && !isPerformingWork) { 
      isHostCallbackScheduled = true; 
      // requestHostCallback调度flushWork 
      requestHostCallback(flushWork); 
    } 
  } 
 
  return newTask; 
} 
requestHostCallback 调度及时任务执行 
cancelHostCallback 取消及时任务执行 
requestHostTimeout 调度延迟任务执行 
cancelHostTimeout 取消延迟任务执行 
const channel = new MessageChannel(); 
const port = channel.port2; 
channel.port1.onmessage = performWorkUntilDeadline; 
 
 requestHostCallback = function(callback) { 
    scheduledHostCallback = callback; 
    if (!isMessageLoopRunning) { 
      isMessageLoopRunning = true; 
      port.postMessage(null); 
    } 
  }; 
   
 cancelHostCallback = function() { 
    scheduledHostCallback = null; 
 }; 
  
 requestHostTimeout = function(callback, ms) { 
    taskTimeoutID = setTimeout(() => { 
      callback(getCurrentTime()); 
    }, ms); 
 }; 
  
cancelHostTimeout = function() { 
    clearTimeout(taskTimeoutID); 
    taskTimeoutID = -1; 
}; 

因此requestHostCallback(flushWork); 将flushWork函数赋值给了scheduledHostCallback, 借助messageChannelPort.postMessage,在下一个宏任务调用了performWorkUntilDeadline

 const performWorkUntilDeadline = () => { 
    if (scheduledHostCallback !== null) { 
      const currentTime = getCurrentTime(); 
      // Yield after `yieldInterval` ms, regardless of where we are in the vsync 
      // cycle. This means there's always time remaining at the beginning of 
      // the message event. 
      // 设置该任务的一个过期时间, yieldInterval 默认为5ms 
      deadline = currentTime + yieldInterval; 
      const hasTimeRemaining = true; 
      try { 
      // 判断是否还有剩余的任务 
        const hasMoreWork = scheduledHostCallback( 
          hasTimeRemaining, 
          currentTime, 
        ); 
        // 没有剩余的任务,就退出调度 
        if (!hasMoreWork) { 
          isMessageLoopRunning = false; 
          scheduledHostCallback = null; 
        } else { 
        // 有剩余的任务,调用post.postMessage 在下一次宏任务处理 
          // If there's more work, schedule the next message event at the end 
          // of the preceding one. 
          port.postMessage(null); 
        } 
      } catch (error) { 
        // If a scheduler task throws, exit the current browser task so the 
        // error can be observed. 
        port.postMessage(null); 
        throw error; 
      } 
    } else { 
      isMessageLoopRunning = false; 
    } 
    // Yielding to the browser will give it a chance to paint, so we can 
    // reset this. 
    needsPaint = false; 
  }; 
   
 // yieldInterval 是可改变的,scheduler提供对外的api 
 forceFrameRate = function(fps) { 
    if (fps < 0 || fps > 125) { 
      // Using console['error'] to evade Babel and ESLint 
      console['error']( 
        'forceFrameRate takes a positive int between 0 and 125, ' + 
          'forcing frame rates higher than 125 fps is not supported', 
      ); 
      return; 
    } 
    if (fps > 0) { 
      yieldInterval = Math.floor(1000 / fps); 
    } else { 
      // reset the framerate 
      yieldInterval = 5; 
    } 
 }; 

下一次宏任务到来,performWorkUntilDeadline 会被调用,flushWork 作为scheduleHostCallback也会被调用

function flushWork(hasTimeRemaining, initialTime) { 
  // We'll need a host callback the next time work is scheduled. 
  isHostCallbackScheduled = false; 
  // 这个版本只会isHostCallbackScheduled 为true 
  if (isHostTimeoutScheduled) { 
    // We scheduled a timeout but it's no longer needed. Cancel it. 
    isHostTimeoutScheduled = false; 
    cancelHostTimeout(); 
  } 
 
  isPerformingWork = true; 
  const previousPriorityLevel = currentPriorityLevel; 
  try { 
 
    // 会返回true/false,说明是否剩余的任务需要执行 
    return workLoop(hasTimeRemaining, initialTime); 
 
  } finally { 
    currentTask = null; 
    currentPriorityLevel = previousPriorityLevel; 
    isPerformingWork = false; 
  } 
} 

如果taskQueue有任务需要处理,会调用workLoop 里面的while循环进行时间分片处理

每次while循环的退出,都是一次时间分片,有两种情况退出while循环

  1. taskQueue的所有任务在5ms都被清空了,退出循环
  2. 每将要执行一个新任务,判断下时间片内的时间是否用光了:currentTime >= deadline ,如果这个任务也没达到过期时间,那么就跳出循环,等待下一次时间分片进行执行,但是如果任务过期了,则马上执行,避免该任务一直被delay
  3. whilte循环的退出,可能还存在单个任务执行时间过长,业务调用方自己主动退出了,会返回下次执行的回调函数: 如 React Concurrent 模式

React 的更新模式

www.yuque.com/guang-nzhrr…

React 的合成事件

import React,{ useCallback } from 'react'; 
 
function App() { 
 const handleClick = useCallback((event) => { 
     console.log(event) 
 },[]) 
 return <div onClick={handleClick}>click it</div> 
} 

正如上面的代码,我们对div的点击触发的handleClick事件,其实是React本身自己处理好事件之后派发给我们的fiber的props.onClick属性: handleClick,俗称 React的合成事件。