請你說說 Vue 中 slot 和 slot-scope 的原理(2.6.11 深度解析)

前言

Vue 中的 slotslot-scope 一直是一個進階的概念,對於咱們的平常的組件開發中不常接觸,可是卻很是強大和靈活。前端

在 Vue 2.6 中vue

  1. slotslot-scope 在組件內部被統一整合成了 函數
  2. 他們的渲染做用域都是 子組件
  3. 而且都能經過 this.$scopedSlots去訪問

這使得這種模式的開發體驗變的更爲統一,本篇文章就基於 2.6.11 的最新代碼來解析它的原理。node

對於 2.6 版本更新的插槽語法,若是你還不太瞭解,能夠看看這篇尤大的官宣react

Vue 2.6 發佈了算法

舉個簡單的例子,社區有個異步流程管理的庫: vue-promised,它的用法是這樣的:promise

<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>

複製代碼

能夠看到,咱們只要把一個用來處理請求的異步 promise 傳遞給組件,它就會自動幫咱們去完成這個 promise,而且響應式的對外拋出 pendingrejected,和異步執行成功後的數據 datamarkdown

這能夠大大簡化咱們的異步開發體驗,本來咱們要手動執行這個 promise,手動管理狀態處理錯誤等等……app

而這一切強大的功能都得益於Vue 提供的 slot-scope 功能,它在封裝的靈活性上甚至有點接近於 Hook,組件甚至能夠徹底不關心 UI 渲染,只幫助父組件管理一些 狀態dom

類比 React

若是你有 React 的開發經驗,其實這就類比 React 中的 renderProps 去理解就行了。(若是你沒有 React 開發經驗,請跳過)異步

import React from 'react'
import ReactDOM from 'react-dom'
import PropTypes from 'prop-types'

// 這是一個對外提供鼠標位置的 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 當作函數執行,來對外提供子組件內部的 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 來渲染咱們想要的 <h1>The mouse position is ({x}, {y})</h1> ) </Mouse> </div>
    )
  }
})

ReactDOM.render(<App/>, document.getElementById('app'))
複製代碼

原理解析

初始化

對於這樣的一個例子來講

<test>
  <template v-slot:bar>
    <span>Hello</span>
  </template>
  <template v-slot:foo="prop">
    <span>{{prop.msg}}</span>
  </template>
</test>
複製代碼

這段模板會被編譯成這樣:

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))])];
        },
      },
    ]),
  });
}
複製代碼

而後通過初始化時的一系列處理(resolveScopedSlots, normalizeScopedSlotstest 組件的實例 this.$scopedSlots 就能夠訪問到這兩個 foobar 函數。(若是未命名的話,key 會是 default 。)

進入 test 組件內部,假設它是這樣定義的:

<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 就會被編譯爲這樣的函數:

with (this) {
  return _c("div", [_t("bar"), _t("foo", null, null, { msg })], 2);
}
複製代碼

已經有那麼些端倪了,接下來就研究一下 _t 函數的實現,就能夠接近真相了。

_t 也就是 renderSlot的別名,簡化後的實現是這樣的:

export function renderSlot ( name: string, fallback: ?Array<VNode>, props: ?Object, bindObject: ?Object ): ?Array<VNode> {
  // 經過 name 拿到函數
  const scopedSlotFn = this.$scopedSlots[name]
  let nodes
  if (scopedSlotFn) { // scoped slot
    props = props || {}
    // 執行函數返回 vnode
    nodes = scopedSlotFn(props) || fallback
  }
  return nodes
}

複製代碼

其實很簡單,

若是是 普通插槽,就直接調用函數生成 vnode,若是是 做用域插槽

就直接帶着 props 也就是 { msg } 去調用函數生成 vnode。 2.6 版本後統一爲函數的插槽下降了不少心智負擔。

更新

在上面的 test 組件中, 1s 後咱們經過 this.msg = "Changed"; 觸發響應式更新,此時編譯後的 render 函數:

with (this) {
  return _c("div", [_t("bar"), _t("foo", null, null, { msg })], 2);
}
複製代碼

從新執行,此時的 msg 已是更新後的 Changed 了,天然也就實現了更新。

一種特殊狀況是,在父組件的做用域裏也使用了響應式的屬性並更新,好比這樣:

<test>
  <template v-slot:bar>
    <span>Hello {{msgInParent}}</span>
  </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: ` <div> <slot name="bar"></slot> <slot name="foo" v-bind="{ msg }"></slot> </div> `, }, }, }); </script>
複製代碼

其實,是由於執行 _t 函數時,全局的組件渲染上下文是 子組件,那麼依賴收集天然也就是收集到 子組件的依賴了。因此在 msgInParent 更新後,實際上是直接去觸發子組件的從新渲染的,對比 2.5 的版本,這是一個優化。

那麼還有一些額外的狀況,好比說 template 上有 v-ifv-for 這種狀況,舉個例子來講:

<test>
  <template v-slot:bar v-if="show">
    <span>Hello</span>
  </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 內部直接是一個三元表達式,讀取 _u 是發生在父組件的 _render 中,那麼此時子組件是收集不到這個 show 的依賴的,因此說 show 的更新只會觸發父組件的更新,那這種狀況下子組件是怎麼從新執行 $scopedSlot 函數並重渲染的呢?

咱們已經有了必定的前置知識:Vue的更新粒度,知道 Vue 的組件不是遞歸更新的,可是 slotScopes 的函數執行是發生在子組件內的,父組件在更新的時候必定是有某種方式去通知子組件也進行更新。

其實這個過程就發生在父組件的重渲染的 patchVnode中,到了 test 組件的 patch 過程,進入了 updateChildComponent 這個函數後,會去檢查它的 slot 是不是穩定的,顯然 v-if 控制的 slot 是很是不穩定的。

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 對應 test 也就是子組件的實例,至關於觸發了子組件強制渲染。
    vm.$forceUpdate()
  }
複製代碼

這裏有一些優化措施,並非說只要有 slotScope 就會去觸發子組件強制更新。

有以下三種狀況會強制觸發子組件更新:

  1. scopedSlots 上的 $stable 屬性爲 false

一路追尋這個邏輯,最終發現這個 $stable_u 也就是 resolveScopedSlots 函數的第三個參數決定的,因爲這個 _u 是由編譯器生成 render 函數時生成的的,那麼就到 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
    )
  })
複製代碼

簡單來講,就是用到了一些動態語法的狀況下,就會通知子組件對這段 scopedSlots 進行強制更新。

  1. 也是 $stable 屬性相關,舊的 scopedSlots 不穩定

這個很好理解,舊的scopedSlots須要強制更新,那麼渲染後必定要強制更新。

  1. 舊的 $key 不等於新的 $key

這個邏輯比較有意思,一路追回去看 $key 的生成,能夠看到是 _u 的第四個參數 contentHashKey,這個contentHashKey 是在 codegen 的時候利用 hash 算法對生成代碼的字符串進行計算獲得的,也就是說,這串函數的生成的 字符串 改變了,就須要強制更新子組件。

function hash(str) {
  let hash = 5381
  let i = str.length
  while(i) {
    hash = (hash * 33) ^ str.charCodeAt(--i)
  }
  return hash >>> 0
}
複製代碼

總結

Vue 2.6 版本後對 slotslot-scope 作了一次統一的整合,讓它們所有都變爲函數的形式,全部的插槽均可以在 this.$scopedSlots 上直接訪問,這讓咱們在開發高級組件的時候變得更加方便。

在優化上,Vue 2.6 也儘量的讓 slot 的更新不觸發父組件的渲染,經過一系列巧妙的判斷和算法去儘量避免沒必要要的渲染。(在 2.5 的版本中,因爲生成 slot 的做用域是在父組件中,因此明明是子組件的插槽 slot 的更新是會帶着父組件一塊兒更新的)

以前聽尤大的演講,Vue3 會更多的利用模板的靜態特性作更多的預編譯優化,在文中生成代碼的過程當中咱們已經感覺到了他爲此付出努力,很是期待 Vue3 帶來的更增強悍的性能。

❤️感謝你們

1.若是本文對你有幫助,就點個贊支持下吧,你的「贊」是我創做的動力。

2.關注公衆號「前端從進階到入院」便可加我好友,我拉你進「前端進階交流羣」,你們一塊兒共同交流和進步。

相關文章
相關標籤/搜索