在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实战之仿淘宝商城左侧导航效果
Apr 12 Javascript
jQuery实现仿百度首页滑动伸缩展开的添加服务效果代码
Sep 09 Javascript
js滑动提示效果代码分享
Mar 10 Javascript
JS限制条件补全问题实例分析
Dec 16 Javascript
微信小程序实现图片预加载组件
Jan 18 Javascript
微信小程序实现点击按钮移动view标签的位置功能示例【附demo源码下载】
Dec 06 Javascript
vue项目中应用ueditor自定义上传按钮功能
Apr 27 Javascript
angular中两种表单的区别(响应式和模板驱动表单)
Dec 06 Javascript
VUE v-model表单数据双向绑定完整示例
Jan 21 Javascript
详解小程序input框失焦事件在提交事件前的处理
May 05 Javascript
javascript中的相等操作符(==与===区别)
Dec 21 Javascript
Vue如何实现监听组件原生事件
Jul 03 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数组去除空值函数分享
2015/02/02 PHP
PHP设计模式之模板方法模式定义与用法详解
2018/04/02 PHP
php将字符串转换为数组实例讲解
2020/05/05 PHP
javascript 选择文件夹对话框(web)
2009/07/07 Javascript
Javascript 继承机制实例
2009/08/12 Javascript
jquery判断当前浏览器的实现代码
2015/11/07 Javascript
总结JavaScript设计模式编程中的享元模式使用
2016/05/21 Javascript
AngularJS实现在ng-Options加上index的解决方法
2016/11/03 Javascript
Angular JS数据的双向绑定详解及实例
2016/12/31 Javascript
react-native ListView下拉刷新上拉加载实现代码
2017/08/03 Javascript
vue项目中使用百度地图的方法
2018/06/08 Javascript
微信小程序页面间值传递的两种方法
2018/11/26 Javascript
JS使用Prim算法和Kruskal算法实现最小生成树
2019/01/17 Javascript
React中获取数据的3种方法及优缺点
2020/02/18 Javascript
python实现simhash算法实例
2014/04/25 Python
浅谈Django中的数据库模型类-models.py(一对一的关系)
2018/05/30 Python
彻底理解Python中的yield关键字
2019/04/01 Python
Django实现auth模块下的登录注册与注销功能
2019/10/10 Python
python pyecharts 实现一个文件绘制多张图
2020/05/13 Python
如何用Anaconda搭建虚拟环境并创建Django项目
2020/08/02 Python
python上下文管理器异常问题解决方法
2021/02/07 Python
10种CSS3实现的loading动画,挑一个走吧?
2020/11/16 HTML / CSS
CSS3中animation实现流光按钮效果
2020/12/21 HTML / CSS
调用HTML5的Canvas API绘制图形的快速入门指南
2016/06/17 HTML / CSS
Tory Burch美国官方网站:美国时尚生活品牌
2016/08/01 全球购物
阿迪达斯比利时官方商城:adidas比利时
2016/10/10 全球购物
服装促销活动方案
2014/02/23 职场文书
伊索寓言教学反思
2014/05/01 职场文书
公证处委托书
2015/01/28 职场文书
2015年社区中秋节活动总结
2015/03/23 职场文书
保险公司增员口号
2015/12/25 职场文书
大学生创业计划书常用模板
2019/08/07 职场文书
python中pandas对多列进行分组统计的实现
2021/06/18 Python
一级电子管军用接收机测评
2022/04/05 无线电
win11怎么消除图标小盾牌?win11消除图标小盾牌解决方法
2022/08/05 数码科技