Appearance
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渲染进 - 把一个耗时任务切分成一个个小任务,
分布在每一帧里
的方式就叫时间切片
为什么会出现 fiber
React每次更新都是从根节点开始
递归
调度并且无法中断,如果 dom 树的结构比较复杂对应的虚拟节点计算会消耗对应的时间
如果此时用户在进行表单填写操作,会感觉的卡顿,因为
js渲染
和UI渲染
是互斥fiber 架构利用
浏览器空闲时间
(requestIdleCallback)执行任务,拒绝长时间占用主线程,放弃递归
,只采用循环
,因为循环可以被中断,任务拆分,将任务拆分成一个个小任务
fiber数据结构
- fiber 是一个链表的数据结构,他的 child 指向第一个子节点,sibling 指向最近弟弟,return 指向父 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,渲染在页面上
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阶段
- 1.1 在构建过程中
2、completeWork阶段:根据当前
workInProgress
的fiber创建真实的DOM节点,赋值给fiber.stateNode3、当所有的fiber节点,都构建完成后进入
commitRoot
阶段3、commitRoot阶段:从根fiber开始遍历,将fiber节点上的
stateNode
真实的DOM,插入在对应的容器中,最后渲染到页面上
React 的 Diff 原理
DOM DIFF 的三个规则
- 只对同级元素进行比较,不会跨层级
- 不同的类型对应不同的元素,可以通过 key 来标识同一个节点
组件在渲染的时候,如果有老的Fiber,说明不是首次渲染,需要走更新的策略
React更新策略都是用
老的Fiber
和新的虚拟DOM
进行比较,如果key
和type
类型一样,复用老的 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,最大程度的复用)
- 1、如果有老的Fiber,遍历新的虚拟节点,用
老的Fiber
和新的虚拟dom
进行对比 - 2、如果
key
和type
类型相同复用老的 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
- 1、当fiber更新队列为空时,让新的队列
目的:即可以保证链表的顺序也可以减少放入队列的循环次数,在更新状态的时候,需要从第一个进入的队列开始遍历,这里在添加队列的时候只需要改变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 缓存函数
- memo 函数组件在更新之前,对比之前的 props,如果未发生变化不执行更新操作
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错误处理componentDidCatch
、useErrorBoundary
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>
)
}