在vue中,高阶组件其实就是一个高阶函数, 即返回一个组件函数的函数。高阶组件的特点:1、是无副作用的纯函数,且不应该修改原组件,即原组件不能有变动;2、不关心传递的数据(props)是什么,并且新生成组件不关心数据来源;3、接收到的props应该传递给被包装组件,即直接将原组件prop传给包装组件;4、高阶组件完全可以添加、删除、修改props。
本教程操作环境:windows7系统、vue3版,dell g3电脑。
高阶组件介绍
vue 高阶组件的认识,在react中组件是以复用代码实现的,而vue中是以mixins 实现,并且官方文档中也缺少一些高阶组件的概念,因为在vue中实现高阶组很困难,并不像react简单,其实vue中mixins也同样和以代替,在读了一部分源码之后,对vue有了更深的认识
所谓高阶组件其实就是一个高阶函数, 即返回一个组件函数的函数,vue中怎么实现呢? 注意 高阶组件有如下特点
高阶组件(hoc)应该是无副作用的纯函数,且不应该修改原组件,即原组件不能有变动 高阶组件(hoc)不关心你传递的数据(props)是什么,并且新生成组件不关心数据来源 高阶组件(hoc)接收到的 props 应该传递给被包装组件即直接将原组件prop传给包装组件 高阶组件完全可以添加、删除、修改 props
高阶组件举例
base.vue
props: {{test}}
vue 组件主要就是三点:props、event 以及 slots。对于 base组件 组件而言,它接收一个数字类型的 props 即 test,并触发一个自定义事件,事件的名称是:base-click,没有 slots。我们会这样使用该组件:
现在我们需要 base-component 组件每次挂载完成的时候都打印一句话:haha,同时这也许是很多组件的需求,所以按照 mixins 的方式,我们可以这样做,首先定义个 mixins
export default consolemixin { mounted () { console.log('haha') } }
然后在 base 组件中将 consolemixin 混入:
props: {{test}}
这样使用 base 组件的时候,每次挂载完成之后都会打印一句 haha,不过现在我们要使用高阶组件的方式实现同样的功能,回忆高阶组件的定义:接收一个组件作为参数,返回一个新的组件,那么此时我们需要思考的是,在 vue 中组件是什么?vue 中组件是函数,不过那是最终结果,比如我们在单文件组件中的组件定义其实就是一个普通的选项对象,如下:
export default { name: 'base', props: {...}, mixins: [...] methods: {...} }
这难道不是一个纯对象嘛
import base from './base.vue' console.log(base)
这里的base是什么呢 对就是一个json对象,而当以把他加入到一个组件的components,vu最终会以该参数即option来构造实例的构造函数,所以vue中组件就是个函数,但是在引入之前仍只是一个options对象,所以这样就很好明白了 vue中组件开始只是一个对象,即高阶组件就是 一个函数接受一个纯对象,并且返回一个新纯对象
export default function console (basecomponent) { return { template: '', components: { wrapped: basecomponent }, mounted () { console.log('haha') } } }
这里 console就是一个高阶组件,它接受一个参数 basecomponent即传入的组件,返回一个新组件,将basecomponent作为新组件的子组件并且在mounted里设置钩子函数 打印haha,我们可以完成mixins同样做到的事,我们并没有修改子组件base,这里的 $listeners $attrs 其实是在透传props 和事件 那这样真的就完美解决问题了吗?不是的,首先 template 选项只有在完整版的 vue 中可以使用,在运行时版本中是不能使用的,所以最起码我们应该使用渲染函数(render)替代模板(template)
console.js
export default function console (basecomponent) { return { mounted () { console.log('haha') }, render (h) { return h(basecomponent, { on: this.$listeners, attrs: this.$attrs, }) } } }
我们将模板改写成了渲染函数,看上去没什么问题,实际还是有问题,上面的代码中 basecomponent 组件依然收不到 props,为什么呢,我们不是已经在 h 函数的第二个参数中将 attrs 传递过去了吗,怎么还收不到?当然收不到,attrs 指的是那些没有被声明为 props 的属性,所以在渲染函数中还需要添加 props 参数:
export default function console (basecomponent) { return { mounted () { console.log('haha') }, render (h) { return h(basecomponent, { on: this.$listeners, attrs: this.$attrs, props: this.$props }) } } }
那这样呢 其实还是不行 props始终是空对象,这里的props是高阶组件的对象,但是高阶组件并没有声明props所以如此故要再声明一个props
export default function console (basecomponent) { return { mounted () { console.log('haha') }, props: basecomponent.props, render (h) { return h(basecomponent, { on: this.$listeners, attrs: this.$attrs, props: this.$props }) } } }
ok 一个差不多的高阶组件就完成了 但是能还每完 我们只实现了 透传props,透传事件,emmmm就剩下slot了 我们修改 base 组件为其添加一个具名插槽和默认插槽 base.vue
props: {{test}}===========
basecomponent slot
default slot
enhancedcomponent slot
default slot
这里的执行结果就是 wrapbase里的slot都没有了 所以就要改一下高阶组建了
function console (basecomponent) { return { mounted () { console.log('haha') }, props: basecomponent.props, render (h) { // 将 this.$slots 格式化为数组,因为 h 函数第三个参数是子节点,是一个数组 const slots = object.keys(this.$slots) .reduce((arr, key) => arr.concat(this.$slots[key]), []) return h(basecomponent, { on: this.$listeners, attrs: this.$attrs, props: this.$props }, slots) // 将 slots 作为 h 函数的第三个参数 } } }
这时 slot内容确实渲染出来了 但是顺序不太对 高阶组件的全部渲染到了末尾。。 其实 vue在处理具名插槽会考虑作用域的因素 首先 vue 会把模板(template)编译成渲染函数(render),比如如下模板:
base slot
会被编译成如下渲染函数:
var render = function() { var _vm = this var _h = _vm.$createelement var _c = _vm._self._c || _h return _c("div", [ _c("div", { attrs: { slot: "slot1" }, slot: "slot1" }, [ _vm._v("base slot") ]) ]) }
观察上面的渲染函数我们发现普通的 dom 是通过 _c 函数创建对应的 vnode 的。现在我们修改模板,模板中除了有普通 dom 之外,还有组件,如下:
base slot
default slot
其render函数
var render = function() { var _vm = this var _h = _vm.$createelement var _c = _vm._self._c || _h return _c( "div", [ _c("base", [ _c("p", { attrs: { slot: "slot1" }, slot: "slot1" }, [ _vm._v("base slot") ]), _vm._v(" "), _c("p", [_vm._v("default slot")]) ]) ], ) }
我们发现无论是普通dom还是组件,都是通过 _c 函数创建其对应的 vnode 的 其实 _c 在 vue 内部就是 createelement 函数。createelement 函数会自动检测第一个参数是不是普通dom标签如果不是普通dom标签那么 createelement 会将其视为组件,并且创建组件实例,注意组件实例是这个时候才创建的 但是创建组件实例的过程中就面临一个问题:组件需要知道父级模板中是否传递了 slot 以及传递了多少,传递的是具名的还是不具名的等等。那么子组件如何才能得知这些信息呢?很简单,假如组件的模板如下
base slot
default slot
父组件的模板最终会生成父组件对应的 vnode,所以以上模板对应的 vnode 全部由父组件所有,那么在创建子组件实例的时候能否通过获取父组件的 vnode 进而拿到 slot 的内容呢?即通过父组件将下面这段模板对应的 vnode 拿到
base slot
default slot
如果能够通过父级拿到这段模板对应的 vnode,那么子组件就知道要渲染哪些 slot 了,其实 vue 内部就是这么干的,实际上你可以通过访问子组件的 this.$vnode 来获取这段模板对应的 vnode
this.$vnode 并没有写进 vue 的官方文档
子组件拿到了需要渲染的 slot 之后进入到了关键的一步,这一步就是导致高阶组件中透传 slot 给 base组件 却无法正确渲染的原因 children的vnode中的context引用父组件实例 其本身的context也会引用本身实例 其实是一个东西
console.log(this. vnode.context===this.vnode.componentoptions.children[0].context) //ture
而 vue 内部做了一件很重要的事儿,即上面那个表达式必须成立,才能够正确处理具名 slot,否则即使 slot 具名也不会被考虑,而是被作为默认插槽。这就是高阶组件中不能正确渲染 slot 的原因
即 高阶组件中 本来时父组件和子组件之间插入了一个组件(高阶组件),而子组件的 this.$vnode其实是高阶组件的实例,但是我们将slot透传给子组件,slot里 vnode 的context实际引用的还是父组件 所以
console.log(this.vnode.context === this.vnode.componentoptions.children[0].context) // false
最终导致具名插槽被作为默认插槽,从而渲染不正确。
决办法也很简单,只需要手动设置一下 slot 中 vnode 的 context 值为高阶组件实例即可
function console (base) { return { mounted () { console.log('haha') }, props: base.props, render (h) { const slots = object.keys(this.$slots) .reduce((arr, key) => arr.concat(this.$slots[key]), []) // 手动更正 context .map(vnode => { vnode.context = this._self //绑定到高阶组件上 return vnode }) return h(wrappedcomponent, { on: this.$listeners, props: this.$props, attrs: this.$attrs }, slots) } } }
说明白就是强制把slot的归属权给高阶组件 而不是 父组件 通过当前实例 _self 属性访问当实例本身,而不是直接使用 this,因为 this 是一个代理对象
【相关推荐:vuejs视频教程、web前端开发】
以上就是vue高阶组件是什么的详细内容,更多请关注其它相关文章!