5

前言

最近在准备面试。复习了一些react的知识点,特此总结。

开始

React 生命周期

react 16以前的生命周期是这样的

组件在首次渲染时会被实例化,然后调用实例上面的componentWillMount,render和componentDidMount函数。组件在更新渲染时可以调用componentWillReceiveProps,shouldComponentUpdate,componentWillUpdate,render和componentDidUpdate函数。组件在卸载时可以调用componentWillUnmount函数。

借图:

image.png

从 React v16.3 开始,React 建议使用getDerivedStateFromPropsgetSnapshotBeforeUpdate两个生命周期函数替代 componentWillMountcomponentWillReceivePropscomponentWillUpdate三个生命周期函数。这里需要注意的是 新增的两个生命周期 函数和原有的三个生命周期函数必须分开使用,不能混合使用

目前的生命周期(借图):

image.png

componentWillMount存在的问题

有人认为在componentWillMount中可以提前进行异步请求,避免白屏。但是react在调用render渲染页面的时候,render并不会等待异步请求结束,再获取数据渲染。这么写是有潜在隐患的。

而在react fiber之后 可能在一次渲染中多次调用。原因是:react fiber技术使用增量渲染来解决掉帧的问题,通过requestIdleCallback调度执行每个任务单元,可以中断和恢复,生命周期一旦中断,恢复之后会重新跑一次之前的生命周期

新的生命周期

static getDerivedStateFromProps

  • 触发时间(v16.4修正):组件每次被rerender的时候,包括在组件构建之后(render之前最后执行),每次获取新的props或state之后。在v16.3版本时,组件state的更新不会触发该生命周期
  • 每次接收新的props之后都会返回一个对象作为新的state,返回null则说明不需要更新state
  • 配合componentDidUpdate,可以覆盖componentWillReceiveProps的所有用法

getSnapshotBeforeUpdate

  • 触发时间: update发生的时候,在render之后,在组件dom渲染之前。
  • 返回一个值,作为componentDidUpdate的第三个参数。
  • 配合componentDidUpdate, 可以覆盖componentWillUpdate的所有用法。

React Fiber

由于React渲染/更新过程一旦开始无法中断,持续占用主线程,主线程忙于执行JS,无暇他顾(布局、动画),造成掉帧、延迟响应(甚至无响应)等不佳体验。fiber应运而生。

Fiber 是对react reconciler(调和) 核心算法的重构。关键特性如下:

  • 增量渲染(把渲染任务拆分成块,匀到多帧)
  • 更新时能够暂停,终止,复用渲染任务
  • 给不同类型的更新赋予优先级
  • 并发方面新的基础能力

增量渲染用来解决掉帧的问题,渲染任务拆分之后,每次只做一小段,做完一段就把时间控制权交还给主线程,而不像之前长时间占用。

Fiber tree

  • Fiber之前的reconciler(被称为Stack reconciler)自顶向下的递归mount/update,无法中断(持续占用主线程),这样主线程上的布局、动画等周期性任务以及交互响应就无法立即得到处理,影响体验。
  • Fiber解决这个问题的思路是把渲染/更新过程(递归diff)拆分成一系列小任务,每次检查树上的一小部分,做完看是否还有时间继续下一个任务,有的话继续,没有的话把自己挂起,主线程不忙的时候再继续。

fiber树其实是一个单链表结构,child指向第一个子节点,return指向父节点,sibling指向下个兄弟节点。结构如下:

// fiber tree节点结构
{
    stateNode,
    child,
    return,
    sibling,
    ...
}

Fiber reconciler

reconcile过程分为2个阶段:

1.(可中断)render/reconciliation 通过构造workInProgress tree得出change

2.(不可中断)commit 应用这些DOM change(更新DOM树、调用组件生命周期函数以及更新ref等内部状态)

构建workInProgress tree的过程就是diff的过程,通过requestIdleCallback来调度执行一组任务,每完成一个任务后回来看看有没有插队的(更紧急的),每完成一组任务,把时间控制权交还给主线程,直到下一次requestIdleCallback回调再继续构建workInProgress tree

生命周期也被分成了两个阶段:

// 第1阶段 render/reconciliation
componentWillMount
componentWillReceiveProps
shouldComponentUpdate
componentWillUpdate

// 第2阶段 commit
componentDidMount
componentDidUpdate
componentWillUnmount

第1阶段的生命周期函数可能会被多次调用,默认以low优先级执行,被高优先级任务打断的话,稍后重新执行。

fiber tree与workInProgress tree

双缓冲技术:指的是workInProgress tree构造完毕,得到的就是新的fiber tree,然后把current指针指向workInProgress tree,由于fiber与workInProgress互相持有引用,旧fiber就作为新fiber更新的预留空间,达到复用fiber实例的目的。

