VUE响应式系统的基本原理

538 阅读6分钟

这篇文章主要了解vue是如何实现数据的响应式以及这种方式的优缺点并探索更好的响应式方式

Object.defineProperty

在vue中,实现响应式的主要方式就是Object.defineProperty,关于Object.defineProperty的更多内容,可以参考MDN

下面看一下这个方法具体如何使用

/**
 * obj 需要操作的目标对象
 * key 需要操作的属性名称
 * descriptor 将被定义或修改的属性描述符
 */
Object.defineProperty(obj, key, descriptor)

descriptor中有几个常用的属性和方法,分别看一下

configurable: 是否可配置,默认为false;只有当属性的可配置性为true时,才能对该属性进行修改和删除 enumerable: 是否可枚举,默认为false;只有当属性的可枚举性为true时,才能使用for..inObject.keys()对这个属性进行遍历 get: 获取属性时将触发这个方法 set: 设置属性时将触发这个方法,接收这个属性的新的参数值作为参数

通过Object.defineProperty方法,当一个属性获取和设置的时候都会触发相应的拦截方法,所以接下来就要实现一个Observer类,让所有属性都变成响应式

Observer(观察者)

Observer类接收一个对象作为需要操作的数据;之后关于Observer的所有方法都将是这个类的方法

/**
 * 观察者
 */
class Observer {
    constructor (data) {
        this.data = data
    }
}

接下来实现Observer类最核心的方法defineReactive,这个方法通过Object.defineProperty来实现属性的响应化;

它需要接收三个参数,分别是当前操作的对象(obj),当前操作的属性(key)以及当前操作属性的值(value)

defineReactive(obj, key, value) {
    Object.defineProperty(obj, key, {
        configurable: true, // 属性可以修改和删除
        enumerable: true, // 属性可以通过for...in和Object.keys()遍历
        get() {
            // 获取属性时触发
            return value
        },
        set(newVal) {
            // 设置属性时触发
            if (newVal != value) { // 如果新的值和旧的值一致,则没有设置的必要
                value = new Val
            }
        }
    })
}   

因为需要让每一个属性都是响应式的,所以还需要实现一个observer方法,这个方法需要将实例中的data接收过来作为参数;它的主要功能是判断接收到的参数是否是对象,然后遍历对象中的每一个参数并将他们设置为可响应的

observer(data) {
    if (data && type data === 'object') {
        for (let key in data) {
            this.defineReactive(data, key, data[key])
        }
    }
}

考虑到observer循环的时候,当前的value可能也是一个对象,所以在defineReactive方法中需要使用当前的value参数再次调用observer方法进行递归遍历;同时设置属性的时候也可能设置一个对象,所以在设置属性的时候也需要调用observer方法

defineReactive(obj, key, value) {
    this.observer(value) // 如果value为object,则会遍历该对象的所有属性
    Object.defineProperty(obj, key, {
        configurable: true, // 属性可以修改和删除
        enumerable: true, // 属性可以通过for...in和Object.keys()遍历
        get() {
            // 获取属性时触发
            return value
        },
        set: (newVal) => { // 方法内部使用了this,保证指向正确
            // 设置属性时触发
            if (newVal != value) { // 如果新的值和旧的值一致,则没有设置的必要
                this.observer(value)    // 如果newVal为object,则遍历改对象下的所有属性
                value = new Val
            }
        }
    })
}

到这里,一个简单的Observer类旧封装完成了;只需要在Vue进行实例化的时候,将data作为参数传递给Observer类并将它实例化,就会将data中的数据全部变为响应式的

缺陷

Object.defineProperty方法对数据进行劫持,完成数据的响应化,但它还是有一些缺陷;先来看一段代码

class Vue {
    constructor(options) {
        this.$el = options.el
        this.$data = options.data
        // 实现响应化
        new Observer(this.$data)
        console.log('新增前' this.$data) // 查看响应化之后的数据
        this.$data.test = 'test'
        console.log('新增后', this.$data) // 查看新增属性之后的数据
    }
}
new Vue({
    data: {
        user: {
        name: '阿白Smile',
            age: 24,
            sex: `<p>性别:男</p>`
        },
        location: '北京'
    }
})

在浏览器中运行一下上面的代码

ObserverTest

从上图中可以看到,在Vue实例化的时候作为参数传入的这些属性都是响应式的(都有get和set方法);但是我们可以发现,在实例化之后新增的属性却没有变成响应式

出现这个问题的主要原因是Object.defineProperty方法中的get和set只能拦截到属性的获取和设置操作,并不能拦截到属性的新增

所以,在vue3.0种,使用了ES6新增的构造函数Proxy进行数据响应化

Proxy

Proxy是ES6原生提供的一个构造函数,用来生成proxy实例;先看一下MDN的描述:Proxy对象用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)

简单的来说,就是proxy在目标对象之前架设了一层拦截,外界对目标对象的访问都需要经过这层拦截

那么接下来就使用ProxyObserver类进行一些改造

class Observer {
    // 接收Vue实例作为参数,并Vue实例中的$data进行Proxy改造
    constructor (data, vm) {
        vm.$data = this.observer(data)
    }

    observer(data) {
        if (data && typeof data === 'object') {
            for (let key in data) {
                data[key] = this.observer(data[key])
            }
            return this.defineReactive(data)
        } else {
            return data
        }
    }

    defineReactive(obj) {
        return new Proxy(obj, {
            get(target, key) {
                return target[key]
            },
            set: (target, key, value) => {
               if (target[key] != value) {
                  console.log('set', key) // 设置属性时打印属性名
                  target[key] = this.observer(value)  // 如果设置的属性是对象,则对其加上Proxy拦截
                  return true
               }
            }
        })
    }
}

通过上面的优化后,Vue实例中的data数据将变为使用data作为目标对象的Proxy实例,对data中数据的访问,修改和新增等都会经过Proxy实例的拦截,这样就是实现了一个非常简单的响应式,可以监听到所有数据的增删查改;现在把改造后的方法放到实例中去看一下

class Vue {
    constructor(options) {
        this.$el = options.el
        this.$data = options.data
        // 实现响应化
        new Observer(this.$data, this)
        this.$data.test = 'test'    // 新增一个属性
    }
}
new Vue({
    data: {
        user: {
        name: '阿白Smile',
            age: 24,
            sex: `<p>性别:男</p>`
        },
        location: '北京'
    }
})

在浏览器中运行查看一下

ProxyTest

通过上图看到,经过Proxy改造后,Vue实例化之后新增的属性依然可以被拦截,解决了Object.defineProperty方法的缺陷

在上一篇文章中实现了简单的模板编译,本文主要了解了vue响应式系统的响应原理以及当前版本中响应式的缺陷,最后还探索了新的响应式方式;但是,数据的修改还不能让视图发生改变,所以在下一篇文章中将学习响应式系统的另一个重要部分——依赖收集

本文的源代码我已经提交到我的GitHub,欢迎大佬们拍砖

end