前端知识点-react

121 阅读7分钟

React

生命周期

初始化阶段

  • constructor,在这个阶段初始化 state 和 props,在更新阶段不会执行

挂载阶段

  • componentWillMount 16.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) ):

  1. DOM 结点跨层级移动情况很少,可以不考虑;
  2. 同类的组件生成相似的树结构,不同类的组件生成不同的树结构
  3. 同层级的组件,可以通过唯一标识区分 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 实现了自己的组件调用栈,以链表的形式遍历组件树,可以灵活的暂停、继续和丢弃执行的任务(浏览器 requestIdleCallback API,react 用 RequestAnimationFrame+postMessage实现)
  • fiber tree 是按照虚拟 DOM 生成的,只是结点携带的信息不一样。因此每个组件实例和 DOM 结点的抽象表示都是一个 fiber。

hook

  1. 为什么hook使用链表而不是数组
  • 因为数组是一块连续的内存,链表可以不是连续的内存
  • 链表和数组的查找性一样都是O(n)
  • 但插入删除的性能更好,数组需要大面积的移位,

react 框架如何运作

内部分三层:

  1. virtualDom,描述页面
  2. reconciler(fiber reconciler),调用组件声明周期,进行 diff 运算
  3. rerender,根据不同的平台渲染不同的页面,比如 reactDOMreactNative
  • 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)

  • 时间分片指的是把多个小粒度的任务放到一个时间切片(帧)中执行的一种方案
  • 时间分片依赖于requestIdleCallback API,在浏览器空闲的时候执行低优先级的任务

渲染挂起(Suspense)

  • Suspense 让子组件在渲染之前等待,并在等待的时候显示 fallback 的内容
  1. 异步加载组件(react v16 只支持这个场景)
const LazyComponent = React.lazy(() => import("./component"));

const SuspenseComponent = () => {
    return (
        <React.Suspense fallback={<Spinner />}>
            <LazyComponent />
        </React.Suspense>
    )
};
  1. 异步获取数据(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 优化手段

  1. 减少重新 render 的次数
    • React.memo():仅检查 props 的变更
    • React.PureComponent:浅对比 state 和 props 实现了 shouldComponentUpdate;但是在数据层次较复杂的情况下组件不能得到正确的更新
    • Context 中只定义被公用的信息
    • 避免将回调函数包裹在匿名函数中通过 props 传递,因为每次
  2. 降低重复的计算
    • React.useMemo(fn, [dep])
  3. 渲染列表时记得加上 key
  4. 使用 immutable 数据(immutable.js)