react源码架构

fagaMarch 22, 2022About 28 min

react源码架构

React15架构

React15架构分为两层:

  • reconciler(协调器):负责找出组件变化
  • renderer(渲染器):负责操作dom,将变化的地方渲染到页面

Reconciler

reconciler主要做了这么几件事:

  • 执行函数式组件或者class组件的render方法,拿到其返回值的jsx生成虚拟DOM(虚拟DOM其实本质上就是js对象,其中包括了所有dom的属性,以及props,state)
  • 通过深度优先遍历比较上次虚拟DOM与这次虚拟DOM的区别
  • 通知renderer将变化的虚拟DOM渲染

Renderer

有虚拟DOM的存在,就可以实现跨平台的功能,通过不同的渲染器,渲染在不同平台上。我们前端最熟悉的是负责在浏览器环境渲染的Renderer —— ReactDOM。

  • ReactNative (opens new window)渲染器,渲染App原生组件
  • ReactTest (opens new window)渲染器,渲染出纯Js对象用于测试
  • ReactArt (opens new window)渲染器,渲染到Canvas, SVG VML (IE8)

React15架构的缺点

由于递归执行,所以更新一旦开始,中途就无法中断。当层级很深时,递归更新时间超过了16ms,用户交互就会卡顿。因此在React16版本提出了concurrent模式,该模式用可中断的异步更新替代同步更新。

React16架构

React16架构可以分为三层:

  • Scheduler(调度器)—— 调度任务的优先级,高优任务优先进入Reconciler
  • Reconciler(协调器)—— 负责找出变化的组件
  • Renderer(渲染器)—— 负责将变化的组件渲染到页面上

React16架构是在React15架构上增加了一个Scheduler,Scheduler做的事情就是当浏览器有时间的时候通知我们,因此在协调器里面代码改为了

function workLoopConcurrent() {
  // Perform work until Scheduler asks us to yield
  while (workInProgress !== null && !shouldYield()) {
    workInProgress = performUnitOfWork(workInProgress);
  }
}

通过浏览器是否有时间来判断是否应该终端递归

在React15中Reconciler与Renderer是交替工作的,检查到某个节点更新了就会交给Renderer渲染,在React16中,ReconcilerRenderer不再是交替工作。当Scheduler将任务交给Reconciler后,Reconciler会为变化的虚拟DOM打上代表增/删/更新的标记,类似这样:

