Skip to content
On this page

因为 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 上。

Released under the MIT License.