让虚拟DOM和DOM-diff不再成为你的绊脚石

神奇的虚拟DOM

首先神奇不神奇的我们先不去关注,先来简单说说何为虚拟DOM

虚拟DOM简而言之就是,用JS去按照DOM结构来实现的树形结构对象,你也可以叫做DOM对象

好了,一句话就把这么伟大的东西给解释了,那么不再耽误时间了,赶紧进入主环节吧

当然,这里还有整个项目的地址方便查看

实现一下虚拟DOM

在亲自上阵之前,我们让粮草先行,先发个图,来看一下整个目录结构是什么样子的

 

这个目录结构是用create-react-app脚手架直接生成的,也是为了方便编译调试

// 全局安装
npm i create-react-app -g
// 生成项目
create-react-app dom-diff
// 进入项目目录
cd dom-diff
// 编译
npm run start

现在我们开始正式写吧,从创建虚拟DOM及渲染DOM起步吧

创建虚拟DOM

在element.js文件中要实现如何创建虚拟DOM以及将创建出来的虚拟DOM渲染成真实的DOM

首先实现一下如何创建虚拟DOM,看代码:

// element.js

// 虚拟DOM元素的类,构建实例对象,用来描述DOM
class Element {
    constructor(type, props, children) {
        this.type = type;
        this.props = props;
        this.children = children;
    }
}
// 创建虚拟DOM,返回虚拟节点(object)
function createElement(type, props, children) {
    return new Element(type, props, children);
}

export {
    Element,
    createElement
}

写好了方法,我们就从index.js文件入手来看看是否成功吧

调用createElement方法

在主入口文件里,我们主要做的操作就是来创建一个DOM对象,渲染DOM以及通过diff后去打补丁更新DOM,不啰嗦了,直接看代码:

// index.js

// 首先引入对应的方法来创建虚拟DOM
import { createElement } from './element';

let virtualDom = createElement('ul', {class: 'list'}, [
    createElement('li', {class: 'item'}, ['周杰伦']),
    createElement('li', {class: 'item'}, ['林俊杰']),
    createElement('li', {class: 'item'}, ['王力宏'])
]);

console.log(virtualDom);

createElement方法也是vue和react用来创建虚拟DOM的方法,我们也叫这个名字,方便记忆。接收三个参数,分别是typepropschildren

  • 参数分析
    • type: 指定元素的标签类型,如'li', 'div', 'a'等
    • props: 表示指定元素身上的属性,如class, style, 自定义属性等
    • children: 表示指定元素是否有子节点,参数以数组的形式传入

下面来看一下打印出来的虚拟DOM,如下图

 

到目前为止,已经轻而易举的实现了创建虚拟DOM。那么,接下来进行下一步,将其渲染为真实的DOM,别犹豫,继续回到element.js文件中

渲染虚拟DOM

// element.js
class Element {
    // 省略
}
function createElement() {
    // 省略
}
// render方法可以将虚拟DOM转化成真实DOM
function render(domObj) {
    // 根据type类型来创建对应的元素
    let el = document.createElement(domObj.type);
    
    // 再去遍历props属性对象,然后给创建的元素el设置属性
    for (let key in domObj.props) {
        // 设置属性的方法
        setAttr(el, key, domObj.props[key]);
    }
    
    // 遍历子节点
    // 如果是虚拟DOM,就继续递归渲染
    // 不是就代表是文本节点,直接创建
    domObj.children.forEach(child => {
        child = (child instanceof Element) ? render(child) : document.createTextNode(child);
        // 添加到对应元素内
        el.appendChild(child);
    });

    return el;
}
// 设置属性
function setAttr(node, key, value) {
    switch(key) {
        case 'value':
            // node是一个input或者textarea就直接设置其value即可
            if (node.tagName.toLowerCase() === 'input' ||
                node.tagName.toLowerCase() === 'textarea') {
                node.value = value;
            } else {
                node.setAttribute(key, value);
            }
            break;
        case 'style':
            // 直接赋值行内样式
            node.style.cssText = value;
            break;
        default:
            node.setAttribute(key, value);
            break;
    }
}

// 将元素插入到页面内
function renderDom(el, target) {
    target.appendChild(el);
}

export {
    Element,
    createElement,
    render,
    setAttr,
    renderDom
};

既然写完了,那就赶快来看看成果吧

调用render方法

再次回到index.js文件中,修改为如下代码

// index.js

// 引入createElement、render和renderDom方法
import { createElement, render, renderDom } from './element';