export const Placement = /*             */ 0b0000000000010;
export const Update = /*                */ 0b0000000000100;
export const PlacementAndUpdate = /*    */ 0b0000000000110;
export const Deletion = /*    

整个SchedulerReconciler的工作都在内存中进行。只有当所有组件都完成Reconciler的工作,才会统一交给Renderer

Fiber结构

React15及以前,Reconciler采用递归的方式创建虚拟DOM,递归过程是不能中断的。如果组件树的层级很深,递归会占用线程很多时间,造成卡顿。

为了解决这个问题,React16递归的无法中断的更新重构为异步的可中断更新,由于曾经用于递归的虚拟DOM数据结构已经无法满足需要。于是,全新的Fiber架构应运而生。

function FiberNode(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {
  // 作为静态数据结构的属性
  this.tag = tag;
  this.key = key;
  this.elementType = null;
  this.type = null;
  this.stateNode = null;

  // 用于连接其他Fiber节点形成Fiber树
  this.return = null;
  this.child = null;
  this.sibling = null;
  this.index = 0;

  this.ref = null;

  // 作为动态的工作单元的属性(保存了本次更新该组件改变的状态,要执行的工作)
  this.pendingProps = pendingProps;
  this.memoizedProps = null;
  this.updateQueue = null;
  this.memoizedState = null;
  this.dependencies = null;

  this.mode = mode;

  this.effectTag = NoEffect;
  this.nextEffect = null;

  this.firstEffect = null;
  this.lastEffect = null;

  // 调度优先级相关
  this.lanes = NoLanes;
  this.childLanes = NoLanes;

  // 指向该fiber在另一次更新时对应的fiber
  this.alternate = null;
}

Fiber双缓存

在React中最多会同时存在两颗Fiber树。当前屏幕上显示内容对应的Fiber树称为current Fiber树,正在内存中构建的Fiber树称为workInProgress Fiber树,他们通过alternate`属性连接。

currentFiber.alternate === workInProgressFiber;
workInProgressFiber.alternate === currentFiber;

workInProgress Fiber树构建完成交给Renderer渲染在页面上后,应用根节点的current指针指向workInProgress Fiber树,此时workInProgress Fiber树就变为current Fiber树

每次状态更新都会产生新的workInProgress Fiber树,通过currentworkInProgress的替换,完成DOM更新。

首次执行ReactDOM.render会创建fiberRootNode(源码中叫fiberRoot)和rootFiber。其中fiberRootNode是整个应用的根节点,rootFiber<App/>所在组件树的根节点。

rootFiber

一个应用中只有一个fiberRoot,但可以有多个rootFiber,rootFiber是通过ReactDom.render创建的

深入理解jsx

JSX在编译时会被Babel编译为React.createElement方法。

export function createElement(type, config, children) {
  let propName;

  const props = {};

  let key = null;
  let ref = null;
  let self = null;
  let source = null;

  if (config != null) {
    // 将 config 处理后赋值给 props
    // ...省略
  }

  const childrenLength = arguments.length - 2;
  // 处理 children,会被赋值给props.children
  // ...省略

  // 处理 defaultProps
  // ...省略

  return ReactElement(
    type,
    key,
    ref,
    self,
    source,
    ReactCurrentOwner.current,
    props,
  );
}

const ReactElement = function(type, key, ref, self, source, owner, props) {
  const element = {
    // 标记这是个 React Element
    $$typeof: REACT_ELEMENT_TYPE,

    type: type,
    key: key,
    ref: ref,
    props: props,
    _owner: owner,
  };

  return element;
};

React.createElement 方法会返回一个包含组件数据的对象,对象有个参数$$typeof: REACT_ELEMENT_TYPE标记了该对象是个React Element

JSX是一种描述当前组件内容的数据结构,他不包含组件schedulereconcilerender所需的相关信息。

比如如下信息就不包括在JSX中:

  • 组件在更新中的优先级
  • 组件的state
  • 组件被打上的用于Renderer标记

所以,在组件mount时,Reconciler根据JSX描述的组件内容生成组件对应的Fiber节点

update时,ReconcilerJSXFiber节点保存的数据对比,生成组件对应的Fiber节点,并根据对比结果为Fiber节点打上标记

Render阶段

react源码8.1

render阶段 的入口函数是performSyncWorkOnRootperformConcurrentWorkOnRoot 这取决于同步更新还是异步更新。

// performSyncWorkOnRoot会调用该方法
function workLoopSync() {
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

// performConcurrentWorkOnRoot会调用该方法
function workLoopConcurrent() {
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}

performConcurrentWorkOnRoot 多了个判断浏览器帧是否有时间。shouldYield会终止循环,知道浏览器有空闲时间后再继续遍历。

performUnitOfWork 方法会创建下一个Fiber节点,并将workInProgress与其子Fiber节点连接。performUnitOfwork分为两个过程,“递”和“归”

递阶段:

rootFiber向下深度优先遍历,并调用beginWork方法。

该方法做的事情是为传入的Fiber节点创建其子Fiber节点,并将两个Fiber节点连接起来。

当遍历到叶子节点(即没有子组件的组件)时就会进入“归”阶段。

归过程:

在“归”阶段会调用completeWork处理Fiber节点。当某个Fiber节点执行完completeWork,如果其存在兄弟Fiber节点(即fiber.sibling !== null),会进入其兄弟Fiber的“递”阶段。

如果不存在兄弟Fiber,会进入父级Fiber的“归”阶段。

“递”和“归”阶段会交错执行直到“归”到rootFiber。至此,render阶段的工作就结束了。

beginwork

image-20220226153155497

beginWork的工作可以分为两部分:

  • update时:如果current存在,在满足一定条件时可以复用current节点,这样就能克隆current.child作为workInProgress.child,而不需要新建workInProgress.child。如果不能复用就进入到reconcileChildren,通过diff算法生成带effectTag的子Fiber节点
  • mount时:除fiberRootNode以外,current === null。会根据fiber.tag不同,创建不同类型的子Fiber节点。mount会直接进入到reconcileChildren函数,并且生成其子节点。
function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes
): Fiber | null {

  // update时:如果current存在可能存在优化路径,可以复用current(即上一次更新的Fiber节点)
  if (current !== null) {
    // ...省略

    // 复用current
    return bailoutOnAlreadyFinishedWork(
      current,
      workInProgress,
      renderLanes,
    );
  } else {
    didReceiveUpdate = false;
  }

  // mount时:根据tag不同,创建不同的子Fiber节点
  switch (workInProgress.tag) {
    case IndeterminateComponent: 
      // ...省略
    case LazyComponent: 
      // ...省略
    case FunctionComponent: 
      // ...省略
    case ClassComponent: 
      // ...省略
    case HostRoot:
      // ...省略
    case HostComponent:
      // ...省略
    case HostText:
      // ...省略
    // ...省略其他类型
  }
}

diff算法

diff算法需要判断新节点是不是单节点,单节点只有两种情况oldfiber链是否为空,如果为空那就新建一个节点,如果不为空,就找到和之前key相同的节点,删除其余节点

多节点则需要分为四种情况:节点更新,新增节点,删除节点,节点移动,多节点更新需要经过最多三轮的遍历(不过感觉是两轮的样子),每一轮都是上轮结束的断点的继续。

第一轮遍历会从头开始遍历newChildren,会逐个与oldFiber链中的节点进行比较,如果说key和tag都没有变化,那么就clone props更新的节点,props使用新的props,实现节点更新。

有变化则认为不是节点更新,直接进入下一轮循环。

我们称保留原位的节点为固定节点,第一轮遍历如果没有跳出循环的话就会设置一个lastPlaceIndex,用来记录最右边的固定节点。

旧: A - B - C - D - E 新: A - B - C

删除节点:当新节点遍历完之后如果oldFibers还没遍历完,就会删除后续没遍历完的节点。

旧: A - B - C 新: A - B - C - D - E

新增节点:和删除节点类似,如果新节点没遍历完,那么新增后续节点。

旧 A - B - C - D - E - F 新 A - B - D - C - E

节点移动:先将剩余oldFibers放入key为键,值为oldFiber节点的map中,成为existingChildren.

开始遍历newChildren,如果oldFiber在lastPlaceIndex右边,则代表他对顺序没有影响,则更新lastPlaceIndex = max(index,lastPlaceIndex),接着删除map中的节点,如果oldFiber的index<lastPlaceIndex,那么认为它是需要移动的,把它移动到最右端,删除map中的节点。如果遍历完成之后,existingChildren中还有节点,那么就直接删除,同样,如果有新的节点(existingChildren中没有的)那么就会新增这个节点。

effectTag

effectTag是Fiber的一个属性,记录了commit阶段需要对其进行的操作。

// DOM需要插入到页面中
export const Placement = /*                */ 0b00000000000010;
// DOM需要更新
export const Update = /*                   */ 0b00000000000100;
// DOM需要插入到页面中并更新
export const PlacementAndUpdate = /*       */ 0b00000000000110;
// DOM需要删除
export const Deletion = /*                 */ 0b00000000001000;

