30天前端学习计划第一天---vnode与diff算法学习笔记(一)

概述:根据vnode生成真实节点el,给真实节点添加属性,遍历children生成子节点,将子节点插入到父节点中,将el插入到html页面中,比较新旧vnode和新旧子vnode的差异,得到补丁(差异对象),遍历每个补丁,根据补丁,给对应旧节点进行更新。

一。VNode部分

第一步,通过createElement方法生成vnode(虚拟节点)

  • ul为元素的名称
  • class为元素的class属性
  • []为子元素列表
let vnode = createElement('ul', {
            class: 'list'
        }, [
            createElement('li', {
                class: 'item'
            }, ['a']),
            createElement('li', {
                class: 'item'
            }, ['a']),
            createElement('li', {
                class: 'item'
            }, ['a'])
        ]);
复制代码

第二步,通过定义createElement方法,返回一个VNode的实例化对象

  • type为元素的名称
  • props为元素的属性
  • children为子元素列表
function createElement(type, props, children) {
    return new VNode(type, props, children);
}
复制代码

第三步,定义VNode类

class VNode {
    constructor(type, props, children) {
        this.type = type;
        this.props = props;
        this.children = children;
    }
}
复制代码

第四步,打印vnode,即可得到一个vnode对象

    console.log(vnode);
打印结果:
    Element {type: "ul", props: {…}, children: Array(3)}
    children: (3) [Element, Element, Element]
    props: {class: "list"}
    type: "ul"
复制代码

第五步,定义render函数,生成真实节点,调用setAttri方法为真实节点添加属性:

function render(vnode) {
    //生成节点
    let el = document.createElement(vnode.type);
    for (let key in vnode.props) {
        setAttri(el, key, vnode.props[key]);
    }
    return el;
}
复制代码

第六步,定义setAttri方法,遍历属性对象,为节点添加属性:

function setAttri(node, key, value) {
    switch (key) {
        //如果属性为value
        case 'value':
            //判断node是否是一个input或者textarea元素
            if (node.targetName.toUpperCase() === 'INPUT' || node.targetName.toUpperCase() === 'TEXTAREA') {
                node.value = value;
            } else {
                node.setAttribute(key, value);
            }
            break;
        //如果属性为style
        case 'style':
            node.style.cssText = value;
            break;
        //其他普通属性
        default:
            node.setAttribute(key, value);
            break;
    }
}
复制代码

第七步,此时打印el:

let el = render(vnode);
console.log(el);
打印可得:
        <ul class="list" style="width: 100%;"></ul>
复制代码

第八步,在render函数中,遍历children,生成子节点:

vnode.children.forEach(child => {
        //判断child是否是vnode,是则调用render函数生成vnode,不是则生成文本节点
        child = (child instanceof VNode) ? render(child) : document.createTextNode(child);
        el.appendChild(child);
    });
复制代码
打印el可得:
<ul class="list" style="width: 100%;">
    <li class="item">a</li>
    <li class="item">a</li>
    <li class="item">a</li>
</ul>
复制代码

第九步,定义mount方法,将el插入到html页面中:

    let el = render(vnode);
    mount(el,document.querySelector("#app"));
复制代码
mount方法的定义
//将通过vnode生成的el节点插入到页面中,target为页面中的元素
function mount(el, target){
    target.appendChild(el);
}
复制代码

二。diff算法部分一

第一步,定义两个vnode,newVnode和oldVnode

let oldVnode = createElement('ul', {
            class: 'list',
            style: 'width:100%'
        }, [
            createElement('li', {
                class: 'item'
            }, ['a']),
            createElement('li', {
                class: 'item'
            }, ['b']),
            createElement('li', {
                class: 'item'
            }, ['c'])
        ]);
        
let newVnode = createElement('ul', {
            class: 'list-group'
        }, [
            createElement('li', {
                class: 'item'
            }, ['A']),
            createElement('li', {
                class: 'item'
            }, ['B']),
            createElement('li', {
                class: 'item'
            }, ['C'])
        ]);
复制代码

第二步,调用diff方法比较新旧vnode的差异,得到差异对象:

let patchs = diff(oldVnode, newVnode);

第三步,定义diff方法,在diff方法中定义补丁对象存储补丁,调用walk方法进行递归比较新老vnode,将结果放到补丁包中:

  • 比较规则:
  • 节点类型相同,看属性是否相同,产生一个属性的补丁包:{type: 'ATTRS', attrs: {class: 'list-group'}}
  • 节点被删了,产生一个节点被删除的补丁包:{type: 'REMOVE', index: xxx}
  • 节点类型不同,产生一个被替换的补丁包:{type: 'REPLACE', newNode: newNode}
  • 文本的变化,产生一个文本变化的补丁包:{type: 'TEXT', text:1}
function diff(oldVnode, newVnode) {
    //补丁对象
    let patches = {};
    //比较的第一个元素的下标
    let index = 0;
    //定义walk方法进行递归比较新老vnode,将结果放到补丁包中
    walk(oldVnode, newVnode, index, patches);
    return patches;
}
复制代码

第四步,定义walk方法,比较新旧节点,获取补丁:

