React 查漏补缺

作者: 880d91446f17 | 来源:发表于2018-11-22 21:51 被阅读11次

    在工作做我们一直使用react及react相关框架(antd/antd-mobile)

    但是对于react的深层的了解不够:JSX语法与虚拟DOM的关系?高阶组件是如何实现的?dom diff的原理?

    通过写一篇react小册来查缺补漏。

    JSX和虚拟DOM

    import React from 'react';
    import ReactDOM from 'react-dom';
    
    ReactDOM.render(
        <label className="test" htmlFor='hello'>
            hello<span>world</span>
        </label>, 
        document.getElementById('root')
    );
    

    使用 ReactDOM.render ,第一个参数传入JSX语法糖,第二个参数传入container,能简单实现在document上创建h1 dom节点。

    其实,内部的执行方式如下:

    import React from 'react';
    import {render} from 'react-dom';
    render(
        React.createElement(
            'h1',
            {name:'yy',className:'test'},
            'hello',
            React.createElement(
                'span',
                null,
                'world'
            )
        ),
        document.getElementById('root')
    );
    

    所以ReactDOM.render的时候,看似引入的React没有用,但必须引入。

    生产的HTML:

    <label for="hello" class="test">hello<span>world</span></label>
    

    debug一下react createElement源码了解流程:

    var React = {
        ...
        createElement: createElementWithValidation,
        ...
    }
    
    function createElementWithValidation(type, props, children) {
    
        var element = createElement.apply(this, arguments);
    
        ...//校验迭代器数组是否存在唯一key
        ...//校验fragment片段props
        ...//props type校验
    
        return element    
    }
    
    function createElement(type, config, children) {
      var propName = void 0;
    
      // Reserved names are extracted
      var props = {};
    
      var key = null;
      var ref = null;
      var self = null;
      var source = null;
    
      ...
    
      return ReactElement(type, key, ref, self, source, ReactCurrentOwner.current, props);
    
    }
    

    React.createElement 开始执行,完成了参数校验, 迭代 展开childrens的参数:type, props, key, ref, self, source。返回了一个类似于babel语法树结构的嵌套对象(只是个人认为...),如图下:

    image

    </figure>

    简化版:保留了返回对象中最关键的属性(type,props)

    function ReactElement(type,props) {
        this.type = type;
        this.props = props;
    }
    let React = {
        createElement(type,props={},...childrens){
            childrens.length===1?childrens = childrens[0]:void 0
            return new ReactElement(type,{...props,children:childrens})
        }
    };
    

    通过上面的梳理,React.createElement返回的是一个含有type(标签),和它标签属性和内部对象(children)的Object

    {
        props:{
            childrens:['text',{type:'xx',props:{}}]
            name:'xx'
            className:'xx'
        }
        type:'xx'
    }
    

    我们可以根据ReactDom.render()的入参,简写出它的实现方法。

    let render = (vNode,container)=>{
        let {type,props} = vNode;
        let elementNode = document.createElement(type); // 创建第一个元素
        for(let attr in props){ // 循环所有属性
            if(attr === 'children'){ // 如果是children表示有嵌套关系
                if(typeof props[attr] == 'object'){ // 看是否是只有一个文本节点
                    props[attr].forEach(item=>{ // 多个的话循环判断 如果是对象再次调用render方法
                        if(typeof item === 'object'){
                            render(item,elementNode)
                        }else{ //是文本节点 直接创建即可
                            elementNode.appendChild(document.createTextNode(item));
                        }
                    })
                }else{ // 只有一个文本节点直接创建即可
                    elementNode.appendChild(document.createTextNode(props[attr]));
                }
            }else{
                elementNode = setAttribute(elementNode,attr,props[attr])
            }
        }
        container.appendChild(elementNode)
    };
    
    function setAttribute(dom,name,value) {
        if(name === 'className') name = 'class'
    
        if(/on\w+/.test(name)){
            name = name.toLowerCase();
            dom[ name ] = value || '';
        }else if ( name === 'style' ) {
            if ( !value || typeof value === 'string' ) {
                dom.style.cssText = value || '';
            } else if ( value && typeof value === 'object' ) {
                for ( let name in value ) {
                    dom.style[ name ] = typeof value[ name ] === 'number' ? value[ name ] + 'px' : value[ name ];
                }
            }
        }else{
            if ( name in dom ) {
                dom[ name ] = value || '';
            }
            if ( value ) {
                dom.setAttribute( name, value );
            } else {
                dom.removeAttribute( name );
            }
        }
        return dom
    }
    

    dom diff

    Ract作为数据渲染DOM的框架,如果用传统的删除整个节点并新建节点的方式会很消耗性能。

    React渲染页面的方法时比较对比虚拟DOM前后的变化,再生产新的DOM。

    检查一个节点是否变化,要比较节点自身及它的父子节点,所以查找任意两棵树之间最少修改数的时间复杂度是O(n^3)。

    image

    React比较只比较当前层(同一颜色层),将比较步骤优化到了接近O(n)。

    一、创建dom

    优化JSX和虚拟DOM中, createElement 方法:

    element.js
    let utils = require('./utils')
    class Element {
        constructor(tagName, attrs, key, children) {
            this.tagName = tagName;
            this.attrs = attrs;
            this.key = key;
            this.children = children || [];
        }
    
        render() {
            let element = document.createElement(this.tagName);
            for (let attr in this.attrs) {
                utils.setAttribute(element, attr, this.attrs[attr]);
                element.setAttribute('key', this.key)
            }
            let children = this.children || [];
            //先序深度遍历
            children.forEach(child => {
                let childElement = (child instanceof Element) ? child.render() : document.createTextNode(child);
                element.appendChild(childElement);
            });
            return element;
        }
    }
    

    引申一下(先序遍历)

    class Tree {
        constructor(v, children) {
            this.v = v
            this.children = children || null
        }
    }
    
    const tree = new Tree(10, [
        new Tree(5),
        new Tree(3, [new Tree(7), new Tree(11)]),
        new Tree(2)
    ])
    
    module.exports = tree
    
    const tree = require('./1.Tree')
    function tree_transverse(tree) {
        console.log(tree.v)//10 5 3 7 11 2
        tree.children && tree.children.forEach(tree_transverse)
    }
    
    tree_transverse(tree)
    

    创建原始dom dom1,插入到页面。

    let ul1 = createElement('ul', {class: 'list'}, 'A', [
        createElement('li', {class: 'list1'}, 'B', ['1']),
        createElement('li', {class: 'list2'}, 'C', ['2']),
        createElement('li', {class: 'list3'}, 'D', ['3'])
    ]);
    let root = dom1.render();
    document.body.appendChild(root);
    
    image

    创建节点变化的DOM树 dom2,修改了dom2的父节点ul的属性class,新增并修改了子节点的位置

    let ul2 = createElement('ul', {class: 'list2'}, 'A', [
        createElement('li', {class: 'list4'}, 'E', ['6']),
        createElement('li', {class: 'list1'}, 'B', ['1']),
        createElement('li', {class: 'list3'}, 'D', ['3']),
        createElement('li', {class: 'list5'}, 'F', ['5']),
    ]);
    

    我们不能生硬得去直接销毁dom1,新建dom2。而是应该比较新旧两个dom,在原始dom上增删改。

    let patches = diff(dom1, dom2,root)

    1. 首先对两个节点进行 文本节点 比较
    function diff(oldTree, newTree, root) {
        let patches = {};
        let index = 0;
        walk(oldTree, newTree, index, patches, root);
        return patches;
    }
    function walk(oldNode, newNode, index, patches, root) {
        let currentPatch = [];
        if (utils.isString(oldNode) && utils.isString(newNode)) {
            if (oldNode != newNode) {
                currentPatch.push({type: utils.TEXT, content: newNode});
            }
        } 
    }    
    

    如果文本不同,我们 打补丁 ,记录修改的类型和文本内容

    1. 标签比较:如果标签一致,进行属性比较。不一致说明节点被替换,记录替换补丁
      else if (oldNode.tagName == newNode.tagName) {
            let attrsPatch = diffAttrs(oldNode, newNode);
            if (Object.keys(attrsPatch).length > 0) {
                currentPatch.push({type: utils.ATTRS, node: attrsPatch});
            }
        } else {
            currentPatch.push({type: utils.REPLACE, node: newNode});
        }
        ···
    
    1. 根据补丁,修改原始dom节点
    let keyIndex = 0;
    let utils = require('./utils');
    let allPatches;//这里就是完整的补丁包
    let {Element} = require('./element')
    
    function patch(root, patches) {
        allPatches = patches;
        walk(root);
    }
    
    function walk(node) {
        let currentPatches = allPatches[keyIndex++];
        (node.childNodes || []).forEach(child => walk(child));
        if (currentPatches) {
            doPatch(node, currentPatches);
        }
    }
    
    function doPatch(node, currentPatches) {
        currentPatches.forEach(patch=> {
            switch (patch.type) {
                case utils.ATTRS:
                    for (let attr in patch.node) {
                        let value = patch.node[attr];
                        if (value) {
                            utils.setAttribute(node, attr, value);
                        } else {
                            node.removeAttribute(attr);
                        }
                    }
                    break;
                case utils.TEXT:
                    node.textContent = patch.content;
                    break;
                case utils.REPLACE:
                    let newNode = (patch.node instanceof Element) ? patch.node.render() : document.createTextNode(patch.node);
                    node.parentNode.replaceChild(newNode, node);
                    break;
                case utils.REMOVE:
                    node.parentNode.removeChild(node);
                    break;
            }
        })
    }
    
    module.exports = patch
    

    进行到这里,我们已经完成了父节点的修补。

    对于ul的子节点,我们可以使用同样的方法进行迭代一次。但是我们推荐用子节点的key来更快速得去判断是否删除、新增、顺序变换。

    image

    在oldTree中,有三个子元素 B、C、D 在newTree中,有四个子元素 E、B、C、D

    1. 在oldTree中去除newTree中没有的元素
    function childDiff(oldChildren, newChildren) {
        let patches = []
        let newKeys = newChildren.map(item=>item.key)
        let oldIndex = 0;
        while (oldIndex < oldChildren.length) {
            let oldKey = oldChildren[oldIndex].key;//A
            if (!newKeys.includes(oldKey)) {
                remove(oldIndex);
                oldChildren.splice(oldIndex, 1);
            } else {
                oldIndex++;
            }
        }
    }    
    
    //标记去除的index
    function remove(index) {
        patches.push({type: utils.REMOVE, index})
    }
    
    1. 接下来将newTree数组合并到oldTree中,我的口诀是:新向旧合并,相等旧位移,记录新位标(O(∩_∩)O哈哈哈~)

    function childDiff(oldChildren, newChildren) {
    ...

    oldIndex = 0;
    newIndex = 0;
    
    while (newIndex < newChildren.length) {
        let newKey = (newChildren[newIndex] || {}).key;
        let oldKey = (oldChildren[oldIndex] || {}).key;
        if (!oldKey) {
            insert(newIndex,newChildren[newIndex]);
            newIndex++;
        } else if (oldKey != newKey) {
            let nextOldKey = (oldChildren[oldIndex + 1] || {}).key;
            if (nextOldKey == newKey) {
                remove(newIndex);
                oldChildren.splice(oldIndex, 1);
            } else {
                insert(newIndex, newChildren[newIndex]);
                newIndex++;
            }
        } else {
            oldIndex++;
            newIndex++;
        }
    }
    
    function remove(index) {
        patches.push({type: utils.REMOVE, index})
    }
    ...
    
    
    3.  删除多余节点
    

    while (oldIndex++ < oldChildren.length) {
    remove(newIndex)
    }

    4.  根据补丁修改节点
    

    function childPatch(root, patches = []) {

    let nodeMap = {};
    
    (Array.from(root.childNodes)).forEach(node => {
        nodeMap[node.getAttribute('key')] = node
    });
    
    patches.forEach(path=> {
        let oldNode
        switch (path.type) {
            case utils.INSERT:
                let newNode = nodeMap[path.node.key] || path.node.render()
                oldNode = root.childNodes[path.index]
                if (oldNode) {
                    root.insertBefore(newNode, oldNode)
                } else {
                    root.appendChild(newNode)
                }
                break;
            case utils.REMOVE:
                oldNode = root.childNodes[path.index]
                if (oldNode) {
                    root.removeChild(oldNode)
                }
                break;
            default:
                throw new Error('没有这种补丁类型')
        }
    })
    

    }

    
    记录补丁修改节点结果:
    

    相关文章

      网友评论

        本文标题:React 查漏补缺

        本文链接:https://www.haomeiwen.com/subject/tvbrqqtx.html