关于前端:React-架构的演变-更新机制

41次阅读

共计 16454 个字符,预计需要花费 42 分钟才能阅读完成。

后面的文章剖析了 Concurrent 模式下异步更新的逻辑,以及 Fiber 架构是如何进行工夫分片的,更新过程中的很多内容都省略了,评论区也收到了一些同学对更新过程的纳闷,明天的文章就来解说下 React Fiber 架构的更新机制。

Fiber 数据结构

咱们先回顾一下 Fiber 节点的数据结构(之前文章省略了一部分属性,所以和之前文章略有不同):

function FiberNode (tag, key) {
  // 节点 key,次要用于了优化列表 diff
  this.key = key
  // 节点类型;FunctionComponent: 0, ClassComponent: 1, HostRoot: 3 ...
  this.tag = tag

    // 子节点
  this.child = null
  // 父节点
  this.return = null 
  // 兄弟节点
  this.sibling = null
  
  // 更新队列,用于暂存 setState 的值
  this.updateQueue = null
  // 新传入的 props
  this.pendingProps = pendingProps;
  // 之前的 props
  this.memoizedProps = null;
  // 之前的 state
  this.memoizedState = null;

  // 节点更新过期工夫,用于工夫分片
  // react 17 改为:lanes、childLanes
  this.expirationTime = NoLanes
  this.childExpirationTime = NoLanes

  // 对应到页面的实在 DOM 节点
  this.stateNode = null
  // Fiber 节点的正本,能够了解为备胎,次要用于晋升更新的性能
  this.alternate = null

  // 副作用相干,用于标记节点是否须要更新
  // 以及更新的类型:替换成新节点、更新属性、更新文本、删除……
  this.effectTag = NoEffect
  // 指向下一个须要更新的节点
  this.nextEffect = null
  this.firstEffect = null
  this.lastEffect = null
}

缓存机制

能够留神到 Fiber 节点有个 alternate 属性,该属性在节点初始化的时候默认为空(this.alternate = null)。这个节点的作用就是用来缓存之前的 Fiber 节点,更新的时候会判断 fiber.alternate 是否为空来确定以后是首次渲染还是更新。上面咱们上代码:

import React from 'react';
import ReactDOM from 'react-dom';

class App extends React.Component {state = { val: 0}
  render() {return <div>val: { this.state.val}</div>
  }
}

ReactDOM.unstable_createRoot(document.getElementById('root')
).render(<App />)

在调用 createRoot 的时候,会学生成一个FiberRootNode,在 FiberRootNode 下会有个 current 属性,current 指向 RootFiber 能够了解为一个空 Fiber。后续调用的 render 办法,就是将传入的组件挂载到 FiberRootNode.current(即 RootFiber)的空 Fiber 节点上。

// 试验版本对外裸露的 createRoot 须要加上 `unstable_` 前缀
exports.unstable_createRoot = createRoot

function createRoot(container) {return new ReactDOMRoot(container)
}
function ReactDOMRoot(container) {var root = new FiberRootNode()
  // createRootFiber => createFiber => return new FiberNode(tag);
  root.current = createRootFiber() // 挂载一个空的 fiber 节点
  this._internalRoot = root
}
ReactDOMRoot.prototype.render = function render(children) {
  var root = this._internalRoot
  var update = createUpdate()
  update.payload = {element: children}
  const rootFiber = root.current
  // update 对象放到 rootFiber 的 updateQueue 中
  enqueueUpdate(rootFiber, update)
  // 开始更新流程
  scheduleUpdateOnFiber(rootFiber)
}

render 最初调用 scheduleUpdateOnFiber 进入更新工作,该办法之前有阐明,最初会通过 scheduleCallback 走 MessageChannel 音讯进入下个工作队列,最初调用 performConcurrentWorkOnRoot 办法。

// scheduleUpdateOnFiber
// => ensureRootIsScheduled
// => scheduleCallback(performConcurrentWorkOnRoot)
function performConcurrentWorkOnRoot(root) {renderRootConcurrent(root)
}
function renderRootConcurrent(root) {
  // workInProgressRoot 为空,则创立 workInProgress
  if (workInProgressRoot !== root) {createWorkInProgress()
  }
}
function createWorkInProgress() {
  workInProgressRoot = root
  var current = root.current
  var workInProgress = current.alternate;
  if (workInProgress === null) {
    // 第一次构建,须要创立正本
    workInProgress = createFiber(current.tag)
    workInProgress.alternate = current
    current.alternate = workInProgress
  } else {
    // 更新过程能够复用
    workInProgress.nextEffect = null
    workInProgress.firstEffect = null
    workInProgress.lastEffect = null
  }
}

