在Vue中使用HOC模式的实现


Posted in Javascript onAugust 23, 2020

前言

HOC是React常用的一种模式,但HOC只能是在React才能玩吗?先来看看React官方文档是怎么介绍HOC的:

高阶组件(HOC)是React中用于复用组件逻辑的一种高级技巧。HOC自身不是ReactAPI的一部分,它是一种基于React的组合特性而形成的设计模式。

HOC它是一个模式,是一种思想,并不是只能在React中才能用。所以结合Vue的特性,一样能在Vue中玩HOC。

HOC

HOC要解决的问题

并不是说哪种技术新颖,就得使用哪一种。得看这种技术能够解决哪些痛点。

HOC主要解决的是可复用性的问题。在Vue中,这种问题一般是用Mixin解决的。Mixin是一种通过扩展收集功能的方式,它本质上是将一个对象的属性拷贝到另一个对象上去。

最初React也是使用Mixin的,但是后面发现Mixin在React中并不是一种好的模式,它有以下的缺点:

  • mixin与组件之间容易导致命名冲突
  • mixin是侵入式的,改变了原组件,复杂性大大提高。

所以React就慢慢的脱离了mixin,从而推荐使用HOC。并不是mixin不优秀,只是mixin不适合React。

HOC是什么

HOC全称:high-order component--也就是高阶组件。具体而言,高阶组件是参数为组件,返回值为新组件的函数。

而在React和Vue中组件就是函数,所以的高阶组件其实就是高阶函数,也就是返回一个函数的函数。

来看看HOC在React的用法:

function withComponent(WrappedComponent) {
  return class extends Component {
    componentDidMount () {
      console.log('已经挂载完成')
    }
    render() {
      return <WrappedComponent {...props} />;
    }
  }
}

withComponent就是一个高阶组件,它有以下特点:

  • HOC是一个纯函数,且不应该修改原组件
  • HOC不关心传递的props是什么,并且WrappedComponent不关心数据来源
  • HOC接收到的props应该透传给WrapperComponent

在Vue中使用HOC

怎么样才能将Vue上使用HOC的模式呢?

我们一般书写的Vue组件是这样的:

<template>
 <div>
  <p>{{title}}</p>
  <button @click="changeTitle"></button>
 </div>
</template>

<script>
export default {
 name: 'ChildComponent',
 props: ['title'],
 methods: {
  changeTitle () {
    this.$emit('changeTitle');
  }
 }
}
</script>

而withComponet函数的功能是在每次挂载完成后都打印一句:已经挂载完成。

既然HOC是替代mixin的,所以我们先用mixin书写一遍:

export default {
  mounted () {
    console.log('已经挂载完成')
  }
}

然后导入到ChildComponent中

import withComponent from './withComponent';
export default {
  ...
  mixins: ['withComponet'],
}

对于这个组件,我们在父组件中是这样调用的

<child-component :title='title' @changeTitle='changeTitle'></child-component>

<script>
import ChildComponent from './childComponent.vue';
export default {
  ...
  components: {ChildComponent}
}
</script>

大家有没有发现,当我们导入一个Vue组件时,其实是导入一个对象。

export default {}

至于说组件是函数,其实是经过处理之后的结果。所以Vue中的高阶组件也可以是:接收一个纯对象,返回一个纯对象。

所以改为HOC模式,是这样的:

export default function withComponent (WrapperComponent) {
  return {
    mounted () {
      console.log('已经挂载完成')
    },
    props: WrappedComponent.props,
    render (h) {
      return h(WrapperComponent, {
        on: this.$listeners,
        attrs: this.$attrs,
        props: this.$props
      })
    }
  }
}

注意{on: this.$listeners,attr: this.$attrs, props: this.props}这一句就是透传props的原理,等价于React中的<WrappedComponent {...props} />;

this.$props是指已经被声明的props属性,this.$attrs是指没被声明的props属性。这一定要两个一起透传,缺少哪一个,props都不完整。

为了通用性,这里使用了render函数来构建,这是因为template只有在完整版的Vue中才能使用。

这样似乎还不错,但是还有一个重要的问题,在Vue组件中是可以使用插槽的。

比如:

<template>
 <div>
  <p>{{title}}</p>
  <button @click="changeTitle"></button>
  <slot></slot>
 </div>
</template>

在父组件中

<child-component :title='title' @changeTitle='changeTitle'>Hello, HOC</child-component>

可以用this.$solts访问到被插槽分发的内容。每个具名插槽都有其相应的property,例如v-slot:foo中的内容将会在this.$slots.foo中被找到。而default property包括了所有没有被包含在具名插槽中的节点,或v-slot:default的内容。

所以在使用渲染函数书写一个组件时,访问this.$slots最有帮助的。

先将this.$slots转化为数组,因为渲染函数的第三个参数是子节点,是一个数组

export default function withComponent (WrapperComponent) {
  return {
    mounted () {
         console.log('已经挂载完成')
    },
    props: WrappedComponent.props,
    render (h) {
      const keys = Object.keys(this.$slots);
      const slotList = keys.reduce((arr, key) => arr.concat(this.$slots[key]), []);
      return h(WrapperComponent, {
        on: this.$listeners,
        attrs: this.$attrs,
        props: this.$props
      }, slotList)
    }
  }
}

总算是有模有样了,但这还没结束,你会发现使不使用具名插槽都一样,最后都是按默认插槽来处理的。

有点纳闷,去看看Vue源码中是怎么具名插槽的。
在src/core/instance/render.js文件中找到了initRender函数,在初始化render函数时

const options = vm.$options
const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree
const renderContext = parentVnode && parentVnode.context
vm.$slots = resolveSlots(options._renderChildren, renderContext)

这一段代码是Vue解析并处理slot的。
将vm.$options._parentVnode赋值为vm.$vnode,也就是$vnode就是父组件的vnode。如果父组件存在,定义renderContext = vm.$vnode.context。renderContext就是父组件要渲染的实例。 然后把renderContext和$options._renderChildren作为参数传进resolveSlots()函数中。

接下里看看resolveSlots()函数,在src/core/instance/render-helper/resolve-slots.js文件中

export function resolveSlots (
 children: ?Array<VNode>,
 context: ?Component
): { [key: string]: Array<VNode> } {
 if (!children || !children.length) {
  return {}
 }
 const slots = {}
 for (let i = 0, l = children.length; i < l; i++) {
  const child = children[i]
  const data = child.data
  // remove slot attribute if the node is resolved as a Vue slot node
  if (data && data.attrs && data.attrs.slot) {
   delete data.attrs.slot
  }
  // named slots should only be respected if the vnode was rendered in the
  // same context.
  if ((child.context === context || child.fnContext === context) &&
   data && data.slot != null
  ) {
   const name = data.slot
   const slot = (slots[name] || (slots[name] = []))
   if (child.tag === 'template') {
    slot.push.apply(slot, child.children || [])
   } else {
    slot.push(child)
   }
  } else {
   (slots.default || (slots.default = [])).push(child)
  }
 }
 // ignore slots that contains only whitespace
 for (const name in slots) {
  if (slots[name].every(isWhitespace)) {
   delete slots[name]
  }
 }
 return slots
}

重点来看里面的一段if语句

// named slots should only be respected if the vnode was rendered in the
// same context.
if ((child.context === context || child.fnContext === context) &&
 data && data.slot != null
) {
 const name = data.slot
 const slot = (slots[name] || (slots[name] = []))
 if (child.tag === 'template') {
  slot.push.apply(slot, child.children || [])
 } else {
  slot.push(child)
 }
} else {
 (slots.default || (slots.default = [])).push(child)
}

只有当if ((child.context === context || child.fnContext === context) && data && data.slot != null ) 为真时,才处理为具名插槽,否则不管具名不具名,都当成默认插槽处理

else {
 (slots.default || (slots.default = [])).push(child)
}

那为什么HOC上的if条件是不成立的呢?

这是因为由于HOC的介入,在原本的父组件与子组件之间插入了一个组件--也就是HOC,这导致了子组件中访问的this.$vode已经不是原本的父组件的vnode了,而是HOC中的vnode,所以这时的this.$vnode.context引用的是高阶组件,但是我们却将slot透传了,slot中的VNode的context引用的还是原来的父组件实例,所以就导致不成立。

从而都被处理为默认插槽。

解决方法也很简单,只需手动的将slot中的vnode的context指向为HOC实例即可。注意当前实例 _self 属性访问当前实例本身,而不是直接使用 this,因为 this 是一个代理对象。

export default function withComponent (WrapperComponent) {
  return {
    mounted () {
         console.log('已经挂载完成')
    },
    props: WrappedComponent.props,
    render (h) {
      const keys = Object.keys(this.$slots);
      const slotList = keys.reduce((arr, key) => arr.concat(this.$slots[key]), []).map(vnode => {
        vnode.context = this._self
        return vnode
      });
      return h(WrapperComponent, {
        on: this.$listeners,
        attrs: this.$attrs,
        props: this.$props
      }, slotList)
    }
  }
}

而且scopeSlot与slot的处理方式是不同的,所以将scopeSlot一起透传

export default function withComponent (WrapperComponent) {
  return {
    mounted () {
         console.log('已经挂载完成')
    },
    props: WrappedComponent.props,
    render (h) {
      const keys = Object.keys(this.$slots);
      const slotList = keys.reduce((arr, key) => arr.concat(this.$slots[key]), []).map(vnode => {
        vnode.context = this._self
        return vnode
      });
      return h(WrapperComponent, {
        on: this.$listeners,
        attrs: this.$attrs,
        props: this.$props,
        scopedSlots: this.$scopedSlots
      }, slotList)
    }
  }
}

