Appearance
因为 Array 是不具备 defineProperty 属性的,所以不能像 Object 一样的被监听属性变化,然而 vue 也实现了特有的方法,去实现了监听 Array 的变化。
来看这段代码:
js
let arr = []
arr.__proto__.newPush() = function mutator (val){
console.log('数据变了') // 数据变了
this.push.call(this,val)
}
arr.newPush(1)
console.log(arr) // [1]
从这段代码我们可以看出,我们只需要修改 Array 原型上的方法,就可以知道什么时候数据发生了变化;
Vue 同样也是这样实现的,我们来看一下源码中的实现过程:
- 地址:
src/core/observer/array.js
js
import { def } from '../util/index'
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
/**
* copy一份数组的原型方法,防止污染Array的原型
*/
// 改变数组的7个方法
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function (method) {
// 缓存原生方法
const original = arrayProto[method]
/**
* 通过def给对象赋值,并设置描述符
* Object.defineProperty(obj,key,{
* value:val
* })
*/
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
// __ob__存的是Observer实例
const ob = this.__ob__
let inserted
//对数组新增元素和删除元素进行转换成响应式
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2) //args是个数组,splice(开始位置,个数,替换的元素),所以参数下标为2的是新加的元素
break
}
if (inserted) ob.observeArray(inserted) //对新增元素转换为响应式
// 通知更新
ob.dep.notify()
return result
})
})
从上面的源码中可以看出,vue 先是拷贝了一份数组对象原型上的方法,避免造成污染,然后创建了一个对象,并指定了原型,在arrayMethods上定义了 7 个方法,给这 7 个方法指定了函数,如果数据发生了变化,就转化为响应式触发更新
Vue 通过创建了一个数组拦截器,在其内重写了操作数组的方法,我们操作数组的时候,从拦截器就可以实现对于数组的观测。
- 地址:
src/core/observer/index.js
js
export class Observer {
value: any;
constructor (value: any) {
this.value = value
/**
* 给value增加一个属性'__ob__',值为该value的Observer的实例
* 这样是相当于在value上打一个补丁,避免重复操作
* 方法在util/lang.js
*/
def(value, '__ob__', this)
if (Array.isArray(value)) { // 数组逻辑
if (hasProto) { //数组是否支持"__proto__"属性 const hasProto = '__proto__' in {}
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value) //深度监测,给数组下面的子元素转换给响应式
} else {
// 操作对象的逻辑
this.walk(value)
}
//直接替换原型
function protoAugment (target, src: Object) {
/* eslint-disable no-proto */
target.__proto__ = src
/* eslint-enable no-proto */
}}
// 直接添加到对象上
function copyAugment (target: Object, src: Object, keys: Array<string>) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
def(target, key, src[key])
}
}
//对数组的成员进行observe
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
/*
* 给值value创建观察者实例
* 如果观察成功就返回新的观察者实例
* 如果已经观察过了,就返回现有的
*/
function observe (value: any, asRootData: ?boolean): Observer | void {
// 如果不是对象,就不必设置getter好和setter
if (!isObject(value) {
return
}
let ob: Observer | void
//通过‘__ob__’,判断是否有Observer实例,如果已经打过标记了,就直接拿出Observer的实例对象
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
/**
* 确保value纯对象,且没有被是否Observer过
*/
shouldObserve && //是否Observer过,通过toggleObserving来修改
(Array.isArray(value) || isPlainObject(value)) && //isPlainObject判断类型是否是object
Object.isExtensible(value) && //isExtensible判断对象是否可以扩展
!value._isVue // 避免vue实例被观察
) {
ob = new Observer(value)
}
return ob
}
上方代码先是判断了浏览器是否支持__proto__
属性,如果支持就把__proto__
属性设置为arrayMethods
;如果不支持就直接循环,把方法加到 value 上。