實戰技巧,Vue原來還可以這樣寫

子君發表於2020-06-29
兩隻黃鸝鳴翠柳,一堆bug上西天。

每天上班寫著重複的程式碼,當一個cv仔,忙到八九點,工作效率低,感覺自己沒有任何提升。如何能更快的完成手頭的工作,今天小編整理了一些新的Vue使用技巧。你們先加班,我先下班陪女神去逛街了。

hookEvent,原來可以這樣監聽元件生命週期

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>

小專案還用Vuex?用Vue.observable手寫一個狀態管理吧

在前端專案中,有許多資料需要在各個元件之間進行傳遞共享,這時候就需要有一個狀態管理工具,一般情況下,我們都會使用Vuex,但對於小型專案來說,就像Vuex官網所說:“如果您不打算開發大型單頁應用,使用 Vuex 可能是繁瑣冗餘的。確實是如此——如果您的應用夠簡單,您最好不要使用 Vuex”。這時候我們就可以使用Vue2.6提供的新API Vue.observable手動打造一個Vuex

1. 建立 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
   }
}

2. 在元件中引用

<template>
   <div>
     {{ userInfo.name }}
   </div>
</template>
<script>
  import { store, mutations } from '../store'
  export default {
    computed: {
      userInfo() {
        return store.userInfo 
      }
   },
   created() {
     mutations.setUserInfo({
       name: '子君'
     })
   }
}
</script>

開發全域性元件,你可能需要了解一下Vue.extend

Vue.extend是一個全域性Api,平時我們在開發業務的時候很少會用到它,但有時候我們希望可以開發一些全域性元件比如Loading,Notify,Message等元件時,這時候就可以使用Vue.extend

同學們在使用element-uiloading時,在程式碼中可能會這樣寫

// 顯示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

1. 開發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>

但這樣使用並不能滿足我們的需求

  1. 可以通過js直接呼叫方法來顯示關閉
  2. loading可以將整個頁面全部遮罩起來

2.通過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呼叫,還需要改造一下

4. 將元件掛載到Vue.prototype上面

Vue.prototype.$loading = Loading
// 在export之前將Loading方法進行繫結
export default Loading

// 在元件內使用
this.$loading()

自定義指令,從底層解決問題

什麼是指令?指令就是你女朋友指著你說,“那邊搓衣板,跪下,這是命令!”。開玩笑啦,程式設計師哪裡會有女朋友。

通過上一節我們開發了一個loading元件,開發完之後,其他開發在使用的時候又提出來了兩個需求

  1. 可以將loading掛載到某一個元素上面,現在只能是全屏使用
  2. 可以使用指令在指定的元素上面掛載loading

有需求,我們就做,沒話說

1.開發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
  }
})

2.在元素上面使用指令

<template>
  <div v-loading="visible"></div>
</template>
<script>
export default {
  data() {
    return {
      visible: false
    }
  },
  created() {
    this.visible = true
    fetch().then(() => {
      this.visible = false
    })
  }
}
</script>

3.專案中哪些場景可以自定義指令

  1. 為元件新增loading效果
  2. 按鈕級別許可權控制 v-permission
  3. 程式碼埋點,根據操作型別定義指令
  4. input輸入框自動獲取焦點
  5. 其他等等。。。

深度watchwatch立即觸發回撥,我可以監聽到你的一舉一動

在開發Vue專案時,我們會經常性的使用到watch去監聽資料的變化,然後在變化之後做一系列操作。

1.基礎用法

比如一個列表頁,我們希望使用者在搜尋框輸入搜尋關鍵字的時候,可以自動觸發搜尋,此時除了監聽搜尋框的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>

2.立即觸發

通過上面的程式碼,現在已經可以在值發生變化的時候觸發載入資料了,但是如果要在頁面初始化時候載入資料,我們還需要在created或者mounted生命週期鉤子裡面再次呼叫$_loadData方法。不過,現在可以不用這樣寫了,通過配置watch的立即觸發屬性,就可以滿足需求了

// 改造watch
export default {
  watch: {
    // 在值發生變化之後,重新載入資料
    searchValue: {
    // 通過handler來監聽屬性變化, 初次呼叫 newValue為""空字串, oldValue為 undefined
      handler(newValue, oldValue) {
        if (newValue !== oldValue) {
          this.$_loadData()
        }
      },
      // 配置立即執行屬性
      immediate: true
    }
  }
}

3.深度監聽(我可以看到你內心的一舉一動)

一個表單頁面,需求希望使用者在修改表單的任意一項之後,表單頁面就需要變更為被修改狀態。如果按照上例中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()即可取消

函式式元件,函式是元件?

什麼是函式式元件?函式式元件就是函式是元件,感覺在玩文字遊戲。使用過React的同學,應該不會對函式式元件感到陌生。函式式元件,我們可以理解為沒有內部狀態,沒有生命週期鉤子函式,沒有this(不需要例項化的元件)。

在日常寫bug的過程中,經常會開發一些純展示性的業務元件,比如一些詳情頁面,列表介面等,它們有一個共同的特點是隻需要將外部傳入的資料進行展現,不需要有內部狀態,不需要在生命週期鉤子函式裡面做處理,這時候你就可以考慮使用函式式元件。

1. 先來一個函式式元件的程式碼

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,小編將在後續文章中會出詳細的使用教程。

2.為什麼使用函式式元件

  1. 最主要最關鍵的原因是函式式元件不需要例項化,無狀態,沒有生命週期,所以渲染效能要好於普通元件
  2. 函式式元件結構比較簡單,程式碼結構更清晰

3. 函式式元件與普通元件的區別

  1. 函式式元件需要在宣告元件是指定functional
  2. 函式式元件不需要例項化,所以沒有this,this通過render函式的第二個引數來代替
  3. 函式式元件沒有生命週期鉤子函式,不能使用計算屬性,watch等等
  4. 函式式元件不能通過$emit對外暴露事件,呼叫事件只能通過context.listeners.click的方式呼叫外部傳入的事件
  5. 因為函式式元件是沒有例項化的,所以在外部通過ref去引用元件時,實際引用的是HTMLElement
  6. 函式式元件的props可以不用顯示宣告,所以沒有在props裡面宣告的屬性都會被自動隱式解析為prop,而普通元件所有未宣告的屬性都被解析到$attrs裡面,並自動掛載到元件根元素上面(可以通過inheritAttrs屬性禁止)

4.我不想用JSX,能用函式式元件嗎?

Vue2.5之前,使用函式式元件只能通過JSX的方式,在之後,可以通過模板語法來生命函式式元件

<!--在template 上面新增 functional屬性-->
<template functional>
  <img :src="props.avatar ? props.avatar : 'default-avatar.png'" />
</template>
<!--根據上一節第六條,可以省略宣告props-->

結語:

不要吹滅你的靈感和你的想象力; 不要成為你的模型的奴隸。 ——文森特・梵高

相關文章