React渲染原理(面试硬核问题)

渲染原理

在了解渲染原理之前我们有必要知道一些专业术语

  • 渲染: render, 生成用于显示的对象, 以及将这些对象形成真实的dom对象

  • React元素: React Element, 通过React.createElment创建(语法糖: jsx表达式)

// app存储了一个react元素div, 本质上app现在是一个对象, 而非真实的dom元素
const app = <div>我是一个react元素</div>
  • React节点: React Node, 专门用于渲染进UI界面的对象, React会通过React元素来创建React节点
    ReactDOM一定是通过React节点进行渲染的
    节点类型:
    1. React dom节点(ReactDOMComponent): 如果一个React元素的type值为字符串, 则会生成React dom节点
    2. React 组件节点(ReactCompsiteComponent): 创建该节点的React元素是一个函数或者是一个类
    3. Text 文本节点(TextNode): 由字符串, 数字创建的
    4. React 空节点: 由null undefined false, true创建的
    5. React 数组节点: 由一个数组创建的

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uSUwVYTA-1580978346793)('../../../csdn博客素材/react渲染原理/节点.png')]

首次渲染

  1. 根据参数的值创建节点
  2. 根据不同的节点做不同的事情
    • 文本节点: 通过document.createTextNode创建真实的文本节点
    • 空节点: 什么都不做
    • 数组节点: 遍历数组, 将数组每一项递归创建节点
    • DOM节点: 通过document.createElement创建真实的dom元素, 然后遍历对应React元素的children属性进行递归遍历操作
    • 组件节点:
      1. 函数组件: 调用函数, 该函数必须返回一个可以生成节点的内容, react会将该函数的返回结果递归生成节点
      2. 类组件: 创建类的实例对象保存到组件节点上, 然后立即调用组件的生命周期方法static getDerivedStateFromProps, 再运行组件的render方法返回生成节点的内容开始递归生成节点,在render方法运行完成之后, 将该组件的componentDidMount放入执行队列中等待虚拟dom树整体构建完毕并且将真实的dom挂载到页面后再按顺序执行(先进先出, 后进后执行)
  3. 生成虚拟don树以后, 将该树保存起来以便以后使用, 同时将创建出来的真实dom对象, 加入到容器中
    (会将自带的真实dom元素依次插入父级, 最后再将根元素一次性插入html中)

那么第一次渲染到底发生了什么?我们拿下方代码举例

const app = <div className = 'wrapper'>
                <h1>
                    标题
                    {['abc', null}
                </h1>
                <p>{ undefined }</p>
            </div>

ReactDOM.render(app, document.getElementById('root'));

我们用图来解析一下这一套流程是怎么渲染的

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bdKLJ3C8-1580978346794)('...')];

经过上方图中的操作以后, 就进行第三步操作将真实的dom放置进html了, 哦对了,上方的图他有一个大家都熟悉的名字叫做虚拟dom树

那么一个函数组件的渲染过程又是怎样的呢


function ChildB(props) {
    return (
        <div>helloWold</div>
    )
}

function ChildA(props) {
    return (
        <div>
            <ChildB />
        </div>
    )
}

ReactDOM.render(<ChildA />, document.getElementById('root'));

同样上图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-erRU0PfT-1580978346794)('...')]

函数组件在渲染中主要也就是看中这个返回值, 除了会生成一个组件节点以外其他方面笔者认为没有太大的冲突

最后我们再来看看类组件


class ChildA extends React.PureComponent {
    render() {
        return (
            <ChildB />
        )
    }
}


class ChildB extends React.PureComponent {
    render() {
        return (
            <h1>helloWorld</h1>
        )
    }
}
ReactDOM.render(<ChildA />, document.getElementById('root'));

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pfd2T7Xd-1580978346795)('...')]

类组件除了会保存类的实例以及会执行生命周期函数以外, 其他倒是也没太大的区别

说到类组件, 那我们应该要探究一下类组件的生命周期运行轨迹

class ChildA extends React.PureComponent {
    state = {

    }
    constructor(props) {
        super(props);
        console.log('1, ChildA的constructor执行');
    }

    componentDidMount() {
        console.log('8, ChildA的componentDidMount执行')
    }

    static getDerivedStateFromProps() {
        console.log('2, ChildA的getDeriveStateFromProps执行');
        return null;
    }

    render() {
        console.log('3, ChildA的render执行');
        return (
            <ChildB />
        )
    }
}


class ChildB extends React.PureComponent {
    state = {

    }
    constructor(props) {
        super(props);
        console.log('4, ChildB的constructor执行');
    }

    static getDerivedStateFromProps() {
        console.log('5, ChildB的getDeriveStateFromProps执行');
        return null;
    }
    componentDidMount() {
        console.log('7, ChildB的componentDidMount执行')
    }
    render() {
        console.log('6, ChildB的render执行');
        return (
            <h1>helloWorld</h1>
        )
    }
}
ReactDOM.render(<ChildA />, document.getElementById('root'));

