序、Vue.js 简介
时至今日,Vue.js 就像曾经的 JQuery,已经成为前端工程师必备的技能。不可否认,它可以极大的提高我们的开发效率,并且很容易学习。
这就造成了一个很普遍的现象,大部分前端工程师对框架以及第三方周边插件的关注程度越来越高,甚至把自己全部的关注点都放在了框架上。
在我看来,这多少有点亚健康,不是很利于前端工程师的技术成长。因为我发现大家关注框架时,更多的是关注其用法(包括框架自身、第三方插件和 UI 组件库等)、奇淫技巧和最佳实践等。
大家在使用 Vue.js 开发项目时,不免总会遇到一些奇奇怪怪的问题,而我们是否能很快解决这些问题以及理解这些问题为什么会发生,主要取决于对 Vue.js 原理的理解是否足够深入。
Vue 是一款渐进式的 JavaScript 框架。渐进式即:假如现已有一个现成的非单页应用,可以将 Vue 作为该应用的一部分嵌入其中,带来更加丰富的交互体验。
第一篇、变化侦测
变化侦测就是侦测数据的变化,当数据变化时,要能侦测到并发出通知。
Angular 和 React 中的变化侦测属于“拉”,即状态变化时,它不知道哪个状态变了,只知道状态有可能变了,然后会发信号给框架,框架会进行暴力对比找出需要重新渲染的 DOM。在 Angular 中是脏检查的流程,在 React 中使用的是虚拟 DOM。
而 Vue 中属于“推”。状态变化时,Vue 在一定程度上知道哪些状态变了,可进行更细粒度的更新。
但粒度越细,每个状态绑定的依赖越多,依赖追踪在内存上开销就越大。因此 Vue 2.0 开始引入虚拟 DOM,调整为中等粒度,即 一个状态所绑定的依赖不再是具体的 DOM 节点,而是一个组件。状态变化后,会通知组件,组件内部再使用虚拟 DOM 对比。可大大降低依赖数量,从而降低依赖追踪所消耗的内存。
变化侦测是响应式系统的核心,Vue 2.x 中 Object 和 Array 的变化侦测采用不同的处理方式。
- Object.defineProperty 能够监听对象的变化 (Vue 2.x — 当时ES6再浏览器支持度不理想),对于数组要进行重写 Array 原型方法(push、pop…)
- ES6 的 Proxy 可监听对象和数组的变化 (Vue 3.x)
第 1 章、Object 的变化侦测
Object 通过封装 Object.defineProperty
方法得到的 defineReactive
函数来劫持属性的getter
和setter
,以此来追踪数据变化。
- 读取数据时触发
getter
来收集依赖(Watcher
)到Dep
, - 修改数据时触发
setter
,并遍历依赖列表,通知所有相关依赖(Watcher
) -
Dep
类用来收集和管理依赖,在getter
中depned
,在setter
中notify
-
Watcher
类就是收集的依赖,实际上是一个订阅器,Watcher
会将自己的实例赋值给window.target
(全局变量)上,然后去主动访问属性,触发属性的getter
,getter
中会将此Watcher
收集到Dep
中,Watcher
的update
方法会在Dep
的notify
方法中被调用,触发更新。 -
Observer
类用来将一个对象的所有属性和子属性都变成响应式的,通过递归调用defineReactive
来实现。 - 由于无法侦测对象上新增/删除属性,所以提供
$set
和 $delete
API
1.1 如何追踪变化
使用Object.defineProperty
劫持对象的get
和set
属性,封装成为defineReactive(data, key, value)
这样一个函数
const defineReactive = (data, key, value) => {
// 递归子属性, 都转换成响应式
if (typeof value === 'object') {
new Observer(value);
}
let dep = new Dep(); // 实例化 Dep(管理依赖)类
// 拦截 get 和 set
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
dep.depend(); // 收集依赖
return value;
},
set(newVal) {
if (newVal === value) {
return;
}
value = newVal;
dep.notify(); // 触发依赖更新
}
});
};
衍生实战题:如何使变量 a 同时满足:(a === 1) && (a === 2) && (a === 3)
?
let current = 0
Object.defineProperty(window, 'a', {
get() {
current ++
return current
}
})
console.log((a === 1) && (a === 2) && (a === 3)) // true
1.2 如何收集依赖
在getter
中收集依赖,在setter
中触发更新
1.3 依赖收集在哪里
首先将依赖保存在一个全局变量上(比如window.target
),然后用一个数组来保存收集的依赖。
下面代码封装为Dep
类,专门用来管理依赖,它包括addSub、removeSub、depend、notify
方法。
Dep
会在defineReactive
中实例化,在get
中depend
,在set
中notify
class Dep {
constructor() {
this.subs = [];
}
addSub(sub) {
this.subs.push(sub);
}
removeSub(sub) {
if (this.subs.length) {
const index = this.subs.indexOf(sub);
if (index > -1) {
arr.splice(index, 1)
}
}
}
// 将依赖添加到 sub 数组中收集起来
depend() {
if (window.target) {
this.addSub(window.target);
}
}
// 触发set时,循环触发收集到的依赖
notify() {
const subs = this.subs.slice();
for (let i = 0; i < subs.length; i++) {
subs[i].update();
}
}
}
1.4 依赖(Watcher)是什么
Watcher
类,它是抽象出来,用来集中处理各种情况的类。依赖收集阶段只收集这个封装好的类的实例,通知只通知它一个,再由它通知其他地方。
class Watcher {
constructor(vm, expOrFn, cb) {
this.vm = vm;
this.cb = cb; // callback 回调函数
this.getter = parsePath(expOrFn); // 将字符串转成 Keypath
this.value = this.get();
}
get() {
window.target = this; // 先把 Watcher 实例赋值给 window.target
this.value = this.getter.call(this.vm, this.vm); // 触发属性值的 getter 收集依赖
window.target = undefined;
return this.value;
}
// 在 Dep 的 notify 中调用,数据更新时,触发真正要执行的方法
update() {
const oldValue = this.value;
this.value = this.get();
this.cb.call(this.vm, this.value, oldValue);
}
}
无论是用户执行的vm.$watch('a.b.c', (nv, ov) => {})
,还是模板中用到的data
,都是通过Watcher
来通知自己是否需要发生变化。
1.5 递归侦测所有key(Observer)
上面只能侦测单个属性 ,所以封装了Observer
类来递归侦测数据所有的属性(包括子属性),将所有的属性都转换为getter/setter
的形式,追踪其变化
class Observer {
constructor(value) {
this.value = value;
if (!Array.isArray(value)) {
// 遍历所有属性
this.walk(value);
}
}
walk(value) {
const keys = Object.keys(value);
for (let i = 0; i < keys.length; i++) {
defineReactive(value, keys[i], value[keys[i]]);
}
}
}
const defineReactive = (data, key, value) => {
// 递归子属性
if (typeof value === 'object') {
new Observer(value);
}
let dep = new Dep();
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
dep.depend();
return value;
},
set(newVal) {
if (newVal === value) {
return;
}
value = newVal;
dep.notify();
}
});
};