<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          面試偶爾問(wèn)的 Vue2 中 slot 和 slot-scope 的原理

          共 6379字,需瀏覽 13分鐘

           ·

          2020-12-23 13:37

          前言

          Vue 中的 slotslot-scope 一直是一個(gè)進(jìn)階的概念,對(duì)于我們的日常的組件開(kāi)發(fā)中不常接觸,但是卻非常強(qiáng)大和靈活。

          在 Vue 2.6 中

          1. slotslot-scope 在組件內(nèi)部被統(tǒng)一整合成了 函數(shù)
          2. 他們的渲染作用域都是 子組件
          3. 并且都能通過(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ì)外拋出 pendingrejected,和異步執(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, normalizeScopedSlotstest ?組件的實(shí)例 this.$slotScopes 就可以訪問(wèn)到這兩個(gè) foobar 函數(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-ifv-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ā)子組件更新:

          1. 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)制更新。

          1. 也是 $stable 屬性相關(guān),舊的 scopedSlots 不穩(wěn)定

          這個(gè)很好理解,舊的scopedSlots需要強(qiáng)制更新,那么渲染后一定要強(qiáng)制更新。

          1. 舊的 $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ì) slotslot-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)悍的性能。

          參考資料

          [1]

          Vue 2.6 發(fā)布了: https://zhuanlan.zhihu.com/p/56260917

          [2]

          Vue的更新粒度: https://juejin.im/post/5e854a32518825736c5b807f#heading-3


          瀏覽 46
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  欧美日韩性交 | 小蔡头喵喵喵-新年小奶牛 | 免费看毛片的网站 | 欧美经典怡红院肏肥屄淫荡视频在线观看 | 亚洲成人免费无码视频 |