Исследуйте компоненты высшего порядка Vue

внешний интерфейс Vue.js

Исследуйте компоненты высшего порядка Vue

Компоненты высшего порядка (HOC)ДаReactОбщий словарь экосистем,ReactОсновным способом повторного использования кода в Китае является использование компонентов более высокого порядка, и это также является официально рекомендуемой практикой. а такжеVueОсновным способом повторного использования кода является использованиеmixins, И вVueКонцепция компонентов более высокого порядка редко упоминается вVueРеализация компонентов более высокого порядка в среде не похожа наReactтак же просто, какReactа такжеVueДизайнерское мышление отличается, но это не значит, чтоVueВы не можете использовать компоненты более высокого порядка вVueПреимущества использования высокоуровневых компонентов в середине относительноmixinsКачественного изменения нет. Эта статья в основном с технической точки зренияVueРеализация компонентов более высокого порядка, и начнется сReactа такжеVueПроанализировано с обеих сторон.

Начните с Реакта

вначалеReactтакже использоватьmixinsДля полного повторного использования кода, например, чтобы избежать ненужного повторного рендеринга компонентов, мы можем смешивать компонентыPureRenderMixin:

const PureRenderMixin = require('react-addons-pure-render-mixin')
const MyComponent = React.createClass({
  mixins: [PureRenderMixin]
})

позжеReactотказался от этого подхода и использовалshallowCompare:

const shallowCompare = require('react-addons-shallow-compare')
const Button = React.createClass({
  shouldComponentUpdate: function(nextProps, nextState) {
    return shallowCompare(this, nextProps, nextState);
  }
})

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

после этогоReactВо избежание того, чтобы разработчики всегда писали один и тот же фрагмент кода в компоненте, рекомендуется использоватьReact.PureComponent, вкратцеReactшаг за шагомmixins,они думаютmixinsсуществуетReactНехорошая модель в экосистеме (примечание: не сказаноmixinsнет, только дляReactэкосистема), мнение выглядит следующим образом:

1,mixinsприносит неявные зависимости
2,mixinsа такжеmixinsмежду,mixinsЛегко вызвать конфликт имен с компонентами.
3. Потому чтоmixinsявляется навязчивым, он изменяет исходный компонент, поэтому изменитеmixinsЭквивалентно модификации оригинальных компонентов по мере роста спросаmixinsстанет сложным, ведущим к снежению сложности.

Для получения подробной информации вы можете проверить эту статьюMixins Considered Harmful. ноHOCЭто не серебряная пуля. Это, естественно, приносит свои проблемы. Заинтересованные студенты могут посмотреть это видео:Michael Jackson - Never Write Another HoC, чья точка зрения: использовать общие компоненты для сопряженияrender propМожет делать все, что может делать HOC.

В этой статье не будет обсуждаться слишком многоmixinsа такжеHOCКто хорош, а кто плох, как и сама техника не бывает хорошей или плохой, только годной или нет. ЭтоReactа такжеVueРазве не так обстоит дело с этими двумя приятелями 🙂

ok, вернемся к компонентам более высокого порядка.Так называемые компоненты более высокого порядка на самом деле являются функциями более высокого порядка.Reactа такжеVueВсе это доказывает одно:Функция — это компонент. Таким образом, утверждение о том, что компонент является функцией, установлено, тогда компонент более высокого порядка, естественно, является функцией более высокого порядка, то есть функцией, которая возвращает функцию. Мы знаем, что вReactНаписание компонентов более высокого порядка в среде — это написание функций более высокого порядка, что очень просто.VueТак же легко реализовать компоненты высокого порядка? фактическиVueНемного сложно, даже требует от васVueДостаточно понять, тогда давайтеVueКомпоненты высокого уровня реализованы посередине, и мы проанализируем, почему они одинаковы, далее в статье.函数就是组件подумал о,Vueно не нравитсяReactТак просто реализовать компоненты более высокого порядка.

Именно поэтому нам необходимо осознатьVueПолностью понять, прежде чем компоненты более высокого порядкаReactКомпоненты высокого порядка в середине, см. СледующееReactКод:

