# vue 数据与视图更新

# 场景

vue 数据 data 更新了,但是视图没有更新
其中缘由在于对 vue 的响应式原理的理解偏差

# 追踪变化

把一个普通的 JavaScript 对象传给 Vue 实例的 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter
这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 追踪依赖,在属性被访问和修改时通知变化。 每个组件实例都有相应的 watcher 实例对象,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会通知 watcher 重新计算,从而致使它关联的组件得以更新。

数据更新

# 声明响应性属性

Vue 中,一般只有在 data 选项中声明的属性(或者是属性的属性)才是具有响应特性的。如果需要在 data 选项之外对已有属性添加具有响应特性的属性,需要用到 Vueset 方法。

var vm = new Vue({
  data: {
    a: {              //a就是根级属性,不可动态添加
      b: 0          //b就是属性的属性,可以动态添加
    }
  }
})

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

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

何为响应特性?就是当我们更改 data 中的值的时候,HTML 与之绑定的部分会随之更新的特性。

# 数组更新检测

# 变异方法替换数组

Vue 包含一组观察数组的变异方法,所以它们也将会触发视图更新。这些方法如下:

  • push()
  • pop()
  • shift()
  • unshift()
  • splice()
  • sort()
  • reverse()

打开控制台,然后用 items 数组调用变异方法:example1.items.push({ message: 'Baz' })

# 非变异方法替换数组

变异方法 (mutation method),顾名思义,会改变被这些方法调用的原始数组。相比之下,也有非变异 (non-mutating method) 方法,例如:filter(), concat()slice() 。这些不会改变原始数组,但总是返回一个新数组。当使用非变异方法时,可以用新数组替换旧数组:

example1.items = example1.items.filter(function (item) {
  return item.message.match(/Foo/)
})

你可能认为这将导致 Vue 丢弃现有 DOM 并重新渲染整个列表。幸运的是,事实并非如此。Vue 为了使得 DOM 元素得到最大范围的重用而实现了一些智能的、启发式的方法,所以用一个含有相同元素的数组去替换原来的数组是非常高效的操作。

# 注意事项

由于 JavaScript 的限制,Vue 不能检测以下变动的数组:

  1. 当你利用索引直接设置一个项时,例如:vm.items[indexOfItem] = newValue
  2. 当你修改数组的长度时,例如:vm.items.length = newLength

举个栗子:

var vm = new Vue({
  data: {
    items: ['a', 'b', 'c']
  }
})
vm.items[1] = 'x' // 不是响应性的
vm.items.length = 2 // 不是响应性的

为了解决第一类问题,以下两种方式都可以实现和 vm.items[indexOfItem] = newValue 相同的效果,同时也将触发状态更新:

// Vue.set
Vue.set(vm.items, indexOfItem, newValue)

你也可以使用 vm.$set 实例方法,该方法是全局方法 Vue.set 的一个别名:

vm.$set(vm.items, indexOfItem, newValue)

为了解决第二类问题,你可以使用 splice

vm.items.splice(newLength)

# 对象更新检测

还是由于 JavaScript 的限制,Vue 不能检测对象属性的添加或删除:

var vm = new Vue({
  data: {
    a: 1
  }
})
// `vm.a` 现在是响应式的

vm.b = 2
// `vm.b` 不是响应式的

你可以添加一个新的 age 属性到嵌套的 userProfile 对象:

Vue.set(vm.userProfile, 'age', 27)

你还可以使用 vm.$set 实例方法,它只是全局 Vue.set 的别名:

vm.$set(vm.userProfile, 'age', 27)

有时你可能需要为已有对象赋予多个新属性,比如使用 Object.assign()_.extend()。在这种情况下,你应该用两个对象的属性创建一个新的对象。所以,如果你想添加新的响应式属性,不要像这样:

Object.assign(vm.userProfile, {
  age: 27,
  favoriteColor: 'Vue Green'
})

你应该这样做:

vm.userProfile = Object.assign({}, vm.userProfile, {
  age: 27,
  favoriteColor: 'Vue Green'
})

# 数据响应式的几个例子

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

// `vm.a` 是响应的

vm.b = 2
// `vm.b` 是非响应的

a 就是在 data 中声明的具有响应特性的属性,而 b 就不是。

var vm = new Vue({
  data: {
    a: {
      a1:''
    }
  },
  methods: {
    change:function(){
      this.a.a1 = "text1"    //a1就是响应式的
      this.a.a2 = "text2"    //a2就不是响应式的
    }
  }
})

