Vue源码解读:02变化侦测篇

1,273 阅读5分钟

Vue源码解读:02变化侦测篇

目录

image.png

第一节·先看目录结构

本篇研究的代码位置:src/core/observer
├─observe                   # 实现变化观测的代码
      ├─array.js             #实现数组类型数据变化观测的代码
      ├─dep.js           #实现依赖管理器的代码
      ├─index.js             #实现变化侦测的入口文件,以及实现object数据可观测的代码
      ├─scheduler.js      #实现对watcher管理的调度代码
      ├─traverse.js       #实现数据深层次观测的代码
      ├─watcher.js    #实现为依赖项创建watcher实例,处理依赖收集与通知的代码

第二节·变化侦测简述

observer,中文意为观察者,24中设计模式中有一种叫观察者模式,别名发布-订阅模式,我简单的联想到vue中实现变化侦测使用到了观察者模式。

变化侦测,在前端三大框架React,Angular,Vue中均有所体现,React是通过对比虚拟dom实现的,Angular是通过脏检查实现的。本文就是通过研究observer中的代码,学习Vue的变化侦测机制。

变化侦测简单理解就是对数据变化的观测。变化侦测只是一种手段,根据侦测结果去实现自己的目的才是重点。但这个手段,是实现数据驱动,实现数据双向绑定,实现数据响应式的关键之关键。

第三节·Vue中的变化侦测机制。

1.前言

实现变化侦测的关键思路就是进行数据劫持,数据劫持的目的也很明显,就是拦截重组重新包装数据,使数据变成可观测数据。这个可观测数据,要具备数据的读写操作可知,比如谁读写,什么时候读写,读写了什么这些特点。劫持的目的就是重包装,重包装的目的就是使数据的读写变得可知,实现可知的代码,就是重包装的要添加的内容。当数据的读写变得可知了,那么根据读写的变化内容,然后去主动通知视图更新,从而省去手动操作dom更新视图的工作,这就实现了操作dom变得自动化,这就是数据驱动了啊。数据驱动下实现根据数据变化主动更新视图,取代了手动操作dom更新视图,这是一个视图更新或说操作dom操作自动化的过程。而通过dom监听,根据dom变化情况,实现数据状态更新。这一来,所谓的数据双向绑定,数据响应式,就实现了啊。

变化侦测机制的应用成果包含了数据驱动的实现,数据响应式的实现等,前文也提及了变化侦测机制的重要性,这也是本文取名变化侦测篇的理由之一。

嗯,这都是文字,下来来看下vue2中的代码是如何实现变化侦测的。

2.object类数据的变化观测

①先看源码

//源码位置: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.
 *
 * 附加后于每个观察对象的观察者类。
 * 附加后,观察者将目标对象的属性键转换为getter/setter,
 * 后者收集依赖项并分派更新。
 */
export class Observer {
    value: any;
    dep: Dep;
    vmCount: number; // number of vms that have this object as root $data

    constructor(value: any) {
        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 {//使非数组,即object数据变得可观测的入口
            this.walk(value)//遍历object数据所有属性并将它们转换为getter/setter。
        }
    }
}

②思路分析

从变化侦测的入口文件index.js,上面贴出的代码就可知,vue2中变化观测机制,将数据分为两类,一类是object,另一类是array数据。实现object数据可观测的主要代码就放在index.js文件中,而实现array的可观测主要代码则放在observer目录下的array.js文件中。

进入实现object数据可观测的主题,就实现而言并不难,vue中做的更多工作是更优雅的实现。它是通过Object.defineProperty()来实现的,defineProperty()是Object的一个方法,通过defineProperty()重新定义object数据,被重新定义之后的object数据就会带有get()和set(),并使用get()和set()对每一个属性的读写进行拦截,这里的拦截就是劫持,而劫持的目的,就是要在数据被读写的时候得到通知。这里有一个关键点,就是通过Object.defineProperty()重定义过后的数据,在该数据被读取时,必然会经过get(),而数据被修改时必然会经过set(),这是Object.defineProperty()带来的特性,也是选择使用defineProperty()进行数据劫持的原理,也是实现object数据可观测的关键。

使用defineProperty()重定义数据后,就实现了object数据的可观测。因为该数据被读写时,必然经过get()和set(),这样就知道了数据什么时候被读取了,什么时候被修改了。如此,数据就是可观测的数据了。看下源码是咋写的,因为代码太长,省略了许多代码。

// 源码位置:src/core/observer/index.js
/**
 * Define a reactive property on an Object.
 *
 *将对象的属性定义为响应式(可观测)的。
 */
export function defineReactive (
    obj: Object,
    key: string,
    val: any,
    customSetter?: ?Function,
    shallow?: boolean
) {

    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter () {
            //...数据被读取时,你想要做的事
            console.log('数据被读取了,我要记录下来');
            return value
        },
        set: function reactiveSetter (newVal) {
            //..数据被修改时,你想要做的事
            console.log('数据被修改了,我要通知视图更新');
            val = newVal
        }
    })
}

