『 Vue 小 Case 』- 别被字面量 Prop 坑了

用过 Vue 的各位,对于 Prop 一定不会陌生,相信大家都能够信手拈来。但就是这么一个大家都熟悉的 Prop,有时候也会把我们坑了。在介绍这个坑之前,我们先再来温习一下 Prop 的基础用法。

第一部分“Prop 的基础用法”部分,比较熟悉的朋友,可以直接跳过,从第二部分开始即可。

一、Prop 的基础用法

1.1 Prop 的大小写

Vue 官方文档的Prop 章节[1]第一段就重点强调了 Prop 的大小写问题。HTML 中的特性名是大小写不敏感的,所有的大写字母都会被浏览器解释成小写字母

文档指出在 DOM 中使用模板时,驼峰命名法的 prop 名需要使用对应的短横线分隔命名。但是如果是使用字符串模板,则不存在这个限制。下面我们来试一下( 代码示例链接 [2])。

HTML:

<div id="app">    <h2>      HTML 中的特性名是大小写不敏感的,      <br>      所有的大写字母都会被浏览器解释成小写字母。    </h2>    <!-- 这种写法,blog-post组件内postTitle的值为空 -->    <blog-post postTitle="『 Vue小Case 』- 别被字面量Prop坑了"></blog-post>    <!-- 短横线分隔则postTitle的值不为空 -->    <blog-post post-title="『 Vue小Case 』- 别被字面量Prop坑了"></blog-post>  </div>  

JavaScript:

Vue.component('blog-post', {    props: ['postTitle'],    template: '<h3>{{ postTitle || "o(╥﹏╥)o 未传入标题" }}</h3>',  })    new Vue({    el: '#app',  })  

效果图如下:

嗯,一番操作下来,正如文档所说,也符合我对于 HTML 文档的认知。但是要注意一点,如果你用的是 Vue 的单文件组件方式来试的话,你可能得不到期望的结果,这是为什么呢?因为 Vue 的单文件组件模式属于上面所说的使用字符串模板的方式,单文件组件会通过 Vue-Loader 进行编译。

1.2 Props 选项的写法

首先,最简单的方式,我们可以通过数组的形式,列出所有的 Props 字段。

props: ['title', 'category', 'author', 'likes', 'comments']  

其次,如果希望指定 prop 的值类型,则可以以对象形式列出 prop,属性的名称对应 prop 的名称,属性值对应 prop 的类型。如下所示:

