
当我们在Vue中写下
this.message = 'Hello World'
时,视图的自动更新看似魔法,实则是精密的响应式系统在默默运转。这背后其实是三个核心类的精密协作:Observer、Dep、Watcher。本文将通过源码逐层拆解,带你看懂Vue2响应式系统的运作机制。
# 写在前面
很多人会认为数组没办法做响应式(即通过索引修改数组元素)是因为Object.defineProperty
没法对数组做劫持,其实不然。我们看下面这个例子:
const testArr = [1, 2, 3, 4]
for(let i in testArr) {
let val = testArr[i]
Object.defineProperty(testArr,i,{
enumerable: true,
configurable: true,
get() {
return val
},
set(newVal) {
if(val === newVal) {
return
}
val = newVal
console.log('set')
}
})
}
testArr[2] = 5
console.log(testArr)
// 输出
// 'set'
// [1, 2, 5, 4]
可以看到遍历数组索引绑定劫持是可以做到的,那么为什么 vue2 里修改索引下标确无法实现响应式处理呢?对原理不清楚的同学可以跟着我把响应式原理过一遍,知道的同学可以翻到最后看看我的理解。
源码分析路线图
src/core/observer/
├── index.js # 数据的初始化
├── Observer # 观察者类
|—— Dep # 依赖管理
├── Watcher # 更新触发器
你将看到:
- 如何通过
Object.defineProperty
添加观察标记 - 数组和对象如何实现响应式
- Dep 如何管理当前 Watcher
- 不同类型 Watcher 如何进行收集和更新
因为源码内容较多,我会挑些重要的步骤贴出来。
# 1.data数据的初始化——initData
在这个函数中主要做了三件事:
- 检查
data
上的属性不能和 props 、methods 上的属性相同。 - 通过
proxy
把vm._data.xxx
的访问代理到vm.xxx
上。 - 调用
observe
把data
上的数据变成响应式。
function initData(vm) {
var data = vm.$options.data;
data = vm._data = isFunction(data) ? getData(data, vm) : data || {};
//data不是对象就初始化为空对象
if (!isPlainObject(data)) {
data = {};
...
}
var keys = Object.keys(data);
var props = vm.$options.props;
var methods = vm.$options.methods;
var i = keys.length;
// 判断去重, data 上的属性不能和 props、methods 上的属性相同。
while (i--) {
var key = keys[i];
if (process.env.NODE_ENV !== 'production') {
if (methods && hasOwn(methods, key)) {
warn$2("Method \"".concat(key, "\" has already been defined as a data property."), vm);
}
}
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== 'production' &&
warn$2("The data property \"".concat(key, "\" is already declared as a prop. ") +
"Use prop default value instead.", vm);
}
// 代理数据到 vm 实例上
else if (!isReserved(key)) {
proxy(vm, "_data", key);
}
}
// 响应式操作
var ob = observe(data);
ob && ob.vmCount++;
}
第一点我这里就不讲了,比较好理解,就是检查 data 上和 props、methods 上的属性是否相同,相同就报错。这里主要讲一下第二点。通过 proxy 把 vm._data.xxx 的访问代理到 vm.xxx 上。
proxy
具体实现如下:
var sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
};
function proxy(target, sourceKey, key) {
sharedPropertyDefinition.get = function proxyGetter() {
return this[sourceKey][key]; // 实际访问 this._data.xxx
};
sharedPropertyDefinition.set = function proxySetter(val) {
this[sourceKey][key] = val; // 实际修改 this._data.xxx
};
Object.defineProperty(target, key, sharedPropertyDefinition);
}
proxy 接受三个参数target:vm实例,sourceKey:_data,key:data 上属性的key。
这段代码的主要作用是通过 Object.defineProperty 把 vm 实例的 key 属性代理到特定数据源(如_data或_props)上,当开发者访问 this.message
时,通过代理实际访问的是 this._data.message
。这也就是为什么我们把数据定义在data上,却能直接通过 this.xxx 访问到数据的原因。
# 2.创建观察者实例——Observer
首先我们在inintData中调用了observe(data)
,这个函数就是创建观察者实例。看下这个函数的具体实现
function observe(value, shallow) {
// 非对象和已经是响应式和 VNode 实例不做响应式处理
if (!isObject(value) || isRef(value) || value instanceof VNode) {
return;
}
var ob;
// 如果 value 对象上存在 __ob__ 属性,则表示已经做过观察了,直接返回 __ob__ 属性
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__;
}
else if (shouldObserve &&
!isServerRendering() &&
(isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value.__v_skip) {
// 创建观察者实例
ob = new Observer(value, shallow);
}
return ob;
}
observe
就是给非 VNode 的对象类型创建观察者实例 Observer,如果已观察成功,直接返回已有的观察者,否则创建新的实例。
Observer 类是响应式系统的核心,它通过递归遍历 data 对象,为每个属性创建一个 Dep 实例,并给每个属性添加一个 getter 和 setter。我们先来看看 Observer 类的构造函数。
var Observer = (function () {
function Observer(value, shallow) {
if (shallow === void 0) { shallow = false; }
this.value = value;
this.shallow = shallow;
// 依赖存放的数组
this.dep = new Dep();
this.vmCount = 0;
// 在 value 对象上设置 __ob__ 属性,引用了当前 Observer 实例
def(value, '__ob__', this);
if (isArray(value)) {
// var hasProto = '__proto__' in {};
// 覆盖数组默认的七个原型方法,以实现数组响应式
if (hasProto) {
protoAugment(value, arrayMethods);
}
else {
copyAugment(value, arrayMethods, arrayKeys);
}
if (!shallow) {
this.observeArray(value);
}
}
else {
this.walk(value, shallow);
}
}
Observer.prototype.walk = function (obj, shallow) {
var keys = Object.keys(obj);
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
defineReactive(obj, key, NO_INIITIAL_VALUE, undefined, shallow);
}
};
Observer.prototype.observeArray = function (items) {
for (var i = 0, l = items.length; i < l; i++) {
observe(items[i]);
}
};
return Observer;
}());
Observer
类构造函数主要做四件事:
- 在实例上定义了
dep
属性,用来存放依赖。 - 设置观测标识
def(value, '$\__ob__', this)
通过__ob__属性标记已观测对象,实现观测实例复用。 - 对于数组类型,它覆盖了数组的七个原型方法,以实现数组响应式。同时,当
shallow=false(默认)
它也调用了observeArray
方法,递归遍历数组的每一项,为对象类型创建观察者实例。 - 对于对象类型,它调用了
walk
方法,遍历对象的每一个属性,为每个属性创建一个响应式属性。
接下来详细分析下如何给数组和对象添加响应式处理
# 2.1 给数组添加响应式处理
我们先回顾下对数组处理方法的实现
if (isArray(value)) {
// var hasProto = '__proto__' in {};
// 覆盖数组默认的七个原型方法,以实现数组响应式
if (hasProto) {
protoAugment(value, arrayMethods);
}
else {
copyAugment(value, arrayMethods, arrayKeys);
}
if (!shallow) {
this.observeArray(value);
}
}
1. 先判断 value 是否有 __proto__
属性,如果有,则调用protoAugment
方法。这个方法的作用就是将元素本身的数组方法替换掉,替换成传进来的arrayMethods
。
function protoAugment(target, src) {
target.__proto__ = src;
}
2. 如果没有 __proto__
属性,则调用copyAugment
方法。这个方法的作用就是将arrayMethods上的方法代理到 value 上去。
function copyAugment(target, src, keys) {
for (var i = 0, l = keys.length; i < l; i++) {
var key = keys[i];
def(target, key, src[key]);
}
}
不论是protoAugment
还是copyAugment
,都是为了给数组元素添加上arrayMethods
方法,接下来就详细看下这个方法干了啥。
var arrayProto = Array.prototype;
var arrayMethods = Object.create(arrayProto);
var methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
];
methodsToPatch.forEach(function (method) {
// cache original method
var original = arrayProto[method];
def(arrayMethods, method, function mutator() {
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
var result = original.apply(this, args);
var ob = this.__ob__;
var inserted;
switch (method) {
case 'push':
case 'unshift':
inserted = args;
break;
case 'splice':
inserted = args.slice(2);
break;
}
if (inserted)
ob.observeArray(inserted);
// notify change
if (process.env.NODE_ENV !== 'production') {
ob.dep.notify({
type: "array mutation" /* TriggerOpTypes.ARRAY_MUTATION */,
target: this,
key: method
});
}
else {
ob.dep.notify();
}
return result;
});
});
var arrayKeys = Object.getOwnPropertyNames(arrayMethods);
首先定义了
arrayMethods
为Object.create(arrayProto)
,也就是继承了数组的原型链定义了
methodsToPatch
,这个数组就是我们要拦截的数组方法,如:push、pop、shift、unshift、splice、sort、reverse等。遍历
methodsToPatch
,为数组的每一个方法添加代理。原理还是使用Object.defineProperty
,但是这里我们使用def
,这个方法就是对Object.defineProperty
的封装。所以当我们使用数组方法时,会调用到我们添加的代理方法。这个方法里主要做了三件事:- 调用原数组方法,获取到返回值。
- 根据数组方法,获取到插入的元素,并调用
ob.observeArray(inserted)
,对插入的元素进行响应式处理。 - 通知依赖,调用
ob.dep.notify()
,通知依赖更新。
最后定义
arrayKeys
,方便后续调用copyAugment
时,将arrayMethods
上的方法代理到value上去。
# 2.2 给对象添加响应式处理
我们先回顾下对对象的处理方法。在observer类构造函数中,调用了 walk
方法,这个方法遍历对象的每一个属性,对每个属性调用了defineReactive方法。
defineReactive
的作用就是利用 Object.defineProperty
对数据的读写进行劫持,给属性 key 添加 getter 和 setter ,用于依赖收集和通知更新。如果传进来的值依旧是一个对象,则递归调用 observe
方法,保证子属性都能变成响应式。我们来看看 defineReactive
方法的实现。
function defineReactive(obj, key, val, customSetter, shallow) {
// 实例化 dep,一个 key 一个 dep
var dep = new Dep();
...
// 递归调用,处理 val 的值为对象的情况
var childOb = !shallow && observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
var value = getter ? getter.call(obj) : val;
// Dep.target 是 Dep的一个静态属性,保存的是当前的 Watcher 实例。
// 在 new Watcher 实例化的时候(computed 除外,因为它懒执行)会触发读取造作,被劫持运行这个 get 函数,进行依赖收集。
// 在实例化 Watcher 最后,Dep.target 设置为 null,避免重复收集。
if (Dep.target) {
...
// 依赖收集,在 dep 中添加 watcher,也在 watcher 中添加 dep
dep.depend();
if (childOb) {
childOb.dep.depend();
if (isArray(value)) {
// 处理数组内还是对象的情况
dependArray(value);
}
}
}
return isRef(value) ? value.value : value;
},
set: function reactiveSetter(newVal) {
// 旧的 obj[key]
var value = getter ? getter.call(obj) : val;
// 如果新旧值一样,则直接 return,无需更新
if (!hasChanged(value, newVal)) {
return;
}
...
// 对新值进行观察,让新值也是响应式的
childOb = !shallow && observe(newVal);
...
// 依赖通知更新
dep.notify();
}
});
return dep;
}
defineReactive
主要是做了以下三个事情
- 实例化了一个
Dep
,用来收集依赖。 - 判断传入的值是否是一个对象,如果是,则递归调用
observe
方法,对对象的属性进行响应式处理。 - 为对象添加
getter
和setter
,用于依赖收集和通知更新。- getter:当读取属性时,会触发
getter
,进行依赖收集。它首先会判断Dep.target
,Dep.target
是Dep
的一个静态属性,保存的是当前的 Watcher 实例。在new Watcher
实例化的时候(computed 除外,因为它懒执行),会触发读取操作,被劫持运行这个 get 函数,进行依赖收集。在实例化Watcher
最后,Dep.target
设置为 null,避免重复收集。dep.depend
函数会向当前Watcher
实例中添加 dep,同时也会为 dep 添加Watcher
实例,后续会贴出这个函数的实现。 - setter:当值发生变化时,会触发
setter
,进行依赖通知更新。它首先会判断hasChanged
,判断新值和旧值是否相等,如果相等,则直接返回,无需更新。同时对新值做响应式处理。最后通知依赖更新。
- getter:当读取属性时,会触发
# 3.依赖管理——Dep
在创建 Observer
观察者的时候,就有频繁用到 Dep 构造函数,它是一个管理器,用来管理依赖。我们先来看看构造函数的实现。
var Dep = (function () {
function Dep() {
this.id = uid$2++;
this.subs = [];
}
// 添加订阅,把 Watcher实例,保存到 subs中
Dep.prototype.addSub = function (sub) {
this.subs.push(sub);
};
// 移除订阅,把 Watcher实例,从 subs 中移除
Dep.prototype.removeSub = function (sub) {
remove$2(this.subs, sub);
};
// 向 Watcher 中添加 dep
Dep.prototype.depend = function (info) {
if (Dep.target) {
Dep.target.addDep(this);
if (process.env.NODE_ENV !== 'production' && info && Dep.target.onTrack) {
Dep.target.onTrack(__assign({ effect: Dep.target }, info));
}
}
};
// 通知更新
Dep.prototype.notify = function (info) {
var subs = this.subs.slice();
if (process.env.NODE_ENV !== 'production' && !config.async) {
subs.sort(function (a, b) { return a.id - b.id; });
}
// 遍历 dep 中存储的 watcher,执行 watcher.update()
for (var i = 0, l = subs.length; i < l; i++) {
if (process.env.NODE_ENV !== 'production' && info) {
var sub = subs[i];
sub.onTrigger && sub.onTrigger(__assign({ effect: subs[i] }, info));
}
subs[i].update();
}
};
return Dep;
}());
Dep 构造函数主要用来管理依赖。它主要定义了一个 subs
属性并提供了四个方法:addSub
、removeSub
、depend
和 notify
。
subs
: 用来存放依赖,也就是 Watcher 实例。addSub
:添加依赖,把 Watcher 实例添加到subs
中。这个方法调用是在 Watcher 构造函数中的addDep方法中执行,后续讲 Watcher 的时候会看到removeSub
:移除依赖,把 Watcher 实例从subs
中移除。depend
:双向添加,depend
函数的实现里会调用addSub
方法把 Watcher 实例添加到subs
中。同时也会为 Watcher 实例添加 dep。notify
:通知更新,遍历subs
中的 Watcher 实例,执行update
方法进行视图更新。
# 4.调度中枢——Watcher
Watcher 在 Vue 的响应式系统中扮演重要角色,负责依赖收集和更新触发。 首先,Watcher 分为三种类型:渲染Watcher、计算属性Watcher 和 监听Watcher。渲染 Watcher 负责组件的渲染,计算属性 Watcher 处理 computed 属性,监听 Watcher 是用户自定义的watch选项。
类型 | 创建场景 | 功能特点 |
---|---|---|
渲染 Watcher | 组件挂载时创建 | 负责触发组件重新渲染 |
计算属性 Watcher | computed 属性创建时创建 | 惰性求值,缓存计算结果 |
监听 Watcher | watch 选项创建 | 监听属性变化,触发回调 |
除了功能上的区别,这三种 watcher 也有固定的执行顺序,分别是:computed-render -> normal-watcher -> render-watcher。这样就能尽可能的保证,在更新组件视图的时候,computed
属性已经是最新值了,如果 render-watcher 排在 computed-render 前面,就会导致页面更新的时候 computed 值为旧数据。
因为 Watcher 类的实现比较复杂,这里我就挑重点贴下代码。
var Watcher = (function () {
function Watcher(vm, expOrFn, cb, options, isRenderWatcher) {
...
if (options) {
...
this.lazy = !!options.lazy;
this.sync = !!options.sync;
...
}
else {
this.deep = this.user = this.lazy = this.sync = false;
}
this.active = true;
this.deps = [];
...
// parse expression for getter
if (isFunction(expOrFn)) {
this.getter = expOrFn;
}
else {
this.getter = parsePath(expOrFn);
if (!this.getter) {
this.getter = noop;
...
}
}
this.value = this.lazy ? undefined : this.get();
}
Watcher.prototype.get = function () {
pushTarget(this);
var value;
var vm = this.vm;
try {
value = this.getter.call(vm, vm);
}
catch (e) {
...
}
finally {
if (this.deep) {
traverse(value);
}
popTarget();
this.cleanupDeps();
}
return value;
};
Watcher.prototype.addDep = function (dep) {
var id = dep.id;
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id);
this.newDeps.push(dep);
if (!this.depIds.has(id)) {
dep.addSub(this);
}
}
};
Watcher.prototype.update = function () {
if (this.lazy) {
this.dirty = true;
}
else if (this.sync) {
this.run();
}
else {
queueWatcher(this);
}
};
Watcher.prototype.run = function () {
if (this.active) {
var value = this.get();
if (value !== this.value ||isObject(value) ||this.deep) {
var oldValue = this.value;
this.value = value;
if (this.user) {
var info = "callback for watcher \"".concat(this.expression, "\"");
invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info);
}
else {
this.cb.call(this.vm, value, oldValue);
}
}
}
};
Watcher.prototype.evaluate = function () {
this.value = this.get();
this.dirty = false;
};
return Watcher;
}());
我们来剖析下 Watcher 构造函数都干了些啥:
# 4.1 Watcher 函数
在执行new Watcher
时,会调用 Watcher
函数,传入参数。上面我们有提到过 Watcher 有三种类型,那么在实例化的时候,不同类型的 Watcher 也会传入不同的参数,后续做响应式处理的操作也会因此而不同。以下的 noop
是个空函数。
render watcher: 这里注意到第五个参数为 true。说明
isRenderWatcher=true
。wacherOptions
具体的值我就不贴出来了,里面没有lazy 或者 sync 属性new Watcher(vm, updateComponent, noop, watcherOptions, true)
computed watcher: 注意到
lazy: true
。new Watcher(vm, getter || noop, noop, { lazy: true })
normal watcher: 这里的参数一般都是设置 immediate 和 deep 属性。
// expOrFn: 就是key,自定义监听的那个变量 // cb: handler 回调函数 // options: 自定义选项,一般也不会传入lazy值 var watcher = new Watcher(vm, expOrFn, cb, options); if (options.immediate) { var info = "callback for immediate watcher \"".concat(watcher.expression, "\""); pushTarget(); invokeWithErrorHandling(cb, vm, [watcher.value], vm, info); popTarget(); }
在实例化 Watcher 时,下面这行代码非常关键,它决定了依赖收集的触发时机。this.get()
就是进行依赖收集的函数。根据上面传进来的 lazy 参数可以判断 computed watcher 是延迟执行收集依赖。其它两个都是立即执行收集依赖
this.value = this.lazy ? undefined : this.get()
接下来看看 watcher.get()
是如何工作进行依赖收集的。
# 4.2 Watcher.get()
Watcher.prototype.get = function () {
pushTarget(this);
var value;
var vm = this.vm;
try {
value = this.getter.call(vm, vm);
}
catch (e) {
...
}
finally {
if (this.deep) {
traverse(value);
}
popTarget();
this.cleanupDeps();
}
return value;
};
- 首先调用
pushTarget(this)
,这一步就是设置Dep.target
为当前 Watcher 实例。Dep.target
的作用大家可以回顾下2.2 章节。 - 执行
this.getter.call(vm, vm)
,这里就是调用了getter
函数,也就是我们传进来的expOrFn
。对于不同 Watcher 类型,getter 函数也不同。- render watcher:
getter
函数为updateComponent
。这个函数主要作用就是虚拟 DOM 生成和 Diff 比对。 - computed watcher:
getter
函数就是我们自己定义在computed
里的函数 - normal watcher:
getter
函数其实就是我们监听的那个变量的 get 函数
- render watcher:
- 判断 deep 属性,递归去访问 value,触发它所有子项的 getter 。
- 执行
popTarget()
,将Dep.target
设置为null
。
上述的 getter 在执行过程会触发我们之前利用 Object.defineProperty
定义的 get 函数。我们再来回顾下 get 函数是如何进行依赖收集的。
get: function reactiveGetter() {
var value = getter ? getter.call(obj) : val;
if (Dep.target) {
...
// 依赖收集,在 dep 中添加 watcher,也在 watcher 中添加 dep
dep.depend();
if (childOb) {
childOb.dep.depend();
if (isArray(value)) {
// 处理数组内还是对象的情况
dependArray(value);
}
}
}
return isRef(value) ? value.value : value;
},
// 顺便补上Dep.depend函数,省的大家翻来翻去
Dep.prototype.depend = function (info) {
if (Dep.target) {
Dep.target.addDep(this);
if (process.env.NODE_ENV !== 'production' && info && Dep.target.onTrack) {
Dep.target.onTrack(__assign({ effect: Dep.target }, info));
}
}
};
这里就是先执行 dep.depend()
,然后这个函数里调用的 Dep.target.addDep(this)
就是执行 Watcher 的 addDep 方法将当前 Wacher 实例添加到 dep.subs
中。
# 4.3 Watcher.update()
update 函数就是执行视图更新的入口。再讲 update 函数之前,我们先来梳理下什么时候执行 update 函数。回顾下2.2 章节。里面定义了 setter 函数,这个函数就是当数据变化时执行的。里面有这么一行代码 dep.notify()
。而 notify 函数里执行了 subs[i].update()
。 subs 就是存储 watcher 实例的数组,所以在这里遍历执行了所有 watcher 实例的 update 函数。
Watcher.prototype.update = function () {
if (this.lazy) {
this.dirty = true;
}
else if (this.sync) {
this.run();
}
else {
queueWatcher(this);
}
};
update 函数里对于不同的情况,执行了不同的操作。
- 当
this.lazy = true
时, 这种情况是针对compute watcher 的。执行this.dirty = true
标记脏值,这样做就把更新触发时机延迟到了访问时。访问时会执行computedGetter
, 这个函数里面会检查ditry 标记,如果为 true,就会执行watcher.evaluate
重新收集依赖并更新值。
function computedGetter() {
var watcher = this._computedWatchers && this._computedWatchers[key];
if (watcher) {
if (watcher.dirty) { // 脏检查
watcher.evaluate();// 重新计算
}
if (Dep.target) {
...
watcher.depend(); // 建立渲染依赖
}
return watcher.value;
}
};
- 当
this.sync = true
时, 这种情况基本上只有normal watcher自定义了这个属性才会执行这个分支。立即执行this.run()
, 里面会执行回调函数cb。 - 最后一种情况就是默认的,执行
queueWatcher(this)
,也就是将当前 watcher 实例添加到queue
中。这是因为 Vue 的更新机制是异步的,所以这里会先将 watcher 实例添加到队列中,等下一个事件循环时,再执行渲染操作。
为什么compute watcher 要延迟更新呢?包括之前也是延迟收集依赖?
- 性能优化:避免不必要的计算:一个 compute watcher 里面可能有多个计算属性,而且有逻辑运算。如果初始化时就立即执行,可能会收集到永远都不会被访问到的计算依赖,计算了永远都用不到的值。
- 缓存机制:与惰性求值协同工作:计算属性的核心特性是缓存,当依赖项发生变化时,会标记 dirty: true。下次访问时,如果有脏值标记,就重新计算,没有就直接返回缓存值。这样做还可以将多次依赖值得变化合并为一次计算。
# 5.总结整个流程
- 首先在初始化时,遍历递归定义在 data 里的属性加上 Observer 观察者,对数组做原型链劫持操作,对对象使用
Object.defineProperty
进行劫持,将数据变成响应式的。 - 每个响应式数据都实例化了一个 Dep 实例,用来收集依赖。
- 当初始化模板、compute 选项和 watch 选项时,会实例化 Watcher 实例,分别是render watcher、compute watcher 和 normal watcher。其中render watcher 和 normal watcher 会立即执行收集依赖,而 compute watcher 则是延迟收集依赖。收集依赖主要是通过触发 getter 函数,将 Watchr 实例添加到 dep 里的数组中。
- 当值发生变更时,会触发 setter 函数,执行
Dep.notify()
,通知 dep 数组里所有的 Watcher 实例更新。其中render watcher 和 normal watcher 会立即将 Watcher 实例添加到队列中,而 compute watcher 则是标记脏值,等待下次访问时重新计算。
# 写在最后
了解完整个响应式原理,相比大家也清楚了为什么 Vue2 通过修改索引下标没法实现响应式处理(忘记的同学可以再回顾下2.2 章节),是因为它根本就没做、没做、没做。
那么为什么不做呢?我认为是为了性能,就是给遍历数组的每一项加上 Observer 的性价比高不高,因为这里是要消耗时间和内存的。
一个对象比较大的话可能也就几百上千个属性,但是一个数组比较大的话,起码就是几万几十万个值了。这性能上的开销绝对不能忽略。
还需要考虑到一种情况,就是数组的赋值操作。举个例子:
const arr = [] arr[100000] = 10
这种情况下,数组长度就是 100001,前面有 100000 个空位。这种时候做不做响应式处理呢?做了那么就会浪费内存,还要消耗大量的时间,性价比是极低的。