如果想要通知Renderer要操作Fiber对应的DOM节点,那么就需要你有fiber有对应的DOM节点以及effectTag上有对应的操作,但是在mount时fiber.stateNode===null,并且在reconcileChildren中也没有为其添加effectTag,那么首屏渲染该如何完成呢?

fiber.stateNode会在completeWork的时候创建,但mount时是不会给每个Fiber的effectTag赋值placement的,因为在commit阶段每次进行一次插入操作的效率是很低的,为了解决这个问题,在mount时只有rootFiber会赋值Placement effectTag,在commit阶段只会执行一次插入操作。

completeWork

update

update时Fiber节点已经存在对应DOM节点,因此只需要处理props,比如:

  • onClickonChange等回调函数的注册
  • 处理style prop
  • 处理DANGEROUSLY_SET_INNER_HTML prop
  • 处理children prop
if (current !== null && workInProgress.stateNode != null) {
  // update的情况
  updateHostComponent(
    current,
    workInProgress,
    type,
    newProps,
    rootContainerInstance,
  );
}

被处理完的props会被赋值给workInProgress.updateQueue,并最终会在commit阶段被渲染在页面上。

workInProgress.updateQueue = (updatePayload: any);

updatePayload为数组形式,偶数记录了props的key,奇数记录了props的value

mount

同样,我们省略了不相关的逻辑。可以看到,mount时的主要逻辑包括三个:

  • Fiber节点生成对应的DOM节点
  • 将子孙DOM节点插入刚生成的DOM节点
  • update逻辑中的updateHostComponent类似的处理props的过程
const currentHostContext = getHostContext();
// 为fiber创建对应DOM节点
const instance = createInstance(
    type,
    newProps,
    rootContainerInstance,
    currentHostContext,
    workInProgress,
  );
// 将子孙DOM节点插入刚生成的DOM节点中
appendAllChildren(instance, workInProgress, false, false);
// DOM节点赋值给fiber.stateNode
workInProgress.stateNode = instance;

// 与update逻辑中的updateHostComponent类似的处理props的过程
if (
  finalizeInitialChildren(
    instance,
    type,
    newProps,
    rootContainerInstance,
    currentHostContext,
  )
) {
  markUpdate(workInProgress);
}

在归操作的过程中每次都会将生成自己的DOM节点,接着把子DOM节点插入刚生成的DOM节点中,这样在归操作结束后,我们就已经构建好了一个DOM树,这时只需要在rootFiber加上Placement effectTag就可以把整颗DOM树添加到页面中去了。

effectList

有一个问题:commit阶段的任务是修改的需要操作的Fiber节点对应的DOM节点,但是commit阶段需要再次遍历Fiber树查看其effectTag来判断需要如何更新吗,显然这样效率很低。

为了解决这个问题,在completeWork的上层函数completeUnitOfWork中,每个执行完completeWork且存在effectTagFiber节点会被保存在一条被称为effectList的单向链表中。

render阶段流程结束,将fiberRoot传入commitRoot()中,开始commit阶段

commit阶段

