Skip to content

react18

什么是 React?

  • React 是一个用于构建用户界面的 JavaScript 库

React 能干什么?

  • 可以通过组件化的方式构建 构建快速响应的大型 Web 应用程序

组件化

  • 组件化 把页面拆分为一个个组件,方便视图的拆分和复用,还可以做到高内聚和低耦合

diff算法时间复杂度

  • 传统diff算法:

    • 通过循环递归对节点进行依次对比,算法复杂度达到 O(n^3)
    • 如果要展示1000个节点,得执行上亿次比较。。即便是CPU快能执行30亿条命令,也很难在一秒内计算出差异。
  • React diff:

  • 首先,进行同级比较,并非循环比较。

  • 这样比较次数就降为一层一次,时间复杂度直接降为O(n)

  • 如果同级相同位置节点不一样,则直接删除替换,简单粗暴。

什么是 JSX

TIP

在 React17 以前,babel 是调用的 React.createElement,并且页面需要引入

在 React17 之后,babel 是调用的(react/jsx-runtimejsx)函数,并且页面无需引入

  • jsx 是一个语法糖,即可以写 html 也可以写 js
  • 因为 jsx 在浏览器端是不能被识别的,所以代码在编译阶段会转换成 ReactElement
  • ReactElement 就是我们所说的虚拟节点

createPortal

  • 将组件渲染到对应的容器中ReactDOM.createPortal(child, container)

虚拟的 dom 优缺点

优点

  • 处理了浏览器兼容性问题,避免用户操作真实 DOM
  • 内容经过了 XSS 处理,可以防范 XSS 攻击
  • 容易实现跨平台开发 Android、iOS、VR 应用
  • 更新的时候可以实现差异化更新,减少更新 DOM 的操作

缺点

  • 不利用SEO
  • 首屏加载速度慢

函数组件和类组件的区别

函数组件类组件
设计理念函数式编程面向对象编程
语法形式普通函数、箭头函数React.Component
组件实例不能实例化没有this能实例化,有this
内部状态没有state可以使用,useState代替有state
生命周期没有生命周期,可以使用useEffect代替有生命周期
逻辑复用使用钩子(hooks)定义高阶组件(HOC)
性能优化使用React.memo或useMemo避免组件重复渲染使用shouldComponentUpdate避免组件重复渲染
主要特点轻量灵活,容易拆分功能全面,逻辑散落

类组件的的问题

  • 1、类需要实例化,占用堆内存
  • 2、类的写法比较繁琐,需要继承需要调用父类的构造
  • 3、代码复用比较繁琐,需要高阶组件,或者minxs

受控组件&非受控组件

  • 优先使用受控组件,符合 React 设计原则
  • 必须操作 DOM 时,再使用非受控组件

受控组件

  • 受控组件是指由React组件的状态(state)来控制其表单元素的值或行为的组件。在受控组件中,表单元素的值(如input、textarea、select等)以及其他用户输入的操作,都通过React组件的状态来进行管理和更新

  • 受控组件的特点包括:

    • 1.状态控制:表单元素的值被保存在React组件的状态中,通过设置状态来控制表单元素的值。
    • 2.事件处理:通过监听表单元素的事件(如onChange事件),将用户输入的值更新到组件的状态中。
    • 3.数据流一致性:React组件的状态是单一数据源,用于更新和渲染表单元素的值。通过确保状态与表单元素的值保持一致,可以实现数据的一致性和同步更新。

非受控组件

  • 非受控组件,将表单数据交给DOM节点来处理,可以使用Ref来获取非受控组件的数据,希望能够赋予表单一个初始值,但是不去控制后续的更新。可以采用defaultValue指定一个默认值

  • 非受控组件使用场景,必须手动操作DOM元素,state 实现不了,文件上传 <input type=file ref={fileRef}/>

js
import { useState } from 'react';
const Controlled: React.FC = () => {
  const [state, setState] = useState<string>('hulei');
  const getValue = () => {
    console.log(state);
  };
  return (
    <>
      <div>受控组件</div>
      <input
        type='text'
        value={state}
        onChange={(e) => setState(e.target.value)}
      />
      <br />
      {state}
      <br />
      <button onClick={getValue}>获取值</button>
    </>
  );
};
js
import { useRef, useState } from 'react';
const Uncontrolled: React.FC = () => {
  const [state, setState] = useState<string>('hulei');
  const inputRef = useRef<HTMLInputElement>(null);
  const getValue = () => {
    // 通过DOM或者到值
    console.log(inputRef.current?.value);
  };
  return (
    <>
      <div>非受控组件</div>
      <input type='text' defaultValue={state} ref={inputRef} />
      <br />
      {state}
      <br />
      <button onClick={getValue}>获取值</button>
    </>
  );
};

