你应该理解的react知识点(更新于2021-5-10)

前言

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

开始

React 生命周期

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

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

借图:算法

image.png

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

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

image.png

componentWillMount存在的问题浏览器

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

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

新的生命周期并发

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。

React 高阶组件

  • 高阶组件不是组件,它是一个将某个组件转换成另外一个组件的纯函数。
  • 高阶组件的主要做用是实现代码复用和逻辑抽象、对 state 和 props 进行抽象和操做、对组件进行细化(如添加生命周期)、实现渲染劫持等。在实际的业务场景中合理的使用高阶组件,能够提升开发效率和提高代码的可维护性。
  • 高阶组件的实用性 使其频繁地被大量 React.js 相关的第三方库,如 React-Redux的 connect 方法、React-Loadable等所使用,了解高阶组件对咱们理解各类 React.js 第三方库的原理颇有帮助 👍。
  • 高阶组件有两种实现方式,分别是属性代理和反向继承。它能够看做是装饰器模式在 React 中的实现:在不修改原组件的状况下实现组件功能的加强。

React 和 vue对比

vue react 共同点:

  • 使用 Virtual DOM,有本身的diff渲染算法
  • 提供了响应式 (Reactive) 和组件化 (Composable) 的视图组件。
  • 将注意力集中保持在核心库,而将其余功能如路由和全局状态管理交给相关的库。

1. 监听数据变化的实现原理不一样

  • React 默认是经过比较引用的方式进行的,若是不优化(PureComponent/shouldComponentUpdate)可能致使大量没必要要的VDOM的从新渲染
  • Vue 经过 getter/setter 以及一些函数的劫持,能精确知道数据变化,不须要特别的优化就能达到很好的性能

2. 数据流的不一样

  • vue是双向绑定
  • react是单向数据流

3. 组件通讯的区别

4. 模板渲染方式的不一样

  • React是在组件JS代码中,经过原生JS实现模板中的常见语法,好比插值,条件,循环等,都是经过JS语法实现的
  • Vue是在和组件JS代码分离的单独的模板中,经过指令来实现的,好比条件语句就须要 v-if 来实现
相关文章
相关标签/搜索