props: {    title: String,    category: [String, Number], // 可以同时指定多个类型    author: Object,    likes: Number,    comments: Array  }  

注意,所有 JavaScript 中的原生构造函数以及自定义的构造函数都可以作为值类型来指定。内部是通过instanceof来进行检查的。此外,基础类型中的nullundefined 会通过任何类型验证。

最后,我们还有做更多的事情,比如默认值的设定、是否必填、自定义校验规则。如下:

props: {    title: {  	type: String,  	required: true, // 文章标题必填    },    // 指定默认值    category: {  	type: [String, Number],  	dafault: 0, // 默认分类为其他分类    },    author: {  	type: Object,  	// 对象或数组类型的属性默认值必须通过函数生成  	default() {  	  return {name: '小歪|very_much'}  	},    },    propFn: {  	type: Function,  	// 与上面的author对比,  	// 如果type为Function,可以直接如下指定  	// 看着挺奇怪的,其实就是default的值是个函数。  	default() {}, // 等价于 default: function() {}    },    likes: {  	type: Number,  	// 自定义校验函数,喜欢数不能小于0  	validator(value) {  	  return value >= 0  	}    },    comments: Array,  }  

注意,组件的 prop 会在一个实例创建之前进行验证,所以实例的属datacomputed等) 在defaultvalidator函数中是不可用的

二、对象字面量的坑

Vue 的 Prop 文档中详细介绍了如何传入各种值类型以及如何传入一个对象的所有属性[3]

其中可以通过如下的方式传入一个对象:

<!-- 即便对象是静态的,我们仍然需要 `v-bind` 来告诉 Vue -->  <!-- 这是一个 JavaScript 表达式而不是一个字符串。-->  <blog-post    v-bind:author="{      name: 'Veronica',      company: 'Veridian Dynamics'    }"  ></blog-post>    <!-- 用一个变量进行动态赋值。-->  <blog-post v-bind:author="post.author"></blog-post>  

如上,可以通过对象字面量的方式传入属性,也可以通过变量的方式传入。这两种方式在一般情况下都没有问题。但如果在下面的这种场景下,通过对象字面量来传值就会出现意料之外的问题。

2.1 场景复现

假设我们需要在<blog-post>文章详情页的最下方会展示相关文章的列表。如下所示:

假设我们的<blog-post>组件内容如下:

<template>    <div class="blog-post">      <h2>{{title}}</h2>      <p>{{content}}</p>      <button @click="addLikeCount">喜欢{{likes}}</button>      <relative-blogs :params="{type: '1'}">    </div>  </template>    <script>  export default {    data: function() {      return {        title: '文章标题',        content: '文章内容:Lorem ipsum dolor sit amet consectetur adipisicing elit. Magnam praesentium consectetur cupiditate in sit sunt animi voluptas laudantium officiis earum illum ullam doloribus perspiciatis, molestiae quidem aspernatur, molestias at nulla!',        likes: 0,      }    },    methods: {  	addLikeCount: function() {  	  this.likes++  	}    }  }  </script>  

子组件<relative-blogs>内容如下:

<template>    <h3>相关文章列表(更新次数{{updateCount}})...</h3>  </template>    <script>  export default {    props: ['params'],    data: function() {      return {        updateCount: 0      }    },    watch: {      // 如果params变化,则重新获取相关文章  	// 此处近通过count来计数      params: function() {        this.updateCount++      }    }  }  </script>  

上述的代码逻辑中,在<blog-post>父组件中,会向子组件传递获取相关文章的参数。此外,文章正文还提供了一个“喜欢”按钮(为了便于演示,喜欢次数可以一直增加)。

查看代码示例[4],上述的逻辑应该是正常可用的。如果你仔细观察你应该会发现,在我们点击喜欢的时候,相关文章列表也会相应的更新。如下图所示:

单纯从上面来看,如果我不把更新次数显示出来,大家一定不会发现相关文章列表被更新了这么多次。

但如果我们在相关文章列表获取数据的时候加上了 loading 效果,那么大家就一定能够发现了,而这种情况显然是不能够接受的。

每次点击喜欢的时候为什么都会触发更新文章列表呢?

2.2 原因分析

其实原因很简单,当我们点击喜欢时,会更新<blog-post>中的likes的值,此时template 中对likes存在依赖,所以<blog-post>会触发更新。

<blog-post>更新的同时,因为<relative-blogs>params是通过对象字面量的形式传入的,新的值与旧的值虽然看上去相同,但是是不同的引用,所以会触发子组件的更新,同时触发 watch 监听。

想规避这一问题也很简单,即将上面通过字面量传值的方式改为变量传值(代码示例[5]),<blog-post>发生更新的时候就不会触发<relative-blogs>的更新了。如下图所示:

最后,值得注意的是,上面我加粗强调了template 中对likes存在依赖这句话。这句话有没有什么特殊的意义呢?

显然是有的,如果在 template 中没有依赖likes时,<blog-post>组件是不会触发更新的,从而也不会影响到<relative-blogs>,感兴趣的话,可以通过示例代码[6]看一下。这是因为 Vue 不会因为无意义的值,触发组件的更新。

三、总结

如上,通过字面量传入数组或者对象作为 prop 值时,会存在一定的隐患,往往会让我们不经意间掉坑里。

在组件内部如果不 watch 这个 prop、不依赖这个 prop 进行 computed 以及不执行 updated 钩子函数,或者不再这三种情况下执行比较显眼的操作(如触发请求、页面刷新 loading 等),不会有什么大的影响,也不容易发现问题,只是会造成一些不必要的计算。

虽然在不发生故障的情况下,影响没有太大,但这终归不是一种好的用法。所以笔者建议在日常的开发中,我们还是尽量通过变量的方式向对象/数组类型的 prop 传值,避免掉坑。

参考链接

  1. https://cn.vuejs.org/v2/guide/components-props.html#传入一个对象的所有属性
  2. https://forum.vuejs.org/t/props/25496/5

文内链接

[1]Prop章节: https://cn.vuejs.org/v2/guide/components-props.html

[2]代码示例链接 : https://jsbin.com/gelaqobixe/1/edit?html,js,output

[3]对象的所有属性: https://cn.vuejs.org/v2/guide/components-props.html#%E4%BC%A0%E5%85%A5%E4%B8%80%E4%B8%AA%E5%AF%B9%E8%B1%A1%E7%9A%84%E6%89%80%E6%9C%89%E5%B1%9E%E6%80%A7

[4]代码示例: https://jsbin.com/wazemak/3/edit?html,js,console,output

[5]代码示例: https://jsbin.com/kadiyaz/edit?html,js,console,output

[6]示例代码: https://jsbin.com/guzifiw/edit?html,js,console,output