React 时间分片的大概原理

  • 浏览器刷新频率为 60Hz也就是一秒钟 60 帧,大概 (1000/60)16.6 毫秒渲染一次,而 JS 线程渲染线程互斥的,所以如果 JS 线程执行任务时间超过 16.6ms 的话,就会导致掉帧,导致卡顿, React 利用空闲的时间进行更新,不影响UI渲染进
  • 把一个耗时任务切分成一个个小任务,分布在每一帧里的方式就叫时间切片

lifeofframe

为什么会出现 fiber

  • React每次更新都是从根节点开始递归调度并且无法中断,如果 dom 树的结构比较复杂对应的虚拟节点计算会消耗对应的时间

  • 如果此时用户在进行表单填写操作,会感觉的卡顿,因为js渲染UI渲染是互斥

  • fiber 架构利用浏览器空闲时间(requestIdleCallback)执行任务,拒绝长时间占用主线程,放弃递归,只采用循环,因为循环可以被中断,任务拆分,将任务拆分成一个个小任务

fiber数据结构

  • fiber 是一个链表的数据结构,他的 child 指向第一个子节点,sibling 指向最近弟弟,return 指向父 fiber,即使是执行被打断,也很容易恢复

Fiber结构

优先级

scheduler优先级

  • scheduler优先级有5种,每一个优先级对应一个过期时间,过期时间越短,对应优先级越高
js
// 无优先级 
export const NoPriority = 0;
// 立刻执行优先级(等待时间-1ms)
export const ImmediatePriority = 1;
//用户阻塞操作优先级 用户点击 ,用户输入(等待时间250ms)
export const UserBlockingPriority = 2;
// 正常优先级(等待时间5000ms)
export const NormalPriority = 3;
// 低优先级(等待时间10000ms)
export const LowPriority = 4;
// 空闲优先级(没有过期时间)
export const IdlePriority = 5;

Lane

  • Lane一共有31条车道,对应5种事件优先级
js
/**
 * 一共有31条车道
 */
const TotalLanes = 31;
//没有车道,所有位都为0
const NoLanes = 0b0000000000000000000000000000000;
const NoLane = 0b0000000000000000000000000000000;
//同步车道,优先级最高
const SyncLane = 0b0000000000000000000000000000001;
const SyncBatchedLane = 0b0000000000000000000000000000010;
//离散用户交互车道 click
const InputDiscreteHydrationLane = 0b0000000000000000000000000000100;
const InputDiscreteLanes = 0b0000000000000000000000000011000;
//连续交互车道 mouseMove
const InputContinuousHydrationLane = 0b0000000000000000000000000100000;
const InputContinuousLanes = 0b0000000000000000000000011000000;
//默认车道
const DefaultHydrationLane = 0b0000000000000000000000100000000;
const DefaultLanes = 0b0000000000000000000111000000000;
//渐变车道
const TransitionHydrationLane = 0b0000000000000000001000000000000;
const TransitionLanes = 0b0000000001111111110000000000000;
//重试车道
const RetryLanes = 0b0000011110000000000000000000000;
const SomeRetryLane = 0b0000010000000000000000000000000;
//选择性水合车道
const SelectiveHydrationLane = 0b0000100000000000000000000000000;
//非空闲车道
const NonIdleLanes = 0b0000111111111111111111111111111;
const IdleHydrationLane = 0b0001000000000000000000000000000;
//空闲车道
const IdleLanes = 0b0110000000000000000000000000000;
//离屏车道
const OffscreenLane = 0b1000000000000000000000000000000;

事件优先级

js
//离散事件优先级 click onchange
export const DiscreteEventPriority = SyncLane;//1
//连续事件的优先级 mousemove 
export const ContinuousEventPriority = InputContinuousLane;//4
//默认事件车道
export const DefaultEventPriority = DefaultLane;//16 
//空闲事件优先级 
export const IdleEventPriority = IdleLane;

React渲染流程:TODO:

  • 1、React每次在更新之前都会通过requestUpdateLane函数取当前的Lane优先级
  • 2、通过markUpdateLaneFromFiberToRoot函数,找到Root根节点,并且把当前的Lane合并到pendingLanes
  • 3、每次在调度之前,会从Root根节点的pendingLanes,获取一个最高的Lane,如果是NoLane表示没有更新的更新,执行调度器上的Scheduler_cancelCallback调度取消
  • 4、因为在并发更新时调度是可以中断的,所以需要当前的Lane优先级和上次的Lane进行对比,如果之前的优先级等于当前的优先级, 不需要开启新的调度,scheduler会自动的获取performConcurrentWorkOnRoot的返回函数继续调度
  • 5、如果当前的Lane优先级比上次的高,取消之前的任务调用,开始新的调度任务
  • 6、如果是同步Lane优先级,用微任务调度,并且不会走并发更新策略,在构建fiber树时无法中断
  • 7、如果是非同步Lane优先级,利用Scheduler调度器进行调度,并发更新构建fiber树时,如果有高优先级的任务或者申请的时间片已经过期了(5毫秒)需要中断构建fiber树
  • 8、高优先级任务执行完成后,如果还有为完成的任务,需要恢复构建。当所有的fiber构建完成后,执行commitRoot把真实的DOM,渲染在页面上

