对响应式数据对象以及它的 getter 和 setter 部分做了了解,但是对于一些特殊情况是需要注意的
对象添加属性
使用Object.defineProperty实现响应式的对象,当去给这个对象添加一个新的属性时是不能够触发它的setter的。比如
var vm = new Vue({data:{a:1}})// vm.b 是非响应的vm.b = 2
Vue为了解决添加新属性的问题,定义了全局API方法Vue.set
定义在src/core/global-api/index.js中
Vue.set = set
set方法的实现定义在src/core/observer/index.js中
/*** Set a property on an object. Adds the new property and* triggers change notification if the property doesn't* already exist.*/export function set (target: Array<any> | Object, key: any, val: any): any {if (process.env.NODE_ENV !== 'production' &&(isUndef(target) || isPrimitive(target))) {warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)}// 数组if (Array.isArray(target) && isValidArrayIndex(key)) {target.length = Math.max(target.length, key)target.splice(key, 1, val)return val}// key若已经存在于target中则直接赋值返回,因为这样的变化是可以观测到的if (key in target && !(key in Object.prototype)) {target[key] = valreturn val}const ob = (target: any).__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}// 不存在则说明target不是一个响应式的对象,直接赋值返回if (!ob) {target[key] = valreturn val}// 把新添加的属性变成响应式对象defineReactive(ob.value, key, val)// 手动触发依赖通知ob.dep.notify()return val}
defineReactive方法
/*** Define a reactive property on an Object.*/export function defineReactive (obj: Object,key: string,val: any,customSetter?: ?Function,shallow?: boolean) {// ...let childOb = !shallow && observe(val)Object.defineProperty(obj, key, {enumerable: true,configurable: true,get: function reactiveGetter () {const value = getter ? getter.call(obj) : valif (Dep.target) {dep.depend()if (childOb) {childOb.dep.depend() // 收集依赖if (Array.isArray(value)) {dependArray(value)}}}return value},// ...})}
在 getter 过程中判断了 childOb,并调用了 childOb.dep.depend() 收集了依赖,这就是为什么执行 Vue.set 的时候通过 ob.dep.notify() 能够通知到 watcher,从而让添加新的属性到对象也可以检测到变化
如果 value 是个数组,那么就通过 dependArray 把数组每个元素也去做依赖收集
数组
Vue不能检测到以下变动的数组
- 当利用索引值直接设置一个项时 例如vm.items[indexOfItem] = newValue
- 当修改数组长度时 例如vm.items.length = newLength
对于第一种情况可以使用 Vue.set(example1.items, indexOfItem, newValue)
对于第二种情况可以使用 vm.item.splice(newLength)
之前分析中在通过observe方法去观察对象时会实例化Observer,在它的构造函数中是专门对数组做了处理
定义在src/core/observer/index.js中
/*** Observer class that is attached to each observed* object. Once attached, the observer converts the target* object's property keys into getter/setters that* collect dependencies and dispatch updates.*/export class Observer {value: any;dep: Dep;vmCount: number; // number of vms that have this object as root $dataconstructor (value: any) {this.value = valuethis.dep = new Dep()this.vmCount = 0def(value, '__ob__', this)if (Array.isArray(value)) {// hasProto 判断对象中是否存在__proto__if (hasProto) {protoAugment(value, arrayMethods)} else {copyAugment(value, arrayMethods, arrayKeys)}this.observeArray(value)} else {this.walk(value)}}// ...}
protoAugment和copyAugment函数的定义
/*** Augment a target Object or Array by intercepting* the prototype chain using __proto__* 大部分现代浏览器都会走到protoAugment* 实际上就把value的原型指向了arrayMethods*/function protoAugment (target, src: Object) {/* eslint-disable no-proto */target.__proto__ = src/* eslint-enable no-proto */}/*** Augment a target Object or Array by defining* hidden properties.*//* istanbul ignore next */function copyAugment (target: Object, src: Object, keys: Array<string>) {for (let i = 0, l = keys.length; i < l; i++) {const key = keys[i]// 通过Object.defineProperty去定义它自身的属性值def(target, key, src[key])}}
arrayMethods定义在src/core/observer/array.js中
/** not type checking this file because flow doesn't play well with* dynamically accessing methods on Array prototype*/import { def } from '../util/index'const arrayProto = Array.prototype // 继承Arrayexport const arrayMethods = Object.create(arrayProto)// 数组中所有能改变数组自身的方法const methodsToPatch = ['push','pop','shift','unshift','splice','sort','reverse']/*** Intercept mutating methods and emit events* 对数组中所有能改变数组自身的方法 进行重写*/methodsToPatch.forEach(function (method) {// cache original methodconst original = arrayProto[method]def(arrayMethods, method, function mutator (...args) {// 先执行方法本身原有的逻辑const result = original.apply(this, args)const ob = this.__ob__// 对能增加数组长度的3个方法 push unshift splice做判断// 获取插入的值let insertedswitch (method) {case 'push':case 'unshift':inserted = argsbreakcase 'splice':inserted = args.slice(2)break}// 把新添加的值变成一个响应式对象if (inserted) ob.observeArray(inserted)// notify change// 手动触发依赖通知ob.dep.notify()return result})})
很好的解释了调用vm.items.splice(newLength)方法可以检测到变化