function WithConsole (WrappedComponent) {
  return class extends React.Component {
    componentDidMount () {
      console.log('with console: componentDidMount')
    }
    render () {
      return <WrappedComponent {...this.props}/>
    }
  }
}

WithConsoleЭто компонент более высокого порядка, который имеет следующие характеристики:

1. Компоненты высшего порядка (HOC) должна быть чистой функцией без побочных эффектов и не должна изменять исходный компонент

можно увидетьWithConsoleэто чистая функция, которая принимает компонент в качестве параметра и возвращает новый компонент в новом компонентеrenderФункция отображает только обернутый компонент (WrappedComponent), без интрузивной модификации.

2. Компоненты высшего порядка (HOC) не заботится о данных, которые вы передаете (props) есть и обернут компонентом (WrappedComponent) не заботится об источнике данных

Это основа для обеспечения идеальной совместной работы компонентов более высокого порядка и упакованных компонентов.

3. Компоненты высшего порядка (HOC) полученоpropsдолжен быть передан упакованному компоненту (WrappedComponent)

Компоненты более высокого порядка могут быть добавлены, удалены и измененыprops, но помимоpropsПрозрачная передача, в противном случае в более глубоких вложенных отношениях (这是高阶组件的常见问题) вызоветpropsблокировать.

ВышеReactОсновные соглашения о компонентах среднего и высокого порядка, в дополнение к другим вопросам, таким как: компоненты высокого порядка (HOC) не должно быть вrenderфункция; компоненты более высокого порядка (HOC) также необходимо дублировать статические методы в компонентах; компоненты более высокого порядка (HOC)серединаrefотносится к самому внешнему компоненту контейнера, а не к обернутому компоненту (WrappedComponent) так далее.

Компоненты высшего порядка в Vue

Зная это, мы можем приступить к реализацииVueКомпоненты высокого уровня, чтобы у всех было интуитивное ощущение, я все равно буду использоватьReactа такжеVueСравнительное объяснение. Первый – это базовыйVueкомпоненты, мы часто называем их упакованными компонентами (WrappedComponent), предполагая, что наш компонент называетсяBaseComponent:

base-component.vue

<template>
  <div>
    <span @click="handleClick">props: {{test}}</span>
  </div>
</template>

<script>
export default {
  name: 'BaseComponent',
  props: {
    test: Number
  },
  methods: {
    handleClick () {
      this.$emit('customize-click')
    }
  }
}
</script>

мы наблюдаемVueКомпоненты в основном соблюдают три пункта:props,eventтак же какslots. дляBaseComponentкомпонент, он получает числовой типpropsкоторыйtest, и создайте пользовательское событие с именем:customize-click,нетslots. Мы будем использовать компонент следующим образом:

<base-component @customize-click="handleCustClick" :test="100" />

теперь нам нужноbase-componentКаждый раз, когда компонент монтируется, он печатает предложение:I have already mounted, и это может быть требованием многих компонентов, поэтому следуйтеmixinsобразом, мы можем сделать это, сначала определивmixins:

export default consoleMixin {
  mounted () {
    console.log('I have already mounted')
  }
}

затем вBaseComponentкомпонент генерал-лейтенантconsoleMixinмиксин:

export default {
  name: 'BaseComponent',
  props: {
    test: Number
  },
  mixins: [ consoleMixin ]
  methods: {
    handleClick () {
      this.$emit('customize-click')
    }
  }
}

использовать вот такBaseComponentКогда компонент установлен, предложение будет напечатано после каждого завершения монтирования.I have already mounted, но теперь мы хотим использовать компоненты более высокого порядка для достижения той же функции, вспомните определение компонентов более высокого порядка:Принимает компонент в качестве параметра и возвращает новый компонент, то надо подумать оVueЧто такое средний компонент? У некоторых студентов могут возникнуть вопросы, разве это не функция? правильно,VueНет проблем с тем, что средний компонент является функцией, но это конечный результат.Например, наше определение компонента в однофайловом компоненте на самом деле является обычным объектом опции, как показано ниже:

export default {
  name: 'BaseComponent',
  props: {...},
  mixins: [...]
  methods: {...}
}

