前言
前面几篇文章一直都以源码分析为主,其实枯燥无味,对于新手玩家来说很不友好。这篇文章主要讲讲Vue
的响应式系统,形式与前边的稍显
不同吧,分析为主,源码为辅,如果能达到深入浅出的效果那就更好了。
什么是响应式系统
「响应式系统」一直以来都是我认为Vue
里最核心的几个概念之一。想深入理解Vue
,首先要掌握「响应式系统」的原理。
从一个官方的例子开始
由于 Vue 不允许动态添加根级响应式属性,所以你必须在初始化实例前声明所有根级响应式属性,哪怕只是一个空值:
var vm = new Vue({
data: {
// 声明 message 为一个空值字符串
message: ''
},
template: '<div>{{ message }}</div>'
})
// 之后设置 `message`
vm.message = 'Hello!'
如果你未在 data 选项中声明 message,Vue
将警告你渲染函数正在试图访问不存在的属性。
当然,仅仅从上面这个例子我们也只能知道,Vue
不允许动态添加根级响应式属性。这意味我们需要将使用到的变量先在data
函数中声明。
抛砖?引玉
新建一个空白工程,加入以下代码
export default {
name: 'JustForTest',
data () {
return {}
},
created () {
this.b = 555
console.log(this.observeB)
this.b = 666
console.log(this.observeB)
},
computed: {
observeB () {
return this.b
}
}
}
运行上述代码,结果如下:
555
555
在上面的代码中我们做了些什么?
- 没有在
data
函数中声明变量(意味着此时没有根级响应式属性) - 定义了一个
computed
属性 ——observeB
,用来返回(监听)变量b
- 使用了变量
b
同时赋值555
,打印this.observeB
- 使用了变量
b
同时赋值666
,打印this.observeB
打印结果为什么都是555
?
有段简单的代码可以解释这个原因:
function createComputedGetter (key) {
return function computedGetter () {
var watcher = this._computedWatchers && this._computedWatchers[key];
if (watcher) {
if (watcher.dirty) {
watcher.evaluate();
}
if (Dep.target) {
watcher.depend();
}
return watcher.value
}
}
}
...
Watcher.prototype.evaluate = function evaluate () {
this.value = this.get();
this.dirty = false;
};
createComputedGetter
函数返回一个闭包函数并挂载在computed
属性的getter
上,一旦触发computed
属性的getter
,
那么就会调用computedGetter
显然,输出 555
是因为触发了 this.observeB
的 getter
,从而触发了 computedGetter
,最后执行 Watcher.evalute()
然而,决定 watcher.evalute()
函数执行与否与 watcher
和 watcher.dirty
的值是否为空有关
深入了解响应式系统
Object.defineProperty
Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。
那么这个函数应该怎么使用呢?给个官方的源码当做例子:
function def (obj, key, val, enumerable) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
});
}
def(value, '__ob__', this);
getter
和 setter
上面提到了 Object.defineProperty
函数,其实这个函数有个特别的参数 —— descriptor
(属性描述符),简单看下MDN
上的定义:
对象里目前存在的属性描述符有两种主要形式:数据描述符和存取描述符。数据描述符是一个具有值的属性,该值可能是可写的,也可能不是
可写的。存取描述符是由getter-setter函数对描述的属性。描述符必须是这两种形式之一;不能同时是两者。
其中需要特别提到的就是 getter
和 setter
,在 descriptor
(属性描述符)中分别代表 get
方法和 set
方法
get
一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。当访问该属性时,该方法会被执行,方法执行时没有参数传入,
但是会传入this对象(由于继承关系,这里的this并不一定是定义该属性的对象)。
set
一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。当属性值修改时,触发执行该方法。该方法将接受唯一参数,
即该属性新的参数值。
小结
- 对象在被访问时会触发
getter
- 对象在被赋值是会触发
setter
- 利用
getter
我们可以知道哪些对象被使用了 - 利用
setter
我们可以知道哪些对象被赋值了
依赖收集
Vue
基于Object.defineProperty
函数,可以对变量进行依赖收集,从而在变量的值改变时触发视图的更新。简单点来讲就是:Vue
需要知道用到了哪些变量,不用的变量就不管,在它(变量)变化时,Vue
就通知对应绑定的视图进行更新。
举个例子:
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
var value = getter ? getter.call(obj) : val;
if (Dep.target) {
dep.depend();
if (childOb) {
childOb.dep.depend();
if (Array.isArray(value)) {
dependArray(value);
}
}
}
return value
},
set: function reactiveSetter (newVal) {
var value = getter ? getter.call(obj) : val;
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter();
}
// #7981: for accessor properties without setter
if (getter && !setter) { return }
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = !shallow && observe(newVal);
dep.notify();
}
});
这段代码做了哪些事情呢?主要有以下几点:
- 对于
obj[key]
,定义它的get
和set
函数 - 在
obj[key]
被访问时,触发get
函数,调用dep.depend
函数收集依赖 - 在
obj[key]
被赋值时,调用set
函数,调用dep.notify
函数触发视图更新
如果你再深入探究下去,那么还会发现 dep.notify
函数里还调用了 update
函数,而它恰好就是 Watcher
类所属
的方法,上面所提到的 computed
属性的计算方法也恰好也属于 Watcher
类
Observer
前面所提到的 Object.defineProperty
函数到底是在哪里被调用的呢?答案就是 initData
函数和 Observer
类。
可以归纳出一个清晰的调用逻辑:
- 初始化
data
函数,此时调用initData
函数 - 在调用
initData
函数时,执行observe
函数,这个函数执行成功后会返回一个ob
对象 -
observe
函数返回的ob
对象依赖于Observer
函数 -
Observer
分别对对象和数组做了处理,对于某一个属性,最后都要执行walk
函数 -
walk
函数遍历传入的对象的key
值,对于每个key
值对应的属性,依次调用defineReactive$$1
函数 -
defineReactive$$1
函数中执行Object.defineProperty
函数 - ...
感兴趣的可以看下主要的代码,其实逻辑跟上面描述的一样,只不过步骤比较繁琐,耐心阅读源码的话还是能看懂。
initData
function initData (vm) {
var data = vm.$options.data;
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {};
if (!isPlainObject(data)) {
data = {};
...
}
// proxy data on instance
var keys = Object.keys(data);
var props = vm.$options.props;
var methods = vm.$options.methods;
var i = keys.length;
while (i--) {
var key = keys[i];
...
if (props && hasOwn(props, key)) {
...
} else if (!isReserved(key)) {
proxy(vm, "_data", key);
}
}
// observe data
observe(data, true /* asRootData */);
}
observe
function observe (value, asRootData) {
if (!isObject(value) || value instanceof VNode) {
return
}
var ob;
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__;
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value);
}
if (asRootData && ob) {
ob.vmCount++;
}
return ob
}
Observer
var Observer = function Observer (value) {
this.value = value;
this.dep = new Dep();
this.vmCount = 0;
def(value, '__ob__', this);
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods);
} else {
copyAugment(value, arrayMethods, arrayKeys);
}
this.observeArray(value);
} else {
this.walk(value);
}
};
更加方便的定义响应式属性
文档中提到,Vue
建议在根级声明变量。通过上面的分析我们也知道,在 data
函数中
声明变量则使得变量变成「响应式」的,那么是不是所有的情况下,变量都只能在 data
函数中
事先声明呢?
$set
Vue
其实提供了一个 $set
的全局函数,通过 $set
就可以动态添加响应式属性了。
export default {
data () {
return {}
},
created () {
this.$set(this, 'b', 666)
},
}
然而,执行上面这段代码后控制台却报错了
<font color=Red> [Vue warn]: Avoid adding reactive properties to a Vue instance or its root $data at runtime - declare it upfront in the data option. </font>
其实,对于已经创建的实例,Vue
不允许动态添加根级别的响应式属性。$set
函数的执行逻辑:
- 判断实例是否是数组,如果是则将属性插入
- 判断属性是否已定义,是则赋值后返回
- 判断实例是否是
Vue
的实例或者是已经存在ob
属性(其实也是判断了添加的属性是否属于根级别的属性),是则结束函数并返回 - 执行
defineReactive$$1
,使得属性成为响应式属性 - 执行
ob.dep.notify()
,通知视图更新
相关代码:
function set (target, key, val) {
if (process.env.NODE_ENV !== 'production' &&
(isUndef(target) || isPrimitive(target))
) {
warn(("Cannot set reactive property on undefined, null, or primitive value: " + ((target))));
}
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key);
target.splice(key, 1, val);
return val
}
if (key in target && !(key in Object.prototype)) {
target[key] = val;
return val
}
var ob = (target).__ob__;
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid adding reactive properties to a Vue instance or its root $data ' +
'at runtime - declare it upfront in the data option.'
);
return val
}
if (!ob) {
target[key] = val;
return val
}
c(ob.value, key, val);
ob.dep.notify();
return val
}
数组操作
为了变量的响应式,Vue
重写了数组的操作。其中,重写的方法就有这些:
push
pop
shift
unshift
splice
sort
reverse
那么这些方法是怎么重写的呢?
首先,定义一个 arrayMethods
继承 Array
:
var arrayProto = Array.prototype;
var arrayMethods = Object.create(arrayProto);
然后,利用 object.defineProperty
,将 mutator
函数绑定在数组操作上:
def(arrayMethods, method, function mutator () { ... })
最后在调用数组方法的时候,会直接执行 mutator
函数。源码中,对这三种方法做了特别
处理:
push
unshift
splice
因为这三种方法都会增加原数组的长度。当然如果调用了这三种方法,会再调用一次 observeArray
方法(这里的逻辑就跟前面提到的一样了)
最后的最后,调用 notify
函数
核心代码:
methodsToPatch.forEach(function (method) {
// cache original method
var original = arrayProto[method];
def(arrayMethods, method, function mutator () {
var args = [], len = arguments.length;
while ( len-- ) args[ len ] = arguments[ len ];
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);
}
if (inserted) { ob.observeArray(inserted); }
// notify change
ob.dep.notify();
return result
});
});
总结
「响应式原理」借助了这三个类来实现,分别是:
Watcher
Observer
Dep
初始化阶段,利用 getter
的特点,监听到变量被访问 Observer
和 Dep
实现对变量的「依赖收集」,
赋值阶段利用 setter
的特点,监听到变量赋值,利用 Dep
通知 Watcher
,从而进行视图更新。
参考资料
扫描下方的二维码或搜索「tony老师的前端补习班」关注我的微信公众号,那么就可以第一时间收到我的最新文章。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。