Vue响应式及其缺陷

编辑推荐: 掘金是一个高质量的技术社区,从 CSS 到 Vue.js,性能优化到开源类库,让你不错过前端开发的每一个技术干货。 点击链接查看最新前端内容,或到各大应用市场搜索「 掘金」下载APP,技术干货尽在掌握中。

我们喜欢Vue的原因之之就是其响应式系统。如果改变一个数据值,它会触发一个页面的更新来反映这个变化。

例如:

let app = new Vue({
    el: '#app',
    data () {
        return {
            message: 'Hello World!'
        }
    }
})

setTimeout(() => {
    app.message = 'Goodbye World!'
}, 2000)

<div id="#app">
    <h1>{{ message }}</h1>
</div>

比如这个示例,数据datamessage是响应式的,这也意味着,如果它被更改,将会触发页面的重新渲染。

Vue响应式及其缺陷

页面一开始渲染出来的是Hello World。然后2000ms之后,message的值换成了Goodby World。由于message的值改更了,所以页面{{ message }}会重新渲染。看到的效果就如上图所示。

自动响应式配置中的陷阱

当你创建数据属性,计算属性和绑定属性等,Vue会自动配置响应式:

  • 节约了我们的时间
  • 使我们的代码更简洁
  • 有助于减少我们的认知

简言之就是让事情变得简单。这种简单却又会让我们掉到陷阱中,就像自动汽车一样,自动响应让我们变得懒惰,当它不工作的时候,我们并不知道是为什么?

好的响应式变化的时候

UItimate Vue.js Developers系列教程中的一位学生提了一个很有趣的问题。他正在研究Vue海报店,要求你使用Vue制作一个购物车

商店里展示的产品最初是这样表示的:

var myProduct = {
    id: 1,
    name: 'My Product',
    price: 9.99
}

但是,当你在购物车中添加产品时,需要一个数量,他是这样处理的:

function addToCart (id) {
    var item = this.cart.findById(id);
    if (item) {
        item.qty++
    } else {
        item.qty = 1;
        this.cart.push(item)
    }
}

addToCart(myProduct.id)

该方法的逻辑如下:

  • 在购物车中找到商品
  • 如果它在那里,那么增加数量
  • 如果它不在那里,给它一个数量1,然后把它加到购物车里

问题出现了

购物车模板仅显示购物车项目列表:

<ul class="shopping-cart">
    <li v-for="item in cart"> {{ item.name }} x {{ item.qty }} </li>
</ul>

问题是,无论qty的值是什么,模板总是显示其值为1

我最初以为addToCart函数的逻辑有问题。但调试后我才发现qty属性的值增加时都会调用该方法,这不是问题。

响应式如何覆盖

在大多数情况下,Vue的响应式系统会让人觉得方便,但当它不像你想象的那样工作时,会引起混乱和让人沮丧。

如果你理解它是如何工作的,这在很大程度上是可以避免上面提到的现象。

gettersetter

JavaScript对象的默认行为是直接访问或修改其属性,比如:

var myObj = {
    a: 'Hello World'
}

console.log(myObj.a) // => Hello World

但是,使用gettersetter这些方法将覆盖对象的默认行为。如果你不知道我在说什么,那建议你阅读一些关于gettersetter的教程。

当创建一个Vue实例时,每个数据属性、组件属性等都是可以遍历的,并为每个数据属性添加了gettersettergettersetter允许Vue观察数据的更改并触发更新。

reactiveSetter()

回到我们的product对象,我们可以这样定义它:

{
    id: 1,
    name:  'My Item',
    price: 9.99
}

Vue实例化后,我们可以在控制台中查看该对象,并查看Vue在其上定义的gettersetter

Vue响应式及其缺陷

gettersetter函数可以做很多事(查看源码),但是rectiveSetter的任务之一就是触发一个更改通知,它会导致页面重新渲染。

警告

这是一个很棒的系统,尽管是一个错误系统。如果你在Vue实例化后添加(或删除)一个属性(例如在方法或生命周期钩子中),Vue是不知道它的。

// In the addToCart method, called after instantiation
myProduct.qty = 1;

看,虽然qty是在对象上定义的,但它没有gettersetter

Vue响应式及其缺陷

更新响应式对象

在购物车示例中,解决问题的方法是在添加到购物车时创建一个新对象,而不是添加属性。这是为了确保Vue有机会定义响应式的gettersetter

function addToCart (id) {
    var item = this.cart.findById(id);
    if (item) {
        item.qty++
    } else {
        // 不要添加一个属性,比如 item.qty = 1
        // 创建一个新的对象来替代它
        var newItem = {
            id: item.id,
            name: item.name,
            price: item.price,
            qty: 1
        }
        this.cart.push(item)
    }
}

addToCart(myProduct.id)

Vue.set

但是,如果你不想创建一个新的对象,你可以使用Vue.set设置一个新的对象属性。该方法确保将属性创建为一个响应式属性,并触发视图更新:

function addToCart (id) {
    var item = this.cart.findById(id);
    if (item) {
        item.qty++
    } else {
        // 不要直接添加一个属性,比如 item.qty = 1
        // 使用Vue.set 创建一个响应式属性
        Vue.set(item, 'qty', 1)
        this.cart.push(item)
    }
}

addToCart(myProduct.id)

数组

像对象一样,数组也是响应式的,并观察到其变化。与对象一样,数组也有用于操作的警告。Vue也可以封装数组方法,比如pushsplice等,它们也会触发视图的更新。

当直接使用索引(index)设置数组项时,这是不太可能的:

app.myArray[index] = newVal;

同样可以使用Vue.set来设置数组项:

Vue.set(app.myArray, index, newVal);

本文根据@ANTHONYGORE 的《Reactivity In Vue.js (And Its Pitfalls)》所译,整个译文带有我们自己的理解与思想,如果译得不好或有不对之处还请同行朋友指点。如需转载此译文,需注明英文出处:https://vuejsdevelopers.com/2017/03/05/vue-js-reactivity/

大漠

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

如需转载,烦请注明出处:https://www.w3cplus.com/vue/vue-reactivity-and-pitfalls.html

返回顶部