let virtualDom = createElement('ul', {class: 'list'}, [
    createElement('li', {class: 'item'}, ['周杰伦']),
    createElement('li', {class: 'item'}, ['林俊杰']),
    createElement('li', {class: 'item'}, ['王力宏'])
]);

console.log(virtualDom);

// +++
let el = render(virtualDom); // 渲染虚拟DOM得到真实的DOM结构
console.log(el);
// 直接将DOM添加到页面内
renderDom(el, document.getElementById('root'));
// +++

通过调用render方法转为真实DOM,并调用renderDom方法直接将DOM添加到了页面内

下图为打印后的结果:

 

截止目前,我们已经实现了虚拟DOM并进行了渲染真实DOM到页面中。那么接下来我们就有请DOM-diff隆重登场,来看一下这大有来头的diff算法是如何发光发热的吧!

DOM-diff闪亮登场

说到DOM-diff那一定要清楚其存在的意义,给定任意两棵树,采用先序深度优先遍历的算法找到最少的转换步骤

DOM-diff比较两个虚拟DOM的区别,也就是在比较两个对象的区别。

作用: 根据两个虚拟对象创建出补丁,描述改变的内容,将这个补丁用来更新DOM

已经了解到DOM-diff是干嘛的了,那就没什么好说的了,继续往下写吧

// diff.js

function diff(oldTree, newTree) {
    // 声明变量patches用来存放补丁的对象
    let patches = {};
    // 第一次比较应该是树的第0个索引
    let index = 0;
    // 递归树 比较后的结果放到补丁里
    walk(oldTree, newTree, index, patches);
    return patches;
}

function walk(oldNode, newNode, index, patches) {
    // 每个元素都有一个补丁
    let current = [];

    if (!newNode) { // rule1
        current.push({ type: 'REMOVE', index });
    } else if (isString(oldNode) && isString(newNode)) {
        // 判断文本是否一致
        if (oldNode !== newNode) {
            current.push({ type: 'TEXT', text: newNode });
        }

    } else if (oldNode.type === newNode.type) {
        // 比较属性是否有更改
        let attr = diffAttr(oldNode.props, newNode.props);
        if (Object.keys(attr).length > 0) {
            current.push({ type: 'ATTR', attr });
        }
        // 如果有子节点,遍历子节点
        diffChildren(oldNode.children, newNode.children, patches);
    } else {    // 说明节点被替换了
        current.push({ type: 'REPLACE', newNode});
    }
    
    // 当前元素确实有补丁存在
    if (current.length) {
        // 将元素和补丁对应起来,放到大补丁包中
        patches[index] = current;
    }
}

function isString(obj) {
    return typeof obj === 'string';
}

function diffAttr(oldAttrs, newAttrs) {
    let patch = {};
    // 判断老的属性中和新的属性的关系
    for (let key in oldAttrs) {
        if (oldAttrs[key] !== newAttrs[key]) {
            patch[key] = newAttrs[key]; // 有可能还是undefined
        }
    }

    for (let key in newAttrs) {
        // 老节点没有新节点的属性
        if (!oldAttrs.hasOwnProperty(key)) {
            patch[key] = newAttrs[key];
        }
    }
    return patch;
}

// 所有都基于一个序号来实现
let num = 0;

function diffChildren(oldChildren, newChildren, patches) {
    // 比较老的第一个和新的第一个
    oldChildren.forEach((child, index) => {
        walk(child, newChildren[index], ++num, patches);
    });
}

// 默认导出
export default diff;

代码虽然又臭又长,但是这些代码就让我们实现了diff算法了,所以大家先不要盲动,不要盲动,且听风吟,让我一一道来

比较规则

  • 新的DOM节点不存在{type: 'REMOVE', index}
  • 文本的变化{type: 'TEXT', text: 1}
  • 当节点类型相同时,去看一下属性是否相同,产生一个属性的补丁包{type: 'ATTR', attr: {class: 'list-group'}}
  • 节点类型不相同,直接采用替换模式{type: 'REPLACE', newNode}

四句话

我们来梳理一下整个DOM-diff的过程:

  1. 用JS对象模拟DOM(虚拟DOM)
  2. 把此虚拟DOM转成真实DOM并插入页面中(render)
  3. 如果有事件发生修改了虚拟DOM,比较两棵虚拟DOM树的差异,得到差异对象(diff)
  4. 把差异对象应用到真正的DOM树上(patch)

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值