React面试宝典
React Diff
在 React 中,diff 算法需要与虚拟 DOM 配合才能发挥出真正的威力。react 会使用 diff 算法计算出虚拟 DOM 中真正发生变化的部分,并且只会针对该部分进行 dom 操作,从而避免了对页面进行大面积的更新渲染,减少性能的开销。
React Diff 算法
在传统的 diff 算法中复杂度会达到 O(n^3)。React 中定义了三种策略,在对比时,根据策略只需要遍历一次树就可以完成对比,将复杂度降到了 O(n):
- tree diff: 在两个树对比时,只会比较同一层级的节点,会忽略掉跨层级的操作。
- component diff: 在对比两个组件时,首先会判断它们两个的类型是否相同,如果相同,则继续比较它们的 props,如果不同,则将该组件判断为 dirty component,从而替换整个组建下的所有子节点。
3. element diff: 对于同一层级的一组节点,会使用具有唯一性的 key 来区分是否需要创建,删除,或者是移动。
Element Diff
当节点处于同一层级时,React diff 提供了三种节点操作,分别为:
-
INSERE_MARKUP(插入)
- 新的 component 类型不再老集合里,即是全新的节点,需要对新节点执行插入操作。
-
MOVE_EXISTING(移动)
- 在老集合有新 component 类型,且 element 是可更新的类型,这种情况下 prevChild=nextChild,就需要做移动操作,可以复用以前的 DOM 节点。
-
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 调用流程:
-
调用 this.setState(newState)
-
将新状态 newState 存入 pending 队列
-
判断是否处于 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 阶段。
- 组件的 render 函数执行时称为 render(本次更新需要做哪些变更),纯 js 计算;
- 而将 render 的结果渲染到页面的过程称为 commit(变更到真实的宿主环境中,在浏览器中就是操作 DOM)。
在 Sync 模式下,render 阶段是一次性执行完成;而在 Concurrent 模式下 render 阶段可以被拆解,每个时间片执行一部分,知道执行完毕。由于 commit 阶段由 DOM 的更新,不可能让 DOM 更新到一半中断,必须一次性执行完毕。
React 并发新特性
并发渲染机制 concurrent rendering 的目的:根据用户的设备性能和网速对渲染过程进行适当的调整,保证 React 应用在长时间的渲染过程中依旧保持可交互性,避免页面出现卡顿或无响应的情况,从而提升用户体验。
- 新 rootAPI
- 通过 createRoot Api 手动创建 root 节点。
- 自动批处理优化 Automatic batching
- React 将多个状态更新分组到一个重新渲染中以获得更好的性能。(将多次 setstate 事件合并)
- 在 18 之前只在事件处理函数中实现了批处理,在 18 中所有更新都将自动批处理,包括 promise 链、setTimeout 等异步代码以及原生事件处理函数。
- 想退出自动批处理立即更新的话,可以使用 ReactDOM.flushSync()进行包裹
- startTransition
- 可以用来降低渲染优先级。分别用来包裹计算量的的 function 和 value,降低优先级,减少重复渲染次数。
- startTransition 可以指定 UI 的渲染优先级,哪些需要实时更新,哪些需要延迟更新
- hook 版本的 useTransition,接受传入一个毫秒的参数用来修改最迟更新时间,返回一个过渡期的 pending 状态和 startTransition 函数。
- useDefferdValue
- 通过 useDefferdValue 允许变量延迟更新,同时接受一个可选的延迟更新的最大值。React 将尝试尽快更新延迟值,如果在给定的 timeoutMs 期限内未能完成,它将强制更新
- const defferValue = useDeferredValue(value,{ tiemoutMs: 1000 })
- useDefferdValue 能够很好的展示并渲染时优先级调整的特性,可以用于延迟计算逻辑比较复杂的状态,让其他组件优先渲染,等待这个状态更新完毕之后再渲染。
React 生命周期
React 的生命周期主要是有两个比较大的版本,分别是
16.0 前 和 16.4 两个版本的生命周期。
16.0 前
总共分为四大阶段:
- 初始化 Intialization
- 挂载 Mounting
- 更新 Update
- 卸载 Unmounting
Intialization(初始化)
在初始化阶段,会用到 constructor()这个构造函数,如:
constructor(props){
super(props);
}
- super 的作用
- 用来调用基类的构造方法(constructor())
- 也将父组件的 props 注入给子组件,供子组件读取
- 初始化操作,定义 this.state 的初始内容
- 只会执行一次
Mounting(挂载)3 个
- componentWillMount:在组件挂载到 DOM 前调用
- 这里面调用的 this.setState 不会引起组件的重新渲染,也可以把写在这边的内容提到 constructor(),所以在项目中很少。
- 只会调用一次
- render:渲染
- 只要 props 和 state 发生改变(无论值是否有变化,两者的重传递和重赋值,都可以引起组件重新 redner),都会重新渲染 render。
- return:是必须的,是一个 React 元素, 不负责组件实际渲染工作,由 React 自身根据此元素去渲染出 DOM。
- render 是纯函数,不能执行 this.setState
- componentDidMount:组件挂载到 DOM 后调用 (调用一次)
Update(更新)5 个
- componentWillReceiveProps(nextprops):调用于 props 引起的组件更新过程中
- nextProps:父组件传给当前组件新的 props
- 可以用 nextProps 和 this.props 来查明重传 props 是否发生改变(原因:不能保证父组件重传 props 由变化)
- 只要 props 发生变化就会引起调用
- 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 了
- componentWillUpdate(nextProps,nextState):组件更新前调用
- 在 render 方法前执行
- 由于组件更新就会调用,所以一般很少使用
- render:重新渲染
- componentDidUpdate(prevProps,prevState):组件更新后被调用
- prevProps:组件更新前的 props
- prevState:组件更新前的 state
- 可以操作组件更新的 DOM
Unmounting(卸载)
componentWillUnmount:组件被卸载前调用
可以在这里执行一些清理工作,比如清除组件中使用的定时器,清除 componentDidMount 中手动创建的 DOM 元素等,以避免引起内存泄漏
16.4
与 16.0 的生命周期相比
新增了两个
- getDerivedStateFormProps
- getSnapshotBeforeUpdate
删除了是三个
- componentWillMount
- componentWillReceiveProps
- 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 使用:
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 这个语法糖开进一步简化调用方式
const [state, setState] = useImmer({
a: 1,
b: {
c: [1, 2],
d: 2,
},
});
setState((prev) => {
prev.b.c.push(3);
});
深入 useState 本质
当组件初次渲染(挂载)时
- 在初次渲染时,我们通过 useState 定义了多个状态;
- 每调用一次 useState,都会在组件之外生成一条 hook 记录,同时包括状态值和状态更新 setter 函数;
- 多次调用 useState 生产的 hook 记录形成了一条链表;
- 触发 onClick 回调函数,调用 setS1 函数修改 s1 的状态,不仅修改了 hook 记录中的状态值,还即将触发重渲染
组件重渲染时
在初次渲染结束后、重渲染之前,hook 记录链表依然存在。当我们逐个调用 useState 的时候,useState 便返回了 hook 链表中存储的状态,以及修改状态的 setter
useEffect
useEffect:副作用,你可以理解为是类组件的生命周期,也是我们最常见的钩子
副作用(side effect):是指 function 做了和本身运算返回值无关的事,如请求数据、修改全局变量,打印、数据获取、设置订阅以及手动更改 React 组件中的 DOM 都属于副作用操作
- 不断执行 当 useEffect 不设立第二个参数时,无论什么情况,都会执行
- 根据依赖值改变 设置 useEffect 的第二个值
useContext
useContext:上下文,类似于 Context:其本意就是设置全局共享数据,使所有组件可跨层级实现数据共享
useContext 的参数一般时由 createContext 创建,通过 xxContextProvider 包裹的组件,才能通过 useContext 获取对应的值
存在的问题及解决方案
useContext 是 React 官方推荐的共享状态的方式,然而在需要共享状态的组件非常多的情况下,着有这严重的性能问题, 例如有 A/B 组件,A 组件只更新 state.a,并没有用到 state.b,B 组件更新 state.b 的时候 A 组件也会刷新,在组件非常多的情况下,就卡死了,用户体验非常不好。
useReducer
类似于 redux 功能的 api
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。
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 的方式,即能保持引用,又不用声明依赖项。
const [state, setState] = useState('');
// func 地址永远不会变化
const func = useMemoizedFn(() => {
console.log(state);
});
useRef
可以获取当前元素的所有属性,并且返回一个可变的 ref 对象,并且这个对象只有 current 属性,可设置 initialValue
- 通过 useRef 获取对应的 React 元素的属性值
- 缓存数据
useLayoutEffect
与 useEffect 基本一致,不同的地方时,useLayouEffect 是同步要注意的是 useLayoutEffect 在 Dom 更新之后,浏览器绘制之前,这样做的好处是可以更加方便的修改 dom,获取 dom 信息, 这样浏览器只会绘制一次,所以 useLayoutEffect 仔 useEffect 之前执行。如果是 useEffect 的话,useEffect 执行仔浏览器绘制视图之后,如果在此时改变 dom,有可能会导致浏览器再次回流和重绘。
除此之外 useLayoutEffect 的 callback 中代码执行会阻塞浏览器绘制
useCallback vs useMemo 的区别
useMemo
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
useMemo:与 memo 的理念上差不多,都是判断是否满足当前的限定条件来决定是否执行 callback 函数,而 useMemo 的第二个参数是一个数组,通过这个数组来判定是否执行回调函数。
| 当一个父组件中调用了一个子组件的时候,父组件的 state 发生变化,会导致父组件更新,而子组件虽然没有发生改变,但也会进行更新。
只要父组件的状态更新,无论有没有对子组件进行操作,子组件都会进行更新,useMemo 就是为了防止这点而出现的。
useCallback
| useCallback 可以理解为 useMemo 的语法糖
const memoizedCallback = useCallback(() => doSomething(a, b), [a, b]);
useCallback 与 useMemo 极其类似,唯一不同的是
- useMemo 返回的是函数运行的结果
- useCallback 返回的是函数
- 这个函数是父组件传递给子组件的一个函数,防止做无关的刷新
- 其次,这个子组件必须配合 React.memo,否则不但不会提升性能,还有可能降低性能
React.memo
memo:结合了 pureComponent 纯组件和 componentShouldUpdate()功能,会对传入的 props 进行一次对比,然后根据第二个函数返回值来进一步判断哪些 props 需要更新
要注意 memo 是一个高阶组件,函数式组件和类组件都可以使用。
// memo接收两个参数
function MyComponent(props) {}
function areEqual(prevProps, nextProps) {}
export default React.memo(MyComponent, areEqual);
- 第一个参数:组件本身,也就是要优化的组件
- 第二个参数:(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 中无论是函数组件,还是类组件,其实它们最终呈现的效果都是一致的。
不同点
设计思想
- 类组件的根基是 OOP(面向对象),所以它会有继承,有内部状态管理等
- 函数组件的根基是 FP(函数式编程)
未来的发展趋势
React 团队从 Facebook 的实际业务场景触发,通过探索世界切片和并发模式,以及考虑性能的进一步优化和组件间更合理的代码拆分后,认为类组件的模式并不能很好地适应未来的趋势,他们给出了一下 3 个原因:
- this 的模糊性
- 业务逻辑耦合在生命周期中
- react 的组件代码缺乏标准的拆分方式
React 组件优化
- 父组件刷新,而不波及子组件
- 组件自己控制自己是否刷新
- 减少波及范围,无关刷新数据不存入 state 中
- 合并 state,减少重复 setState 操作
父组件刷新,而不波及子组件
- 子组件自己判断是否需要更新,典型的就是
- PureComponent
- shouldComponentUpdate
- React.memo
- 父组件对子组件做个缓冲判断
使用 PureComponent 注意点
- 父组件是函数组件,子组件用 PureComponent 时,匿名函数,箭头函数和普通函数都会重新声明
- 可以使用 useMemo 或者 useCallback,利用他们缓冲一份函数,保证不会出现重复声明就可以了。
- 类组件中不使用箭头函数,匿名函数
- class 组件中每一次刷新都会重复调用 render 函数,那么 render 函数中使用的匿名函数,箭头函数就会造成重复刷新的问题
- 处理方式-换成普通函数
- 在 class 组件的 render 函数中调用 bind 函数
- 把 bind 操作放在 constructor 中
shouldComponentUpdate
class 组件中使用 shouldComponentUpdate 时主要的优化方式,它不仅仅可以判断来自父组件的 nextprops,还可以根据 nextState 和最新的 nextContext 来决定是否更新。
React.memo
React.memo 的规则是如果想要复用最后一次渲染结果,就返回 true,不想复用就返回 false。所以它和 shouldComponentUpdate 的正好相反,false 才会更新,true 就返回缓冲。
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 子组件,实现了我们对组件的缓冲控制。
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 中
- 无意义重复调用 setState,合并相关的 state
- 和页面刷新无关的数据,不存入 state 中
- 通过存入 useRef 的数据中,避免父子组件的重复刷新
- 合并 state,减少重复 setState 的操作
- ReactDOM.unstable_batchedUpdates
- 多个 setState 会合并执行一次
React-Router 实现原理
react-router-dom 和 react-router 和 history 库三者什么瓜系
- history 可以理解为 react-router 的核心,也是整个路由原理的核心,里面集成了 popState,history.pushState 等底层陆游实现的原理方法
- react-router 可以理解为是 react-router-dom 的核心,里面封装了 Router,Route,Switch 等核心组件,实现了从路由的改变到组件的更新的核心功能。
- react-router-dom,在 react-router 的核心基础上,添加了用于跳转的 Link 组件,和 history 模式下的 BrowserRouter 和 hash 模式下的 HashRouter 组件等。
- 所谓 BrrowserRouter 和 HashRouter,也只不过用了 history 库中 createBrowserHistory 和 createHashHistory 方法
单页面实现核心原理
单页面应用路由实现原理是,切换 url,监听 url 变化,从而渲染不同的页面组件。
主要的方式有 history 模式和 hash 模式
history 模式原理
- 改变路由 history.pushState(state,title,path)
- 监听路由 window.addEventListener(“popstate”,function(e){})
hash 模式原理
- 改变路由 通过 window.location.hash 属性获取和设置 hash 值
- 监听路由 window.addEventListener(“hashchange”,function(e){})
Vue 和 React 的区别
共同点
- 数据驱动视图
- 组件化
- 都使用 Virtual DOM
不同点
- 核心思想
- Vue 灵活易用的渐进式框架,进行数据拦截/代理,它对侦测数据的变化更敏感、更精确
- React 推崇函数式编程(纯组件),数据不可变以及单向数据流
- 组件写法差异
- React 推荐的做法是 JSX + inline style,也就是把 HTML 和 CSS 全都写进 JavaScrip 中,即 all in js
- Vue 推荐的做法是 template 的单文件组件格式即 html,css,JS 写在同一文件
- diff 算法不同
- 两者流程思路上是类似的:不同的组件产生不同的 DOM 结构。当 type 不相同时,对应 DOM 操作就是直接销毁老的 DOM,创建新的 DOM。同一层次的一组子节点,可以通过唯一的 key 区分
- Vue-diff 算法采用了双端比较的算法,同时从新旧 children 的两端开始进行比较,借助 key 值找到可复用的节点,再进行相关操作。相比 React 的 diff 算法,同样情况下可以减少移动节点次数,减少不必要的性能损耗,更加的优雅。
- 响应式原理不同
- 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 的数据结构有三层信息:(采用链表结构)
- 实例属性 该 Fiber 的基本信息,例如组件类型等。
- 构建属性 构建属性(return、child、sibling)
- 工作属性
- 数据的变更会导致 ui 层的变更
- 为了减少对 DOM 的直接操作,通过 Reconcile 进行 diff 查找,并将需要变更节点,打上标签,变更路径保留在 effectList 里。
- 待变更内容要有 Scheduler 优先级处理
- 涉及到 diff 等查找操作,时需要有个高效手段来处理前后变化,即双缓存机制。
链表结构即可支持随时中断的诉求
Scheduler 运行核心点
- 有个任务队列 queue,该队列存放可中断的任务。
- workLoop 对队列里取第一个任务 currentTask,进入循环开始执行。
- 当该任务没有时间或需要中断(渲染任务 或 其他高优任务插入等),则让出主线程。
- requestAnimationFrame 计算一帧的空余时间;
- 使用 new MessageChannel()执行宏任务;
React 实现原理
React-Hook 为什么不能放到条件语句中
每一次渲染都是完全独立的。
每次渲染具有独立的状态值(每次渲染都是完全独立的)。也就是说,每个函数中的 state 变量只是一个简单的常量,每次渲染时从钩子中获取到的常量,并没有附着数据绑定之类的神奇魔法。
这也就是老生常谈的 CaptureValue 特性。可以看下面这段经典的计数器
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:
- React 函数组件
- 自定义 Hook
第一点毋庸置疑,第二点通过刚才的两个动画你也可以轻松的得出一个结论:
自定义 Hook 本质上只是把调用内置 Hook 的过程封装成一个个可以复用的函数,并不影响 Hook 链表的生产和读取。
useCallBack
依赖数组在判断元素是否发生改变时使用了 object.is 进行比较,因此当 deps 中某一元素为非原始类型时(例如函数、对象等),每次渲染都会发生改变,从而每次都会触发 Effect,失去了 deps 本身的意义。
useCallBack 使用方法和原理解析
为了解决函数在多次渲染中的引用相等问题,React 引入了一个重要的 Hook - useCallBack。
const memoizedCallback = useCallback(callback, deps);
第一个参数 callback 就是需要记忆的函数,第二个参数时 deps 参数,同样也是一个依赖数组。在 Memoization 的上下文中,这个 deps 的作用相当于缓存中的键(key),如果键没有改变,那么就直接返回缓存中的函数,并且确保时引用相同的函数。
组件初次渲染(deps 为空数组的情况)
调用 useCallback 也是追加到 Hook 链表上,不过这里着重强调了这个函数 f1 所指向的内存位置,从而明确告诉我们:这个f1 始终是指向同一个函数。然后返回的 onClick 则是指向 Hook 中存储的 f1
组件重新渲染
重渲染的时候,再次调用 useCallback 同样返回给我们 f1 函数,并且这个函数还是指向同一快内存,从而使得 onClick 函数和上次渲染时真正做到了引用相等。
React 组件通信方式
react 组件通信常见的几种情况:
- 父组件向子组件通信
- 子组件向父组件通信
- 跨级组件通信
- 非嵌套关系的组件通信
父组件项子组件通信
父组件通过 props 向子组件传递需要的信息。父传子是在父组件中绑定一个正常的属性,这个属性就是指具体的值,在子组件中,用 props 就可以获取到这个值
// 子组件
const Child = (props) => {
return <p>{props.name}</p>;
};
// 父组件
const Parent = () => {
return <Child name="京 程一灯" />;
};
子组件向父组件通信
props+回调的方式,使用公共组件进行状态提升。子传父是先在父组件上绑定属性设置为一个函数,当子组件需要给父组件传值的时候,则通过 prpos 调用该函数将参数传入到该函数当中,此时就可以在父组件的函数中接收到该参数了,这个参数则为子组件传递过来的值
// 子组件
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 实现。
// 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>
);
};
非嵌套关系的组件通信
即没有任何包含关系的组件,包括兄弟组件以及不再同一个父级中的非兄弟组件。
- 可以使用自定义事件通信(发布订阅模式),使用 pubsub-js
- 可以通过 redux 等进行全局状态管理
- 如果是兄弟组件通信,可以找到这两个兄弟节点共同的父节点,结合父子间通信方式进行通信
Redux 内部实现
createStore
function createStore(reducer, preloadedState, enhancer) {
let state;
// 用 于 存 放被 subscribe 订 阅 的 函数(监听函数)
let listeners = [];
// getState 是一个 很 简 单 的 函数
const getState = () => state;
return {
dispatch,
getState,
subscribe,
replaceReducer,
};
}
dispatch
function dispatch(action) {
// 通 过 reducer 返 回新 的 state
// 这个 reducer 就 是 createStore 函数的 第一个 参数
state = reducer(state, action);
// 每一 次 状 态 更 新 后,都 需 要 调用 listeners listeners.forEach(listener => listener());
return action; // 返 回 action
}
subscribe
function subscribe(listener) {
listeners.push(listener);
// 函数取 消 订 阅 函数
return () => {
listeners = listeners.filter((fn) => fn !== listener);
};
}
combineReducers
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
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 };
};
};
}