Vue 2.0的学习笔记: Vue中的响应式

发布于 大漠

上一节学习了Vue中的代理,知道了Vue中代理数据和方法,今天我们接着来了解Vue中的响应式。我们将使用上一篇中的示例来继续介绍Vue中响应式相关的知识。同样的,将大部分的时间用在浏览器的控制台上。

<div id="app">
    <h1>Hello! {{ firstName }}</h1>
</div>

let app = new Vue({
    el: '#app',
    data () {
        return {
            firstName: '大漠',
            lastName: 'W3cplus',
            axisX: 1,
            axisY: 2
        }
    },
    methods: {
        fullName: function () {
            console.log( this.firstName + '_' + this.lastName )
        }
    },
    computed: {
        axisZ: function () {
            return this.axisX + this.axisY
        }
    }
})

让我们输出Vue实例上的$data属性。

Vue中的响应式

在这里,我们可以看到Vue自动添加了响应式gettersetter。当你在实例化一个Vue实例时传入一个data对象时,Vue会遍历所有属性并将它们转换为gettersetter。当你使用Vue时,这是完全透明的,除非你深入到Vue的内部工作中,否则你不会注意到这个。

那么这些gettersetter是什么意思呢?简单地说,它们就是当数据属性发生变化时DOM会更新的原因。使用这些响应式,Vue可以检测何时更改,以及何时访问属性。最重要的是,它们允许Vue跟踪数据属性之间的依赖关系。

为了更详细地讲一下,我准备了一张图,用这张图来说明我刚才所说的。

Vue中的响应式

首先,我们有包含每个数据属性的gettersetter函数的数据。当调用setter时,它为数据属性设置一个新值,并通知一个观察者,它被挂载到Vue实例上。这个观察者负责对数据变化做出响应。当某些东西发生变化时,它通过调用渲染函数来触发模板重新渲染,这个函数基本上是一个负责重新渲染一个Vue实例或组件的函数。这对我们来说,并不重要,所以也不在这里阐述过多,但重要的是重新渲染模板。当渲染或处理模板时,会更新所谓的虚拟DOM。这也是DOM更新之前的步骤。而这里其中的原理,我们后续再深入学习。当渲染时,数据属性会被“触摸”,这意味着getter函数被调用。这使Vue有够解析依赖项并通知观察者。通过这个,Vue知道要观察哪些属性来进行更改,当值发生变化时,观察者会做出适当的响应。

也就是说,Vue使用gettersetter来解决依赖性,最重要的是对数据更改进行重新激活。这就是我们所需要知道的。其余的都很好,因为它可以帮助你了解幕后发生了什么,但在与Vue一起工作时,并不需要知道这一点。

现在回到我们实例的代码中来,因为想向大家展示一些重要的东西。你是滞注意到,在整个过程中,如果我们最初没有赋值给它,我们总是声明具有空值的数据属性?你可能会想,这是否真的是必要的。正如你现在看到的,这确实是必要的。

我们尝试删除firstName数据属性并动态添加它,只需要使用app变量。

app.firstName = '大漠'

这个时候在控制台上就会看到一个警告,我们使用的是未在Vue实例上定义的数据属性。它还指出,我们必须在数据对象中声明它。

Vue中的响应式

在这样做之前,让我们看一些更有趣的东西,在模板中添加一个按钮,当这个按钮被点击时,它会调用fullName方法。

<button @click="fullName">Click Me!</button>

当我点击按钮时,控制台会报错误信息。

如果我们再次检查Vue实例,并查找fistName属性,我们会看到它确实被添加到Vue实例中。

Vue中的响应式

但是,你可能也注意到属性没有与之关联的gettersetter,这也意味着没有将代理函数添加到此属性中。同样,如果我们查看$data属性,我们将看到没有firstName任何标志,这意味着它没有做出响应。

Vue中的响应式

这主要是因为Vue无法检测何时添加或删除属性。由于Vue在初始化Vue实例时将数据属性转换为gettersetter,因此在初始化过程中必须在数据对象中显示属性。否则,Vue不能添加代理或响应式函数。虽然在Vue实例上确实可以使用该属性,但是当单击按钮时,属性将不会发生响应。如果我们改变它,与Vue实例相关联的观察者将不会被通知,我们也不能在模板中使用它。

正如你所看到的,确实有一个很好的理由,为什么我们使用一个空值为初始化数据属性,你只需要这样做就可以使用Vue的响应式。

虽然不可能动态地向Vue实例添加新的吶应式顶层数据属性,但是可以向嵌套对象添加一个新的响应式属性。我们之前看到过的set方法,这可以通过使用全局Vue实现。

上面看到的只是Vue中的一些基本知识,我们接下来看看其中的一些简单原理。