commit阶段的主要工作(即Renderer的工作流程)分为三部分:

  • before mutation阶段(执行DOM操作前)
  • mutation阶段(执行DOM操作)
  • layout阶段(执行DOM操作后)

合成事件

为什么需要有合成事件

  • 进行浏览器兼容,实现更好的跨平台

    React 采用的是顶层事件代理机制,能够保证冒泡一致性,可以跨浏览器执行。React 提供的合成事件用来抹平不同浏览器事件对象之间的差异,将不同平台事件模拟合成事件。

  • 避免垃圾回收

    事件对象可能会被频繁创建和回收,因此 React 引入事件池,在事件池中获取或释放事件对象。即 React 事件对象不会被释放掉,而是存放进一个数组中,当事件触发,就从这个数组中弹出,避免频繁地去创建和销毁(垃圾回收)

  • 方便事件统一管理和事务机制

  • 原生事件React 事件
    事件名称命名方式名称全部小写
    (onclick, onblur)
    名称采用小驼峰
    (onClick, onBlur)
    事件处理函数语法字符串函数
    阻止默认行为方式事件返回 false使用 e.preventDefault() 方法

合成事件触发流程

当真实 DOM 元素触发事件,会冒泡到 document 对象后,再处理 React 事件;所以会先执行原生事件,然后处理 React 事件;最后真正执行 document 上挂载的事件。采用的是事件委托。

React事件池

每次我们使用事件对象,在函数执行后会通过releaseTopLevelCallbackBookKeeping将事件对象释放到事件池中,这样的好处就是 不用每次都创建事件对象,可以从事件池中取出一个事件源对象进行复用,在事件处理函数执行完毕后,会释放事件对象到事件池中,清空属性,这就是setTimeout中打印为什么是null的原因了。

事件注册

在源码中提到过,render阶段的completeWork会对Fiber节点的props进行处理,这里就包括了对事件的处理

function setInitialDOMProperties(
  tag: string,
  domElement: Element,
  rootContainerElement: Element | Document,
  nextProps: Object,
  isCustomComponentTag: boolean,
): void {
  for (const propKey in nextProps) {
    if (!nextProps.hasOwnProperty(propKey)) {
      ...
    } else if (registrationNameDependencies.hasOwnProperty(propKey)) {
        // 如果propKey属于事件类型,则进行事件绑定
        ensureListeningTo(rootContainerElement, propKey, domElement);
      }
    }
  }
}

registrationNameDependencies是一个对象,用来判断该props是否为一个事件。

ensureListerningTo()函数来执行事件绑定,他会通过事件名称创建不同的优先级Listener(root上绑定的就是这个带有优先级的监听器),还会根据名称判断是在捕获还是冒泡阶段触发

  // 根据事件名称,创建不同优先级的事件监听器。
  let listener = createEventListenerWrapperWithPriority(
    targetContainer,
    domEventName,
    eventSystemFlags,
    listenerPriority,
  );

  // 绑定事件
  if (isCapturePhaseListener) {
    ...
    unsubscribeListener = addEventCaptureListener(
      targetContainer,
      domEventName,
      listener,
    );
  } else {
    ...
    unsubscribeListener = addEventBubbleListener(
      targetContainer,
      domEventName,
      listener,
    );

  }

事件触发

事件触发的流程:首先是对事件对象的合成

  // 构造合成事件对象
  const event = new SyntheticEvent(
    reactName,
    null,
    nativeEvent,
    nativeEventTarget,
    EventInterface,
  );

原生事件只是合成事件的一个属性,它还包括更多的属性,但说白了这和就是用来描述这个事件的,比如说位置啊,组件名啊啥的。事件对象合成完毕之后,会从触发该事件的节点一直往上,判断是否有绑定这个事件,如果有那就把它的事件处理函数收集起来push进一个数组(执行路径)中,事件执行时这些事件处理函数会共用这同一个合成事件,并且改变其currentTarget,以及阻止其冒泡。

当我们点击了一个按钮,listener就调用了dispatchEventsForPlugins函数

function dispatchEventsForPlugins(
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  nativeEvent: AnyNativeEvent,
  targetInst: null | Fiber,
  targetContainer: EventTarget,
): void {
  const nativeEventTarget = getEventTarget(nativeEvent);
  const dispatchQueue: DispatchQueue = [];

  // 事件对象的合成,收集事件到执行路径上
  extractEvents(
    dispatchQueue,
    domEventName,
    targetInst,
    nativeEvent,
    nativeEventTarget,
    eventSystemFlags,
    targetContainer,
  );

  // 执行收集到的组件中真正的事件
  processDispatchQueue(dispatchQueue, eventSystemFlags);
}

这个函数体可看成两部分:事件对象的合成和事件收集事件执行,涵盖了上述三个过程。

dispatchQueue,它承载了本次合成的事件对象和收集到事件执行路径上的事件处理函数。

extractEvents()做了两件事:构造合成事件以及收集事件路径上的事件处理函数

