本文使用的Vue版本:2.6.10
Vue为咱们提供了不少高级特性,学习和掌握它们有助于提升你的代码水平。css
从咱们刚开始学习Vue的时候,对于侦听属性,都是简单地以下面通常使用:html
watch:{ a(){ //doSomething } }
实际上,Vue对watch提供了不少进阶用法。vue
以对象和handler函数的方式来定义一个监听属性,handler就是处理监听变更时的函数:webpack
watch:{ a:{ handler:'doSomething' } }, methods:{ doSomething(){ //当 a 发生变化的时候,作些处理 } }
handler有啥用?是画蛇添足么?用途主要有两点:web
不知道你注意到了没有?正则表达式
当watch的是一个Object类型的数据,若是这个对象内部的某个值发生了改变,并不会触发watch动做!数组
也就是说,watch默认状况下,不监测内部嵌套数据的变更。可是不少状况下,咱们是须要监测的!缓存
为解决这一问题,就要使用deep属性:服务器
watch:{ obj:{ handler:'doSomething', deep:true } }, methods:{ doSomething(){ //当 obj 发生变化的时候,作些处理 } }
deep属性默认为false,也就是咱们经常使用的watch模式。app
watch
的handler
函数一般状况下只有在监听的属性发生改变时才会触发。
但有些时候,咱们但愿在组件建立后,或者说watch被声明和绑定的时候,马上执行一次handler函数,这就须要使用immediate
属性了,它默认为false,改成true后,就会马上执行handler。
watch:{ obj:{ handler:'doSomething', deep:true, immediate:true } }, methods:{ doSomething(){ //当 obj 发生变化的时候,作些处理 } }
使用数组能够设置多项,形式包括字符串、函数、对象
watch: { // 你能够传入回调数组,它们会被逐一调用 a: [ 'handle1', function handle2 (val, oldVal) { /* ... */ }, { handler: function handle3 (val, oldVal) { /* ... */ }, /* ... */ } ], }
$event
是事件对象的特殊变量,在两种场景下,它有不一样的意义,表明不一样的对象。
$event.target
得到事件所在的DOM对象,再经过value进一步获取具体的值。<template> <div> <input type="text" @input="inputHandler('hello', $event)" /> </div> </template> export default { methods: { inputHandler(msg, e) { console.log(e.target.value) } } }
看下面的例子:
//blog-post组件的模板 <button v-on:click="$emit('enlarge-text', 0.1)"> Enlarge text </button>
在父级组件监听这个事件的时候,能够经过 $event
访问到blog-post
子组件传递出来的0.1这个值:
<blog-post ... v-on:enlarge-text="postFontSize += $event" ></blog-post>
此时,$event
的值就是0.1,而不是前面的事件对象。
这种在缓冲时去除重复数据对于避免没必要要的计算和 DOM 操做是很是重要的。而后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工做。Vue 在内部对异步队列尝试使用原生的 Promise.then
、MutationObserver
和 setImmediate
,若是执行环境不支持,则会采用 setTimeout(fn, 0)
代替。
例如,当你设置 vm.someData = 'new value'
,该组件不会当即从新渲染。当刷新队列时,组件会在下一个事件循环“tick”中更新。
多数状况咱们不须要关心这个过程,可是若是你想基于更新后的 DOM 状态来作点什么,这就可能会有些棘手。
虽然 Vue.js 一般鼓励开发人员使用“数据驱动”的方式思考,避免直接接触 DOM,可是有时咱们必需要这么作。为了在数据变化以后等待 Vue 完成更新 DOM,能够在数据变化以后当即使用 Vue.nextTick(callback)
。
这样回调函数将在 DOM 更新完成后被调用。例如:
<div id="example">{{message}}</div> var vm = new Vue({ el: '#example', data: { message: '123' } }) vm.message = 'new message' // 更改数据 vm.$el.textContent === 'new message' // false Vue.nextTick(function () { vm.$el.textContent === 'new message' // true })
在组件内使用 vm.$nextTick()
实例方法特别方便,由于它不须要全局 Vue
,而且回调函数中的 this
将自动绑定到当前的 Vue 实例上:
Vue.component('example', { template: '<span>{{ message }}</span>', data: function () { return { message: '未更新' } }, methods: { updateMessage: function () { this.message = '已更新' console.log(this.$el.textContent) // => '未更新' this.$nextTick(function () { console.log(this.$el.textContent) // => '已更新' }) } } })
由于 $nextTick()
返回一个 Promise
对象,因此你可使用新的 ES2017 async/await
语法完成相同的事情:
methods: { updateMessage: async function () { this.message = '已更新' //在这里能够看出,message并无马上被执行 //要理解页面刷新和代码执行速度的差异 //一般咱们在页面上马上就能看到结果,那是由于一轮队列执行其实很快,感受不出DOM刷新的过程和所耗费的时间 //但对于代码的执行,属于即刻级别,DOM没更新就是没更新,就是会有问题 console.log(this.$el.textContent) // => '未更新' await this.$nextTick() console.log(this.$el.textContent) // => '已更新' } }
通俗的解释:
this.nextTick(回调函数)
方法,将对DOM的操做做为它的回调函数使用。由于传统编写模板的能力不足,咱们引入了渲染函数createElement。咱们又但愿得到更多的灵活度,因而引入了JSX。最后,咱们发现有些简单的模板能够更简单更小巧的实现,因而引入了函数式组件。Vue老是试图为每一种场景提供不一样的能力。
有这么一类组件,它的特色是:
那么这个组件能够定义为函数式组件。与普通组件相比,函数式组件是无状态的,没法实例化,没有任何的生命周期和方法,适合只依赖于外部数据的变化而变化的组件,因其轻量,渲染性能会有所提升。
Vue.component('my-component', { functional: true, // Props 是可选的 props: { // ... }, // 为了弥补缺乏的实例 // 提供第二个参数做为上下文 render: function (createElement, context) { // ... } })
注意其中的functional: true,
在 Vue 2.3.0 或以上的版本中,你能够省略props
选项,全部组件上的 attribute 都会被自动隐式解析为 prop。当使用函数式组件时,该引用将会是 HTMLElement,由于他们是无状态的也是无实例的。
functional
属性<template functional> ... </template> <script> ... </script> <style> ... </style>
由于无状态,没有this上下文,因此函数式组件须要的一切都是经过 context
参数来传递,它是一个包括以下字段的对象:
props
:提供全部 prop 的对象children
:VNode 子节点的数组slots
:一个函数,返回了包含全部插槽的对象scopedSlots
:(2.6.0+) 一个暴露传入的做用域插槽的对象。也以函数形式暴露普通插槽。data
:传递给组件的整个数据对象,做为 createElement
的第二个参数传入组件parent
:对父组件的引用listeners
:(2.3.0+) 一个包含了全部父组件为当前组件注册的事件监听器的对象。这是 data.on
的一个别名。injections
:(2.3.0+) 若是使用了 inject
选项,则该对象包含了应当被注入的 property。函数式组件的一个典型应用场景是做为包装组件,好比当你碰到下面需求时:
children
、props
、data
传递给子组件以前操做它们。下面是一个 smart-list
组件的例子,它能根据传入 prop 的值来代为渲染更具体的组件:
var EmptyList = { /* ... */ } var TableList = { /* ... */ } var OrderedList = { /* ... */ } var UnorderedList = { /* ... */ } Vue.component('smart-list', { functional: true, props: { items: { type: Array, required: true }, isOrdered: Boolean }, render: function (createElement, context) { function appropriateListComponent () { var items = context.props.items if (items.length === 0) return EmptyList if (typeof items[0] === 'object') return TableList if (context.props.isOrdered) return OrderedList return UnorderedList } return createElement( appropriateListComponent(), context.data, context.children ) } })
假如咱们有父组件Parent
和子组件Child
,若是在父组件中须要监听子组件的mounted这个生命周期函数,并作一些逻辑处理,常规写法可能以下:
// Parent.vue <Child @mounted="doSth" /> //Child.vue mounted(){ this.$emit('mounted'); }
可是,Vue给咱们提供了一种更简便的方法,子组件无需作任何处理,只须要在父组件引用子组件时使用@hook
事件来监听便可,代码以下:
// Parent.vue <Child @hook:mounted="doSth" /> methods:{ doSth(){ //some codes here } }
核心是@hook:mounted="doSth"
的写法!
固然这里不只仅能够监听mounted,其余生命周期均可以监听,例如created、updated等。
咱们知道,在单文件组件的style中使用 scoped
属性后,父组件的样式将不会渗透到子组件中。
不过一个子组件的根节点会同时受其父组件的 scoped CSS 和子组件的 scoped CSS 的影响。这样设计是为了让父组件能够从布局的角度出发,调整其子组件根元素的样式。
若是你但愿父组件的 scoped
样式中的一个选择器可以做用得“更深”,例如影响子组件,可使用深度选择器: >>>
操做符。
<style scoped> .a >>> .b { /* ... */ } </style>
上述代码将会编译成:
.a[data-v-f3f3eg9] .b { /* ... */ }
可是,有些像 Sass 之类的预处理器没法正确解析 >>>
。这种状况下你可使用 /deep/
或 ::v-deep
操做符,这二者都是 >>>
的别名,实现一样的功能。
咱们都知道,经过 v-html
建立的 DOM 内容不受 scoped 样式影响,能够经过深度做用选择器>>>
来为他们设置样式。
通常在组件内使用路由参数,大多数人会这样作:
export default { methods: { getParamsId() { return this.$route.params.id } } }
当你随便用用,临时凑手,这没什么问题,毕竟解决了需求。
可咱们要随时谨记:组件是用来复用的!组件应该有高度的封闭性!
在组件中使用 $route
会使它与路由系统造成高度耦合,从而使组件只能在使用了路由功能的项目内,或某些特定的 URL 上使用,限制了其灵活性。
试想一下,若是你的组件被人拿去复用了,可是那我的并无使用路由系统,而是经过别的方式传递id参数,那么他该怎么办?
正确的作法是经过 props
解耦!
首先,为组件定义一个叫作id
的prop:
export default { props: ['id'], methods: { getParamsId() { return this.id } } }
若是组件没有对应路由,那么这个id也能够经过父组件向子组件传值的方式使用。
若是使用了路由,能够经过路由的prop属性,传递id的值:
const router = new VueRouter({ routes: [{ path: '/user/:id', component: User, props: true }] })
将路由的 props
属性设置为 true
后,组件内可经过 props
接收到 params
参数
另外,你还能够经过函数模式来返回 props
const router = new VueRouter({ routes: [{ path: '/user/:id', component: User, props: (route) => ({ id: route.query.id }) }] })
其实,上面的技巧,在VueRouter的官档都有说明。
在大型应用中,咱们可能须要将应用分割成小一些的代码块,而且只在须要的时候才从服务器加载一个模块。
为了简化,Vue 容许你以一个工厂函数的方式定义你的组件,这个工厂函数会异步解析你的组件定义。Vue 只有在这个组件须要被渲染的时候才会触发该工厂函数,且会把结果缓存起来供将来重渲染。例如:
Vue.component('async-example', function (resolve, reject) { setTimeout(function () { // 向 `resolve` 回调传递组件定义 resolve({ template: '<div>I am async!</div>' }) }, 1000) })
如你所见,这个工厂函数会收到一个 resolve
回调,这个回调函数会在你从服务器获得组件定义的时候被调用。
你也能够调用 reject(reason)
来表示加载失败。这里的 setTimeout
是为了演示用的,如何获取组件取决于你本身。
一个推荐的作法是将异步组件和 webpack 的 code-splitting 功能一块儿配合使用:
Vue.component('async-webpack-example', function (resolve) { // 这个特殊的 `require` 语法将会告诉 webpack // 自动将你的构建代码切割成多个包,这些包 // 会经过 Ajax 请求加载 require(['./my-async-component'], resolve) })
你也能够在工厂函数中返回一个 Promise
,因此把 webpack 2 和 ES2015 语法加在一块儿,咱们能够写成这样:
Vue.component( 'async-webpack-example', // 这个 `import` 函数会返回一个 `Promise` 对象。 () => import('./my-async-component') )
当使用局部注册组件的时候,你也能够直接提供一个返回 Promise
的函数:
new Vue({ // ... components: { 'my-component': () => import('./my-async-component') } })
若是你想实现异步加载组件的功能,提升首屏显示速度,那么可使用上面例子中的定义组件的方法,也就是:箭头函数+import语句!
2.3.0+ 新增
异步组件的工厂函数也能够返回一个以下格式的对象,用来灵活定制异步加载过程:
const AsyncComponent = () => ({ // 须要加载的组件 (应该是一个 `Promise` 对象) component: import('./MyComponent.vue'), // 异步组件加载时使用的组件 loading: LoadingComponent, // 加载失败时使用的组件 error: ErrorComponent, // 展现加载时组件的延时时间。默认值是 200 (毫秒) delay: 200, // 若是提供了超时时间且组件加载也超时了, // 则使用加载失败时使用的组件。默认值是:`Infinity` timeout: 3000 })
注意若是你但愿在 Vue Router 的路由组件中使用上述语法的话,必须使用 Vue Router 2.4.0+ 版本。
不少时候咱们会编写一些相似输入框或按钮之类的基础组件,它们是相对通用的组件,称为基础组件,它们会在更大一些的组件中被频繁的用到。
这很容易致使大的组件里有一个很长的导入基础组件的语句列表,例如:
import BaseButton from './BaseButton.vue' import BaseIcon from './BaseIcon.vue' import BaseInput from './BaseInput.vue' //更多导入 export default { components: { BaseButton, BaseIcon, BaseInput } }
当你的基础组件不少的时候,这个过程将很是重复、麻烦和无聊。
若是你刚好使用了 webpack (或在内部使用了 webpack 的 Vue CLI 3+),那么就可使用 require.context
方法批量导入这些组件,而后将它们注册为全局组件,这样就能够在任何地方直接使用它们了,不再用为导入的事情烦恼了!
下面是一个示例代码:
import Vue from 'vue' import upperFirst from 'lodash/upperFirst' import camelCase 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 .split('/') .pop() .replace(/\.\w+$/, '') ) ) // 全局注册组件 Vue.component( componentName, // 若是这个组件选项是经过 `export default` 导出的, // 那么就会优先使用 `.default`, // 不然回退到使用模块的根。 componentConfig.default || componentConfig ) })
记住全局注册的行为必须在根 Vue 实例 (经过 new Vue
) 建立以前发生。