1. 程式人生 > 實用技巧 >這是最新的一波Vue實戰技巧,不用則已,一用驚人

這是最新的一波Vue實戰技巧,不用則已,一用驚人

插槽,相信每一位Vue都有使用過,但是如何更好的去理解插槽,如何去自定義插槽,今天小編為你帶來更形象的說明。

預設插槽

大學畢業剛上班,窮鬼一個,想著每個月租房還要掏房租,所以小編決定買一個一居室,東拼西湊借了一堆債,終於湊夠了首付,買了一個小小的毛坯房。我們可以把這個一居室的毛坯房想想成一個元件,這個房子的戶型,面積,樓層都是固定的,但是室內如何裝修,擺什麼傢俱,這個卻是由你來決定的,房間內部就可以理解為插槽,允許使用者去自定義內容。

1. 開發商終於將一居室開發完交房了

<template>
  <!--這是一個一居室-->
  <div class="one-bedroom">
    <!--新增一個預設插槽,使用者可以在外部隨意定義這個一居室的內容-->
    <slot></slot>
  </div>
</template>
複製程式碼

2. 小編要開始裝修了

<template>
  <!--這裡一居室-->
  <one-bedroom>
    <!--將傢俱放到房間裡面,元件內部就是上面提供的預設插槽的空間-->
    <span>先放一個小床,反正沒有女朋友</span>
    <span>再放一個電腦桌,在家還要加班寫bug</span>
  </one-bedroom>
</template>
<script>
import OneBedroom from '../components/one-bedroom'
export default {
  components: {
    OneBedroom
  }
}
</script>

複製程式碼

具名插槽

過了幾年,小編有了女朋友,準備結婚了,一居室房間肯定不行啊,丈母孃嫌小不同意,沒辦法,只能又湊錢買大房子,買了一個兩居室(窮逼一個),因為是兩居室,所以有了主臥和次臥之分,裝修是否也不能把主臥和次臥裝修的一模一樣,所以就需要進行區分。將房子想想成元件,那麼元件就有兩個插槽,並且需要起名字進行區分。

1. 開發商終於開發完交房了

<template>
  <div class="two-bedroom">
    <!--這是主臥-->
    <div class="master-bedroom">
      <!---主臥使用預設插槽-->
      <slot></slot>
    </div>
    <!--這是次臥-->
    <div class="secondary-bedroom">
      <!--次臥使用具名插槽-->
      <slot name="secondard"></slot>
    </div>
  </div>
</template>

複製程式碼

2. 小編要賣血攢錢裝修了

<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>

複製程式碼

作用域插槽

裝修的時候,裝修師傅問我洗衣機是要放到衛生間還是陽臺,一般情況下開發商會預留放洗衣機的位置。而這個位置可以理解為插槽傳的引數,這個就是作用域插槽。

1. 看一下衛生間插槽傳了什麼引數

<template>
  <div class="two-bedroom">
    <!--其他內容省略-->
    <div class="toilet">
      <!--通過v-bind 可以向外傳遞引數, 告訴外面衛生間可以放洗衣機-->
      <slot name="toilet" v-bind="{ washer: true }"></slot>
    </div>
  </div>
</template>

複製程式碼

2. 把洗衣機放到衛生間

<template>
  <two-bedroom>
    <!--其他省略-->
    <!--衛生間插槽,通過v-slot="scope"可以獲取元件內部通過v-bind傳的值-->
    <template v-slot:toilet="scope">
      <!--判斷是否可以放洗衣機-->
      <span v-if="scope.washer">這裡放洗衣機</span>
    </template>
  </two-bedroom>
</template>
複製程式碼

插槽預設值

小編的同事不想等期房,所以就買了二手房,二手房前業主都裝修好了,可以直接入住。當然也可以重新裝修,下面是同事買的二手房。

1. 這是裝修好的二手房

<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>

複製程式碼

2. 同事決定先把主臥裝修了,以後結婚用

<second-hand-house>
    <!--主臥使用預設插槽,只裝修主臥-->
    <div>
      <span>放一個大床,要結婚了,嘿嘿嘿</span>
      <span>放一個衣櫃,老婆的衣服太多了</span>
      <span>算了,還是放一個電腦桌吧,還要寫bug</span>
    </div>
  </second-hand-house>
複製程式碼

瞭解選項合併策略,自定義生命週期鉤子函式

當你使用Vuemixins的時候,是否有發現,如果混入的methods裡面的方法與元件的方法同名,則會被元件方法覆蓋,但是生命週期函式如果重名,混入的與元件自身的都會被執行,且執行順序是先混入和自身,這是怎麼做到的呢?

1. 瞭解Vue合併策略

Vue中,不同的選項有不同的合併策略,比如 data,props,methods是同名屬性覆蓋合併,其他直接合並,而生命週期鉤子函式則是將同名的函式放到一個數組中,在呼叫的時候依次呼叫,具體可參考小編前面的一篇文章絕對乾貨~!學會這些Vue小技巧,可以早點下班和女神約會了

