React面试宝典

React Diff

在 React 中,diff 算法需要与虚拟 DOM 配合才能发挥出真正的威力。react 会使用 diff 算法计算出虚拟 DOM 中真正发生变化的部分,并且只会针对该部分进行 dom 操作,从而避免了对页面进行大面积的更新渲染,减少性能的开销

React Diff 算法

在传统的 diff 算法中复杂度会达到 O(n^3)。React 中定义了三种策略,在对比时,根据策略只需要遍历一次树就可以完成对比,将复杂度降到了 O(n):

  1. tree diff: 在两个树对比时,只会比较同一层级的节点,会忽略掉跨层级的操作。
  1. component diff: 在对比两个组件时,首先会判断它们两个的类型是否相同,如果相同,则继续比较它们的 props,如果不同,则将该组件判断为 dirty component,从而替换整个组建下的所有子节点。
  1. element diff: 对于同一层级的一组节点,会使用具有唯一性的 key 来区分是否需要创建,删除,或者是移动。

Element Diff

当节点处于同一层级时,React diff 提供了三种节点操作,分别为:

  1. INSERE_MARKUP(插入)

    • 新的 component 类型不再老集合里,即是全新的节点,需要对新节点执行插入操作
  2. MOVE_EXISTING(移动)

    • 在老集合有新 component 类型,且 element 是可更新的类型,这种情况下 prevChild=nextChild,就需要做移动操作,可以复用以前的 DOM 节点。
  3. REMOVE_NODE(删除)

    • 老 component 类型,在新集合里也有,但对应的 element 不同 则不能直接复用和更新,需要执行删除操作。
    • 或者老 component 不在新集合里的,也需要执行删除操作

存在如下结构:

新老集合进行 diff 差异化对比,通过 key 发现新老集合中的节点都是相同的节点,因此无需进行节点删除和创建,只需要将新老集合中节点的位置进行移动,更新为新集合中节点的位置,此时 React 给出的 diff 结果为:B、D 不做任何操作,A、C 进行移动操作

  • 首先对新集合的节点进行循环便利,for(name in nextChildren),
  • 通过唯一 key 可以判断新老集合中是否存在相同的节点,if(prevChild === nextChild)
  • 如果存在,则将老集合中节点的位置移动到新集合中,即:nextChildren[name] = prevChild;
    • 但在移动前需要将当前节点在老集合中的位置与 lastIndex 进行比较,则进行节点移动操作,否则不执行该操作。
    • lastIndex 一直在更新,表示访问过的节点在老集合中最右的位置(即最大的位置)。
    • 如果新集合中当前访问的节点 比 lastIndex 大,说明当前访问节点在老集合中就比上一个节点位置靠后,则该节点不会影响其他节点的位置, 因此不用添加到差异队列中,即不执行移动操作。
    • 只有当访问的节点比 lastIndex 小时,才需要进行移动操作。

当完成新集合中的所有节点 diff 时,最后还需要对老集合进行循环遍历, 判断是否存在新集合中没有但老集合仍存在的节点,发现存在这样的节点 x,因此删除节点 x,到此 dff 全部完成

setState 同步异步问题

18.x 之前版本

如果直接在 setState 后面获取 state 的值时获取不到的

  • 在 React 内部机制能检测到的地方,setState 就是异步的;

  • 在 React 检测不到的地方,例如原声事件

    addEventListener,setInterval,setTimeout,setState 就是同步更新的

setState 并不是单纯的异步或者同步,这其实与调用时的环境相关

  • 在合成事件和生命周期狗子(除 componentDidUpdate)中,setState 是异步的;
  • 在原生事件和 setTimeout 中,setState 是同步的,可以马上获取更新后的值;

批量更新

多个顺序的 setState 不是同步地一个一个执行的,会一个一个加入队列,然后最后一起执行。在合成事件和生命周期钩子中,setState 更新队列时,存储的时合并状态(Object.assign)。因此前面设置的 key 值会被后面所覆盖,最终只会执行一次更新。

异步现象原因

setState 的"异步"并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和生命钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形成了所谓的"异步",当然可以通过第二个参数 setState(partialState, callback)中的 callback 拿到更新后的结果。

setState 并非真异步,只是看上去像异步。在源码中,通过 isBatchingUpdates 来判断

setState 调用流程:

  1. 调用 this.setState(newState)

  2. 将新状态 newState 存入 pending 队列

  3. 判断是否处于 batch Update(isBatchingUpdates 是否为 true)

    • isBatchingUpdates 为 true,保存组件于 dirtyComponents 中,走异步更新流程,合并操作,延迟更新;

    • isBatchingUpdates=false,走同步过程。遍历所有的 dirtyComponents,调用 updateComponent,更新 pending state or props

为什么直接修改 this.state 无效

setState 本质是通过一个队列机制实现 state 更新的。执行 setState 时,会将需要更新的 state 合并后放入状态队列,而不会立刻更新 state,队列机制可以批量更新 state。

如果不通过 setState 而直接修改 this.state,那么这个 state 不会放入状态队列中,下次调用 setState 时对状态队列进行合并时,会忽略之前直接被修改的 state,这样我们就无法合并了,而且实际也没有把你想要的 state 更新上去

React18