开始更新时,如果 workInProgress 为空会指向一个新的空 Fiber 节点,示意正在进行工作的 Fiber 节点。

workInProgress.alternate = current
current.alternate = workInProgress

结构好 workInProgress 之后,就会开始在新的 RootFiber 下生成新的子 Fiber 节点了。

function renderRootConcurrent(root) {
  // 结构 workInProgress...
  // workInProgress.alternate = current
    // current.alternate = workInProgress

  // 进入遍历 fiber 树的流程
  workLoopConcurrent()}

function workLoopConcurrent() {while (workInProgress !== null && !shouldYield()) {performUnitOfWork()
  }
}

function performUnitOfWork() {
  var current = workInProgress.alternate
  // 返回以后 Fiber 的 child
  const next = beginWork(current, workInProgress)
  // 省略后续代码...
}

依照咱们后面的案例,workLoopConcurrent 调用实现后,最初失去的 fiber 树如下:

class App extends React.Component {state = { val: 0}
  render() {return <div>val: { this.state.val}</div>
  }
}

最初进入 Commit 阶段的时候,会切换 FiberRootNode 的 current 属性:

function performConcurrentWorkOnRoot() {renderRootConcurrent() // 完结遍历流程,fiber tree 曾经结构结束

  var finishedWork = root.current.alternate
  root.finishedWork = finishedWork
  commitRoot(root)
}
function commitRoot() {
  var finishedWork = root.finishedWork
  root.finishedWork = null
  root.current = finishedWork // 切换到新的 fiber 树
}

下面的流程为第一次渲染,通过 setState({val: 1}) 更新时,workInProgress 会切换到 root.current.alternate

function createWorkInProgress() {
  workInProgressRoot = root
  var current = root.current
  var workInProgress = current.alternate;
  if (workInProgress === null) {
    // 第一次构建,须要创立正本
    workInProgress = createFiber(current.tag)
    workInProgress.alternate = current
    current.alternate = workInProgress
  } else {
    // 更新过程能够复用
    workInProgress.nextEffect = null
    workInProgress.firstEffect = null
    workInProgress.lastEffect = null
  }
}

在后续的遍历过程中(workLoopConcurrent()),会在旧的 RootFiber 下构建一个新的 fiber tree,并且每个 fiber 节点的 alternate 都会指向 current fiber tree 下的节点。

这样 FiberRootNode 的 current 属性就会轮流在两棵 fiber tree 不停的切换,即达到了缓存的目标,也不会过分的占用内存。

更新队列

在 React 15 里,屡次 setState 会被放到一个队列中,期待一次更新。

// setState 办法挂载到原型链上
ReactComponent.prototype.setState = function (partialState, callback) {
  // 调用 setState 后,会调用外部的 updater.enqueueSetState
  this.updater.enqueueSetState(this, partialState)
};

var ReactUpdateQueue = {enqueueSetState(component, partialState) {
    // 在组件的 _pendingStateQueue 上暂存新的 state
    if (!component._pendingStateQueue) {component._pendingStateQueue = []
    }
    // 将 setState 的值放入队列中
    var queue = component._pendingStateQueue
    queue.push(partialState)
    enqueueUpdate(component)
  }
}

同样在 Fiber 架构中,也会有一个队列用来寄存 setState 的值。每个 Fiber 节点都有一个 updateQueue 属性,这个属性就是用来缓存 setState 值的,只是构造从 React 15 的数组变成了链表构造。

无论是首次 Render 的 Mount 阶段,还是 setState 的 Update 阶段,外部都会调用 enqueueUpdate 办法。

