2020-06-09虚拟(Dom)+Diff算发

虚拟Dom

创建虚拟DOM(创建文件element.js>如何创建Dom以及虚拟Dom渲染真实的Dom)

            // element.js
            // 虚拟DOM元素的类,构建实例对象,用来描述DOM
            class Element {
                constructor(type, props, children) {
                    this.type = type;
                    this.props = props;
                    this.children = children;
                }
            }
参数分析:
type: 指定元素的标签类型,如'li', 'div', 'a'等
props: 表示指定元素身上的属性,如class, style, 自定义属性等
children: 表示指定元素是否有子节点,参数以数组的形式传入
            // 创建虚拟DOM,返回虚拟节点(object)
            function createElement(type, props, children) {
                return new Element(type, props, children);
            }
            export {
                Element,
                createElement
            }

主文件(index.js)调用element.js里的方法

1:调用创建虚拟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);

2:虚拟的Dom 已创建 接下啦 就是转化成真是的Dom (接下来继续操作element.js文件)

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

Diff算法(意义:给定两颗大树,采用先序深度优先遍历)的算法找到最少的转化步骤

作用: 根据两个虚拟对象创建出补丁,描述改变的内容,将这个补丁用来更新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;

比较规则

新的DOM节点不存在{type: 'REMOVE', index}

文本的变化{type: 'TEXT', text: 1}

当节点类型相同时,去看一下属性是否相同,产生一个属性的补丁包{type: 'ATTR', attr: {class: 'list-group'}}

节点类型不相同,直接采用替换模式{type: 'REPLACE', newNode}

根据这些规则,我们再来看一下diff代码中的walk方法这位关键先生(讲解上文的js代码)

walk方法都做了什么?

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

:2:如果没有new节点的话,就直接将type为REMOVE的类型放到当前补丁里

    if (!newNode) {
      current.push({ type: 'REMOVE', index });
    }

3: 如果新老节点的类型相同,那么就来比较一下他们的属性props

    遍历oldChildren,然后递归调用walk再通过child和newChildren[index]去diff
    
    去比较新老Attr是否相同
    
    把newAttr的键值对赋给patch对象上并返回此对象
    
    属性比较 diffAttr
    
    然后如果有子节点的话就再比较一下子节点的不同,再调一次walk diffChildren

    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);
        }

4:上面三个如果都没有发生的话,那就表示节点单纯的被替换了,type为REPLACE,直接用newNode替换即可

     else {
        current.push({ type: 'REPLACE', newNode});
    }

5:当前补丁里确实有值的情况,就将对应的补丁放进大补丁包里

  if (current.length > 0) {
        // 将元素和补丁对应起来,放到大补丁包中
        patches[index] = current;
    }

diff已经完事了,那么最后一步就是大家所熟知的打补丁了,补丁要怎么打?那么让久违的patch出来吧

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;

patch做了什么?

          patch做了什么?
    
    用一个变量来得到传递过来的所有补丁allPatches
    
    patch方法接收两个参数(node, patches)
    
    在方法内部调用walk方法,给某个元素打上补丁
    
    walk方法里获取所有的子节点
    
    给子节点也进行先序深度优先遍历,递归walk
    
    如果当前的补丁是存在的,那么就对其打补丁(doPatch)
    
    doPatch打补丁方法会根据传递的patches进行遍历
    
    判断补丁的类型来进行不同的操作???????????:
    
     属性ATTR for in去遍历attrs对象,当前的key值如果存在,就直接设置属性setAttr;如果不存在对应的key值那就直接删除这个key键的属性2. 文字TEXT 直接将补丁的text赋值给node节点的textContent即可3. 替换REPLACE 新节点替换老节点,需要先判断新节点是不是Element的实例,是的话调用render方法渲染新节点;不是的话就表明新节点是个文本节点,直接创建一个文本节点就OK了。之后再通过调用父级parentNode的replaceChild方法替换为新的节点4. 删除REMOVE 直接调用父级的removeChild方法删除该节点
    
    将patch方法默认导出方便调用好了,一切都安静下来了。
    让我们回归index.js文件中,去调用一下diff和patch这两个重要方法,看看奇迹会不会发生吧
  判断补丁的类型来进行不同的操作:
    
    属性ATTR for in去遍历attrs对象,当前的key值如果存在,就直接设置属性setAttr;如果不存在对应的key值那就直接删除这个key键的属性2. 文字TEXT 直接将补丁的text赋值给node节点的textContent即可3. 替换REPLACE 新节点替换老节点,需要先判断新节点是不是Element的实例,是的话调用render方法渲染新节点;不是的话就表明新节点是个文本节点,直接创建一个文本节点就OK了。之后再通过调用父级parentNode的replaceChild方法替换为新的节点4. 删除REMOVE 直接调用父级的removeChild方法删除该节点
    
    将patch方法默认导出方便调用
    
    好了,一切都安静下来了。让我们回归index.js文件中,去调用一下diff和patch这两个重要方法,看看奇迹会不会发生吧
    
    回归
    
    // 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);
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343