如何追踪变化

把一个普通JavaScript对象传给Vue实例的data选项,Vue将遍历此对象所有的属性,并使用Object.defineProperty把这些属性全部转为gettersetter

用户看不到gettersetter,但是在内部它们让Vue追踪依赖,在属性被访问和修改时通知变化。这里需要注意的问题是浏览器控制台在打印数据对象时gettersetter的格式并不同,所以你可以需要安装vue-devtools 来获取更加友好的检查接口。

每个组件实例都有相应的watcher实例对象,它会在组件渲染的过程中把属性记录为依赖,这后当依赖项setter被调用时,会通知watcher重新计算,从而致使它关联的组件得以更新。

Vue中的响应式

变化检测问题

受现代JavaScript的限制(以及废弃Object.observe),Vue不能检测到对象属性的添加或删除。由于Vue会在初始化实例时对属性执行gettersetter转化过程,所以属性必须在data对象上存在才能让Vue转换它,这样才能让它是响应的。比如前面的示例中有提到过:

let app = new Vue({
    el: '#app',
    data () {
        return {
            firstName: '大漠' // 这个firstName是响应式的
        }
    }
})

app.lastName = 'W3cplus_大漠' // 这个lastName不是响应式的

Vue不允许在已经创建的实例上动态添加新的根级响应式属性。然而它可以使用Vue.set(object, key, value)方法将响应属性添加到嵌套的对象上:

Vue.set(app.data, 'lastName', 'W3cplus')

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

this.$set(this.data, 'lastName', 'W3plus')

有时你想向已有对象上添加一些属性,例如使用Object.assign()_.extend()方法来添加属性。但是,添加到对象上的新属性不会触发更新。在这种情况下可以创建一个新的对象,让它包含原对象的属性和新的属性:

// 代替 Object.assign(this.data, {firstName: '大漠', lastName: 'W3cplus'})
this.data = Object.assign({}, this.data, {firstName: '大漠', lastName: 'W3cplus'})

声明响应式属性

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

let app = new Vue({
    data: {
        message: ''
    },
    template: '<div> {{ message }} </div>'
})
// 之后设置message
app.message = 'Hello!'

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

这样的限制在背后是有其技术原因的,它削除了在依赖项跟踪系统中的一类边界情况,也使Vue实例在类型检查系统的帮助下运行的更高效。而且在代码可维护性方面也有一点重要的考虑:data对象就像组件状态的概要,提前声明所有的响应式属性,可以让组件代码在以后重新阅读或其他开发人员阅读时更易于被理解。

异步更新队列

你可能还没有注意到,Vue异步执行DOM更新。只要观察到数据变化,Vue将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。如果同一个观察者被多次触发,只会一次推入到队列中。这种在缓冲时去除重复数据对于避免不必要的计算和DOM操作上非常重要。然后,在下一个的事件循环tick中,Vue刷新队列并执行实际(已去重的)工作。Vue在内部尝试对异步队列使用原生的Promise.thenMutationObserver,如果执行环境不支持,会采用setTimeout(fn, 0)代替。

例如,当你设置app.data = 'new value',该组件不会立即重新渲染。当刷新队列时,组件会在事件循环队列清空时的下一个tick更新。多数情况我们不需要关心这个过程,但是如果你想在DOM状态更新后做点什么,这就可能会有些棘手。虽然Vue通常鼓励开发人员沿着数据驱动的方式思考,避免直接接触DOM,但是有时候我们确实要这么做。为了在数据变化之后等待Vue完成更新DOM,可以在数据变化之后立即使用Vue.nextTick(callback)。这样回调函数在DOM更新完成后就会调用。例如:

<div id="app"></div>

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

app.message = 'new message' // 更改数据
app.$el.textContent === 'new message' // false
Vue.nextTick(function () {
    app.$el.textContent === 'new message' // true
})

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

Vue.component('my-component', {
    template: `<h1>{{ message }}</h1>`,
    data () {
        return {
            message: 'not updated'
        }
    },
    methods: {
        updateMessage: function () {
            this.message = 'updated'
            console.log(this.$el.textContent) // => not updated
            this.$nextTick(function () {
                console.log(this.$el.textContent) // => updated
            })
        }
    }
})

大漠

常用昵称“大漠”,W3CPlus创始人,目前就职于手淘。对HTML5、CSS3和Sass等前端脚本语言有非常深入的认识和丰富的实践经验,尤其专注对CSS3的研究,是国内最早研究和使用CSS3技术的一批人。CSS3、Sass和Drupal中国布道者。2014年出版《图解CSS3:核心技术与案例实战》。

如需转载,烦请注明出处:https://www.fedev.cn/vue/vue-reactivity.htmlLebron Soldiers XI 11