React
生命周期
初始化阶段
- constructor,在这个阶段初始化 state 和 props,在更新阶段不会执行
挂载阶段
componentWillMount16.3 废弃- 还没挂载 DOM
- render
- componentDidMount
- DOM 已挂载
- 发送网络请求、启用事件监听
- 可以调用 setState 更新阶段
- ~~componentWillReceiveProps(nextProps, nextState)~~16.3 废弃
- 主要监听 props 的改变,在这个生命周期里可以设置 state,不会引起二次更新
- static getDerivedStateFromProps(nextProps,nextState)
- 在组件实例化、props 变动、组件状态更新时调用
- 可以返回一个对象,会跟 state 合并,但是不会触发二次更新
- 主要是提供了一个在 props、state发生变动后,根据 props 修改 state 的一个时机
- 代替 componentWillMount、componentWillReceiveProps
- shouldComponentUpdate(nextProps, nextState)
- 需要返回 boolean 告知是否要更新,返回 false 就到此结束不会往下执行更新
- 这里不能设置 state,会引起循环调用
componentWillUpdate16.3 废弃- 在这里可以在 DOM 发生更新之前获取一些信息,比如元素坐标。
- 这里不能设置 state,还会引起循环调用
到此,state 和 props 都还未发生更新,这也是 setState 表现为异步的原因
- render
- getSnapshotBeforeUpdate
- 在 render 函数调用之后,实际的 Dom 渲染之前,函数的返回值会作为 componentDidUpdate 的第三个参数化
- componentDidUpdate(preProps, preState)
- DOM 已完成更新,props、state 都已更新
卸载阶段
- componentWillUnmount
- 在组件卸载及销毁之前调用,这里多处理一些清理操作,比如清除 timer,取消订阅等
setState 是异步还是同步
setState 本身的执行过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,因此在合成事件和钩子函数中不能立即拿到更新后的值,使得看起来像是异步的,可以用函数式的写法拿到更新之后的值 setState(partialState, callback)
- 异步:钩子函数、react 合成事件
- 同步:setTimeout、原生事件
- 说明:每次 setState 产生的新 state 会被放进一个队列里,有一个变量叫 isBathingUpdates,它默认是 false,表示不批量更新,也就是会直接更新 this.state,但是在调用事件处理函数之前,会调用一个 batchedUpdates 方法,这个方法会把 isBathingUpdates 设置为 true,就导致合成事件中的 setState 变成了异步。
- setState 的更新优化也是建立在异步这个行为上的,当多次更新 state 的值,会被合并成一次执行
虚拟 DOM
虚拟 DOM 主要是为了解决浏览器性能问题而设计的,使用 js 对象模拟的 DOM 结点,主要包含 tag、props、children 三个属性。
- react 中虚拟 dom 主要结构
ReactDOM.render(<h1>Hello World</h1>,document.getElementById('root'));
// babel 转换之后
ReactDOM.render(React.createElement(
'h1',
null,
'Hello World'
), document.getElementById('root'));
// 生成的 ReactElement(VDom)
{
$$typeof: Symbol(react.element)
key: null
props: {children: "Hello World"}
ref: null
type: "h1"
_owner: null
_store: {validated: false}
_self: null
_source: null
__proto__: Object
}
diff
详解 策略(算法复杂度从 O(n^3) 下降到 O(n) ):
- DOM 结点跨层级移动情况很少,可以不考虑;
- 同类的组件生成相似的树结构,不同类的组件生成不同的树结构
- 同层级的组件,可以通过唯一标识区分 tree diff
- 只会对同层级的节点进行比较,当发现节点不存在,会直接删掉节点及其子节点 component diff
- 同一类型的组件,按照原策略进行比较
- 不是同类型的组件直接替换
- 对于同一类型的组件,有可能其 Virtual DOM 没有任何变化,如果能够确切的知道这点那可以节省大量的 diff 运算时间,因此 React 允许用户通过 shouldComponentUpdate() 来判断该组件是否需要进行 diff element diff
- 设置唯一标识。如果没有唯一标识,那么会依次对节点进行比较,如果只是同层级的节点被移动了,没有 key 的话就会平白做一些删除创建节点的工作;如果有唯一 key,那么通过唯一 key 判断有节点,那么只需要进行移动
Fiber
详解 15.x 的问题
- 当 state 发生改变,react 会遍历所有结点计算出差异,再更新 UI,且整个过程是不能被打断的。我们知道,js 运算、页面布局、页面绘制都是运行在浏览器的主线程中,他们是互斥的,如果 js 运算持续占用主线程,那 UI 就不能及时得到更新,当运算量太大,超过 16ms,就会出现掉帧,页面表现就是卡顿。 解决方案
- 问题出现在 js 运算量庞大长时间占用主线程,那就将运算切割,分批完成。完成一部分任务之后,就将控制权交给浏览器,让浏览器做优先级更高的工作,然后再来做之前没做完的运算。
- react 将任务分成小片,在一个片段时间内运行这些分片,
- 一个 fiber 是一个工作单元,组件的本质可以看做输入数据,输出 UI 的描述信息(即虚拟DOM)
ui=f(data) - 旧版 react 通过递归的方式进行渲染,使用的是 js 引擎自身的函数调用栈,它会一直执行到栈空为止;fiber 实现了自己的组件调用栈,以链表的形式遍历组件树,可以灵活的暂停、继续和丢弃执行的任务(浏览器
requestIdleCallbackAPI,react 用RequestAnimationFrame+postMessage实现) - fiber tree 是按照虚拟 DOM 生成的,只是结点携带的信息不一样。因此每个组件实例和 DOM 结点的抽象表示都是一个 fiber。
hook
- 为什么hook使用链表而不是数组
- 因为数组是一块连续的内存,链表可以不是连续的内存
- 链表和数组的查找性一样都是O(n)
- 但插入删除的性能更好,数组需要大面积的移位,
react 框架如何运作
内部分三层:
- virtualDom,描述页面
- reconciler(fiber reconciler),调用组件声明周期,进行 diff 运算
- rerender,根据不同的平台渲染不同的页面,比如
reactDOM和reactNative
- fiber 是一种是数据结构
const fiber = {
stateNode, // 节点实例
child, // 子节点
sibling, // 兄弟节点
return, // 父节点
}
- 为了实现任务的有序执行,有一个调度器
Scheduler来进行任务分配,任务按优先级分:- synchronous,同步任务,优先级最高(跟旧版表现一致)
- task,当前调度正执行的任务
- animation,动画,在下一帧之前执行
- high,高优先级,在不久的将来立即执行
- low,低优先级,延迟一会儿执行
- offscreen,当前屏幕外的更新,优先级最低,下一次 render 执行
- 优先级高的任务(如键盘输入)可以打断低优先级的任务(如 diff),优先执行
- fiber 的两个阶段
- reconciliation 阶段:生成fiber树,得出需要更新的结点信息存在 effect 中。可以被打断,让优先级更高的任务先执行,从框架层面大大降低了页面掉帧的概率
- commit 阶段:将上一步 effect 中需要更新的结点一次性批量更新,不可以被打断
react 的渲染机制、更新机制
react 的异步渲染
时间分片(time slicing)
- 时间分片指的是把多个小粒度的任务放到一个时间切片(帧)中执行的一种方案
- 时间分片依赖于
requestIdleCallbackAPI,在浏览器空闲的时候执行低优先级的任务
渲染挂起(Suspense)
- Suspense 让子组件在渲染之前等待,并在等待的时候显示 fallback 的内容
- 异步加载组件(react v16 只支持这个场景)
const LazyComponent = React.lazy(() => import("./component"));
const SuspenseComponent = () => {
return (
<React.Suspense fallback={<Spinner />}>
<LazyComponent />
</React.Suspense>
)
};
- 异步获取数据(react v18)
- 请求需要额外做包装 详解
- Suspense 的原理
- 它其实是监听了子组件抛出来的 promise 异常
class Suspense extends React.Component {
state = { loading: true }
componentDidCatch(error){
if (error && typeof error.then === "function"){
error.then(() => {
this.setState({loading: true});
});
this.setState({loading: false});
}
}
render(){
const {callback, children} = this.props;
return this.state.loading ? callback : children;
}
}
react 优化手段
- 减少重新 render 的次数
- React.memo():仅检查 props 的变更
- React.PureComponent:浅对比 state 和 props 实现了 shouldComponentUpdate;但是在数据层次较复杂的情况下组件不能得到正确的更新
- Context 中只定义被公用的信息
- 避免将回调函数包裹在匿名函数中通过 props 传递,因为每次
- 降低重复的计算
- React.useMemo(fn, [dep])
- 渲染列表时记得加上 key
- 使用 immutable 数据(immutable.js)