面試偶爾問(wèn)的 Vue2 中 slot 和 slot-scope 的原理
前言
Vue 中的 slot 和 slot-scope 一直是一個(gè)進(jìn)階的概念,對(duì)于我們的日常的組件開(kāi)發(fā)中不常接觸,但是卻非常強(qiáng)大和靈活。
在 Vue 2.6 中
slot和slot-scope在組件內(nèi)部被統(tǒng)一整合成了函數(shù)他們的渲染作用域都是 子組件并且都能通過(guò) this.$slotScopes去訪問(wèn)
這使得這種模式的開(kāi)發(fā)體驗(yàn)變的更為統(tǒng)一,本篇文章就基于 2.6.11 的最新代碼來(lái)解析它的原理。
對(duì)于 2.6 版本更新的插槽語(yǔ)法,如果你還不太了解,可以看看這篇尤大的官宣
Vue 2.6 發(fā)布了[1]
舉個(gè)簡(jiǎn)單的例子,社區(qū)有個(gè)異步流程管理的庫(kù):vue-promised,它的用法是這樣的:
<Promised?:promise="usersPromise">
??<template?v-slot:pending>
????<p>Loading...p>
??template>
??<template?v-slot="data">
????<ul>
??????<li?v-for="user?in?data">{{?user.name?}}li>
????ul>
??template>
??<template?v-slot:rejected="error">
????<p>Error:?{{?error.message?}}p>
??template>
Promised>
可以看到,我們只要把一個(gè)用來(lái)處理請(qǐng)求的異步 promise 傳遞給組件,它就會(huì)自動(dòng)幫我們?nèi)ネ瓿蛇@個(gè) promise,并且響應(yīng)式的對(duì)外拋出 pending,rejected,和異步執(zhí)行成功后的數(shù)據(jù) data。
這可以大大簡(jiǎn)化我們的異步開(kāi)發(fā)體驗(yàn),原本我們要手動(dòng)執(zhí)行這個(gè) promise,手動(dòng)管理狀態(tài)處理錯(cuò)誤等等……
而這一切強(qiáng)大的功能都得益于Vue 提供的 slot-scope 功能,它在封裝的靈活性上甚至有點(diǎn)接近于 Hook,組件甚至可以完全不關(guān)心 UI 渲染,只幫助父組件管理一些 狀態(tài)。
類比 React
如果你有 React 的開(kāi)發(fā)經(jīng)驗(yàn),其實(shí)這就類比 React 中的 renderProps 去理解就好了。(如果你沒(méi)有 React 開(kāi)發(fā)經(jīng)驗(yàn),請(qǐng)?zhí)^(guò))
import?React?from?'react'
import?ReactDOM?from?'react-dom'
import?PropTypes?from?'prop-types'
//?這是一個(gè)對(duì)外提供鼠標(biāo)位置的?render?props?組件
class?Mouse?extends?React.Component?{
??state?=?{?x:?0,?y:?0?}
??handleMouseMove?=?(event)?=>?{
????this.setState({
??????x:?event.clientX,
??????y:?event.clientY
????})
??}
??render()?{
????return?(
??????<div?style={{?height:?'100%'?}}?onMouseMove={this.handleMouseMove}>
????????//?這里把?children?當(dāng)做函數(shù)執(zhí)行,來(lái)對(duì)外提供子組件內(nèi)部的?state
????????{this.props.children(this.state)}
??????div>
????)
??}
}
class?App?extends?React.Component?{
??render()?{
????return?(
??????<div?style={{?height:?'100%'?}}>
????????//?這里就很像?Vue?的?作用域插槽
????????<Mouse>
?????????({?x,?y?})?=>?(
???????????//?render?prop?給了我們所需要的?state?來(lái)渲染我們想要的
???????????<h1>The?mouse?position?is?({x},?{y})h1>
?????????)
????????Mouse>
??????div>
????)
??}
})
ReactDOM.render(<App/>,?document.getElementById('app'))
原理解析
初始化
對(duì)于這樣的一個(gè)例子來(lái)說(shuō)
<test>
??<template?v-slot:bar>
????<span>Hellospan>
??template>
??<template?v-slot:foo="prop">
????<span>{{prop.msg}}span>
??template>
test>
這段模板會(huì)被編譯成這樣:
with?(this)?{
??return?_c("test",?{
????scopedSlots:?_u([
??????{
????????key:?"bar",
????????fn:?function?()?{
??????????return?[_c("span",?[_v("Hello")])];
????????},
??????},
??????{
????????key:?"foo",
????????fn:?function?(prop)?{
??????????return?[_c("span",?[_v(_s(prop.msg))])];
????????},
??????},
????]),
??});
}
然后經(jīng)過(guò)初始化時(shí)的一系列處理(resolveScopedSlots, normalizeScopedSlots) test ?組件的實(shí)例 this.$slotScopes 就可以訪問(wèn)到這兩個(gè) foo 、 bar 函數(shù)。(如果未命名的話,key 會(huì)是 default 。)
進(jìn)入 test 組件內(nèi)部,假設(shè)它是這樣定義的:
<div>
??<slot?name="bar">slot>
??<slot?name="foo"?v-bind="{?msg?}">slot>
div>
<script>
??new?Vue({
????name:?"test",
????data()?{
??????return?{
????????msg:?"World",
??????};
????},
????mounted()?{
??????//?一秒后更新
??????setTimeout(()?=>?{
????????this.msg?=?"Changed";
??????},?1000);
????},
??});
script>
那么 template 就會(huì)被編譯為這樣的函數(shù):
with?(this)?{
??return?_c("div",?[_t("bar"),?_t("foo",?null,?null,?{?msg?})],?2);
}
已經(jīng)有那么些端倪了,接下來(lái)就研究一下 _t 函數(shù)的實(shí)現(xiàn),就可以接近真相了。
_t 也就是 renderSlot的別名,簡(jiǎn)化后的實(shí)現(xiàn)是這樣的:
export?function?renderSlot?(
??name:?string,
??fallback:??Array,
??props:??Object,
??bindObject:??Object
):??Array<VNode>?{
??//?通過(guò)?name?拿到函數(shù)
??const?scopedSlotFn?=?this.$scopedSlots[name]
??let?nodes
??if?(scopedSlotFn)?{?//?scoped?slot
????props?=?props?||?{}
????//?執(zhí)行函數(shù)返回?vnode
????nodes?=?scopedSlotFn(props)?||?fallback
??}
??return?nodes
}
其實(shí)很簡(jiǎn)單,
如果是 普通插槽,就直接調(diào)用函數(shù)生成 vnode,如果是 作用域插槽,
就直接帶著 props 也就是 { msg } 去調(diào)用函數(shù)生成 vnode。2.6 版本后統(tǒng)一為函數(shù)的插槽降低了很多心智負(fù)擔(dān)。
更新
在上面的 test 組件中, 1s 后我們通過(guò) this.msg = "Changed"; 觸發(fā)響應(yīng)式更新,此時(shí)編譯后的 render 函數(shù):
with?(this)?{
??return?_c("div",?[_t("bar"),?_t("foo",?null,?null,?{?msg?})],?2);
}
重新執(zhí)行,此時(shí)的 msg 已經(jīng)是更新后的 Changed 了,自然也就實(shí)現(xiàn)了更新。
一種特殊情況是,在父組件的作用于里也使用了響應(yīng)式的屬性并更新,比如這樣:
<test>
??<template?v-slot:bar>
????<span>Hellospan>
??template>
??<template?v-slot:foo="prop">
????<span>{{prop.msg}}span>
??template>
test>
<script>
??new?Vue({
????name:?"App",
????el:?"#app",
????mounted()?{
??????setTimeout(()?=>?{
????????this.msgInParent?=?"Changed";
??????},?1000);
????},
????data()?{
??????return?{
????????msgInParent:?"msgInParent",
??????};
????},
????components:?{
??????test:?{
????????name:?"test",
????????data()?{
??????????return?{
????????????msg:?"World",
??????????};
????????},
????????template:?`
??????????
????????????
????????????
??????????
????????`,
??????},
????},
??});
script>
其實(shí),是因?yàn)閳?zhí)行 _t 函數(shù)時(shí),全局的組件渲染上下文是 子組件,那么依賴收集自然也就是收集到 子組件的依賴了。所以在 msgInParent 更新后,其實(shí)是直接去觸發(fā)子組件的重新渲染的,對(duì)比 2.5 的版本,這是一個(gè)優(yōu)化。
那么還有一些額外的情況,比如說(shuō) template 上有 v-if、 v-for 這種情況,舉個(gè)例子來(lái)說(shuō):
<test>
??<template?v-slot:bar?v-if="show">
????<span>Hellospan>
??template>
test>
function?render()?{
??with(this)?{
????return?_c('test',?{
??????scopedSlots:?_u([(show)???{
????????key:?"bar",
????????fn:?function?()?{
??????????return?[_c('span',?[_v("Hello")])]
????????},
????????proxy:?true
??????}?:?null],?null,?true)
????})
??}
}
注意這里的 _u 內(nèi)部直接是一個(gè)三元表達(dá)式,讀取 _u 是發(fā)生在父組件的 _render 中,那么此時(shí)子組件是收集不到這個(gè) show 的依賴的,所以說(shuō) show 的更新只會(huì)觸發(fā)父組件的更新,那這種情況下子組件是怎么重新執(zhí)行 $scopedSlot 函數(shù)并重渲染的呢?
我們已經(jīng)有了一定的前置知識(shí):Vue的更新粒度[2],知道 Vue 的組件不是遞歸更新的,但是 slotScopes 的函數(shù)執(zhí)行是發(fā)生在子組件內(nèi)的,父組件在更新的時(shí)候一定是有某種方式去通知子組件也進(jìn)行更新。
其實(shí)這個(gè)過(guò)程就發(fā)生在父組件的重渲染的 patchVnode中,到了 test 組件的 patch 過(guò)程,進(jìn)入了 updateChildComponent 這個(gè)函數(shù)后,會(huì)去檢查它的 slot 是否是穩(wěn)定的,顯然 v-if 控制的 slot 是非常不穩(wěn)定的。
??const?newScopedSlots?=?parentVnode.data.scopedSlots
??const?oldScopedSlots?=?vm.$scopedSlots
??const?hasDynamicScopedSlot?=?!!(
????(newScopedSlots?&&?!newScopedSlots.$stable)?||
????(oldScopedSlots?!==?emptyObject?&&?!oldScopedSlots.$stable)?||
????(newScopedSlots?&&?vm.$scopedSlots.$key?!==?newScopedSlots.$key)
??)
??//?Any?static?slot?children?from?the?parent?may?have?changed?during?parent's
??//?update.?Dynamic?scoped?slots?may?also?have?changed.?In?such?cases,?a?forced
??//?update?is?necessary?to?ensure?correctness.
??const?needsForceUpdate?=?!!hasDynamicScopedSlot
??
??if?(needsForceUpdate)?{
????//?這里的 vm 對(duì)應(yīng) test 也就是子組件的實(shí)例,相當(dāng)于觸發(fā)了子組件強(qiáng)制渲染。
????vm.$forceUpdate()
??}
這里有一些優(yōu)化措施,并不是說(shuō)只要有 slotScope 就會(huì)去觸發(fā)子組件強(qiáng)制更新。
有如下三種情況會(huì)強(qiáng)制觸發(fā)子組件更新:
scopedSlots上的$stable屬性為false
一路追尋這個(gè)邏輯,最終發(fā)現(xiàn)這個(gè) $stable 是 _u 也就是 resolveScopedSlots 函數(shù)的第三個(gè)參數(shù)決定的,由于這個(gè) _u 是由編譯器生成 render 函數(shù)時(shí)生成的的,那么就到 codegen 的邏輯中去看:
??let?needsForceUpdate?=?el.for?||?Object.keys(slots).some(key?=>?{
????const?slot?=?slots[key]
????return?(
??????slot.slotTargetDynamic?||
??????slot.if?||
??????slot.for?||
??????containsSlotChild(slot)?//?is?passing?down?slot?from?parent?which?may?be?dynamic
????)
??})
簡(jiǎn)單來(lái)說(shuō),就是用到了一些動(dòng)態(tài)語(yǔ)法的情況下,就會(huì)通知子組件對(duì)這段 scopedSlots 進(jìn)行強(qiáng)制更新。
也是 $stable屬性相關(guān),舊的scopedSlots不穩(wěn)定
這個(gè)很好理解,舊的scopedSlots需要強(qiáng)制更新,那么渲染后一定要強(qiáng)制更新。
舊的 $key不等于新的$key
這個(gè)邏輯比較有意思,一路追回去看 $key 的生成,可以看到是 _u 的第四個(gè)參數(shù) contentHashKey,這個(gè)contentHashKey 是在 codegen 的時(shí)候利用 hash 算法對(duì)生成代碼的字符串進(jìn)行計(jì)算得到的,也就是說(shuō),這串函數(shù)的生成的 字符串 改變了,就需要強(qiáng)制更新子組件。
function?hash(str)?{
??let?hash?=?5381
??let?i?=?str.length
??while(i)?{
????hash?=?(hash?*?33)?^?str.charCodeAt(--i)
??}
??return?hash?>>>?0
}
總結(jié)
Vue 2.6 版本后對(duì) slot 和 slot-scope ?做了一次統(tǒng)一的整合,讓它們?nèi)慷甲優(yōu)楹瘮?shù)的形式,所有的插槽都可以在 this.$slotScopes 上直接訪問(wèn),這讓我們?cè)陂_(kāi)發(fā)高級(jí)組件的時(shí)候變得更加方便。
在優(yōu)化上,Vue 2.6 也盡可能的讓 slot 的更新不觸發(fā)父組件的渲染,通過(guò)一系列巧妙的判斷和算法去盡可能避免不必要的渲染。(在 2.5 的版本中,由于生成 slot 的作用域是在父組件中,所以明明是子組件的插槽 slot 的更新是會(huì)帶著父組件一起更新的)
之前聽(tīng)尤大的演講,Vue3 會(huì)更多的利用模板的靜態(tài)特性做更多的預(yù)編譯優(yōu)化,在文中生成代碼的過(guò)程中我們已經(jīng)感受到了他為此付出努力,非常期待 Vue3 帶來(lái)的更加強(qiáng)悍的性能。
參考資料
Vue 2.6 發(fā)布了: https://zhuanlan.zhihu.com/p/56260917
[2]Vue的更新粒度: https://juejin.im/post/5e854a32518825736c5b807f#heading-3
