Vue原理
前言
为什么会产生 Vue、React 等这些框架并且被前端开发者所使用?
问题的产生:UI 与状态同步
早期前端开发主要内容仅数据展示,提交表单
业务复杂 => 数据复杂性,深度交互 => 频繁编写根据数据操作 DOM 的逻辑
缓解: 将用户界面更新操作独立
mvc: model 数据模型 view 视图 control 控制器
问题:频繁操作 DOM 性能低下(如果没有注意性能的话);中间步骤过多,易产生 bug 且不易维护。
解决:MVVM 的核心 VM
视图数据层,存在于视图层(view)之上,使我们不用关注 view 的更新,只需关注视图数据层的逻辑,因为 view 已经被视图数据层直接控制,我们只需专注视图数据层的逻辑即可;
结果:产生了 Vue、React 等这种 MVVM 模型 框架/库
Vue 数据劫持、Angular 的脏检测还是 React 的组件级 reRender 都是解决 ui 与状态同步问题的不同实现;
成为主流原由:同类中的是佼佼者,不管是数据流管理架构还是成熟的前端框架自带的生态解决方案。后续各种UI 库出现巩固了前端开发中地位;
总结:提高开发效率
Vue实例生命周期
生命周期概括了vue在所有情况下的操作逻辑
init vue 实例:生命周期逻辑
beforeCreated
inject 与 data 响应式(通过 defineProperty 为 data 设置set,get;数据劫持是后续响应式和依赖收集的基础)
created
是否存在 el
选项,存在则立即将调用 $mount
方法;否则等待手动调用 vm.$mount()
(调用挂载)
编译:是否存在 template
选项,存在(即运行时编译)则编译 template
为 jsx 在 render
函数生成虚拟节点;否则直接将挂载元素放入 render
中(编译即语法解析,生成AST语法树得到渲染函数)
beforeMounted
创建 vm.$el(真实 DOM 树) 替换 el(执行渲染函数得到虚拟DOM,此过程完成了依赖收集;有了虚拟DOM,解析它替换到真实DOM即完成挂载)
mounted
监听到响应数据修改(set触发响应式)
beforeUpdate
调用 re-render, diff 对比后更新(patch)真实 DOM
updated
beforeDestroy
清除 watcher 监听、子组件、事件监听器
destroyed
Vue内部
new Vue()
发生了什么
-
初始化及调用挂载
最重要的是通过 Object.defineProperty 设置 被设置的对象的 setter 与 getter 函数,这是实现「响应式」以及「依赖收集」的前置条件
-
编译
parse、optimize 与 generate 三阶段
generate 是将 AST 转化成 render function 字符串(jsx)的过程,得到结果是 render 的字符串以及 staticRenderFns 字符串。
在经历过 parse、optimize 与 generate 这三个阶段以后,组件中就会存在渲染 VNode 所需的 render function 了。
-
响应式
render function 渲染时生成虚拟 DOM,解析虚拟 DOM 替换真实 DOM
render function 被渲染的时候,因为会读取所需对象的值,所以会触发 getter 函数进行「依赖收集」,「依赖收集」的目的是将观察者 Watcher 对象存放到当前闭包中的订阅者 Dep 的 subs 中
在修改对象的值的时候,会触发对应的 setter, setter 通知之前「依赖收集」得到的 Dep 中的每一个 Watcher,告诉它们自己的值改变了,需要重新渲染视图。这时候这些 Watcher 就会开始调用 update 来更新视图,当然这中间还有一个 patch 的过程以及使用队列来异步更新的策略
-
Virtual DOM
用 js 对象属性来描述节点,类 DOM 树的数据结构对真实 DOM 树的抽象,实际上它只是一层对真实 DOM 的抽象。
JavaScript 对象为基础而不依赖真实平台环境,所以使它具有了跨平台的能力,比如说浏览器平台、Weex、Node 等。
更新视图(真实 DOM)都基于 Virtual DOM 的更新,update 会再次执行 render function, 得到一个新的 VNode 节点
解析这个新的 VNode 节点,然后用 innerHTML 直接全部渲染到真实 DOM 中。可行但性能待考虑,因为我们只对其中的一小块内容进行了修改。「patch」:将新的 VNode 与旧的 VNode 一起传入 patch 进行比较,经过 diff 算法得出它们虚拟节点的「差异」。根据虚拟节点的差异对真实 DOM 进行修改
实践
响应式
基于 Object.defineProperty 的响应式
此处位于流程的初始化(init)阶段中,目的是观察数据模型(数据被读取/修改时具备自执行操作的能力)。
依赖收集
只有响应式无法处理多实例(视图)依赖多数据模型的各个属性情况;
订阅者 Dep ,它的主要作用是用来存放Watcher
观察者对象,且通知Watcher
更新视图;
观察者 Watch,它的主要作用是存放当前视图更新操作,且能够在视图依赖数据模型的某个属性时,在属性的getter中能被属性的dep(订阅者)收集;
依赖收集,new Vue实例执行observer时,规定属性的getter中将视图的Watch收集到属性的dep(订阅器),setter中调用dep.notify;
// vue.js
// 订阅者
class Dep {
constructor() {
this.subs = [];
}
static target = null;
addSub(watcher) {
this.subs.push(watcher);
}
notify() {
this.subs.forEach((watcher) => watcher.update());
}
}
// 监听者
class Watcher {
constructor(options) {
Dep.target = this;
this._update = options.update;
}
update() {
console.log("update");
this._update();
}
}
// 对象响应化(可观察)
function observer(obj) {
if (!(obj && typeof obj === "object")) return;
Object.keys(obj).forEach((attr) => {
const dep = new Dep();
let val = obj[attr];
Object.defineProperty(obj, attr, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
// 依赖收集
if (Dep.target) dep.addSub(Dep.target);
console.log(attr, dep);
return val;
},
set: function reactiveSetter(newVal) {
if (newVal === val) return;
// 更新视图
val = newVal;
dep.notify();
}
});
});
return { value: obj, dep: new Dep() };
}
// Vue 类
class Vue {
constructor(options = {}) {
this._data = typeof options.data === "function" ? options.data() : options.data;
options.template && (this._template = options.template);
// 1. 声明 get 方法
if (!this._data.__ob__) this._data.__ob__ = observer(this._data);
// 2. 新建一个 Watcher 对象
new Watcher({ update: () => this._update() });
// 3. 触发 get 方法
options.el && this.$mount(options.el);
Dep.target = null;
}
$mount(el) {
this.$el = document.querySelector(el);
// 挂载替换$el...
let innerHTML = this._template;
Object.keys(this._data).forEach((attr) => {
const value = this._data[attr];
innerHTML = innerHTML.replaceAll(`}`, value);
});
this.$el.innerHTML = innerHTML;
}
// 视图更新
_update() {
console.log(`更新视图成功!`);
let innerHTML = this._template;
Object.keys(this._data).forEach((attr) => {
const value = this._data[attr];
innerHTML = innerHTML.replaceAll(`}`, value);
});
this.$el.innerHTML = innerHTML;
}
}
<div id="app"></div>
<div id="h5"></div>
<script>
const model = {
text: "",
time: new Date()
};
const App = new Vue({
el: "#app",
data: model,
template: `
<input value="" />
<div>内容:</div>
<div>时间:</div>`
});
const H5 = new Vue({
el: "#h5",
data: model,
template: `
<div>vm2内容:</div>
<div>vm2时间:</div>`
});
console.log(App);
console.log(H5);
</script>
VNode
Virtural DOM由虚拟节点(VNode)组成,vue的createElement接口实现VNode创建;虚拟DOM是编译生成的render函数的执行结果;
VNode由标签、节点描述Attr(class,style…)、子节点三部分组成;
VNode中节点描述引用vue实例的数据模型(data/props);数据模型修改即VNode数据修改,修改后无需其它操作可作为更新视图的依据;
Compile
vue支持template模板语法,所以内部存在编译将源代码转为抽象语法结构的树状表现形式;
3阶段:parse
,optimize
,generate
parse
用正则等方式将 template
模板中进行字符串解析,得到指令、class、style等数据,形成 AST (抽象语法树);
optimize
为静态节点添加标记,方便patch
时直接跳过这些被标记的节点的比对,实现优化;
generate
generate
是将 AST 转化成 render function 字符串的过程,得到结果是 render 的字符串以及 staticRenderFns 字符串;
Diff Patch
适配层
Virtural DOM以JS对象来描述节点树,这使其具备跨平台能力;适配层将不同平台的 API 封装在内,以同样的接口对外提供视图操作;
diff
- 对虚拟DOM树上同层的树节点进行比较,新旧节点存在/不存在,增删新旧节点;
- 若有新旧节点相同(sameVnode),则进入patchVnode(比对);
- 对虚拟节点子节点进行比较,新旧节点的子节点存在/不存在,增删新旧节点的子节点;
- 若有新旧子节点都存在,则进入updateChildren(子节点比对);
- 存在
oldStartIdx
、newStartIdx
、oldEndIdx
以及newEndIdx
;从新节点索引项开始向旧的比对,对旧节点调整位置,更新新旧索引; - 若新旧起始结束节点都不相同,则对维护的旧节点key索引映射中查找对应的新节点,找到且sameVnode则旧节点调整位置,更新新旧索引;未找到sameVnode则将新节点插入起始/结束索引,更新新旧索引;、
- 当任一起始索引贴近,则相同项比对完毕,此时对多余旧节点remove或多余新节点添加至旧节点;