Vue.js响应式原理

265 阅读5分钟

如何追踪变化

当你把一个普通的JavaScript对象传入Vue实例作为data的选项,Vue将会遍历该对象的所有属性,并且使用Object.defineProperty()方法将对象上的属性全部变成访问器属性getter/setter,这样就实现了对对象的 响应式化。

每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据属性记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。

QaKA1g.png

一 对象的响应式化

首先定义一个 defineReactive 函数,这个方法通过 Object.defineProperty 来实现对对象的「响应式」化,入参是一个 obj(需要绑定的对象)、key(obj的某一个属性),val(具体的值)。经过 defineReactive 函数处理之后,obj的某一个属性在被读取时就会触发 getter 方法,在属性被写入时就会触发 setter 方法。

function defineReactive(obj,key,value) {
    Object.defineProperty(obj,key,{
        configurable: true,
        enumerable: true,
        get: function(){
            return value
        },
        set: function(newVal){
            if(value === newVal) {
                return
            }
            value = newVal
        }
    })
}

以上方法只能对obj的一个属性进行响应式化,我们需要将对象中的所有属性都进行响应式化,所以封装一个Observer 类,将对象的所有属性都转换成 getter/setter 的形式

class Observer{
    constructor(value) {
        this.value = value
        if(!Array.isArray(value)) {
            this.walk(value)
        }
    }
    /* 
        * walk会将对象的每一个属性都转换成getter/setter的形式来将属性响应式化 
        * 这个方法只有在数据类型为Object时调用(即非数组),因为对象的响应式化与
        * 数组不同,这里只讨论非组数的情况
    */
    walk(obj){
        Object.keys(obj).forEach( (key) => {
            defineReactive(obj,key,obj[key])
        })
        
    }
}

此时函数 defineReactive 需要作出一些变化,来递归对象的所有子属性,这样就可以将对象的所有属性都响应式化。

function defineReactive(obj,key,value) {
    /* 递归子属性,变成响应式的*/
    if(typeof value === 'object'){
        new Observer(value)
    }
    Object.defineProperty(obj,key,{
        configurable: true,
        enumerable: true,
        get: function(){
            return val
        },
        set: function(newVal){
            if(val === newVal) {
                return
            }
            val = newVal
        }
    })
}

二 依赖收集

为什么要收集依赖? 我们之所以要观测数据,是为了当数据发生了变化,可以通知那些曾经使用了该数据的地方,以便数据发送变化时,视图能够得到更新。

<template>
    <div>
        {{ title }}
    </div>
</template>

以上模板中使用了title,故当title的值发生变化时,需要通知使用了它的地方,依赖收集会让 title 属性知道有个地方在依赖我的数据,我发生变化时需要通知它。

在如何追踪变化那里已经讲过,每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据属性记录为依赖;之后当依赖项的 setter 触发时,会通知 watcher,watcher 又会通知外界,从而使它关联的组件重新渲染。

由于组件在渲染的过程中会去读取相应的属性,即会触发getter,那么只要在getter中收集依赖就可以了。而当属性的值发生变化时会触发setter,所以只要在setter时触发依赖,以更新对应的视图即可。

那么依赖收集在哪里呢? 我们将依赖收集的代码封装成一个Dep类,这个类用于管理依赖。使用这个类,可以添加依赖,删除依赖或者向依赖发送通知等。代码如下:

class Dep {
    constructor() {
        this.subs = [];
    }
    addSub(sub) {
        this.subs.push(sub)
    }
    removeSub(sub) {
        remove(this.subs, sub)
    }
    notify() {
        // stabilize the subscriber list first
        const subs = this.subs.slice()
        for (let i = 0, l = subs.length; i < l; i++) {
            subs[i].update()
        }
    }
}
function remove(arr, item) {
    if (arr.length) {
        const index = arr.indexOf(item)
        if (index > -1) {
            return arr.splice(index, 1)
        }
    }
}

此时将 defineReactive 改成如下:

function defineReactive(obj,key,value) {
    let dep = new Dep()
    /* 递归子属性,变成响应式化*/
    if(typeof value === 'object'){
        new Observer(value)
    }
    Object.defineProperty(obj,key,{
        configurable: true,
        enumerable: true,
        get: function(){
            /* 收集依赖,Dep.target是Watcher实例*/
            dep.addSub(Dep.target)
            return val
        },
        set: function(newVal){
            if(val === newVal) {
                return
            }
            val = newVal
            /* 通知依赖以更新视图*/
            dep.notify()
        }
    })
}

可以看到将依赖收集到了Dep中的subs属性。

上面说了那么多,我们仍然不知道依赖是什么😈? 也就是说我们需要收集谁,收集谁,换句话说,就是当属性发生变化时,需要通知谁。

当然通知的是属性所用到的地方咯,而使用这个属性的地方有很多,而且类型还不一样,极有可能是模板,也有可能是用户写的一个watch,这时候需要抽象出一个能集中处理这些情况的类。然后在依赖收集阶段将这个类的实例收集进来,通知也只通知他一个,它在负责通知其他地方,类似于一个中介的角色,称它为 Watcher。

综上我们收集的是Watcher实例,当属性发送变化时通知的也是Watcher实例,它再负责通知其它地方以更新视图。在如何追踪变化那里说过,每个组件实例对应一个Watcher实例。

class Watcher{
    constructor(){
        /* new一个Watcher对象时会将该实例赋值给Dep.target*/
        Dep.target = this
    }
    /* 更新视图的方法 */
    update(){
        console.log(`视图更新了!`)
    }
}

最后封装一个Vue类:

class Vue{
    constructor(options){
        this.$data = options.data
        /* 使数据对象变成响应式的*/
        new Observer(this.$data)
        /* 新建一个Watcher观察者对象,这时候Dep.target会指向Watcher实例 */
        new Watcher()
        /* 在这里模拟render function的过程,为了触发name属性的getter函数以进行依赖收集 */
        console.log(`开始render`,this.$data.name)
    }
}

这样我们只要 new 一个 Vue 对象,就会将 data 中的数据进行化

let app = new Vue({
    data: {
        name: 'jack',
        address: {
            addOne: `china`,
            addTwo: `Japan`
        }
    }
})

总结

defineReactive 函数中的Dep实例用来收集依赖,即Watcher实例。在对象被读时,会触发getter,此时将Watcher实例对象(存放在Dep.target)收集到Dep类中的subs中去。以后当对象的属性被重写之后,就会触发setter,调用Dep类上的notify方法通知所有的Watcher实例并触发该实例上的update方法以更新视图。