Vue中数据响应式原理---对象的变化侦测

什么是变化侦测?

Vue.js会自动通过状态生成DOM,并将其输出到页面上显示出来,这个过程叫做渲染。
那么,在运行时应用内部的状态会不断发生变化,此时需要不停的重新渲染,如何确定状态中发生了什么变化?
这里就用到了 “变化侦测”,它就是用来监听内部数据状态变化的。

变化侦测的分类?

变化侦测分为两种类型,一种是 “推”(push) ,一种是 “拉”(pull)。
目前我们的前端主流框架中,Angular 和 React 中的变化侦测都属于 “拉” 的模式,也就是说,当状态发生变化时,它们并不知道哪个状态变了,只知道有可能变了,然后会发出一个信号告诉框架,框架接收到信号后,会进行一个暴力比对来找出那些DOM节点需要重新渲染。

这在Angular中是脏检查的流程,在React中使用的是虚拟DOM。

在Vue.js中的变化侦测属于 “推” 的过程,当状态发生变化时,Vue.js立刻就知道了,而且在一定程度上知道那些状态变化了。因此,在Vue.js中可以进行更细粒度的更新。

如何侦测一个对象的变化?

在JS中,如果想侦测一个对象的变化,有两种办法:

  • 1.使用 Object.defineProperty
  • 2.使用 ES6 的 Proxy

注: 由于ES6在浏览器中的支持度并不理想,在Vue2.0源码中,还是使用了Object.defineProperty来实现的,但使用Object.defineProperty来侦测变化会有很多缺陷,比如:

  • 没办法直接侦测数组的变化
  • 对于多层的对象,需要使用递归进行侦测,多层递归会影响性能
  • 没办法对新增、删除的属性进行侦测

所以,在已发布的测试版Vue3.0的源码里,作者使用Proxy重写了这部分的代码。

这里主要说说Vue2.0源码中的数据侦测的实现,同时也是对学习过程中的一个知识梳理。Proxy的方式实现数据侦测待Vue3.0源码正式版发布后,再和小伙伴们做讨论。

=> 使用Object.defineProperty来实现数据追踪?

代码如下:

// 定义一个响应式数据,在此函数中追踪变化
function defineReactive(data, key, val) {
     
  Object.defineProperty(data, key, {
     
    enumerable: true,
    configurable: true,
    get: function() {
     
      return val
    },
    set: function(newVal) {
     
      if(val === newVal) {
     
        return
      }
      val = newVal
    }
  })
}

通过上方defineReactive函数的封装,就能实现每当从data的key中读取数据时,get函数被触发,每当往data的key中设置数据时,set函数被触发。

如何收集依赖?

我们之所以要观察数据,目的是当数据发生变化时,可以通知曾经使用了该数据的地方,进行更新、渲染,所以我们要先收集依赖
收集依赖: 即把用到数据的地方收集起来,等属性发生变化时,把之前收集好的依赖循环触发一遍就好了。

在Vue2.0中,模板使用数据等同于组件使用数据,所以当数据发生变化时,会将通知发送到组件,然后组件内部再通过虚拟DOM重新渲染。

总结下来就是:在getter中收集依赖,在setter中触发依赖。

=> 依赖是什么?

数据变化时,曾经使用了该数据的地方有很多,而且类型可能还不一样,既有可能是模板,也有可能是用户写的一个watch。所以,我们需要抽象出一个能集中处理这些情况的类。
然后,在收集依赖阶段,只收集这个封装好的类的实例进来,通知也只通知它一个,它再负责通知其他地方,这个抽象的东西(类),就叫做 Watcher .

=> 依赖收集在哪里?

现在已经明确了,在getter中收集依赖,假设依赖是一个函数,保存在window.target上,我们就可以把依赖收集的代码封装成一个Dep类,帮助我们管理依赖:

// 为了减少耦合,把收集依赖的代码封装成一个dep类,专门帮助我们管理依赖
export default class Dep {
     
  constructor () {
     
    this.subs = []
  }
  addSub(sub) {
      // 收集依赖
    this.subs.push(sub)
  }
  removeSub(sub) {
     
    remove(this.subs, sub)
  }
  depend () {
     
    if(window.target) {
     
      this.addSub(window.target)
    }
  }
  notify() {
      //通知依赖更新
    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) {
     
        arr.splice(index, 1)
      }
    }
  }
}

使用上方的Dep类,我们可以做收集依赖、删除依赖、向依赖发送通知等操作。
然后把上方的defineReactive函数进行改造:

// 改造defineReactive函数,使用dep类
function defineReactive(data, key, val) {
     
  let dep = new Dep() // 实例化一个对象(类)
  Object.defineProperty(data, key, {
     
    enumerable: true,
    configurable: true,
    get: function () {
     
      dep.depend() // 收集依赖(调用类中的收集函数)
    },
    set: function (newVal) {
     
      if(val === newVal) {
     
        return
      }
      dep.notify() // 通知依赖
      val = newVal
    }
  })
}

到此,“依赖收集在哪里” 的疑问仿佛已经揭秘了,依赖收集到 Dep 中。

=> 什么是Watcher?