Разве это не чистый объект? Итак, когда мы импортируем компонент из одного файла:

import BaseComponent from './base-component.vue'
console.log(BaseComponent)

Подумайте об этом, здесьBaseComponentчто это такое? Это функция? Нет, хотя однофайловые компоненты будутvue-loaderЛечение, но результаты лечения, то есть мы тутBaseComponentВсе еще обычный объект JSON, но когда вы регистрируете этот объект как компонент (componentsвариант),VueНаконец, будет создан конструктор с объектом в качестве параметра, который является конструктором экземпляра производственного компонента, поэтому вVueСредняя компонента действительно является функцией, но это конечный результат.До этого мы можем сказать, что вVueКомпонент также может быть обычным объектом, как и объект, экспортируемый в однофайловый компонент.

Исходя из этого, мы знаем, что вVueКомпонент может существовать как чистый объект, такVueКомпоненты более высокого порядка могут быть определены следующим образом:Принимает чистый объект и возвращает новый чистый объект, следующий код:

hoc.js

export default function WithConsole (WrappedComponent) {
  return {
    template: '<wrapped v-on="$listeners" v-bind="$attrs"/>',
    components: {
      wrapped: WrappedComponent
    },
    mounted () {
      console.log('I have already mounted')
    }
  }
}

WithConsoleявляется компонентом более высокого порядка, который принимает компонент в качестве параметра:WrappedComponentи возвращает новый компонент. В новом определении компонента мы будемWrappedComponentзарегистрирован какwrappedкомпоненты, а вtemplateВизуализируй и добавьmountedкрючок, печатьI have already mounted.

Вышеупомянутое делается сmixinsТа же функция, но на этот раз мы используем компоненты более высокого порядка, поэтому она ненавязчива, мы не модифицировали исходные компоненты (WrappedComponent), исходный компонент визуализируется в новом компоненте без каких-либо изменений в исходном компоненте. И здесь всем следует обратить внимание$listenersа также$attrs:

'<wrapped v-on="$listeners" v-bind="$attrs"/>'

Это необходимо сделать, что эквивалентноReactСредняя передачаprops:

<WrappedComponent {...this.props}/>

В противном случае при использовании компонентов более высокого порядка обернутый компонент (WrappedComponent)Не получитьpropsа также事件.

Это действительно решает проблему идеально? Нет, прежде всегоtemplateвариант только в полной версииVueможет использоваться в версии выполнения, но не в версии выполнения, поэтому, по крайней мере, мы должны использовать функцию рендеринга (render) вместо шаблона (template),следующим образом:

hoc.js

export default function WithConsole (WrappedComponent) {
  return {
    mounted () {
      console.log('I have already mounted')
    },
    render (h) {
      return h(WrappedComponent, {
        on: this.$listeners,
        attrs: this.$attrs,
      })
    }
  }
}

В приведенном выше коде мы переписываем шаблон в функцию рендеринга, что вроде бы не проблема, но это не так.WrappedComponentКомпоненты еще не полученыprops, могут спросить некоторые студенты, разве мы уже не вhВторым параметром функции будетattrsОн был доставлен, почему я не могу его получить? Конечно, нет,attrsотносится к тем, которые не объявленыprops, поэтому в функцию рендеринга нужно еще добавитьpropsпараметр:

hoc.js

export default function WithConsole (WrappedComponent) {
  return {
    mounted () {
      console.log('I have already mounted')
    },
    render (h) {
      return h(WrappedComponent, {
        on: this.$listeners,
        attrs: this.$attrs,
        props: this.$props
      })
    }
  }
}

Тогда это возможно? еще нет, потому чтоthis.$propsвсегда пустой объект, потому что здесьthis.$propsОтносится к полученному компоненту более высокого порядкаprops, в то время как HOC не объявляет никакихprops,такthis.$propsЕстественно это пустой объект, так что мне делать? Очень простойpropsустановить с обернутым компонентомpropsДостаточно того же:

hoc.js

export default function WithConsole (WrappedComponent) {
  return {
    mounted () {
      console.log('I have already mounted')
    },
    props: WrappedComponent.props,
    render (h) {
      return h(WrappedComponent, {
        on: this.$listeners,
        attrs: this.$attrs,
        props: this.$props
      })
    }
  }
}