accumulateSinglePhaseListeners用来事件收集

export function accumulateSinglePhaseListeners(
  targetFiber: Fiber | null,
  dispatchQueue: DispatchQueue,
  event: ReactSyntheticEvent,
  inCapturePhase: boolean,
  accumulateTargetOnly: boolean,
): void {

  // 根据事件名来识别是冒泡阶段的事件还是捕获阶段的事件
  const bubbled = event._reactName;
  const captured = bubbled !== null ? bubbled + 'Capture' : null;

  // 声明存放事件监听的数组
  const listeners: Array<DispatchListener> = [];

  // 找到目标元素
  let instance = targetFiber;

  // 从目标元素开始一直到root,累加所有的fiber对象和事件监听。
  while (instance !== null) {
    const {stateNode, tag} = instance;

    if (tag === HostComponent && stateNode !== null) {
      const currentTarget = stateNode;

      // 事件捕获
      if (captured !== null && inCapturePhase) {
        // 从fiber中获取事件处理函数
        const captureListener = getListener(instance, captured);
        if (captureListener != null) {
          listeners.push(
            createDispatchListener(instance, captureListener, currentTarget),
          );
        }
      }

      // 事件冒泡
      if (bubbled !== null && !inCapturePhase) {
        // 从fiber中获取事件处理函数
        const bubbleListener = getListener(instance, bubbled);
        if (bubbleListener != null) {
          listeners.push(
            createDispatchListener(instance, bubbleListener, currentTarget),
          );
        }
      }
    }
    instance = instance.return;// 其父节点
  }
  // 收集事件对象
  if (listeners.length !== 0) {
    dispatchQueue.push(createDispatchEntry(event, listeners));
  }
}

dispatchQueue的机构:

[
  {
    event: SyntheticEvent,
    listeners: [ listener1, listener2, ... ]
  }
]

listeren就是遍历得到的事件处理函数,由于是同一个事件,listener会共享这个事件,比如传参的时候就会传入这个事件处理函数。

合成事件会将事件都绑定到root上,react17之前是document,在render阶段的completeWork阶段时会判断该prop是不是事件进行相应处理。在绑定过程中,会在root上绑定带有优先级的listerner,带有targetContainer,domEventName,listerner(这个listerner带有容器名和事件名),在接下来触发事件的时候会有一个数组称他为执行路径,触发事件时,会从触发事件的那个元素开始一直往上收集绑定在fiber节点身上的事件,并push到执行路径里,事件收集完毕,事件开始执行,事件执行会根据是事件冒泡还是事件捕获来确定遍历顺序,每执行一个事件监听函数,就可以更改公用的合成事件上的currentTarget.

https://segmentfault.com/a/1190000039108951

对函数式组件的理解

函数式组件的本质只是一个函数而已,只是经过react的封装,让他能够渲染成dom,因此每次更新和创建组件都是执行一次该函数,所以对hooks更应该从执行函数的角度来理解。函数式组件应该是一个纯函数。

useEffect

先提供一段简单的useEffect的代码

import React, { useState, useEffect } from "react";

// 该组件定时从服务器获取好友的在线状态
function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }
    // 在浏览器渲染结束后执行
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);

    // 在每次渲染产生的 effect 执行之前执行
    return function cleanup() {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };

    // 只有 props.friend.id 更新了才会重新执行这个 hook
  }, [props.friend.id]);

  if (isOnline === null) {
    return "Loading...";
  }
  return isOnline ? "Online" : "Offline";
}

useEffect可以看作是componentDidMount/componentDidUpdate/componentWillUnmount 这三个生命周期函数的替代。但不是完全相同。

useEffect叫做副作用函数,因此他应该是放副作用函数的hook,函数式组件是一个纯函数,因此,在有副作用的时候我们就需要用到useEffect,因此我们不能把useEffect和componentDidMount/componentDidUpdate/componentWillUnmount 这三个生命周期函数等同起来。

useEffect是异步调用的,componenDidMount是同步调用的。

react源码分为三个部分:

调度器

协调器

渲染器

协调器会为需要更新的fiber打上标签,并生成一条effectList,渲染器根据effectList执行对应的操作。当渲染器遍历到该fiber,并且有passive标记,那么就会执行其useEffect

而协调器会从上往下一次遍历,再从下往上遍历,在从下往上的过程中会生成effectList,因此effectList的顺序是从下往上的,因此当一个组件创建之后,会从下往上调用useEffect

useEffect有两个参数,第一个参数传入一个函数,第二个参数传入一个依赖数组,当依赖数组中的内容变化时,(可以简单地理解为vue中的watch,即使好多人认为不能这么理解)将会执行第一个参数中的函数,第一个参数的return必须是一个函数,用于组件销毁时调用。

依赖数组为空和不传入第二个参数的区别,不传入第二个参数就会在每次组件创建和组件更新时调用,而依赖数组为空只会在组件刚创建的时候调用。