Vue中,提供了一個api, Vue.config.optionMergeStrategies,可以通過這個api去自定義選項的合併策略。

在程式碼中列印

console.log(Vue.config.optionMergeStrategies)
複製程式碼
控制檯列印內容

通過上圖可以看到Vue所有選項的合併策略函式,我們可以通過覆蓋上面的方法,來自定義合併策略函式,不過一般用不到。

2. 通過合併策略自定義生命週期函式

背景

最近客戶給領導反饋,我們的系統用一段時間,瀏覽器就變得有點卡,不知道為什麼。問題出來了,本來想甩鍋到後端,但是瀏覽器問題,沒法甩鍋啊,那就排查吧。

後來發現頁面有許多定時器,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') {
        // 開啟那一堆東西
      }
    }
  }
}
複製程式碼

通過上面的程式碼,可以看到在每一個需要監聽處理的檔案都要寫一堆事件監聽,判斷頁面是否顯示的程式碼,一處兩處還可以,檔案多了就頭疼了,這時候小編突發奇想,定義一個頁面顯示隱藏的生命週期鉤子,把這些判斷都封裝起來,哪裡需要點哪裡,so easy(點讀機記得廣告費)。

自定義生命週期鉤子函式

定義生命週期函式 pageHiddenpageVisible

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)
    })
  }
}

/**
 * 新增生命週期鉤子函式
 * @param {*} rootVm vue 根例項,在頁面顯示隱藏時候,通過root向下通知
 */
export function init() {
  const optionMergeStrategies = Vue.config.optionMergeStrategies
  /*
    定義了兩個生命週期函式 pageVisible, pageHidden
    為什麼要賦值為 optionMergeStrategies.created呢
    這個相當於指定 pageVisible, pageHidden 的合併策略與 created的相同(其他生命週期函式都一樣)
   */
  optionMergeStrategies.pageVisible = optionMergeStrategies.beforeCreate
  optionMergeStrategies.pageHidden = optionMergeStrategies.created
}

/**
 * 將事件變化繫結到根節點上面
 * @param {*} rootVm
 */
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)
    }
  })
}

複製程式碼

應用

  1. 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)

複製程式碼
  1. 在需要的地方監聽生命週期函式
export default {
  pageVisible() {
    console.log('頁面顯示出來了')
  },
  pageHidden() {
    console.log('頁面隱藏了')
  }
}
複製程式碼

provideinject,不止父子傳值,祖宗傳值也可以

Vue相關的面試經常會被面試官問道,Vue父子之間傳值的方式有哪些,通常我們會回答,props傳值,$emit事件傳值,vuex傳值,還有eventbus傳值等等,今天再加一種provideinject傳值,離offer又近了一步。(對了,下一節還有一種)

使用過React的同學都知道,在React中有一個上下文Context,元件可以通過Context向任意後代傳值,而Vueprovideinject的作用於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,這個是怎麼做到的?接下來我們自己手寫一個表單模擬一下

自己手寫一個表單

我們現在模仿element-ui的表單,自己自定義一個,檔案目錄如下

自定義表單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例項上面的屬性與方法了

**在上面程式碼中我們使用了自定義v-model,關於自定義v-model可以閱讀小編前面的文章絕對乾貨~!學會這些Vue小技巧,可以早點下班和女神約會了 **

在專案中使用

<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'
      })
    }
  }
}
複製程式碼

如果我們希望inject進來的屬性的名字不叫customForm,而是叫parentForm,如下程式碼

inject: {
    // 注入的屬性名稱
    parentForm: {
      // 通過 from 指定從哪個屬性注入
      from: 'customForm',
      default: () => ({
        size: 'default'
      })
    }
  },
  computed: {
    // 通過計算元件獲取元件的size, 如果當前元件傳入,則使用當前元件的,否則是否form元件的
    getSize() {
      return this.size || this.parentForm.size
    }
  }
複製程式碼

使用限制

  1. provideinject的繫結不是可響應式的。但是,如果你傳入的是一個可監聽的物件,如上面的customForm: this,那麼其物件的屬性還是可響應的。

  2. Vue官網建議provideinject 主要在開發高階外掛/元件庫時使用。不推薦用於普通應用程式程式碼中。因為provideinject在程式碼中是不可追溯的(ctrl + f可以搜),建議可以使用Vuex代替。 但是,也不是說不能用,在區域性功能有時候用了作用還是比較大的。

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監聽要註冊的事件

export default {
  name: 'Board',
  created() {
    this.$on('register',(component) => {
      // 處理註冊邏輯
    })
  }
}
複製程式碼

$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 也沒有解決兄弟元件間的通訊問題。“

確實如官網所說,這種事件流的方式確實不容易讓人理解,而且後期維護成本比較高。但是在小編看來,不管黑貓白貓,能抓老鼠的都是好貓,在許多特定的業務場景中,因為業務的複雜性,很有可能使用到這樣的通訊方式。但是使用歸使用,但是不能濫用,小編一直就在專案中有使用。