简单理解响应式原理

Vue 最独特的特性之一,是其非侵入性的响应式系统。数据模型仅仅是普通的 JavaScript 对象。而当你修改它们时,视图会进行更新。这使得状态管理很是简单直接,不过理解其工做原理一样重要,这样你能够避开一些常见的问题。html

如何追踪变化

把一个普通的 JavaScript 对象传入Vue实例做为 data 选项,Vue 将遍历里面全部的属性,并使用 Object.defineProperty 把这些属性所有转为 getter/setterObject.defineProperty 是 es5 中一个没法 shim(Object.defineProperty这个特性是没法使用低级浏览器中的方法来实现) 的特性,这也是 Vue 不支持 IE8 及更低版本浏览器的 缘由。vue

这些 getter/setter 对用户来讲是不可见的,可是在内部他们让 Vue 可以追踪依赖, 在属性被访问和修改时通知变动。数组

每一个组件实例都对应一个 watcher 实例,它会在组件渲染的过程当中把“接触”过的数据属性记录为依赖。以后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件从新渲染。浏览器

响应示意图

检测变化的注意事项

受现代 JavaScript 的限制 (并且 Object.observe 也已经被废弃),Vue 没法检测到对象属性的添加或删除。因为 Vue 会在初始化实例时对属性执行 getter/setter 转化,因此属性必须在 data 对象上存在才能让 Vue 将它转换为响应式的。例如:bash

var vm = new Vue({
  data:{
    a:1
  }
})

// `vm.a` 是响应式的

vm.b = 2
// `vm.b` 是非响应式的
复制代码

对于已经建立的实例,Vue 不容许动态添加根级别的响应式属性(实例的一级属性,data 中的属性)。可是,可使用 Vue.set(object, propertyName, value) 方法向嵌套对象(对象里包含对象,如:数组中含有对象元素,对象中含有对象属性)添加响应式属性。例如,对于:异步

Vue.set(vm.someObject, 'b', 2)
复制代码

还可使用 vm.$set 实例方法,这也是全局 Vue.set 方法的别名:async

this.$set(this.someObject,'b',2)
复制代码

补充: Vue.set/this.$set 的应用 当vue的data里边声明或者已经赋值过的对象或者数组(数组里边的值是对象)时,向对象中添加新的属性,若是更新此属性的值,是不会更新视图的。 对于已经建立的实例,Vue 不容许动态添加根级别的响应式属性,意味着若是在实例建立以后添加新的属性到实例上,Vue 不能经过 getter/setter 进行转换,它不会进行响应式处理触发视图更新。 如例:在实例中建立属性 testObjide

<template>
    <div>
        <p >{{testObj.a}}</p>
        <p @click="add(testObj)"> {{testObj.c}}</p>
    </div>
</template>

 <script>
export default {
    data() {
        return {
            testObj: {}
        }
    },
    mounted() {
        this.testObj = { a: 0, b: 1 };
        this.testObj.c = 2;
        console.log(this.testObj, this.testObj.a, this.testObj.b, this.testObj.c);
      },
      methods: {
        add(obj) {
          // obj.a = obj.a + 2;
          obj.c = obj.c + 4;
          console.log(this.testObj, this.testObj.a, this.testObj.b, this.testObj.c);
    },
}
 </script>
复制代码

控制台中打印结果: 函数

能够看到属性 a 和 b 具备 get、set 方法,c 没有。点击c对应的增值方法,界面结果以下: ui

打印结果以下:

能够看到,在方法中去直接修改 c 属性,没有更新视图,可是值发生了变化,当把add方法里 obj.a = obj.a + 2;语句放开,视图中 a、c 都发生变化

打印出的 a、c 值也发生变化:

这时候将mounted方法里 this.testObj.c = 2; 改为 this.$set(this.testObj, "c", 2),打印出结果为:

这时候出现了 get/set,能够直接修改 c 属性触发视图变化。 当须要新增对象内的响应式属性时,能够不关心其余原有属性。

数组相关的参考文档:(cn.vuejs.org/v2/guide/li…)

有时你可能须要为已有对象赋值多个新属性,好比使用 Object.assign()_.extend()。 可是,这样添加到对象上的新属性不会触发更新。在这种状况下,你应该用原对象与要混合进去的对象的属性一块儿建立一个新的对象。

// 代替 `Object.assign(this.someObject, { a: 1, b: 2 })`
this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 })
复制代码

声明响应式属性

因为 Vue 不容许动态添加根级响应式属性,因此你必须在初始化实例前声明全部根级响应式属性,哪怕只是一个空值:

var vm = new Vue({
  data: {
    // 声明 message 为一个空值字符串
    message: ''
  },
  template: '<div>{{ message }}</div>'
})
// 以后设置 `message`
vm.message = 'Hello!'
复制代码

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

这样的限制在背后是有其技术缘由的,它消除了在依赖项跟踪系统中的一类边界状况,也使 Vue 实例能更好地配合类型检查系统工做。但与此同时在代码可维护性方面也有一点重要的考虑:data 对象就像组件状态的结构 (schema)。提早声明全部的响应式属性,可让组件代码在将来修改或给其余开发人员阅读时更易于理解。

异步更新队列

Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的全部数据变动。若是同一个 watcher 被屡次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免没必要要的计算和 DOM 操做是很是重要的。而后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工做。Vue 在内部对异步队列尝试使用原生的 Promise.thenMutationObserversetImmediate,若是执行环境不支持,则会采用 setTimeout(fn, 0) 代替。

例如,当你设置 vm.someData = 'new value',该组件不会当即从新渲染。当刷新队列时,组件会在下一个事件循环“tick”中更新。多数状况咱们不须要关心这个过程,可是若是你想基于更新后的 DOM 状态来作点什么,这就可能会有些棘手。虽然 Vue.js 一般鼓励开发人员使用“数据驱动”的方式思考,避免直接接触 DOM,可是有时咱们必需要这么作。为了在数据变化以后等待 Vue 完成更新 DOM,能够在数据变化以后当即使用 Vue.nextTick(callback)。这样回调函数将在 DOM 更新完成后被调用。例如:

<div id="example">{{message}}</div>
复制代码
var vm = new Vue({
  el: '#example',
  data: {
    message: '123'
  }
})
vm.message = 'new message' // 更改数据
vm.$el.textContent === 'new message' // false
Vue.nextTick(function () {
  vm.$el.textContent === 'new message' // true
})
复制代码

在组件内使用 vm.$nextTick() 实例方法特别方便,由于它不须要全局 Vue,而且回调函数中的 this 将自动绑定到当前的 Vue 实例上:

Vue.component('example', {
  template: '<span>{{ message }}</span>',
  data: function () {
    return {
      message: '未更新'
    }
  },
  methods: {
    updateMessage: function () {
      this.message = '已更新'
      console.log(this.$el.textContent) // => '未更新'
      this.$nextTick(function () {
        console.log(this.$el.textContent) // => '已更新'
      })
    }
  }
})
复制代码

由于 $nextTick() 返回一个 Promise 对象,因此你可使用新的 ES2016 async/await 语法完成相同的事情:

methods: {
  updateMessage: async function () {
    this.message = '已更新'
    console.log(this.$el.textContent) // => '未更新'
    await this.$nextTick()
    console.log(this.$el.textContent) // => '已更新'
  }
}
复制代码
相关文章
相关标签/搜索