③源码中实现object数据的关键路径

Observer 类中,调用walk()函数,walk()函数中调用defineReactive(),defineReactive函数中,使用Object.defineProperty()重定义数据,重定义之后的数据就是可观测数据,至此就实现object类型数据的可观测了。

④依赖机制

到了这里,我们知道通过Object.defineProperty(),我们可以实现数据的可观测。回过头来说,观测数据并不是我们的目标,我们的目标是通过数据观测,去做一些事情,例如实现数据驱动视图等。以驱动视图为例,当观测到某个数据变化了,要去更新视图,那么整个视图那么大,更新谁呢?怎么找对应关系,这里就引出了依赖机制。依赖表示访问数据的一方和数据本身的一种关系。在源码中专门有一个dep.js文件,就是放置依赖的文件。有了依赖记录,就可以知道数据和他的使用者们了,也就知道该通知谁更新了。

// 源码位置:src/core/observer/index.js
Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
        if (Dep.target) {
            dep.depend()//为数据和其访问者,添加(生成)依赖
        }
        return value
    },
    set: function reactiveSetter (newVal) {
        //...其他代码
        dep.notify()//通知依赖更新
    }
})

从上面贴出的源码,可以知道,依赖是在get()被触发的时候产生的,因为访问数据必然经过get()。用一个词来表示依赖产生的过程,叫依赖收集。为每一个访问该数据的访问者,生成一个依赖,有了依赖记录,就可以知道都有谁访问了该数据。而一个数据是可以有多个访问者的,这就是一对多的关系了,为了更好地管理这些依赖,vue中创建了一个依赖管理器。

依赖收集是在get()中,而依赖更新则是在set(),此前说过可观测数据中,每一次数据的更改,都会经过set(),所以这是通知依赖更新的好时机,依赖接到更新通知后,进而通知依赖关系的使用方,比如视图。这样一步步就做到了数据变化,被观测到后通知到依赖,进而通知依赖关系方,该更新就更新了。

3.array的变化观测

object数据变化观测的实现利用了Object.defineProperty(),显然array数据是不支持这个的,defineProperty()是Object的方法。那么array数据如何观测数据呢?

在vue的开发中,我们把数据定义在data()函数里面,例如下面这样。

data(){
    return{
        arr:[]//在一个对象里面,定义一个数组
    }
}

也就是array数据的父级是object类型的数据,所以array也可以有一个对应的get(),那么array的依赖收集,也就可以取巧的和object类型的数据一样在get()中收集。有人可能会觉得那array也可以set()方法,我认为有是可以有的,但是没有意义啊。因为array数据的值改变和object是不一样的,从平时开发就知道了,我们操作数组通常是通过js提供的方法,例如push(),pop(),shift(),unshift(),splice(),sort(),reverse()等。array数据的值改变不再是必然经过set(),所以array数据的依赖更新通知,只能另寻途径。现在要求观测array数据的值改变,而array数据的值改变,我们通常是通过调用js提供的操作数据的方法进行的,那么有了,重写这些个操作数组的方法。看下源码,vue中就是通过重写可操作数组的且改变数组自身的几个方法实现的变化观测。瞅一下源码。

// 源码位置 src/core/observer/array.js

const methodsToPatch = [
    'push',
    'pop',
    'shift',
    'unshift',
    'splice',
    'sort',
    'reverse'
]

/**
 * Intercept mutating methods and emit events
 *
 * 拦截重写后的方法并触发事件(事件就是更新依赖)
 */
methodsToPatch.forEach(function (method) {
    // cache original method
    // 缓存拷贝原来的方法,使重写后的方法具备原来的功能
    const original = arrayProto[method]
    def(arrayMethods, method, function mutator (...args) {
        const result = original.apply(this, args)
        const ob = this.__ob__
        let inserted
        switch (method) {
            case 'push':
            case 'unshift':
                inserted = args
                break
            case 'splice':
                inserted = args.slice(2)
                break
        }
        if (inserted) ob.observeArray(inserted)
        // notify change
        // 通知依赖更新
        ob.dep.notify()
        return result
    })
})

所以,在vue中,array数据值变化的观测,是通过重写会改变数组自身的7个方法实现的。array的依赖更新即是在这时候。

4.深度观测

什么是深度侦测。通过实际开发经验,我们只知道,不仅数据本身的改变会被观测到,数据嵌套的子元素也是可观测的,也是响应式的。实现数据嵌套的深层次元素的可观测,就是深度侦测了。


/**
 * 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.
 *
 * 附加后于每个观察对象的观察者类。
 * 附加后,观察者将目标对象的属性键转换为getter/setter,
 * 后者收集依赖项并分派更新。
 */
export class Observer {
    value: any;
    dep: Dep;
    vmCount: number; // number of vms that have this object as root $data

