Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[笔记]- React 相关 #27

Open
zhaofeihao opened this issue May 4, 2020 · 0 comments
Open

[笔记]- React 相关 #27

zhaofeihao opened this issue May 4, 2020 · 0 comments
Labels

Comments

@zhaofeihao
Copy link
Owner

[TOC]
印象笔记转过来的,部分图片丢失

生命周期

提到 React 就必需会的生命周期
1833e217417d195e41de27815b891119.png
很全面 - React生命周期管理

渲染过程

graph TD
A[jsx] -->|createElement| B(virtual-DOM)
B -->|mountComponent| C(真实DOM)
C --> D(state/props发生改变)
D --> E(重新生成V-DOM)
E --> F{diff算法}
F -->|节点类型不同| G[删旧建新]
F -->|节点类型相同| H[DOM/React]
H -->|DOM元素| I[更新属性]
H -->|React组件| J[更新状态]
Loading
  • 在页面一开始打开的时候,React会调用render函数构建一棵Dom树;
    • 先将jsx文件中的代码通过createElement生成一个js对象,这个对象代表一个dom节点,也就是常说的虚拟dom;
    • 有了虚拟 dom,接下来的工作就是把这个虚拟 dom 树真正渲染成一个 dom 树。React 的做法是针对不同的 type 构造相应的渲染对象,渲染对象提供一个 mountComponent 方法(负责把对应的某个虚拟 dom 的节点生成具体的 dom node),然后循环迭代整个 vdom tree 生成一个完整的 dom node tree,最终插入容器节点;
  • 在state/props发生改变的时候,render函数会被再次调用渲染出另外一棵树,接着,React会用对两棵树进行对比,找到需要更新的地方批量改动,触发diff算法。
    • 对于不同的节点类型,react会基于第一条假设(两个相同的组件产生类似的DOM结构,不同组件产生不同DOM结构),直接删去旧的节点,新建一个新的节点。
    • 当对比相同的节点类型比较简单,这里分为两种情况,一种是DOM元素类型,对应html直接支持的元素类型:div,span和p,还有一种是React组件。
      • 对于DOM元素类型,react会对比它们的属性,只改变需要改变的属性;
      • 对于react组件类型,实例仍保持一致,以让状态能够在渲染之间保留。React通过更新底层组件实例的props来产生新元素,并在底层实例上依次调用componentWillReceiveProps() 和 componentWillUpdate() 方法。有key的时候再去判断key。

虚拟DOM生成真实DOM的过程

虚拟的DOM的核心思想是:对复杂的文档DOM结构,提供一种方便的工具,进行最小化地DOM操作。

DOM很慢,而javascript很快,用javascript对象可以很容易地表示DOM节点。DOM节点包括标签、属性和子节点,通过VElement表示如下。

// 用JavaScript表示一个DOM节点
function VElement(tagName,props,children){
    this.tagName = tagName;
    this.props = props;
    this.children = children;
}

var ul = new VElement('ul', {id: 'list'}, [
  new VElement('li', {class: 'item'}, ['Item 1']),
  new VElement('li', {class: 'item'}, ['Item 2']),
  new VElement('li', {class: 'item'}, ['Item 3'])
])

