1. 內部監聽生命週期函數
今天產品經理又給我甩過來一個需求,需要開發一個圖表,拿到需求,瞄了一眼,然後我就去echarts
官網複製示例代碼了,複製完改了改差不多了,改完代碼長這樣
<template>
<div class="echarts"></div>
</template>
<script>
export default {
mounted() {
this.chart = echarts.init(this.$el)
// 請求數據,賦值數據 等等一系列操作...
// 監聽窗口發生變化,resize組件
window.addEventListener('resize', this.$_handleResizeChart)
},
updated() {
// 幹了一堆活
},
created() {
// 幹了一堆活
},
beforeDestroy() {
// 組件銷燬時,銷燬監聽事件
window.removeEventListener('resize', this.$_handleResizeChart)
},
methods: {
$_handleResizeChart() {
this.chart.resize()
},
// 其他一堆方法
}
}
</script>
複製代碼
功能寫完開開心心的提測了,測試沒啥問題,產品經理表示做的很棒。然而code review時候,技術大佬說了,這樣有問題。
大佬:這樣寫不是很好,應該將監聽`resize`事件與銷燬`resize`事件放到一起,現在兩段代碼分開而且相隔幾百行代碼,可讀性比較差
我:那我把兩個生命週期鉤子函數位置換一下,放到一起?
大佬: `hook`聽過沒?
我:`Vue3.0`纔有啊,咋,咱要升級`Vue`?
複製代碼
然後技術大佬就不理我了,並向我扔過來一段代碼
export default {
mounted() {
this.chart = echarts.init(this.$el)
// 請求數據,賦值數據 等等一系列操作...
// 監聽窗口發生變化,resize組件
window.addEventListener('resize', this.$_handleResizeChart)
// 通過hook監聽組件銷燬鉤子函數,並取消監聽事件
this.$once('hook:beforeDestroy', () => {
window.removeEventListener('resize', this.$_handleResizeChart)
})
},
updated() {},
created() {},
methods: {
$_handleResizeChart() {
// this.chart.resize()
}
}
}
複製代碼
看完代碼,恍然大悟,大佬不愧是大佬,原來Vue
還可以這樣監聽生命週期函數。
在Vue
組件中,可以用過$on
,$once
去監聽所有的生命週期鉤子函數,如監聽組件的updated
鉤子函數可以寫成 this.$on('hook:updated', () => {})
2. 外部監聽生命週期函數
今天同事在公司羣裏問,想在外部監聽組件的生命週期函數,有沒有辦法啊?
爲什麼會有這樣的需求呢,原來同事用了一個第三方組件,需要監聽第三方組件數據的變化,但是組件又沒有提供change
事件,同事也沒辦法了,纔想出來要去在外部監聽組件的updated
鉤子函數。查看了一番資料,發現Vue
支持在外部監聽組件的生命週期鉤子函數。
<template>
<!--通過@hook:updated監聽組件的updated生命鉤子函數-->
<!--組件的所有生命週期鉤子都可以通過@hook:鉤子函數名 來監聽觸發-->
<custom-select @hook:updated="$_handleSelectUpdated" />
</template>
<script>
import CustomSelect from '../components/custom-select'
export default {
components: {
CustomSelect
},
methods: {
$_handleSelectUpdated() {
console.log('custom-select組件的updated鉤子函數被觸發')
}
}
}
</script>
3.用Vue.observable
手寫一個狀態管理吧
在前端項目中,有許多數據需要在各個組件之間進行傳遞共享,這時候就需要有一個狀態管理工具,一般情況下,我們都會使用Vuex
,但對於小型項目來說,就像Vuex
官網所說:“如果您不打算開發大型單頁應用,使用 Vuex 可能是繁瑣冗餘的。確實是如此——如果您的應用夠簡單,您最好不要使用 Vuex”。這時候我們就可以使用Vue2.6
提供的新API Vue.observable
手動打造一個Vuex
創建 store
import Vue from 'vue'
// 通過Vue.observable創建一個可響應的對象
export const store = Vue.observable({
userInfo: {},
roleIds: []
})
// 定義 mutations, 修改屬性
export const mutations = {
setUserInfo(userInfo) {
store.userInfo = userInfo
},
setRoleIds(roleIds) {
store.roleIds = roleIds
}
}
複製代碼
在組件中引用
<template>
<div>
{{ userInfo.name }}
</div>
</template>
<script>
import { store, mutations } from '../store'
export default {
computed: {
userInfo() {
return store.userInfo
}
},
created() {
mutations.setUserInfo({
name: '子君'
})
}
}
</script>
4.開發全局組件,你可能需要了解一下Vue.extend
Vue.extend
是一個全局Api,平時我們在開發業務的時候很少會用到它,但有時候我們希望可以開發一些全局組件比如Loading
,Notify
,Message
等組件時,這時候就可以使用Vue.extend
。
同學們在使用element-ui
的loading
時,在代碼中可能會這樣寫
// 顯示loading
const loading = this.$loading()
// 關閉loading
loading.close()
複製代碼
這樣寫可能沒什麼特別的,但是如果你這樣寫
const loading = this.$loading()
const loading1 = this.$loading()
setTimeout(() => {
loading.close()
}, 1000 * 3)
複製代碼
這時候你會發現,我調用了兩次loading,但是隻出現了一個,而且我只關閉了loading
,但是loading1
也被關閉了。這是怎麼實現的呢?我們現在就是用Vue.extend
+ 單例模式去實現一個loading
開發loading
組件
<template>
<transition name="custom-loading-fade">
<!--loading蒙版-->
<div v-show="visible" class="custom-loading-mask">
<!--loading中間的圖標-->
<div class="custom-loading-spinner">
<i class="custom-spinner-icon"></i>
<!--loading上面顯示的文字-->
<p class="custom-loading-text">{{ text }}</p>
</div>
</div>
</transition>
</template>
<script>
export default {
props: {
// 是否顯示loading
visible: {
type: Boolean,
default: false
},
// loading上面的顯示文字
text: {
type: String,
default: ''
}
}
}
</script>
複製代碼
開發出來loading
組件之後,如果需要直接使用,就要這樣去用
<template>
<div class="component-code">
<!--其他一堆代碼-->
<custom-loading :visible="visible" text="加載中" />
</div>
</template>
<script>
export default {
data() {
return {
visible: false
}
}
}
</script>
複製代碼
但這樣使用並不能滿足我們的需求
-
可以通過js直接調用方法來顯示關閉
-
loading
可以將整個頁面全部遮罩起來
通過Vue.extend
將組件轉換爲全局組件
1. 改造loading
組件,將組件的props
改爲data
export default {
data() {
return {
text: '',
visible: false
}
}
}
複製代碼
2. 通過Vue.extend
改造組件
// loading/index.js
import Vue from 'vue'
import LoadingComponent from './loading.vue'
// 通過Vue.extend將組件包裝成一個子類
const LoadingConstructor = Vue.extend(LoadingComponent)
let loading = undefined
LoadingConstructor.prototype.close = function() {
// 如果loading 有引用,則去掉引用
if (loading) {
loading = undefined
}
// 先將組件隱藏
this.visible = false
// 延遲300毫秒,等待loading關閉動畫執行完之後銷燬組件
setTimeout(() => {
// 移除掛載的dom元素
if (this.$el && this.$el.parentNode) {
this.$el.parentNode.removeChild(this.$el)
}
// 調用組件的$destroy方法進行組件銷燬
this.$destroy()
}, 300)
}
const Loading = (options = {}) => {
// 如果組件已渲染,則返回即可
if (loading) {
return loading
}
// 要掛載的元素
const parent = document.body
// 組件屬性
const opts = {
text: '',
...options
}
// 通過構造函數初始化組件 相當於 new Vue()
const instance = new LoadingConstructor({
el: document.createElement('div'),
data: opts
})
// 將loading元素掛在到parent上面
parent.appendChild(instance.$el)
// 顯示loading
Vue.nextTick(() => {
instance.visible = true
})
// 將組件實例賦值給loading
loading = instance
return instance
}
export default Loading
複製代碼
3. 在頁面使用loading
import Loading from './loading/index.js'
export default {
created() {
const loading = Loading({ text: '正在加載。。。' })
// 三秒鐘後關閉
setTimeout(() => {
loading.close()
}, 3000)
}
}
複製代碼
通過上面的改造,loading已經可以在全局使用了,如果需要像element-ui
一樣掛載到Vue.prototype
上面,通過this.$loading
調用,還需要改造一下
將組件掛載到Vue.prototype
上面
Vue.prototype.$loading = Loading
// 在export之前將Loading方法進行綁定
export default Loading
// 在組件內使用
this.$loading()
5.自定義指令,從底層解決問題
什麼是指令?指令就是你女朋友指着你說,“那邊搓衣板,跪下,這是命令!”。開玩笑啦,程序員哪裏會有女朋友。
通過上一節我們開發了一個loading
組件,開發完之後,其他開發在使用的時候又提出來了兩個需求
-
可以將
loading
掛載到某一個元素上面,現在只能是全屏使用 -
可以使用指令在指定的元素上面掛載
loading
有需求,咱就做,沒話說
開發v-loading
指令
import Vue from 'vue'
import LoadingComponent from './loading'
// 使用 Vue.extend構造組件子類
const LoadingContructor = Vue.extend(LoadingComponent)
// 定義一個名爲loading的指令
Vue.directive('loading', {
/**
* 只調用一次,在指令第一次綁定到元素時調用,可以在這裏做一些初始化的設置
* @param {*} el 指令要綁定的元素
* @param {*} binding 指令傳入的信息,包括 {name:'指令名稱', value: '指令綁定的值',arg: '指令參數 v-bind:text 對應 text'}
*/
bind(el, binding) {
const instance = new LoadingContructor({
el: document.createElement('div'),
data: {}
})
el.appendChild(instance.$el)
el.instance = instance
Vue.nextTick(() => {
el.instance.visible = binding.value
})
},
/**
* 所在組件的 VNode 更新時調用
* @param {*} el
* @param {*} binding
*/
update(el, binding) {
// 通過對比值的變化判斷loading是否顯示
if (binding.oldValue !== binding.value) {
el.instance.visible = binding.value
}
},
/**
* 只調用一次,在 指令與元素解綁時調用
* @param {*} el
*/
unbind(el) {
const mask = el.instance.$el
if (mask.parentNode) {
mask.parentNode.removeChild(mask)
}
el.instance.$destroy()
el.instance = undefined
}
})
複製代碼
在元素上面使用指令
<template>
<div v-loading="visible"></div>
</template>
<script>
export default {
data() {
return {
visible: false
}
},
created() {
this.visible = true
fetch().then(() => {
this.visible = false
})
}
}
</script>
複製代碼
項目中哪些場景可以自定義指令
-
爲組件添加
loading
效果 -
按鈕級別權限控制
v-permission
-
代碼埋點,根據操作類型定義指令
-
input輸入框自動獲取焦點
-
其他等等。。。
6.深度watch
與watch
立即觸發回調,我可以監聽到你的一舉一動
在開發Vue項目時,我們會經常性的使用到watch
去監聽數據的變化,然後在變化之後做一系列操作。
基礎用法
比如一個列表頁,我們希望用戶在搜索框輸入搜索關鍵字的時候,可以自動觸發搜索,此時除了監聽搜索框的change
事件之外,我們也可以通過watch
監聽搜索關鍵字的變化
<template>
<!--此處示例使用了element-ui-->
<div>
<div>
<span>搜索</span>
<input v-model="searchValue" />
</div>
<!--列表,代碼省略-->
</div>
</template>
<script>
export default {
data() {
return {
searchValue: ''
}
},
watch: {
// 在值發生變化之後,重新加載數據
searchValue(newValue, oldValue) {
// 判斷搜索
if (newValue !== oldValue) {
this.$_loadData()
}
}
},
methods: {
$_loadData() {
// 重新加載數據,此處需要通過函數防抖
}
}
}
</script>
複製代碼
立即觸發
通過上面的代碼,現在已經可以在值發生變化的時候觸發加載數據了,但是如果要在頁面初始化時候加載數據,我們還需要在created
或者mounted
生命週期鉤子裏面再次調用$_loadData
方法。不過,現在可以不用這樣寫了,通過配置watch
的立即觸發屬性,就可以滿足需求了
// 改造watch
export default {
watch: {
// 在值發生變化之後,重新加載數據
searchValue: {
// 通過handler來監聽屬性變化, 初次調用 newValue爲""空字符串, oldValue爲 undefined
handler(newValue, oldValue) {
if (newValue !== oldValue) {
this.$_loadData()
}
},
// 配置立即執行屬性
immediate: true
}
}
}
複製代碼
深度監聽(我可以看到你內心的一舉一動)
一個表單頁面,需求希望用戶在修改表單的任意一項之後,表單頁面就需要變更爲被修改狀態。如果按照上例中watch
的寫法,那麼我們就需要去監聽表單每一個屬性,太麻煩了,這時候就需要用到watch
的深度監聽deep
export default {
data() {
return {
formData: {
name: '',
sex: '',
age: 0,
deptId: ''
}
}
},
watch: {
// 在值發生變化之後,重新加載數據
formData: {
// 需要注意,因爲對象引用的原因, newValue和oldValue的值一直相等
handler(newValue, oldValue) {
// 在這裏標記頁面編輯狀態
},
// 通過指定deep屬性爲true, watch會監聽對象裏面每一個值的變化
deep: true
}
}
}
複製代碼
隨時監聽,隨時取消,瞭解一下$watch
有這樣一個需求,有一個表單,在編輯的時候需要監聽表單的變化,如果發生變化則保存按鈕啓用,否則保存按鈕禁用。這時候對於新增表單來說,可以直接通過watch
去監聽表單數據(假設是formData
),如上例所述,但對於編輯表單來說,表單需要回填數據,這時候會修改formData
的值,會觸發watch
,無法準確的判斷是否啓用保存按鈕。現在你就需要了解一下$watch
export default {
data() {
return {
formData: {
name: '',
age: 0
}
}
},
created() {
this.$_loadData()
},
methods: {
// 模擬異步請求數據
$_loadData() {
setTimeout(() => {
// 先賦值
this.formData = {
name: '子君',
age: 18
}
// 等表單數據回填之後,監聽數據是否發生變化
const unwatch = this.$watch(
'formData',
() => {
console.log('數據發生了變化')
},
{
deep: true
}
)
// 模擬數據發生了變化
setTimeout(() => {
this.formData.name = '張三'
}, 1000)
}, 1000)
}
}
}
複製代碼
根據上例可以看到,我們可以在需要的時候通過this.$watch
來監聽數據變化。那麼如何取消監聽呢,上例中this.$watch
返回了一個值unwatch
,是一個函數,在需要取消的時候,執行 unwatch()
即可取消
7.函數式組件,函數是組件?
什麼是函數式組件?函數式組件就是函數是組件,感覺在玩文字遊戲。使用過React
的同學,應該不會對函數式組件感到陌生。函數式組件,我們可以理解爲沒有內部狀態,沒有生命週期鉤子函數,沒有this
(不需要實例化的組件)。
在日常寫bug的過程中,經常會開發一些純展示性的業務組件,比如一些詳情頁面,列表界面等,它們有一個共同的特點是隻需要將外部傳入的數據進行展現,不需要有內部狀態,不需要在生命週期鉤子函數裏面做處理,這時候你就可以考慮使用函數式組件。
先來一個函數式組件的代碼
export default {
// 通過配置functional屬性指定組件爲函數式組件
functional: true,
// 組件接收的外部屬性
props: {
avatar: {
type: String
}
},
/**
* 渲染函數
* @param {*} h
* @param {*} context 函數式組件沒有this, props, slots等都在context上面掛着
*/
render(h, context) {
const { props } = context
if (props.avatar) {
return <img src={props.avatar}></img>
}
return <img src="default-avatar.png"></img>
}
}
複製代碼
在上例中,我們定義了一個頭像組件,如果外部傳入頭像,則顯示傳入的頭像,否則顯示默認頭像。上面的代碼中大家看到有一個render函數,這個是Vue
使用JSX
的寫法,關於JSX
,小編將在後續文章中會出詳細的使用教程。
爲什麼使用函數式組件
-
最主要最關鍵的原因是函數式組件不需要實例化,無狀態,沒有生命週期,所以渲染性能要好於普通組件
-
函數式組件結構比較簡單,代碼結構更清晰
函數式組件與普通組件的區別
-
函數式組件需要在聲明組件是指定functional
-
函數式組件不需要實例化,所以沒有
this
,this
通過render
函數的第二個參數來代替 -
函數式組件沒有生命週期鉤子函數,不能使用計算屬性,watch等等
-
函數式組件不能通過$emit對外暴露事件,調用事件只能通過
context.listeners.click
的方式調用外部傳入的事件 -
因爲函數式組件是沒有實例化的,所以在外部通過
ref
去引用組件時,實際引用的是HTMLElement
-
函數式組件的
props
可以不用顯示聲明,所以沒有在props
裏面聲明的屬性都會被自動隱式解析爲prop
,而普通組件所有未聲明的屬性都被解析到$attrs
裏面,並自動掛載到組件根元素上面(可以通過inheritAttrs
屬性禁止)
我不想用JSX
,能用函數式組件嗎?
在Vue2.5
之前,使用函數式組件只能通過JSX
的方式,在之後,可以通過模板語法來生命函數式組件
<!--在template 上面添加 functional屬性-->
<template functional>
<img :src="props.avatar ? props.avatar : 'default-avatar.png'" />
</template>
<!--根據上一節第六條,可以省略聲明props-->