Virtual DOM 介绍及区分算法
什么是 Virtual Dom
我们知道我们正常的页面是由很多个 Dom 组成的,那么 Virtual Dom 到底是什么呢?简单来说,就是一个真正的dom节点。使用JavaScript来模拟一下这个,对比一下Js层的Dom变化。
以下是大家都应该熟悉的传统dom结。 ?属性,如果每个dom都有children,那么它在children中表现为一个数组,数组中的每一项都是另一个虚拟dom结构。
这里之所以使用Js来实现虚拟dom,是因为在前端领域,Js是唯一图灵完备的语言;所谓图灵完备的语言,是指能够进行复杂的逻辑运算,实现各种逻辑算法的语言。
为什么要使用虚拟DOM
有人可能会问DOM很好。当我们第一次学习前端的时候,肯定会接触到JQuery。 JQuery 是一个典型的操作 DOM 的框架工具库。我们使用 JQuery 来设计场景。我们解释了虚拟 dom 的用途和价值。
这个有一个需求场景
var data = [
{
name: '张三',
age: '20',
address: '杭州'
},
{
name: '李四',
age: '22',
address: '北京'
},
{
name: '隔壁老王',
age: '24',
address: "西溪水岸"
}
]
复制代码现在我们想把这个数据渲染成一个表格,点击页面上的一个按钮来替换部分数据。我们为此使用 Jquery。
<div id="container"></div>
<button id="btn-change">change</button>
<script ></script>
<script>
var data = [
{
name: '张三',
age: '20',
address: '杭州'
},
{
name: '李四',
age: '22',
address: '北京'
},
{
name: '隔壁老王',
age: '24',
address: "西溪水岸"
}
]
function render(data) {
var $container = $('#container')
//清空现有内容
$container.html('')
// 拼接 table
var $table = $('<table>')
$table.append($('<tr><td>name</td><td>age</td><td>address</td></tr>'))
data.forEach(function (item) {
$table.append($('<tr><td>'+ item.name +'</td><td>'+item.age+'</td><td>'+item.address+'</td></tr>'))
})
// 渲染到页面
$container.append($table)
}
$('#btn-change').click(function () {
data[1].age = 30
data[2].address = '上海'
render(data)
})
// 初始化时候渲染
render(data)
复制代码您可以看到我们已经替换了数据第二项年龄及其❓第三件衣服。点击更改按钮:![]()
vdom解决的问题从图中可以看到,我们只更改了表中的部分数据,但是table的所有节点都闪烁了,说明整个桌子又被更换了。
这个常识性的 JQuery 操作对于网页来说可能是一个巨大的实时性能杀手。由于这会更改 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中使用的虚拟域库snapdom。下图是其github主页的截图示例:
仔细观察,我们发现这个snapdom官方案例的主要内容是两个函数——h函数和函数
h函数
可以看到函数h有3个参数例如第一个h函数生成的vnode是div,点击事件绑定 someFn,没有 。 时尚地,sapn是一个文本节点它是粗体,第二个孩子直接是一个文本节点。三个孩子与herf有a联系。布局函数
patch分为两种情况
- 第一种是第一次渲染抛出
patch容器❓❓❓空容器。var vnode = h('ul#list',{},[ h('li.item',{},'大冰哥'), h('li.item',{},'伦哥'), h('li.item',{},'阿孔') ]) patch(container, vnode) // vnode 将 container 节点替换 复制代码
第一次渲染时,生成的vnode会被扔进一个空容器中。你可以将它与以前的Jquery进行比较。第一次渲染表格时,将表格的html添加到容器中
- 第二次是更新节点时
newVnode将替换为旧的Vnode,此处补丁与之前的vnode并且只改变了改变的部分,未改变的部分保持不变。核心就是这里包含的diff算法
与之前的JQuery相比我们可以清楚地看到。要替换整个页面 dom,请使用 vdom 函数
pathc只对我们相对较旧的 vnode 进行更改,而保留未更改的部分不变(如页面闪烁所示)使用 vdom 之前的 Jq -重做
vdom核心api
hh和h功能的案例。我们已经有了基本的了解。为了巩固对此的理解,我们继续下来用这里重点解释参数snabbdom重做我们之前的JQuery案例直接从代码开始
<div id="container"></div> <button id="btn-change">change</button> <script ></script> <script ></script> <script ></script> <script ></script> <script ></script> <script ></script> <script> let container = document.getElementById('container') let btn = document.getElementById('btn-change') let snabbdom = window.snabbdom let patch = snabbdom.init([ snabbdom_class, snabbdom_props, snabbdom_style, snabbdom_eventlisteners ]) let h = snabbdom.h let data = [ { name: '张三', age: '20', address: '杭州' }, { name: '李四', age: '22', address: '北京' }, { name: '隔壁老王', age: '24', address: "西溪水岸" } ] data.unshift({ name: '姓名', age: '年龄', address: '地址' }) let vnode function render(data) { // 创建虚拟table节点 第三个参数,也就是虚拟table的孩子 应该是虚拟的 行节点 let newVnode = h('table', {}, data.map(item => { let tds = [] // 列,作为虚拟行的子项 let i for(i in item) { if (item.hasOwnProperty(i)) { tds.push(h('td', {}, item[i]+'')) } } return h('tr', {}, tds) // 虚拟行节点的 孩子 应该是虚拟的 列节点 })) if (vnode) { patch(vnode,newVnode) } else { // 初次渲染 patch(container,newVnode) } vnode = newVnode } btn.addEventListener('click', function(){ data[1].age = 30, data[3].name = '一个女孩', render(data) }) // 初始化时候渲染 render(data) </script> </body> 复制代码代码有点长。其实内容就是我们前面讲的。代码主要做了以下几件事
- 介绍snabbdom core文件,第一次加载时初始化h函数和patch函数
- ,渲染本质其实是
patch(container, newVnode) - 然后点击change
生成新的vnode,然后patch(vnode ,newVnode)
函数render
const list = [ { id: 1, name: 'test1', }, { id: 2, name: 'test2', }, { id: 3, name: 'test3', }, { id: 4, name: '添加到最后的一条数据', }, ] 复制代码,当生成第三个参数newV时,第三个
表的孩子是行节点。 tr线路节点也是v节点。当它们恢复时,它们还使用功能h。第三个参数是列td。 vnodetd列中的第三个vnode参数直接是文本节点。迭代每个项目并将其推入tds数组。
到这里,大家应该对vdom有了一个大概的了解了。其实,与其说vdom快,不如说相对于覆盖dom的Jquer,不保证慢
总结
vdom核心api
- h('tag) name' , 'property', [子元素])
- h('tagname', 'property', 'text')
- patch(container, vnode)
- patch(oldVnode, newVnode) Diff算法简介介绍
什么是 diff 算法
在日常工作中,我们其实很多时候都会用到 diff 算法
比如你在提交代码到 git 的时候会用到git♾❀ diff 或者一些代码对比的时候会用到它网上找的工具,我们虚拟域的核心是diff-算法。正如我们之前所说,在需要更新的节点上查找更新,而不更新不需要的节点。移动。其核心是如何找出哪些已更新,哪些未更新。需要 diff 算法来完成这个过程。与算法的差异。在Snabbdom中,差异主要体现在
补丁。我们先看一下两种情况patch(container, vnode)和patch(vnode, newVnode),所以空间有限(实际上空间有限)。我在这里简单解释一下,因为完成的diff算法的东西太多了。有兴趣的可以查看snabbdom的源码
patch(container, vnode)
我们知道打补丁的过程就是在一个空容器中添加一个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为nullpatch(vnode, newVnode)是比较过程。这里我们只模拟最简单的场景
第三项变化,添加第四项
// 简化流程 假设跟标签相同的两个虚拟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) } }) } 复制代码这里Point也是递归的。这里我们简单的使用
标签来设置更新条件。事实上,事情比这复杂得多;而函数replace的实际作用是newVnode生成的真实dom替换旧dom。涉及到的本地操作比较多,就不详细说了。现在大家应该已经了解差异的基本概念了。同样,为了便于理解,diff 算法过程被大大简化。实际的微分算法的复杂度远高于上述,比如
- 添加和删除节点
- 重新组织和优化这个过程
- 节点属性样式事件的变化等将算法优化为极端等
如果您有兴趣,可以获取更多信息。
总结
本文包含的知识是一个介绍。通过阅读本文,让不了解virtual dom的同学对virtual dom有一个很好的了解,并对diff算法有一个大概的了解。如果能够达到这样的效果,我想这篇文章将会非常有价值。想要进一步了解virtual dom或者diff算法的同学可以阅读snabbdom
patch.js源码来加深学习。Vue额外密钥
在写文章的时候,遇到了绑定vue密钥的问题。在这里我利用这份热情以及 virtual dom 和 diff 算法来了解 Vue 的关键
Vue。关键
首先来自Vue官网的解释:
当Vue.js使用❙ v-for更新渲染元素列表时,默认采用“适当重用”策略。当数据项的顺序更改时,Vue 不会移动 DOM 元素来匹配数据项的顺序,它只是重用那里的每个元素,并确保它渲染在特定索引处渲染的每个元素。
这里的就地回收策略是回收未发生变化的元素,其余的必须按顺序重新定位。
为了让 Vue 跟踪每个节点的身份,从而重用和重新排序现有元素,您必须为每个项目提供唯一的
key属性。key的理想值是每个项目的唯一 ID。我们经常使用
索引(即数组下标)作为key,但这实际上不是推荐的❙用法。请参阅以下示例:这是数组数据。 ,没有问题,因为
索引收集了末尾 1。但如果插入的数据是插入到中间而不是末尾,
const list = [ { id: 1, name: 'test1', }, { id: 4, name: '不甘落后跑到第二的的一条数据', } { id: 2, name: 'test2', }, { id: 3, name: 'test3', }, ] 复制代码这时候就会出现这种情况:
之前的数据 之后的数据 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 复制代码这样数据插入后,除了前面的部分数据可以被复用之外。 最后三块要重新渲染,这显然不是我们想要的结果。
使用独特钥匙来改善:
这次我们绑定
钥匙钥匙钥匙钥匙 作为每个新 ID 的补充。紧随其后的 ID 为 4 的数据是新添加的,其他数据复用了之前的 dom,因为这里是通过唯一 key 关联的,不关联重排序。渲染。所以我们需要使用一个key来唯一标识每个节点。 Vue Diff算法可以正确识别这个节点,并找到正确的位置插入新节点。一句话key的作用主要是有效更新虚拟DOM
灵魂画师上线:
可以看到当我们的旧数据转换为新数据时[a,b,c,d] ] --> [a, e ,b,c,d]
如果我们没有使用正确的
key,除了a数据可以重用外,接下来的4个数据都必须重用 -渲染为。而如果使用输入正确的
key时,只需要改变一件事,就是新的数据e,其他的按照箭头所示继续。作者:noobakong
链接:https://juejin.im/post/5c4a76b4e51d4526e57da225
来源:版权所有来源:掘金。商业转载请联系作者获取授权。非商业转载请注明出处。
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
code前端网