现在ul只是一个 JavaScript 对象表示的 DOM 结构,页面上并没有这个结构。我们可以根据这个ul构建真正的

    VElement.prototype.render = function(){
        var el = document.createElement(this.tagName);
        var props = this.props;
    
        for(var prop in props){
            var value = props[prop];
            el.setAttribute(prop,value);
        }
        var children = this.children || [];
    
        children.forEach(function(child){
            var childEL = (child instanceof VElement)
            ?child.render() 
            : document.createTextNode(child);
    
            el.appendChild(childEL);
        })
        return el;
    }

    render方法会根据tagName构建一个真正的DOM节点,然后设置这个节点的属性,最后递归地把自己的子节点也构建起来。所以只需要:

    var ulRoot = ul.render();
    document.body.appendChild(ulRoot);

    你不知道的Virtual DOM(这篇实现DOM写的好)
    【React深入】深入分析虚拟DOM的渲染原理和特性

    Virtual Dom 的优势在哪里?

    答题思路:
    传统DOM的劣势 --> react diff --> vdom出现解决什么问题 --> vdom的优势

    传统前端编程方式是命令式的,直接操作DOM,代码可读性差可维护性低;

    react的声明式编程,抛弃了直接操作DOM,只关注数据变动,DOM操作由框架完成,提升了可读性可维护性;

    最初react在更新的过程中会刷新整个页面,后来引入的diff过程,对比数据变动前后的DOM结构,但DOM结构diff起来太复杂,由此引出了vdom;

    VDOM 和 Diff 算法的出现是为了解决由命令式编程转变为声明式编程、数据驱动后所带来的性能问题的。换句话说,直接操作 DOM 的性能并不会低于虚拟 DOM 和 Diff 算法,甚至还会优于。

    这么说的原因是因为 Diff 算法的比较过程,比较是为了找出不同从而有的放矢地更新页面。但是比较也是要消耗性能的。而直接操作 DOM 就是有的放矢,我们知道该更新什么不该更新什么,所以不需要有比较的过程。所以直接操作 DOM 效率可能更高。

    React 厉害的地方并不是说它比 DOM 快,而是说不管你数据怎么变化,我都可以以最小的代价来进行更新 DOM。 方法就是我在内存里面用新的数据刷新一个虚拟 DOM 树,然后新旧 VDOM 进行比较,找出差异,再更新到 DOM 树上。

    虚拟DOM的作用:

    • Virtual DOM 在牺牲(牺牲很关键)部分性能的前提下,增加了可维护性,这也是很多框架的通性。
    • 实现了对 DOM 的集中化操作,在数据改变时先对虚拟 DOM 进行修改,再反映到真实的 DOM 中,用最小的代价来更新 DOM,提升效率(要想想是跟哪个阶段相比提升了效率,别只记住了这一条)
    • 打开了函数式 UI 编程的大门
    • 可以渲染到 DOM 以外的端,使得框架跨平台,比如 ReactNative,React VR 等
    • 可以更好地实现 SSR、同构渲染等。这条其实是跟上面一条差不多的
    • 组件的高度抽象化

    虚拟DOM的缺点:

    • 首次渲染大量 DOM 时,由于多了一层虚拟 DOM 的计算,会比 innerHTML 插入慢。

    • 虚拟 DOM 需要在内存中维护一份 DOM 的副本(跟上面一条其实也差不多,上面一条是从速度上,这条是从空间上)。

    • 如果虚拟 DOM 大量更改,这是合适的。但是单一频繁地更新的话,虚拟 DOM 将会花费更多的时间处理计算的工作。所以,如果你有一个 DOM 节点相对较少页面,用虚拟 DOM,它实际上有可能会更慢。但对于大多数单页面应用,这应该都会更快。

    把这篇文章看完 - 从 React 历史的长河里聊虚拟 DOM 及其价值
    Virtual Dom 的优势在哪里?

    diff 算法

    React 在比较新旧 2 棵虚拟 DOM 树的时候,会同时考虑两点:

    • 尽量少的创建 / 删除节点,多使用移动节点的方式
    • 比较次数要尽量少,算法要足够的快

    React 选用了启发式的算法,将时间复杂度控制在 O(n) 的级别。这个算法基于以下 2 个假设:

    • 如果 2 个节点的类型不一样,就认为以这 2 个节点为根结点的树会完全不同
    • 对于多次 render 中结构保持不变的节点,开发者会用一个 key 属性标识出来,以便重用

    另外,React 只会对同一层的节点作比较,不会跨层级比较,如图所示:
    7757aa7dc1e9de3ba49143d850d2bfd1.png

    Diff 使用的是深度优先算法,当遇到下图这样的情况:
    6fd8c2a98274f2bbbaa3c47112dd42d1.png

    最高效的算法应该是直接将 A 子树移动到 D 节点,但这样就涉及到跨层级比较,时间复杂度会陡然上升。React 的做法比较简单,它会先删除整个 A 子树,然后再重新创建一遍。结合到实际的使用场景,改变一个组件的从属关系的情况也是很少的。

    3103150694c6a385a67bc590eebdf48e.png
    同样道理,当 D 节点改为 G 节点时,整棵 D 子树也会被删掉,E、F 节点会重新创建。

    对于列表的 Diff,节点的 key 有助于节点的重用

    4ad883e09aa34fc1ae6f977df99e5dcf.png
    如上图所示,当没有 key 的时候,如果中间插入一个新节点,Diff 过程中从第三个节点开始的节点都是删除旧节点,创建新节点。当有 key 的时候,除了第三个节点是新创建外,第四和第五个节点都是通过移动实现的。

    深挖key diff
    d131145ec392156ebd53edc206c4f0fb.png

    nextIndex lastIndex _mountIndex
    遍历 nextChildren 时候的 index,每遍历一个元素加 1 上一次从 prevChildren 中取出来元素时,这个元素在 prevChildren 中的 index 元素在数组中的位置

    下面我们来走一遍流程:

    • nextChildren 的第一个元素是 B ,在 prevChildren 中的位置是 1(_mountIndex),nextIndex 和 lastIndex 都是 0。节点移动的前提是_mountIndex < lastIndex,因此 B 不需要移动。lastIndex 更新为 _mountIndex 和 lastIndex 中较大的:1 。nextIndex 自增:1;
    • nextChildren 的第二个元素是 A ,在 prevChildren 中的位置是 0(_mountIndex),nextIndex 和 lastIndex 都是 1。这时_mountIndex < lastIndex,因此将 A 移动到 lastPlacedNode (B)的后面 。lastIndex 更新为 _mountIndex 和 lastIndex 中较大的:1 。nextIndex 自增:2;
    • nextChildren 的第三个元素是 D ,中 prevChildren 中的位置是 3(_mountIndex),nextIndex 是 2 ,lastIndex 是 1。这时不满足_mountIndex < lastIndex,因此 D 不需要移动。lastIndex 更新为 _mountIndex 和 lastIndex 中较大的:3 。nextIndex 自增:3;
    • nextChildren 的第四个元素是 C ,中 prevChildren 中的位置是 2(_mountIndex),nextIndex 是 3 ,lastIndex 是 3。这时_mountIndex < lastIndex,因此将 C 移动到 lastPlacedNode (D)的后面。循环结束。

    参考 - Diff 算法详解(主要看带key diff)
    react diff 策略(这篇更全)

    diff 策略(版本2)

    • Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计。
    • 拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构。
    • 对于同一层级的一组子节点,它们可以通过唯一 key 进行区分。

    tree diff

    基于策略一,React 对树的算法进行了简洁明了的优化,即对树进行分层比较,两棵树只会对同一层次的节点进行比较。
    即同一个父节点下的所有子节点。当发现节点已经不存在,则该节点及其子节点会被完全删除掉,不会用于进一步的比较。这样只需要对树进行一次遍历,便能完成整个 DOM 树的比较。

    component diff

    React 是基于组件构建应用的,对于组件间的比较所采取的策略也是简洁高效。

    • 如果是同一类型的组件,按照原策略继续比较 virtual DOM tree。
    • 如果不是,则将该组件判断为 dirty component,从而替换整个组件下的所有子节点。
    • 对于同一类型的组件,有可能其 Virtual DOM 没有任何变化,如果能够确切的知道这点那可以节省大量的 diff 运算时间,因此 React 允许用户通过 shouldComponentUpdate() 来判断该组件是否需要进行 diff。

    element diff(不带key)

    • element相同,原地复用
    • element 不同,删旧建新

    element diff(带key)

    image
    带key diff的过程看这里

    深度优先遍历

    二叉树与JavaScript

    react 事件机制

    【React深入】React事件机制

    调用 setState 之后发生了什么?

    • 当对象作为参数执行setState时,React内部会以一种对象合并的方式来批量更新组件的状态,类似于Object.assign(),把需要更新的state合并后放入状态队列,利用这个队列可以更加高效的批量更新state;
    • 当参数为函数时,React会将所有更新组成队列,并且按顺序来执行,这样避免了将state合并成一个对象的问题,之后会启动一个reconciliation调和过程,即创建一个新的 React Element tree(UI层面的对象表示)并且和之前的tree作比较,基于你传递给setState的对象找出发生的变化,最后更新DOM中需改动的部分。

    setState 同步异步问题

    • setState 只在合成事件和钩子函数中是“异步”的,在原生事件和 setTimeout 中都是同步的。
    • setState的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形式了所谓的“异步”,当然可以通过第二个参数 setState(partialState, callback) 中的callback拿到更新后的结果。
    • setState 的批量更新优化也是建立在“异步”(合成事件、钩子函数)之上的,在原生事件和setTimeout 中不会批量更新,在“异步”中如果对同一个值进行多次 setState , setState 的批量更新策略会对其进行覆盖,取最后一次的执行,如果是同时 setState 多个不同的值,在更新时会对其进行合并批量更新。

    详细解读见 - 你真的理解setState吗?

    例题

    React setState 笔试题,下面的代码输出什么?

    render 函数的返回值类型有哪些

    16.x render函数新增的返回类型
    render 函数

    在 render 函数中可以写 if ... else 吗

    React的8种条件渲染方法

    React 中如何减少 render 次数

    React优化:竭尽全力的减少render渲染
    性能!!让你的 React 组件跑得再快一点

    constructor 中的 super(props)的作用?传与不传有什么区别?

    调用super的原因:

    • ES6 中,在子类的 constructor 中必须先调用 super 才能引用 this;
    • super(props)的目的:在constructor中可以使用this.props;
    • 根本原因是constructor会覆盖父类的constructor,导致你父类构造函数没执行,所以手动执行下。

    如果要从另一个角度看的话:

    假设在es5要实现继承,首先定义一个父类:

    //父类
    function sup(name) {
        this.name = name;
    }
    //定义父类原型上的方法
    sup.prototype.printName = function (){
        console.log(this.name);
    }

    再定义他sup的子类,继承sup的属性和方法:

    function sub(name, age){
        sup.call(this,name);    //调用call方法,继承sup超类属性
        this.age = age;
    }    
    
    sub.prototype = new sup();   //把子类sub的原型对象指向父类的实例化对象,这样即可以继承父类sup原型对象上的属性和方法
    sub.prototype.constructor = sub;    //这时会有个问题子类的constructor属性会指向sup,手动把constructor属性指向子类sub
    //这时就可以在父类的基础上添加属性和方法了
    sub.prototype.printAge = function (){
        console.log(this.age);
    }

    这时调用父类生成一个实例化对象:

        let jack = new sub('feihao', 18);
        jack.printName() ;   //输出 : feihao
        jack.printAge();    //输出 : 18

    这就是es5中实现继承的方法。
    而在es6中实现继承:

      class sup {
            constructor(name) {
                this.name = name
            }
        
            printName() {
                console.log(this.name)
            }
        }
    
    class sub extends sup{
        constructor(name,age) {
            super(name) // super代表的事父类的构造函数
            this.age = age
        }
    
        printAge() {
            console.log(this.age)
        }
    }
    
    let jack = new sub('feihao', 18)
        jack.printName()    //输出 : feihao
        jack.printAge()    //输出 : 18

    为什么我们要写 super(props) ?

    Dialog 组件设计

    React造轮系列:对话框组件 - Dialog 思路

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant