Vue 高级组件(HOC)实现原理

Vue 高级组件(HOC)实现原理

转至:Vue 进阶必学之高阶组件 HOCjavascript

在实际业务中, 若想简化异步状态管理, 能够使用基于slot-scopes的开源库vue-promised.css

本文主是强调实际此类高阶段组件的思想, 若是要将此使用到生产环境, 建议使用开源库vue-promisedhtml

举个例子

在日常开发中, 最多见的需求是: 异常请求数据, 并作出相应的处理:前端

  • 数据请求中, 给出提示, 如: 'loading'
  • 数据请求出错时, 给出错误提示, 如: 'failed to load data'

例如: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

什么是高阶(HOC)组件?

高阶组件, 实际上是一个函数接受一个组件为参数, 返回一个包装后的组件.github

在Vue中, 组件是一个对象, 所以高阶组件就是一个函数接受一个对象, 返回一个包装好的对象, 即:编程

高阶组件是: fn(object) => newObject
复制代码

初步实现

基于这个思路, 咱们就能够开始尝试了redux

高阶组件实际上是一个函数, 所以咱们须要实现请求管理的高阶函数, 先定义它的两个入参:

  • component, 须要被包裹的组件对象
  • promiseFunc, 异步请求函数, 必须是一个promise

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. 从子组件上取得参数, 用于发送异步请求的参数
  2. 监听子组件中请求参数的变化, 并从新发送请求
  3. 外部组件传递给hoc组件的参数, 没有传递下去.例如, 咱们在最外层使用hoc组件时, 但愿能些额外的props或attrs(或者slot等)给最内层的被包装的组件. 此时, 就须要hoc组件将这些信息传递下去.

为实现第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函数拿来使用.

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

相关连接

相关文章
相关标签/搜索