在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 相关文章推荐
JQuery上传插件Uploadify使用详解及错误处理
Apr 27 Javascript
JavaScript 类的定义和引用 JavaScript高级培训 自定义对象
Apr 27 Javascript
Node.js实用代码段之正确拼接Buffer
Mar 17 Javascript
原生js实现百叶窗效果及原理介绍
Apr 12 Javascript
JavaScript的模块化开发框架Sea.js上手指南
May 12 Javascript
本地Bootstrap文件字体图标引入却无法显示问题的解决方法
Apr 18 Javascript
Javascript ES6中对象类型Sets的介绍与使用详解
Jul 17 Javascript
vue拦截器实现统一token,并兼容IE9验证功能
Apr 26 Javascript
AngularJS ui-router刷新子页面路由的方法
Jul 23 Javascript
在vue中使用vue-echarts-v3的实例代码
Sep 13 Javascript
使用vue2.0创建的项目的步骤方法
Sep 25 Javascript
微信小程序登录数据解密及状态维持实例详解
May 06 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
js限制checkbox勾选的个数以及php获取多个checkbbox的方法深入解析
2013/07/18 PHP
typecho插件编写教程(五):核心代码
2015/05/28 PHP
thinkphp框架表单数组实现图片批量上传功能示例
2020/04/04 PHP
关于document.cookie的使用javascript
2008/04/11 Javascript
javascript eval和JSON之间的联系
2009/12/31 Javascript
一步一步制作jquery插件Tabs实现过程
2010/07/06 Javascript
使用JS 清空File控件的路径值
2013/07/08 Javascript
浏览器页面区域大小的js获取方法
2013/09/21 Javascript
jQuery延迟加载图片插件Lazy Load使用指南
2015/03/25 Javascript
JavaScript使用pop方法移除数组最后一个元素用法实例
2015/04/06 Javascript
javaScript数组迭代方法详解
2016/04/14 Javascript
JavaScript箭头(arrow)函数详解
2017/06/04 Javascript
bootstrap-Treeview实现级联勾选
2017/11/23 Javascript
微信小程序中使用wxss加载图片并实现动画效果
2018/08/13 Javascript
css配合JavaScript实现tab标签切换效果
2018/10/11 Javascript
ES6顶层对象、global对象实例分析
2019/06/14 Javascript
IntelliJ IDEA编辑器配置vue高亮显示
2019/09/26 Javascript
微信小程序实现注册登录功能(表单校验、错误提示)
2019/12/10 Javascript
JavaScript实现拖拽盒子效果
2020/02/06 Javascript
OpenLayers3加载常用控件使用方法详解
2020/09/25 Javascript
Python进行数据科学工作的简单入门教程
2015/04/01 Python
python 类详解及简单实例
2017/03/24 Python
基于Python闭包及其作用域详解
2017/08/28 Python
Python实现爬虫抓取与读写、追加到excel文件操作示例
2018/06/27 Python
Python Pandas数据中对时间的操作
2019/07/30 Python
python各层级目录下import方法代码实例
2020/01/20 Python
Python用access判断文件是否被占用的实例方法
2020/12/17 Python
丝芙兰新加坡官网:Sephora新加坡
2018/12/04 全球购物
乡下人家教学反思
2014/02/01 职场文书
抗洪抢险事迹材料
2014/05/06 职场文书
中职生求职信
2014/07/01 职场文书
英文慰问信范文
2015/03/24 职场文书
2015年学生管理工作总结
2015/05/26 职场文书
《云雀的心愿》教学反思
2016/02/23 职场文书
2007年老电脑安装win11会怎么样? 网友实测win11在老电脑运行良好
2021/11/21 数码科技
深入解析Apache Hudi内核文件标记机制
2022/03/31 Servers