функция рендеринга/jsx и слот в vue

Vue.js

1. Введение

За это время я перечитал документацию vue и обнаружил, что есть еще много знаний, которые используются не так часто, или которые использовались просто, но не так ясно. Сегодня мы рассмотрим функцию рендеринга render, синтаксис jsx и использование слота slot.

2. Недостатки синтаксиса шаблонов

Учащиеся, знакомые с методом написания однофайловых компонентов vue, знают, что html-часть vue-файла состоит из<template></template>Composition, этот метод относительно прост в использовании и может удовлетворить потребности в большинстве случаев с инструкциями vue. Тем не менее, бывают случаи, когда синтаксис шаблона неудобен, например, необходимо разработать компонент, которому нужно выбрать отображаемый html-тег в соответствии со значением, переданным родительским компонентом. Рассмотрим пример:

// hLabel.vue
<template>
  <h1 v-if="level === 1">
    <slot></slot>
  </h1>
  <h2 v-else-if="level === 2">
    <slot></slot>
  </h2>
  <h3 v-else-if="level === 3">
    <slot></slot>
  </h3>
  <h4 v-else-if="level === 4">
    <slot></slot>
  </h4>
  <h5 v-else-if="level === 5">
    <slot></slot>
  </h5>
  <h6 v-else-if="level === 6">
    <slot></slot>
  </h6>
</template>
<script>
export default{
  props: {
    level:{
      type: Number
    }
  }
}
</script>

Хотя вышеуказанный компонент может отображать соответствующий контент в соответствии со значением уровня<h1>,<h2>...теги, но есть также много избыточного кода, и по одному в теге заголовка каждого уровня<slot>Этикетка.

Чтобы решить эту проблему, нам нужно использовать функцию рендеринга в vue.render.

3. Рендер функции рендеринга

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

// hLabel.vue
<script>
export default {
  props: {
    level: {
      type: Number,
    }
  },
  render: function(createElement){
    return createElement(
      'h' + this.level, // 标签名称,根据父组件传入的level值确定
      this.$slots.default // 子节点数组
    )
  }
}
</script>

Приведенный выше код очень лаконичен, и шаблон этикетки можно отобразить с помощью функции рендеринга. В то же время, если вам нужно передать оригинал<slot>Полученный контент, на этот раз для использования$slots.default, мы отдельно упомянем об использовании слота позже.

Vue предоставляет параметр createElement для функции рендеринга. Этот параметр также является методом функции. Он принимает определенные параметры и возвращает виртуальный DOM (Virtual Dom) VNode. В Vue мы обычно согласны с тем, что createElement можно сократить как h. Давайте посмотрим на использование createElement:

render: function(createElement){
  // @returns {VNode}
  createElement(
    // {String | Object | Function}
    // 一个 HTML 标签名、组件选项对象,或者
    // resolve 了上述任何一种的一个 async 函数。必填项。
    'div',

    // {Object}
    // 一个与模板中属性对应的数据对象。可选。
    {
      // 主要是html模板标签中的属性值的写法,下面单独介绍
    },

    // {String | Array}
    // 子级虚拟节点 (VNodes),由 `createElement()` 构建而成,
    // 也可以使用字符串来生成“文本虚拟节点”。可选。
    [
      '先写一些文字', // 如果是字符串,表示是生成标签中的内容
      createElement('h1', '一则头条'), // createElement生成的新VNode
      createElement(MyComponent, {
        props: {
          someProp: 'foobar'
        }
      })
    ]
  )
}

Давайте посмотрим, как записываются атрибуты в шаблоне в функции createElement:

{
  // 与 `v-bind:class` 的 API 相同,
  // 接受一个字符串、对象或字符串和对象组成的数组
  'class': {
    foo: true,
    bar: false
  },
  // 与 `v-bind:style` 的 API 相同,
  // 接受一个字符串、对象,或对象组成的数组
  style: {
    color: 'red',
    fontSize: '14px'
  },
  // 普通的 HTML 特性
  attrs: {
    id: 'foo'
  },
  // 组件 prop,这个属性是当createElement渲染的是一个组件时使用
  props: {
    myProp: 'bar'
  },
  // DOM 属性
  domProps: {
    innerHTML: 'baz'
  },
  // 事件监听器在 `on` 属性内,
  // 但不再支持如 `v-on:keyup.enter` 这样的修饰器。
  // 需要在处理函数中手动检查 keyCode。
  on: {
    click: this.clickHandler
  },
  // 仅用于组件,用于监听原生事件,而不是组件内部使用
  // 组件内的原生事件触发时,使用`vm.$emit` 触发的事件。
  nativeOn: {
    click: this.nativeClickHandler
  },
  // 自定义指令。注意,你无法对 `binding` 中的 `oldValue`
  // 赋值,因为 Vue 已经自动为你进行了同步。
  directives: [
    {
      name: 'my-custom-directive',
      value: '2',
      expression: '1 + 1',
      arg: 'foo',
      modifiers: {
        bar: true
      }
    }
  ],
  // 作用域插槽的格式为
  // { name: props => VNode | Array<VNode> }
  scopedSlots: {
    default: props => createElement('span', props.text)
  },
  // 如果组件是其它组件的子组件,需为插槽指定名称
  slot: 'name-of-slot',
  // 其它特殊顶层属性
  key: 'myKey',
  ref: 'myRef',
  // 如果你在渲染函数中给多个元素都应用了相同的 ref 名,
  // 那么 `$refs.myRef` 会变成一个数组。
  refInFor: true
}

