Vue--手写虚拟 DOM

1,594 阅读9分钟
  • 虚拟DOM是什么?
  • 如何创建虚拟DOM?
  • 虚拟DOM如何渲染成真实DOM?
  • 虚拟DOM如何patch?
  • 虚拟DOM的优势?
  • Vue中的key到底有什么用?
  • Vue中的diff算法实现?

虚拟 DOM

virtual dom,它通过js对象模拟DOM中的节点,然后再通过特定的render方法将其渲染成真实的DOM节点。当我们更改元素的节点、属性,并不是真正的改变DOM,做dom diff算法来进行对比

在比对的过程中,以最小的代价来更新DOM,可以进行复用,性能更好

vue提供h方法(createElement),根据DOM的属性、类型、孩子产生一个虚拟DOM(入参)

虚拟节点是一个对象,将类型和key单独提出来,形成虚拟节点(区分元素和文本)

通过render方法把虚拟节点变为真实节点(区分文本和元素)

比较老属性和新属性的差异,算出最新的,赋给真实DOM,子节点递归.

渲染真实DOM

1.构建项目

npm init -y
cnpm i webpack webpack-cli webpack-dev-server html-webpack-plugin --save

创建文件 webpack.config.js 创建public/Index.html

//出口文件需要拿到path
const path = require('path')
const  HtmlwebpackPlugin   = require('html-webpack-plugin')

module.exports = {
    entry:'./src.index.js',
    output:{
        filename:'bundle.js',
        path:path.resolve(__dirname,'dist')
    },
    //配置调试,可以快速找到源码
    devtool:'source-map',
    //更改查找模块的方式
    resolve:{
        modules:[path.resolve(__dirname,'source'),path.resolve('node_modules')]
    },
    plugins:[
        //热更新
        new HtmlwebpackPlugin({
            template:path.resolve(__dirname,'public/index.html')
        })
    ]

}

配置启动项

"scripts": {
    "start""webpack-dev-server",
    "build":"webpack"}

2.正式 真实DOM

div中包含一个文本节点,和一个元素节点

<div id = "wrapper" a = 1 >
    <span style="color:red">hello</span>
    zf
    </div>
vue中 h方法渲染的是虚拟DOM,是一个对象,元素调用h方法
// 标签  属性 子节点
h('div',{id:'wrapper',a:1},
    h('span',{style:{color:'red'}},'hello'),
    'zf')

生成的对象

//type 标签 props 属性 children 子节点 text  文本    
type:'div',
    props:{id:"wrapper",a:1},
    children:[
        {
            type:'span',
            props:{color:'red'},
            children:[{}],
        },
        {
            type:'',
            props:'',
            text:'zf',
            children:[{}],
        },
       
    ]
}
  1. h

h方法就是根据DOM的属性,类型,孩子 产生一个虚拟DOM

首先写一个初始的版本,此时儿子里没有进行循环处理

//h.js
/**
 * 
 * @param {*} type 类型
 * @param {*} pops 节点属性
 * @param  {...any} children 所有孩子 
 */
//区分元素中特殊的属性 key,根据key进行比对操作,默认不会传给当前元素的属性,key不属于props中的一个属性
export default function createElement(type, props, ...children) {
    console.log('h方法');
    let key;
    if (props.key) {
        key = props.key;
        delete props.key;
    }
    //将不是虚拟节点的子节点,变成虚拟节点
    children = children.map(child =>{
        if(typeof child === 'string'){
            return vnode(undefined,undefined,undefined,undefined,child)

        }else{
            return child;
        }
    })
    return vnode(type,key,props,children)
}
//虚拟节点  工厂方法
function vnode(type,key,props,children,text){
    return {
        type,
        props,
        key,
        children,
        text
    }

}

相比于直接操作DOM(一个节点包含很多属性) 采用一个对象,来描述这个节点,

let app = document.getElementById('app');

for(let key in app){
    console.log(key);

}

图片
图片

  1. 渲染真实DOM

vue中的render方法将虚拟节点,渲染成为真实DOM

export function render(vnode,container){
    console.log(vnode,container)
    //从虚拟转真实
    //创建标签
    let ele = createDomElementFromVnode(vnode);
    container.appendChild(ele)
  
}
//通过虚拟的DOM,创建真实DOM元素