在 v18 之前只在事件处理函数中实现了批量处理,在 v18 中所有更新都将自动批量处理,包括 promise 链、setTimeout 等异步代码以及原声事件处理函数

React18 新特性

React 从 16 到 18 主打的特性包括三个变化:

  • 16:Async Mode(异步模式)
  • 17:Concurrent Mode(并发模式)
  • 18:Concurrent Render(并发更新)

React 中 Fiber 树的更新流程分为两个阶段 render 阶段和 commit 阶段。

  1. 组件的 render 函数执行时称为 render(本次更新需要做哪些变更),纯 js 计算;
  2. 而将 render 的结果渲染到页面的过程称为 commit(变更到真实的宿主环境中,在浏览器中就是操作 DOM)。

在 Sync 模式下,render 阶段是一次性执行完成;而在 Concurrent 模式下 render 阶段可以被拆解,每个时间片执行一部分,知道执行完毕。由于 commit 阶段由 DOM 的更新,不可能让 DOM 更新到一半中断,必须一次性执行完毕。

React 并发新特性

并发渲染机制 concurrent rendering 的目的:根据用户的设备性能网速对渲染过程进行适当的调整,保证 React 应用在长时间的渲染过程中依旧保持可交互性,避免页面出现卡顿或无响应的情况,从而提升用户体验。

  1. 新 rootAPI
    • 通过 createRoot Api 手动创建 root 节点。
  2. 自动批处理优化 Automatic batching
    • React 将多个状态更新分组到一个重新渲染中以获得更好的性能。(将多次 setstate 事件合并)
    • 在 18 之前只在事件处理函数中实现了批处理,在 18 中所有更新都将自动批处理,包括 promise 链、setTimeout 等异步代码以及原生事件处理函数。
    • 想退出自动批处理立即更新的话,可以使用 ReactDOM.flushSync()进行包裹
  3. startTransition
    • 可以用来降低渲染优先级。分别用来包裹计算量的的 function 和 value,降低优先级,减少重复渲染次数。
    • startTransition 可以指定 UI 的渲染优先级,哪些需要实时更新,哪些需要延迟更新
    • hook 版本的 useTransition,接受传入一个毫秒的参数用来修改最迟更新时间,返回一个过渡期的 pending 状态和 startTransition 函数。
  4. useDefferdValue
    • 通过 useDefferdValue 允许变量延迟更新,同时接受一个可选的延迟更新的最大值。React 将尝试尽快更新延迟值,如果在给定的 timeoutMs 期限内未能完成,它将强制更新
    • const defferValue = useDeferredValue(value,{ tiemoutMs: 1000 })
    • useDefferdValue 能够很好的展示并渲染时优先级调整的特性 ,可以用于延迟计算逻辑比较复杂的状态,让其他组件优先渲染,等待这个状态更新完毕之后再渲染。

React 生命周期

React 的生命周期主要是有两个比较大的版本,分别是

16.0 前 和 16.4 两个版本的生命周期。

16.0 前

总共分为四大阶段:

  1. 初始化 Intialization
  2. 挂载 Mounting
  3. 更新 Update
  4. 卸载 Unmounting
Intialization(初始化)

在初始化阶段,会用到 constructor()这个构造函数,如:

js 复制代码
constructor(props){
  super(props);
}
  • super 的作用
    • 用来调用基类的构造方法(constructor())
    • 也将父组件的 props 注入给子组件,供子组件读取
  • 初始化操作,定义 this.state 的初始内容
  • 只会执行一次