React渲染流程

Scheduler调度器

  • 1、Scheduler调度器在执行的时候,接收一个Scheduler优先级并发更新的任务(performConcurrentWorkOnRoot)
  • 2、根据Scheduler优先级计算出过期的时间,将更新的任务放入最小堆中,并且返回当前的任务
  • 3、React会在每一帧的空闲时间申请5毫秒的执行时间,每次从最小堆从取出一个堆顶的任务开始执行
  • 4、如果时间片过期,或者最小堆中还有未完成的任务,将在下一帧继续执行

fiber树构建过程

  • fiber构建分为三个部分beginWork,completeWork,commitRoot

  • 1、beginWork阶段,根据虚拟节点构建fiber树标记flags副作用,

    • 1.1 在构建过程中循环遍历子虚拟节点,让父Fiber.child指向第一个子节点构建的Fiber,让第一个子Fiber.sibling指向下一个兄弟
    • 1.2 beginWork会返回父Fiber指向的第一个子Fiber,作为下个workInProgress工作单元
    • 1.3 当一个Fiber没有子Fiber,进入completeWork阶段
  • 2、completeWork阶段:根据当前workInProgress的fiber创建真实的DOM节点,赋值给fiber.stateNode

  • 3、当所有的fiber节点,都构建完成后进入commitRoot阶段

  • 3、commitRoot阶段:从根fiber开始遍历,将fiber节点上的stateNode真实的DOM,插入在对应的容器中,最后渲染到页面上

React 的 Diff 原理

  • DOM DIFF 的三个规则

    • 只对同级元素进行比较,不会跨层级
    • 不同的类型对应不同的元素,可以通过 key 来标识同一个节点
  • 组件在渲染的时候,如果有老的Fiber,说明不是首次渲染,需要走更新的策略

  • React更新策略都是用老的Fiber新的虚拟DOM进行比较,如果keytype类型一样,复用老的 Fiber 节点,如果不一样,根据新的Vnode创建新的Fiber,并且把老的Fiber标记为删除

新的虚拟是单节点

  • 1、循环老的Fiber节点,如果新的虚拟节点的类型(type)key和老Fiber的一致,复用老的Fiber节点,同时还需要继续循环把剩下的Fiber节点标记为删除
  • 2、如果新的虚拟节点的类型(type)key和老Fiber的不一致,当前的Fiber节点标记为删除,通过老Fiber.sibling找下一个Fiber
  • 3、如果循环完毕后,未能找到能够复用的Fiber节点,需要根据新的虚拟DOM创建新的Fiber节点

新的虚拟是多节点(多节点分为三种情况)

  • 1、老的Fiber全部能被复用,新的节点还没有遍历完成(新的节点比老的多)
  • 2、老的Fiber复用了部分,新的节点已经遍历完了(新的节点比老的少)
  • 3、老的Fiber,新的虚拟DOM,都没有循环完毕(根据老的Fiber生成map对象,遍历还未完成的虚拟DOM,最大程度的复用)

diff1

diff2

  • 1、如果有老的Fiber,遍历新的虚拟节点,用老的Fiber新的虚拟dom进行对比
  • 2、如果keytype类型相同复用老的 Fiber,否则标记老 Fiber节点为删除,根据新的虚拟DOM创建新的 Fiber,并且跳出当前的循环
  • 3、检测新的虚拟节点是否全部遍历完成,如果新的遍历完成,还有老 Fiber说明更新之后节点变少,需要把剩下的Fiber全部标记为删除
  • 4、如果老的fiber已经没有了,新的虚拟DOM还有,说明更新之后节点变多进入插入新节点的逻辑
  • 5、如果老的Fiber,新的虚拟DOM,都没有循环完毕,据老的Fiber生成map对象
  • 6、遍历剩余的新的虚拟 DOM,如果能复用将老的Fiber从map中移除,如果不能复用,根据新的虚拟 DOM创建Fiber
  • 7、新的虚拟DOM遍历完成后,将 map 所有的 Fiber 标记为删除

