阅读 152

Vue深入响应式原理

Vue最独特的特性之一,非侵入性的响应式系统。

如何追踪变化

当把一个普通的js对象传入Vue实例作为data选项,Vue将遍历此对象的所有属性,并使用Object.defineProperty把这些属性全部转为getter/sertter。

Object.defineProperty是ES5的特性,所以Vue不支持IE8及以下版本的浏览器。

这些getter/setter对用户来说不可见,但是内部却让Vue能够追踪依赖,在属性被访问和修改时通知变更。

不同浏览器在控制台打印数据对象时对getter/setter的格式化不同,所以建议用vue-devtools来更好的获取数据。

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

检测变化的注意事项

因为Object.observer已经被废弃,Vue无法检测到对象属性的添加或删除。 由于Vue会在初始化实例时,对属性执行getter/setter转化,所以属性必须在data对象上存在,才能让Vue将它转换为响应式的。

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

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

对于已经创建的实例,Vue不允许动态添加跟级别的响应式属性,但是可以使用Vue.set(object,properName,value)方法向嵌套对象添加响应式属性。

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

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

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

有时候可能需要为已有对象赋值多个新属性,比如使用Objct.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:''
    },
    template:'<div>{{ message }}</div>'
})

//之后设置message
vm.message = 'hello'
复制代码

如果没有在data中声明message,Vue将警告渲染函数正在试图访问不存在的属性。

这样的限制

  • 消除了在依赖项跟踪系统的一类边界情况
  • 使得Vue实例能更好配合类型检查系统工作

但是这样代码可维护性有考虑:data 对象就像组件状态的结构 (schema)。

异步更新队列

Vue在更新DOM时是异步执行的,只要监听到数据变化,Vue将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。

如果同一个watcher被多次触发,只会被推入到队列一次。

这种在缓冲时去除重复数据对于避免不必要的计算和dom操作是很重要的。

然后在下一个事件循环'tick'中,Vue刷新队列并执行实际,已去重的工作。

Vue在内部对异步队列尝试使用原生的Promise.then、MutationObserver和setImmediate,如果执行环节不支持,就采用setTimeout代替。

例如,当设置vm.someData = 'new value',该组件不会立即重新渲染。当刷新队列时,组件会在下一个事件循环tick中更新。

虽然避免接触DOM但是必须这样做。

为了在数据变化后等待Vue更新DOM,可以在数据变化之后立即使用Vue.nextTick(callback)。这样回调函数将在DOM更新完成后被调用。

var vm = new Vue({
    el:'#app',
    data:{
        message:'123'
    }
})

vm.message = 'new message'
vm.$el.textContent === 'new message' //false 
Vue.nextTick(function(){
    vm.$el.textContent === 'new message' //ture
})
复制代码

在组件内使用vm.$nextTick()实例方法特别方便,因为它不需要全局Vue,并且回调函数中的this会自动绑定到Vue实例上:

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

因为$nextTick()返回一个Promise对象,所以你可以使用新的async/await语法完成相同的事

methods: {
        async updateMessage() {
            this.message = '已更新'
            console.log(this.$el.textContent);//未更新
            await this.$nextTick()
            console.log(this.$el.textContent);//已更新
        }
    }
复制代码