转至: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
)的同窗, 可能比较难以理解, 但一旦理解了, 你的函数式编程思想就又升华了. :::