源码分析useEffect和useLayoutEffect

hooks链表

当函数组件进入render阶段时,会被renderWithHooks函数处理。函数组件作为一个函数,它的渲染其实就是函数调用,而函数组件又会调用React提供的hooks函数。初始挂载和更新时,所用的hooks函数是不同的,比如初次挂载时调用的useEffect,和后续更新时调用的useEffect,虽然都是同一个hook,但是因为在两个不同的渲染过程中调用它们,所以本质上他们两个是不一样的。这种不一样来源于函数组件要维护一个hooks的链表,初次挂载时要创建链表,后续更新的时候要更新链表。

每次函数组件调用hooks函数的时候,都会生成一个hook对象

// hook对象
{
    baseQueue: null,
    baseState: 'hook1',
    memoizedState: null,
    queue: null,
    next: {
        baseQueue: null,
        baseState: null,
        memoizedState: 'hook2',
        next: null
        queue: null
    }
}

每个hook对象的next指向下一个生成的hook对象形成一个hooks链表,hooks链表最终会放到fiber节点的memoizedState属性上。

挂载时,组件上没有任何hooks的信息,所以,这个过程主要是在fiber上创建hooks链表。

更新时,这时已经有了一个current 树,因此我们可以通过workInProgress.alternate,获取到current节点,再拿到memoizedState上的hooks链表,这样就可以获取到之前创建的hook对象,新的hook对象可以根据它来构建,还可以获得一些信息,比如useEffect的依赖项。

Effect数据结构

use(Layout)Effect会在调用后会创建一个effect对象,存储到hook.memorizedState上,不同的hooks函数放在memorizedState上的值是不同的

const UseEffectExp = () => {
    const [ text, setText ] = useState('hello')
    useEffect(() => {
        console.log('effect1')
        return () => {
            console.log('destory1');
        }
    })
    useLayoutEffect(() => {
        console.log('effect2')
        return () => {
            console.log('destory2');
        }
    })
    return <div>effect</div>
}

preview

useState放的就是state值,而use(Layout)Effect放的就是effect对象

单个的effect对象包括以下几个属性:

  • create: 传入use(Layout)Effect函数的第一个参数,即回调函数
  • destroy: 回调函数return的函数,在该effect销毁的时候执行
  • deps: 依赖项
  • next: 指向下一个effect
  • tag: effect的类型,区分是useEffect还是useLayoutEffect,以及是否需要更新
fiber.memoizedState ---> useState hook
                             |
                             |
                            next
                             |
                             ↓
                        useEffect hook
                        memoizedState: useEffect的effect对象 ---> useLayoutEffect的effect对象
                             |              ↑__________________________________|
                             |
                            next
                             |
                             ↓
                        useLayoutffect hook
                        memoizedState: useLayoutEffect的effect对象 ---> useEffect的effect对象
                                            ↑___________________________________|

effect对象除了会挂载到fiber.memoizedState上,还会保存在fiber的updateQueue,updateQueue链表是在completeWork阶段根据props的不同而创建的链表,将来如果这个节点需要更新,那么就会遍历这个fiber节点的updateQueue。updateQueue是一个环状链表,挂载或者更新时会把effect链表放到updateQueue后面

流程概述

基于上面的数据结构,对于use(Layout)Effect来说,React做的事情就是

  • render阶段:函数组件开始渲染的时候,创建出对应的hook链表挂载到workInProgress的memoizedState上,并创建effect链表,但是基于上次和本次依赖项的比较结果, 创建的effect是有差异的。这一点暂且可以理解为:依赖项有变化,effect可以被处理,否则不会被处理。
  • commit阶段:异步调度useEffect,layout阶段同步处理useLayoutEffect的effect。等到commit阶段完成,更新应用到页面上之后,开始处理useEffect产生的effect。

第二点提到了一个重点,就是useEffect和useLayoutEffect的执行时机不一样,前者被异步调度,当页面渲染完成后再去执行,不会阻塞页面渲染。 后者是在commit阶段新的DOM准备完成,但还未渲染到屏幕之前,同步执行。

实现细节

在commit阶段,会有三个地方调度useEffect:

  • commit开始时:这和useEffect异步调度的特点有关,它以一般的优先级被调度,这就意味着一旦上一次更新有更高优先级的任务进入到commit阶段,上一次任务的useEffect就有可能没得到执行。所以在本次更新开始前,需要先将之前的useEffect都执行掉,以保证本次调度的useEffect都是本次更新产生的。
  • beforeMutation阶段:这个是实打实地针对effectList上有副作用的节点,去异步调度useEffect。在实现上利用scheduler的异步调度函数:scheduleCallback,将执行useEffect的动作作为一个任务去调度,这个任务会异步调用。
  • layout阶段layout阶段填充effect执行数组:真正useEffect执行的时候,实际上是先执行上一次effect的销毁,再执行本次effect的创建。React用两个数组来分别存储销毁函数和 创建函数,这两个数组的填充就是在layout阶段,到时候循环释放执行两个数组中的函数即可。

