Vue响应式原理初探

一、数据侦测

Vue响应式原理初探_第1张图片

(一)侦测 Object

1. Object.defineProperty(obj, prop, descriptor)

该方法用于设置对象属性,其接受 3 个参数,第一个参数是需要定义的对象,第二个是需要定义的属性,第三个则被称为描述符。JS 中的对象身上有两种描述符,第一种是数据描述符,具有以下键值:

  1. configurable,用于描述数据的可配置性,是否可通过 delete 操作符删除,默认值为 false;
  2. enumerable,表示该属性是否可枚举,即可通过 for...in 访问其属性,默认值为 false;
  3. value,表示该属性的值,可以是任何有效的 JS 值,默认值为 undefined;
  4. writable,表示该属性的值是否可写,默认值为 false,当值为 true 时其值才可被赋值运算符改变。

第二种是访问器描述符,具有以下键值:

  1. configurable,用于描述数据的可配置性,是否可通过 delete 操作符删除,默认值为 false;
  2. enumerable,表示该属性是否可枚举,即可通过 for...in 访问其属性,默认值为 false;
  3. getter,在读取属性时调用的函数,默认值为 undefined;
  4. setter,在写入属性时调用的函数,默认值为 undefined。

这两种描述符不可以同时使用。同时,请注意 getter 与 setter 两个方法,一个是读取对象属性时调用,一个是写入时调用。这意味着对象的属性是可“侦测”的。

2. 数据响应

现在,对象属性已经是可以侦测的了,但实现视图层的同步更新是一个问题。
其实,数据虽多,但只需在使用数据的位置进行更新即可。前面也说到,getter 是一个属性被读取时调用的函数,通过它,Vue 知道了谁读取了该属性,顺即将其存在getter中。同样的,谁触发了 setter 方法,由 setter 去通知依赖了该属性的元素更新就好。但Vue中的实现并非这么简单,Vue 建立了一个 Watcher,其代替 getter 行使收集那些依赖了数据的元素的职能,谁使用了数据,便为其建立一个 watcher 实例,当数据更新时,由这个 watcher 通知元素更新数据。但这个通知不是直接通知,Vue是通过虚拟 Dom 实现真实 Dom 的更新,这个 watcher 通知渲染函数,再由渲染函数操作虚拟 Dom 实现视图的更新。

3. 属性的使用

Vue 无法检测 property 的添加或移除。由于 Vue 会在初始化实例时对 property 执行 getter、setter 转化,所以 property 必须在 data 对象上存在才能让 Vue 将它转换为响应式的。

(二)侦测 Array

1. 利用对象的 getter 与 setter

Array 类型并不同于 Object 类型,并没有 getter 与 setter,其并不能使用这两种方法。但巧妙的是,data 里 return 的就是一个对象,数组存放在其中,恰作为 data 的一个属性,谁读取它,便可以被侦测到了。但这仅仅是侦测到 array 的使用者,array 的自身的变化如何侦测?

2. 操作即变化

对于数组而言,数组的变化便表明使用者调用了数组方法,例如 push(),pop() 等。明确了这个概念,Vue 便可以实现 Array 的侦测。其将可以改变数组的方法(push,pop,shift,unshift,splice,sort,reverse)进行了从新封装,在不改变这些方法的原始功能的基础上安插了侦测器(拦截器),一旦调用这些方法,意味着数组的改变,这些侦听器便会通知Vue数组发生了变化。通过实现一个存储那些使用了数组的依赖存储器,再实现一个访问通知这些依赖的方法,便实现了数组的侦测。

3. 弥补不足

由于数组的检测通过重写数组方法实现,所以 Vue 并不能直接通过索引操作数组,也不能使用其 length 属性操作数组。于是 Vue 实现了两个 API。以下两个方法实现第一种功能:

1. Vue.set(vm.items, indexOfItem, newValue)或vm.$set(vm.items, indexOfItem, newValue)
2. vm.items.splice(indexOfItem, 1, newValue)

以下方法实现第二种功能:

1. vm.items.splice(newLength)

这两个方法有效弥补了上述不足。

二、声明响应式property

由于 Vue 不允许动态添加根级响应式 property,所以你必须在初始化实例前声明所有根级响应式 property,哪怕只是一个空值:

var vm = new Vue({
  data: {
    // 声明 message 为一个空值字符串
    message: ''
  },
  template: '
{{ message }}
' }) // 之后设置 `message` vm.message = 'Hello!'

如果你未在 data 选项中声明 message,Vue 将警告你渲染函数正在试图访问不存在的 property。

三、异步更新队列

vue更新dom时是异步执行的

数据变化、更新是在主线程中同步执行的;在侦听到数据变化时,watcher将数据变更存储到异步队列中,当本次数据变化,即主线成任务执行完毕,异步队列中的任务才会被执行(已去重)。

如果你在js中更新数据后立即去操作DOM,这时候DOM还未更新;vue提供了nextTick接口来处理这样的情况,它的参数是一个回调函数,会在本次DOM更新完成后被调用。

使用方法:

  • 1.在组件内使用 vm.$nextTick() 实例方法特别方便,因为它不需要全局 Vue,并且回调函数中的 this 将自动绑定到当前的 Vue 实例上:
Vue.component('example', {
  template: '{{ message }}',
  data: function () {
    return {
      message: '未更新'
    }
  },
  methods: {
    updateMessage: function () {
      this.message = '已更新'
      console.log(this.$el.textContent) // => '未更新'
      this.$nextTick(function () {
        console.log(this.$el.textContent) // => '已更新'
      })
    }
  }
})
  • 2.因为 $nextTick() 返回一个 Promise 对象,所以你可以使用新的 ES2016 async/await 语法完成相同的事情:
methods: {
  updateMessage: async function () {
    this.message = '已更新'
    console.log(this.$el.textContent) // => '未更新'
    await this.$nextTick()
    console.log(this.$el.textContent) // => '已更新'
  }
}

       Vue 在观察数据变化时并不是直接更新 DOM,而是开启一个队列,并缓存同一个事件循环中发生的所有数据改变。在缓存时会除去重复数据,从而避免不必要的计算和 DOM 操作。然后,在下一个事件循环 tick 中,Vue 刷新队列并执行实际(已去重)的工作。
       Vue 这种去重机制减少了开销,如果一个for循环来动态改变数据 100 次,其实它只会应用最后一次改变,如果没有这种机制,DOM 就要重绘 100 次。Vue会根据当前浏览器环境优先使用原生的Promise.then 和MutationObserve。如果都不支持就会采用setTimeout代替。为了在数据变化之后等待 Vue 完成更新 DOM,可以在数据变化之后立即使用 Vue.nextTick(callback)。这样回调函数将在 DOM 更新完成后被调用。
       说人话就是 Vue 中的 Dom 更新并不是立即执行的,在观察数据变化后会将 Dom 更新存放起来,并对其进行一些去重工作,比如某个数据被循环了,不会直接循环N次,而是存起来一次性更新完以实现更高的性能。也就是说数据虽然已经改变,但Dom的更新不是实时的,要想针对Dom做一些操作,就得用到 Vue.nextTick(callback) 方法,在其回调函数里实现自己想要进行的操作。   

你可能感兴趣的:(Vue,web前端,Vue源码解析,vue响应式)