React 查漏补缺

栏目: 服务器 · 发布时间: 6年前

内容简介:在工作做我们一直使用react及react相关框架(antd/antd-mobile)但是对于react的深层的了解不够:JSX语法与虚拟DOM的关系?高阶组件是如何实现的?dom diff的原理?通过写一篇react小册来查缺补漏。

在工作做我们一直使用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语法树结构的嵌套对象(只是个人认为...),如图下:

React 查漏补缺

简化版:保留了返回对象中最关键的属性(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)。

React 查漏补缺

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);
复制代码
React 查漏补缺

创建节点变化的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
复制代码

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

React 查漏补缺

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

React 查漏补缺

在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})
    }
    ...
复制代码
  1. 删除多余节点
while (oldIndex++ < oldChildren.length) {
        remove(newIndex)
    }
复制代码
  1. 根据补丁修改节点
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 查漏补缺

详细的dom-diff地址


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Head First jQuery

Head First jQuery

Ryan Benedetti , Ronan Cranley / O'Reilly Media / 2011-9 / USD 39.99

Want to add more interactivity and polish to your websites? Discover how jQuery can help you build complex scripting functionality in just a few lines of code. With Head First jQuery, you'll quickly g......一起来看看 《Head First jQuery》 这本书的介绍吧!

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具