在填充effect执行数组时,只把有HasEffect tag的才会被添加进执行数组,这是因为在生成effect对象的时候,会根据依赖项是否有变化,来确定tag,如果没有变化,那么tag就是hooksFlags(useEffect或者useLayoutEffect),如果变化了就是HasEffect tag。

总结

在每次执行useEffect函数时,都会创建一个effect对象,但是挂载和更新时调用的是两个函数,render阶段,在挂载时会直接创建一个hook对象,并且创建effect对象挂载到hook对象还有updateQueue上,在更新时会根据hook对象上的effect对比前后两次的依赖项,如果依赖项有变化tag就会加入HookHasEffect标志位,接着把effect挂载到upateQueue上。commit阶段,在commit开始时由于useEffect是一般优先级被调度,因此可能会被打断,因此要先把之前的useEffect执行掉,beforeMutation会异步调度effectList上有副作用的节点,layout阶段会根据tag是否有HasEffectTag判断是否加入执行数组,先循环destory函数,再循环create函数。useLayoutEffect的destory和create分别在mutation和layout阶段同步执行。

https://zhuanlan.zhihu.com/p/346696902

useMemo和useCallback

useMemo和useCallback都是用来缓存的,由于每次状态改变都会重新执行一次App函数,但如果修改的状态和我们想要传递的值无关,我们就希望将其缓存起来

useCallback

import { useState, useMemo } from "react";
import "./styles.css";

export default function App() {
  const [count, setCount] = useState(0);
  const [total, setTotal] = useState(0);

  // 没有使用 useMemo,即使是更新 total, countToString 也会重新计算
  const countToString = (() => {
    console.log("countToString 被调用");
    return count.toString();
  })();

  // 使用了 useMemo, 只有 total 改变,才会重新计算
  const totalToStringByMemo = useMemo(() => {
    console.log("totalToStringByMemo 被调用");
    return total + "";
  }, [total]);

  return (
    <div className="App">
      <h3>countToString: {countToString}</h3>
      <h3>countToString: {totalToStringByMemo}</h3>
      <button
        onClick={() => {
          setCount((count) => count + 1);
        }}
      >
        Add Count
      </button>
      <br />
      <button
        onClick={() => {
          setTotal((total) => total + 1);
        }}
      >
        Add Total
      </button>
    </div>
  );
}

useMemo

import React, { useCallback, useEffect, useState } from "react";
import "./styles.css";

export default function App() {
  const [count, setCount] = useState(0);

  // 使用 useCallBack 缓存
  const handleCountAddByCallBack = useCallback(() => {
    setCount((count) => count + 1);
  }, []);

  // 不缓存,每次 count 更新时都会重新创建
  const handleCountAdd = () => {
    setCount((count) => count + 1);
  };

  return (
    <div className="App">
      <h3>CountAddByChild1: {count}</h3>
      <Child1 addByCallBack={handleCountAddByCallBack} add={handleCountAdd} />
    </div>
  );
}

const Child1 = React.memo(function (props) {
  const { add, addByCallBack } = props;
  
  // 没有缓存,由于每次都创建,memo 认为两次地址都不同,属于不同的函数,所以会触发 useEffect
  useEffect(() => {
    console.log("Child1----addFcUpdate", props);
  }, [add]);

  // 有缓存,memo 判定两次地址都相同,所以不触发 useEffect
  useEffect(() => {
    console.log("Child1----addByCallBackFcUpdate", props);
  }, [addByCallBack]);

  return (
    <div>
      <button onClick={props.add}>+1</button>
      <br />
      <button onClick={props.addByCallBack}>+1(addByCallBack)</button>
    </div>
  );
});

useRef

我们点击按钮让stateNumber和numRef都加1

function incrementAndDelayLogging() {
		  // 点击按钮 stateNumber + 1
        setStateNumber(stateNumber + 1)
		  // 同时 ref 对象的 current 属性值也 + 1
        numRef.current++
		  // 定时器函数中产生了闭包, 这里 stateNumber 的是组件更新前的 stateNumber 对象, 所以值一直会滞后 1(setState是异步的)
        setTimeout(
            () => alert(`state: ${stateNumber} | ref: ${numRef.current}`),
            1000
        )
 }

useRef就是一个容器,它可以放dom节点,也可以放数据,在用来放数据的时候主要是用来获取由于异步操作加闭包造成的渲染不及时的问题,并且修改ref不会造成组件重新render。

React.memo

const MyComponent = React.memo(function MyComponent(props) {
  /* 使用 props 渲染 */
});

如果组件在传入props相同情况下返回的是相同的,那么我们可以使用就React.memo。

React.memo和useCallback一定要搭配使用,缺少了一个可能会导致性能不降反升。

useReducer