    constructor (value: any) {
        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 {//使非数组,即object数据变得可观测的入口
            this.walk(value)//遍历object数据所有属性并将它们转换为getter/setter。
        }
    }


    /**
     * Observe a list of Array items.
     *
     * 观测数组的子项列表。
     * 遍历数组,将数组元素通过observe()转换为响应式可观测的
     */
    observeArray (items: Array<any>) {
        for (let i = 0, l = items.length; i < l; i++) {
            observe(items[i])
        }
    }
}

/**
 * Define a reactive property on an Object.
 *
 *将对象的属性定义为响应式(可观测)的。
 */
export function defineReactive (
    obj: Object,
    key: string,
    val: any,
    customSetter?: ?Function,
    shallow?: boolean
) {
    const dep = new Dep()//创建一个依赖管理器实例

    const property = Object.getOwnPropertyDescriptor(obj, key)
    if (property && property.configurable === false) {
        return
    }

    // cater for pre-defined getter/setters
    const getter = property && property.get
    const setter = property && property.set
    if ((!getter || setter) && arguments.length === 2) {
        val = obj[key]
    }

    let childOb = !shallow && observe(val)//
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter () {
            const 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) {
            const 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()
        }
    })
}


/**
 * Attempt to create an observer instance for a value,
 * returns the new observer if successfully observed,
 * or the existing observer if the value already has one.
 *
 * 尝试为一个值创建一个观察者实例,如果成功观察到,
 * 则返回新的观察者,如果该值已经存在,则返回现有的观察者。
 */
export function observe (value: any, asRootData: ?boolean): Observer | void {
    if (!isObject(value) || value instanceof VNode) {
        return
    }
    let ob: Observer | void
    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
}

从上面的源码可以看到,源码中是通过observeArray ()遍历数组,observeArray ()内部调用observe()函数,observe函数返回一个观察者实例。简单地说array数据是通过observeArray ()遍历数组,实现的深度观测。

而object的深层次递归路径:Observer类调用walk()方法,walk()方法内部调用defineReactive()方法,defineReactive()方法内部,调用observe()函数,observe()函数内部new Observer(value)实例。有一点绕,简单那说就是通过递归实现的object类数据的深层次可观测。

5.了解一下Watcher

前面说到数据变成可观测数据之后,当数据变化之后,就会通知视图更新,而通过依赖我们可以知道具体上该通知谁,实际上真正通知视图的是watcher实例。在源码中,依赖管理器会为每一个依赖创建一个对应的watcher实例,或者说watcher实例是依赖的一部分,而watcher则代表依赖去做一些工作,例如调用update()方法去通知视图更新。

/**
 * A dep is an observable that can have multiple
 * directives subscribing to it.
 *
 * dep是一个可观察的,支持多个指令订阅它的对象。
 */
export default class Dep {
    static target: ?Watcher;
    id: number;
    subs: Array<Watcher>;

    constructor () {
        this.id = uid++
        this.subs = []
    }

    addSub (sub: Watcher) {//依赖管理器中,添加Watcher实例的方法
        this.subs.push(sub)
    }

    //...省略其他代码

    //通知函数
    notify () {
        // stabilize the subscriber list first
        const subs = this.subs.slice()
        if (process.env.NODE_ENV !== 'production' && !config.async) {
            // subs aren't sorted in scheduler if not running async
            // we need to sort them now to make sure they fire in correct
            // order
            subs.sort((a, b) => a.id - b.id)
        }
        for (let i = 0, l = subs.length; i < l; i++) {
            subs[i].update()//subs[i]即是依赖对应的watcher实例,它调用update方法去通知视图更新
        }
    }
}

6.Vue.set和Vue.delete

到这里,我们知道了,object数据通过Object.defineProperty,array数据通过拦截重写可改变自身的7个操作方法,可以实现数据的可观测。但是这个可观测是有缺点的,比如array数据,要是通过下标操作数组,或通过修改数组长度清空数组,是无法观测到数据变化的。通过Object.defineProperty()实现的object数据观测原理,只有当访问数据和设置值才能会被观测到,像给数据新增一对key/value的属性,或删除一对已有的key/value的属性,是无法被观测的。显然,vue也意识到这点,提供了两个全局API的方法,vue.set和vue.delete来解决新增和删除带来的数据变化无法被观测的问题。至于用法,这里不就不说了,不了解可以去官网看。原理嘛,在后面的篇章会说的。

第四节·篇章小结

综上,我们知道了:

①object类型数据实现数据可观测,是通过Object的defineProperty()实现的。

②array类型数据实现数据可观测,是通过拦截重写数据的7个可操作数组且会改变数组自身的方法实现的。

③依赖是一种表示数据和其使用者的关系,依赖管理器会为每一个依赖创建watcher实例。

④数据变化被观测到后,会通过代表依赖的watcher实例,调用update()方法,通知视图更新。

⑤vue提供了set和delete两个全局API,弥补部分新增和删除数据手法,无法被观测,进而影响数据响应式实现的不足。