- 虚拟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:[{}],
},
]
}
- 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);
}


- 渲染真实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三种优化策略
- 同层
- 同层可以复用 key
差异计算
先序深度优先遍历
- 用JavaScript对象模拟DOM
- 把此虚拟DOM转换成真实DOM,并插入页面中
- 如果有事件发生修改了虚拟DOM,比较两颗虚拟DOM树的差异,得到差异对象
- 把差异对象应用到真正的DOM树上
let patchs = diff(vertualDom1,vertualDom2)
规则:
- 当节点类型相同时,看属性是否相同,产生一个属性的补丁包
{type:"ATTRS",attrs:{class:"list-group"}}
- 新的DOM节点不存在
{type:'REMOVE',index:xxx}
- 节点类型不相同,直接采用替换模式
{type:'REPLACE',newNode:newNode}
- 文本的变化
{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)