Теперь это немного более полный и удобный компонент высшего порядка. Обратите внимание на слова:稍微, Нани? Все изменилось на это, не так ли? Конечно, вышеприведенные компоненты более высокого порядка могут делать следующее:

1. Прозрачная передачаprops
2. Прозрачная передача не заявлена ​​какpropsсвойства
3, пропускное событие

Вам не кажется, что здесь не хватает смысла? Мы уже говорили, что одинVueТри важных фактора для компонентов:props,事件так же какslots, первые два выполнены, ноslotsЕще нет. мы модифицируемBaseComponentКомпонент добавляет именованный слот и слот по умолчанию, следующим образом:

base-component.vue

<template>
  <div>
    <span @click="handleClick">props: {{test}}</span>
    <slot name="slot1"/> <!-- 具名插槽 -->
    <p>===========</p>
    <slot/> <!-- 默认插槽 -->
  </div>
</template>

<script>
export default {
  ...
}
</script>

Затем пишем следующий тестовый код:

<template>
  <div>
    <base-component>
      <h2 slot="slot1">BaseComponent slot</h2>
      <p>default slot</p>
    </base-component>
    <enhanced-com>
      <h2 slot="slot1">EnhancedComponent slot</h2>
      <p>default slot</p>
    </enhanced-com>
  </div>
</template>

<script>
  import BaseComponent from './base-component.vue'
  import hoc from './hoc.js'

  const EnhancedCom = hoc(BaseComponent)

  export default {
    components: {
      BaseComponent,
      EnhancedCom
    }
  }
</script>

Синяя рамка на картинке вышеBaseComponentКонтент, отображаемый компонентом, является нормальным. Красное поле — это содержимое, отображаемое компонентом более высокого порядка, и можно обнаружить, что как именованный слот, так и слот по умолчанию отсутствуют. Причина очень проста, потому что мы не передаем прозрачно содержимое распределенного слота упакованному компоненту в компонент более высокого порядка (WrappedComponent), поэтому мы пытаемся изменить компонент более высокого порядка:

hoc.js

function WithConsole (WrappedComponent) {
  return {
    mounted () {
      console.log('I have already mounted')
    },
    props: WrappedComponent.props,
    render (h) {

      // 将 this.$slots 格式化为数组,因为 h 函数第三个参数是子节点,是一个数组
      const slots = Object.keys(this.$slots)
        .reduce((arr, key) => arr.concat(this.$slots[key]), [])

      return h(WrappedComponent, {
        on: this.$listeners,
        attrs: this.$attrs,
        props: this.$props
      }, slots) // 将 slots 作为 h 函数的第三个参数
    }
  }
}

Хорошо, все готово, обновите страницу следующим образом:

Нани 😱? Мы обнаружили, что содержимое раздачи действительно отображается, но кажется, что порядок не правильный. . . . . . Синий прямоугольник — это нормально, и есть разделительная линия между именованным слотом и слотом по умолчанию (===========), а все слоты в красной рамке отображаются на разделительной линии (===========), похоже, что именованные слоты также обрабатываются как слоты по умолчанию. что это за события такого рода?

Чтобы понять это, я возвращаюсь к тому, о чем я упоминал в начале статьи, а именно о том, что вам нужноVueНеобходимо иметь некоторое представление о принципе реализации, иначе решения нет. Далее мы объясним, как решить эту проблему по принципу срабатывания. Корень этой проблемы в следующем:VueСоображения области действия принимаются во внимание при работе с именованными слотами. Неважно, если вы не понимаете, давайте немного проанализируем.

Сначала добавьте подсказку:Vueпоставлю шаблон(template) компилируется в функцию рендеринга (render), например следующий шаблон:

<div>
  <h2 slot="slot1">BaseComponent slot</h2>
</div>

Будет скомпилировано в следующую функцию рендеринга:

var render = function() {
  var _vm = this
  var _h = _vm.$createElement
  var _c = _vm._self._c || _h
  return _c("div", [
    _c("h2", {
      attrs: { slot: "slot1" },
      slot: "slot1"
    }, [
      _vm._v("BaseComponent slot")
    ])
  ])
}

