vue底层原理
虚拟Dom
虚拟DOM是一个在内存中的数据结构,它是真实DOM的抽象表示。虚拟DOM是由一系列的虚拟节点(VNode)组成的,每一个虚拟节点都对应一个真实的DOM节点。
作用:
虚拟DOM的主要作用是提供一个在内存中操作DOM的平台,使得我们可以在不直接操作真实DOM的情况下进行复杂的操作。当状态变化时,Vue会生成一个新的虚拟节点树,然后和旧的虚拟节点树进行对比,找出差异,然后只更新差异部分到真实的DOM上,这个过程称为“打补丁”。
优势:
性能优化:由于直接操作DOM通常是前端性能瓶颈的主要原因,虚拟DOM通过在内存中进行计算,避免了直接操作DOM,从而提高了性能。
跨平台:虚拟DOM不仅可以表示浏览器中的DOM,还可以表示其他的平台,比如移动应用的UI。
缺陷:
首次渲染开销大:虚拟DOM需要在内存中构建一颗DOM树,所以在首次渲染时,虚拟DOM的开销会比直接操作DOM大。
内存消耗:虚拟DOM需要在内存中维护一颗DOM树,这会消耗一定的内存。
一个虚拟Dom节点至少包括三个属性:type、attr、children
响应式
effect 副作用
effect是一个‘桶’, 他收集并触发指定方法
1 | /** |
计算属性的实现流程
1 | const fullName = computed(()=> firstName.value + lastName.value) |
响应式系统设计
- 依赖,就是需要重复调用的函数
- 用set保存依赖,防止重复调用
- 用map关联属性和依赖集合,key一个对象里的属性,value指向依赖集合
- 用weakmap管理多个对象。
reactive
reactive 只能监听复杂数据类型,因为proxy只能监听负责数据类型的getter和setter
1 | function reactive(obj){ |
为什么用Reflect
Reflect 就是反射,可以直接调用对象的基本方法。
1 | let obj = { |
1 | const proxy = new Proxy(obj, { |
1 | const proxy = new Proxy(obj, { |
ref
那么ref怎么监听原始值类型?
利用了get 和 set 标记语法
- ref 接收任意数据
- ref 返回RefImpl实例
- 利用get和set监听
1 | class RefImpl { |
1 | // 测试一下! |
上述effect函数和activeEffect都是简化后的,没有判断该副作用到底依赖了谁
编辑器
模板DSL
把template模板编译为render函数
三大流程:
- 通过 parse 方法进行解析,得到 AST(抽象语法树)
- 通过 transform 方法对 AST 进行转化,得到 JavaScript AST(大部分和AST相同)
- 通过 generate 方法根据 AST 生成 render 函数
1 | <div v-if="isShow"> |
得到的AST如下
1 | { |
v-model原理
在原生元素上
transform之后不会生成modelValue属性,但会生成onUpdate:modelValue回调函数和vModelText
自定义指令
vModelText指令在create钩子里监听input或change事件,来调用onUpdate:modelValue实现view流向vm; 在mount和beforeUpdate钩子里修改原生元素的value实现 model->view
在组件上
const model = defineModel() // 返回一个ref
首先在vue编译阶段
- 解析出AST
此时v-model当作一个属性名
- 调用transform函数处理AST
transform函数内部调用不同的函数来解析各种指令。经过transform处理AST,表示该节点的对象的属性codeGenNode
从 undefined 变成一个对象
两张图片看到经过处理后的codeGenNode对象内部。其中properties数组里每一个item都是有key和value。modelValue对应值,onUpdate:modelValue对应方法。
他们都用在后续步骤拼接成render函数。
例如: transformModel、transformIf
- 调用generate生成render函数
把上面两个放入组件实例里
此时v-model最终被编译成了 :modelValue
属性 和 @update:modelValue
事件
vue 工作流程
- 编译
- 通过 parse 方法进行解析,得到 AST(抽象语法树)
- 通过 transform 方法对 AST 进行转化,得到 JavaScript AST(大部分和AST相同)
- 通过 generate 方法根据 JavaScript AST 生成 render 函数
- 创建Vue实例
- 处理响应式数据
- 挂载阶段
- 调用render生成虚拟dom
- 生成真实dom并插入dom树指定位置
- 记录vnode为preVnode
至此初次渲染完成,如果改变了响应式数据导致需要更新视图则会进入更新阶段
- 更新阶段
- 重新调用render生成新虚拟dom
- 调用patch方法
- 利用diff算法比较新旧虚拟dom找到差异
- 根据差异更新真实dom
diff算法
本质是一个对比算法,旧DOM更新为新DOM时怎么提高效率
但是不是DOM更新了就会触发diff,而是DOM挂载、卸载或变换顺序才会触发diff
原本两个树的完全的 diff 算法是一个时间复杂度为 O(n3) , Vue 进行了优化转换成 O(n) 复杂度的问题(只比较同级不考虑跨级问题)
节点比较:首先,Vue.js会比较新旧两个虚拟DOM树的根节点。如果根节点类型不同(例如,一个是div,另一个是p),那么Vue.js会直接替换旧的根节点及其所有子节点。如果根节点类型相同,那么Vue.js会保留该节点,并进一步比较其属性和子节点。
属性比较:Vue.js会比较同一节点的新旧属性,包括样式、类名、事件监听器等。如果新旧属性不同,那么Vue.js会更新这些属性。
子节点比较:如果节点类型相同并且属性也相同,那么Vue.js会进一步比较节点的子节点。这是一个递归过程,Vue.js会继续比较子节点的节点类型、属性和子节点,直到所有节点都被比较。
列表优化:在比较子节点时,Vue.js使用了一种称为keyed diffing的优化策略。通过给每个子节点分配一个唯一的key,Vue.js可以更快地找到新旧子节点列表中相同的节点,从而避免不必要的节点创建和删除操作。
v-for key 的意义
1 | <!-- 原本 item = [{id=1,title='a'},{id=2,title='b'},{id=3,title='c'}] --> |
此时diff算法开始工作,
首先,怎么比较?isSameVNodeTyppe(n1: VNode, n2: VNode)
方法通过比较n1和n2的type
和key
,如果都相等就说明没有变化,无需挂载卸载DOM元素
- type 是AST中的type,表示节点类型
- key 则是 v-for 中的key
上述情况可知,第三个li变了需要更新
diff 算法五大步骤
sync from start:自前向后的对比
- 把两组 dom 自前开始,相同的 dom 节点(vnode)完成对比处理
sync from end:自后向前的对比
- 把两组 dom 自后开始,相同的 dom 节点(vnode)完成对比处理
common sequence + mount:新节点多于旧节点,需要挂载
common sequence + unmount:旧节点多于新节点,需要卸载
unknown sequence:乱序
1 | //前者是一组旧DOM的key数组 后者是更新后新DOM的key数组 |
阶段5是最复杂的,单独考虑一个进入阶段5的例子
1 | oldChildren= [a, b, c, d, e, f, g] |
总结
- _update
1
2
3
4
5
6
7function _update(vnode) {
const oldVnode = this._vnode //保存旧虚拟dom树
this._vnode = vnode//赋值新虚拟dom树
//对比新旧doom patch
patch(oldVnode, this._vnode)
//重新渲染render() 生成新真实dom树
}