循环链表

  • 实现思路:

    • 1、当fiber更新队列为空时,让新的队列queue1.next指向头节点,让头节点指向queue1
    • 2、当queue2进入队列时,让queue2.next指向头节点的next(queue1),让queue1.next指向queue2(形成循环链表结构),让头节点在指向queue2
    • 3、当queue3进入队列时,让queue3.next指向头节点(queue2)的next(queue1),让头节点(queue2.next)指向queue3,让头节点在指向queue3
  • 目的:即可以保证链表的顺序也可以减少放入队列的循环次数,在更新状态的时候,需要从第一个进入的队列开始遍历,这里在添加队列的时候只需要改变2个指针,在遍历的时候找最后一个队列的下一个就是头节点,并且让最后一个next指向null

循环链表

合成事件

TIP

合成事件好处:可以抹平浏览器事件的兼容性、可以实现跨平台

  • 1、React 容器 在初始化时,会调用 createRoot 在根节点上进行事件注册
  • 2、在创建真实的 dom 的节点时,会把 React 事件挂载到真实的 dom 节点属性
  • 3、当在触发 React 事件时,通过 event.target,找到点击到DOM,判断节点上有对应的挂载事件
  • 4、如果有,从当前的事件源向上查找所有的,扑捉、冒泡事件,用于模拟原生的事件
  • 5、先执行扑捉事件队列,在执行冒泡事件队列
  • 6、如果有在执行的过程中阻止了默认行为,后续事件不给执行
js
// 支持的事件类型
const allNativeEvents = ["click"];
const elementEventPropsKey = "__props";

const listeningMarker = `_reactListening` + Math.random().toString(36).slice(2);

// 事件映射
function getEventCallbackNameFromtEventType(eventType) {
  return {
    click: ["onClickCapture", "onClick"],
  }[eventType];
}

// 收集从目标元素到HostRoot之间所有目标回调函数
const collectPaths = (targetElement, container, eventType) => {
  const paths = {
    capture: [],
    bubble: [],
  };
  // 收集事件回调是冒泡的顺序
  while (targetElement && targetElement !== container) {
    // div.__props = {onClickCapture:fn,onClick:fn....}
    const eventProps = targetElement[elementEventPropsKey];
    if (eventProps) {
      const callbackNameList = getEventCallbackNameFromtEventType(eventType);
      // 取出映射的事件
      if (callbackNameList) {
        callbackNameList.forEach((callbackName, i) => {
          // react事件
          const eventCallback = eventProps[callbackName];
          if (eventCallback) {
            if (i === 0) {
              // 由于捕获的执行顺序是从上往下,所以是反向收集
              // 反向插入捕获阶段的事件回调
              paths.capture.unshift(eventCallback);
            } else {
              // 由于冒泡的执行顺序是从下往上,所以是正向收集
              // 正向插入冒泡阶段的事件回调
              paths.bubble.push(eventCallback);
            }
          }
        });
      }
    }
    targetElement = targetElement.parentNode;
  }
  return paths;
};

const dispatchEvent = (rootContainerElement, eventType, e) => {
  const targetElement = e.target; // 事件源

  if (targetElement === null) {
    console.error("事件不存在target", e);
    return;
  }
  // 从当前的事件源向上查找所有的,扑捉、冒泡事件,用于模拟原生的事件
  const { capture, bubble } = collectPaths(
    targetElement,
    rootContainerElement,
    eventType
  );
  // 合成原生事件
  const se = createSyntheticEvent(e);
  triggerEventFlow(capture, se);
  // 如果事件被阻止不需要在执行
  if (!se.__stopPropagation) {
    triggerEventFlow(bubble, se);
  }
};

export function listenToAllSupportedEvents(rootContainerElement) {
  //监听根容器,也就是div#root只监听一次
  if (!rootContainerElement[listeningMarker]) {
    rootContainerElement[listeningMarker] = true;
    allNativeEvents.forEach((eventType) => {
      // 注册代理事件
      rootContainerElement.addEventListener(eventType, (e) => {
        dispatchEvent(rootContainerElement, eventType, e);
      });
    });
  }
}

function createSyntheticEvent(e) {
  const syntheticEvent = e;
  syntheticEvent.__stopPropagation = false;
  // 阻止事件传递
  const originStopPropagation = e.stopPropagation;

  syntheticEvent.stopPropagation = () => {
    syntheticEvent.__stopPropagation = true;
    if (originStopPropagation) {
      originStopPropagation();
    }
  };

  return syntheticEvent;
}

// 执行react事件
const triggerEventFlow = (paths, se) => {
  for (let i = 0; i < paths.length; i++) {
    const callback = paths[i];
    callback.call(null, se);
    if (se.__stopPropagation) {
      break;
    }
  }
};