Легко просмотреть скомпилированную функцию рендеринга шаблона компонента, просто посетитеthis.$options.renderВот и все. Вышеуказанные наблюдения мы находим общий функцию рендерингаDOMчерез_cфункция для создания соответствующейVNodeиз. Теперь модифицируем шаблон, разве что есть обычныеDOMКроме того, существуют следующие компоненты:

<div>
  <base-component>
    <h2 slot="slot1">BaseComponent slot</h2>
    <p>default slot</p>
  </base-component>
</div>

Затем результирующая функция рендеринга (render) такова, что:

var render = function() {
  var _vm = this
  var _h = _vm.$createElement
  var _c = _vm._self._c || _h
  return _c(
    "div",
    [
      _c("base-component", [
        _c("h2", { attrs: { slot: "slot1" }, slot: "slot1" }, [
          _vm._v("BaseComponent slot")
        ]),
        _vm._v(" "),
        _c("p", [_vm._v("default slot")])
      ])
    ],
    1
  )
}

Мы обнаружили, что будь то обычный DOM или компонент, он передается через_cфункция создает соответствующийVNodeиз. фактически_cсуществуетVueвнутриcreateElementфункция.createElementФункция автоматически определит, является ли первый параметр обычным тегом DOM, если это не обычный тег DOM, тоcreateElementбудет рассматривать его как компонент и создавать экземпляр компонента,Обратите внимание, что экземпляр компонента создается только в это время.. Но есть проблема в процессе создания экземпляра компонента: компоненту нужно знать прошел ли родительский шаблонslotИ сколько было пройдено, было ли пройдено именно или безымянно и т.д. Так как же подкомпоненты могут узнать эту информацию? Очень просто, если шаблон компонента такой:

<div>
  <base-component>
    <h2 slot="slot1">BaseComponent slot</h2>
    <p>default slot</p>
  </base-component>
</div>

Шаблон родительского компонента в конечном итоге сгенерирует соответствующий родительский компонент.VNode, поэтому приведенный выше шаблон соответствуетVNodeВсе они принадлежат родительскому компоненту, поэтому при создании экземпляра дочернего компонента вы можете получитьVNodeполучитьslotсодержание? То есть через родительский компонент следующий шаблон соответствуетVNodeполучать:

<base-component>
  <h2 slot="slot1">BaseComponent slot</h2>
  <p>default slot</p>
</base-component>

Если вы можете получить соответствующий шаблон этого шаблона через родительскийVNode, то дочерний компонент знает, что отображатьslotДа, на самом делеVueЭто то, что он делает внутри, на самом деле вы можете получить доступ к подкомпонентуthis.$vnodeчтобы получить соответствующий шаблонVNode:

вthis.$vnodeне написал вVueофициальная документация. Дочерний компонент получает то, что нужно отобразитьslotПосле этого он вступил в ключевой шаг, который должен вызвать прозрачную передачу в компонентах высокого уровня.slotДатьBaseComponentПричина, по которой он не может быть отрендерен правильно, смотрите на следующем рисунке:

Это изображение такое же, как и предыдущее изображение, напечатанное в дочернем компоненте.this.$vnode, на этикеткеcontextцитируетсяVNodeЭкземпляр компонента, в котором он был создан, посколькуthis.$vnodeцитируется вVNodeОбъект создается в родительском компоненте, поэтомуthis.$vnodeсерединаcontextСсылается на родительский экземпляр. Теоретически два отмеченных на рисункеcontextдолжно быть равно:

console.log(this.$vnode.context === this.$vnode.componentOptions.children[0].context) // true

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

Так почему же приведенное выше выражение не выполняется в компонентах более высокого порядка? Это связано с тем, что из-за введения компонентов более высокого порядка компонент (也就是高阶组件), что приводит к доступу кthis.$vnodeЕго больше нет в исходном родительском компонентеVNodeФрагменты, но компоненты более высокого порядкаVNodeфрагмент, так что на этот разthis.$vnode.contextотносится к компонентам более высокого порядка, но вместо этого мыslotПроникнуть,slotсерединаVNodeизcontextСсылка по-прежнему относится к исходному экземпляру родительского компонента, поэтому следующее выражение становится ложным:

