Render-функции | Vue.js
Для большинства случаев рекомендуется использовать шаблоны для создания приложений. Но бывают ситуации, когда необходима вся программная мощь JavaScript. В таких случаях можно использовать render-функции.
Рассмотрим небольшой пример, где функция render()
оказалась бы практичнее обычного подхода. Например, требуется сгенерировать заголовки с якорными ссылками:
<h2> <a name="hello-world" href="#hello-world"> Hello world! </a> </h2>
1
2
3
4
5
Такие заголовки будут использоваться часто, поэтому сразу стоит создать компонент:
<anchored-heading :level="1">Hello world!</anchored-heading>
1
Компонент должен генерировать заголовок, в зависимости от входного параметра level
, что скорее всего приведёт к такому решению:
const { createApp } = Vue const app = createApp({}) app.component('anchored-heading', { template: ` <h2 v-if="level === 1"> <slot></slot> </h2> <h3 v-else-if="level === 2"> <slot></slot> </h3> <h4 v-else-if="level === 3"> <slot></slot> </h4> <h5 v-else-if="level === 4"> <slot></slot> </h5> <h5 v-else-if="level === 5"> <slot></slot> </h5> <h6 v-else-if="level === 6"> <slot></slot> </h6> `, props: { level: { type: Number, required: true } } })
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
Шаблон такого компонента выглядит не очень. Он не только многословен, но и дублирует <slot></slot>
для каждого уровня заголовка. А при добавлении нового элемента якоря, потребуется снова дублировать его в каждой ветке v-if/v-else-if
.
Хотя шаблоны отлично работают для большинства компонентов, очевидно, что данный случай не один из них. Давайте перепишем компонент с помощью функции render()
:
const { createApp, h } = Vue const app = createApp({}) app.component('anchored-heading', { render() { return h( 'h' + this.level, // имя тега {}, // входные параметры/атрибуты this.$slots.default() // массив дочерних элементов ) }, props: { level: { type: Number, required: true } } })
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Реализация с функцией render()
получилась гораздо проще, но требует больше знаний о свойствах экземпляра компонента. Для этого примера потребуется знать, что при передаче дочерних элементов в компонент без директивы
, например Hello world!
внутрь anchored-heading
, они будут доступны в экземпляре компонента через $slots.default()
. Если это ещё непонятно, рекомендуем сначала прочитать раздел API свойств экземпляра перед углублением в render-функции.
DOM-дерево
Прежде чем погрузиться в изучение render-функций, важно немного подробнее разобраться в том, как работают браузеры. Возьмём, к примеру, этот HTML:
<div> <h2>My title</h2> Some text content <!-- TODO: Add tagline --> </div>
1
2
3
4
5
Когда браузер читает этот код, он строит дерево «DOM узлов» (opens new window), чтобы помочь себе отслеживать всё.
Для HTML из примера выше дерево DOM-узлов получится таким:
Каждый элемент является узлом. Каждый текст является узлом. Каждый комментарий является узлом! Каждый узел может иметь дочерние элементы (т.е. каждый узел может содержать другие узлы).
Эффективно обновлять все эти узлы — непростая задача, но, к счастью, это не потребуется делать вручную. Требуется лишь сообщать Vue какой HTML нужен на странице в шаблоне:
<h2>{{ blogTitle }}</h2>
1
Или в render-функции:
render() { return h('h2', {}, this.blogTitle) }
1
2
3
В обоих случаях Vue будет автоматически поддерживать страницу в обновлённом состоянии, даже при изменениях значения blogTitle
.
Виртуальное DOM-дерево
Vue поддерживает страницу в обновлённом состоянии с помощью виртуального DOM. Он помогает определить изменения, которые необходимо внести в реальный DOM. Взглянем внимательнее на эту строку:
return h('h2', {}, this.blogTitle)
1
Что вернёт функция h()
? Это не совсем настоящий DOM-элемент.
Аргументы
h()
Функция h()
— утилита для создания VNode. Возможно её стоило назвать createVNode()
для точности, но она называется h()
из-за частого использования и для краткости. Функция принимает три аргумента:
// @returns {VNode} h( // {String | Object | Function} тег // Имя HTML-тега, компонента, асинхронного или функционального компонента. // Использование функции, возвращающей null, будет отрисовывать комментарий. // // Обязательный параметр 'div', // {Object} входные параметры // Объект, соответствующий атрибутам, входным параметрам // и событиям, которые использовались бы в шаблоне. // // Опциональный {}, // {String | Array | Object} дочерние элементы // Дочерние VNode, созданные с помощью `h()`, // или строки для получения 'текстовых VNode' или // объект со слотами. // // Опциональный [ 'Какой-то текст в начале.', h('h2', 'Заголовок'), h(MyComponent, { someProp: 'foobar' }) ] )
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Если входных параметров нет, то дочерние элементы можно передать вторым аргументом. В случаях, если это может добавить путаницы, можно указывать null
вторым аргументом, чтобы третьим явно передавать дочерние элементы.
Полный пример
С полученными знаниями можно теперь завершить начатый компонент:
const { createApp, h } = Vue const app = createApp({}) /** Рекурсивно получаем текст из дочерних узлов */ function getChildrenTextContent(children) { return children .-|-$)/g, '') // удаляем в начале и конце висящие тире return h('h' + this.level, [ h( 'a', { name: headingId, href: '#' + headingId }, this.$slots.default() ) ]) }, props: { level: { type: Number, required: true } } })
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
Ограничения
VNode должны быть уникальными
В дереве компонентов все VNode должны быть уникальными. Это означает, что следующая render-функция некорректна:
render() { const myParagraphVNode = h('p', 'hi') return h('div', [ // НЕПРАВИЛЬНО - одинаковые VNode! myParagraphVNode, myParagraphVNode ]) }
1
2
3
4
5
6
7
Если требуется многократно дублировать один и тот же элемент/компонент, то реализовать это можно с помощью функции-фабрики. Например, следующая render-функция будет абсолютно корректным способом для отрисовки 20 одинаковых параграфов:
render() { return h('div', Array.from({ length: 20 }).map(() => { return h('p', 'hi') }) ) }
1
2
3
4
5
6
7
Создание VNode компонентов
При создании VNode для компонента первым аргументом h
должен быть сам компонент:
render() { return h(ButtonCounter) }
1
2
3
Если необходимо разрешить компонент по имени, можно использовать resolveComponent
:
const { h, resolveComponent } = Vue // ... render() { const ButtonCounter = resolveComponent('ButtonCounter') return h(ButtonCounter) }
1
2
4
5
6
7
8
Функцию resolveComponent
используют шаблоны под капотом для разрешения компонентов по имени.
В функции render
обычно приходится использовать resolveComponent
для компонентов зарегистрированных глобально. При локальной регистрации компонентов обычно можно обойтись без неё. Рассмотрим следующий пример:
// Такой код можно упростить components: { ButtonCounter }, render() { return h(resolveComponent('ButtonCounter')) }
1
2
3
4
5
6
7
Вместо регистрации компонента по имени, а затем поиска, можно использовать его сразу:
render() { return h(ButtonCounter) }
1
2
3
Замена возможностей шаблона обычным JavaScript
v-if
и v-for
Всё что используется можно легко реализовать на простом JavaScript, для render-функции Vue не создаёт никакой проприетарной альтернативы. Например, шаблон с v-if
и v-for
:
<ul v-if="items.length"> <li v-for="item in items">{{ item.name }}</li> </ul> <p v-else>Элементов не найдено.</p>
1
2
3
4
Можно переписать с использованием JavaScript if
/else
и map()
в render-функции:
props: ['items'], render() { if (this. items.length) { return h('ul', this.items.map((item) => { return h('li', item.name) })) } else { return h('p', 'Элементов не найдено.') } }
1
2
3
4
5
6
7
8
9
10
В шаблоне иногда удобно использовать тег <template>
, чтобы указать v-if
или v-for
. При миграции на использование render
-функции тег <template>
можно просто опустить.
v-model
На этапе компиляции шаблона директива v-model
раскладывается на входные параметры modelValue
onUpdate:modelValue
— их потребуется указать самостоятельно:props: ['modelValue'], emits: ['update:modelValue'], render() { return h(SomeComponent, { modelValue: this.modelValue, 'onUpdate:modelValue': value => this.$emit('update:modelValue', value) }) }
1
2
3
4
5
6
7
8
v-on
Необходимо предоставить правильное имя входного параметра для обработчика события, например, для обработки событий click
имя входного параметра должно быть onClick
.
render() { return h('div', { onClick: $event => console.log('кликнули!', $event.target) }) }
1
2
3
4
5
Модификаторы событий
Модификаторы событий .passive
, .capture
и .once
необходимо указывать после имени события в camelCase.
Например:
render() { return h('input', { onClickCapture: this.doThisInCapturingMode, onKeyupOnce: this.doThisOnce, onMouseoverOnceCapture: this.doThisOnceInCapturingMode }) }
1
2
3
4
5
6
7
Для любых других событий и модификаторов клавиш специального API не требуется, потому что в обработчике события можно использовать нативные свойства и методы:
Модификатор(ы) | Эквивалент в обработчике |
---|---|
.stop | event.stopPropagation() |
.prevent | event.preventDefault() |
. self | if (event.target !== event.currentTarget) return |
Клавиши: например, .enter | if (event.key !== 'Enter') return Замените |
Модификаторы клавиш:.ctrl , .alt , .shift , .meta | if (!event.ctrlKey) return Замените |
Пример обработчика со всеми этими модификаторами, используемыми вместе:
render() { return h('input', { onKeyUp: event => { // Отменяем обработку, если элемент вызвавший событие // не является элементом, к которому событие было привязано if (event.target !== event.currentTarget) return // Отменяем обработку, если код клавиши не соответствовал // enter и клавиша shift не была нажата в то же время if (!event. shiftKey || event.key !== 'Enter') return // Останавливаем всплытие события event.stopPropagation() // Останавливаем поведение по умолчанию для этого элемента event.preventDefault() // ... } }) }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Слоты
Доступ к содержимому слотов в виде массива VNode можно получить через this.$slots
:
render() { // `<div><slot></slot></div>` return h('div', this.$slots.default()) }
1
2
3
4
props: ['message'], render() { // `<div><slot :text="message"></slot></div>` return h('div', this.$slots.default({ text: this.message })) }
1
2
3
4
5
6
7
Для VNode компонента необходимо передать дочерние элементы в h
в виде объекта, а не массива. Каждое свойство будет использовано для заполнения одноимённого слота:
render() { // `<div><child v-slot="props"><span>{{ props. text }}</span></child></div>` return h('div', [ h( resolveComponent('child'), null, // передаём `slots` как дочерний объект в формате // { slotName: props => VNode | Array<VNode> } { default: (props) => h('span', props.text) } ) ]) }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Слоты передаются как функции, что позволяет дочернему компоненту управлять созданием содержимого каждого слота. Любые реактивные данные должны быть доступны внутри функции слота, чтобы гарантировать, что они зарегистрированы как зависимость дочернего компонента, а не родительского. И наоборот, обращения к resolveComponent
нужно делать вне функции слота, иначе они будут разрешаться относительно неправильного компонента:
// `<MyButton><MyIcon :name="icon" />{{ text }}</MyButton>` render() { // Вызовы resolveComponent должны находиться вне функции слота const Button = resolveComponent('MyButton') const Icon = resolveComponent('MyIcon') return h( Button, null, { // Используем стрелочную функцию для сохранения значения `this` default: (props) => { // Реактивные свойства должны считываться внутри функции слота, // чтобы они стали зависимостями для отрисовки дочернего компонента return [ h(Icon, { name: this. icon }), this.text ] } } ) }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Если компонент получает слоты из родителя — их можно передать напрямую дочернему:
render() { return h(Panel, null, this.$slots) }
1
2
3
Но можно также передавать их по-отдельности или оборачивать по необходимости:
render() { return h( Panel, null, { // Если хотим просто передать функцию слота header: this.$slots.header, // Если требуется как-то управлять слотом, // тогда нужно обернуть его в новую функцию default: (props) => { const children = this.$slots.default ? this.$slots.default(props) : [] return children.concat(h('div', 'Extra child')) } } ) }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<component>
и is
Шаблоны для реализации атрибута is
используют под капотом resolveDynamicComponent
. Можно воспользоваться этой же функцией, если в создаваемой render
-функции требуется вся гибкость, предоставляемая is
:
const { h, resolveDynamicComponent } = Vue // ... // `<component :is="name"></component>` render() { const Component = resolveDynamicComponent(this.name) return h(Component) }
1
2
3
4
5
6
7
8
9
Аналогично is
, resolveDynamicComponent
поддерживает передачу имени компонента, имени HTML-элемента или объекта с опциями компонента.
Но обычно такой уровень гибкости не требуется. Поэтому resolveDynamicComponent
часто можно заменить на более конкретную альтернативу.
К примеру, если нужно поддерживать только имена компонентов — можно использовать resolveComponent
.
Если VNode всегда будет HTML-элементом — можно передавать имя напрямую в h
:
// `<component :is="bold ? 'strong' : 'em'"></component>` render() { return h(this. bold ? 'strong' : 'em') }
1
2
3
4
Аналогично, если передаваемое в is
значение будет объектом опций компонента, то не нужно ничего разрешать и можно сразу передать его первым аргументом в h
.
Подобно тегу <template>
, тег <component>
нужен в шаблонах только в качестве синтаксического сахара и его следует опустить при миграции на render
-функции.
Пользовательские директивы
Пользовательские директивы можно применить к VNode с помощью withDirectives
:
const { h, resolveDirective, withDirectives } = Vue // ... // <div v-pin:top.animate="200"></div> render () { const pin = resolveDirective('pin') return withDirectives(h('div'), [ [pin, 200, 'top', { animate: true }] ]) }
1
2
3
4
5
6
7
8
9
Функция resolveDirective
используется в шаблонах под капотом, чтобы разрешить директиву по имени. Это нужно лишь в случаях, когда нет прямого доступа к объекту с объявлением директивы.
Встроенные компоненты
Встроенные компоненты, такие как <keep-alive>
, <transition>
, <transition-group>
и <teleport>
по умолчанию не регистрируются глобально. Это позволяет системе сборки выполнять tree-shaking, чтобы добавлять эти компоненты в сборку только в случае, если они используются. Но это также означает, что не выйдет получить к ним доступ с помощью resolveComponent
или resolveDynamicComponent
.
Шаблоны имеют специальную обработку для этих компонентов, автоматически импортируя их при использовании. Но при создании собственных render
функций импортировать потребуется их самостоятельно:
const { h, KeepAlive, Teleport, Transition, TransitionGroup } = Vue // ... render () { return h(Transition, { mode: 'out-in' }, /* ... */) }
1
2
3
4
5
6
7
Возвращаемые значения render-функций
Во всех примерах, рассматривавшихся ранее, функция render
возвращала один корневой узел VNode. Но могут быть и другие варианты.
Если вернуть строку, то будет создана текстовая VNode без какого-либо элемента-обёртки:
render() { return 'Привет мир!' }
1
2
3
Также можно вернуть массив дочерних узлов, не оборачивая их в корневой узел. В таком случае будет создан фрагмент:
// Аналогично шаблону `Пример<br>мир!` render() { return [ 'Привет', h('br'), 'мир!' ] }
1
2
3
4
5
6
7
8
Если компоненту не нужно ничего отображать (например, потому что ещё загружаются данные), то можно просто вернуть null
. Тогда в DOM будет создан узел комментария.
JSX
При создании множества render
-функций может быть мучительно писать подобное:
h( resolveComponent('anchored-heading'), { level: 1 }, { default: () => [h('span', 'Привет'), ' мир!'] } )
1
2
3
4
5
6
7
8
9
Особенно, когда эквивалент в шаблоне выглядит очень лаконично:
<anchored-heading :level="1"> <span>Привет</span> мир! </anchored-heading>
1
Поэтому есть плагин для Babel (opens new window), чтобы использовать JSX во Vue и получить синтаксис, близкий к шаблонам:
import AnchoredHeading from '. /AnchoredHeading.vue' const app = createApp({ render() { return ( <AnchoredHeading level={1}> <span>Привет</span> мир! </AnchoredHeading> ) } }) app.mount('#demo')
1
2
3
4
5
6
7
8
9
10
11
12
13
Подробнее о том, как JSX преобразуется в JavaScript смотрите в документации плагина (opens new window).
Функциональные компоненты
Функциональные компоненты — альтернативная форма компонентов без собственного состояния. Они отрисовываются без создания экземпляра компонента, минуя обычный жизненный цикл компонента.
Для создания функционального компонента нужно использовать простую функцию, а не объект с опциями компонента. Она фактически является render
-функцией компонента. И так как у функционального компонента нет this
, то props
будут передаваться первым аргументом:
const FunctionalComponent = (props, context) => { // . .. }
1
2
3
Вторым аргументом передаётся context
, содержащий три свойства: attrs
, emit
и slots
. Они эквивалентны свойствам экземпляра $attrs
, $emit
и $slots
.
В функциональных компонентах большинство обычных опций конфигурации компонентов недоступны. Но можно определять props
и emits
, добавив их в качестве свойств:
FunctionalComponent.props = ['value'] FunctionalComponent.emits = ['click']
1
2
Если не указана опция props
, то передаваемый в функцию объект props
будет содержать все атрибуты, как и attrs
. Также имена входных параметров не будут нормализоваться в camelCase.
Функциональные компоненты можно регистрировать и использовать так же, как и обычные компоненты. Если передавать функцию в качестве первого аргумента в h
, то она будет рассматриваться как функциональный компонент.
Компиляция шаблона
Интересный факт, все шаблоны во Vue компилируются в render-функции. Обычно нет нужды знать такие детали реализации, но может быть любопытно увидеть как же компилируются те или иные возможности шаблона. Небольшая демонстрация ниже показывает работу метода Vue.compile
при компиляции строковых шаблонов на лету, попробуйте сами:
Лямбды — Kotlin
В Kotlin функции являются функциями первого класса. Это значит, что они могут храниться в переменных и структурах данных, передаваться в качестве аргументов и возвращаться из других функций высшего порядка. Вы можете работать с функциями любым способом, который возможен для других нефункциональных значений.
Чтобы это облегчить, Kotlin, как статически типизированный язык программирования, использует семейство функциональных типов для представления функций и предоставляет набор специализированных языковых конструкций, таких как лямбда-выражения.
Функции высшего порядка
Функция высшего порядка — это функция, которая принимает функции как параметры, или возвращает функцию в качестве результата.
Хорошим примером такой функции является идиома функционального программирования fold
для коллекций, которая
принимает начальное значение — accumulator
вместе с комбинирующей функцией и строит возвращаемое значение,
последовательно комбинируя текущее значение accumulator
с каждым элементом коллекции, заменяя значение accumulator
.
fun <T, R> Collection<T>.fold( initial: R, combine: (acc: R, nextElement: T) -> R ): R { var accumulator: R = initial for (element: T in this) { accumulator = combine(accumulator, element) } return accumulator }
В приведённом выше коде параметр combine
имеет функциональный тип (R, T) -> R
, поэтому он
принимает функцию, которая принимает два аргумента типа R
и T
и возвращает значение типа R
. Он
вызывается внутри цикла for
и присваивает accumulator
возвращаемое значение.
Чтобы вызвать fold
, вы должны передать ему экземпляр функционального типа в качестве
аргумента и лямбда-выражение (описание ниже). Лямбда-выражения часто
используются в качестве параметра функции высшего порядка.
fun main() { val items = listOf(1, 2, 3, 4, 5) // Лямбда - это блок кода, заключенный в фигурные скобки. items.fold(0, { // Если у лямбды есть параметры, то они указываются перед знаком '->' acc: Int, i: Int -> print("acc = $acc, i = $i, ") val result = acc + i println("result = $result") // Последнее выражение в лямбде считается возвращаемым значением: result }) // Типы параметров в лямбде необязательны, если они могут быть выведены: val joinedToString = items.fold("Elements:", { acc, i -> acc + " " + i }) // Ссылки на функции также могут использоваться для вызовов функций высшего порядка: val product = items.fold(1, Int::times) println("joinedToString = $joinedToString") println("product = $product") }
Функциональные типы
Kotlin использует семейство функциональных типов, таких как (Int) -> String
, для объявлений, которые являются частью
функций: val onClick: () -> Unit = . ..
.
Эти типы имеют специальные обозначения, которые соответствуют сигнатурам функций, то есть их параметрам и возвращаемым значениям:
- У всех функциональных типов есть список с типами параметров, заключенный в скобки, и возвращаемый тип:
(A, B) -> C
обозначает тип, который предоставляет функции два принятых аргумента типаA
иB
, а также возвращает значение типаC
. Список с типами параметров может быть пустым, как, например, в() -> A
. Возвращаемый типUnit
не может быть опущен;
- У функциональных типов может быть дополнительный тип — получатель (ориг.: receiver), который указывается в
объявлении перед точкой: тип
A.(B) -> C
описывает функции, которые могут быть вызваны для объекта-получателяA
с параметромB
и возвращаемым значениемC
. Литералы функций с объектом-приёмником часто используются вместе с этими типами;
- Останавливаемые функции (ориг. : suspending functions)
принадлежат к особому виду функциональных типов, у которых в объявлении присутствует модификатор
suspend
, например,suspend () -> Unit
илиsuspend A.(B) -> C
.
Объявление функционального типа также может включать именованные параметры: (x: Int, y: Int) -> Point
.
Именованные параметры могут быть использованы для описания смысла каждого из параметров.
Чтобы указать, что функциональный тип может быть nullable,
используйте круглые скобки: ((Int, Int) -> Int)?
.
При помощи круглых скобок функциональные типы можно объединять: (Int) -> ((Int) -> Unit)
.
Стрелка в объявлении является правоассоциативной (ориг.: right-associative), т.е. объявление
(Int) -> (Int) -> Unit
эквивалентно объявлению из предыдущего примера, а не((Int) -> (Int)) -> Unit
.
Вы также можете присвоить функциональному типу альтернативное имя, используя псевдонимы типов.
typealias ClickHandler = (Button, ClickEvent) -> Unit
Создание функционального типа
Существует несколько способов получить экземпляр функционального типа:
Используя блок с кодом внутри функционального литерала в одной из форм:
- лямбда-выражение:
{ a, b -> a + b }
, - анонимная функция:
fun(s: String): Int { return s.toIntOrNull() ?: 0 }
Литералы функций с объектом-приёмником могут использоваться как значения функциональных типов с получателем.
- лямбда-выражение:
Используя вызываемую ссылку на существующее объявление:
- функции верхнего уровня, локальной функции, функции-члена или функции-расширения:
::isOdd
,String::toInt
, - свойства верхнего уровня, члена или свойства-расширения:
List<Int>::size
, - конструктора:
::Regex
К ним относятся привязанные вызываемые ссылки, которые указывают на член конкретного экземпляра:
foo::toString
.- функции верхнего уровня, локальной функции, функции-члена или функции-расширения:
- Используя экземпляр пользовательского класса, который реализует функциональный тип в качестве интерфейса:
class IntTransformer: (Int) -> Int { override operator fun invoke(x: Int): Int = TODO() } val intFunction: (Int) -> Int = IntTransformer()
При достаточной информации компилятор может самостоятельно вывести функциональный тип для переменной.
val a = { i: Int -> i + 1 } // Выведенный тип - (Int) -> Int
Небуквальные (ориг.: non-literal) значения функциональных типов с и без получателя являются взаимозаменяемыми,
поэтому получатель может заменить первый параметр, и наоборот. Например, значение типа (A, B) -> C
может быть передано
или назначено там, где ожидается A.(B) -> C
, и наоборот.
fun main() { val repeatFun: String.(Int) -> String = { times -> this.repeat(times) } val twoParameters: (String, Int) -> String = repeatFun // OK fun runTransformation(f: (String, Int) -> String): String { return f("hello", 3) } val result = runTransformation(repeatFun) // OK println("result = $result") }
Обратите внимание, что функциональный тип без получателя выводится по умолчанию, даже если переменная инициализируется со ссылкой на функцию-расширение. Чтобы это изменить, укажите тип переменной явно.
Вызов экземпляра функционального типа
Значение функционального типа может быть вызвано с помощью оператора invoke(...)
: f.invoke(x)
или просто f(x)
.
Если значение имеет тип получателя, то объект-приёмник должен быть передан в качестве первого аргумента. Другой способ
вызвать значение функционального типа с получателем — это добавить его к объекту-приёмнику, как если бы это была
функция-расширение: 1.foo(2)
.
Пример:
fun main() { val stringPlus: (String, String) -> String = String::plus val intPlus: Int.(Int) -> Int = Int::plus println(stringPlus.invoke("<-", "->")) println(stringPlus("Hello, ", "world!")) println(intPlus.invoke(1, 1)) println(intPlus(1, 2)) println(2.intPlus(3)) // вызывается как функция-расширение }
Встроенные функции
Иногда выгодно улучшить производительность функций высшего порядка, используя встроенные функции (ориг. : inline functions).
Лямбда-выражения и анонимные функции
Лямбда-выражения и анонимные функции — это «функциональный литерал», то есть необъявленная функция, которая немедленно используется в качестве выражения. Рассмотрим следующий пример:
max(strings, { a, b -> a.length < b.length })
Функция max
является функцией высшего порядка, потому что она принимает функцию в качестве второго аргумента.
Этот второй аргумент является выражением, которое в свою очередь есть функция, то есть функциональный литерал.
Как функция он эквивалентен объявлению:
fun compare(a: String, b: String): Boolean = a.length < b.length
Синтаксис лямбда-выражений
Полная синтаксическая форма лямбда-выражений может быть представлена следующим образом:
val sum: (Int, Int) -> Int = { x: Int, y: Int -> x + y }
- Лямбда-выражение всегда заключено в скобки
{...}
; - Объявление параметров при таком синтаксисе происходит внутри этих скобок и может включать в себя аннотации типов;
- Тело функции начинается после знака
->
; - Если тип возвращаемого значения не
Unit
, то в качестве возвращаемого типа принимается последнее (а возможно и единственное) выражение внутри тела лямбды.
Если вы вынесите все необязательные объявления, то, что останется, будет выглядеть следующим образом:
val sum = { x: Int, y: Int -> x + y }
Передача лямбды в качестве последнего параметра
В Kotlin существует соглашение: если последний параметр функции является функцией, то лямбда-выражение, переданное в качестве соответствующего аргумента, может быть вынесено за круглые скобки.
val product = items.fold(1) { acc, e -> acc * e }
Такой синтаксис также известен как trailing lambda.
Когда лямбда-выражение является единственным аргументом функции, круглые скобки могут быть опущены.
run { println("...") }
it: неявное имя единственного параметра
Очень часто лямбда-выражение имеет только один параметр.
Если компилятор способен самостоятельно определить сигнатуру, то объявление параметра можно опустить вместе с ->
.
Параметр будет неявно объявлен под именем it
.
ints. filter { it > 0 } // этот литерал имеет тип '(it: Int) -> Boolean'
Возвращение значения из лямбда-выражения
Вы можете вернуть значение из лямбды явно, используя оператор return. Либо неявно будет возвращено значение последнего выражения.
Таким образом, два следующих фрагмента равнозначны:
ints.filter { val shouldFilter = it > 0 shouldFilter } ints.filter { val shouldFilter = it > 0 return@filter shouldFilter }
Это соглашение, вместе с передачей лямбда-выражения вне скобок, позволяет писать код в стиле LINQ.
strings.filter { it.length == 5 }.sortedBy { it }.map { it.uppercase() }
Символ подчеркивания для неиспользуемых переменных
Если параметр лямбды не используется, то разрешено его имя заменить на символ подчёркивания.
map.forEach { _, value -> println("$value!") }
Деструктуризация в лямбдах
Деструктуризация в лямбдах описана в Деструктурирующие объявления.
Анонимные функции
Единственной особенностью синтаксиса лямбда-выражений, о которой ещё не было сказано, является способность определять и назначать возвращаемый функцией тип. В большинстве случаев в этом нет особой необходимости, потому что он может быть вычислен автоматически. Однако, если у вас есть потребность в определении возвращаемого типа, вы можете воспользоваться альтернативным синтаксисом: анонимной функцией.
fun(x: Int, y: Int): Int = x + y
Объявление анонимной функции выглядит очень похоже на обычное объявление функции, за исключением того, что её имя опущено. Тело такой функции может быть описано и выражением (как показано выше), и блоком.
fun(x: Int, y: Int): Int { return x + y }
Параметры функции и возвращаемый тип обозначаются таким же образом, как в обычных функциях, за исключением того, что тип параметра может быть опущен, если его значение следует из контекста.
ints.filter(fun(item) = item > 0)
Аналогично и с типом возвращаемого значения: он вычисляется автоматически для функций-выражений или же должен быть явно
определён (если не является типом Unit
) для анонимных функций с блоком в качестве тела.
Обратите внимание, что параметры анонимных функций всегда заключены в круглые скобки
(...)
. Приём, позволяющий оставлять параметры вне скобок, работает только с лямбда-выражениями.
Одним из отличий лямбда-выражений от анонимных функций является поведение оператора return
(non-local returns).
Слово return
, не имеющее метки (@
), всегда возвращается из функции, объявленной ключевым словом fun
. Это означает,
что return
внутри лямбда-выражения возвратит выполнение к функции, включающей в себя это лямбда-выражение. Внутри
анонимных функций оператор return
, в свою очередь, выйдет, собственно, из анонимной функции.
Замыкания
Лямбда-выражение или анонимная функция (так же, как и локальная функция или анонимные объекты) имеет доступ к своему замыканию, то есть к переменным, объявленным вне этого выражения или функции. Переменные, захваченные в замыкании, могут быть изменены в лямбде.
var sum = 0 ints.filter { it > 0 }.forEach { sum += it } print(sum)
Литералы функций с объектом-приёмником
Функциональные типы с получателем, такие как A.(B) -> C
, могут быть вызваны с помощью особой
формы – литералов функций с объектом-приёмником.
Как было сказано выше, Kotlin позволяет вызывать экземпляр функционального типа с получателем, предоставляющим объект-приёмник.
Внутри тела литерала объект-приёмник, переданный при вызове функции, становится неявным this
, поэтому вы можете
получить доступ к членам этого объекта-приёмника без каких-либо дополнительных определителей, а обращение к самому
объекту-приёмнику осуществляется с помощью выражения this
.
Это схоже с принципом работы функций-расширений, которые позволяют получить доступ к членам объекта-приёмника внутри тела функции.
Ниже приведён пример литерала с получателем вместе с его типом, где plus
вызывается для объекта-приёмника:
val sum: Int. (Int) -> Int = { other -> plus(other) }
Синтаксис анонимной функции позволяет вам явно указать тип приёмника. Это может быть полезно в случае, если вам нужно объявить переменную типа нашей функции для использования в дальнейшем.
val sum = fun Int.(other: Int): Int = this + other
Лямбда-выражения могут быть использованы как литералы функций с приёмником, когда тип приёмника может быть выведен из контекста. Один из самых важных примеров их использования это типобезопасные строители (ориг.: type-safe builders).
class HTML { fun body() { ... } } fun html(init: HTML.() -> Unit): HTML { val html = HTML() // создание объекта-приёмника html.init() // передача приёмника в лямбду return html } html { // лямбда с приёмником начинается тут body() // вызов метода объекта-приёмника }
Как вызвать функцию JavaScript в HTML
Джейми Джувилер
Обновлено:
Опубликовано:
Как только вы хорошо разберетесь в HTML и CSS, следующим шагом будет JavaScript, чтобы вывести ваши веб-проекты на новый уровень. JavaScript добавляет к веб-страницам интерактивные и динамические функции — это то, что делает их интересными и привлекательными, помимо статического контента.
Если вы хотите создать интерактивную страницу, одним из основных навыков, которые вам необходимо знать, является вызов функций JavaScript из HTML-документа. Другими словами, когда пользователь каким-то образом взаимодействует с элементом HTML (например, щелкает по нему), как вы можете вызвать в ответ функцию JavaScript?
К счастью, HTML и JavaScript упрощают эту задачу. В этом посте вы узнаете три метода вызова функции JavaScript с помощью HTML. Давайте углубимся.
Как вызвать функцию JavaScript в HTML
В этом разделе мы рассмотрим три способа вызова функции с помощью HTML
- с тегами сценария
- с внешним файлом JavaScript
- с прослушивателями событий
Использовать теги сценария
Самый простой способ вызвать функцию JavaScript в документе HTML — определить функцию внутри пары тегов голова> <тело> <дел>дел> тело>