上方的代码的生命周期函数输出顺序, 我想应该很简单都能猜到会按照顺序输出12345678了吧, ChildA会被先被生成实例, 所以ChildA的constructor会先行输出1, 而由我们之前的知识我们知道static getDerivedStateFromProps会在类实例生成以后就会调用, 所以2输出的理所当然, 之后render方法调用会输出3, (注意这个时候知识ChildA的render开始执行但并不是执行完毕)在render的解析过程中发现有类组件ChildB, 所以会按照上面的流程再走一次, 所以最后结果123456,同时当ChildB中的h1解析完毕以后代表ChildB的render执行完毕, 这个时候ChildB中的componentDidMount会被加入到执行队列中, 而后ChildArender执行完毕, childA的componentDidMount也被加入到执行队列中, 虚拟dom构建完毕真实dom挂载, 两个component遵循先进先执行的规则依次执行 如下图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KCo9LMjQ-1580978346795)('...')]

更新和卸载的渲染

更新节点

更新的场景

  1. 重新调用ReactDOM.render, 完全重新生成节点树(触发的是根节点更新)
  2. 在类组件的实例中调用setState(触发的该类组件的实例的节点的更新)

节点的更新

  • 如果调用的是ReactDOM.render, 进入根节点的对比更新(diff)
  • 如果调用的是setState
    1. 运行生命周期函数 static getDerivedStateFromProps
    2. 运行生命周期函数shouldComponentUpdate, 如果返回false则结束当前流程
    3. 运行render得到一个新的节点, 进入该新节点的对比更新
    4. 将生命周期函数getSnapshotBeforeUpdate加入执行队列等待执行
    5. 将生命周期函数componentDidMount加入执行队列等待执行

后续步骤

  1. 更新虚拟dom树, 完成真实的dom更新
  2. 依次调用执行队列中的componentDidMount(因为更新阶段可能会有新的组件被创建)
  3. 依次调用执行队列中的getSnapshotBeforeUpdate
  4. 依次调用执行队列中的componentDidUpdate
  5. 依次调用执行队列中的componentWillUnMount

那么什么是对比更新呢?就是将新产生的节点对比之前虚拟dom中的节点发现差异,完成更新**

问题:那么怎么进行对比更新呢?换一种说法程序怎么知道自己应该怎么对比呢?

如果做唯一标识的话可能会导致效率特别低, 比如我这个dom要更新他要找到之前的dom进行比对如果是
通过唯一标识的话, 它势必是要遍历所有的节点找到这一个唯一标识, 假设有几万个节点的话就会导致效率特别的低

所以react为了提高效率做出以下假设

  1. 假设节点不会进行层级的移动(对比时直接找到旧的树中对应位置的节点进行优化)
  2. 不同的节点类型会生成不同的结构
    • 相同的节点类型: 节点类型本身相同(比如之前是个文本节点现在也是文本节点), 如果是组件节点组件类型都必须相同(之前的节点是A现在是B就是不行的),如果是由React元素生成的, type值还必须是一致的
    • 不同的节点类型: 除去相同的节点就是不同的
  3. 多个兄弟节点通过唯一表示key来确定对比的新节点

找到了对比的目标

  1. 判断节点类型是否一致
    • 如果一致的话会根据不同的节点类型做不同的事情

      1. 空节点: 不做任何事情
      2. dom节点: 直接使用之前的真实dom对象, 不会再去创建新的真实dom, 但是将其属性的变化记录下来, 以待将来统一完成更新, 同时在遍历该新react元素的子元素, 进行递归对比更新
      3. 文本节点: 同样重用之前的真实dom对象, 但是记录更新的value值, 以待将来统一完成更新
      4. 组件节点
        函数节点
        直接重新调用函数得到一个节点对象重新进入递归对比更新
        类组件
        直接重用之前的组件实例, 调用生命周期方法 static getDerivedStateFromProps,再调用shouldComponentUpdate如果返回false则终止流程, 如果返回的是true就运行render得到新的节点对象再次进入递归对比更新, 然后将该对象的getSnapshotBeforeUpdate加入队列等待执行, 再讲该对象的componentDidUpdate放进执行队列等待执行
      5. 数组节点: 将数组的每一项拿出来进行递归对比更新
    • 不一致
      如果不一致的话会整体上卸载旧的节点, 全新创建新的节点
      创建新节点

      1. 进入上方写的节点首次渲染的流程

      卸载旧节点

      1. 如果旧节点是文本节点,dom节点, 空节点, 数组节点或函数组件节点的话, 直接放弃该节点, 如果节点有子元素, 递归卸载节点
      2. 如果节点是类组件节点, 直接放弃该节点, 调用该节点的componentWillUnMount, 再递归卸载子元素节点

没找到对比的目标

新的dom树种有节点被删除或者添加则会没有找到对比目标

创建新加入的节点
卸载多余的节点

我们最后来说说这个key值

key值的作用是用于通过旧节点得到对应的新节点, 帮助react找到对比目标

如果某个旧节点有key值, 则其更新时会寻找相同层级中的相同key值的节点进行对比, 如果找不到就会进入没有找到对比目标的流程

key值在某一个范围内应该唯一且稳定, 不然会对react的对比算法进行影响从而造成不必要的渲染

React要求在数组渲染中必须提供key值, 因为如果不提供key值会导致数组发生改变以后React永远无法找到对比目标, 所以永远都会卸载旧的创建新的, 根本不存在重用dom的可能, 同时在数组中尽量要避免用index为key值, 因为index也会改变会导致key值不稳定也会导致dom很难重用

至此, react首次渲染页面原理已经基本描述完毕, 都是笔者通过学习和研究自己的一些理解, 如果有不同的想法欢迎交流

发布了33 篇原创文章 · 获赞 11 · 访问量 2251

猜你喜欢

转载自blog.csdn.net/weixin_44238796/article/details/104198378