前面几篇文章一直都以源码分析为主,其实枯燥无味,对于新手玩家来讲很不友好。这篇文章主要讲讲Vue
的响应式系统,形式与前边的稍显
不一样吧,分析为主,源码为辅,若是能达到深刻浅出的效果那就更好了。
「响应式系统」一直以来都是我认为Vue
里最核心的几个概念之一。想深刻理解Vue
,首先要掌握「响应式系统」的原理。
因为 Vue 不容许动态添加根级响应式属性,因此你必须在初始化实例前声明全部根级响应式属性,哪怕只是一个空值:
var vm = new Vue({ data: { // 声明 message 为一个空值字符串 message: '' }, template: '<div>{{ message }}</div>' }) // 以后设置 `message` vm.message = 'Hello!'
若是你未在 data 选项中声明 message,
Vue
将警告你渲染函数正在试图访问不存在的属性。
固然,仅仅从上面这个例子咱们也只能知道,Vue
不容许动态添加根级响应式属性。这意味咱们须要将使用到的变量先在data
函数中声明。javascript
新建一个空白工程,加入如下代码html
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
?有段简单的代码能够解释这个缘由:vue
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
java
显然,输出 555
是由于触发了 this.observeB
的 getter
,从而触发了 computedGetter
,最后执行 Watcher.evalute()
然而,决定 watcher.evalute()
函数执行与否与 watcher
和 watcher.dirty
的值是否为空有关react
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
就通知对应绑定的视图进行更新。
举个例子:app
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老师的前端补习班」关注个人微信公众号,那么就能够第一时间收到个人最新文章。