//节点变化的四种标识
const ATTRS = 'ATTRS';
const TEXT = 'TEXT';
const REMOVE = 'REMOVE';
const REPLACE = 'REPLACE';
//节点序号
let Index = 0;
//判断节点是否是字符串(文本节点)
function isString(node) {
    return Object.prototype.toString.call(node) === '[object String]';
}

function walk(oldVnode, newVnode, index, patches) {
    let currenPatch = [];// 每个元素都有一个补丁对象
   //情况一:节点被删了 
    if(!newVnode){ 
        currenPatch.push({type: REMOVE, index});
    }
    //情况二:判断是否是文本节点,且文本节点是否相同
    else if(isString(oldVnode) && isString(newVnode)) {
        if(oldVnode !== newVnode){
            currenPatch.push({type: TEXT, text: newVnode});
        }
    }
    //情况三:判断节点类型是否相同,相同则调用diffAttr方法比较属性是否相同,返回一个attrs属性差异对象
    else if (oldVnode.type === newVnode.type) {
        //比较属性是否有更改
        let attrs = diffAttr(oldVnode.props, newVnode.props);
        //判断是否有属性更改
        if(Object.keys(attrs).length > 0){
            currenPatch.push({type: ATTRS, attrs});
        }
        //遍历子vnode
        diffChildren(oldVnode.children, newVnode.children, patches);
    }
    //情况四:说明节点被替换了
    else {
        currenPatch.push({type: REPLACE, newVnode});
    }
    //得到第一个newVnode和第一个oldVnode的差异对象,存入到patches中
    if(currenPatch.length > 0){
        //将元素和补丁建立对应关系 
        patches[index] = currenPatch;
    }
}
复制代码

第五步,定义diffAttr方法,比较新旧vnode的属性的差异,返回属性差异对象:

//比较新旧vnode的属性的差异,返回属性差异对象

function diffAttr(oldAttrs, newAttrs) {
    let patch = {};
    //遍历旧节点属性,比较和新节点的属性的区别
    for (let key in oldAttrs) {
        if(oldAttrs[key] !== newAttrs[key]){
            patch[key] = newAttrs[key];//有可能是undefined,因为新节点中没有这个属性了
        }
    }
    //当旧vnode没有新vnode中的新属性的情况
    for (let key in newAttrs) {
        if(!oldAttrs.hasOwnProperty(key)){
            patch[key] = newAttrs[key];//有可能是undefined,因为新节点中没有这个属性了
        }
    }
    return patch;
}
复制代码

第八步,定义diffChildren,比较子vnode的区别

//遍历新老子vnode,比较子vnode的区别
function diffChildren(oldChildren, newChildren, patches){
    // 比较旧vnode的第一个子vnode和新的第一个
    oldChildren.forEach((child, _index) => {
        //下标累加
        walk(child, newChildren[_index], ++Index, patches);
    })
}
复制代码

第九步,给el打补丁

    //给元素打补丁,重新更新视图
    patch(el, patches);
复制代码

第十步,定义patch方法,调用work给节点打补丁

//补丁对象
let allPatches;
//补丁初始化下标
let index = 0;
function patch(node, patches){
    allPatches = patches;
    //给节点打补丁
    work(node);
}
复制代码

第十一步,定义work方法,获取当前补丁,调用doPatch方法给当前元素打补丁,递归子节点,给子节点打补丁

//获取当前补丁,调用doPatch方法给当前元素打补丁,递归子节点,给子节点打补丁
function work(node){
    //当前补丁
    let currenPatch = allPatches[index++];
    //子节点
    let childNodes = node.childNodes; 
    //遍历子节点
    childNodes.forEach(child => {
        //给子节点打补丁
        work(child);
    })
    //当前补丁存在,则根据补丁更新当前节点
    if(currenPatch){
        doPatch(node, currenPatch);
    }
}
复制代码

第十二步,定义doPatch方法给元素打补丁:

//遍历当前补丁数组,判断补丁类型,根据不同的补丁类型给节点打补丁
function doPatch(node, patches){
    patches.forEach(patch=>{
        switch (patch.type) {
            //属性更改类型的补丁
            case 'ATTRS':
                for(let key in patch.attrs){
                    let value = patch.attrs[key];
                    if(value) {
                        //添加属性
                        setAttri(node, key, value);
                    }
                    else {
                        //删除属性
                        node.removeAttribute(key);
                    }
                }
                break;
            //文本类型的补丁,替换文本内容
            case 'TEXT':
                node.textContent = patch.text;
                break;
            //节点替换类型
            case 'REPLACE':
                let newNode = (patch.newVnode instanceof VNode) ? render(patch.newVnode) : document.createTextNode(patch.newVnode);
                node.parentNode.replaceChild(newNode, node);
                break;
            //节点被删除了
            case 'REMOVE':
                node.parentNode.removeChild(node);
                break;
        }
    })
}
复制代码

第十三步,测试结果:小写abc被大写ABC替换。

三。diff算法部分二(平级元素互换位置与新增节点更新):

待续。。。。

学习来源:

  • https://www.bilibili.com/video/av31174086/

转载于:https://juejin.im/post/5c8612456fb9a049fa109727

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值