function createDomElementFromVnode(vnode){
    let { type,key,props,children,text} = vnode;
    //根据类型创建元素,有类型,是标签,无类型,是文本
    if(type){
        //建立虚拟节点与真实元素的关系,后面可以用来更新真实DOM
      vnode.domElement = document.createElement(type)
      updateProperties(vnode);

    }else{
        vnode.domElement = document.createTextNode(text)
   

    }
    //返回真实DOM
    return vnode.domElement;

}

添加属性,也可以通过setAttribute

//传入当前最新的虚拟节点,先取出DOM,再更新属性
function   updateProperties(newVode,oldProps = {}){
    //真实的dom
    let domElement = newVode.domElement;
    //当前虚拟节点中的属性
    let newProps = newVode.props

    //如果老的里面有,新的没有,说明属性被移除
    //把老的属性里的key拿出来
    for(let oldPropName in oldProps){
        if(!newProps[oldPropName]){
            delete domElement[oldPropName]
        }
    }
    //老的里面没有,新的里面有,添加属性
    for(let newPropName in newProps){
        //用新节点的属性,直接覆盖掉老节点的属性,即可
             domElement[newPropName] = newProps[newPropName]
    }

}

此时可以看到div上已经有了wrapper的id属性,但是没有看到a 实际上有a,但因为a并不是属性

console.log(domElement.a)

属性的值为对象 color:{red:color}

    //老的里面没有,新的里面有,添加属性
    for(let newPropName in newProps){
        //用新节点的属性,直接覆盖掉老节点的属性,即可

        //区别判断属性值是对象的情况 例如: @click addEventListener
        if(newPropName === 'style'){
            //循环取出属性值 中的 键值
            let styleObj = newProps.style;
            for(let s in styleObj){
                domElement.style[s] = styleObj[s]
            }

        }else{
            domElement[newPropName] = newProps[newPropName]
        }
   

}

新的里面有style,老的里面也有style,对style的新旧做判断

    let newStyleObj = newProps.style || {};
    let oldStyleObj = oldProps.style || {};
    for(let propName in oldStyleObj){
        if( !newStyleObj[propName]){
            //老dom元素上更新之后,没有了某个样式,要删除掉
            domElement.style[propName] = ''
        }
    }

渲染儿子

//递归渲染子的虚拟节点  
children.forEach(childVnode => render(childVnode,vnode.domElement))

DOM diff

比较两个虚拟DOM,两个对象的区别,创建出补丁,描述改变的内容,将这个补丁用来更新dom DOM diff是通过JS层面的计算,返回一个patch对象,即补丁,再通过特定的操作解析patch对象,完成页面的重新渲染。

DOM diff三种优化策略

  1. 同层
  2. 同层可以复用 key

差异计算

先序深度优先遍历

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

规则:

  1. 当节点类型相同时,看属性是否相同,产生一个属性的补丁包

{type:"ATTRS",attrs:{class:"list-group"}}

  1. 新的DOM节点不存在

{type:'REMOVE',index:xxx}

  1. 节点类型不相同,直接采用替换模式

{type:'REPLACE',newNode:newNode}

  1. 文本的变化

{type:"TEXT",text1}

比较节点

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

    if(oldNode.type === newNode.type){
        //相同节点,比较属性,返回变化对象
        let attrs = diffAttr(oldNode.props,newNode.props)
        //比较之后,放入大补丁包
        console.log(attrs)
        //判断attrs中有没有变化补丁
        if(Object.keys(attrs).length > 0){
            currentPatch.push({type:ATTRS,attrs})
        }
    }

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

}

比较属性

function diffAttr(oldAttrs,newAttrs){
    let patch = {};
    //判断老的属性和新的属性的关系
    for(let key in oldAttrs){
        //比较新的和老的不一样
        if(oldAttrs[key] !== newAttrs[key]){
            patch[key] = newAttrs[key];//有可能新的没有这个属性,undefined
        }
    }
    //新增(老节点没有的属性,新节点出现)
    for(let in newAttrs){
        if(!oldAttrs.hasOwnProperty(key)){
            patch[key] = newAttrs[key];
        }
    }
    return patch;
}

打补丁,重新更新视图

patch(el,patches)