vdom的原理

结论

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章