每个fiber上都有个alternate属性,也指向一个fiber,创建workInProgress节点时优先取alternate,没有的话就创建一个

let workInProgress = current.alternate;
if (workInProgress === null) {
  //...
  workInProgress.alternate = current;
  current.alternate = workInProgress;
} else {
  // We already have an alternate.
  // Reset the effect tag.
  workInProgress.effectTag = NoEffect;

  // The effect list is no longer valid.
  workInProgress.nextEffect = null;
  workInProgress.firstEffect = null;
  workInProgress.lastEffect = null;
}

这么做的好处:

  • 能够复用内部对象(fiber)
  • 节省内存分配、GC的时间开销

fiber 中断 恢复

中断:检查当前正在处理的工作单元,保存当前成果(firstEffect, lastEffect),修改tag标记一下,迅速收尾并再开一个requestIdleCallback,下次有机会再做

断点恢复:下次再处理到该工作单元时,看tag是被打断的任务,接着做未完成的部分或者重做

P.S.无论是时间用尽“自然”中断,还是被高优任务粗暴打断,对中断机制来说都一样。

React setState

在代码中调用setState函数之后,React 会将传入的参数对象与组件当前的状态合并,然后触发所谓的调和过程(Reconciliation)。经过调和过程,React 会以相对高效的方式根据新的状态构建 React 元素树并且着手重新渲染整个UI界面。在 React 得到元素树之后,React 会自动计算出新的树与老树的节点差异,然后根据差异对界面进行最小化重渲染。在差异计算算法中,React 能够相对精确地知道哪些位置发生了改变以及应该如何改变,这就保证了按需更新,而不是全部重新渲染。

setState调用时有时是同步的(settimeout,自定义dom事件),有时是异步的(普通调用)

React 事件机制

React事件是通过事件代理,在最外层的 document上对事件进行统一分发,并没有绑定在真实的 Dom节点上。
而且react内部对原生的Event对象进行了包裹处理。具有与浏览器原生事件相同的接口,包括 stopPropagation()preventDefault()

image.png

React 更新队列

如果有多个同步setState(...)操作,React 会将它们的更新(update)先后依次加入到更新队列(updateQueue),在应用程序的 render 阶段处理更新队列时会将队列中的所有更新合并成一个,合并原则是相同属性的更新取最后一次的值。如果有异步setState(...)操作,则先进行同步更新,异步更新则遵循 EventLoop 原理后续处理。

React 更新

// 源码位置:packages/react-reconciler/src/ReactUpdateQueue.js
function createUpdate(expirationTime, suspenseConfig) {
  var update = {
    // 过期时间与任务优先级相关联
    expirationTime: expirationTime,
    suspenseConfig: suspenseConfig,
        // tag用于标识更新的类型如UpdateState,ReplaceState,ForceUpdate等
    tag: UpdateState,
    // 更新内容
    payload: null,
    // 更新完成后的回调
    callback: null,
        // 下一个更新(任务)
    next: null,
    // 下一个副作用
    nextEffect: null
  };
  {
    // 优先级会根据任务体系中当前任务队列的执行情况而定
    update.priority = getCurrentPriorityLevel();
  }
  return update;
}

每一个更新对象都有自己的过期时间(expirationTime)、更新内容(payload),优先级(priority)以及指向下一个更新的引用(next)。其中当前更新的优先级由任务体系统一指定。

React 更新队列

// 源码位置:packages/react-reconciler/src/ReactUpdateQueue.js
function createUpdateQueue(baseState) {
  var queue = {
    // 当前的state
    baseState: baseState,
    // 队列中第一个更新
    firstUpdate: null,
    // 队列中的最后一个更新
    lastUpdate: null,
    // 队列中第一个捕获类型的update
    firstCapturedUpdate: null,
    // 队列中第一个捕获类型的update
    lastCapturedUpdate: null,
    // 第一个副作用
    firstEffect: null,
    // 最后一个副作用
    lastEffect: null,
    firstCapturedEffect: null,
    lastCapturedEffect: null
  };
  return queue;
}

这是一个单向链表结构。

当我们使用setState()时, React 会创建一个更新(update)对象,然后通过调用enqueueUpdate函数将其加入到更新队列(updateQueue)

