vue核心原理(Diff算法、虚拟dom)
核心原理&源码
Diff 算法
diff 算法的进化
关于 diff 算法的最经典的就是 Matt Esch 的 virtual-dom,以及 snabbdom(被整合进 vue 2.0中)。
最开始出现的是 virtual-dom 这个库,是大家好奇 React 为什么这么快而搞鼓出来的。它的实现是非常学院风格,通过深度优先搜索与 in-order tree 来实现高效的 diff 。
然后是 cito.js 的横空出世,它对今后所有虚拟 DOM 的算法都有重大影响。它采用两端同时进行比较的算法,将 diff 速度拉高到几个层次。
紧随其后的是 kivi.js,在 cito.js 的基出提出两项优化方案,使用 key 实现移动追踪以及及基于 key 的最长自增子序列算法应用(算法复杂度 为O(n^2))。
但这样的 diff 算法太过复杂了,于是后来者 snabbdom 将 kivi.js 进行简化,去掉编辑长度矩离算法,调整两端比较算法。速度略有损失,但可读性大大提高。再之后,就是著名的vue2.0 把sanbbdom整个库整合掉了。
下面我们就来讲讲这几个虚拟 DOM 库 diff 算法的具体实现:
virtual-dom
virtual-dom 作为虚拟 DOM 开天辟地的作品,采用了对 DOM 树进行了深度优先的遍历的方法。
体现到代码上:(可以看成伪代码)
<script>
function diff(oldTree, newTree) {
let index = 0; // 当前节点的标志(树形层数)
let patches = [] // 用来记录每个节点差异的对象
dfsWalk(oldTree, newTree, patches, index); // 进行深度优先遍历
return patches;
}
// 对两棵树进行深度优先遍历
function dfsWalk(oldNode, newNode, patches, index) {
if (newNode === oldNode) {
return
}
const patch = { type: 'update', vNode: newNode }
const oldChildren = oldNode.children;
const newChildren = newNode.children;
const oldLen = oldChildren.length;
const newLen = newChildren.length;
const len = oldLen > newLen ? oldLen : newLen // 取长的
// 找到对应的子节点进行比较
for (let i = 0; i < len; i++) {
const oldChild = oldChildren[i];
const newChild = newChildren[i];
index++;
// 相同节点进行比对
dfsWalk(oldChild, newChild, patches, index)
if (isArray(oldChild.children)) {
index += oldChild.children.length
}
}
if (patch) {
patches[index] = patch
}
}
</script>
VDOM 节点的对比
上面代码只是对 VDOM 进行了简单的深度优先遍历,在遍历中,还需要对每个 VDOM 进行一些对比,具体分为以下几种情况:
- 旧节点不存在,插入新节点;新节点不存在,删除旧节点
- 新旧节点如果都是 VNode,且新旧节点 tag 相同
- 对比新旧节点的属性
- 对比新旧节点的子节点差异,通过 key 值进行重排序,key 值相同节点继续向下遍历
- 新旧节点如果都是 VText,判断两者文本是否发生变化
- 其他情况直接用新节点替代旧节点
详细代码加详细注释
<script>
function diff(oldTree, newTree) {
let index = 0; // 当前节点的标志(树形层数)
let patches = [] // 用来记录每个节点差异的对象
dfsWalk(oldTree, newTree, patches, index); // 进行深度优先遍历
return patches;
}
import { isVNode, isVText, isArray } from './utils/type.js'
// 对两棵树进行深度优先遍历
function dfsWalk(oldNode, newNode, patches, index) {
if (newNode === oldNode) {
return
}
let patch = patches[index];
if (!oldNode) {
// 旧节点不存在,直接插入
// appendPatch 是用来存节点之间差异的
patch = appendPatch(patch, {
type: PATCH.INSERT,
vNode: newNode,
})
} else if (!newNode) {
// 新节点不存在,删除旧节点
patch = appendPatch(patch, {
type: PATCH.REMOVE,
vNode: null
})
} else if (isVNode(newNode)) { // 新节点是 VNode,就相当于前面写的Element
if (isVNode(oldNode)) { // 旧节点也是 VNode,就要比较这两个节点的 tagName是否一致
// 新旧节点 tagName 一致,并且 key 也一致。
if (newNode.tagName === oldNode.tagName && newNode.key === oldNode.key) {
// 新老节点属性的对比, diffProps方法就是对新旧节点自身属性的对比
// 属性如果有差异,propsPatch的长度 > 0,且差异存在 propsPatch中
const propsPatch = diffProps(newNode.props, oldNode.props)
if (propsPatch && propsPatch.length > 0) {
patch = appendPatch(patch, {
type: PATCH.PROPS, // props这个表示是节点的属性差异
patches: propsPatch // 这里存的是差异的内容
})
}
// 新老节点子节点的对比
// diffChildren 方法是专门来对比子节点的。
patch = diffChildren(oldNode, newNode, patches, patch, index)
}
} else {
// 旧节点不是 VNode, 新节点替换旧节点
patch = appendPatch(patch, {
type: PATCH.REPLACE,
vNode: newNode
})
}
} else if (isVText(newNode)) { // 既然新节点不是 VNode,就判断新节点是否是文本节点
// 旧节点不是文本节点
if (!isText(oldNode)) {
// 将旧节点替换成文本节点
patch = appendPatch(patch, {
type: PATCH.VTEXT,
vNode: newNode,
})
} else if (newNode.text !== oldNode.text) { // 判断两者内容是否相等
// 替换文本
patch = appendPatch(patch, {
type: PATCH.VTEXT,
vNode: newNode,
})
}
}
if (patch) {
// 将补丁放入对应位置
patches[index] = patch
}
}
</script>
属性的对比
<script>
function diffProps(newProps, oldProps) {
const patches = [];
// 将新旧属性都浅拷贝进 props
const props = Object.assign({}, newProps, oldProps)
// 将props对象的键转换成数组
Object.keys(props).forEach(key => {
// 如果新属性里有这个键,就能获取到这个键的属性值
const newVal = newProps[key];
// 旧属性也一样
const oldVal = newProps[key];
// 新属性这个键不存在
if (!newVal) {
// 那就直接用旧的
patches.push({
type: PATCH.REMOVE_PROP,
key,
value: oldVal,
})
}
// 旧的不存在或者新的不等于旧的
if (oldVal === undefined || newVal !== oldVal) {
patches.push({
type: PATCH.SET_PROP,
key,
value: newVal,
})
}
})
}
</script>
子节点的对比
这一部分可以说是 diff 算法中,变动最多的部分,因为前面的部分,各个库对比的方向基本一致,而关于子节点的对比,各个仓库都在前者基础上不断得进行改进。
首先需要明白,为什么需要改进子节点的对比方式。如果我们直接按照深度优先遍历的方式,一个个去对比子节点,子节点的顺序发生改变,那么就会导致 diff 算法认为所有子节点都需要进行 replace,重新将所有子节点的虚拟 DOM 转换成真实 DOM,这种操作是十分消耗性能的。
但是,如果我们能够找到新旧虚拟 DOM 对应的位置,然后进行移动,那么就能够尽量减少 DOM 的操作。
virtual-dom 在一开始就进行了这方面的尝试,对子节点添加 key 值,通过 key 值的对比,来判断子节点是否进行了移动。通过 key 值对比子节点是否移动的模式,被各个库沿用,这也就是为什么主流的视图库中,子节点如果缺失 key 值,会有 warning 的原因。
具体是怎么对比的,我们先看代码:
<script>
function diffChildren(oldNode, newNode, patches, patch, index) {
const oldChildren = oldNode.children;
// 新节点按照旧节点的顺序重新排序
const sortedSet = sortChildren(oldChildren, newNode.children)
// 拿到新节点的子节点
const newChildren = sortedSet.children;
const oldLen = oldChildren.length;
const newLen = newChildren.length;
const len = oldLen > newLen ? oldLen : newLen
for (let i = 0; i < len; i++) {
let leftNode = oldChildren[i];
let rightNode = newChildren[i];
index++;
if (!leftNode) {
if (rightNode) {
// 旧节点不存在,新节点存在,进行插入操作
patch = appendPatch(patch, {
type: PATCH.INSERT,
vNode: rightNode,
})
}
} else {
// 相同节点进行比对
dfsWalk(leftNode, rightNode, patches, index)
}
if (isVNode(leftNode) && isArray(leftNode.children)) {
index += leftNode.children.length
}
}
if (sortedSet.moves) {
// 最后进行重新排序
patch = appendPatch(patch, {
type: PATCH.ORDER,
moves: sortedSet.moves,
})
}
return patch
}
</script>
这里首先需要对新的子节点进行重排序,先进行相同节点的 diff ,最后把子节点按照新的子节点顺序重新排列。
这里有个较复杂的部分,就是对子节点的重新排序。
vue核心原理(Diff算法、虚拟dom)的更多相关文章
- 解析vue2.0的diff算法 虚拟DOM介绍
react虚拟dom:依据diff算法台 前端:更新状态.更新视图:所以前端页面的性能问题主要是由Dom操作引起的,解放Dom操作复杂性 刻不容缓 因为:Dom渲染慢,而JS解析编译相对非常非常非常快 ...
- FinClip 前端之 VUE 核心原理总结
小程序框架有很多,都是支持前端JavaScript语言的,也是支持 vue.js 框架的.FinClip 小程序是兼容各家平台的.所以在学习了框架使用之后的进阶就要熟悉框架的底层原理. 1.数据响应式 ...
- Vue相关,diff算法。
1. 当数据发生变化时,vue是怎么更新节点的? 要知道渲染真实DOM的开销是很大的,比如有时候我们修改了某个数据,如果直接渲染到真实dom上会引起整个dom树的重绘和重排,有没有可能我们只更新我们修 ...
- myvue 模拟vue核心原理
// js部分index.js class Myvue{ constructor(options){ this.data = options.data; this.dep = new Dep(); v ...
- 深度学习之神经网络核心原理与算法-caffe&keras框架图片分类
之前我们在使用cnn做图片分类的时候使用了CIFAR-10数据集 其他框架对于CIFAR-10的图片分类是怎么做的 来与TensorFlow做对比. Caffe Keras 安装 官方安装文档: ht ...
- 【React 7/100 】 虚拟DOM和Diff算法
虚拟DOM和Diff算法 React更新视图的思想是:只要state变化就重新渲染视图 特点:思路非常清晰 问题:组件中只有一个DOM元素需要更新时,也得把整个组件的内容重新渲染吗? 不是这样的 理想 ...
- 虚拟DOM与diff算法
虚拟DOM与diff算法 虚拟DOM 在DOM操作中哪怕我们的数据,发生了一丢丢的变化,也会被强制重建整预DOM树.这么做,涉及到很多元素的重绘和重排,导致性能浪费严重 只要实现按需更新页面上的元素即 ...
- React Diff 算法
React介绍 React是Facebook开发的一款JS库,用于构建用户界面的类库. 它采用声明式范例,可以传递声明代码,最大限度地减少与DOM的交互. 特点: 声明式设计:React采用声明范式, ...
- 【前端知识体系-JS相关】虚拟DOM和Diff算法
1.介绍一下vdom? virtual dom, 虚拟DOM 使用JS来模拟DOM结构 DOM变化的对比,放在JS层来做(图灵完备语言),提高效率 DOM操作非常昂贵(消耗性能) 2.Snabbdom ...
- 【React自制全家桶】二、分析React的虚拟DOM和Diff算法
一.React如何更新DOM内容: 1. 获取state 数据 2. 获取JSX模版 3. 通过数据 +模版结合,生成真实的DOM, 来显示,以下行代码为例(简称代码1) <div id= ...
随机推荐
- 常用的js优秀框架个人记录
一.树框架 zTree http://www.treejs.cn/
- jp@gc - PerfMon Metrics Collector:服务器性能监测控件
1.下载客户端及服务器端插件: 参考如下地址:https://blog.csdn.net/qq_36643889/article/details/119142106 JMeterPlugins-Sta ...
- quartus报错 Error (10054): Verilog HDL File I/O error at sdram_ctrl_tb.v(6): can't open Verilog Design File "Sdram_params.h"
解决方法:包含完整路径. 比如我一开始是:`include "Sdram_params.h" 错误(改为:`include "F:\FPGA\exce\uart2sdra ...
- 洛谷P1203 坏掉的项链
洛谷P1203 坏掉的项链 首先看到这题的数据样例,3<=n<=350,不是水水暴搜就过了嘛 不难想到暴力解,先破环成链 因为可能有全部都可换成一种颜色的情况,考虑每次遇到s[i]==s[ ...
- OO课程第三阶段(实验和pta试题)总结Blog3
OO课程第三阶段(实验和pta试题)总结Blog3 前言:学习OOP课程的第三阶段已经结束了,在此进行对于知识点,题量,难度的个人看法. 学习OOP课程的第三阶段已经结束了,较第一次阶段学习难度加大, ...
- 关于FPGA学习的疑惑
记录心路历程--第一次真正意义上的找工作. 最近在学习小梅哥的FPGA的过程中,总是学习着前面的东西忘着后面的,进行了过一次复习,我准备是在每个章节学完之后,再复习之前的章节,可这样是真的很耗费时间. ...
- 转型.netCore开发 记录日常问题
1.页面问题 asp-for date字段页面显示时间为"yyyy-MM-dd"类型通过@{}来处理,定义常量,将处理后的值赋值给常量,再通过@常量正常赋值例:@{ var beg ...
- day01学习小记
# Markdown学习 ## 标题 ### 三级标题 #### 四级标题 ## 字体 Hellow,World! Hellow,world hellow,world! hellow,world ## ...
- shell脚本基本介绍
1.编程介绍 驱动 硬件默认是不能使用的 CPU控制硬件.不同的厂家硬件设备之间需要进行指令沟通,我们需要驱动程序来进行"翻译": 更趋近与开发的工程师,要学习"汇编语言 ...
- Hbase操作与编程使用
1.任务: 列出HBase所有的表的相关信息,例如表名: 3. 编程完成以下指定功能(教材P92下): (1)createTable(String tableName, String[] fields ...