在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 相关文章推荐
isArray()函数(JavaScript中对象类型判断的几种方法)
Nov 26 Javascript
无阻塞加载脚本分析[全]
Jan 20 Javascript
编写可维护面向对象的JavaScript代码[翻译]
Feb 12 Javascript
扩展js对象数组的OrderByAsc和OrderByDesc方法实现思路
May 17 Javascript
屏蔽IE弹出&quot;您查看的网页正在试图关闭窗口,是否关闭此窗口&quot;的方法
Dec 31 Javascript
使表格的标题列可左右拉伸jquery插件封装
Nov 24 Javascript
javascript 操作符(~、&amp;、|、^、)使用案例
Dec 31 Javascript
使用JavaScript 实现的人脸检测
Mar 24 Javascript
JavaScript获取一个范围内日期的方法
Apr 24 Javascript
RequireJS使用注意细节
May 15 Javascript
AngularJS中一般函数参数传递用法分析
Nov 22 Javascript
webpack配置导致字体图标无法显示的解决方法
Mar 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
PHP中在数据库中保存Checkbox数据(2)
2006/10/09 PHP
ASP SQL防注入的方法
2008/12/25 Javascript
WordPress JQuery处理沙发头像
2009/06/22 Javascript
前端开发部分总结[兼容性、DOM操作、跨域等](持续更新)
2010/03/04 Javascript
js克隆对象、数组的常用方法介绍
2013/09/26 Javascript
原生js做的手风琴效果的导航菜单
2013/11/08 Javascript
ExtJS判断IE浏览器类型的方法
2014/02/10 Javascript
jquery中one()方法的用法实例
2015/01/16 Javascript
AngularJS入门教程之过滤器用法示例
2016/11/02 Javascript
jQuery 遍历map()方法详解
2016/11/04 Javascript
详解angular2采用自定义指令(Directive)方式加载jquery插件
2017/02/09 Javascript
Nodejs--post的公式详解
2017/04/29 NodeJs
详解ES6通过WeakMap解决内存泄漏问题
2018/03/09 Javascript
vue用Object.defineProperty手写一个简单的双向绑定的示例
2018/07/09 Javascript
深入理解Vue 组件之间传值
2018/08/16 Javascript
vue搜索和vue模糊搜索代码实例
2019/05/07 Javascript
js实现树形数据转成扁平数据的方法示例
2020/02/27 Javascript
vue+koa2搭建mock数据环境的详细教程
2020/05/18 Javascript
JavaScript中reduce()的5个基本用法示例
2020/07/19 Javascript
[01:45:05]VGJ.T vs Newbee Supermajor 败者组 BO3 第二场 6.6
2018/06/07 DOTA
python 从远程服务器下载日志文件的程序
2013/02/10 Python
python3 kmp 字符串匹配的方法
2018/07/07 Python
Python之循环结构
2019/01/15 Python
python 实现提取某个索引中某个时间段的数据方法
2019/02/01 Python
详解pandas.DataFrame中删除包涵特定字符串所在的行
2019/04/04 Python
TensorFlow基于MNIST数据集实现车牌识别(初步演示版)
2019/08/05 Python
Python中输入和输出(打印)数据实例方法
2019/10/13 Python
python使用pyecharts库画地图数据可视化的实现
2020/03/25 Python
python中pivot()函数基础知识点
2021/01/03 Python
英国男士时尚购物网站:Stuarts London
2017/10/22 全球购物
市场营销毕业求职信
2014/08/07 职场文书
支部书记四风对照材料
2014/08/28 职场文书
2014年图书室工作总结
2014/12/09 职场文书
2016春季幼儿园小班开学寄语
2015/12/03 职场文书
vue3如何优雅的实现移动端登录注册模块
2021/03/29 Vue.js
Java比较两个对象中全部属性值是否相等的方法
2021/08/07 Java/Android