Vue-数据响应式系统的实现

前言

在我们使用vue的时候,我们知道,如果我们修改vue实例当中data里面的值,那么对应该数据的视图也会很快得到更新,这就是响应式系统。响应式系统,实现原理就是Object.defineProperty(obj, prop, descriptor)。具体讲讲怎么实现,下面只是讲一个简单的例子,实现了依赖的收集和触发。

Object.defineProperty(obj, prop, descriptor)用法

Object.defineProperty,Vue.js就是基于它实现「响应式系统」的。

使用方法:

/*
    obj: 目标对象
    prop: 需要操作的目标对象的属性名
    descriptor: 描述符
    
    return value 传入对象
*/
Object.defineProperty(obj, prop, descriptor)...

descriptor的一些属性,简单介绍几个属性。

1、enumerable,属性是否可枚举,默认 false。

2、configurable,属性是否可以被修改或者删除,默认 false。

3、get,获取属性的方法。

4、set,设置属性的方法。

为对象添加属性

一般来说,给对象添加属性会这样写:

let object = {}
object.test = 'test'

Object.defineProperty 也能做到同样的效果

let object = {}, test = 'test'
Object.defineProperty(object, 'test', {
    configurable: true,            // 描述该属性的描述符能否被改变,默认值为 false
    enumerable: true,               // 能否被遍历,比如 for in,默认值为 false
    get: function(){                // 取值的时候调用,object.test,默认值为 false
        console.log('enter get')
        return test
    },
    set: function(newValue){        // 设置值的时候使用
        console.log('enter set')
        test = newValue
    }
})

这样,可以让属性有了控制控制属性取值和设置值的权利,运行结果:

object.test
// enter get
// test
object.test = 'test2'
// enter set
// test2

封装改造defindProperty

先封装改造definproperty方法,方便我们的调用:

let callback = {
    target: null
}
let defineReactive = function(object, key, value){
    let array = []
    Object.defineProperty(object, key, {
        configurable: true,
        enumerable: true,
        get: function(){
            if(callback.target){
                array.push(callback.target)
            }
            return value
        },
        set: function(newValue){
            if(newValue != value){
                array.forEach((fun)=>fun(newValue, value))
            }
            value = newValue
        }
    })
}

可以从代码中看出来,我在函数内部声明了一个数组用于存放 callback 中的 target,当对 object 进行 get 操作(取值操作)的时候,就会往 callback 中存放函数,进行 set 操作(设置值)的时候执行数组中的函数。此数组的意义其实就是,每次获取对象的值的时候,都会将依赖到该值的一些dom、计算属性等放到这个数组,当我们进行set操作时,运行数组里面的方法,就可以对涉及到这个值的依赖作更新,例如更新dom视图。

看看效果如何:

let object = {}
defineReactive(object, 'test', 'test')
callback.target = function(newValue, oldValue){console.log('我被添加进去了,新的值是:' + newValue)}
object.test
// test
callback.target = null
object.test = 'test2'
// 我被添加进去了,新的值是:test2
callback.target = function(newValue, oldValue){console.log('添加第二个函数,新的值是:' + newValue)}
object.test
// test
callback.target = null
object.test = 'test3'
// 我被添加进去了,新的值是:test3
// 添加第二个函数,新的值是:test3

这样我们就达成了在 object.test 的值发生改变时,运行一个函数队列(虽然这个队列挺简陋的)的目的。

换个说法,当我们取值的时候,函数自动帮我们添加了针对当前值的依赖(例如使用了该值的dom),当这个值发生变化的时候,处理了这些依赖,比如说 DOM 节点的变化(更新对应的dom视图)

这个也是 VUE 中实现 MVVM 的最核心的代码,当然在 VUE 中,这个依赖收集的过程远比现在的代码要复杂,这里仅仅实现了依赖的收集和触发,对于依赖的管理这里的代码还做不到。

依赖收集

这里有一个更加易懂的解释:

当声明一个vue对象时,在执行render函数获取虚拟dom的这个过程中,已经对render中依赖的data属性进行了一次获取操作,这次获取操作便可以拿到所有依赖。

其实不仅是render,任何一个变量的改别,是因为别的变量改变引起,都可以用上述方法,也就是computedwatch的原理

首先需要写一个依赖收集的类,每一个data中的属性都有可能被依赖,因此每个属性在响应式转化(defineReactive)的时候,就初始化它。代码如下:

class Dep {
  constructor() {
    this.subs = []
  }
  add(cb) {
    this.subs.push(cb)
  }
  notify() {
    console.log(this.subs)
    this.subs.forEach((cb) => cb())
  }
}

function defineReactive(obj, key, val, cb) {
  const dep = new Dep()
  Object.defineProperty(obj, key, {
    // 省略
  })
}

那么执行过程就是: 
- 当执行render函数的时候,依赖到的变量的get就会被执行,然后就把这个 render函数加到subs里面去。 
- 当set的时候,就执行notify,将所有的subs数组里的函数执行,其中就包含render的执行。

注:代码中有一个Dep.target值,这个值时用来区分是普通的get还是收集依赖时的get

参考自:http://blog.acohome.cn/2018/04/11/vue-defineproperty/

猜你喜欢

转载自blog.csdn.net/chiuwingyan/article/details/81218855