a2 虽然不是响应式的,但它却是可以在 HTML 部分被渲染更新出来。这里就是一个比较容易掉进去的坑。由于 Vue 是异步执行 DOM 更新,虽然更新的动作是由 this.a.a1 = "text1" 触发,可动作的完成是在 this.a.a2 = "text2" 之后,下面有详细解析。

var vm = new Vue({
  data: {
    a: {
      a1:''
    }
  },
  methods: {
    change: function () {
      this.a.a1 = "text1"    //a1就是响应式的
      this.a.a2 = "text2"    //a2就不是响应式的
      var that = this;
      setTimeout(function () {
        that.a.a3 = 'new text'; //这里与a2是相同的,区别在于这里的a3并不会被渲染到DOM中
        that.$set(that.a, 'a4', 'new text');    //这是正确的添加属性的方法
        that.a = {                              //这种写法与a2不同,a5可以被更新到DOM中
          a5: 'hahaha'
        }
      }, 300);
    }
  }
})

# 异步更新带来的数据响应式误解

异步数据的处理基本是一定会遇到的,处理不好就会遇到数据不更新的问题,但有一种情况是在未正确处理的情况下也能正常更新,这就会造成一种误解,详情如下所示:

new Vue({
    el: '#app',
    data: {
        dataObj: {}
    },
    ready: function () {
        var self = this;

        /**
         * 异步请求模拟
         */
        setTimeout(function () {
            self.dataObj = {}; 
            self.dataObj['text'] = 'new text';
        }, 3000);
    }
})

上面的代码非常简单,我们都知道 vue 中在 data 里面声明的数据才具有响应式的特性,所以我们一开始在 data 中声明了一个 dataObj 空对象,然后在异步请求中执行了两行代码,如下:

self.dataObj = {}; 
self.dataObj['text'] = 'new text';

首先清空原始数据,然后添加一个 text 属性并赋值。到这里为止一切都如我们所想的,数据和模板都更新了。

那么问题来了,dataObj.text 具有响应式的特性吗?

模板更新了,应该具有响应式特性,如果这么想那么你就已经走入了误区,一开始我们并没有在 data 中声明 .text 属性,所以该属性是不具有响应式的特性的。

但模板切切实实已经更新了,这又是怎么回事呢?

那是因为 vuedom 更新是异步的,即当 setter 操作发生后,指令并不会立马更新,指令的更新操作会有一个延迟,当指令更新真正执行的时候,此时 .text 属性已经赋值,所以指令更新模板时得到的是新值。
具体流程如下所示:

  • self.dataObj = { }; 发生 setter 操作
  • vue 监测到 setter 操作,通知相关指令执行更新操作
  • self.dataObj['text'] = 'new text'; 赋值语句
  • 指令更新开始执行

所以真正的触发更新操作是 self.dataObj = { }; 这一句引起的,所以单看上述例子,具有响应式特性的数据只有 dataObj 这一层,它的子属性是不具备的。

注:其实 vue 文档中已经有说明,对于新增以及删除的属性,vue 是无法监测到的。

var a = {};

a.b = 0;    //新增b属性
a = {
    c: 0
};              //更改a属性的值

上述两种赋值方式对 vue 造成的影响是不同的。

对比示例:

new Vue({
    el: '#app',
    data: {
        dataObj: {}
    },
    ready: function () {
        var self = this;

        /**
         * 异步请求模拟
         */
        setTimeout(function () {
            self.dataObj['text'] = 'new text';
        }, 3000);
    }
})

上述例子的模板是不会更新的。

🍭 Vue.$set

通过 $set 方法可以将添加一个具备响应式特性的属性,并且其子属性也具备响应式特性,但是必须是新属性才可以,如果是本身已有的属性该方法是不起作用的。

new Vue({
    el: '#app',
    data: {
        dataObj: {}
    },
    ready: function () {
        var self = this;

        /**
         * 异步请求模拟
         */
        setTimeout(function () {
            var data = {
                name: 'xiaofu',
                age: 18
            };
            var data01 = {
                name: 'yangxiaofu',
                age: 19
            };
            self.dataObj['person'] = {};
            self.$set('dataObj.info', data);
            self.$set('dataObj.person', data01); 
        }, 3000);
    }
})

如上所示,.person 属性是不具备响应式特性的。

上次更新: 9/14/2020, 10:06:06 AM