// 源码位置:packages/react-reconciler/src/ReactUpdateQueue.js
// 每次setState都会创建update并入updateQueue
function enqueueUpdate(fiber, update) {
  // 每个Fiber结点都有自己的updateQueue,其初始值为null,一般只有ClassComponent类型的结点updateQueue才会被赋值
  // fiber.alternate指向的是该结点在workInProgress树上面对应的结点
  var alternate = fiber.alternate;
  var queue1 = void 0;
  var queue2 = void 0;
  if (alternate === null) {
    // 如果fiber.alternate不存在
    queue1 = fiber.updateQueue;
    queue2 = null;
    if (queue1 === null) {
      queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState);
    }
  } else {
    // 如果fiber.alternate存在,也就是说存在current树上的结点和workInProgress树上的结点都存在
    queue1 = fiber.updateQueue;
    queue2 = alternate.updateQueue;
    if (queue1 === null) {
      if (queue2 === null) {
        // 如果两个结点上面均没有updateQueue,则为它们分别创建queue
        queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState);
        queue2 = alternate.updateQueue = createUpdateQueue(alternate.memoizedState);
      } else {
        // 如果只有其中一个存在updateQueue,则将另一个结点的updateQueue克隆到该结点
        queue1 = fiber.updateQueue = cloneUpdateQueue(queue2);
      }
    } else {
      if (queue2 === null) {
        // 如果只有其中一个存在updateQueue,则将另一个结点的updateQueue克隆到该结点
        queue2 = alternate.updateQueue = cloneUpdateQueue(queue1);
      } else {
        // 如果两个结点均有updateQueue,则不需要处理
      }
    }
  }
  if (queue2 === null || queue1 === queue2) {
    // 经过上面的处理后,只有一个queue1或者queue1 == queue2的话,就将更新对象update加入到queue1
    appendUpdateToQueue(queue1, update);
  } else {
    // 经过上面的处理后,如果两个queue均存在
    if (queue1.lastUpdate === null || queue2.lastUpdate === null) {
      // 只要有一个queue不为null,就需要将将update加入到queue中
      appendUpdateToQueue(queue1, update);
      appendUpdateToQueue(queue2, update);
    } else {
      // 如果两个都不是空队列,由于两个结构共享,所以只在queue1加入update
      appendUpdateToQueue(queue1, update);
      // 仍然需要在queue2中,将lastUpdate指向update
      queue2.lastUpdate = update;
    }
  }
  ...
}
  
function appendUpdateToQueue(queue, update) {
  if (queue.lastUpdate === null) {
    // 如果队列为空,则第一个更新和最后一个更新都赋值当前更新
    queue.firstUpdate = queue.lastUpdate = update;
  } else {
    // 如果队列不为空,将update加入到队列的末尾
    queue.lastUpdate.next = update;
    queue.lastUpdate = update;
  }
}

enqueueUpdate函数中,React 将更新加入到更新队列时会同时维护两个队列对象 queue1 和 queue2,其中 queue1 是应用程序运行过程中 current 树上当前 Fiber 结点最新队列,queue2 是应用程序上一次更新时(workInProgress 树)Fiber 结点的更新队列,它们之间的相互逻辑是下面这样的。

  • queue1 取的是fiber.updateQueue,queue2 取的是fiber.alternate.updateQueue
  • 如果两者均为null,则调用createUpdateQueue(...)获取初始队列;
  • 如果两者之一为null,则调用cloneUpdateQueue(...)从对方中获取队列;
  • 如果两者均不为null,则将update作为lastUpdate加入 queue1 中。

React 处理更新队列

React 应用程序运行到 render 阶段时会处理更新队列,处理更新队列的函数是processUpdateQueue

// 源码位置:packages/react-reconciler/src/ReactUpdateQueue.js
function processUpdateQueue(workInProgress, queue, props, instance, renderExpirationTime) {
  ...
  // 从队列中取出第一个更新
  var update = queue.firstUpdate;
  var resultState = newBaseState;
  // 遍历更新队列,处理更新
  while (update !== null) {
    ...
    // 如果第一个更新不为空,紧接着要遍历更新队列
    // getStateFromUpdate函数用于合并更新,合并方式见下面函数实现
    resultState = getStateFromUpdate(workInProgress, queue, update, resultState, props, instance);
    ...
    update = update.next;
  }
  ...
  // 设置当前fiber结点的memoizedState
  workInProgress.memoizedState = resultState;
  ...
}

// 获取下一个更新对象并与现有state对象合并
function getStateFromUpdate(workInProgress, queue, update, prevState, nextProps, instance) {
  switch (update.tag) {
      case UpdateState:
          {
            var _payload2 = update.payload;
            var partialState = void 0;
            if (typeof _payload2 === 'function') {
              // setState传入的参数_payload2类型是function
              ...
              partialState = _payload2.call(instance, prevState, nextProps);
              ...
            } else {
              // setState传入的参数_payload2类型是object
              partialState = _payload2;
            }
            // 合并当前state和上一个state.
            return _assign({}, prevState, partialState);
      }
  }
}

