1.虚拟dom
dom就是html文件里内容,一个页面由多个dom组成
<ul class="lists"> <li class="item">li1</li> <li class="item">li2</li> </ul>
而对应的虚拟dom是
tag: 'ul', attrs: { className: 'lists' }, children: [ { tag: 'li', attrs: { className: 'item' }, children: ['li1'] }, { tag: 'li', attrs: { className: 'item' }, children: ['li2'] } ]
tag
表示标签名,attrs
就是dom的属性,每个dom如果有children的话,就会在children中以数组的形式展示,数组的每一项就又是一个虚拟dom结构。
为什么要使用虚拟dom呢?
举个最简单的列子
使用jq的时候,使用append插入函数
要是后续改了某个值,要重新append.是整个dom发生的替换,并不是修改的那一项
并且单单一个空白的div底下的标签就有那么多
var div = document.createElement('div') var item, result = '' for (item in div) { result += ' | ' + item } console.log(result)
密密麻麻的属性,更何况这还只是一级属性,可想而知直接操作dom的方式是有多么费时,dom操作是费时的, 但是Js作为一门语言,运行速度是非常快的,我们如果在Js层做dom对比,尽量减少不必要的dom操作,而不是每一次都全部翻修,我们的效率就会大大增加。 而vdom就可以完美解决这个问题。
要了解如何使用vdom,我们可以借助现有的vdom实现库,来了解其API,进而了解如何将vdom运用于开发中。
这里我们选择一个Vue2中使用的虚拟dom库 snabbdom
主要有两个函数:
可以看到 h
函数,有三个参数
比如说第一个h函数
生成的vnode,就是一个ul
标签,绑定了className为lists,
第一个children为带有className的li
,li里是一个文本节点li1
,
第一个children为带有className的li
,li里是一个文本节点li2
,
patch
分为两种情况
patch
将vnode丢到container
空容器中
var vnode = h('ul#list',{},[ h('li.item',{},'大冰哥'), h('li.item',{},'伦哥'), h('li.item',{},'阿孔') ]) patch(container, vnode) // vnode 将 container 节点替换 复制代码
第一次patch渲染的时候,是将生成的vnode往空容器里丢 可以对比之前的Jquery第一次渲染表格的时候,将table html append到容器中去
newVnode
将oldVnode
替换
btn.addEventListener('click',function() { var newVnode = h('ul#list',{},[ h('li.item',{},'大冰哥'), h('li.item',{},'伦哥'), h('li.item',{},'孔祥宇'), h('li.item',{},'小老弟'), ]) patch(vnode, newVnode) }) 复制代码
这里的patch就会将的vonde和之前的vnode进行比对,只修改改动的地方,没动的地方保持不变,这里的核心就是涉及的diff算法
我们可以清楚的看到,相对于之前的JQuery整个页面dom全部替换的情况,用vdom的pathc
函数只修改了我们相对老的vnode变动的地方,没改动的地方就没用动(从页面的闪烁可以看出来)
vdom的核心api
2.diff算法
我们在平时工作中,其实很多时候都会使用到diff算法
比如你在git提交代码的时候使用的 git diff
命令,再或者是网上的一些代码比对工具如svn上的,vue的key后续会说
而我们的虚拟dom,核心就是diff算法,我们前面讲过,找出有必要更新的节点更新,没有更新的节点就不要动。
这其中的核心就是如何找出哪些更新哪些不更新,这个过程就需要diff算法来完成
这个patch的过程是将一个vnode(vdom)添加到空容器生成真实dom的过程,主要的简化代码流程如下:
function creatElement(vnode) { let tag = vnode.tag let attrs = vnode.attrs || {} let children = vnode.children || [] // 无标签 直接跳出 if (!tag) { return null } // 创建元素 let elem = document.createElement(tag) // 添加属性 for(let attrName in attrs) { if (attrs.hasOwnProperty(attrName)) { elem.setAttribute(arrtName, arrts[attrName]) } } // 递归创建子元素 children.forEach((childVnode) => { elem.appendChild(createElement(childVnode)) }) return elem }
简化后的代码很简单,大家也都能够理解,其中的一个重要的点就是 自递归调用生成孩子节点,终止条件就是tag
为null
的情况
这个patch过程就是比较差异的过程,我们这里就只模拟最简单的场景
// 简化流程 假设跟标签相同的两个虚拟dom function updateChildren (vnode, newVnode) { let children = vnode.children || [] let newChildren = newVnode.children || [] // 遍历现有的孩子 children.forEach((oldChild, index) => { let newChild = newChildren[index] if (newChild === null) { return } // 两者tag一样,值得比较 if (oldChild.tag === newChild.tag) { // 递归继续比较子项 updateChildren(oldchild, newChild) } else { // 两者tag不一样 replaceNode(oldChild, newChild) } }) }
这里面的点就也递归,这里只是简单的拿tag
来判断更新条件,其实实际的比这复杂很多很多;
而replace
函数实际的操作就是将newVnode
新生成的真实dom将老的dom替换掉,这里涉及更多的是原生dom操作,就不在赘述了。
key
属性。理想的 key
值是每项都有的唯一 id。
我们在使用的使用经常会使用index
(即数组的下标)来作为key
,但其实这是不推荐的一种使用方法
要是出现如下情况:
在第二条加了一条数据
之前的数据 之后的数据 key: 0 index: 0 name: test1 key: 0 index: 0 name: test1 key: 1 index: 1 name: test2 key: 1 index: 1 name: 不甘落后跑到第二的的一条数据 key: 2 index: 2 name: test3 key: 2 index: 2 name: test2 key: 3 index: 3 name: test3。
这样一来,追加数据以后,除了第一条数据能够就地复用
,后三条都要重新渲染,这显然不是我们想要的结果。
所以我们需要使用key来给每个节点做一个唯一标识,Vue的Diff算法就可以正确的识别此节点,找到正确的位置区插入新的节点,所以一句话,key的作用主要是为了高效的更新虚拟DOM
原文链接:https://juejin.cn/post/6844903767473651720#heading-5