vue源码理解——数据的变化侦测

113 阅读3分钟

  我们都知道,vue最大的一个特点就是数据驱动视图,也就是当数据变化的时候,对应的视图也随之变化。想要实现这一点,其重点在于要知道什么时候数据发生了变化,也就是说要对数据的变化进行侦测。
  那我们就从源码出发,来看一看vue是如何对数据的变化进行侦测的。
  vue将数据分为了两类进行侦测,分别是Object和Array

1、Object的变化侦测

在vue中实现数据的可侦测,用到的核心API就是:Object.defineProperty()方法

// 首先定义一个对象,我们可以通过obj.name的方式来读取或修改name属性
let obj={
    name:'xxx',
    age:'26'
}
// 使用Object.defineProperty改写,实现可侦测
Object.defineProperty(obj,'name',{
    enumerable: true, // 此属性是否可以被枚举
    configurable: true, // 目标属性是否可以被删除或是否可以再次修改特性
    get(){
        console.log('读取name属性')
        return val
    },
    set(newVal){
        console.log('修改name属性')
        val = newVal
    }
})

上面已经实现了对obj对象中一个属性进行侦测,接下来对整个obj对象进行侦测。在源码中vue中封装了一个Observer类来实现,Observer类会通过递归,将整个对象的属性都转化为可侦测的对象

let obj=new Observer({
    name:'xxx',
    age:'26'
})

export class Observer{
    constructor(value){
        this.value = value
        // 给value新增一个_ob_属性,相当于打了一个标记,表示已经被转化成响应式了
        def(value, '__ob__', this)
        if (isArray(value)) {
             // 当属性为数组的时候
        }else{
           const keys = Object.keys(value)
           for (let i = 0; i < keys.length; i++) {
               const key = keys[i]
               defineReactive(value, key[i])
           }
        }
   }
}
export function defineReactive(obj,key,val){
    if(arguments.length === 2){
        val = obj[key]
    }
    if(typeof val === 'object'){
        // 如果属性为对象,递归
        new Observer(val)
    }
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get(){
            console.log('读取属性')
            return val
        },
        set(newVal){
            val = newVal
            console.log('修改属性')
        }
    }
}

2、Array的变化侦测

为什么这两种数据要用不同的方式进行侦测呢?
主要是因为Object.defineProperty()这个方法是原型对象上的,所以数组无法使用这个方法。
Object的变化是通过setter来追踪的,但是Array并没有setter。但是换一种角度思考,如果Array数据发生了变化,那一定是进行了操作,而js中提供的操作数组的方法就那么几种,我们可以在不改变原有功能的前提下,新增一些功能进去。也就是对原数组的方法进行“劫持”!

// 数组劫持
let arr = []
arr.push(1)
Array.prototype.pushNew = function(){
    // 进行其他操作
    this.push(val)
}
arr.pushNew(2)

于是vue实现了一个数组方法拦截器
Array原型中可以改变数组本身的方法共有7个:push,pop,shift,unshift,splice,sort,reverse

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

methodsToPatch.forEach(function (method){
    const original = arrayProto[method]
    // 源码封装了def()方法,这里直接将def方法提出来了
    Object.defineProperty(arrayMethods, method, {
        enumerable: false,
        configurable: true,
        writable: true,
        value:function mutator(...args){
          const result = original.apply(this, args)
          return result
        }
    })
    
    // 将数组的新增元素也转化为响应式
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // 通知更新
    ob.dep.notify()
})

// 遍历数组,使数组每一项都转化为响应式
observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }

不足之处
拦截器只能拦截到通过调用原型上的方法进行的改变,如果通过下标修改数据则无法侦测。 vue为解决这点,增加了两个全局API:Vue.setVue.delete