// 将支持的事件回调保存在DOM中
export const updateEventProps = (node, props) => {
  node[elementEventPropsKey] = node[elementEventPropsKey] || {};
  allNativeEvents.forEach((eventType) => {
    // 获取支持的事件类型
    const callbackNameList = getEventCallbackNameFromtEventType(eventType);
    if (!callbackNameList) {
      return;
    }
    // 事件映射
    callbackNameList.forEach((callbackName) => {
      if (Object.hasOwnProperty.call(props, callbackName)) {
        node[elementEventPropsKey][callbackName] = props[callbackName];
      }
    });
  });
  return node;
};

react16 版本事件 bug

  • 比如需要一个事件来控制一个元素的展示,在当前组件注册一个原生的 document 冒泡事件,在处理合成事件让元素展示的时候,如果原生事件处理的是隐藏,其结果就是元素无法展示

  • 出现的原因是 react 冒泡事件先注册,注册在 document,所以 react 冒泡事件先执行,后执行原生的 document 冒泡事件,所以无法展示,通过阻止默认行为(冒泡),无效,因为是同级节点,只能阻止上级

js
// react16版本
function App() {
  let [visibility, setVisibility] = useState(false);
  // 注册冒泡事件
  document.addEventListener("click", () => {
    setVisibility(false);
  });
  let handlerClick = (e: any) => {
    setVisibility(true);
    // 阻止默认行为(冒泡),只能阻止上级,不能阻止同级
    e.stopPropagation();
    // 阻止所有
    e.nativeEvent.stopImmediatePropagation();
  };

  return (
    <div onClick={handlerClick}>
      {visibility && "visibility"}
      点击
    </div>
  );
}

context 用作原理

  • 用作:数据共享,可以避免 props 一层成传递,可以实现跨组件数据共享

  • 原理

    • 1、在创建 createContext 时,会返回一个 context 对象,包含内容提供者(Provider)和消费者(Consumer),他们都有一个_context 属性,都指向同一个引用地址
    • 2、react 在渲染时,如果是 Provider 组件,会给组件_context._currentValue进行赋值,在渲染当前的 children 组件
    • 3、react 在渲染时,如果是 Consumer 组件,会取出_context._currentValue的值,传递给子组件(因为指向同一个引用地址,所以值是一样的)
    • 4、如果是类组件,有 contextType 属性,会对类上的 context 进行赋值
js
// 渲染provider
function mountProviderComponent(vdom) {
  let { type, props } = vdom; // {$$typeof: REACT_PROVIDER,_context: context,};
  // 引用赋值
  type._context._currentValue = props.value;
  // 用于下次新老做对比(渲染的其实是子元素)
  vdom.oldRenderVdom = props.children;
  if (!vdom.oldRenderVdom) return null;
  return createDOM(props.children);
}
js
// 处理函数类件
function mountClassComponent(vdom) {
  let { type: ClassComponent, props, ref } = vdom;
  let classInstance = new ClassComponent(props);
  vdom.classInstance = classInstance;
  // 如果有contextType,把context绑定在实例上
  if (ClassComponent.contextType) {
    classInstance.context = ClassComponent.contextType._currentValue;
  }
  // .....
  return dom;
}
js
function createContext() {
  let context = { $$typeof: REACT_CONTEXT };
  // 提供者
  context.Provider = {
    $$typeof: REACT_PROVIDER,
    _context: context,
  };
  // 消费者
  context.Consumer = {
    $$typeof: REACT_CONTEXT,
    _context: context,
  };
  return context;
}

this.state = {
  color: "red",
};
const Context = React.createContext();
<Context.Provider value={{red: this.state.color}}>
  <div>{this.state.color}</div>
</Context.Provider>


 <Context.Consumer>
    {(props) => {
      return (
        <div>
          <span style={{ color: props.red }}>main</span>
        </div>
      );
    }}
</Context.Consumer>

React 优化方案( shouldComponentUpdate,PureComponent,memo,useMemo,useCallback)

  • 类组件
    • shouldComponentUpdate 组件在更新之前会触发该生命周期,会把上次的 props、state 传递过来,可以根据自己的需求进行对比,如果返回 true,执行更新,如果 false 不更新组件
    • PureComponent 内部实现了 shouldComponentUpdate
  • 函数组件
    • memo 函数组件在更新之前,对比之前的 props,如果未发生变化不执行更新操作
      • 如果未提供 arePropsEqual,React 将对 props 执行浅相等检查。这意味着如果父组件重新渲染并传递相同的 props 对象引用,则子组件不会重新渲染,因为相等检查将通过。
    • useMemo 缓存值
    • useCallback 缓存函数

setState

  • 在 React 事件中调用 setState,会进行批量更新策略,多个 setState 会进行合并
  • 在原生事件和异步任务中调度 setState 不会走批处理,相等于同步执行,里面能获取到新的值

