最新的一波Vue實戰技巧,不用則已,一用驚人
在Vue
中,不同的選項有不同的合併策略,比如 data
,props
,methods
是同名屬性覆蓋合併,其他直接合並,而生命週期鉤子函式則是將同名的函式放到一個數組中,在呼叫的時候依次呼叫
在Vue
中,提供了一個api
, Vue.config.optionMergeStrategies
,可以通過這個api去自定義選項的合併策略。
在程式碼中列印
console.log(Vue.config.optionMergeStrategies)
通過合併策略自定義生命週期函式
背景
最近客戶給領導反饋,我們的系統用一段時間,瀏覽器就變得有點卡,不知道為什麼。問題出來了,本來想甩鍋到後端,但是瀏覽器問題,沒法甩鍋啊,那就排查吧。
後來發現頁面有許多定時器,ajax
輪詢還有動畫,開啟一個瀏覽器頁籤沒法問題,開啟多了,瀏覽器就變得卡了,這時候我就想如果能在使用者切換頁籤時候將這些都停掉,不久解決了。百度裡面上下檢索,找到了一個事件visibilitychange
,可以用來判斷瀏覽器頁籤是否顯示。
有方法了,就寫唄
export default { created() { window.addEventListener('visibilitychange', this.$_hanldeVisiblityChange) // 此處用了hookEvent,可以參考小編前一篇文章 this.$on('hook:beforeDestroy', () => { window.removeEventListener( 'visibilitychange', this.$_hanldeVisiblityChange ) }) }, methods: { $_hanldeVisiblityChange() { if (document.visibilityState === 'hidden') { // 停掉那一堆東西 } if (document.visibilityState === 'visible') { // 開啟那一堆東西 } } } }
通過上面的程式碼,可以看到在每一個需要監聽處理的檔案都要寫一堆事件監聽,判斷頁面是否顯示的程式碼,一處兩處還可以,檔案多了就頭疼了,這時候小編突發奇想,定義一個頁面顯示隱藏的生命週期鉤子,把這些判斷都封裝起來
自定義生命週期鉤子函式
定義生命週期函式 pageHidden
與 pageVisible
import Vue from 'vue' // 通知所有元件頁面狀態發生了變化 const notifyVisibilityChange = (lifeCycleName, vm) => { // 生命週期函式會存在$options中,通過$options[lifeCycleName]獲取生命週期 const lifeCycles = vm.$options[lifeCycleName] // 因為使用了created的合併策略,所以是一個數組 if (lifeCycles && lifeCycles.length) { // 遍歷 lifeCycleName對應的生命週期函式列表,依次執行 lifeCycles.forEach(lifecycle => { lifecycle.call(vm) }) } // 遍歷所有的子元件,然後依次遞迴執行 if (vm.$children && vm.$children.length) { vm.$children.forEach(child => { notifyVisibilityChange(lifeCycleName, child) }) } } // 新增生命週期函式 export function init() { const optionMergeStrategies = Vue.config.optionMergeStrategies // 定義了兩個生命週期函式 pageVisible, pageHidden // 為什麼要賦值為 optionMergeStrategies.created呢 // 這個相當於指定 pageVisible, pageHidden 的合併策略與 created的相同(其他生命週期函式都一樣) optionMergeStrategies.pageVisible = optionMergeStrategies.beforeCreate optionMergeStrategies.pageHidden = optionMergeStrategies.created } // 將事件變化繫結到根節點上面 // rootVm vue根節點例項 export function bind(rootVm) { window.addEventListener('visibilitychange', () => { // 判斷呼叫哪個生命週期函式 let lifeCycleName = undefined if (document.visibilityState === 'hidden') { lifeCycleName = 'pageHidden' } else if (document.visibilityState === 'visible') { lifeCycleName = 'pageVisible' } if (lifeCycleName) { // 通過所有元件生命週期發生變化了 notifyVisibilityChange(lifeCycleName, rootVm) } }) }
應用
- 在
main.js
主入口檔案引入
import { init, bind } from './utils/custom-life-cycle' // 初始化生命週期函式, 必須在Vue例項化之前確定合併策略 init() const vm = new Vue({ router, render: h => h(App) }).$mount('#app') // 將rootVm 繫結到生命週期函式監聽裡面 bind(vm)
2. 在需要的地方監聽生命週期函式
export default { pageVisible() { console.log('頁面顯示出來了') }, pageHidden() { console.log('頁面隱藏了') } }
provide
與inject
,不止父子傳值,祖宗傳值也可以
Vue
相關的面試經常會被面試官問道,Vue
父子之間傳值的方式有哪些,通常我們會回答,props
傳值,$emit
事件傳值,vuex
傳值,還有eventbus
傳值等等,今天再加一種provide
與inject
傳值,離offer
又近了一步。(對了,下一節還有一種)
使用過React
的同學都知道,在React
中有一個上下文Context
,元件可以通過Context
向任意後代傳值,而Vue
的provide
與inject
的作用於Context
的作用基本一樣
先舉一個例子
使用過elemment-ui
的同學一定對下面的程式碼感到熟悉
<template> <el-form :model="formData" size="small"> <el-form-item label="姓名" prop="name"> <el-input v-model="formData.name" /> </el-form-item> <el-form-item label="年齡" prop="age"> <el-input-number v-model="formData.age" /> </el-form-item> <el-button>提交</el-button> </el-form> </template> <script> export default { data() { return { formData: { name: '', age: 0 } } } } </script>
看了上面的程式碼,貌似沒啥特殊的,天天寫啊。在el-form
上面我們指定了一個屬性size="small"
,然後有沒有發現表單裡面的所有表單元素以及按鈕的 size
都變成了small
,這個是怎麼做到的?接下來我們自己手寫一個表單模擬一下
自己手寫一個表單
自定義表單custom-form.vue
<template> <form class="custom-form"> <slot></slot> </form> </template> <script> export default { props: { // 控制表單元素的大小 size: { type: String, default: 'default', // size 只能是下面的四個值 validator(value) { return ['default', 'large', 'small', 'mini'].includes(value) } }, // 控制表單元素的禁用狀態 disabled: { type: Boolean, default: false } }, // 通過provide將當前表單例項傳遞到所有後代元件中 provide() { return { customForm: this } } } </script>
在上面程式碼中,我們通過provide
將當前元件的例項傳遞到後代元件中,provide
是一個函式,函式返回的是一個物件
自定義表單項custom-form-item.vue
沒有什麼特殊的,只是加了一個label
,element-ui
更復雜一些
<template> <div class="custom-form-item"> <label class="custom-form-item__label">{{ label }}</label> <div class="custom-form-item__content"> <slot></slot> </div> </div> </template> <script> export default { props: { label: { type: String, default: '' } } } </script>
自定義輸入框 custom-input.vue
<template> <div class="custom-input" :class="[ `custom-input--${getSize}`, getDisabled && `custom-input--disabled` ]" > <input class="custom-input__input" :value="value" @input="$_handleChange" /> </div> </template> <script> /* eslint-disable vue/require-default-prop */ export default { props: { // 這裡用了自定義v-model value: { type: String, default: '' }, size: { type: String }, disabled: { type: Boolean } }, // 通過inject 將form元件注入的例項新增進來 inject: ['customForm'], computed: { // 通過計算元件獲取元件的size, 如果當前元件傳入,則使用當前元件的,否則是否form元件的 getSize() { return this.size || this.customForm.size }, // 元件是否禁用 getDisabled() { const { disabled } = this if (disabled !== undefined) { return disabled } return this.customForm.disabled } }, methods: { // 自定義v-model $_handleChange(e) { this.$emit('input', e.target.value) } } } </script>
在
form
中,我們通過provide
返回了一個物件,在input
中,我們可以通過inject
獲取form
中返回物件中的項,如上程式碼inject:['customForm']
所示,然後就可以在元件內通過this.customForm
呼叫form
例項上面的屬性與方法了
在專案中使用
<template> <custom-form size="small"> <custom-form-item label="姓名"> <custom-input v-model="formData.name" /> </custom-form-item> </custom-form> </template> <script> import CustomForm from '../components/custom-form' import CustomFormItem from '../components/custom-form-item' import CustomInput from '../components/custom-input' export default { components: { CustomForm, CustomFormItem, CustomInput }, data() { return { formData: { name: '', age: 0 } } } } </script>
執行上面程式碼,執行結果為:
<form class="custom-form"> <div class="custom-form-item"> <label class="custom-form-item__label">姓名</label> <div class="custom-form-item__content"> <!--size=small已經新增到指定的位置了--> <div class="custom-input custom-input--small"> <input class="custom-input__input"> </div> </div> </div> </form>
通過上面的程式碼可以看到,input
元件已經設定元件樣式為custom-input--small
了
inject
格式說明
除了上面程式碼中所使用的inject:['customForm']
寫法之外,inject
還可以是一個物件。且可以指定預設值
修改上例,如果custom-input
外部沒有custom-form
,則不會注入customForm
,此時為customForm
指定預設值
{ inject: { customForm: { // 對於非原始值,和props一樣,需要提供一個工廠方法 default: () => ({ size: 'default' }) } } }
使用限制
1.provide
和inject
的繫結不是可響應式的。但是,如果你傳入的是一個可監聽的物件,如上面的customForm: this
,那麼其物件的屬性還是可響應的。
2.Vue
官網建議provide
和 inject
主要在開發高階外掛/元件庫時使用。不推薦用於普通應用程式程式碼中。因為provide
和inject
在程式碼中是不可追溯的(ctrl + f可以搜),建議可以使用Vuex
代替。 但是,也不是說不能用,在區域性功能有時候用了作用還是比較大的。
插槽,我要鑽到你的懷裡
插槽,相信每一位Vue
都有使用過,但是如何更好的去理解插槽,如何去自定義插槽,今天小編為你帶來更形象的說明。
預設插槽
<template> <!--這是一個一居室--> <div class="one-bedroom"> <!--新增一個預設插槽,使用者可以在外部隨意定義這個一居室的內容--> <slot></slot> </div> </template>
<template> <!--這裡一居室--> <one-bedroom> <!--將傢俱放到房間裡面,元件內部就是上面提供的預設插槽的空間--> <span>先放一個小床,反正沒有女朋友</span> <span>再放一個電腦桌,在家還要加班寫bug</span> </one-bedroom> </template> <script> import OneBedroom from '../components/one-bedroom' export default { components: { OneBedroom } } </script>
具名插槽
<template> <div class="two-bedroom"> <!--這是主臥--> <div class="master-bedroom"> <!---主臥使用預設插槽--> <slot></slot> </div> <!--這是次臥--> <div class="secondary-bedroom"> <!--次臥使用具名插槽--> <slot name="secondard"></slot> </div> </div> </template>
<template> <two-bedroom> <!--主臥使用預設插槽--> <div> <span>放一個大床,要結婚了,嘿嘿嘿</span> <span>放一個衣櫃,老婆的衣服太多了</span> <span>算了,還是放一個電腦桌吧,還要寫bug</span> </div> <!--次臥,通過v-slot:secondard 可以指定使用哪一個具名插槽, v-slot:secondard 也可以簡寫為 #secondard--> <template v-slot:secondard> <div> <span>父母要住,放一個硬一點的床,軟床對腰不好</span> <span>放一個衣櫃</span> </div> </template> </two-bedroom> </template> <script> import TwoBedroom from '../components/slot/two-bedroom' export default { components: { TwoBedroom } } </script>
作用域插槽
<template> <div class="two-bedroom"> <!--其他內容省略--> <div class="toilet"> <!--通過v-bind 可以向外傳遞引數, 告訴外面衛生間可以放洗衣機--> <slot name="toilet" v-bind="{ washer: true }"></slot> </div> </div> </template>
<template> <two-bedroom> <!--其他省略--> <!--衛生間插槽,通過v-slot="scope"可以獲取元件內部通過v-bind傳的值--> <template v-slot:toilet="scope"> <!--判斷是否可以放洗衣機--> <span v-if="scope.washer">這裡放洗衣機</span> </template> </two-bedroom> </template>
插槽預設值
<template> <div class="second-hand-house"> <div class="master-bedroom"> <!--插槽可以指定預設值,如果外部呼叫元件時沒有修改插槽內容,則使用預設插槽--> <slot> <span>這裡有一張水床,玩的夠嗨</span> <span>還有一個衣櫃,有點舊了</span> </slot> </div> <!--這是次臥--> <div class="secondary-bedroom"> <!--次臥使用具名插槽--> <slot name="secondard"> <span>這裡有一張嬰兒床</span> </slot> </div> </div> </template>
<second-hand-house> <!--主臥使用預設插槽,只裝修主臥--> <div> <span>放一個大床,要結婚了,嘿嘿嘿</span> <span>放一個衣櫃,老婆的衣服太多了</span> <span>算了,還是放一個電腦桌吧,還要寫bug</span> </div> </second-hand-house>
dispatch
和broadcast
,這是一種有歷史的元件通訊方式
dispatch
與broadcast
是一種有歷史的元件通訊方式,為什麼是有歷史的,因為他們是Vue1.0
提供的一種方式,在Vue2.0
中廢棄了。但是廢棄了不代表我們不能自己手動實現,像許多UI庫內部都有實現。本文以element-ui
實現為基礎進行介紹。同時看完本節,你會對元件的$parent
,$children
,$options
有所瞭解。
方法介紹
❝❞
$dispatch
:$dispatch
會向上觸發一個事件,同時傳遞要觸發的祖先元件的名稱與引數,當事件向上傳遞到對應的元件上時會觸發元件上的事件偵聽器,同時傳播會停止。
❝
$broadcast
:$broadcast
會向所有的後代元件傳播一個事件,同時傳遞要觸發的後代元件的名稱與引數,當事件傳遞到對應的後代元件時,會觸發元件上的事件偵聽器,同時傳播會停止(因為向下傳遞是樹形的,所以只會停止其中一個葉子分支的傳遞)。
$dispatch
實現與應用
1. 程式碼實現
// 向上傳播事件 // @param {*} eventName 事件名稱 // @param {*} componentName 接收事件的元件名稱 // @param {...any} params 傳遞的引數,可以有多個 function dispatch(eventName, componentName, ...params) { // 如果沒有$parent, 則取$root let parent = this.$parent || this.$root while (parent) { // 元件的name儲存在元件的$options.componentName 上面 const name = parent.$options.name // 如果接收事件的元件是當前元件 if (name === componentName) { // 通過當前元件上面的$emit觸發事件,同事傳遞事件名稱與引數 parent.$emit.apply(parent, [eventName, ...params]) break } else { // 否則繼續向上判斷 parent = parent.$parent } } } // 匯出一個物件,然後在需要用到的地方通過混入新增 export default { methods: { $dispatch: dispatch } }
2. 程式碼應用
-
在子元件中通過
$dispatch
向上觸發事件import emitter from '../mixins/emitter' export default { name: 'Chart', // 通過混入將$dispatch加入進來 mixins: [emitter], mounted() { // 在元件渲染完之後,將元件通過$dispatch將自己註冊到Board元件上 this.$dispatch('register', 'Board', this) } }
-
在
Board
元件上通過$on
監聽要註冊的事件
$broadcast
實現與應用
1. 程式碼實現
//向下傳播事件 // @param {*} eventName 事件名稱 // @param {*} componentName 要觸發元件的名稱 // @param {...any} params 傳遞的引數 function broadcast(eventName, componentName, ...params) { this.$children.forEach(child => { const name = child.$options.name if (name === componentName) { child.$emit.apply(child, [eventName, ...params]) } else { broadcast.apply(child, [eventName, componentName, ...params]) } }) } // 匯出一個物件,然後在需要用到的地方通過混入新增 export default { methods: { $broadcast: broadcast } }
2. 程式碼應用
在父元件中通過$broadcast
向下觸發事件
import emitter from '../mixins/emitter' export default { name: 'Board', // 通過混入將$dispatch加入進來 mixins: [emitter], methods:{ //在需要的時候,重新整理元件 $_refreshChildren(params) { this.$broadcast('refresh', 'Chart', params) } } }
在後代元件中通過$on
監聽重新整理事件
export default { name: 'Chart', created() { this.$on('refresh',(params) => { // 重新整理事件 }) } }
總結
通過上面的例子,同學們應該都能對$dispatch
和$broadcast
有所瞭解,但是為什麼Vue2.0
要放棄這兩個方法呢?官方給出的解釋是:”因為基於元件樹結構的事件流方式實在是讓人難以理解,並且在元件結構擴充套件的過程中會變得越來越脆弱。這種事件方式確實不太好,我們也不希望在以後讓開發者們太痛苦。並且 $dispatch
和 $broadcast
也沒有解決兄弟元件間的通訊問題。“
確實如官網所說,這種事件流的方式確實不容易讓人理解,而且後期維護成本比較高。但是在小編看來,不管黑貓白貓,能抓老鼠的都是好貓,在許多特定的業務場景中,因為業務的複雜性,很有可能使用到這樣的通訊方式。但是使用歸使用,但是不能濫用,小編一直就在專案中有使用。
&n