[vue] 你了解什么是高阶组件吗?可否举个例子说明下?
[vue] 你了解什么是高階組件嗎?可否舉個(gè)例子說明下?
高階組件
高階組件介紹
vue 高階組件的認(rèn)識(shí),在React中組件是以復(fù)用代碼實(shí)現(xiàn)的,而Vue中是以mixins 實(shí)現(xiàn),并且官方文檔中也缺少一些高階組件的概念,因?yàn)樵趘ue中實(shí)現(xiàn)高階組很困難,并不像React簡單,其實(shí)vue中mixins也同樣和以代替,在讀了一部分源碼之后,對(duì)vue有了更深的認(rèn)識(shí)
所謂高階組件其實(shí)就是一個(gè)高階函數(shù), 即返回一個(gè)組件函數(shù)的函數(shù),Vue中怎么實(shí)現(xiàn)呢? 注意 高階組件有如下特點(diǎn)
高階組件(HOC)應(yīng)該是無副作用的純函數(shù),且不應(yīng)該修改原組件,即原組件不能有變動(dòng) 高階組件(HOC)不關(guān)心你傳遞的數(shù)據(jù)(props)是什么,并且新生成組件不關(guān)心數(shù)據(jù)來源 高階組件(HOC)接收到的 props 應(yīng)該透傳給被包裝組件即直接將原組件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 組件主要就是三點(diǎn):props、event 以及 slots。對(duì)于 Base組件 組件而言,它接收一個(gè)數(shù)字類型的 props 即 test,并觸發(fā)一個(gè)自定義事件,事件的名稱是:Base-click,沒有 slots。我們會(huì)這樣使用該組件:
現(xiàn)在我們需要 base-component 組件每次掛載完成的時(shí)候都打印一句話:haha,同時(shí)這也許是很多組件的需求,所以按照 mixins 的方式,我們可以這樣做,首先定義個(gè) 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 組件的時(shí)候,每次掛載完成之后都會(huì)打印一句 haha,不過現(xiàn)在我們要使用高階組件的方式實(shí)現(xiàn)同樣的功能,回憶高階組件的定義:接收一個(gè)組件作為參數(shù),返回一個(gè)新的組件,那么此時(shí)我們需要思考的是,在 Vue 中組件是什么?Vue 中組件是函數(shù),不過那是最終結(jié)果,比如我們?cè)趩挝募M件中的組件定義其實(shí)就是一個(gè)普通的選項(xiàng)對(duì)象,如下:
export default {name: 'Base',props: {...},mixins: [...]methods: {...} }這難道不是一個(gè)純對(duì)象嘛
import Base from './Base.vue' console.log(Base)這里的Base是什么呢 對(duì)就是一個(gè)JSON對(duì)象,而當(dāng)以把他加入到一個(gè)組件的components,Vu最終會(huì)以該參數(shù)即option來構(gòu)造實(shí)例的構(gòu)造函數(shù),所以Vue中組件就是個(gè)函數(shù),但是在引入之前仍只是一個(gè)options對(duì)象,所以這樣就很好明白了 Vue中組件開始只是一個(gè)對(duì)象,即高階組件就是 一個(gè)函數(shù)接受一個(gè)純對(duì)象,并且返回一個(gè)新純對(duì)象
export default function Console (BaseComponent) {return {template: '<wrapped v-on="$listeners" v-bind="$attrs"/>',components: {wrapped: BaseComponent},mounted () {console.log('haha')}} }這里 Console就是一個(gè)高階組件,它接受一個(gè)參數(shù) BaseComponent即傳入的組件,返回一個(gè)新組件,將BaseComponent作為新組件的子組件并且在mounted里設(shè)置鉤子函數(shù) 打印haha,我們可以完成mixins同樣做到的事,我們并沒有修改子組件Base,這里的 $listeners $attrs 其實(shí)是在透傳props 和事件 那這樣真的就完美解決問題了嗎?不是的,首先 template 選項(xiàng)只有在完整版的 Vue 中可以使用,在運(yùn)行時(shí)版本中是不能使用的,所以最起碼我們應(yīng)該使用渲染函數(shù)(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,})}} }我們將模板改寫成了渲染函數(shù),看上去沒什么問題,實(shí)際還是有問題,上面的代碼中 BaseComponent 組件依然收不到 props,為什么呢,我們不是已經(jīng)在 h 函數(shù)的第二個(gè)參數(shù)中將 attrs 傳遞過去了嗎,怎么還收不到?當(dāng)然收不到,attrs 指的是那些沒有被聲明為 props 的屬性,所以在渲染函數(shù)中還需要添加 props 參數(shù):
export default function Console (BaseComponent) {return {mounted () {console.log('haha')},render (h) {return h(BaseComponent, {on: this.$listeners,attrs: this.$attrs,props: this.$props})}} }那這樣呢 其實(shí)還是不行 props始終是空對(duì)象,這里的props是高階組件的對(duì)象,但是高階組件并沒有聲明props所以如此故要再聲明一個(gè)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 一個(gè)差不多的高階組件就完成了 但是能還每完 我們只實(shí)現(xiàn)了 透傳props,透傳事件,emmmm就剩下slot了 我們修改 Base 組件為其添加一個(gè)具名插槽和默認(rèn)插槽 Base.vue
<template><div><span @click="handleClick">props: {{test}}</span><slot name="slot1"/> <!-- 具名插槽 --></slot><p>===========</p><slot><slot/> <!-- 默認(rèn)插槽 --></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>這里的執(zhí)行結(jié)果就是 wrapBase里的slot都沒有了 所以就要改一下高階組建了
function Console (BaseComponent) {return {mounted () {console.log('haha')},props: BaseComponent.props,render (h) {// 將 this.$slots 格式化為數(shù)組,因?yàn)?h 函數(shù)第三個(gè)參數(shù)是子節(jié)點(diǎn),是一個(gè)數(shù)組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 函數(shù)的第三個(gè)參數(shù)}} }這時(shí) slot內(nèi)容確實(shí)渲染出來了 但是順序不太對(duì) 高階組件的全部渲染到了末尾。。 其實(shí) Vue在處理具名插槽會(huì)考慮作用域的因素 首先 Vue 會(huì)把模板(template)編譯成渲染函數(shù)(render),比如如下模板:
<div><p slot="slot1">Base slot</p> </div>會(huì)被編譯成如下渲染函數(shù):
var render = function() {var _vm = thisvar _h = _vm.$createElementvar _c = _vm._self._c || _hreturn _c("div", [_c("div", {attrs: { slot: "slot1" },slot: "slot1"}, [_vm._v("Base slot")])]) }觀察上面的渲染函數(shù)我們發(fā)現(xiàn)普通的 DOM 是通過 _c 函數(shù)創(chuàng)建對(duì)應(yīng)的 VNode 的。現(xiàn)在我們修改模板,模板中除了有普通 DOM 之外,還有組件,如下:
<div><Base><p slot="slot1">Base slot</p><p>default slot</p></Base> </div>其render函數(shù)
var render = function() {var _vm = thisvar _h = _vm.$createElementvar _c = _vm._self._c || _hreturn _c("div",[_c("Base", [_c("p", { attrs: { slot: "slot1" }, slot: "slot1" }, [_vm._v("Base slot")]),_vm._v(" "),_c("p", [_vm._v("default slot")])])],) }我們發(fā)現(xiàn)無論是普通DOM還是組件,都是通過 _c 函數(shù)創(chuàng)建其對(duì)應(yīng)的 VNode 的 其實(shí) _c 在 Vue 內(nèi)部就是 createElement 函數(shù)。createElement 函數(shù)會(huì)自動(dòng)檢測第一個(gè)參數(shù)是不是普通DOM標(biāo)簽如果不是普通DOM標(biāo)簽?zāi)敲?createElement 會(huì)將其視為組件,并且創(chuàng)建組件實(shí)例,注意組件實(shí)例是這個(gè)時(shí)候才創(chuàng)建的 但是創(chuàng)建組件實(shí)例的過程中就面臨一個(gè)問題:組件需要知道父級(jí)模板中是否傳遞了 slot 以及傳遞了多少,傳遞的是具名的還是不具名的等等。那么子組件如何才能得知這些信息呢?很簡單,假如組件的模板如下
<div><Base><p slot="slot1">Base slot</p><p>default slot</p></Base> </div>父組件的模板最終會(huì)生成父組件對(duì)應(yīng)的 VNode,所以以上模板對(duì)應(yīng)的 VNode 全部由父組件所有,那么在創(chuàng)建子組件實(shí)例的時(shí)候能否通過獲取父組件的 VNode 進(jìn)而拿到 slot 的內(nèi)容呢?即通過父組件將下面這段模板對(duì)應(yīng)的 VNode 拿到
<Base><p slot="slot1">Base slot</p><p>default slot</p></Base>如果能夠通過父級(jí)拿到這段模板對(duì)應(yīng)的 VNode,那么子組件就知道要渲染哪些 slot 了,其實(shí) Vue 內(nèi)部就是這么干的,實(shí)際上你可以通過訪問子組件的 this.$vnode 來獲取這段模板對(duì)應(yīng)的 VNode
this.$vnode 并沒有寫進(jìn) Vue 的官方文檔
子組件拿到了需要渲染的 slot 之后進(jìn)入到了關(guān)鍵的一步,這一步就是導(dǎo)致高階組件中透傳 slot 給 Base組件 卻無法正確渲染的原因 children的VNode中的context引用父組件實(shí)例 其本身的context也會(huì)引用本身實(shí)例 其實(shí)是一個(gè)東西
console.log(this.vnode.context===this.vnode.context === this.vnode.context===this.vnode.componentOptions.children[0].context) //ture
而 Vue 內(nèi)部做了一件很重要的事兒,即上面那個(gè)表達(dá)式必須成立,才能夠正確處理具名 slot,否則即使 slot 具名也不會(huì)被考慮,而是被作為默認(rèn)插槽。這就是高階組件中不能正確渲染 slot 的原因
即 高階組件中 本來時(shí)父組件和子組件之間插入了一個(gè)組件(高階組件),而子組件的 this.$vnode其實(shí)是高階組件的實(shí)例,但是我們將slot透傳給子組件,slot里 VNode 的context實(shí)際引用的還是父組件 所以
console.log(this.vnode.context===this.vnode.context === this.vnode.context===this.vnode.componentOptions.children[0].context) // false
最終導(dǎo)致具名插槽被作為默認(rèn)插槽,從而渲染不正確。
決辦法也很簡單,只需要手動(dòng)設(shè)置一下 slot 中 VNode 的 context 值為高階組件實(shí)例即可
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]), [])// 手動(dòng)更正 context.map(vnode => {vnode.context = this._self //綁定到高階組件上return vnode})return h(WrappedComponent, {on: this.$listeners,props: this.$props,attrs: this.$attrs}, slots)}} }說明白就是強(qiáng)制把slot的歸屬權(quán)給高階組件 而不是 父組件 通過當(dāng)前實(shí)例 _self 屬性訪問當(dāng)實(shí)例本身,而不是直接使用 this,因?yàn)?this 是一個(gè)代理對(duì)象
個(gè)人簡介
我是歌謠,歡迎和大家一起交流前后端知識(shí)。放棄很容易,
但堅(jiān)持一定很酷。歡迎大家一起討論
主目錄
與歌謠一起通關(guān)前端面試題
總結(jié)
以上是生活随笔為你收集整理的[vue] 你了解什么是高阶组件吗?可否举个例子说明下?的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 华为交换机基础操作
- 下一篇: 电机驱动芯片——DRV8833、TB66