// --- Render 阶段 ---
function initializeUpdateQueue(fiber) {
  var queue = {
    baseState: fiber.memoizedState,
    firstBaseUpdate: null,
    lastBaseUpdate: null,
    shared: {pending: null},
    effects: null
  }
  fiber.updateQueue = queue
}
ReactDOMRoot.prototype.render = function render(children) {
  var root = this._internalRoot
  var update = createUpdate()
  update.payload = {element: children}
  const rootFiber = root.current
  // 初始化 rootFiber 的 updateQueue
  initializeUpdateQueue(rootFiber)
  // update 对象放到 rootFiber 的 updateQueue 中
  enqueueUpdate(rootFiber, update)
  // 开始更新流程
  scheduleUpdateOnFiber(rootFiber)
}

// --- Update 阶段 ---
Component.prototype.setState = function (partialState, callback) {this.updater.enqueueSetState(this, partialState)
}
var classComponentUpdater = {enqueueSetState: function (inst, payload) {
    // 获取实例对应的 fiber
    var fiber = get(inst)
    var update = createUpdate()
    update.payload = payload

    // update 对象放到 rootFiber 的 updateQueue 中
    enqueueUpdate(fiber, update)
    scheduleUpdateOnFiber(fiber)
  }
}

enqueueUpdate 办法的次要作用就是将 setState 的值挂载到 Fiber 节点上。

function enqueueUpdate(fiber, update) {
  var updateQueue = fiber.updateQueue;

  if (updateQueue === null) {
    // updateQueue 为空则跳过
    return;
  }
  var sharedQueue = updateQueue.shared;
  var pending = sharedQueue.pending;

  if (pending === null) {update.next = update;} else {
    update.next = pending.next;
    pending.next = update;
  }

  sharedQueue.pending = update;
}

屡次 setState 会在 sharedQueue.pending 上造成一个单向循环链表,具体例子更形象的展现下这个链表构造。

class App extends React.Component {state = { val: 0}
  click () {for (let i = 0; i < 3; i++) {this.setState({ val: this.state.val + 1})
    }
  }
  render() {return <div onClick={() => {this.click()
    }}>val: {this.state.val}</div>
  }
}

点击 div 之后,会间断进行三次 setState,每次 setState 都会更新 updateQueue。

更新过程中,咱们遍历下 updateQueue 链表,能够看到后果与预期的统一。

let $pending = sharedQueue.pending
// 遍历链表,在控制台输入 payload
while($pending) {console.log('update.payload', $pending.payload)
  $pending = $pending.next
}

递归 Fiber 节点

Fiber 架构下每个节点都会经验 递(beginWork) 归(completeWork)两个过程:

  • beginWork:生成新的 state,调用 render 创立子节点,连贯以后节点与子节点;
  • completeWork:根据 EffectTag 收集 Effect,结构 Effect List;

先回顾下这个流程:

function workLoopConcurrent() {while (workInProgress !== null && !shouldYield()) {performUnitOfWork()
  }
}

function performUnitOfWork() {
  var current = workInProgress.alternate
  // 返回以后 Fiber 的 child
  const next = beginWork(current, workInProgress)
  if (next === null) { // child 不存在
    completeUnitOfWork()} else { // child 存在
    // 重置 workInProgress 为 child
    workInProgress = next
  }
}
function completeUnitOfWork() {
  // 向上回溯节点
  let completedWork = workInProgress
  while (completedWork !== null) {
    // 收集副作用,次要是用于标记节点是否须要操作 DOM
    var current = completedWork.alternate
    completeWork(current, completedWork)

    // 省略结构 Effect List 过程

    // 获取 Fiber.sibling
    let siblingFiber = workInProgress.sibling
    if (siblingFiber) {
      // sibling 存在,则跳出 complete 流程,持续 beginWork
      workInProgress = siblingFiber
      return
    }

    completedWork = completedWork.return
    workInProgress = completedWork
  }
}

递(beginWork)

先看看 beginWork 进行了哪些操作:

function beginWork(current, workInProgress) {if (current !== null) { // current 不为空,示意须要进行 update
    var oldProps = current.memoizedProps // 原先传入的 props
    var newProps = workInProgress.pendingProps // 更新过程中新的 props
    // 组件的 props 发生变化,或者 type 发生变化
    if (oldProps !== newProps || workInProgress.type !== current.type) {
      // 设置更新标记位为 true
      didReceiveUpdate = true
    }
  } else { // current 为空示意首次加载,须要进行 mount
    didReceiveUpdate = false
  }
  
  // tag 示意组件类型,不必类型的组件调用不同办法获取 child
  switch(workInProgress.tag) {
    // 函数组件
    case FunctionComponent:
      return updateFunctionComponent(current, workInProgress, newProps)
    // Class 组件
    case ClassComponent:
      return updateClassComponent(current, workInProgress, newProps)
    // DOM 原生组件(div、span、button……)case HostComponent:
      return updateHostComponent(current, workInProgress)
    // DOM 文本组件
    case HostText:
      return updateHostText(current, workInProgress)
  }
}