为什么会出现 hooks

  • 在 React 16.8 之前,函数组件只能接收 props 作为输入,无法使用 state 和其他 React 特性。React Hooks 则允许我们在函数组件中使用 state、生命周期方法、上下文、Refs 等特性。

hooks 原理

TIP

类组件中的 setState 是状态合并策略 hooks 队列中有多个时,状态不会进行合并,是直接覆盖更新,后者覆盖前者

  • 1、React 在构建 Fiber 时,如果是函数组件首先会调用renderWithHooks进行初始化
  • 2、构建全局 hooks ReactCurrentDispatcher派发器,如果是首次渲染挂载 MountHooksDispatcherOnMount、如果是更新挂载 UpdateHooksDispatcherOnUpdate(派发器就是提供给组件调用的如:useState...)
  • 3、当函数组件在被调用时,如果是初次渲染,会根据调用的情况,构建 hooks 链表
  • 4、每一个 hook 都会创建一个更新的队列和一个 dispatchSetState 函数并且已经绑定 fiber, hook队列,并且 next 指向下一个 hook
  • 5、当 hook 被提交时,其实是调用的 hooks.dispatchSetState 函数,此时会将提交的 新的 State 存储在 hook 队列中,并且触发组件重新渲染
  • 6、当组件重新渲染时,React 会依据 hooks 链表,按照 hooks 的顺序依次执行对应的函数,从而实现对应的特性。

hooks 注意事项

  • 因为函数组件所有的 hook,以链表的形式存储的 fiber 中,当函数组件在更新的时候,会按照存储的顺序一个个进行查找
  • 如果在条件中使用 hooks、或者在循环中使用 hooks,会导致位置出错

useMemo 和 useCallback 区别

  • 应用场景如需要缓存的函数,因为函数式组件每次任何一个 state 发生变化,会触发整个组件更新,一些函数是没有必要更新的,此时就应该缓存起来,提高性能,减少对资源的浪费;另外还需要注意的是,useCallback 应该和 React.memo 配套使用,缺了一个都可能导致性能不升反而下降。

  • 相同点

    • 1、useMemo 和 useCallback 都是 reactHook 提供的两个 API,用于缓存数据,优化性能;
    • 2、两者接收的参数都是一样的,第一个参数表示一个回调函数,第二个表示依赖的数据。
    • 3、在依赖数据发生变化的时候,才会调用传进去的回调函数去重新计算结果,起到一个缓存的作用
  • 两者的区别

    • 1、useMemo 缓存的结果是回调函数中 return 回来的值,主要用于缓存计算结果的值,应用场景如需要计算的状态
    • 2、useCallback 缓存的结果是函数,主要用于缓存函数
js
function useMemo(factory, deps) {
  let hook =
    workInProgressFiber.alternate &&
    workInProgressFiber.alternate.hooks &&
    workInProgressFiber.alternate.hooks[hookIndex];
  let currentMemo = null;
  if (hook) {
    let [lastMemo, lastDeps] = hook;
    // 检测依赖的属性是否发生变化
    let same = deps.every((item, index) => item === lastDeps[index]);
    if (same) {
      // 依赖的属性未发生变化返回之前的值
      currentMemo = lastMemo;
    } else {
      // 返回新的值
      currentMemo = factory();
    }
  } else {
    // 创建新的
    currentMemo = factory();
  }
  workInProgressFiber.hooks[hookIndex++] = [currentMemo, deps];
  return currentMemo;
}
js
function useCallback(callback, deps) {
  let hook =
    workInProgressFiber.alternate &&
    workInProgressFiber.alternate.hooks &&
    workInProgressFiber.alternate.hooks[hookIndex];
  let currentCallback = null;
  if (hook) {
    let [lastCallback, lastDeps] = hook;
    let same = deps.every((item, index) => item === lastDeps[index]);
    if (same) {
      currentCallback = lastCallback;
    } else {
      currentCallback = callback;
    }
  } else {
    currentCallback = callback;
  }
  workInProgressFiber.hooks[hookIndex++] = [currentCallback, deps];
  return currentCallback;
}

useEffect 和 useLayoutEffect 区别

相同点

  • 1、useEffect 和 useLayoutEffect 都是 reactHook 提供的两个 API,可以模拟类组件的部分生命周期
  • 2、两者接收的参数都是一样的,第一个参数表示一个回调函数(函数可以返回一个函数),第二个表示依赖的数据。
  • 3、在依赖数据发生变化的时候,才会调用传进去的回调函数,如果上次之前的函数有返回函数,会在执行回调函数之前执行

