# 在本文中,笔者又提炼了以下几个重点
- 补偿双向数据绑定 Vue.$set
- 数据侦听 Vue.$watch
- 表单绑定修饰符
- 动态组件
- 基础组件的自动化全局注册
- Vue.$emit参数,及与 v-on 事件命名规范
- Prop传递数据时防脏
- 插槽及高复用组件
# 补偿双向数据绑定 Vue.$set
官网说受JS限制,笔者觉得讲的太浅了。相信了解双向数据绑定原理的朋友都知道,Vue 2是依赖原生JS中Object.defineProperty()
方法的存取操作符set
即数据劫持实现数据实时更新。然而对于一些引用类型的数据,如果操作不是发生在已经定义好的数据结构本身,Vue 无法探测普通的新增属性 (比如 this.myObject.newProperty = 'hi'
),我们称它为不是响应式的。如:
(1) 对象属性的添加和删除时: 见案例
(2) 利用索引直接设置数组的一个项时: vm.items[2] = 'red'
(3) 修改数组长度时: vm.items.length = newLen
data () {
return {
user: { name: 'zfs', age: 25 }
}
},
mounted: {
this.user = { name: 'borui' } // 改变对象本身,触发setter
this.user.tall = 178 // 新增属性,未触发setter,视图不更新
}
为了解决这个问题,尤大大重写了set
方法,提供了$set
API接口。注意不要写成vm.$set(key, value)
形式,这种错误就略低级了。对比一下原生和API接口
原生方法:
Object.defineProperty(object, 'key’, descriptor)
API 接口:vm.$set(vm.Obj, 'key', 'value')
很容易发现,该接口原理将描述符descriptor
设置为set
, 将输入的新值value
作为参数传递给set
调用从而手动触发数据劫持。因此,上述案例需要改成:
mounted () {
this.$set(this.student, "tall", 178)
}
但是问题又来了,如果需要一次性增加多个新的响应式属性,显然多次调用$set
方法不是个很好的选择。通常我们会使用Object.assign()
或_extend()
来实现。Vue建议创建一个新的对象来存放两个合并对象的所有属性(通常用空对象{}
),然后再赋值给目标元素。而不是直接合并到目标元素上。做法即:
vm.user = Object.assign({}, vm.user, {
hobby: 'basketball',
favoriteColor: 'Green'
})
# 数据侦听 Vue.$watch
watch
提供了观察和响应实例上数据变动的办法,当有一些数据需要跟随其他数据变化而变化时,如子组件某个数据依赖来自于父组件的prop
计算。很直观的会想到计算这功能和计算属性十分类似。Vue建议用户使用计算属性,除非如下情况:
(1)当要执行的操作是异步操作时,
(2)相应事件是开销较大的操作时。
watch: {
question: function (newVal, oldVal) {
this.answer = 'Waiting for you to stop typing...'
axios.get('https://yesno.wtf/api')
.then(function (response) {
vm.answer = _.capitalize(response.data.answer)
})
.catch(function (error) {
vm.answer = 'Error! Could not reach the API. ' + error
})
}
}
}
当观察的值发生改变时, 观察者会接收到两个参数:(1) 新值,(2)原先的值。 值得注意的是, watch
在组件第一次被挂载时不会触发, 只有值被改变时才触发。
watch: {
selectedVal ( newVal, oldVal ) {
console.log(newVal)
}
}
# data选项为什么是一个函数?
Vue官网第一课描述的data
选项就是一个对象,为什么在编写组件的时候却要定义成一个函数?
我们知道对象是引用类型,而组件最大的特性就是可复用性,当一个组件被多次复用却指向同一个引用类型数据,组件间将无独立性而言。因此,将data选项
定义成一个函数,是为了利用函数的私有作用域特性实现不同组件间数据私有的效果
# 计算属性缓存 及 get()/set()方法
一个需要计算的数据,通常有: (1)计算属性获取,(2)定义一个方法实现。虽然实现结果相同,但前者优势在于计算属性是基于它们的依赖进行缓存的。也就是说:
(1)计算属性依赖不改变,计算就不会触发,改变了才重新触发计算;而调用方法总会再次执行函数
(2)当依赖不是响应式依赖时, 计算属性将永远不会触发计算。如
computed: {
now () {
return Date.now()
}
}
计算属性默认只有 getter
,常规用法其实是调用了计算属性的getter
方法。
什么情况下使用setter
?一般计算属性都是根据依赖来计算自身的值,如果计算属性自身需要手动传入值时,就需要提供一个setter
。例如:将一个计算属性绑定给v-model
。
提供get()
、set()
的计算属性, 需要调整为一个对象。
<template>
<input v-model="name" />
</template>
computed: {
reserve : {
get () {
return this. $store.state.name
},
set (val) {
return this.$store.state.name = `李${val}`
}
}
}
# v-if 惰性、缓存 及 使用 <template>
我们知道,v-if
能决定DOM结构存不存在,而v-show
只是控制了DOM元素的display
属性,当页面切换频率不高时,Vue建议使用v-if
。
所谓的惰性,就是当遇到条件为非真时直接跳过,只有第一次遇到真值才开始渲染条件块。
而缓存,官网给出解释如下:
Vue 会尽可能高效地渲染元素,通常会复用已有元素而不是从头开始渲染。
也就是说,假设页面原本渲染了一个input
标签,而状态改变后也有一个input
标签,Vue检查到新老标签标签名和属性列表都相同。将保留已渲染的标签继续使用。
这种缓存机制是Vue默认的,想修改这种动作,只要给标签加上具有唯一值的key
属性即可,
<input placeholder="Enter your username" key="username-input">
正常情况下,v-if
会被设置在一个标签元素内使用,当遇到前后两个或多个兄弟标签都需要使用相同状态值来判断是否渲染时,可以一个无状态不可见标签<template>
来包裹,Vue在构建DOM时会将其丢弃,并正确的将v-if
作用到相应的标签上。
<template v-if="real">
<div>实体车位</div>
<div>实体车辆</div>
</template>
# v-if 与 v-for 优先级
根据Vue的风格指南,不建议将v-if
和v-for
放在一起使用,我们来探索一下为什么.
它们一起使用的场景无非就有两个
(1)希望通过v-if
控制v-for
代码块是否显示。这种情况下一般v-if
变量是个状态量,与v-for
循环变量无关。
(2)希望通过循环变量中的某个属性的真假值,来控制该项是否应该被循环渲染出来
这两种用法有什么问题?在Vue语法中有个规则:循环体中,v-for属性优先级高于其他属性。也就是说:
场景(1): v-if
的渲染会发生在循环之后,列表优先生成,这就无法提前阻止循环列表的渲染。这与我们初衷想要决定循环块是否渲染产生冲突。解决办法是:使用<template>
标签包裹并在这里设置v-if
控制
场景(2): 如果存在不该被渲染的项,这个项就不应该出现在循环变量中,Vue建议使用计算属性过滤数组。因此也不再需要v-if
# v-for 作用于对象
循环不止作用于数组,同样可作用于所有可迭代类型变量中。
在遍历对象时,通常是按 Object.keys() 的结果遍历,但是不能保证它的结果在不同的 JavaScript 引擎下是一致的。
// 对象遍历,第一个是值,第二个是键,第三个才是索引
<div v-for="(value, key, index) in object" :key="index">
{{ key }} : {{ value }}
</div>
# v-for渲染后的数组缓存替换规则
Vue 包含一组观察数组的变异方法(mutation method
),它们会触发视图更新。包括: push()、pop()、shift()、unshift()、splice()、sort()、reserve()
等。这些方法都会改变原数组。
同样还包含非变异方法,如filter()、concat()、slice()
。他们不改变原数组,而是返回一个新数组。
如果我们对已渲染过后的数组进行非变异方法操作,直觉上列表会重新渲染,其实不然。
Vue 为了使得 DOM 元素得到最大范围的重用而实现了一些智能的、启发式的方法,所以用一个含有相同元素的数组去替换原来的数组是非常高效的操作。
exa.items = exa.items.filter(function (item) {
return item.message.match(/Foo/)
})
# 冻结双向数据绑定
如果初始渲染后不想让视图层响应模型层变化, 可以使用v-once
标签属性, 告知被包含在该标签内部的所有数据绑定不要响应视图更新
<span v-once>这个数据不会发生改变: {{ message }}</span>
# 绑定一段 HTML
Vue在html部分, 无论是利用双大括号{{ }}
还是v-model
绑定的值都会被解释为普通文本。如果需要绑定一段 HTML,可以使用v-html
<p v-html="htmlCode"></p>
# 修饰符
(1) .prevent / .stop / .passive
如果你遇到过在页面执行一个Click事件,触发了两次函数调用,你则需要检查一下是否由事件冒泡引起的。 在DOM2级, DOM3级事件标准中, 浏览器接受一个点击交互后, 产生事件流会有两个过程,捕获和冒泡。 过程如下:
为解决该问题,Vue提供了修饰符.prevent
可以告诉v-on
指令对于触发的事件调用event.preventDefault()
来阻止浏览器的默认行为。 .stop
则是调用event.stopPropagation()
来阻止目标元素的冒泡事件
.passive
不能和.prevent
一同使用,它会屏蔽.prevent
的冒泡效果。.passive
主要使用在移动端,它能提高其性能
(2)键盘修饰符 .enter / .tab / .delete ...
Vue提供监听键盘按键键值的办法,方便我们监听键盘事件。一般情况下,直接使用键值修饰,如enter
键的键值为13
,则使用办法为:
<input @click.13="handleClick"></input>
Vue为方便记忆,绑定了常用键名与键值的关系可直接使用键名绑定
<input @click.enter="handleClick"></input>
常用的有:.enter
,.tab
,.delete
,.esc
,.space
,.up
,.down
,.left
,.right
也可以用通过config.keyCodes
对象自定义按键修饰符别名:
// 可以使用 `v-on:keyup.f1`
Vue.config.keyCodes.f1 = 112
(3)鼠标修饰符
鼠标修饰符限制处理函数仅响应特定的鼠标按钮,包括.left
,.right
,.middle
。
# 动态样式绑定 :class
Vue允许动态切换一个样式, 支持两种语法: 对象形式 | 数组形式
- 对象办法:键表示样式类名,值为 Truthy 表示添加该样式
<div class="wrap" :class="{ borderTop: boolean, active: isActive }"></div>
(2)给:class
传递一个数组,表示应用一组样式
<div :class="[ classA, classB ]"></div>
当
v-bind:style
使用需要添加浏览器引擎前缀的 CSS 属性时,如transform
,Vue.js 会自动侦测并添加相应的前缀。
# 事件绑定传参
如下,前者使用监听事件,而后者是内联处理器
<div id="example">
<button @click="handleSubmit">提交</button>
<button @click="say('Hi')">问候</button>
</div>
# 表单输入绑定
对于普通元素如<div> {{ message }} </div>
等并没有真正表现出Vue双向数据绑定的魅力,其只展现了从ViewModel层发生变化后反馈到View层的单方面特性。而表单输入的双向数据绑定还增加了用户交互使得View层发生改变并响应到ViewModel层,真正体现了“双向”功能。
v-model
可以在表单元素<input>, <textarea>
及<select>
上创建双向数据绑定。Vue会根据空间类型自动选取正确的方法更新元素。值得注意的是,v-model
会忽略所有表单元素的value, chekcd, selected
特性的初始值而总是将Vue实例的数据data选项
作为数据来源。也就是说,不能通过特性自身赋值绑定到v-model
上,而需要在data
中手动赋初始值
- 对于单行多行输入框,经
v-model
绑定过后的元素在文本区域中插值并不会生效,Vue只读绑定中的内容。如
<textarea>{{text}}</textarea>
-
单个复选框,
v-model
绑定到布尔值;而多个复选框则绑定到同一个数组
# 只有一个checkbox则v-model输出true/false
<input type="checkbox" id="jack" value="Jack" v-model="checkedName">
<label for="jack">{{checkedName}}</label> // checkedName: true / false
# 若在此基础上,再增加一个,则输出选中的数组
<input type="checkbox" id="mike" value="Mike" v-model="checkedNames">
<label for="mike">Mike</label>
# 输出checkedName: [jack, mike]
-
单选按钮,绑定到同一个字符串,其值是
value
所对应的值
<input type="radio" id="one" value="One" v-model="picked">
<label for="one">One</label> // 选中时输出对应value值: One
- 下拉选择菜单,单选时绑定到一个值上,多选时绑定到一个数组
# 单选下拉框去掉 multiple属性
<select v-model="selected" multiple style="width: 50px;">
<option v-for="opt in options" v-bind:value="opt .value">
{{ opt .text }}
</option>
</select>
<span>Array: {{ selected }}</span>
# 表单绑定修饰符
-
.lazy:将
input
触发的更新延迟至change
触发
<input v-model.lazy="msg" >
-
.number:将用户输入的内容转化为数字,否则总是返回字符串。设置
type
属性移动端可以调起数字键盘。如果这个值无法被parseFloat()
解析,则会返回原始的值
<input v-model.number="age" type="number">
- .trim:自动过滤用户输入的首尾空白字符
<input v-model.trim="msg">
# Vue.$emit参数,及与 v-on 事件命名规范
在刚开始开发时可能会思考为什么prop
没有子向父传递。不幸运的是,prop
的逆向会给数据流向带来巨大的维护和理解困难,这也是为什么Vue封装了$emit
的模式 触发事件来取而代之的原因
this.$emit('method-name', param)
第一个参数是抛出的事件名,对应父级v-on
事件名,第二个参数是要带出的数据,该数据使用$event
捕获
<Children @click="$emit('enlarge-text', 0.1)"> Enlarge text </Children >
<blog @enlarge-text="postFontSize += $event"></blog>
通常父组件中会绑定给一个属性,该属性定义为一个方法且它的第一个参数就是被带出来的数据
<blog @enlarge-text="enlargeText"></blog>
methods: {
enlargeText (num) {
this.postFontSize += num
}
}
【注意】不同于组件和prop,经$emit
抛出的事件名不会被用作一个JavaScript变量名或属性名,所以就没有理由使用camelCase(驼峰式)或PascalCase(短线式)。因为HTML大小写不明感因素,v-on事件监听器在DOM模板中实质上会被自动转换为全小写,如此一来,原本计划通过驼峰式转换成的短线式的监听事件名也不可能被触发了,所以如果$emit
使用驼峰式命名规则那么你的监听事件也需要驼峰式命名。
Vue建议使用短线式或全小写,特别是前者
this.$emit('my-event', params) // 发起
<my-component v-on:my-event="handleEmit"></my-component> // 接收
# 动态组件
比如我们有一个tab栏,其中有三个tab页,点击不同tab页需切换至不同的组件下,此时非常适合使用is
来指定不同的组件达到动态组件效果,如下。 完整示例
<component :is="currentTabComponent"></component>
通过切换不同的tab
能够实现不同组件的渲染。注意当你每次切换新标签的时候,Vue都创建了一个新的currentTabComponent
实例,因此,他不会保留切换前用户停留的那个页面状态。通常来说,重新创建实例的行为是符合预期的,但也会有需要保留状态的时候,就像是缓存下来一般
>> 使用keep-alive
保留状态
// 注意使用了keep-alive的组件必须要有name属性
<keep-alive>
<component :is="currentTabComponent"></component>
</keep-alive>
# 基础组件的自动化全局注册
经常为了美化页面效果,我们会对HTML元素做一层封装,成为基础组件,可能是一个输入框、一个按钮又或者是别的。对于这些组件,Vue建议使用具有语义化的规范命名风格,如以Base
开头,BaseButton
,BaseIcon
, BaseInput
等。引入这些组件往往占据了大量代码空间
import
好几行,components: {}
又有好几行,但是他们又只是模板中的很小的一部分。
在 Vue CLI 3+ 中提供了require.context
通过全局注册这些非常通用的基础组件,允许你在应用入口文件(src/main.js)全局导入它们
import Vue from 'vue'
import upperFirst from 'lodash/upperFirst'
import camlCase from 'lodash/camelCase'
const requireComponent = require.context(
// 其组件目录的相对路径
'./components',
// 是否查询其子目录
false,
// 匹配基础组件文件名的正则表达式
/Base[A-Z]\w+\.(vue|js)$/
)
requireComponent.keys().forEach(fileName => {
// 获取组件配置
const componentConfig = requireComponent(fileName)
// 获取组件的 PascalCase 命名
const componentName = upperFirst(
camelCase(
// 剥去文件名开头的 `./` 和结尾的扩展名
fileName.replace(/^\.\/(.*)\.\w+$/, '$1')
)
)
// 全局注册组件
Vue.component(
componentName,
// 如果这个组件选项是通过 `export default` 导出的,
// 那么就会优先使用 `.default`,否则回退到使用模块的根。
componentConfig.default || componentConfig
)
})
这里有一个真实案例
# Prop传递数据防脏
所有的 prop
都使得其父子组件形成一个单向下行绑定,父级prop
的更新会流动到子组件中,但反过来不行。这种设计办法是为了防止子组件意外改变父组件的状态,从而导致你的应用的数据流向难以理解。另外,如果该数据还被其他子组件使用,也将受影响,产生泛洪式灾难。因此不应该在子组件中设计修改prop
数据的操作。
在Javascript中对象和数组都是通过引用传入的,因此对于引用类型的prop来说,在子组件中修改数据本身将直接改变父级的数据。
常见的试图改变prop
的操作有一下两种情形:
- 接收的
prop
作为一个初始值,这个子组件接下来希望将其作为一个本地的prop
数据来使用。这种情况下应该使用子组件中的data
来拷贝一份prop
数据数据
prop: [ 'initialNum' ],
data () {
return {
num: this.initialNum
}
}
- 接收的
prop
作为原始的值需要进行格式转换。这种情况下,应该使用计算属性来实现
props: ['size'],
computed: {
normalizedSize () {
return this.size.trim().toLowerCase()
}
}
当不需要对prop
做改变只是进行使用时可以不用data
拷贝,但也需要注意使用,曾经遇到将 ==
写成 =
,花了不少时间找bug
。当系统比较庞大时这种问题不好找,所以大家一定要细心实在不行就多做个data
拷贝。
# prop自定义检查函数
Vue允许在进行prop传值时对值进行验证,type
可以验证数据类型,default
可以设置当未传入时的默认值。除此之外,还允许开发者们自定义验证函数
function CheckName (firstName, lastName) {
this.firstName = firstName
this.lastName = lastName
}
验证办法如下
prop: {
userName: CheckName
}
# Prop与自身属性重名问题
当使用ElementUI
或Bootstrap
这些第三方插件时,往往他们定义有自己的属性,如果开发者们自定义的prop
属性与其发生重名时,Vue在大多数情况下,从外部提供给组件的值会替换掉组件内部设置好的值。
即假设存在传入type="text"
就会替换掉本身type="date"
类型,原来的就会被破坏。庆幸的是, class
和style
会智能一些,即两边的值会合并起来
# Prop实现‘双向绑定’效果 .sync
父组件中的某个属性值需要根据自身利用prop传递给子组件,然后在子组件中做一些操作后响应回给父组件来更新这样的需求时,除了使用$emit
API,不妨试试.sync
作为一种语法糖存在,.sync
修饰在v-bind
上,可以替代prop传递数据时v-on:updata:title="titleName"
这种写法,(给属性增加update:
是Vue在这种需求下的推荐用法),后者还需要使用$emit
来回传值this.$emit('update:title', newTitle)
。.sync
则显得更加简便
需要注意的是,.sync
修饰的属性不能和表达是一起使用,如doc.title + "!"
<text-document v-bind:title.sync="doc.title"></text-document
# 将原生事件绑定到组件上
通常都是在原生的标签上使用事件的绑定,但有时候,你可能想要在一个组件的根元素上直接监听一个原生事件,如使用CubeUI(一般UI库自身会提供原生的继承方法)或自定义组件上。这时,你可以使用 v-on
的 .native
修饰符:
<tab-item @click.native=""></tab-item>
# Vue 插槽
Vue插槽非常重要,笔者为其特意编写了一个专题,详情阅读Vue插槽,高复用组件
# $ref
有时候需要直接访问一个子组件或子元素,此时可以为他赋予一个ref
作为唯一标识,通过$refs
来访问
<self-input ref="nameInput"></self-input>
访问时使用 this.$refs.nameInput
,这样就可以自由访问其内部数据和方法了。这种办法同样适用于元素上。
<input ref="innerInput"></input>
比如我们想在父组件中控制子组件中的input框自动获取焦点,可以这么做
method: {
focus: function() {
this.$refs.innerInput.focus()
}
}
当
ref
和v-ror
一起使用时,得到的结果是包含了对应数据源的这些子组件的数组。另外,需要注意的是,$refs
只会在组件渲染完成之后生效,并且不是响应式的。它并不适用与计算属性
# $root
& $parent
在每个vue实例中,提供了根实例和父实例的数据和方法,这只是一种访问数据的实例,对于小型应用来说很方便,跟建议使用Vuex的状态管理机制。
-
$root
: 访问根实例的数据和方法,包括计算属性等 -
$parent
: 访问父实例的数据和方法,包括计算属性等。修改父组件容易导致难以查找数据变更源
# 依赖注入provide & inject
$root
和 $parent
只能实现根级实例访问和父级实例访问,然而对于跨级的组件间数据交互,虽然可以通过$parent
一层层传递,但这不是一个好办法。依赖注入有了用武之地,通过两个新的选项:provide
和inject
。
provide
允许我们在当前组件中指定想要提供
给后代组件的数据和方法,表现形式很像data选项
provide () {
return {
getMsg: this.getMsg
}
}
它就像是一个大范围的prop
,后代组件都可以使用inject
选项俩注入它。
inject: [ 'getMsg' ]
通过依赖注入的数据也是非响应式的,同样不适用与计算属性
# 后语
本文内容大部分来自官网,作为提炼和融入笔者的一点思考,如有不对和不理解的地方欢迎与笔者交流和提出质疑