在Vue中使用HOC模式的实现

前言

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模式内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 详解Vue 开发模式下跨域问题

    设置请求头部 后端设置请求头部Access-Control-Allow-Credentials: true和Access-Control-Allow-Origin: www.xxx.com 前端post请求设置withCredentials=true 这里用了axios的请求数据方法代码如下: import axios from 'axios' import config from '../config' export default { request (method, uri, data,

  • Vue2.0 事件的广播与接收(观察者模式)

    1.Vue1.0 事件的广播与接收(观察者模式) 早期vue1.0组件之间的这通信传递数据的方法,vue官网给出了两上方法 $dispatch 和 $broadcast. 但vue2.0之后就弃用 这两个方法,以下原因是vue官网给出来的 官方文档 2.Vue2.0 事件的广播与接收(观察者模式) vue2.0中可以使用 $emit, $on, $off 分别来分发.监听.取消监听事件.官方的给出的最简单的升级建议是使用集中的事件处理器,而且也明确说明了 一个空的vue实例就可以做到,因为Vue

  • Vue-router 中hash模式和history模式的区别

    Vue-router 中hash模式和history模式的关系 在vue的路由配置中有mode选项 最直观的区别就是在url中 hash 带了一个很丑的 # 而history是没有#的 mode:"hash";   mode:"history"; hash模式和history模式的不同 对于vue这类渐进式前端开发框架,为了构建 SPA(单页面应用),需要引入前端路由系统,这也就是 Vue-Router 存在的意义.前端路由的核心,就在于 -- 改变视图的同时不会向

  • Vue cli3 库模式搭建组件库并发布到 npm的流程

    市面上目前已有各种各样的UI组件库,比如 Element 和 iView ,他们的强大毋庸置疑.但是我们面临的情况是需求越来越复杂,当它们不能再满足我们需求的时候,这个时候就有必要开发一套属于自己团队的组件库了. 所以本文的目的就是让读者能通过此文,小能做一个简单的插件供人使用,大能架构和维护一个组件库不在话下. 以下一个简单的颜色选择器插件 vColorPicker 讲述从开发到上线到npm的流程. vColorPicker 插件 DEMO 一.技术栈 如何通过新版脚手架创建项目,这里就不提了

  • Vue编写可显示周和月模式的日历 Vue自定义日历内容的显示

    之前写了一篇周和月日历,但感觉体验不是太好,所以有重新做了一遍,加上了动画.还可以自定义显示日历里的内容. 现在贴出项目源码,我现在是放在CSDN里的下载资源,这里哦 现在我上传带了GitHub上了,可以去这里下载哦,如果觉得好的话希望能给个star,谢谢支持 1.总共分为两个组件(父组件calendar.vue) <template> <div class="calendar-box"> <ul class="calendar-head&quo

  • 详解如何去除vue项目中的#——History模式

    使用vue-cli搭建的环境,在配置好路由之后,可以看到下面的情况: 但是不难发现#的出现真的很丑陋,并且也不知道这是什么作用? 所以就去Stack Overflow上搜索了,果然还有~  看来Stack Overflow是真的强大,你在项目中遇到的问题实际上在so上都已经被问过并且解决了,这不:   这是最高票的回答,即在vue2中将mode模式设置为history,试过之后确实奏效! 但是知道这样可以解决问题,却不知道为什么,这是不行的, 随着连接,我们看到了文档. 所以这篇文章也就是引申到

  • Vue单页式应用(Hash模式下)实现微信分享的实例

    本文介绍了Vue单页式应用(Hash模式下)实现微信分享的实例,分享给大家,具体如下: 前端微信分享的基本步骤: 一.绑定域名: 先登录微信公众平台进入"公众号设置"的"功能设置"里填写"JS接口安全域名".这个不多说,微信开发的都应该清楚. 二.引入js文件: 在需要调用JS接口的页面引入如下JS文件,(支持https):http://res.wx.qq.com/open/js/jweixin-1.0.0.js. 请注意,如果你的页面启用了ht

  • 在nginx上部署vue项目(history模式)的方法

    vue-router 默认是hash模式,使用url的hash来模拟一个完整的url,当url改变的时候,页面不会重新加载.但是如果我们不想hash这种以#号结尾的路径时候的话,我们可以使用路由的history的模式.比如如下网址: 使用hash模式的话,那么访问变成 http://localhost:8080/bank/page/count/#/ 这样的访问,如果路由使用 history的话,那么访问的路径变成 如下: http://localhost:8080/bank/page/count

  • 在Vue中使用HOC模式的实现

    前言 HOC是React常用的一种模式,但HOC只能是在React才能玩吗?先来看看React官方文档是怎么介绍HOC的: 高阶组件(HOC)是React中用于复用组件逻辑的一种高级技巧.HOC自身不是ReactAPI的一部分,它是一种基于React的组合特性而形成的设计模式. HOC它是一个模式,是一种思想,并不是只能在React中才能用.所以结合Vue的特性,一样能在Vue中玩HOC. HOC HOC要解决的问题 并不是说哪种技术新颖,就得使用哪一种.得看这种技术能够解决哪些痛点. HOC主

  • vue中的mvvm模式讲解

    学习了MVVM模式 起先还觉得有点难,后面与双向数据绑定链接起来还是很容易理解的. 那么什么是MVVM呢? 不明思议咋们肯定和我想的一样   每个单词的首写字母呗! 对没错就是它 Model-View-ViewModel.  可以实现我们的双向数据绑定 下面我来用我的理解解析下它们之间的关系: 哈哈 有点小尴尬哈 先来说下View与Model之间有联系吗? 在MVVM架构下,View 和 Model 之间其实并没有直接的联系,而是通过ViewModel进行交互,Model 和 ViewModel

  • vue中的严格模式如何取消

    目录 如何取消严格模式 解决办法 取消严格模式,去除格式警告 如何取消严格模式 做项目当中如果遇到以下这种报错: 解决办法 1.vue cli4项目中会有单独的一个eslintrc.js文件,这个js文件里面就是代码规范的配置项,你可以尝试注释掉’@vue/standard’, 即关闭Eslint语法规范 // '@vue/standard' 重启项目npm run serve即可. 2.找到vue.config.js,如果没有你可以自己新建一个,里面配置以下内容, lintOnSave: fa

  • Vue中Router路由两种模式hash与history详解

    hash 模式 (默认) 工作原理: 监听网页的hash值变化 -> onhashchange事件, 获取location.hash 使用 URL 的 hash 来模拟一个完整的 URL,于是当 URL 改变时,页面不会重新加载. 会给用户好像跳转了网页一样的感觉, 但是实际上没有跳转 主要用在单页面应用(SPA) // 模拟原理 // 监听页面hash值变化 window.onhashchange = function(){ // 获取当前url的哈希值 const _hash = locat

  • Vue中$on和$emit的实现原理分析

    目录 Vue中发布订阅模式 $emit和$on用法深挖 首先剖析一下$on的原理实现 有一点要记住先定义的先触发 分析$emit 先打个断点 Vue中发布订阅模式 在Vue中采用了发布订阅模式,典型的兄弟组件间的通信$on和$emit 发布订阅模式:(订阅者.发布者.信号中心) 一个发布者$emit发布一个事件到信号中心 eventBus ,订阅者们 $on 通过信号中心收到该事件,进行处理 在这里模拟一个自定义事件 $on和$emit事件 class EventBus{ constructor

  • 发布订阅模式在vue中的实际运用实例详解

    订阅发布模式定义了一种一对多的依赖关系,让多个订阅者对象同时监听某一个主题对象.这个主题对象在自身状态变化时,会通知所有订阅者对象,使它们能够自动更新自己的状态. 比如addEventListener 这个api就是个发布订阅模式 如果用过vue的同学,可以把他类比于 watch 下面我们看一个例子 var observe={ fnsObj:{}, // 订阅方法 on:function(key,fn){ if(!observe.fnsObj[key]){ observe.fnsObj[key]

  • 在vue中封装的弹窗组件使用队列模式实现方法

    前言 由于业务需要,需要在封装的弹窗组件中引入定时器实现倒计时效果,但是如果同时触发两个弹窗,就会导致计时器bug,前一个弹窗的定时器没有被清除,倒计时就会错乱,此时想到的解决办法就是采用队列模式,将每一个需要的弹窗存到队列中,依次的将弹窗展示出来,同时清除定时器 什么是队列 队列(Queue)是先进先出(FIFO, First-In-First-Out)的线性表.在具体应用中通常用链表或者数组来实现.队列只允许在尾部进行插入操作(入队 enqueue),在头部进行删除操作(出队 dequeue

  • Vue中如何把hash模式改为history模式

    目录 把hash模式改为history模式 关于路由hash和history模式 hash模式 history 模式 二者对比 把hash模式改为history模式 如上图所示非常简单 只需要在文件router下的index.js里加上一个mode:'history’即可把hash模式改为history模式.这个时候url上面的#号就不会再存在了,这样就把url成功把hash模式改成history了 关于路由hash和history模式 hash模式 hash 就是指 url 后的 # 号以及后

  • 在Vue中使用echarts的实例代码(3种图)

    前言 公司的项目中需要对数据做可视化处理,高级点的D3.js目前还没接触到,因此选用了大众化的Echarts, 在vue的生态系统中已经有实现好的vue-echarts,但是使用现成的就意味着必须使用它定制好的数据结构,我也没办法对他进行一些修改.我个人也偏向于原生JS编程,因此没有采用,而是自己在vue中实现了对数据的可视化处理,先来看看效果图 以下数据已做脱敏处理 这是目前用到的三种图. 可以看到,我在图表的外部添加了标题及说明,以及右侧的选择框组件,视图可以根据选择的不同,图表进行动态切换

  • Vue中添加过渡效果的方法

    关于vue的过渡效果,vue官方给的概述是这样的. Vue 在插入.更新或者移除 DOM 时,提供多种不同方式的应用过渡效果. 包括以下工具: 1.在 CSS 过渡和动画中自动应用 class 2.可以配合使用第三方 CSS 动画库,如 Animate.css 3.在过渡钩子函数中使用 JavaScript 直接操作 DOM 4.可以配合使用第三方 JavaScript 动画库,如 Velocity.js 其中里面也给出了许多的例子,但是例子给到了"多个元素过渡"的时候,就没有案例的代

随机推荐