Virtual DOM + Diff


虚拟 DOM 通过以下步骤实现:

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

1 为什么Vue和React都选择Virtual DOM

1.1 减少直接操作DOM。

框架给我们提供了屏蔽底层dom书写的方式,减少频繁的整更新dom,同时也使得数据驱动视图

当有一个表格需要做排序功能时,有出生年月、性别等排序方式可选,当选择某排序方式时,表格将按该方式重新排序。

真实 DOM:排序操作需要将表格内的所有 DOM 树删除后新建;
虚拟 DOM:使用 diff 算法得到需要修改的部分,仅更新需要发生修改的 DOM 节点;

从上可知,虚拟 DOM 通过 diff 算法,帮助我们大量的减少 DOM 操作。

1.2 函数式的 UI 编程方式(React核心思想)

从另一个角度看,虚拟 DOM 为我们提供了函数式的编程方式,使代码可读性和可维护性更高。符合React的核心思想。

const App = () => {
  return (
      <div>

      </div>
  )
}

1.3 支持跨平台

通过虚拟DOM,渲染到DOM(web)之外的平台。比如ReactNative,Weex

2 Diff算法

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

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

作用:在patchvnode过程中,找到与新vnode对应的老vnode,复用真实的DOM节点,避免不必要的性能开销。

// 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;

2.1 比较规则

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

2.2 walk方法

每个元素都有一个补丁,所以需要创建一个放当前补丁的数组

  • 如果没有new节点的话,就直接将type为REMOVE的类型放到当前补丁里
     if (!newNode) {
            current.push({ type: 'REMOVE', index });
        }
    
  • 如果新老节点是文本的话,判断一下文本是否一致,再指定类型TEXT并把新节点放到当前补丁
        else if (isString(oldNode) && isString(newNode)) {
            if (oldNode !== newNode) {
                current.push({ type: 'TEXT', text: newNode });
            }
        }
    
  • 如果新老节点的类型相同,那么就来比较一下他们的属性props,然后如果有子节点的话就再比较一下子节点的不同,再调一次walk
        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);
        }
    
  • 上面三个如果都没有发生的话,那就表示节点单纯的被替换了,type为REPLACE,直接用newNode替换即可
        else {
            current.push({ type: 'REPLACE', newNode});
        }
    
  • 当前补丁里确实有值的情况,就将对应的补丁放进大补丁包里
        if (current.length > 0) {
            // 将元素和补丁对应起来,放到大补丁包中
            patches[index] = current;
        }
    

2.3 patch补丁更新

打补丁需要传入两个参数,一个是要打补丁的元素,另一个就是所要打的补丁

import { Element, render, setAttr } from './element';

let allPatches;
let index = 0;  // 默认哪个需要打补丁

function patch(node, patches) {
    allPatches = patches;
    
    // 给某个元素打补丁
    walk(node);
}

function walk(node) {
    let current = allPatches[index++];
    let childNodes = node.childNodes;

    // 先序深度,继续遍历递归子节点
    childNodes.forEach(child => walk(child));

    if (current) {
        doPatch(node, current); // 打上补丁
    }
}

function doPatch(node, patches) {
    // 遍历所有打过的补丁
    patches.forEach(patch => {
        switch (patch.type) {
            case 'ATTR':
                for (let key in patch.attr) {
                    let value = patch.attr[key];
                    if (value) {
                        setAttr(node, key, value);
                    } else {
                        node.removeAttribute(key);
                    }
                }
                break;
            case 'TEXT':
                node.textContent = patch.text;
                break;
            case 'REPLACE':
                let newNode = patch.newNode;
                newNode = (newNode instanceof Element) ? render(newNode) : document.createTextNode(newNode);
                node.parentNode.replaceChild(newNode, node);
                break;
            case 'REMOVE':
                node.parentNode.removeChild(node);
                break;
            default:
                break;
        }
    });
}

export default patch;

2.3.1 patch做了什么?

  • 用一个变量来得到传递过来的所有补丁allPatches
  • patch方法接收两个参数(node, patches)
    • 在方法内部调用walk方法,给某个元素打上补丁
  • walk方法里获取所有的子节点
    • 给子节点也进行先序深度优先遍历,递归walk
    • 如果当前的补丁是存在的,那么就对其打补丁(doPatch)
  • doPatch打补丁方法会根据传递的patches进行遍历
    • 判断补丁的类型来进行不同的操作
    • 将patch方法默认导出方便调用

2.4 回归

// index.js

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

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

let el = render(virtualDom);
renderDom(el, window.root);

// +++
// 创建另一个新的虚拟DOM
let virtualDom2 = createElement('ul', {class: 'list-group'}, [
    createElement('li', {class: 'item active'}, ['七里香']),
    createElement('li', {class: 'item'}, ['一千年以后']),
    createElement('li', {class: 'item'}, ['需要人陪'])    
]);
// diff一下两个不同的虚拟DOM
let patches = diff(virtualDom, virtualDom2);
console.log(patches);
// 将变化打补丁,更新到el
patch(el, patches);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值