Из приведенного выше примера видно, что так же, как v-bind:class и v-bind:style специально обрабатываются в синтаксисе шаблона, они также имеют соответствующие поля верхнего уровня в объектах данных VNode. Этот объект также позволяет привязывать обычные свойства HTML, а также свойства DOM, такие как innerHTML (это переопределяет директиву v-html).

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

4. JSX-синтаксис

Я считаю, что люди, которые написали React, определенно не будут незнакомы с этим синтаксисом. Благодаря поддержке плагина babel синтаксис jsx также можно использовать непосредственно в функции рендеринга vue. Если вы используете проект, созданный vue-cli 3.x, вам не нужна никакая конфигурация, просто используйте jsx напрямую.

// hLabel.vue
<script>
export default{
  props: {
    level: {
      type: Number,
    }
  },
  methods: {
    clickHandler(){

    },
    nativeClickHandler(){

    }
  },
  render:function(h) { // createElement约定可简写为h
    let tag = `h${this.level}`
    return (
      <tag
        key="key"
        ref="ref"
        id='title'
        class={{'foo':true,, 'bar':false}}
        style={{margin: '10px', color:'red'}}
        onClick={this.clickHandler}
        nativeOnClick={this.nativeClickHandler} // 监听组件内的原生事件
      >{this.$slots.default}
      </tag>
    )
  }
}
</script>

В приведенном выше примере приведен простой пример синтаксиса jsx и добавления атрибутов в теги. Однако, если мы используем функцию рендеринга, некоторые инструкции, поставляемые с Vue, больше не будут действовать, в том числеv-if,v-forа такжеv-model, Нам нужно реализовать себя.

5. Реализация инструкции vue в функции рендеринга

v-если и v-для:

<ul v-if="items.length">
  <li v-for="item in items">{{ item.name }}</li>
</ul>
<p v-else>No items found.</p>
// 在渲染函数中需要使用 if/else 和 map 来重写
props: ['items'],
render: function (h) {
  if (this.items.length) {
    return h('ul', this.items.map(function (item) {
      return h('li', item.name)
    }))
  } else {
    return h('p', 'No items found.')
  }
}

v-model:

props: ['value'],
render: function (createElement) {
  var self = this
  return createElement('input', {
    domProps: {
      value: self.value
    },
    on: {
      input: function (event) {
        self.$emit('input', event.target.value)
      }
    }
  })
}

По сути, приведенный выше код — это принцип двухсторонней привязки инструкций v-model в vue, но v-model сделал совместимую обработку для разных элементов привязки. В то же время v-модель также может быть привязана к компоненту, конкретное использование можно щелкнутьПосмотреть здесь.

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

6. Функциональные компоненты

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

Функциональный компонент выглядит так:

<script>
export default{
  functional: true, // 添加属性functional: true,表示该组件为函数式组件
  // Props 是可选的
  props: {
    // ...
  },
  // 为了弥补缺少的实例
  // 提供第二个参数作为上下文
  render: function (createElement, context) {
    // ...
  },
}
</script>

В однофайловых компонентах версии 2.5.0 и выше функциональные компоненты на основе шаблонов могут быть объявлены следующим образом:

<template functional>
</template>

Поскольку функциональные компоненты не имеют состояния, этот контекст и такие атрибуты, как данные, отсутствуют, поэтому, если требуемые данные являются вторым параметром функции рендерингаcontextприобретенный:

  • реквизит: объект, предоставляющий все реквизиты
  • data: весь объект данных, переданный компоненту, переданный в компонент в качестве второго параметра createElement
  • Children: массив дочерних узлов VNode
  • parent: ссылка на родительский компонент
  • слоты: функция, которая возвращает объект, содержащий все слоты
  • scopedSlots: (2.6.0+) Объект, предоставляющий переданные слоты с областью видимости. Также предоставляет обычные слоты как функции.
  • Слушатели: (2.3.0+) Объект, содержащий все родительские компоненты, зарегистрированные с текущим компонентом. Это псевдоним Data.on.
  • инъекции: (2.3.0+) Если используется опция инъекции, этот объект содержит свойства, которые должны быть внедрены.

После перехода на функциональный компонент нам нужно изменить функцию рендеринга нашего компонента, добавить в него параметры контекста, и если естьthis.$slots.defaultизменить наcontext.children,Потомthis.levelизменить наcontext.props.levelЖдать.

7. Слот,slots 和scopedSlots

Выше мы видели многократное использование слота в функции рендеринга. Итак, на этот раз давайте посмотрим, что такое слот.

Содержание слота:

Vue реализует набор API для распространения контента,<slot>Элемент действует как выход для переноса распределенного контента, то есть контент, заполненный компонентом, может быть отображен между слотами дочернего компонента.

<!-- 父组件 -->
<navigation-link url="/profile">
  Your Profile
</navigation-link>
<!-- navigation-link组件 -->
<a
  v-bind:href="url"
  class="nav-link"
>
  <slot></slot>
</a>

Когда компонент визуализируется,<slot></slot>будет заменен на «Ваш профиль».

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

<navigation-link url="/profile">
  <!-- 添加一个 Font Awesome 图标 -->
  <span class="fa fa-user"></span>
  Your Profile
</navigation-link>

<!-- 插槽内为组件 -->
<navigation-link url="/profile">
  <!-- 添加一个图标的组件 -->
  <font-awesome-icon name="user"></font-awesome-icon>
  Your Profile
</navigation-link>

Объем компиляции:

Область компиляции относится к содержимому, записанному внутри ссылочного компонента, и содержимому внутри подкомпонента, и все, что можно получить, — это значение в его текущей области. Существует принцип, что все в родительском шаблоне компилируется в родительской области, все в дочернем шаблоне компилируется в дочерней области. ** В сочетании с кодом для просмотра:

<!-- 如果想在插槽中使用数据user -->
<!-- user必须是navigation-link组件坐在的作用域可以访问到的值 -->
<navigation-link url="/profile">
  Logged in as {{ user.name }}
</navigation-link>

Ниже приведен пример недостижимой ошибки:

<!-- 这里是访问不到url的 -->
<!-- 因为当前的url值"/profile"是在navigation-link组件内部定义的 -->
<!-- 在navigation-link组件所在的作用域,是访问不到url的 -->
<navigation-link url="/profile">
  Clicking here will send you to: {{ url }}
</navigation-link>

Резервный контент и именованные слоты:

Пример кода для непосредственного просмотра резервного содержимого:

<!-- 父组件 -->
<submit-button>
  Save
</submit-button>

<!-- submit-button组件 -->
<button type="submit">
  <slot>Submit</slot>
</button>

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

<!-- 父组件 -->
<base-layout>
  <template v-slot:header>
    <h1>Here might be a page title</h1>
  </template>

  <p>A paragraph for the main content.</p>
  <p>And another one.</p>

  <template #footer>
    <p>Here's some contact info</p>
  </template>
</base-layout>
<!-- base-layout 组件 -->
<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>

Выше приведен пример использования именованных слотов. Если вам нужно указать содержимое для рендеринга нескольких слотов, вы можете добавить атрибут имени к слоту, а при предоставлении контента для слота вы можете использовать шаблон для переноса контента и написать спецификацию v-slot на шаблоне. , и используйте параметр Форма предоставляет имя слота для рендеринга на v-слоте. Таким образом, содержимое шаблона может быть отображено в слоте с указанным именем. v-slot: имя в шаблоне может быть сокращено до #name. Если в шаблоне не указано имя, используется имя по умолчанию.

Слоты с ограниченной областью действия:

Как мы упоминали выше, область действия слота ограничена, содержимое родительского шаблона может иметь доступ только к области действия родительского шаблона, а содержимое дочернего компонента может отображаться только в области дочернего элемента. Если я хочу использовать значение в дочернем компоненте в родительском шаблоне, как я могу это реализовать?На данный момент мне нужно использовать слот, а именно:

<!-- 错误示范 -->
<!-- 父组件想要使用current-user组件内的值user -->
<current-user>
  {{ user.firstName }}
</current-user>

<!-- current-user组件 -->
<span>
  <slot>{{ user.lastName }}</slot>
</span>

<!-- 正确示范 -->
<current-user>
  <template v-slot:default="slotProps">
    {{ slotProps.user.firstName }}
  </template>
</current-user>
<span>
  <slot v-bind:user="user">
    {{ user.lastName }}
  </slot>
</span>

$slots:

API, используемый в vue для доступа к контенту, распространяемому слотом, эквивалентен API в шаблоне.<slot></slot>. Каждый именованный слот имеет свои собственные свойства (например: содержимое v-slot:foo будет найдено в vm.$slots.foo). Атрибут по умолчанию включает все узлы, не содержащиеся в именованном слоте, или содержимое v-slot:default.

$scopedSlots:

Используется для доступа к слотам с ограниченной областью действия, что может дать<slot>Область слота, предоставляющая значение. Для каждого слота, включая слот по умолчанию, этот объект содержит функцию, которая возвращает соответствующий VNode.

8. Резюме

Это то, что я представлю на этот раз.Хотя это относительно просто, но в нем задействованы основные принципы использования.Я надеюсь, что это будет полезно для всех в будущем развитии или интервью.

9. Справочные статьи

Официальная документация Vue: функция рендеринга и jsx

официальная документация vue: слоты

vue официальный API

Использование синтаксиса jsx в vue

vue jsx не полностью указывает на север


Об авторе: Гун Ченгуан,люди и будущееИнженер по работе с большими данными.