首先判断 current(即:workInProgress.alternate) 是否存在,如果存在示意须要更新,不存在就是首次加载,didReceiveUpdate 变量设置为 false,didReceiveUpdate 变量用于标记是否须要调用 render 新建 fiber.child,如果为 false 就会从新构建fiber.child,否则复用之前的 fiber.child

而后会根据 workInProgress.tag 调用不同的办法构建 fiber.child。对于 workInProgress.tag 的含意能够参考 react/packages/shared/ReactWorkTags.js,次要是用来辨别每个节点各自的类型,上面是罕用的几个:

var FunctionComponent = 0; // 函数组件
var ClassComponent = 1; // Class 组件
var HostComponent = 5; // 原生组件
var HostText = 6; // 文本组件

调用的办法不一一开展解说,咱们只看看 updateClassComponent

// 更新 class 组件
function updateClassComponent(current, workInProgress, newProps) {
  // 更新 state,省略了一万行代码,只保留了外围逻辑,看看就好
  var oldState = workInProgress.memoizedState
  var newState = oldState

  var queue = workInProgress.updateQueue
  var pendingQueue = queue.shared.pending
  var firstUpdate = pendingQueue
  var update = pendingQueue

  do {
    // 合并 state
    var partialState = update.payload
    newState = Object.assign({}, newState, partialState)

    // 链表遍历结束
    update = update.next
    if (update === firstUpdate) {
        // 链表遍历结束
      queue.shared.pending = null
      break
    }
  } while (true)

    workInProgress.memoizedState = newState // state 更新结束
  
  // 检测 oldState 和 newState 是否统一,如果统一,跳过更新
  // 调用 componentWillUpdate 判断是否须要更新
  

  var instance = workInProgress.stateNode
  instance.props = newProps
  instance.state = newState

  // 调用 Component 实例的 render
  var nextChildren = instance.render()
  reconcileChildren(current, workInProgress, nextChildren)
  return workInProgress.child
}

首先遍历了之前提到的 updateQueue 更新 state,而后就是判断 state 是否更新,以此来推到组件是否须要更新(这部分代码省略了),最初调用的组件 render 办法生成子组件的虚构 DOM。最初的 reconcileChildren 就是根据 render 的返回值来生成 fiber 节点并挂载到 workInProgress.child 上。

// 结构子节点
function reconcileChildren(current, workInProgress, nextChildren) {if (current === null) {
    workInProgress.child = mountChildFibers(workInProgress, null, nextChildren)
  } else {
    workInProgress.child = reconcileChildFibers(workInProgress, current.child, nextChildren)
  }
}

// 两个办法实质上一样,只是一个须要生成新的 fiber,一个复用之前的
var reconcileChildFibers = ChildReconciler(true)
var mountChildFibers = ChildReconciler(false)