上方我们已经提到依赖是 Watcher,Watcher 在这里代表的是一个中介的角色,数据发生变化时通知它,然后它再通知其他地方。它的主要作用是:

  • Watcher 就是一个抽象的类,收集依赖时,只收集封装好的类的实例,通知也只通知它一个。
  • Watcher 可以主动去 订阅 任何一个数据的变化。

比如我们的watch监听函数的使用:

//keypath
vm.$watch('a.b.c', function(newVal, oldVal)) {
     
	// 监听数据变化,做具体操作
}

这段代码表示:当data.a.b.c属性发生变化时,触发第二个参数中的函数。

通过以上数据侦测的介绍,实现以上功能,接着我们就想到只要把watcher实例,添加到data.a.b.c属性的Dep中就行了,这个过程也叫做收集依赖的过程。然后,当data.a.b.c的值发生变化时,通知Watcher。接着,Watcher再执行参数中的这个回调函数。

实现以上功能,我们可以对Watcher抽象类做如下的封装:

// 什么是watcher?watcher抽象类
export default class Watcher {
     
  constructor (vm, expOrFn, cb) {
     
    this.vm = vm
    // 执行this.getter(),就可以读取data.a.b.c的内容
    this.getter = parsePath(expOrFn) // parsePath是一个读取一个字符串keypath的函数,这里不再列举
    this.cb = cb
    this.value = this.get()
  }
  get() {
     
    window.target = this
    let value = this.getter.call(this.vm, this.vm)
    window.target = undefined
    return value
  }
  update() {
     
    const oldValue = this.value
    this.value = this.get()
    this.cb.call(this.vm, this.value, oldValue)
  }
}

这段代码可以把自己主动添加到data.a.b.c的Dep中去。因为,在get方法中先把window.target设置成了this,也就是当前的watcher实例。然后读data.a.b.c的值,就会触发getter(收集依赖的逻辑),并把依赖添加到Dep中。

依赖注入到Dep中后,每当data.a.b.c的值发生变化时,就会让所有的依赖循环触发update方法,而update方法会执行参数中的回调函数,将value和oldValue传到参数中。

所以,不管是用户执行watch用到data数据,还是在模板中用到data数据,都是通过 Watcher 来通知自己是否需要发生变化。

递归侦测对象的所有key

通过上方赘述,我们已经实现了变化侦测功能了,但只能侦测数据中的某一个属性,而我们所期望的是把数据中的所有属性(包括子属性)都侦测到。所以,我们封装一个Observer类,这个类的作用是把数据内的所有属性都转换成getter/setter的形式,然后追踪它们的变化。

// 封装一个Observer类,递归检测所有属性key
export class Observer {
     
  constructor (value) {
     
    this.value = value
    if(!Array.isArray(value)) {
     
      this.walk(value)
    }
  }
  // 将每个属性都转换成getter/setter的形式检测变化
  // 这个方法只有在数据类型为object的时被调用
  walk (obj) {
     
    const keys = Object.keys(obj)
    for(let i = 0;i < keys.length;i++) {
     
      defineReactive(obj, keys[i], obj[keys[i]])
    }
  }
  // 定义检测对象变化的函数
  function defineReactive(data, key, val) {
     
    // 判断某个属性值是否仍然为一个对象,如果是,则继续检测变化
    if(typeof val === 'object') {
     
      new Observer(val)
    }
    let dep = new Dep()
    Object.defineProperty(data, key, {
     
      enumerable: true,
      configurable: true,
      get: function () {
      // 收集依赖
        dep.depend()
        return val
      },
      set: function (newVal) {
      // 通知更新
        if(val === newVal) {
     
          return
        }
        val = newVal
        dep.notify()
      }
    }) 
  }
}

上方我们定义的Observer类,它用来将一个正常的object转换成被侦测的object(响应式的object)。也就是说,我们只要将一个对象传到Observer中,那么这个对象就会变成响应式的了。这就是我们Vue2.0源码中数据响应的实现方式。

关于Object的问题

在深究了 object 的变化侦测原理后,也会发现一些问题,当我们去给data新增、删除属性时,Vue.js是无法侦测到这个变化的。以为在ES6之前,JS没有提供元编程的能力,无法侦测到一个属性的新增和删除。为了解决此问题,Vue.js 提供了两个API :

  • vm.$set ------- 新增响应式属性
  • vm.$delete ------- 删除现有的响应式属性

数据监测图解

Data、Observer、Dep、Watcher之前的关系:
Vue中数据响应式原理---对象的变化侦测_第1张图片
总结:Vue.js中的的数据响应侦测原理就是:

  • 初始化数据Data通过Observer转换成了 getter/setter 的形式来追踪变化。
  • 当外界通过 Watcher 读取数据时(watcher订阅的数据),会触发getter将watcher添加到依赖中。
  • 当订阅的数据发生变化时,会触发setter,向Dep中的依赖(watcher)发送通知。
  • Watcher 接收到通知后,会向外界发送通知,触发视图的更新。

以上就是对Vue2.0中对象变化侦测的原理分析,在此梳理知识和大家分享,后期还会对数组侦测原理Vue3.0实现数据侦测原理做进一步的更新分享。

喜欢记得点赞,你的认可是我学习分享的最大动力 ~ ❤️

你可能感兴趣的:(VUE,Vue中对象的变化侦测)