const DemoUseReducer = ()=>{
    /* number为更新后的state值,  dispatchNumbner 为当前的派发函数 */
   const [ number , dispatchNumbner ] = useReducer((state,action)=>{
       const { payload , name  } = action
       /* return的值为新的state */
       switch(name){
           case 'add':
               return state + 1
           case 'sub':
               return state - 1 
           case 'reset':
             return payload       
       }
       return state
   },0)
   return <div>
      当前值:{ number }
      { /* 派发更新 */ }
      <button onClick={()=>dispatchNumbner({ name:'add' })} >增加</button>
      <button onClick={()=>dispatchNumbner({ name:'sub' })} >减少</button>
      <button onClick={()=>dispatchNumbner({ name:'reset' ,payload:666 })} >赋值</button>
      { /* 把dispatch 和 state 传递给子组件  */ }
      <MyChildren  dispatch={ dispatchNumbner } State={{ number }} />
   </div>
}

useReducer会返回一个数组,数组的第一个是状态,第二个是dispatch,需要给useReducer传入一个回调函数,回调函数第一个是state,第二个是dispatch传入的数据,回调函数的返回值是新state

useContext

import React, { createContext, useContext, useReducer, useState } from 'react'
import ReactDOM from 'react-dom'

// 创造一个上下文
const C = createContext(null);

function App(){
  const [n,setN] = useState(0)
  return(
    // 指定上下文使用范围,使用provider,并传入读数据和写入据
    <C.Provider value={{n,setN}}>
      这是爷爷
      <Baba></Baba>
    </C.Provider>
  )
}

function Baba(){
  return(
    <div>
      这是爸爸
      <Child></Child>
    </div>
  )
}
function Child(){
  // 使用上下文,因为传入的是对象,则接受也应该是对象
  const {n,setN} = useContext(C)
  const add=()=>{
    setN(n=>n+1)
  };
  return(
    <div>
      这是儿子:n:{n}
      <button onClick={add}>+1</button>
    </div>
  )
}


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

useState

useState可以传入函数,其初始state是函数执行的返回值

react-activation

import React, { Component, createContext } from 'react'

const { Provider, Consumer } = createContext()
const withScope = WrappedComponent => props => (
  <Consumer>{keep => <WrappedComponent {...props} keep={keep} />}</Consumer>
)
// 使用Context 拿到keep函数,用来修改AliveScope的state

export class AliveScope extends Component {
  nodes = {}
  state = {}

  keep = (id, children) =>
    new Promise(resolve =>
      this.setState(
        {
          [id]: { id, children }
        },
        () => resolve(this.nodes[id])
      )
    )

  render() {
    return (
      <Provider value={this.keep}>
        {this.props.children}
        // 将想要keepalive的组件打破层级关系,与app同层级渲染,后面会在keepalive组件中下面这个ref,并把下面的元素放到keepalive里面
        {Object.values(this.state).map(({ id, children }) => (
          <div
            key={id}
            ref={node => {
              this.nodes[id] = node
            }}
          >
             {children}
          </div>
        ))}
      </Provider>
    )
  }
}

@withScope // 高阶组件
class KeepAlive extends Component {
  constructor(props) {
    super(props)
    this.init(props)
  }

  init = async ({ id, children, keep }) => {
    const realContent = await keep(id, children)
    // 给div添加保存在Alivescope的children
    this.placeholder.appendChild(realContent)
  }

  render() {
    return (
    // 在这里div里面并没有内容,内容来源于init
      <div
        ref={node => {
          this.placeholder = node
        }}
      />
    )
  }
}

export default KeepAlive

把keepalive 的children 保存到最上层的AliveScope里,再把keepalive的children渲染到和app同一层级下,接着在keepalive中拿到dom,通过appendChild放回keepalive组件中

错误边界

过去,组件内的 JavaScript 错误会导致 React 的内部状态被破坏,并且在下一次渲染时产生可能无法追踪的错误。

部分 UI 的 JavaScript 错误不应该导致整个应用崩溃,为了解决这个问题,React 16 引入了一个新的概念 —— 错误边界。

相当于js的catch 但他是一个组件,如果一个class组件定义了getDerivedStateFromError()或componentDidCatch()生命周期,那么它就会被认定为是一个错误边界。当抛出错误后,请使用 static getDerivedStateFromError() 渲染备用 UI ,使用 componentDidCatch() 打印错误信息。

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    // 更新 state 使下一次渲染能够显示降级后的 UI
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    // 你同样可以将错误日志上报给服务器
    logErrorToMyService(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      // 你可以自定义降级后的 UI 并渲染
      return <h1>Something went wrong.</h1>;
    }

    return this.props.children; 
  }
}

然后你可以将它作为一个常规组件去使用:

<ErrorBoundary>
  <MyWidget />
</ErrorBoundary>
Last update:
Contributors: lzc
Comments
  • Latest
  • Oldest
  • Hottest
Powered by Waline v2.13.0