结论
vue
中vdom渲染页面的过程:将<template>
模板,通过render
渲染函数(createElement())得到虚拟的DOM树,通过diff
算法进行新旧虚拟节点的比较,再通过patch
更新到真实的dom上实现视图的更新。
1.什么是Virtal DOM?
vdom指的是用JS模拟的DOM结构,将DOM的变化对比放在JS层。
<ul id="list">
<li class="item">Item1</li>
</ul>
变成vdom就是:
{
tag:'ul',
attrs:{
id:'list'
},
children:[{
tag:'li',
attrs:{
className:'item',
},
children:['Item1']
}]
}
2.为什么要使用vdom?
问题
在《高性能javascript》中提到,操作dom的代价很‘昂贵’,会导致页面的重排和重绘等问题,影响js的性能。所以应该减少dom的访问次数,将操作放在js中。
解决
使用vdom,只需要改变更新的DOM,不需要改动的地方不动,对dom的一些频繁操作都在vdom树上,减少重排重绘带来的性能消耗,提高渲染的效率。
- 将真实的DOM编译成vnode
- diff,比较oldVnode和newVnode之间的变化
- patch,将这些变化用打补丁的方式更新到真实的dom上去。
3.借用snabbdom的思想实现vdom
3.1介绍snabbdom
snabbdom
是一个简易的实现vdom
功能的库,vdom
里面有两个核心的API,一个是h
函数,一个是patch
函数。前者是用来生成vdom对象(vue
中使用render
函数,将真实的节点转换成vnode
),后者是用做vdom
之间的对比以及将vdom挂载到真实的dom上。vue就是因为其使用了snabbdom而有更优异的性能。
var snabbdom = require('snabbdom');
var patch = snabbdom.init([ // Init patch function with chosen modules
require('snabbdom/modules/class').default, // makes it easy to toggle classes
require('snabbdom/modules/props').default, // for setting properties on DOM elements
require('snabbdom/modules/style').default, // handles styling on elements with support for animations
require('snabbdom/modules/eventlisteners').default, // attaches event listeners
]);
var h = require('snabbdom/h').default; // helper function for creating vnodes
//h:创建一个虚拟的节点,定义id=container
var container = document.getElementById('container');
//h:container有两个类名class和two 绑定一个click事件,跟着一个数组
var vnode = h('div#container.two.classes', {on: {click: someFn}}, [
//h: <h style='font-weight:bold'>This is bold </h>
h('span', {style: {fontWeight: 'bold'}}, 'This is bold'),
' and this is just normal text',
h('a', {props: {href: '/foo'}}, 'I\'ll take you places!')
]);
//pacth:首次渲染时将vnode放入到空的container中
patch(container, vnode);
//一个新的vnode
var newVnode = h('div#container.two.classes', {on: {click: anotherEventHandler}}, [
h('span', {style: {fontWeight: 'normal', fontStyle: 'italic'}}, 'This is now italic type'),
' and this is still just normal text',
h('a', {props: {href: '/bar'}}, 'I\'ll take you places!')
]);
//patch: dom变化时,新旧vnode比较,只更新需要改动的内容
patch(vnode, newVnode); // Snabbdom efficiently updates the old view to the new state
3.2用h方法实现创建vdom
<ul id="list">
<li class="item">Item1</li>
</ul>
var vnode = h('ul#list', {}, [
h('li.item', {}, 'Item 1'),,
])
3.3patch源码
return function patch(oldVnode, vnode) {
var i, elm, parent;
//记录被插入的vnode队列,用于批触发insert
var insertedVnodeQueue = [];
//调用全局pre钩子
for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();
//如果oldvnode是dom节点,转化为oldvnode
if (isUndef(oldVnode.sel)) {
oldVnode = emptyNodeAt(oldVnode);
}
//如果oldvnode与vnode相似,进行更新
if (sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode, insertedVnodeQueue);//patchnode里有diff算法的核心updatechildren
} else {
//否则,将vnode插入,并将oldvnode从其父节点上直接删除
elm = oldVnode.elm;
parent = api.parentNode(elm);
//将vnode生成真实的dom
createElm(vnode, insertedVnodeQueue);
if (parent !== null) {
api.insertBefore(parent, vnode.elm, api.nextSibling(elm));
removeVnodes(parent, [oldVnode], 0, 0);
}
}
//插入完后,调用被插入的vnode的insert钩子
for (i = 0; i < insertedVnodeQueue.length; ++i) {
insertedVnodeQueue[i].data.hook.insert(insertedVnodeQueue[i]);
}
//然后调用全局下的post钩子
for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();
//返回vnode用作下次patch的oldvnode
return vnode;
};
patch
方法中实现了snabbdom作为高效vdom库的法宝——diff
算法,diff
为了找出需要更新的节点,核心逻辑在updateChildren
函数中。
patch中的重要函数sameVnode实现了只能同级访问。
并且对于vdom中的children比较,因为同层和有可能移动,顺序比较无法最大化的复用已有的DOM,所以通过给每个vnode加上key来跟踪这种顺序的变动,形成为一标识,高效更新虚拟DOM。
3.4diff算法
为什么要是有diff算法
找出新旧虚拟dom的差别,来更新一些节点。怎么找出,就是通过diff算法。
如果要比较vdom树的差异理论上的时间复杂度高达O(n^3),但是由于我们在实际开发中很少出现跨级的DOM变更,所以在vdom库中,之比较同级的,此时的复杂度为O(n).
diff的算法实现的流程
- 实现的过程patch(vnode,container)初始化加载,将vnode打包渲染到空的容器中和patch(vnode,newVnode)。新旧vnode的比较
- 核心就是createElement和updateChildren
vnode通过updateChildren找出区别:
//简单的实现更新的操作
function updateChildren(vnode, newVnode) {
let children = vnode.children || [];
let newChildren = newVnode.children || [];
children.forEach((childVnode, index) => {
let newChildVNode = newChildren[index];
//新旧的标签名是否相等
if(childVnode.tag === newChildVNode.tag) {
//深层次对比, 递归过程
updateChildren(childVnode, newChildVNode);
} else {
//替换
replaceNode(childVnode, newChildVNode);
}
})
}
通过createElement将模拟的JS转化成真实的DOM
const createElement = (vnode) => {
let tag = vnode.tag;
let attrs = vnode.attrs || {};
let children = vnode.children || [];
if(!tag) {
return null;
}
//创建元素
let elem = document.createElement(tag);
//属性
let attrName;
for (attrName in attrs) {
if(attrs.hasOwnProperty(attrName)) {
elem.setAttribute(attrName, attrs[attrName]);
}
}
//子元素
children.forEach(childVnode => {
//给elem添加子元素
elem.appendChild(createElement(childVnode));
})
//返回真实的dom元素
return elem;
}
https://www.cnblogs.com/chrislinlin/p/12585851.html
https://www.cnblogs.com/tomatoto/p/10002343.html