Mounting(挂载)3 个
  1. componentWillMount:在组件挂载到 DOM 前调用
    • 这里面调用的 this.setState 不会引起组件的重新渲染,也可以把写在这边的内容提到 constructor(),所以在项目中很少。
    • 只会调用一次
  2. render:渲染
    • 只要 props 和 state 发生改变(无论值是否有变化,两者的重传递和重赋值,都可以引起组件重新 redner),都会重新渲染 render。
    • return:是必须的,是一个 React 元素, 不负责组件实际渲染工作,由 React 自身根据此元素去渲染出 DOM。
    • render 是纯函数,不能执行 this.setState
  3. componentDidMount:组件挂载到 DOM 后调用 (调用一次
Update(更新)5 个
  1. componentWillReceiveProps(nextprops):调用于 props 引起的组件更新过程中
    • nextProps:父组件传给当前组件新的 props
    • 可以用 nextProps 和 this.props 来查明重传 props 是否发生改变(原因:不能保证父组件重传 props 由变化)
    • 只要 props 发生变化就会引起调用
  2. shouldComponentUpdate(nextProps,nextState):用于性能优化
    • nextProps:当前组件的 this.props
    • nextState:当前组件的 this.state
    • 通过比较 nextProps 和 nextState,来判断当前组件是否有必要继续执行更新过程。
    • 返回 false:表示停止更新,用于减少组件的不必要渲染,优化性能
    • 返回 true:继续执行更新
    • 像 componentWillReceiveProps()中执行了 this.setState,更新了 state,但在 render 前(如 shouldComponentUpdate,componentWillUpdate),this.state 依然指向更新前的 state,不然 nextState 及当前组件的 this.state 的对比就一直是 true 了
  3. componentWillUpdate(nextProps,nextState):组件更新前调用
    • 在 render 方法前执行
    • 由于组件更新就会调用,所以一般很少使用
  4. render:重新渲染
  5. componentDidUpdate(prevProps,prevState):组件更新后被调用
    • prevProps:组件更新前的 props
    • prevState:组件更新前的 state
    • 可以操作组件更新的 DOM
Unmounting(卸载)

componentWillUnmount:组件被卸载前调用

可以在这里执行一些清理工作,比如清除组件中使用的定时器,清除 componentDidMount 中手动创建的 DOM 元素等,以避免引起内存泄漏

16.4

与 16.0 的生命周期相比

新增了两个

  1. getDerivedStateFormProps
  2. getSnapshotBeforeUpdate

删除了是三个

  1. componentWillMount
  2. componentWillReceiveProps
  3. componentWillUpdate
getDerivedStateFormProps

getDerivedStateFromProps(prevProps,prevState):组件创建和更新时调用的方法

  • prevProps:组件更新前的 props
  • prevState:组件更新前的 state

在 React16.3 中,在创建和更新时,只能是由赴组件引发才会调用这个函数,在 React16.4 改为无论是 mounting 还是 Updating,全部都会调用。

是一个静态函数,也就是这个函数不能通过 this 访问到 class 的属性。

如果 props 传入的内容不需要影响到你的 state,那么就需要返回一个 null,这个返回值是必须的,所以尽量将其写到函数的末尾。

在组件创建时和更新时的 render 方法之前调用,它应该

  • 返回一个对象来更新状态
  • 返回 null 来不更新任何内容
getSnapshotBeforeUpdate

getSnapshotBeforeUpdate(prevProps,prevState):Updating 时的函数,在 render 之后调用

  • prevProps:组件更新前的 props
  • prevState:组件更新前的 state

可以读取,但无法使用 DOM 的时候,在组件可以在可能更改之前从 DOM 捕获一些信息(例如滚动位置)

返回的任何值都将作为参数传递给 componentDidUpdate()

Hook 的相关知识点

react-hooks 是 react16.8 之后才出现的,在 react16.8 之前,只能使用 class 组件,在 react16.8 之后,可以使用函数组件,也可以使用 class 组件。

React16.8 中的 hooks

useState

useState:定义变量,可以理解为他是类组件中的 this.state 使用:

js 复制代码
const [state, setState] = useState(initialState);
  • state:目的是提供给 UI,作为渲染视图的数据源
  • setState:改变 state 的函数,可以理解为 this.setState
  • initialState:初始值 useState 有点类似于 PureComponent,会进行一个比较浅的比较,如果是对象的时候直接传入并不会更新
解决传入对象的 问题

immer.js 这个库,是基于 proxy 拦截 getter 和 setter 的能力,让我们可以很方便的通过修改对象本身,创建新的对象。

React 通过 Object.js 函数比较 props,也就是说对于引用一致的对象,react 是不会刷新视图的,这也是为什么我们不能直接修改调用 useState 的道德 state 来更新视图,而是要通过 setState 刷新视图,通常,为了方便,我们会使用 es6 的 spread 运算符构造新的对象(钱拷贝)

对于嵌套层级多的对象,使用 spread 构造新的对象写起来心智负担很大,也不易于维护

常规的处理方式是对数据进行 deepClone,但是这种处理方式针对结构简单的数据来讲还算 ok,但是遇到大数据的话,就不够优雅了

所以,我们可以直接使用 useImmer 这个语法糖开进一步简化调用方式

js 复制代码
const [state, setState] = useImmer({
	a: 1,
	b: {
		c: [1, 2],
		d: 2,
	},
});
setState((prev) => {
	prev.b.c.push(3);
});

深入 useState 本质

当组件初次渲染(挂载)时
  1. 在初次渲染时,我们通过 useState 定义了多个状态;
  2. 每调用一次 useState,都会在组件之外生成一条 hook 记录,同时包括状态值和状态更新 setter 函数;
  3. 多次调用 useState 生产的 hook 记录形成了一条链表;
  4. 触发 onClick 回调函数,调用 setS1 函数修改 s1 的状态,不仅修改了 hook 记录中的状态值,还即将触发重渲染
组件重渲染时

在初次渲染结束后、重渲染之前,hook 记录链表依然存在。当我们逐个调用 useState 的时候,useState 便返回了 hook 链表中存储的状态,以及修改状态的 setter

useEffect

useEffect:副作用,你可以理解为是类组件的生命周期,也是我们最常见的钩子

副作用(side effect):是指 function 做了和本身运算返回值无关的事,如请求数据、修改全局变量,打印、数据获取、设置订阅以及手动更改 React 组件中的 DOM 都属于副作用操作

  1. 不断执行 当 useEffect 不设立第二个参数时,无论什么情况,都会执行
  2. 根据依赖值改变 设置 useEffect 的第二个值

useContext

useContext:上下文,类似于 Context:其本意就是设置全局共享数据,使所有组件可跨层级实现数据共享

useContext 的参数一般时由 createContext 创建,通过 xxContextProvider 包裹的组件,才能通过 useContext 获取对应的值

存在的问题及解决方案

useContext 是 React 官方推荐的共享状态的方式,然而在需要共享状态的组件非常多的情况下,着有这严重的性能问题, 例如有 A/B 组件,A 组件只更新 state.a,并没有用到 state.b,B 组件更新 state.b 的时候 A 组件也会刷新,在组件非常多的情况下,就卡死了,用户体验非常不好。

useReducer

类似于 redux 功能的 api

js 复制代码
const [state, dispatch] = useReducer(reducer, initialState, init);
  • state:当前状态
  • dispatch:触发 reducer 函数,并且传入 action,从而修改 state(可以理解为和 useState 的 setState 一样的效果)
  • reducer:一个纯函数,接收 state 和 action,返回新的 state(可以理解为 redux 的 reducer)
  • initialState:初始值
  • init:惰性初始化

useMemo

与 memo 的理念上差不多,都是判断是否满足当前的限定条件来决定是否执行,callback 函数,而 useMemo 的第二个参数是一个数组,通过这个数组来判断是否执行回调函数

当一个父组件中调用了一个子组件的时候,父组件的 state 发生变化,会导致父组件更新,而子组件虽然没有发生改变,但也会进行更新。

只要父组件的状态更新,无论有没有对子组件进行操作,子组件都会进行更新,useMemo 就是为了防止这点而出现的。

useCallback

useCallback 与 useMemo 极其类似,唯一不同的是

  • useMemo 返回的是函数运行的结果(全能型选手,能后同时胜任引用相等和节约计算的任务)

  • useCallback 返回的是函数(主要是为了解决函数的引用相等问题)

    • 这个函数是父组件传递给子组件的一个函数,防止做无关的刷新
    • 这个子组件必须配合 React.memo,否则不但不会提升性能,还有可能降低性能
存在的问题及解决方案

一个很常见的误区是为了心理上的性能提升把函数通通使用 useCallback 包裹,在大多数情况下,javascript 创建一个函数的开销是很小的,哪怕每次渲染都重新创建,也不会有太大的性能损耗,真正的性能损耗在于,很多时候 callback 函数是组件 props 的一部分,因为每次渲染的时候都会重新创建 callback 导致函数引用不同,所以触发了组件的重渲染,然而一旦函数使用 useCallback 包裹,则要面对声明依赖项的问题,对于一个内部捕获了很多 state 的函数,写依赖项非常容易写错,因此引发 bug。

所以,在大多数场景下,我们应该只在需要维持函数引用的情况下使用 useCallback。

js 复制代码
const [userText, setUserText] = useState('');
const handleUserKey = useCallback((event) => {}, []);

useEffect(() => {
	window.addEventListener('keydown', handleUserKey);
	return () => {
		window.removeEventListener('keydown', handleUserKey);
	};
}, [handleUserKey]);

return <div> {userText}</div>;

在组件卸载的时候移除 event listener callback,因此需要保持 event handler 的引用,所以这里需要使用 useCallback 来保持引用不变。

使用 useCallback,我们又会面临声明依赖项的问题,这里我们可以使用 ahook 中的 useMemoizedFn 的方式,即能保持引用,又不用声明依赖项。

js 复制代码
const [state, setState] = useState('');
// func 地址永远不会变化
const func = useMemoizedFn(() => {
	console.log(state);
});

useRef

可以获取当前元素的所有属性,并且返回一个可变的 ref 对象,并且这个对象只有 current 属性,可设置 initialValue

  1. 通过 useRef 获取对应的 React 元素的属性值
  2. 缓存数据

useLayoutEffect

与 useEffect 基本一致,不同的地方时,useLayouEffect 是同步要注意的是 useLayoutEffect 在 Dom 更新之后,浏览器绘制之前,这样做的好处是可以更加方便的修改 dom,获取 dom 信息 , 这样浏览器只会绘制一次,所以 useLayoutEffect 仔 useEffect 之前执行。如果是 useEffect 的话,useEffect 执行仔浏览器绘制视图之后,如果在此时改变 dom,有可能会导致浏览器再次回流和重绘。

除此之外 useLayoutEffect 的 callback 中代码执行会阻塞浏览器绘制

useCallback vs useMemo 的区别

useMemo

js 复制代码
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

useMemo:与 memo 的理念上差不多,都是判断是否满足当前的限定条件来决定是否执行 callback 函数,而 useMemo 的第二个参数是一个数组,通过这个数组来判定是否执行回调函数。

| 当一个父组件中调用了一个子组件的时候,父组件的 state 发生变化,会导致父组件更新,而子组件虽然没有发生改变,但也会进行更新。

只要父组件的状态更新,无论有没有对子组件进行操作,子组件都会进行更新,useMemo 就是为了防止这点而出现的。

useCallback

| useCallback 可以理解为 useMemo 的语法糖

js 复制代码
const memoizedCallback = useCallback(() => doSomething(a, b), [a, b]);

useCallback 与 useMemo 极其类似,唯一不同的是

  • useMemo 返回的是函数运行的结果
  • useCallback 返回的是函数
    • 这个函数是父组件传递给子组件的一个函数,防止做无关的刷新
    • 其次,这个子组件必须配合 React.memo,否则不但不会提升性能,还有可能降低性能

React.memo

memo:结合了 pureComponent 纯组件和 componentShouldUpdate()功能,会对传入的 props 进行一次对比,然后根据第二个函数返回值来进一步判断哪些 props 需要更新

要注意 memo 是一个高阶组件,函数式组件和类组件都可以使用。

js 复制代码
// memo接收两个参数

function MyComponent(props) {}
function areEqual(prevProps, nextProps) {}
export default React.memo(MyComponent, areEqual);
  1. 第一个参数:组件本身,也就是要优化的组件
  2. 第二个参数:(pre,next) => boolean
    • pre:之前的数据
    • next:现在的数据
    • 返回一个布尔值
    • 为 false 更新

memo 的注意事项

React.memo 与 PureComponent 的区别:

  • 服务对象不同
    • PureComponent 服务于类组件
    • React.memo 即可以服务于类组件,也可以服务与函数式组件
    • useMemo 服务于函数式组件
  • 针对的对象不同:
    • PureComponent 针对的是 props 和 state
    • React.memo 只能针对 props 来决定是否渲染

React.memo 的第二个参数的返回值与 shouldComponentUpdate 的返回值是相反的。

  • React.memo:返回 true 组件不渲染,返回 false 组件重新渲染。
  • shouldComponentUpdate:返回 true 组件渲染,返回 false 组件不渲染

类组件与函数组件的区别

相同点

组件是 React 可复用的最小代码片段,它们会返回要在页面中渲染 React 元素,也正是基于这一点,所以在 React 中无论是函数组件,还是类组件,其实它们最终呈现的效果都是一致的。

不同点

设计思想

  1. 类组件的根基是 OOP(面向对象),所以它会有继承,有内部状态管理等
  2. 函数组件的根基是 FP(函数式编程)

未来的发展趋势

React 团队从 Facebook 的实际业务场景触发,通过探索世界切片和并发模式,以及考虑性能的进一步优化和组件间更合理的代码拆分后,认为类组件的模式并不能很好地适应未来的趋势,他们给出了一下 3 个原因:

  1. this 的模糊性
  2. 业务逻辑耦合在生命周期中
  3. react 的组件代码缺乏标准的拆分方式

React 组件优化

  1. 父组件刷新,而不波及子组件
  2. 组件自己控制自己是否刷新
  3. 减少波及范围,无关刷新数据不存入 state 中
  4. 合并 state,减少重复 setState 操作

父组件刷新,而不波及子组件

  1. 子组件自己判断是否需要更新,典型的就是
    • PureComponent
    • shouldComponentUpdate
    • React.memo
  2. 父组件对子组件做个缓冲判断
使用 PureComponent 注意点
  1. 父组件是函数组件,子组件用 PureComponent 时,匿名函数,箭头函数和普通函数都会重新声明
    • 可以使用 useMemo 或者 useCallback,利用他们缓冲一份函数,保证不会出现重复声明就可以了。
  2. 类组件中不使用箭头函数,匿名函数
    • class 组件中每一次刷新都会重复调用 render 函数,那么 render 函数中使用的匿名函数,箭头函数就会造成重复刷新的问题
    • 处理方式-换成普通函数
  3. 在 class 组件的 render 函数中调用 bind 函数
    • 把 bind 操作放在 constructor 中
shouldComponentUpdate

class 组件中使用 shouldComponentUpdate 时主要的优化方式,它不仅仅可以判断来自父组件的 nextprops,还可以根据 nextState 和最新的 nextContext 来决定是否更新。

React.memo

React.memo 的规则是如果想要复用最后一次渲染结果,就返回 true,不想复用就返回 false。所以它和 shouldComponentUpdate 的正好相反,false 才会更新,true 就返回缓冲。

js 复制代码
const Children = React.memo(
	function ({ count }) {
		return (
			<div>
				只有父组件传入的值是偶数的时候才会更新
				{count}
			</div>
		);
	},
	(prevProps, nextProps) => {
		if (nextProps.count % 2 === 0) {
			return false;
		} else {
			return true;
		}
	}
);
使用 React.useMemo 来实现对子组件的缓冲

子组件只关心 count 数据,当我们刷新 name 数据的时候,并不会触发刷新 Children 子组件,实现了我们对组件的缓冲控制。

js 复制代码
export default function Father() {
	let [count, setCount] = React.useState(0);
	let [name, setName] = React.useState(0);
	const render = React.useMemo(() => <Children count={count} />, [count]);
	return (
		<div>
			<button onClick={() => setCount(++count)}>点击刷新count</button>
			<br />
			<button onClick={() => setName(++name)}>点击刷新name</button>
			<br />
			{'count' + count}
			<br />
			{'name' + name}
			<br />
			{render}
		</div>
	);
}

减少波及范围,无关刷新数据不存入 state 中

  1. 无意义重复调用 setState,合并相关的 state
  2. 和页面刷新无关的数据,不存入 state 中
  3. 通过存入 useRef 的数据中,避免父子组件的重复刷新
  4. 合并 state,减少重复 setState 的操作
    • ReactDOM.unstable_batchedUpdates
    • 多个 setState 会合并执行一次

React-Router 实现原理

react-router-dom 和 react-router 和 history 库三者什么瓜系

  1. history 可以理解为 react-router 的核心,也是整个路由原理的核心,里面集成了 popState,history.pushState 等底层陆游实现的原理方法
  2. react-router 可以理解为是 react-router-dom 的核心,里面封装了 Router,Route,Switch 等核心组件,实现了从路由的改变到组件的更新的核心功能。
  3. react-router-dom,在 react-router 的核心基础上,添加了用于跳转的 Link 组件,和 history 模式下的 BrowserRouter 和 hash 模式下的 HashRouter 组件等。
    • 所谓 BrrowserRouter 和 HashRouter,也只不过用了 history 库中 createBrowserHistory 和 createHashHistory 方法

单页面实现核心原理

单页面应用路由实现原理是,切换 url,监听 url 变化,从而渲染不同的页面组件。

主要的方式有 history 模式和 hash 模式

history 模式原理
  1. 改变路由 history.pushState(state,title,path)
  2. 监听路由 window.addEventListener("popstate",function(e){})
hash 模式原理
  1. 改变路由 通过 window.location.hash 属性获取和设置 hash 值
  2. 监听路由 window.addEventListener("hashchange",function(e){})

Vue 和 React 的区别

共同点

  1. 数据驱动视图
  2. 组件化
  3. 都使用 Virtual DOM

不同点

  1. 核心思想
    • Vue 灵活易用的渐进式框架,进行数据拦截/代理,它对侦测数据的变化更敏感、更精确
    • React 推崇函数式编程(纯组件),数据不可变以及单向数据流
  2. 组件写法差异
    • React 推荐的做法是 JSX + inline style,也就是把 HTML 和 CSS 全都写进 JavaScrip 中,即 all in js
    • Vue 推荐的做法是 template 的单文件组件格式即 html,css,JS 写在同一文件
  3. diff 算法不同
    • 两者流程思路上是类似的:不同的组件产生不同的 DOM 结构。当 type 不相同时,对应 DOM 操作就是直接销毁老的 DOM,创建新的 DOM。同一层次的一组子节点,可以通过唯一的 key 区分
    • Vue-diff 算法采用了双端比较的算法,同时从新旧 children 的两端开始进行比较,借助 key 值找到可复用的节点,再进行相关操作。相比 React 的 diff 算法,同样情况下可以减少移动节点次数,减少不必要的性能损耗,更加的优雅。
  4. 响应式原理不同
    • Vue 依赖收集,自动优化,数据可变,当数据改变时,自动找到引用组件重新渲染
    • React 基于状态机,手动优化,数据不可变,需要 setState 驱动新的 state 替换老的 state。当数据改变时,以组件为根目录,默认全部重新渲染。

Fiber 实现时间切片的原理

React15 架构缺点

React16 之前的版本比更新虚拟 DOM 的过程时采用循环递方式来实现的,这种比对方式有一个问题,就是一旦任务开始进行就无法中断 ,如果应用中数组数量庞大,主线程被长期占用,知道整颗虚拟 DOM 树比对更新完成之后主线程才会被释放,主线程才能执行其他任务,这就会导致一些用户交互或动画等任务无法立即得到执行,页面就会产生卡顿,非常的影响用户体验。

主要原因就是递归无法中断,执行重的任务耗时较长,javascript 又是单线程,无法同时执行其他任务,导致任务延迟页面卡顿用户体验差。

Fiber 架构

界面通过 vdom 描述,但是不是直接手写 vdom,而是 jsx 编译产生的 render function 之后以后生成的。这样就可以加上 state、props 和一些动态逻辑,动态产生 vdom。

vdom 生成之后不再是直接渲染,而是先转成 fiber,这个 vdom 转 fiber 的过程叫做 reconcile。

fiber 是一个链表结构,可以打断,这样就可以通过 requestIdleCallback 来空闲调度 reconcile,这样不断的循环,直到处理完所有的 vdom 转 fiber 的 reconcile,就开始 commit,也就是更新到 dom。

reconcile 的过程会提前创建好 dom,还会标记出增删改,那么 commit 阶段就很快了。

从之前递归渲染时做 diff 来确定增删改以及创建 dom,提前到了可打断的 reconcile 阶段,让 commit 变得非常快,这就是 fiber 架构的目的和意义。

并发&调度(Concurrency & Scheduler)
  • Concurrency 并发:有能力优先处理更高优事务,同时对正在执行的中途任务可暂存,待高优完成后,再去执行。
  • Scheduler 协调调度:暂存未执行任务,等待时机成熟后,再去安排执行剩下未完成任务。

考虑到可中断渲染,并可重回构造。React 自行实现了一套体系叫做 React fiber 架构。

React Fiber 核心:自行实现 虚拟栈帧。

schedule 就是通过空闲调度每个 fiber 节点的 reconcile(vdom 转 fiber),全部 reconcile 完了就执行 commit

Fiber 的数据结构有三层信息:(采用链表结构

  1. 实例属性 该 Fiber 的基本信息,例如组件类型等。
  2. 构建属性 构建属性(return、child、sibling)
  3. 工作属性
    • 数据的变更会导致 ui 层的变更
    • 为了减少对 DOM 的直接操作,通过 Reconcile 进行 diff 查找,并将需要变更节点,打上标签,变更路径保留在 effectList 里。
    • 待变更内容要有 Scheduler 优先级处理
    • 涉及到 diff 等查找操作,时需要有个高效手段来处理前后变化,即双缓存机制。

链表结构即可支持随时中断的诉求

Scheduler 运行核心点
  1. 有个任务队列 queue,该队列存放可中断的任务。
  2. workLoop 对队列里取第一个任务 currentTask,进入循环开始执行。
    • 当该任务没有时间或需要中断(渲染任务 或 其他高优任务插入等),则让出主线程。
  3. requestAnimationFrame 计算一帧的空余时间;
  4. 使用 new MessageChannel()执行宏任务;

React 实现原理

React-Hook 为什么不能放到条件语句中

每一次渲染都是完全独立的。

每次渲染具有独立的状态值(每次渲染都是完全独立的)。也就是说,每个函数中的 state 变量只是一个简单的常量,每次渲染时从钩子中获取到的常量,并没有附着数据绑定之类的神奇魔法。

这也就是老生常谈的 CaptureValue 特性。可以看下面这段经典的计数器

js 复制代码
function Counter() {
	const [count, setCount] = useState(0);
	function handleAlertClick() {
		setTimeout(() => {
			alert('You clicked on: ' + count);
		}, 3000);
	}
	return (
		<div>
			<p>You clicked {count} times</p>
			<button onClick={() => setCount(count + 1)}>Click me</button>
			<button onClick={handleAlertClick}>Show alert</button>
		</div>
	);
}

按下面步骤操作:

  • 点击 Click me 按钮,把数字增加到 3;
  • 点击 Show alert 按钮
  • 在 setTimeout 触发之前点击 Click me,把数字增加到 5。

结果是 Alert 显示 3

解释一下:

  • 每次渲染相互独立,因此每次渲染时组件中的状态、事件处理函数等等都是独立的,或者说只属于所在的那一次渲染
  • 我们在 count 为 3 的时候触发了 handleAlertClick 函数,这个函数所记住的 count 也为 3
  • 三秒后,刚才函数的 setTimeout 结束,输出当时记住的结果:3

深入 useEffect 本质

注意其中一些细节:

  • useState 和 useEffect 仔每次调用时都被添加到 Hook 链表中;
  • useEffect 还会额外地仔一个队列中添加一个等待执行的 effect 函数;
  • 在渲染完成后,依次调用 Effect 队列中的每一个 Effect 函数。

React 官方文档 Rules of Hooks 中强调过一点:

Only call hooks at the top level. 只在最顶层使用 Hook。

具体地说,不要在循环、嵌套、条件语句中使用 hook

因为这些动态的语句很有可能会导致每次执行组件函数时调 Hook 的顺序不能完全一致,导致 Hook 链表记录的数据失效

自定义 hook 实现原理

组件初次渲染

在 app 组件中调用了 usecustomHook 钩子。可以看到,即便我们切换到了自定义 hook 中,Hook 链表的生成依旧没有改变。

组件重新渲染

即便代码的执行进入到自定义 Hook 中,依然可以从 Hook 链表中读取到相应的数据,这个配对的过程总能成功。

而 Rules of Hook。它规定只有在两个地方能勾使用 React Hook:

  1. React 函数组件
  2. 自定义 Hook

第一点毋庸置疑,第二点通过刚才的两个动画你也可以轻松的得出一个结论:

自定义 Hook 本质上只是把调用内置 Hook 的过程封装成一个个可以复用的函数,并不影响 Hook 链表的生产和读取。

useCallBack

依赖数组在判断元素是否发生改变时使用了 object.is 进行比较,因此当 deps 中某一元素为非原始类型时(例如函数、对象等),每次渲染都会发生改变,从而每次都会触发 Effect,失去了 deps 本身的意义。

useCallBack 使用方法和原理解析

为了解决函数在多次渲染中的引用相等问题,React 引入了一个重要的 Hook - useCallBack。

js 复制代码
const memoizedCallback = useCallback(callback, deps);

第一个参数 callback 就是需要记忆的函数,第二个参数时 deps 参数,同样也是一个依赖数组。在 Memoization 的上下文中,这个 deps 的作用相当于缓存中的键(key),如果键没有改变,那么就直接返回缓存中的函数,并且确保时引用相同的函数。

组件初次渲染(deps 为空数组的情况)

调用 useCallback 也是追加到 Hook 链表上,不过这里着重强调了这个函数 f1 所指向的内存位置 ,从而明确告诉我们:这个f1 始终是指向同一个函数。然后返回的 onClick 则是指向 Hook 中存储的 f1

组件重新渲染

重渲染的时候,再次调用 useCallback 同样返回给我们 f1 函数,并且这个函数还是指向同一快内存,从而使得 onClick 函数和上次渲染时真正做到了引用相等。

React 组件通信方式

react 组件通信常见的几种情况:

  1. 父组件向子组件通信
  2. 子组件向父组件通信
  3. 跨级组件通信
  4. 非嵌套关系的组件通信

父组件项子组件通信

父组件通过 props 向子组件传递需要的信息。父传子是在父组件中绑定一个正常的属性,这个属性就是指具体的值,在子组件中,用 props 就可以获取到这个值

js 复制代码
// 子组件
const Child = (props) => {
	return <p>{props.name}</p>;
};
// 父组件
const Parent = () => {
	return <Child name="京 程一灯" />;
};

子组件向父组件通信

props+回调的方式,使用公共组件进行状态提升。子传父是先在父组件上绑定属性设置为一个函数,当子组件需要给父组件传值的时候,则通过 prpos 调用该函数将参数传入到该函数当中,此时就可以在父组件的函数中接收到该参数了,这个参数则为子组件传递过来的值

js 复制代码
// 子组件
const Child = (props) => {
	const cb = (msg) => {
		return () => {
			props.callback(msg);
		};
	};
	return <button onClick={cb('上海欢迎你')}>上海欢迎你</button>;
};

// 父组件
class Parent extends Component {
	callback(msg) {
		console.log(msg);
	}
	render() {
		return <Child callback={this.callback.bind(this)} />;
	}
}

// 函数式组件

const Child = ({ callback }) => {
	return <button onClick={callback('上海欢迎你')}>上海欢迎你</button>;
};

class Parent extends Component {
	callback(msg) {
		console.log(msg);
	}
	render() {
		return <Child callback={(msg) => callback(msg)} />;
	}
}

跨级组件通信

即父组件向子组件的子组件通信,向更深层子组件通信。

  • 使用 props,利用中间组件层层传递,但是如果父组件结构较深,那么中间每一层组件都要去传递 props,增加了 复杂度,并且这些 props 并不是中间组件自己需要的。
  • 使用 context,context 相当于一个大容器,我们可以把要通信的内容放在这个容器中,这样不管潜逃多深,都可以随意取用,对于跨越多层的全局数据可以使用 context 实现。
js 复制代码
// context方式实现跨级组件通信
// context设计目的是为了共享哪些对于一个组件树而言是全聚德数据
const BatteryContext = createContext();
// 子组件的儿子
const GrandChild = () => {
	return (
		<BatteryContext.Consumer>
			{(value) => <h1>我 是 红色 的 :{value}</h1>}
		</BatteryContext.Consumer>
	);
};

// 子组件
const Child = () => {
	return <GrandChild />;
};

// 父组件
const Parent = () => {
	const [color, setColor] = useState('red');

	return (
		<BatteryContext.Provider value={color}>
			<Child />
		</BatteryContext.Provider>
	);
};

非嵌套关系的组件通信

即没有任何包含关系的组件,包括兄弟组件以及不再同一个父级中的非兄弟组件。

  1. 可以使用自定义事件通信(发布订阅模式),使用 pubsub-js
  2. 可以通过 redux 等进行全局状态管理
  3. 如果是兄弟组件通信,可以找到这两个兄弟节点共同的父节点,结合父子间通信方式进行通信

Redux 内部实现

createStore

js 复制代码
function createStore(reducer, preloadedState, enhancer) {
	let state;
	// 用 于 存 放被 subscribe 订 阅 的 函数(监听函数)
	let listeners = [];
	// getState 是一个 很 简 单 的 函数
	const getState = () => state;
	return {
		dispatch,
		getState,
		subscribe,
		replaceReducer,
	};
}

dispatch

js 复制代码
function dispatch(action) {
	// 通 过 reducer 返 回新 的 state
	// 这个 reducer 就 是 createStore 函数的 第一个 参数
	state = reducer(state, action);
	// 每一 次 状 态 更 新 后,都 需 要 调用 listeners listeners.forEach(listener => listener());
	return action; // 返 回 action
}

subscribe

js 复制代码
function subscribe(listener) {
	listeners.push(listener);
	// 函数取 消 订 阅 函数
	return () => {
		listeners = listeners.filter((fn) => fn !== listener);
	};
}

combineReducers

js 复制代码
function combineReducers(reducers) {
	return (state = {}, action) => {
		// 返 回的 是一个 对 象,reducer 就 是 返 回的 对 象
		return Object.keys(reducers).reduce(
			(accum, currentKey) => {
				accum[currentKey] = reducers[currentKey](state[currentKey], action);
				return accum;
			},
			{} // accum 初 始值是 空 对 象
		);
	};
}

applyMiddleware

js 复制代码
function applyMiddleware(...middlewares) {
	return function (createStore) {
		return function (reducer, initialState) {
			var store = createStore(reducer, initialState);
			var dispatch = store.dispatch;
			var chain = [];
			var middlewareAPI = {
				getState: store.getState,
				dispatch: (action) => dispatch(action),
			};
			chain = middlewares.map((middleware) => middleware(middlewareAPI));
			dispatch = compose(...chain)(store.dispatch);

			return { ...store, dispatch };
		};
	};
}
相关推荐
NightCyberpunk3 小时前
JavaScript学习笔记
javascript·笔记·学习
北极糊的狐3 小时前
vue使用List.forEach遍历集合元素
前端·javascript·vue.js
老码沉思录3 小时前
React Native 全栈开发实战班 - 性能与调试之内存管理
javascript·react native·react.js
ZVAyIVqt0UFji3 小时前
Reactflow图形库结合Dagre算法实现函数资源关系图
开发语言·前端·javascript·ecmascript
cooldream20094 小时前
快速上手 Vue 3 的高效组件库Element Plus
前端·javascript·vue.js·element plus
疯狂的沙粒4 小时前
Vue项目开发 vue实例挂载的过程?
前端·javascript·vue.js
zxg_神说要有光5 小时前
快速入门 AI:调用 AI 接口生成 React 组件
前端·javascript·node.js
佚名程序员5 小时前
【Node.js】深入理解 V8 JavaScript 引擎
前端·javascript·node.js
最近好乐5 小时前
TS入门——快速上手(一)
前端·javascript·面试
小粉粉hhh5 小时前
CSS3新特性——字体图标、2D、3D变换、过渡、动画、多列布局
前端·javascript·html