Vue diff 算法与虚拟dom

为什么需要虚拟Dom

在不使用虚拟dom的情况下,修改一个节点会引起整个页面的重绘。比如又一个元素进行了修改(删除),剩余的9个元素都需要加载重绘。虚拟Dom就是有一个虚拟的节点树和原有节点数进行比对,做到了更小量更新元素更新。
main.js

import { createElement, render,renderDom } from './element'
import diff from './diff'
import patch from './patch';
// 旧节点
let myDom = createElement('div',{class:'container',style:'color:red'},
[
    createElement('p',{class:'item'},'child1'),
    createElement('p',{class:'item'},'child2'),
    createElement('p',{class:'item'},'child3'),
    createElement('input',{class:'item',value:'I am input'})
])
// 新节点
let myDom2 = createElement('div',{class:'container'},
[
    createElement('p',{class:'item'},'child1'),
    createElement('p',{class:'item'},'child2'),
    createElement('p',{class:'item'},'child3'),
    createElement('p',{class:'item'},'child4')
])
// 比较新旧节点状态,返回补丁
let patches = diff(myDom,myDom2)
// 渲染旧的节点
let el = render(myDom)
patch(el,patches)
renderDom(document.getElementById('app'),el)

diff.js

 let _index = 0
 function diff(oldNode,newNode){
    // diff 算法的本质就是打补丁,如果有需要更改的元素就设置相应的补丁
    // 同级、同位置进行比较,不会出现越级比较
    // 存储当前修改的元素地址和修改类型
    let patches ={}
    // index 下标,用来记录修改元素的下标
    let index = 0

    setPatch(oldNode,newNode,index,patches)
    return patches
}
// 设置类型,补丁类型
const ATTRS='ATTRS' // 修改或者删除该元素属性
const TEXT='TEXT'    // 节点文本修改或者删除
const REMOV='REMOV'   // 删除节点
const REPLACE='REPLACE'  //替换节点
function setPatch(oldNode,newNode,index,patches){
    // 存储需要打补丁的类型
    let currentPatch = []
    if(!newNode){
        // 如果没有新节点,就说明此时的旧节点是删除状态
        currentPatch.push({type:REMOV,_index})
        if(currentPatch.length>0){
            patches[_index]= currentPatch
        }
        
    }else if (isString(oldNode)&& isString(newNode)){
       
        // 判断是否一致
        if (oldNode !== newNode){
            currentPatch.push({type:TEXT,text:newNode})
        }
        if(currentPatch.length>0){
            patches[_index]= currentPatch
        }
    }
    else if(newNode.type === oldNode.type){
        // 如果节点类型都相同,那就比较节点属性是否相同
        let attrs  = diffAttr(oldNode.props,newNode.props)
        // attrs 返回内容,说明需要补丁
        if(Object.keys(attrs).length>0){
            currentPatch.push({type:ATTRS,attrs,index})
        }
        if (currentPatch.length > 0) {
            patches[_index] = currentPatch
        }
        // 递归子节点
        diffChild(oldNode.children,newNode.children,index,patches)
    }else{
        // 节点替换
        currentPatch.push({ type: REPLACE, newNode })
        if (currentPatch.length > 0) {
            patches[_index] = currentPatch
        }
    }

}
function isString(node){
    return Object.prototype.toString.call(node) ==="[object String]"
}
function diffChild(oldChild,newChild,index,patches){
    // 该函数的功能是递归每一个子节点,然后进行diff补丁
    if(Array.isArray(oldChild)){
        oldChild.forEach((child,idx) => {
            setPatch(child,newChild[idx],++_index,patches)
        });
    }else{
        setPatch(oldChild,newChild,++_index,patches)
    }
   
}
function diffAttr(oldAttrs,newAttrs){
    // 函数的功能是判断当前的新旧节点的属性是否相同,如果不同,新的节点属性覆盖旧的节点属性
    // 维护的属性的补丁
    let patch = {}
    //判断旧的节点是否和新的相同,如果不同新的替换旧的
    for(let key in oldAttrs){
        if(oldAttrs[key] !== newAttrs[key]){
            patch[key] = newAttrs[key]
        }
    }
    for(let key in newAttrs){
        // 新的节点属性如果在旧的节点属性中不存在就补丁更新
        if(!oldAttrs.hasOwnProperty(key)){
            patch[key] = newAttrs[key]
        }
    }
    return patch

}
export default diff;

patch.js

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

let allPatches;
let index = 0;
function patch(node, patches) {
    allPatches = patches
    setDom(node)

}
function setDom(node) {
    // 修改补丁及节点
    let currentPatch = allPatches[index++]
    let childNodes = node.childNodes
    // 递归补丁
    childNodes.forEach(item => setDom(item));
    if (currentPatch) {
        // 如果有补丁,打补丁
        doPatch(node, currentPatch)

    }
}
function doPatch(node, patches) {
    // 打补丁的过程
    patches.forEach(patch => {
        console.log('~~~~~patch', patch)
        switch (patch.type) {
            case 'ATTRS':
                for (const key in patch.attrs) {
                    let value = patch.attrs[key]
                    // 有值添加该属性
                    if (value) {
                        node.setAttribute(key, value)
                    } else {
                        node.removeAttribute(key)
                    }
                }
                break;
            case 'TEXT':
                node.textContent = patch.text
                break;
            case 'REMOV':
                console.log('REMOV',node.parentNode,node)
                node.parentNode.removeChild(node)
                break;
            case 'REPLACE':
                // 替换节点,如果是文本节点,就追加文本节点,否则就替换元素节点
                let newNode = patch.newNode instanceof  Element ? render(patch.newNode):document.createTextNode(patch.newNode)
               node.parentNode.replaceChild(newNode,node)
                break;
        }
    })

}
export default patch

element.js

function Element(type, props, children) {
    this.type = type
    this.props = props
    this.children = children

}
function createElement(type, props, children) {
    return new Element(type, props, children)
}
function render(obj) {
    // 创建节点
    let el = document.createElement(obj.type)
    for (let key in obj.props) {
        el.setAttribute(key, obj.props[key])
    }

    if (Array.isArray(obj.children)) {
        obj.children.forEach(element => {
            // 递归操作,如果当前child不是文本,就继续进行操作,否则创建文本节点
            element = element instanceof Element ? render(element) : document.createTextNode()
            el.appendChild(element)
        });
    }
    else if (typeof obj.children === 'string') { // 如果是字符串当成数组进行递归
        el.appendChild(document.createTextNode(obj.children))
    }

    return el
}
function renderDom(node, target) {
    node.appendChild(target)
}
export { createElement, render, renderDom, Element }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,980评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,422评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,130评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,553评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,408评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,326评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,720评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,373评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,678评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,722评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,486评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,335评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,738评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,283评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,692评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,893评论 2 335

推荐阅读更多精彩内容