在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
<template> <div> <p @click="Click">props: {{test}}</p> </div> </template> <script> export default { name: 'Base', props: { test: Number }, methods: { Click () { this.$emit('Base-click') } } } </script>
Vue 元件主要是三點:props、event 以及 slots。對於 Base元件 元件而言,它接收一個數字類型的 props 即 test,並觸發一個自訂事件,事件的名稱是:Base-click,沒有 slots。我們會這樣使用該元件:
現在我們需要base-component 元件每次掛載完成的時候都會列印一句話:haha,同時這也許是很多元件的需求,所以按照mixins 的方式,我們可以這樣做,先定義個mixins
export default consoleMixin { mounted () { console.log('haha') } }
然後在Base 元件中將consoleMixin 混入:
<template> <div> <p @click="Click">props: {{test}}</p> </div> </template> <script> export default { name: 'Base', props: { test: Number }, mixins: [ consoleMixin ], methods: { Click () { this.$emit('Base-click') } } } </script>
這樣使用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: '<wrapped v-on="$listeners" v-bind="$attrs"/>', 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
<template> <div> <span @click="handleClick">props: {{test}}</span> <slot name="slot1"/> <!-- 具名插槽 --></slot> <p>===========</p> <slot><slot/> <!-- 默认插槽 --> </div> </template> <script> export default { ... } </script> <template> <div> <Base> <h2 slot="slot1">BaseComponent slot</h2> <p>default slot</p> </Base> <wrapBase> <h2 slot="slot1">EnhancedComponent slot</h2> <p>default slot</p> </wrapBase> </div> </template> <script> import Base from './Base.vue' import hoc from './Console.js' const wrapBase = Console(Base) export default { components: { Base, wrapBase } } </script>
這裡的執行結果就是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),比如如下模板:
<div> <p slot="slot1">Base slot</p> </div>
會被編譯成如下渲染函數:
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 之外,還有元件,如下:
<div> <Base> <p slot="slot1">Base slot</p> <p>default slot</p> </Base> </div>
其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 以及传递了多少,传递的是具名的还是不具名的等等。那么子组件如何才能得知这些信息呢?很简单,假如组件的模板如下
<div> <Base> <p slot="slot1">Base slot</p> <p>default slot</p> </Base> </div>
父组件的模板最终会生成父组件对应的 VNode,所以以上模板对应的 VNode 全部由父组件所有,那么在创建子组件实例的时候能否通过获取父组件的 VNode 进而拿到 slot 的内容呢?即通过父组件将下面这段模板对应的 VNode 拿到
<Base> <p slot="slot1">Base slot</p> <p>default slot</p> </Base>
如果能够通过父级拿到这段模板对应的 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中文網其他相關文章!