Пожалуйста, расскажите о принципе слота и области слота в Vue (глубокий анализ 2.6.11)

Vue.js опрос

предисловие

во Вьюslotа такжеslot-scopeЭто всегда была продвинутая концепция, которую нечасто затрагивают в нашей повседневной разработке компонентов, но она очень мощная и гибкая.

в версии 2.6

  1. slotа такжеslot-scopeОн унифицирован и интегрирован внутри компонента в函数
  2. Их области рендеринга子组件
  3. и может пройтиthis.$scopedSlotsпосетить

Это делает опыт разработки этой модели более единства, эта статья основана на2.6.11Последний код для анализа того, как это работает.

Чтобы узнать о синтаксисе слота, обновленном в версии 2.6, если вы мало о нем знаете, вы можете взглянуть на это замечательное официальное объявление.

Выпущена версия 2.6

В качестве простого примера у сообщества есть библиотека для управления асинхронными процессами: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>

Как видите, нам нужно всего лишь поставить асинхронность для обработки запроса.promiseПередайте его к компоненту, он автоматически сделает это для насpromise, и в ответ бросаетpending,rejected, а данные после успешного асинхронного выполненияdata.

Это может значительно упростить наш опыт асинхронной разработки, где нам пришлось бы делать это вручную.promise, ручное управление ошибками обработки состояния и т. д.

И все эти мощные функции получают выгоду от Vue предоставленнойslot-scopeФункция, это очень немного близко к гибкости пакетаHook, компонент может быть даже совершенно безразличнымUIРендеринг только помогает родительскому компоненту управлять некоторыми状态.

Аналогия

если у тебя есть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, normalizeScopedSlots)testэкземпляр компонентаthis.$scopedSlotsвы можете получить доступ к этим двумfoo,barфункция. (Если безымянный,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компонент, через 1с мы передаем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-if,v-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.6slotа такжеslot-scopeСделали унифицированную интеграцию, сделав их все в виде функций, все слоты можно найти вthis.$scopedSlotsПрямой доступ к нему, что делает нам более удобной разработку продвинутых компонентов.

По оптимизации Vue 2.6 тоже можно сделатьslotОбновление не запускает рендеринг родительского компонента и использует ряд умных суждений и алгоритмов, чтобы максимально избежать ненужного рендеринга. (В версии 2.5, начиная с сборкиslotОбласть действия находится в родительском компоненте, поэтому очевидно, что это слот дочернего компонента.slotОбновление будет обновлено с родительским компонентом)

Прослушивая речь Йоды ранее, Vue3 будет больше использовать статические функции шаблонов, чтобы делать больше.预编译优化, в процессе генерации кода в статье мы уже ощутили его усилия в этом направлении, и мы очень рассчитываем на более мощную производительность, которую принесет Vue3.

❤️Спасибо всем

1. Если эта статья была вам полезна, пожалуйста, поддержите ее лайком, ваш "лайк" - движущая сила моего творчества.

2. Подпишитесь на официальный аккаунт «Front-end from advanced to accept», чтобы добавить меня в друзья, я втяну вас в «Front-end группу расширенного обмена», все смогут общаться и добиваться прогресса вместе.