vue如何实现响应式,vue2.0到vue3.0

522 阅读4分钟

基础概念

Object.defineProperty

Object.defineProperty(..) 是用来添加一个新属性或者修改一个已有属性(如果它是 configurable)并对特性进行设置。

语法

Object.defineProperty(obj, prop, descriptor)

var obj = {}
Object.defineProperty(obj, 'a', {
    value: 2,
    writable: true,
    configurable: true,
    enumerable: true,
    get: function() {
        return this._a
    },
    set: function(val) {
        this._a = val + 2
    }
})
obj.a // 2

函数的第三个参数 descriptor 所表示的属性描述符有两种形式:数据描述符和存取描述符

数据描述符和存取描述符都有的属性有:

  • configurable
  • enumerable 数据描述符可选属性有:
  • value
  • writable 存取描述符可选键值有:
  • get
  • set

在了解vue如何实现响应式之前,我们先要知道几个概念:

  • 属性描述符
  • Object.defineProperty
  • [[GET]][[PUT]]
  • 访问描述符

什么是属性描述符

es5之前,js语言本身没有提供可以直接检测属性特性的方法。从es5开始,所有的属性都具备了属性描述符。

var obj = {
    m: 1
}
Object.getOwnPropertyDescriptor(obj, "m")
// {
//   value: 1,
//   writable: true,
//   enumerable: true,
//   configurable: true
// } 

这就是属性描述符,除了数值2,还包含另外三个特性:writable(可写)、 enumerable(可枚举)和 configurable(可配置)

在创建普通对象时,属性描述符会使用默认值,都为true。也可以使用Object.defineProperty对其属性描述符进行设置。

[[GET]]

属性访问时,并非仅仅在对象中查找某个属性,参见下例,obj.m实则是在obj上实现了[[GET]]操作(类似函数调用:[[GET]]()

对象默认的内置 [[Get]] 操作首先在对象中查找是否有名称相同的属性, 如果找到就会返回这个属性的值。如果没有找到,就会进行原型链的查找

var obj = {
    m: 1
}
obj.m // 1

如果无论如何都找不到,[[Get]] 操作就会返回undefined

[[PUT]]

[[Get]]相对应的,就是[[PUT]]操作。

给属性赋值时,会触发[[PUT]],但能否[[PUT]]成功,还取决于属性描述符writable和访问描述符setter

什么是访问描述符

给一个属性定义 getter、setter 或者两者都有时,这个属性会被定义为“访问描述 符”(和“数据描述符”相对)

在 ES5 中可以使用 getter 和 setter 部分改写默认操作,但是只能应用在单个属性上,无法 应用在整个对象上。

  • getter 是一个隐藏函数,会在获取属性值时调用
  • setter 也是一个隐藏函数,会在设置属性值时调用
var obj = {
    get m() {
        return 2
    }
}
obj.m = 3
obj.m // 2

当定义了getter函数时,赋值操作是没有意义的,除非再定义setter。setter 会覆盖单个属性默认的 [[Put]](也被称为赋值)操作。通常来说 getter 和 setter 是成对出现的(只定义一个的话 通常会产生意料之外的行为)

var obj = {
    get m() {
        return this._m
    }
    set m(val) {
        this._m = val * 2
    }
}
obj.m = 3
obj.m // 6

什么是响应式

  • 修改data属性之后,vue立刻监听到,立刻被渲染到页面上
    • 获取属性,如何监听到
    • 赋值属性,如何监听到
  • data属性被代理到vm上
    • 为什么在data中定义的属性,可以用vm访问并操作
    var vm = new Vue({
        el: '#app',
        data: {
            name: 'wowo',
            age: 20
        }
    })
    vm.name // wowo
    vm.age // 20
    vm.age = 22
    vm.age // 22 
    

vue2如何实现响应式

Object.defineProperty

var vm = {}
var data = {
    name: 'wowo'
    age: 20,
}

var key, value
for(key in value) {
    (function watch(key) {
        Object.defineProperty(vm, key, {
            get: function() {
                console.log('get') // 监听获取属性
                return data[key]
            }
            set: function(val) {
                console.log('set') // 监听赋值属性
                data[key] = val
            }
        })
    })(key)
}

vue3为何改用proxy

vue3改用proxy,是为了解决vue2使用defineProperty的缺陷 Object.defineProperty的缺陷:

  1. Object.defineProperty无法监听数组的变化

    vue中是可以监听数组的变化的,是提供了以下方法 push() pop() shift() unshift() splice() sort() reverse()

  2. Object.defineProperty只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历,如果属性值也是对象那么需要深度遍历,性能不高

proxy可以解决这两个缺陷:

  1. 可以监听到数组的变化
  2. proxy可以监听对象而非属性。对该对象的访问,都必须先通过proxy这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy直接可以劫持整个对象,并返回一个新对象。提升了性能。

proxy 最大的问题是浏览器支持度不够,而且很多效果无法使用 poilyfill 来弥补。