这样就行了。

结尾

更多文章请移步楼主github,如果喜欢请点一下star,对作者也是一种鼓励。

到此这篇关于在Vue中使用HOC模式的文章就介绍到这了,更多相关Vue使用HOC模式内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Javascript 相关文章推荐
javascript IE中的DOM ready应用技巧
Jul 23 Javascript
new Date()问题在ie8下面的处理方法
Jul 31 Javascript
前端必备神器 Snap.svg 弹动效果
Nov 10 Javascript
js中unicode转码方法详解
Oct 09 Javascript
JavaScript转换与解析JSON方法实例详解
Nov 24 Javascript
JS实现的仿QQ空间图片弹出效果代码
Feb 23 Javascript
由简入繁实现Jquery树状结构的方法(推荐)
Jun 10 Javascript
JavaScript中实现键值对应的字典与哈希表结构的示例
Jun 12 Javascript
基于JS+Canves实现点击按钮水波纹效果
Sep 15 Javascript
ionic进入多级目录后隐藏底部导航栏(tabs)的完美解决方案
Nov 23 Javascript
JS中promise化微信小程序api
Apr 12 Javascript
解决vue路由name同名,路由重复的问题
Aug 05 Javascript
详解Howler.js Web音频播放终极解决方案
Aug 23 #Javascript
利用React高阶组件实现一个面包屑导航的示例
Aug 23 #Javascript
vue中watch和computed的区别与使用方法
Aug 23 #Javascript
vue动态设置页面title的方法实例
Aug 23 #Javascript
Vue管理系统前端之组件拆分封装详解
Aug 23 #Javascript
Vue中keep-alive组件的深入理解
Aug 23 #Javascript
google广告之另类js调用实现代码
Aug 22 #Javascript
You might like
php采集速度探究总结(原创)
2008/04/18 PHP
yii框架配置默认controller和action示例
2014/04/30 PHP
nicejforms——美化表单不用愁
2007/02/20 Javascript
用javascript自动显示最后更新时间
2007/03/15 Javascript
javascript 循环读取JSON数据的代码
2010/07/17 Javascript
40款非常有用的 jQuery 插件推荐(系列一)
2011/12/21 Javascript
Javascript生成json的函数代码(可以用php的json_decode解码)
2012/06/11 Javascript
Jquery绑定事件(bind和live的区别介绍)
2013/08/23 Javascript
jquery插件冲突(jquery.noconflict)解决方法分享
2014/03/20 Javascript
node.js中的http.response.write方法使用说明
2014/12/14 Javascript
JavaScript将数组转换成CSV格式的方法
2015/03/19 Javascript
javascript实现动态表头及表列的展现方法
2015/07/14 Javascript
基于js实现微信发送好友如何分享到朋友圈、微博
2015/11/30 Javascript
jQuery实现简易的输入框字数计数功能示例
2017/01/16 Javascript
使用vue 国际化i18n 实现多实现语言切换功能
2018/10/11 Javascript
Angular实现svg和png图片下载实现
2019/05/05 Javascript
探索JavaScript中私有成员的相关知识
2019/06/13 Javascript
基于Taro的微信小程序模板消息-获取formId功能模块封装实践
2019/07/15 Javascript
Vue状态模式实现窗口停靠功能(灵动、自由, 管理后台Admin界面)
2020/03/06 Javascript
react-router-dom 嵌套路由的实现
2020/05/02 Javascript
[10:34]DOTA2上海特级锦标赛全纪录
2016/03/25 DOTA
Python装饰器decorator用法实例
2014/11/10 Python
python使用multiprocessing模块实现带回调函数的异步调用方法
2015/04/18 Python
Python3遍历目录树实现方法
2015/05/22 Python
Python numpy多维数组实现原理详解
2020/03/10 Python
django admin后管定制-显示字段的实例
2020/03/11 Python
如何利用Python识别图片中的文字
2020/05/31 Python
python 调整图片亮度的示例
2020/12/03 Python
兰芝美国网上商城:购买LANEIGE睡眠面膜等
2017/06/30 全球购物
美国健康和保健平台:healtop
2020/07/02 全球购物
说出ArrayList,Vector, LinkedList的存储性能和特性
2015/01/04 面试题
制冷与电控专业应届生求职信
2013/11/11 职场文书
年终奖金发放管理制度,中小企业适用,拿去救急吧!
2019/07/12 职场文书
创业计划书之香辣虾火锅
2019/09/23 职场文书
Vue vee-validate插件的简单使用
2021/06/22 Vue.js
mysql如何能有效防止删库跑路
2021/10/05 MySQL