console.log(this.$vnode.context === this.$vnode.componentOptions.children[0].context) // false

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

Решение также очень простое, просто нужно вручную установитьslotсерединаVNodeизcontextЗначение может быть экземпляром компонента более высокого порядка. Измените компонент более высокого порядка следующим образом:

hoc.js

function WithConsole (WrappedComponent) {
  return {
    mounted () {
      console.log('I have already mounted')
    },
    props: WrappedComponent.props,
    render (h) {
      const slots = Object.keys(this.$slots)
        .reduce((arr, key) => arr.concat(this.$slots[key]), [])
        // 手动更正 context
        .map(vnode => {
          vnode.context = this._self
          return vnode
        })

      return h(WrappedComponent, {
        on: this.$listeners,
        props: this.$props,
        attrs: this.$attrs
      }, slots)
    }
  }
}

Теперь все может быть отрендерено нормально, как показано ниже:

Вот ключевые моменты помимо того, что вам нужно знатьVueиметь дело сslotВ дополнение к пути, вам также нужно знать текущий экземпляр_selfДоступ к свойству осуществляется, когда сам экземпляр, а не напрямуюthis,потому чтоthisявляется прокси-объектом.

Теперь это выглядит, казалось бы, без проблем, но мы забыли одно, то есть,scopedSlots,ноscopedSlotsа такжеslotМеханизм реализации другой, по сутиscopedSlotsЭто метод, который получает данные в качестве параметра и отображает их.VNodeфункции, поэтому не существуетcontextКонцепция, поэтому прямой проход к:

hoc.js

function WithConsole (WrappedComponent) {
  return {
    mounted () {
      console.log('I have already mounted')
    },
    props: WrappedComponent.props,
    render (h) {
      const slots = Object.keys(this.$slots)
        .reduce((arr, key) => arr.concat(this.$slots[key]), [])
        .map(vnode => {
          vnode.context = this._self
          return vnode
        })

      return h(WrappedComponent, {
        on: this.$listeners,
        props: this.$props,
        // 透传 scopedSlots
        scopedSlots: this.$scopedSlots,
        attrs: this.$attrs
      }, slots)
    }
  }
}

На данный момент реализованы основные функции, которыми должен обладать компонент более высокого порядка, но это только начало для достижения полного и надежногоVueДля компонентов более высокого порядка необходимо учитывать множество факторов, таких как:

Для использования в функциональных компонентахrenderВторой параметр функции заменяетthis.
Выше мы говорили только об объектах, существующих в виде чистых объектов.VueСборка, однако, в дополнение к чистым объектам может также функционировать.
СоздайтеrenderМногие шаги функции могут быть инкапсулированы.
Параметры для самостоятельной обработки функциональных компонентов более высокого порядка (而不仅仅是上面例子中的一个简单的生命周期钩子)

Я думаю, мне нужно поставить две справочные ссылки о компонентах более высокого порядка для ознакомления и обмена:

Discussion: Best way to create a HOC
GitHub.com/История Джека Мела/…

Почему сложно реализовать компоненты высшего порядка в Vue

Я сказал ранее, чтобы проанализировать, почемуVueРеализация высокоуровневых компонентов более сложна иReactПолегче. В основном это связано с тем, что идеи дизайна и цели дизайна у них разные.ReactНаписание компонентов в Китае — это написание функций, а функции имеют функциональные компоненты. а такжеVueбольше похоже на сильно инкапсулированные функции, на более высоком уровнеVueОн позволяет легко выполнять некоторые вещи, но в отличие от высокой степени инкапсуляции теряет некоторую гибкость: необходимо соблюдать определенные правила, чтобы система работала лучше.

Есть хорошая поговорка:

Будет сложно, трудно не

Сложные или простые понятия относительные, и, наконец, я надеюсь, что каждый сможет играть.Vueтакже оценилReact. Выложите две картинки, с которыми я согласен больше для вашего обсуждения: