轉至:Vue 進階必學之高階組件 HOCjavascript
在實際業務中, 若想簡化異步狀態管理, 能夠使用基於slot-scopes的開源庫vue-promised.css
本文主是強調實際此類高階段組件的思想, 若是要將此使用到生產環境, 建議使用開源庫vue-promisedhtml
在日常開發中, 最多見的需求是: 異常請求數據, 並作出相應的處理:前端
例如:vue
<template>
<div>
<div v-if="error">failed to load data!</div>
<div v-else-if="loading">loading...</div>
<div v-else>result: {{ result.status }}</div>
</div>
</template>
<script> /* eslint-disable prettier/prettier */ export default { data () { return { result: { status: 200 }, loading: false, error: false } }, async created () { try { this.loading = true const data = await this.$service.get('/api/list') this.result = data } catch (e) { this.error = true } finally { this.loading = false } } } </script>
<style lang="less" scoped></style>
複製代碼
一般狀況下, 咱們可能會這麼寫. 但這樣有一個問題, 每次使用異步請求時, 都須要去管理loading, error狀態, 都須要處理和管理數據.java
有沒有辦法抽象出來呢? 這裏, 高階組件就多是一種選擇了.git
高階組件, 實際上是一個函數接受一個組件爲參數, 返回一個包裝後的組件.github
在Vue中, 組件是一個對象, 所以高階組件就是一個函數接受一個對象, 返回一個包裝好的對象, 即:編程
高階組件是: fn(object) => newObject
複製代碼
基於這個思路, 咱們就能夠開始嘗試了redux
高階組件實際上是一個函數, 所以咱們須要實現請求管理的高階函數, 先定義它的兩個入參:
loading, error等狀態, 對應的視圖, 咱們就在高階函數中處理好, 返回一個包裝後的新組件.
export const wrapperPromise = (component, promiseFn) => {
return {
name: 'promise-component',
data() {
return {
loading: false,
error: false,
result: null
}
},
async mounted() {
this.loading = true
const result = await promiseFn().finally(() => {
this.loading = false
})
this.result = result
},
render(h) {
return h(component, {
props: {
result: this.result,
loading: this.loading
}
})
}
}
}
複製代碼
至此, 算是差很少實現了一個初級版本. 咱們添加一個示例組件:
View.vue
<template>
<div>
{{ result.status }}
</div>
</template>
<script> export default { props: { result: { type: Object, default: () => { } }, loading: { type: Boolean, default: false } } } </script>
複製代碼
此時, 若是咱們使用wrapperPromise
包裹這個View.vue
組件
const request = () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve({status: 200})
}, 500)
})
}
const hoc = wrapperPromise(View, request)
複製代碼
並在父組件(Parent.vue)中使用它渲染:
<template>
<div>
<hoc />
</div>
</template>
<script> import View from './View' import {wrapperPromise} from './utils' const request = () => { return new Promise(resolve => { setTimeout(() => { resolve({status: 200}) }, 500) }) } const hoc = wrapperPromise(View, request) export default { components: { hoc } } </script>
複製代碼
此時, 組件在空白500ms後, 渲染出了200
, 代碼運行成功, 異步數據流run通了.
進一步優化高階組件, 增長"loading"和"error"視圖, 在交互體現上更加友好一些.
/* eslint-disable max-lines-per-function */
export const wrapperPromise = (component, promiseFn) => {
return {
name: 'promise-component',
data() {
return {
loading: false,
error: false,
result: null
}
},
async mounted() {
this.loading = true
const result = await promiseFn().finally(() => {
this.loading = false
})
this.result = result
},
render(h) {
const conf = {
props: {
result: this.result,
loading: this.loading
}
}
const wrapper = h('div', [h(component, conf), this.loading ? h('div', ['loading...']) : null, this.error ? h('div', ['error!!!']) : null])
return wrapper
}
}
}
複製代碼
到目前爲止, 高階組件雖然能夠使用了, 但還不夠好, 仍缺乏一些功能, 如:
爲實現第1點, 須要在View.vue
中添加一個特定的字段, 做爲請求參數, 如: requestParams
<template>
<div>
{{ result.status + ' => ' + result.name }}
</div>
</template>
<script> export default { props: { result: { type: Object, default: () => { } }, loading: { type: Boolean, default: false } }, data () { return { requestParams: { name: 'http' } } } } </script>
<style lang="scss" scoped></style>
複製代碼
同時改寫下request函數, 讓它接受請求參數. 這裏咱們不作什麼處理, 原樣返回.
const request = params => {
return new Promise(resolve => {
setTimeout(() => {
resolve({...params, status: 200})
}, 500)
})
}
複製代碼
有一個問題是, 咱們如何可以拿到View.vue
組件中的值的呢? 能夠考慮經過ref
來獲取, 例如:
export const wrapperPromise = (component, promiseFn) => {
return {
name: 'promise-component',
data() {
return {
loading: false,
error: false,
result: null
}
},
async mounted() {
this.loading = true
// 獲取包裹組件的請求參數
const {requestParams} = this.$refs.wrapper
const result = await promiseFn(requestParams).finally(() => {
this.loading = false
})
this.result = result
},
render(h) {
const conf = {
props: {
result: this.result,
loading: this.loading
},
ref: 'wrapper'
}
const wrapper = h('div', [h(component, conf), this.loading ? h('div', ['loading...']) : null, this.error ? h('div', ['error!!!']) : null])
return wrapper
}
}
}
複製代碼
第2點, 子組件請求參數發生變化時, 父組件要同步更新請求參數, 並從新發送請求, 而後把新數據傳遞給子組件.
/* eslint-disable max-lines-per-function */
export const wrapperPromise = (component, promiseFn) => {
return {
name: 'promise-component',
data() {
return {
loading: false,
error: false,
result: null
}
},
methods: {
async request() {
this.loading = true
const {requestParams} = this.$refs.wrapper
const result = await promiseFn(requestParams).finally(() => {
this.loading = false
})
this.result = result
}
},
async mounted() {
this.$refs.wrapper.$watch('requestParams', this.request.bind(this), {
immediate: true
})
},
render(h) {
const conf = {
props: {
result: this.result,
loading: this.loading
},
ref: 'wrapper'
}
const wrapper = h('div', [h(component, conf), this.loading ? h('div', ['loading...']) : null, this.error ? h('div', ['error!!!']) : null])
return wrapper
}
}
}
複製代碼
第2個問題, 咱們只在渲染子組件時, 把$attrs
, $listeners
, $scopedSlots
傳遞下去便可.
export const wrapperPromise = (component, promiseFn) => {
return {
name: 'promise-component',
data() {
return {
loading: false,
error: false,
result: {}
}
},
methods: {
async request() {
this.loading = true
const {requestParams} = this.$refs.wrapper
const result = await promiseFn(requestParams).finally(() => {
this.loading = false
})
this.result = result
}
},
async mounted() {
this.$refs.wrapper.$watch('requestParams', this.request.bind(this), {
immediate: true,
deep: true
})
},
render(h) {
const conf = {
props: {
// 混入 $attrs
...this.$attrs,
result: this.result,
loading: this.loading
},
// 傳遞事件
on: this.$listeners,
// 傳遞 $scopedSlots
scopedSlots: this.$scopedSlots,
ref: 'wrapper'
}
const wrapper = h('div', [h(component, conf), this.loading ? h('div', ['loading...']) : null, this.error ? h('div', ['error!!!']) : null])
return wrapper
}
}
}
複製代碼
至此, 完整代碼 Parent.vue
<template>
<div>
<hoc />
</div>
</template>
<script> import View from './View' import {wrapperPromise} from './utils' const request = params => { return new Promise(resolve => { setTimeout(() => { resolve({...params, status: 200}) }, 500) }) } const hoc = wrapperPromise(View, request) export default { components: { hoc } } </script>
複製代碼
utils.js
export const wrapperPromise = (component, promiseFn) => {
return {
name: 'promise-component',
data() {
return {
loading: false,
error: false,
result: {}
}
},
methods: {
async request() {
this.loading = true
const {requestParams} = this.$refs.wrapper
const result = await promiseFn(requestParams).finally(() => {
this.loading = false
})
this.result = result
}
},
async mounted() {
this.$refs.wrapper.$watch('requestParams', this.request.bind(this), {
immediate: true,
deep: true
})
},
render(h) {
const conf = {
props: {
// 混入 $attrs
...this.$attrs,
result: this.result,
loading: this.loading
},
// 傳遞事件
on: this.$listeners,
// 傳遞 $scopedSlots
scopedSlots: this.$scopedSlots,
ref: 'wrapper'
}
const wrapper = h('div', [h(component, conf), this.loading ? h('div', ['loading...']) : null, this.error ? h('div', ['error!!!']) : null])
return wrapper
}
}
}
複製代碼
View.vue
<template>
<div>
{{ result.status + ' => ' + result.name }}
</div>
</template>
<script> export default { props: { result: { type: Object, default: () => { } }, loading: { type: Boolean, default: false } }, data () { return { requestParams: { name: 'http' } } } } </script>
複製代碼
假如, 業務上須要在某些組件的mounted的鉤子函數中幫忙打印日誌
const wrapperLog = (component) => {
return {
mounted(){
window.console.log("I am mounted!!!")
},
render(h) {
return h(component, {
on: this.$listeners,
attr: this.$attrs,
scopedSlots: this.$scopedSlots,
})
}
}
}
複製代碼
此時, 若結合前文中實現的高階組件, 若是兩個一塊兒使用呢?
const hoc = wrapperLog(wrapperPromise(View, request))
複製代碼
這樣的寫法, 看起來會有些困難, 若學過React的同窗, 能夠考慮把Redux中的compose函數拿來使用.
在瞭解redux compose函數前, 瞭解下函數式編程中純函數的定義. :::tip 純函數 純函數, 指相同的輸入,永遠會獲得相同的輸出,並且沒有任何可觀察的反作用。 :::
export default function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg
}
if (funcs.length === 1) {
return funcs[0]
}
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
複製代碼
其實compose函數, 是將var a = fn1(fn2(fn3(fn4(x))))
這種難以閱讀的嵌套調用方式改寫爲: var a = compose(fn1, fn2, fn3, fn4)(x)
的方式來調用.
redux的compose實現很簡單, 使用數組的reduce
方法來實現, 核心代碼只有一句:
return funcs.reduce((a,b) => (..args) => a(b(...args)))
複製代碼
雖然寫了多年的前端代碼, 與使用過reduce
函數, 可是看到這句代碼仍是比較懵的.
所以,在這裏舉一個例子來看看這個函數是執行的.
import {compose} from 'redux'
let a = 10
function fn1(s) {return s + 1}
function fn2(s) {return s + 2}
function fn3(s) {return s + 3}
function fn4(s) {return s + 4}
let res = fn1(fn2(fn3(fn4(a)))) // 即: 10 + 4 + 3 + 2 + 1
// 根據compose的功能, 能夠上面的代碼改寫爲:
let composeFn = compose(fn1, fn2, fn3, fn4)
let result = composeFn(a) // 20
複製代碼
根據compose
的源碼來看, composeFn
其執行等價於:
[fn1, fn2, fn3, fn4].reduce((a, b) => (...args) => a(b(...args)))
複製代碼
循環 | a的值 | b的值 | 返回值 |
---|---|---|---|
第1輪 | fn1 | fn2 | (...args) => fn1(fn2(...args)) |
第2輪 | (...args) => fn1(fn2(...args)) | fn3 | (...args) => fn1(fn2(fn3(...args))) |
第3輪 | (...args) => fn1(fn2(fn3(...args))) | fn4 | (...args) => fn1(fn2(fn3(fn4(...args)))) |
循環到最後的返回值: (...args) => fn1(fn2(fn3(fn4(...args))))
. 通過compose處理後, 函數變成了咱們想要的格式.
按這個思路, 咱們改造下wrapperPromise
函數, 讓它只接受一個被包裹的參數, 即進一步高階化它.
const wrapperPromise = (promiseFn) => {
return function(wrapper){
return {
mounted() {},
render() {}
}
}
}
複製代碼
有了這個後, 就能夠更加優雅的組件高階組件了.
const composed = compose(wrapperPromise(request), wrapperLog)
const hoc = composed(View)
複製代碼
compose
函數其實在函數式編程中也比較常見. redux
中對compose
的實現也很簡單, 理解起來應該還好.
主要是對Array.prototype.reduce
函數使用並非很熟練, 再加上使用函數返回函數的寫法, 並配上幾個連續的=>
(箭頭函數), 基本上就暈了.
::: tip warning 對於第一次接觸此類函數(compose
)的同窗, 可能比較難以理解, 但一旦理解了, 你的函數式編程思想就又昇華了. :::