两者的区别(执行回调的时机不用)

  • 1、 useEffect 是在 dom 更新后异步执行
  • 2、 useLayoutEffect 是在 dom 更新前同步执行(有可能会阻塞 dom 渲染),执行完再更新 dom(useLayoutEffect 可以解决更新 dom 时候屏幕闪烁的问题)
js
function useEffect(callback, deps) {
  let hook =
    workInProgressFiber.alternate &&
    workInProgressFiber.alternate.hooks &&
    workInProgressFiber.alternate.hooks[hookIndex];

  if (hook) {
    let [lastCallback, lastDeps] = hook;
    let same = deps && deps.every((item, index) => item === lastDeps[index]);
    // 如果关联的deps未发生变化不做任何操作
    if (same) {
      workInProgressFiber.hooks[hookIndex++] = [lastCallback, deps];
    } else {
      // 如果发生变化,执行销毁的回调
      lastCallback && lastCallback();
      setTimeout(() => {
        // 存储新的
        workInProgressFiber.hooks[hookIndex++] = [callback(), deps];
      });
    }
  } else {
    setTimeout(() => {
      // callback 返回的函数,在下次渲染前执行
      workInProgressFiber.hooks[hookIndex++] = [callback(), deps];
    });
  }
}
js
function useLayoutEffect(callback, dependencies) {
  let hook =
    workInProgressFiber.alternate &&
    workInProgressFiber.alternate.hooks &&
    workInProgressFiber.alternate.hooks[hookIndex];
  if (hook) {
    let [lastCallback, lastDeps] = hook;
    let same =
      dependencies &&
      dependencies.every((item, index) => item === lastDeps[index]);
    if (same) {
      workInProgressFiber.hooks[hookIndex++] = [lastCallback, dependencies];
    } else {
      lastCallback && lastCallback();
      // DOM 更新完成后,浏览器绘制之前
      queueMicrotask(() => {
        lastCallback = callback();
        workInProgressFiber.hooks[hookIndex++] = [lastCallback, dependencies];
      });
    }
  } else {
    // DOM 更新完成后,浏览器绘制之前
    queueMicrotask(() => {
      workInProgressFiber.hooks[hookIndex++] = [callback(), dependencies];
    });
  }
}

useRef&createRef区别

  • 1、都可以在组件中创建ref对象createRef组件函数组件都可以使用,useRef只能在函数组件中使用
  • 2、在函数组件使用createRef时,每次组件更新都会重新去初始化(指向不同的引用地址)
  • 3、useRef存储在fiber链表中,每次组件在更新不会重新去初始化(指向相同的引用地址)
js
// mount 阶段
function mountRef<T>(initialValue: T): {|current: T|} {
  // 获取 hook 对象
  const hook = mountWorkInProgressHook();
  const ref = {current: initialValue};
  if (__DEV__) {
    Object.seal(ref);
  }
  // 存储
  hook.memoizedState = ref;
  return ref;
}


// update 阶段
function updateRef<T>(initialValue: T): {|current: T|} {
  const hook = updateWorkInProgressHook();
  // 返回之前的引用
  return hook.memoizedState;
}

useRef&useState区别

  • useState在更新的时候导致组件重新渲染
  • useRef在更新的时候不会导致组件重新渲染

useRef可以解决闭包陷阱问题

js
// 1、先点击第一个按钮6次
// 2、在点击第二个按钮1次
// 3、立马在点击第一个按钮4次
import { useState } from 'react';
const App = () => {
  // setState更新会导致组件更新
  const [state, setState] = useState(0);
  function handleClick() {
    setTimeout(() => {
      // 4、由于闭包的特性,当前的值是6,所以打印6
      console.log(state);
    }, 3000);
  }
  return (
    <>
      {/* setState更新会导致组件更新所以是10 */}
      <button onClick={() => setState(state + 1)}>{state}</button>
      <button onClick={handleClick}>print</button>
    </>
  );
};
export default App;
js
// 1、先点击第一个按钮6次
// 2、在点击第二个按钮1次
// 3、立马在点击第一个按钮4次
import { useRef } from 'react';
const App = () => {
  const state = useRef(0);
  function handleClick() {
    setTimeout(() => {
      // 4、由于都是指向同一个引用地址所以是10
      console.log(state.current);
    }, 3000);
  }
  return (
    <>
      <button
        onClick={() => {
          state.current++;
        }}
      >
      {/* useRef值的改变不会导致组件更新,所以一直是0 */}
        {state.current}
      </button>
      <button onClick={handleClick}>print</button>
    </>
  );
};
export default App;

如何拿到useState更新的值

  • 1、可通过useEffect,将更新值作为依赖条件
  • 2、在更新useState之前,先计算值在更新useState

forwardRef(组件转发)