processUpdateQueue函数用于处理更新队列,在该函数内部使用循环的方式来遍历队列,通过update.next依次取出更新(对象)进行合并,合并更新对象的方式是:

  • 如果setState传入的参数类型是function,则通过payload2.call(instance, prevState, nextProps)获取更新对象;
  • 如果setState传入的参数类型是object,则可直接获取更新对象;
  • 最后通过使用Object.assign()合并两个更新对象并返回,如果属性相同的情况下则取最后一次值。

React 页面渲染

React 应用程序首次渲染时在 prerender 阶段会初始化 current 树。最开始的 current 树只有一个根结点— HostRoot类型的 Fiber 结点。在后面的 render 阶段会根据此时的 current 树创建 workInProgress 树。在 workInProgress 树上面进行一系列运算(计算更新等),最后将副作用列表(Effect List)传入到 commit 阶段。当 commit 阶段运行完成后将当前的 current 树替换为 workInProgress 树,至此一个更新流程就完成了。简述:

  • 在 render 阶段 React 依赖 current 树通过工作循环(workLoop)构建 workInProgress 树;
  • 在 workInProgress 树进行一些更新计算,得到副作用列表(Effect List);
  • 在 commit 阶段将副作用列表渲染到页面后,将 current 树替换为 workInProgress 树(执行current = workInProgress)。

current 树是未更新前应用程序对应的 Fiber 树,workInProgress 树是需要更新屏幕的 Fiber 树。

FiberRootNode构造函数只有一个实例就是 fiberRoot 对象。而每个 Fiber 节点都是 FiberNode 构造函数的实例,它们通过return,child和sibling三个属性连接起来,形成了一个巨大链表。React 对每个节点的更新计算都是在这个链表上完成的。React 在对 Fiber 节点标记更新标识的时候的做法就是为节点的effectTag属性赋不同的值。

React hooks 实现原理

借图:

image.png

react hooks 原理

React useCallback useMemo 区别

这两个api,其实概念上还是很好理解的,一个是「缓存函数」, 一个是缓存「函数的返回值」。

  • 在组件内部,那些会成为其他useEffect依赖项的方法,建议用 useCallback 包裹,或者直接编写在引用它的useEffect中。
  • 己所不欲勿施于人,如果你的function会作为props传递给子组件,请一定要使用 useCallback 包裹,对于子组件来说,如果每次render都会导致你传递的函数发生变化,可能会对它造成非常大的困扰。同时也不利于react做渲染优化。
  • 对于使用高阶函数的场景,建议一律使用 useMemo

React 捕获错误的生命周期

  • componentDidCatch
  • static getDerivedStateFromError()

为什么必须在函数组件顶部作用域调用Hooks API 为什么不能在循环和判断里用?

  • Hook API调用会产生一个对应的Hook实例(并追加到Hooks链),但是返回给组件的是state和对应的setter,re-render时框架并不知道这个setter对应哪个Hooks实例(除非用HashMap来存储Hooks,但这就要求调用的时候把相应的key传给React,会增加Hooks使用的复杂度)。
  • re-render时会从第一行代码开始重新执行整个组件,即会按顺序执行整个Hooks链,因为首次render之后,只能通过useState返回的dispatch修改对应Hook的memoizedState,因此必须要保证Hooks的顺序不变,所以不能在分支调用Hooks,只有在顶层调用才能保证各个Hooks的执行顺序!

一旦在条件语句中声明hooks,在下一次函数组件更新,hooks链表结构,将会被破坏,current树的memoizedState缓存hooks信息,和当前workInProgress不一致,如果涉及到读取state等操作,就会发生异常。

总结:可以认为维护了一个state数组 和一个cursor指针 。第一次渲染时把当前的值push进states数组里,把绑定了指针的setter推进 setters 数组中。每次的后续渲染都会重置指针cursor的位置,并会从每个数组中读取对应的值。每个 setter 都会有一个对应的指针位置的引用,因此当触发任何 setter 调用的时候都会触发去改变状态数组中的对应的值。如果放在条件语句中。那么setstate的顺序就会发生错乱。

      let first = true;
      const [num1, setNum1] = usestate(1);
      if (first) {
        const [num2, setNum2] = usestate(2);
        first = false
      }
      const [num3, setNum3] = usestate(3);

第一次渲染时,维护的states数组时[1,2,3] ,setters数组指针指向[state[0],state[1],state[2]]。但是当我们的组件更新时,这时候的states数组是[1,2,3] setters数组是[state[0],state[1]]。显然我们的setNum3被设置成了2。


greet_eason
482 声望1.4k 粉丝

技术性问题欢迎加我一起探讨:zhi794855679