V-for 就地复用原理
举个 🌰:
1 | <div v-for="(item,index) in items"> |
JS 部分
1 | //data里面的items |
效果如下
可以发现:
- 当删掉 items 中的第二个对象时,输入框中的值还是 2–这意味着没有删除对应的第二个节点。这是因为 vue 采用虚拟 DOM+diff 算法导致的数据混乱。
- vue 监听到 items 数组中少了个元素后,会更新虚拟 DOM,然后使用 diff 算法比较新、旧 DOM 树,在这个过程中,由于要计算出真实 DOM 树的最小变更规模,因此会尽可能复用已有的节点(如果节点类型相同)
此处,我们的需求当然是不复用节点,那该如何实现呢?
:key 解决 v-for 导致的数据混乱
- 在渲染列表时,为每个元素绑定独一无二的 key,这样,vue 在更新经 v-for 渲染过的列表时,由于 key 值不同,会认为是不同的节点类型,不采取复用。这样就避免了数据混乱
为什么不能使用数组下标作为 key:
- 不能使用各元素的 index 作为 key,因为当新增或删除列表中元素时,各项索引都会变,也就是说索引对应元素变了,失去了标识的唯一性
声明式渲染
**Vue 提供一套基于 HTML 的模板语法,****允许开发者声明式地将真实 DOM 与 Vue 实例的数据绑定在一起**。
“声明式” 的意思就是: 只需要指出目标, 而不用关心如何实现,将实现交由 vue 处理
虚拟 DOM
- Vdom(virtual dom),可以看作是一个使用 javascript 模拟了 DOM 结构的树形结构
- 其中 Vnode 节点对应真实 DOM 节点
- Vdom 树用于缓存真实 DOM 树的所有信息
为什么要采用虚拟 DOM?
一切为了性能。
“直接操作 DOM 性能差”,这是因为 ——
- DOM 引擎、JS 引擎相互独立,但又工作在同一线程(主线程),因此 JS 代码调用DOM API时必须挂起 JS 引擎、激活 DOM 引擎,完成后再转换到 JS 引擎
- 引擎间切换的代价会迅速积累
- 强制重排的DOM API调用,哪怕只改动一个节点,也会引起整个 DOM 树重排,重新计算布局、重新绘制图像会引起更大的性能消耗
所以,降低引擎切换频率**(减少 DOM 操作次数**)、减小 DOM 变更规模才是DOM 性能优化的两个关键点。
虚拟 DOM +diff 算法是一种可选的解决方案
基本思路:“**在 JS 中**缓存**必要数据,**计算**界面更新时的**数据差异**,只**提交**最终****差集**”。
- 虚拟 dom 只用于缓存,而
- diff 算法负责–
_ 计算出‘虚拟 dom 和目前真实 DOM 之间的数据差异’
_ 提交最终差集注意:“单纯 VDOM 是提高不了性能的,VDOM 主要作用在于它的二次抽象提供了一个 diff/patch 和 batch commit(批量提交)的机会”
watcher 的节流效果:借助 watcher 响应式原理,使数据异步更新(滞后更新),能够实现节流效果,在一段时间内,允许多次更新虚拟 DOM,然后一次性 patch 到真实 DOM 树。像是使用精灵图以减少请求次数那样,达到优化性能的目的。
vue 在监听到数据变动后,会将依赖该数据的 watcher 加入微任务队列,由于微任务是异步的,因此所有同步更新数据的操作,都会及时地在微任务队列中的任务更新前触发 watcher 响应,换个说法:执行第一次变动后的每次变动都会更新 watcher 中的各项依赖。这样的话,在该微任务执行完毕之前的这段时间,就相当于节流中的时延了
Vdom 的 Diff 算法
diff 算法的两个核心:
两个相同的组件产生类似的 DOM 结构,不同的组件产生不同的 DOM 结构。
同一层级的一组节点,他们可以通过唯一的 key 进行区分。
diff 算法的复杂度
- 比较两棵虚拟 DOM 树的差异是 Virtual DOM 算法最核心的部分,这也是所谓的 VirtualDOM 的 diff 算法。两个树的完全的 diff 算法是一个时间复杂度为**O(n^3)**的问题。
- 但是在前端当中,你很少会跨越层级地移动 DOM 元素。所 diff 算法只会对同一个层级的元素进行对比。下面的 div 只会和同一层级的 div 对比,第二层级的只会跟第二层级对比。这样算法复杂度就可以达到**O(n)**。
比较时能否复用的逻辑
当页面的数据发生变化时,Diff 算法只会比较同一层级的节点:
- 如果节点类型不同,直接干掉旧的节点,创建并插入新的那个节点,不会再比较这个节点以后的子节点了。
- 如果节点类型相同,则会直接复用该节点,重新设置该节点的属性,从而实现节点的更新。
当某一层有很多相同的节点时,也就是列表节点时,Diff 算法的更新过程默认情况下也是遵循以上原则。
比如–我们希望可以在 B 和 C 之间加一个 F
Diff 算法默认执行起来是这样的:
- 老的 Vdom 树的该层上有 6 个节点,新的 Vdom 树上有 7 个类型相同的节点,那么就依次复用真实 DOM 树该层上的对应的前 6 个节点,在最后再新建一个节点,赋予之前节点 E 的属性。
- 即把 C 更新成 F,D 更新成 C,E 更新成 D,最后再插入 E,是不是很没有效率?
所以我们需要使用 key 来给每个节点做一个唯一标识,这样 vue 会把他们当做是不同的节点,因此不会复用,diff 算法会直接创建新的节点,并插入正确的位置
key 的作用
- key 的作用主要是为了高效的更新虚拟 DOM。
- 也可避免直接复用 v-for 出来的节点,避免数据混乱
- 另外 vue 中在使用相同标签名元素的过渡切换时,也会使用到 key 属性,其目的也是为了让 vue 可以区分它们,否则 vue 只会替换其内部属性而不会触发过渡效果。
patch 到真实 DOM
模拟实现
如何将 vnode(左边)变成真实的 DOM 元素(右边)
实现如下:
1 | let nodes = { |
PS:
vue 在 patch 时,在一个 update 方法里面调用 createElment()方法,通过虚拟节点创建真实的 DOM 并插入到它的父节点中;
相当于打补丁到真实 DOM
🌰
最后,举个栗子梳理一下:
让 Vue 将name
的数据和<p>
标签绑定在一起:
1 | <p>Hello {{ name }}</p> |
让我们梳理一下 vue 对这个节点 p 和数据所做的一切
- Vue 会把这些模板编译成一个渲染函数 render。
- 该函数被调用后会渲染并且返回一个虚拟的 DOM 树. 这个 “树” 的职责就是描述当前视图应处的状态。
- 之后再通过一个Patch 函数,计算和旧虚拟 dom 树的差集,并通过打补丁的方式将差集中的虚拟节点更新到真实 DOM 树。
- 在整个过程中, Vue 借助数据劫持和订阅者模式实现监听状态、依赖收集、依赖追踪通知变动等。 会侦测在渲染过程中所依赖到的数据来源,以实现双向绑定,自动更新状态。
参考:
https://www.zhihu.com/question/324992717/answer/690011952
既然用 virtual dom 可以提高性能,为什么浏览器不直接自带这个功能呢?–水歌 | 知乎
评论