WARNING

  • 如果对函数组件,使用ref会抛出如下警告,因为函数组件类型组件不同,没有实例所以对函数组件直接使用ref是毫无意义的
  • Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?
  • forwardRef可以让父组件的ref获取到子组件的DOM,同时也可以配合useImperativeHandle,将子组件的函数暴露给父组件ref
js
import React, { useEffect, useRef, forwardRef } from 'react';
const InputText = (props: any, ref: any) => {
  return (
    <>
      <div>
        <input ref={ref} />
      </div>
    </>
  );
};
const ForwardInput = forwardRef(InputText);
const App: React.FC = () => {
  const inputRef = useRef<HTMLInputElement>(null);
  useEffect(() => {
    if (inputRef.current) {
      // 获取到子组件的input
      inputRef.current.focus();
    }
  });
  return (
    <>
      <ForwardInput ref={inputRef}></ForwardInput>
    </>
  );
};
export default App;
  • 1、forwardRef是一个函数组件,它接收一个组件,返回一个对象,包含$$typeof类型,和render(render是外部传递的组件),
  • 3、当组件在渲染的时候,内部会保留父组件传递的ref的引用,如果是$$typeof类型是REACT_FORWARD_REF,调用render渲染组件,并且把props,ref传递给对应的组件
js

function forwardRef(render) {
  return {
    $$typeof: REACT_FORWARD_REF,
    render,
  };
}

if (type && type.$$typeof === REACT_FORWARD_REF) {
  return mountForwardComponent(vdom);
}

function mountForwardComponent(vdom) {
  const { type, props, ref } = vdom;
  const renderVdom = type.render(props, ref);
  if (!renderVdom) return null;
  return createDOM(renderVdom);
}

useImperativeHandle

  • 1、useImperativeHandle其实就是一个普通的函数,接受2个参数一个是父组件的ref,另外是一个函数返回一个对象
  • 2、当useImperativeHandle在被调用时,内部会调用第二个参数将返回的对象,存储在父组件ref.current
  • 3、因为是同一个引用地址,所以通过父组件的ref.current能够调用到子组件的定义的函数
js
import React, {
  useEffect,
  useRef,
  forwardRef,
  useImperativeHandle,
} from 'react';

interface IInputText {
  onClick: () => void;
}

const InputText = (props: any, ref: any) => {
  useImperativeHandle(ref, () => {
    return {
      onClick: () => {
        console.log('子组件的函数');
      },
    };
  });
  return (
    <>
      <div></div>
    </>
  );
};
const ForwardInput = forwardRef(InputText);
const App: React.FC = () => {
  const inputRef = useRef<IInputText>(null);
  useEffect(() => {
    if (inputRef.current) {
      inputRef.current.onClick();
    }
  });
  return (
    <>
      <ForwardInput ref={inputRef}></ForwardInput>
    </>
  );
};
export default App;
js
// 实现
export function useImperativeHandle ( ref, handler ) {
  ref.current = handler();
}

react 新增了什么生命周和删除了什么生命周期,为什么要删除

  • 1、componentWillMount、2、componentWillUpdate 3、componentWillReceiveProps
  • React 废弃的这三个生命周期函数,大都是因为新版本的异步渲染,在调用 render 生成虚拟 DOM 阶段,由于更高级的操作到来而被打断,导致 render 之前的操作都会重来。使得之前的逻辑可能会被重复调用而弃用。

Vue vs React

  • Vue使用模版拥抱html

  • Vue 是真正意义上的做到了组件级更新,每一个组件就对应一个渲染的 effect,让响应式数据去收集 effect

  • Vue 采用的是递归的方式来渲染页面不可中断

  • React使用的JSX拥抱JS

  • React 更新都是从跟节点开始调度,会讲一个大的任务拆分成多个小的任务单元

  • React 更新策略是循环的方式,有任务优先级的概念,可中断执行

React错误处理componentDidCatchuseErrorBoundary

js
import React, { Component } from 'react';
 
class ErrorBoundary extends Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }
 
  componentDidCatch(error, info) {
    this.state = { hasError: true };
  }
 
  render() {
    if (this.state.hasError) {
      // 显示备用 UI
      return <h1>Something went wrong.</h1>;
    }
    return this.props.children;
  }
}
 
export default ErrorBoundary;

 
function App() {
  return (
    <div>
      <ErrorBoundary>
        <h1>Hello, world!</h1>
      </ErrorBoundary>
    </div>
  );
}
js
function ErrorBoundary({children}: {children: ReactNode}) {
  const [errorMsg, updateError] = useState<Error | null>(null);

  useErrorBoundary((e: Error) => {
    updateError(e);
  })

  return (
    <div>
      {errorMsg ? '报错:' + errorMsg.toString() : children}
    </div>
  )
}