function ChildReconciler(shouldTrackSideEffects) {return function (returnFiber, currentChild, nextChildren) {
    // 不同类型进行不同的解决
    // 返回对象
    if (typeof newChild === 'object' && newChild !== null) {
            return placeSingleChild(
        reconcileSingleElement(returnFiber, currentChild, newChild)
      )
    }
    // 返回数组
    if (Array.isArray(newChild)) {// ...}
    // 返回字符串或数字,表明是文本节点
    if (
      typeof newChild === 'string' ||
      typeof newChild === 'number'
    ) {// ...}
    // 返回 null,间接删除节点
    return deleteRemainingChildren(returnFiber, currentChild)
  }
}

篇幅无限,看看 render 返回值为对象的状况(通常状况下,render 办法 return 的如果是 jsx 都会被转化为虚构 DOM,而虚构 DOM 必然是对象或数组):

if (typeof newChild === 'object' && newChild !== null) {
  return placeSingleChild(
    // 结构 fiber,或者是复用 fiber
    reconcileSingleElement(returnFiber, currentChild, newChild)
  )
}

function placeSingleChild(newFiber) {
  // 更新操作,须要设置 effectTag
  if (shouldTrackSideEffects && newFiber.alternate === null) {newFiber.effectTag = Placement}
  return newFiber
}

归(completeWork)

fiber.child 为空时,就会进入 completeWork 流程。而 completeWork 次要就是收集 beginWork 阶段设置的 effectTag,如果有设置 effectTag 就表明该节点产生了变更,effectTag 的次要类型如下(默认为 NoEffect,示意节点无需进行操作,残缺的定义能够参考 react/packages/shared/ReactSideEffectTags.js):

export const NoEffect = /*                     */ 0b000000000000000;
export const PerformedWork = /*                */ 0b000000000000001;

// You can change the rest (and add more).
export const Placement = /*                    */ 0b000000000000010;
export const Update = /*                       */ 0b000000000000100;
export const PlacementAndUpdate = /*           */ 0b000000000000110;
export const Deletion = /*                     */ 0b000000000001000;
export const ContentReset = /*                 */ 0b000000000010000;
export const Callback = /*                     */ 0b000000000100000;
export const DidCapture = /*                   */ 0b000000001000000;

咱们看看 completeWork 过程中,具体进行了哪些操作:

function completeWork(current, workInProgress) {switch (workInProgress.tag) {
    // 这些组件没有反馈到 DOM 的 effect,跳过解决
    case Fragment:
    case MemoComponent:
    case LazyComponent:
    case ContextConsumer:
    case FunctionComponent:
      return null
    // class 组件
    case ClassComponent: {
      // 解决 context
      var Component = workInProgress.type
      if (isContextProvider(Component)) {popContext(workInProgress)
      }
      return null
    }
    case HostComponent: {
      // 这里 Fiber 的 props 对应的就是 DOM 节点的 props
      // 例如:id、src、className ……
          var newProps = workInProgress.pendingProps // props
      if (
        current !== null &&
        workInProgress.stateNode != null
      ) { // current 不为空,示意是更新操作
        var type = workInProgress.type
        updateHostComponent(current, workInProgress, type, newProps)
      } else { // current 为空,示意须要渲染 DOM 节点
        // 实例化 DOM,挂载到 fiber.stateNode
        var instance = createInstance(type, newProps)
        appendAllChildren(instance, workInProgress, false, false);
        workInProgress.stateNode = instance
      }
      return null
    }
    case HostText: {
      var newText = workInProgress.pendingProps // props
      if (current && workInProgress.stateNode != null) {
        var oldText = current.memoizedProps
        // 更新文本节点
        updateHostText(current, workInProgress, oldText, newText)
      } else {
        // 实例文本节点
        workInProgress.stateNode = createTextInstance(newText)
      }
      return null
    }
  }
}

beginWork 一样,completeWork 过程中也会根据 workInProgress.tag 来进行不同的解决,其余类型的组件根本能够略过,只用关注下 HostComponentHostText,这两种类型的节点会反馈到实在 DOM 中,所以会有所解决。

updateHostComponent = function (current, workInProgress, type, newProps) {
  var oldProps = current.memoizedProps

  if (oldProps === newProps) {
    // 新旧 props 无变动
    return
  }

  var instance = workInProgress.stateNode // DOM 实例
  // 比照新旧 props
    var updatePayload = diffProperties(instance, type, oldProps, newProps)
  // 将发生变化的属性放入 updateQueue
  // 留神这里的 updateQueue 不同于 Class 组件对应的 fiber.updateQueue
  workInProgress.updateQueue = updatePayload
};

updateHostComponent 办法最初会通过 diffProperties 办法获取一个更新队列,挂载到 fiber.updateQueue 上,这里的 updateQueue 不同于 Class 组件对应的 fiber.updateQueue,不是一个链表构造,而是一个数组构造,用于更新实在 DOM。

上面举一个例子,批改 App 组件的 state 后,上面的 span 标签对应的 data-valstylechildren 都会相应的产生批改,同时,在控制台打印出 updatePayload 的后果。

import React from 'react'

class App extends React.Component {state = { val: 1}
  clickBtn = () => {this.setState({ val: this.state.val + 1})
  }
  render() {
    return (<div>
      <button onClick={this.clickBtn}>add</button>
      <span
        data-val={this.state.val}
        style={{fontSize: this.state.val * 15}}
      >
        {this.state.val}
      </span>
    </div>)
  }
}

export default App

副作用链表

在最初的更新阶段,为了不必遍历所有的节点,在 completeWork 过程完结后,会结构一个 effectList 连贯所有 effectTag 不为 NoEffect 的节点,在 commit 阶段可能更高效的遍历节点。

function completeUnitOfWork() {
  let completedWork = workInProgress
  while (completedWork !== null) {// 调用 completeWork()...

    // 结构 Effect List 过程
    var returnFiber = completedWork.return
    if (returnFiber !== null) {if (returnFiber.firstEffect === null) {returnFiber.firstEffect = completedWork.firstEffect;}
      if (completedWork.lastEffect !== null) {if (returnFiber.lastEffect !== null) {returnFiber.lastEffect.nextEffect = completedWork.firstEffect;}
        returnFiber.lastEffect = completedWork.lastEffect;
      }

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

    // 判断 completedWork.sibling 是否存在...
  }
}

下面的代码就是结构 effectList 的过程,光看代码还是比拟难了解的,咱们还是通过理论的代码来解释一下。

import React from 'react'

export default class App extends React.Component {state = { val: 0}
  click = () => {this.setState({ val: this.state.val + 1})
  }
  render() {const { val} = this.state
    const array = Array(2).fill()
    const rows = array.map((_, row) => <tr key={row}>
        {array.map((_, col) => <td key={col}>{val}</td>
        )}
      </tr>
    )
    return <table onClick={() => this.click()}>
      {rows}
    </table>
  }
}

咱们结构一个 2 * 2 的 Table,每次点击组件,td 的 children 都会产生批改,上面看看这个过程中的 effectList 是如何变动的。

第一个 td 实现 completeWork 后,EffectList 后果如下:

第二个 td 实现 completeWork 后,EffectList 后果如下:

两个 td 完结了 completeWork 流程,会回溯到 tr 进行 completeWork,tr 完结流程后,table 会间接复用 tr 的 firstEffect 和 lastEffect,EffectList 后果如下:

前面两个 td 完结 completeWork 流程后,EffectList 后果如下:

回溯到第二个 tr 进行 completeWork,因为 table 曾经存在 firstEffect 和 lastEffect,这里会间接批改 table 的 firstEffect 的 nextEffect,以及从新指定 lastEffect,EffectList 后果如下:

最初回溯到 App 组件时,就会间接复用 table 的 firstEffect 和 lastEffect,最初 的 EffectList 后果如下:

提交更新

这一阶段的次要作用就是遍历 effectList 外面的节点,将更新反馈到实在 DOM 中,当然还波及一些生命周期钩子的调用,咱们这里只展现最简略的逻辑。

function commitRoot(root) {
  var finishedWork = root.finishedWork
  var firstEffect = finishedWork
  var nextEffect = firstEffect
  // 遍历 effectList
  while (nextEffect !== null) {
    const effectTag = nextEffect.effectTag
    // 依据 effectTag 进行不同的解决
    switch (effectTag) {
      // 插入 DOM 节点
      case Placement: {commitPlacement(nextEffect)
        nextEffect.effectTag &= ~Placement
        break
      }
      // 更新 DOM 节点
      case Update: {
        const current = nextEffect.alternate
        commitWork(current, nextEffect)
        break
      }
      // 删除 DOM 节点
      case Deletion: {commitDeletion(root, nextEffect)
        break
      }
    }
    nextEffect = nextEffect.nextEffect
  }
}

这里不再开展解说每个 effect 下具体的操作,在遍历完 effectList 之后,就是将以后的 fiber 树进行切换。

function commitRoot() {
  var finishedWork = root.finishedWork

  // 遍历 effectList ……

  root.finishedWork = null
  root.current = finishedWork // 切换到新的 fiber 树
}

总结

到这里整个更新流程就完结了,能够看到 Fiber 架构下,所有数据结构都是链表模式,链表的遍历都是通过循环的形式来实现的,看代码的过程中常常会被忽然呈现的 return、break 扰乱思路,所以要齐全了解这个流程还是很不容易的。

最初,心愿大家在阅读文章的过程中能有播种,下一篇文章会开始写 Hooks 相干的内容。

正文完
 0