在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
d477f9ce7bf77f53fbcf36bec1b69b7a dc6dce4a544fdca2df29d5ac0ea9906b be3b8d7b68acfe2d6133cd36dc6a63f1props: {{test}}94b3e26ee717c64999d7867364b1b4a3 16b28748ea4df4d9c2150843fecfba68 21c97d3a051048b8e55e3c8f199a54b2 3f1c4e4b6b16bbbd69b2ee476dc4f83a export default { name: 'Base', props: { test: Number }, methods: { Click () { this.$emit('Base-click') } } } 2cacc6d41bbb37262a98f745aa00fbf0
Vue 组件主要就是三点:props、event 以及 slots。对于 Base组件 组件而言,它接收一个数字类型的 props 即 test,并触发一个自定义事件,事件的名称是:Base-click,没有 slots。我们会这样使用该组件:
现在我们需要 base-component 组件每次挂载完成的时候都打印一句话:haha,同时这也许是很多组件的需求,所以按照 mixins 的方式,我们可以这样做,首先定义个 mixins
export default consoleMixin { mounted () { console.log('haha') } }
然后在 Base 组件中将 consoleMixin 混入:
d477f9ce7bf77f53fbcf36bec1b69b7a dc6dce4a544fdca2df29d5ac0ea9906b be3b8d7b68acfe2d6133cd36dc6a63f1props: {{test}}94b3e26ee717c64999d7867364b1b4a3 16b28748ea4df4d9c2150843fecfba68 21c97d3a051048b8e55e3c8f199a54b2 3f1c4e4b6b16bbbd69b2ee476dc4f83a export default { name: 'Base', props: { test: Number }, mixins: [ consoleMixin ], methods: { Click () { this.$emit('Base-click') } } } 2cacc6d41bbb37262a98f745aa00fbf0
这样使用 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: '0040ddd46d317a97de61e65cb3442a89', 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
d477f9ce7bf77f53fbcf36bec1b69b7a dc6dce4a544fdca2df29d5ac0ea9906b 0817b6bf5b3183ab1a144d078c45fdbcprops: {{test}}54bdf357c58b8a65c66d7c19c8e4d114 e8dddc28d3bfad1f72ef15bb99cfb297 ce4ce005a24c94d9b4e4a39d8a2161ed7971cf77a46923278913ee247bc958ee e388a4556c0f65e1904146cc1a846bee===========94b3e26ee717c64999d7867364b1b4a3 58cb293b8600657fad49ec2c8d37b47238b537b6886351ac721d89624ba185ca 4842836e9b56128d85f8ca400a7b33ba 16b28748ea4df4d9c2150843fecfba68 21c97d3a051048b8e55e3c8f199a54b2 3f1c4e4b6b16bbbd69b2ee476dc4f83a export default { ... } 2cacc6d41bbb37262a98f745aa00fbf0 d477f9ce7bf77f53fbcf36bec1b69b7a dc6dce4a544fdca2df29d5ac0ea9906b 88d574120e9e0d5bf9eb3bcd60be7003 9d8e5d4a2d5c644920def765daa3de8aBaseComponent slot2e9b454fa8428549ca2e64dfac4625cd e388a4556c0f65e1904146cc1a846beedefault slot94b3e26ee717c64999d7867364b1b4a3 686fde5332128d14fa2aa6dd6ebb0c77 df1b898a74c042562246884f2d946ef4 9d8e5d4a2d5c644920def765daa3de8aEnhancedComponent slot2e9b454fa8428549ca2e64dfac4625cd e388a4556c0f65e1904146cc1a846beedefault slot94b3e26ee717c64999d7867364b1b4a3 c29ea36a6f271e266acde995154dc3a8 16b28748ea4df4d9c2150843fecfba68 21c97d3a051048b8e55e3c8f199a54b2 3f1c4e4b6b16bbbd69b2ee476dc4f83a import Base from './Base.vue' import hoc from './Console.js' const wrapBase = Console(Base) export default { components: { Base, wrapBase } } 2cacc6d41bbb37262a98f745aa00fbf0
这里的执行结果就是 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),比如如下模板:
dc6dce4a544fdca2df29d5ac0ea9906b 74997f6adba13580e4e439db54218f0dBase slot94b3e26ee717c64999d7867364b1b4a3 16b28748ea4df4d9c2150843fecfba68
会被编译成如下渲染函数:
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 之外,还有组件,如下:
dc6dce4a544fdca2df29d5ac0ea9906b 88d574120e9e0d5bf9eb3bcd60be7003 74997f6adba13580e4e439db54218f0dBase slot94b3e26ee717c64999d7867364b1b4a3 e388a4556c0f65e1904146cc1a846beedefault slot94b3e26ee717c64999d7867364b1b4a3 686fde5332128d14fa2aa6dd6ebb0c77 16b28748ea4df4d9c2150843fecfba68
其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 以及传递了多少,传递的是具名的还是不具名的等等。那么子组件如何才能得知这些信息呢?很简单,假如组件的模板如下
dc6dce4a544fdca2df29d5ac0ea9906b 88d574120e9e0d5bf9eb3bcd60be7003 74997f6adba13580e4e439db54218f0dBase slot94b3e26ee717c64999d7867364b1b4a3 e388a4556c0f65e1904146cc1a846beedefault slot94b3e26ee717c64999d7867364b1b4a3 686fde5332128d14fa2aa6dd6ebb0c77 16b28748ea4df4d9c2150843fecfba68
父组件的模板最终会生成父组件对应的 VNode,所以以上模板对应的 VNode 全部由父组件所有,那么在创建子组件实例的时候能否通过获取父组件的 VNode 进而拿到 slot 的内容呢?即通过父组件将下面这段模板对应的 VNode 拿到
88d574120e9e0d5bf9eb3bcd60be7003 74997f6adba13580e4e439db54218f0dBase slot94b3e26ee717c64999d7867364b1b4a3 e388a4556c0f65e1904146cc1a846beedefault slot94b3e26ee717c64999d7867364b1b4a3 686fde5332128d14fa2aa6dd6ebb0c77
如果能够通过父级拿到这段模板对应的 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 是一个代理对象
以上是vue高阶组件是什么的详细内容。更多信息请关注PHP中文网其他相关文章!