前言
前面几篇文章一直都以源码分析为主,其实枯燥无味,对于新手玩家来说很不友好。这篇文章主要讲讲
Vue
的响应式系统,形式与前边的稍显 不同吧,分析为主,源码为辅,如果能达到深入浅出的效果那就更好了。
什么是响应式系统
「响应式系统」一直以来都是我认为
Vue
里最核心的几个概念之一。想深入理解Vue
,首先要掌握「响应式系统」的原理。
从一个官方的例子开始
由于 Vue 不允许动态添加根级响应式属性,所以你必须在初始化实例前声明所有根级响应式属性,哪怕只是一个空值:
var vm = new Vue({ data: { // 声明 message 为一个空值字符串 message: '' }, template: '{ { message }}'})// 之后设置 `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 } }}复制代码
运行上述代码,结果如下:
555555复制代码
在上面的代码中我们做了些什么?
- 没有在
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) },}复制代码
然而,执行上面这段代码后控制台却报错了
[Vue warn]: Avoid adding reactive properties to a Vue instance or its root $data at runtime - declare it upfront in the data option.其实,对于已经创建的实例,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老师的前端补习班」关注我的微信公众号,那么就可以第一时间收到我的最新文章。