В проектах Vue мы часто сталкиваемсяv-if,v-show,v-forилиv-modelЭти встроенные директивы предоставляют нам различные функции. Помимо использования этих встроенных директив, Vue также позволяет регистрировать пользовательские директивы. Далее Abaoge будет использовать официальную документацию Vue 3.пользовательская директиваПримеры, использованные в главах, шаг за шагом раскрывают секреты пользовательских директив.
Следуйте «Дорога полного стека», чтобы прочитать 4 бесплатные электронные книги (всего более 30 000 загрузок) и 9 руководств по Vue 3 для продвинутых пользователей.
1. Пользовательские команды
1. Зарегистрируйте глобальную пользовательскую команду
const app = Vue.createApp({})
// 注册一个全局自定义指令 v-focus
app.directive('focus', {
// 当被绑定的元素挂载到 DOM 中时被调用
mounted(el) {
// 聚焦元素
el.focus()
}
})
2. Используйте глобальные пользовательские директивы
<div id="app">
<input v-focus />
</div>
3. Полный пример использования
<div id="app">
<input v-focus />
</div>
<script>
const { createApp } = Vue
const app = Vue.createApp({}) // ①
app.directive('focus', { // ②
// 当被绑定的元素挂载到 DOM 中时被调用
mounted(el) {
el.focus() // 聚焦元素
}
})
app.mount('#app') // ③
</script>
Когда страница загружается, элемент поля ввода на странице автоматически получает фокус. Код этого примера относительно прост и в основном состоит из 3 шагов:Создавайте объекты приложений, регистрируйте глобальные пользовательские директивы и монтируйте приложения.. Среди них подробности создания объекта App будут представлены отдельно в последующих статьях, а мы сосредоточимся на двух других шагах ниже. Сначала разберем процесс регистрации глобальных пользовательских инструкций.
Во-вторых, процесс регистрации глобальных пользовательских инструкций
В приведенном выше примере мы используемappобъектdirectiveметод для регистрации глобальной пользовательской директивы:
app.directive('focus', {
// 当被绑定的元素挂载到 DOM 中时被调用
mounted(el) {
el.focus() // 聚焦元素
}
})
Конечно, в дополнение к регистрации глобальных пользовательских директив мы также можем зарегистрировать локальные директивы, поскольку компонент также принимаетdirectivesОпции:
directives: {
focus: {
mounted(el) {
el.focus()
}
}
}
Для приведенного выше примера мы использовалиapp.directiveметод определен вruntime-core/src/apiCreateApp.tsВ файле:
// packages/runtime-core/src/apiCreateApp.ts
export function createAppAPI<HostElement>(
render: RootRenderFunction,
hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
return function createApp(rootComponent, rootProps = null) {
const context = createAppContext()
let isMounted = false
const app: App = (context.app = {
// 省略部分代码
_context: context,
// 用于注册或检索全局指令。
directive(name: string, directive?: Directive) {
if (__DEV__) {
validateDirectiveName(name)
}
if (!directive) {
return context.directives[name] as any
}
if (__DEV__ && context.directives[name]) {
warn(`Directive "${name}" has already been registered in target app.`)
}
context.directives[name] = directive
return app
},
return app
}
}
Наблюдая за приведенным выше кодом, мы можем узнатьdirectiveМетод поддерживает следующие два параметра:
- имя: указывает имя инструкции;
- директива (необязательно): указывает определение директивы.
Параметр имени относительно прост, поэтому сосредоточимся на анализеdirectiveпараметр, тип параметраDirectiveТипы:
// packages/runtime-core/src/directives.ts
export type Directive<T = any, V = any> =
| ObjectDirective<T, V>
| FunctionDirective<T, V>
Это видно из вышеизложенногоDirectiveТип является типом объединения, поэтому нам нужно продолжить анализObjectDirectiveа такжеFunctionDirectiveТипы. Здесь мы сначала рассмотримObjectDirectiveОпределение типа:
// packages/runtime-core/src/directives.ts
export interface ObjectDirective<T = any, V = any> {
created?: DirectiveHook<T, null, V>
beforeMount?: DirectiveHook<T, null, V>
mounted?: DirectiveHook<T, null, V>
beforeUpdate?: DirectiveHook<T, VNode<any, T>, V>
updated?: DirectiveHook<T, VNode<any, T>, V>
beforeUnmount?: DirectiveHook<T, null, V>
unmounted?: DirectiveHook<T, null, V>
getSSRProps?: SSRDirectiveHook
}
Этот тип определяет директиву типа объекта, и каждое свойство объекта представляет собой крючок на жизненном цикле директивы. а такжеFunctionDirectiveТип представляет собой инструкцию функционального типа:
// packages/runtime-core/src/directives.ts
export type FunctionDirective<T = any, V = any> = DirectiveHook<T, any, V>
export type DirectiveHook<T = any, Prev = VNode<any, T> | null, V = any> = (
el: T,
binding: DirectiveBinding<V>,
vnode: VNode<any, T>,
prevVNode: Prev
) => void
ВведениеDirectiveТипа, давайте еще раз просмотрим предыдущий пример, думаю вам будет намного понятнее:
app.directive('focus', {
// 当被绑定的元素挂载到 DOM 中时触发
mounted(el) {
el.focus() // 聚焦元素
}
})
В приведенном выше примере, когда мы вызываемapp.directiveпользовательский способ регистрацииfocusкомандой выполняется следующая логика:
directive(name: string, directive?: Directive) {
if (__DEV__) { // 避免自定义指令名称,与已有的内置指令名称冲突
validateDirectiveName(name)
}
if (!directive) { // 获取name对应的指令对象
return context.directives[name] as any
}
if (__DEV__ && context.directives[name]) {
warn(`Directive "${name}" has already been registered in target app.`)
}
context.directives[name] = directive // 注册全局指令
return app
}
когдаfocusПосле того, как инструкция будет успешно зарегистрирована, инструкция будет сохранена вcontextобъектdirectivesсвойства, как показано на следующем рисунке:
Как подсказывает названиеcontextобъект контекста представляет приложение, так как же создается объект? На самом деле объект проходит черезcreateAppContextфункция для создания:
const context = createAppContext()
а такжеcreateAppContextфункция определена вruntime-core/src/apiCreateApp.tsВ файле:
// packages/runtime-core/src/apiCreateApp.ts
export function createAppContext(): AppContext {
return {
app: null as any,
config: {
isNativeTag: NO,
performance: false,
globalProperties: {},
optionMergeStrategies: {},
isCustomElement: NO,
errorHandler: undefined,
warnHandler: undefined
},
mixins: [],
components: {},
directives: {},
provides: Object.create(null)
}
}
Видя это, вы думаете, что внутренняя логика обработки регистрации глобальных пользовательских инструкций на самом деле довольно проста. затем для зарегистрированныхfocusкоманда, когда она будет вызвана? Чтобы ответить на этот вопрос, нам нужно проанализировать еще один шаг —монтирование приложения.
3. Процесс установки приложения
Чтобы понять процесс установки приложения более интуитивно, Brother Abao используетИнструменты разработчика Chrome, который фиксирует основной процесс монтирования приложения:
Из приведенного выше рисунка мы можем узнать основной процесс, через который проходит приложение во время монтирования. Кроме того, на рисунке мы также нашли функцию, связанную с инструкциейresolveDirective. Очевидно, что эта функция используется для разбора инструкций, и функция используется вrenderметод будет вызван. В исходном коде мы находим определение этой функции:
// packages/runtime-core/src/helpers/resolveAssets.ts
const DIRECTIVES = 'directives'
export function resolveDirective(name: string): Directive | undefined {
return resolveAsset(DIRECTIVES, name)
}
Как видно из приведенного выше кода, вresolveDirectiveВнутри функции он будет продолжать вызыватьresolveAsset函数来执行具体的解析操作。 В анализеresolveAssetПеред конкретной реализацией функции мыresolveDirectiveДобавьте точку останова внутри функции, чтобы посмотретьrender«Аромат» метода:
На изображении выше мы видим, что сfocusсвязанные с инструкцией_resolveDirective("focus")вызов функции. Мы уже знали до этого resolveDirectiveФункция будет продолжать вызыватьresolveAssetфункция, конкретная реализация этой функции выглядит следующим образом:
// packages/runtime-core/src/helpers/resolveAssets.ts
function resolveAsset(
type: typeof COMPONENTS | typeof DIRECTIVES,
name: string,
warnMissing = true
) {
const instance = currentRenderingInstance || currentInstance
if (instance) {
const Component = instance.type
// 省略解析组件的处理逻辑
const res =
// 局部注册
resolve(instance[type] || (Component as ComponentOptions)[type], name) ||
// 全局注册
resolve(instance.appContext[type], name)
return res
} else if (__DEV__) {
warn(
`resolve${capitalize(type.slice(0, -1))} ` +
`can only be used in render() or setup().`
)
}
}
из-за регистрацииfocusПри использовании инструкции используется метод глобальной регистрации, поэтому будет выполняться процесс синтаксического анализа.resolve(instance.appContext[type], name)заявление, гдеresolveМетоды определяются следующим образом:
// packages/runtime-core/src/helpers/resolveAssets.ts
function resolve(registry: Record<string, any> | undefined, name: string) {
return (
registry &&
(registry[name] ||
registry[camelize(name)] ||
registry[capitalize(camelize(name))])
)
}
Проанализировав приведенный выше поток обработки, мы знаем, что при разборе глобально зарегистрированных инструкций он пройдетresolveФункция получает зарегистрированный объект инструкции из объекта контекста приложения. в получении_directive_focusПосле объекта инструкцииrenderМетод будет продолжать вызывать_withDirectivesфункция добавления инструкций кVNodeобъект, функция определена вruntime-core/src/directives.tsВ файле:
// packages/runtime-core/src/directives.ts
export function withDirectives<T extends VNode>(
vnode: T,
directives: DirectiveArguments
): T {
const internalInstance = currentRenderingInstance // 获取当前渲染的实例
const instance = internalInstance.proxy
const bindings: DirectiveBinding[] = vnode.dirs || (vnode.dirs = [])
for (let i = 0; i < directives.length; i++) {
let [dir, value, arg, modifiers = EMPTY_OBJ] = directives[i]
// 在 mounted 和 updated 时,触发相同行为,而不关系其他的钩子函数
if (isFunction(dir)) { // 处理函数类型指令
dir = {
mounted: dir,
updated: dir
} as ObjectDirective
}
bindings.push({
dir,
instance,
value,
oldValue: void 0,
arg,
modifiers
})
}
return vnode
}
Поскольку к узлу может применяться несколько инструкций,withDirectivesфункционировать вVNodeобъект определяетdirsсвойство, а значение свойства представляет собой массив. В предыдущем примере при вызовеwithDirectivesПосле функции,VNodeобъект добавитdirsсвойства, как показано на следующем рисунке:
Благодаря приведенному выше анализу мы уже знаем, что в компонентеrenderметод, мы пройдемwithDirectivesФункция регистрирует инструкцию с соответствующимVNodeна объекте. ТакfocusКогда будет вызван хук, определенный в инструкции? Прежде чем продолжить анализ, давайте сначала представим функции ловушек, поддерживаемые объектом инструкции.
Объект определения директивы может предоставлять следующие функции ловушек (все необязательные):
-
created: Вызывается перед применением свойства или прослушивателя событий связанного элемента. -
beforeMount: вызывается, когда директива впервые привязывается к элементу и до монтирования родительского компонента. -
mounted: вызывается после монтирования родительского компонента связанного элемента. -
beforeUpdate: вызывается перед обновлением виртуального узла, содержащего компонент. -
updated: Вызывается после обновления VNode, содержащего компонент и VNode его подкомпонентов. -
beforeUnmount: вызывается перед выгрузкой родительского компонента связанного элемента. -
unmounted: вызывается только один раз, когда директива не привязана к элементу, а родительский компонент размонтирован.
После представления этих функций-ловушек давайте рассмотрим ранее представленныеObjectDirectiveТипы:
// packages/runtime-core/src/directives.ts
export interface ObjectDirective<T = any, V = any> {
created?: DirectiveHook<T, null, V>
beforeMount?: DirectiveHook<T, null, V>
mounted?: DirectiveHook<T, null, V>
beforeUpdate?: DirectiveHook<T, VNode<any, T>, V>
updated?: DirectiveHook<T, VNode<any, T>, V>
beforeUnmount?: DirectiveHook<T, null, V>
unmounted?: DirectiveHook<T, null, V>
getSSRProps?: SSRDirectiveHook
}
Хорошо, давайте проанализируем это дальшеfocusКогда вызываются хуки, определенные в директиве. Точно так же брат Абао находится вfocusДирективаmountedДобавьте точку останова в метод:
В стеке вызовов в правой части диаграммы мы видимinvokeDirectiveHookфункции, очевидно, что функция этой функции состоит в том, чтобы вызвать зарегистрированный хук в инструкции. Из-за нехватки места Brother Abao не будет вводить конкретные детали. Заинтересованные партнеры могут самостоятельно отлаживать точки останова.
4. Брату А Бао есть что сказать
4.1 Каковы встроенные директивы в Vue 3?
Во время введения в регистрацию глобальных пользовательских директив мы увиделиvalidateDirectiveNameФункция используется для проверки имени пользовательской директивы, чтобы избежать конфликта имени пользовательского директива с существующим встроенным названием директива.
// packages/runtime-core/src/directives.ts
export function validateDirectiveName(name: string) {
if (isBuiltInDirective(name)) {
warn('Do not use built-in directive ids as custom directive id: ' + name)
}
}
существуетvalidateDirectiveNameВнутри функции он пройдетisBuiltInDirective(name)оператор, чтобы определить, является ли это встроенной директивой:
const isBuiltInDirective = /*#__PURE__*/ makeMap(
'bind,cloak,else-if,else,for,html,if,model,on,once,pre,show,slot,text'
)
в приведенном выше кодеmakeMapФункция для создания объекта карты (Object.create(null)) и возврата функции для проверки существования ключа в объекте карты. Кроме того, благодаря приведенному выше коду мы можем четко понять, какие встроенные директивы предоставляются нам в Vue 3.
4.2 Сколько существует типов команд?
В Vue 3 директивы делятся наObjectDirectiveа такжеFunctionDirectiveДва типа:
// packages/runtime-core/src/directives.ts
export type Directive<T = any, V = any> =
| ObjectDirective<T, V>
| FunctionDirective<T, V>
ObjectDirective
export interface ObjectDirective<T = any, V = any> {
created?: DirectiveHook<T, null, V>
beforeMount?: DirectiveHook<T, null, V>
mounted?: DirectiveHook<T, null, V>
beforeUpdate?: DirectiveHook<T, VNode<any, T>, V>
updated?: DirectiveHook<T, VNode<any, T>, V>
beforeUnmount?: DirectiveHook<T, null, V>
unmounted?: DirectiveHook<T, null, V>
getSSRProps?: SSRDirectiveHook
}
FunctionDirective
export type FunctionDirective<T = any, V = any> = DirectiveHook<T, any, V>
export type DirectiveHook<T = any, Prev = VNode<any, T> | null, V = any> = (
el: T,
binding: DirectiveBinding<V>,
vnode: VNode<any, T>,
prevVNode: Prev
) => void
если вы хотитеmountedа такжеupdatedзапускает одно и то же поведение, независимо от других функций ловушек. Затем вы можете сделать это, передав функцию обратного вызова директиве:
app.directive('pin', (el, binding) => {
el.style.position = 'fixed'
const s = binding.arg || 'top'
el.style[s] = binding.value + 'px'
})
4.3 В чем разница между регистрацией глобальной директивы и локальной директивы?
Зарегистрировать глобальную директиву
app.directive('focus', {
// 当被绑定的元素挂载到 DOM 中时被调用
mounted(el) {
el.focus() // 聚焦元素
}
});
зарегистрировать местную директиву
const Component = defineComponent({
directives: {
focus: {
mounted(el) {
el.focus()
}
}
},
render() {
const { directives } = this.$options;
return [withDirectives(h('input'), [[directives.focus, ]])]
}
});
Разбирать директивы для глобальной регистрации и локальной регистрации
// packages/runtime-core/src/helpers/resolveAssets.ts
function resolveAsset(
type: typeof COMPONENTS | typeof DIRECTIVES,
name: string,
warnMissing = true
) {
const instance = currentRenderingInstance || currentInstance
if (instance) {
const Component = instance.type
// 省略解析组件的处理逻辑
const res =
// 局部注册
resolve(instance[type] || (Component as ComponentOptions)[type], name) ||
// 全局注册
resolve(instance.appContext[type], name)
return res
}
}
4.4 В чем разница между функциями рендеринга, созданными встроенными директивами, и пользовательскими директивами?
Чтобы понять разницу между функциями рендеринга, созданными встроенными инструкциями, и пользовательскими инструкциями, Brother Abao используетv-if,v-showвстроенные директивы иv-focusВозьмите пользовательскую директиву в качестве примера, затем используйтеVue 3 Template ExplorerЭтот онлайн-инструмент для компиляции и генерации функций рендеринга:
встроенная директива v-if
<input v-if="isShow" />
const _Vue = Vue
return function render(_ctx, _cache, $props, $setup, $data, $options) {
with (_ctx) {
const { createVNode: _createVNode, openBlock: _openBlock,
createBlock: _createBlock, createCommentVNode: _createCommentVNode } = _Vue
return isShow
? (_openBlock(), _createBlock("input", { key: 0 }))
: _createCommentVNode("v-if", true)
}
}
дляv-ifС точки зрения инструкции, после компиляции она будет пропущена через?:Тернарный оператор для достижения функции динамического создания узлов.
Построенная инструкция V-Show
<input v-show="isShow" />
const _Vue = Vue
return function render(_ctx, _cache, $props, $setup, $data, $options) {
with (_ctx) {
const { vShow: _vShow, createVNode: _createVNode, withDirectives: _withDirectives,
openBlock: _openBlock, createBlock: _createBlock } = _Vue
return _withDirectives((_openBlock(), _createBlock("input", null, null, 512 /* NEED_PATCH */)), [
[_vShow, isShow]
])
}
}
В приведенном выше примереvShowдиректива определена вpackages/runtime-dom/src/directives/vShow.tsфайл, директива принадлежитObjectDirectiveтип директивы, которая внутренне определяетbeforeMount,mounted,updatedа такжеbeforeUnmountЧетыре крючка.
пользовательская директива v-focus
<input v-focus />
const _Vue = Vue
return function render(_ctx, _cache, $props, $setup, $data, $options) {
with (_ctx) {
const { resolveDirective: _resolveDirective, createVNode: _createVNode,
withDirectives: _withDirectives, openBlock: _openBlock, createBlock: _createBlock } = _Vue
const _directive_focus = _resolveDirective("focus")
return _withDirectives((_openBlock(), _createBlock("input", null, null, 512 /* NEED_PATCH */)), [
[_directive_focus]
])
}
}
по сравнениюv-focusа такжеv-showФункция рендеринга, сгенерированная инструкцией, нам известнаv-focusпользовательская директива сv-showВстроенные директивы будут проходитьwithDirectivesфункцию, зарегистрируйте инструкцию дляVNodeна объекте. По сравнению со встроенными инструкциями, пользовательские инструкции имеют еще один процесс разбора инструкций.
Кроме того, если вinputэлемент, применяяv-showа такжеv-focusинструкция, при звонке_withDirectivesбудет использоваться двумерный массив:
<input v-show="isShow" v-focus />
const _Vue = Vue
return function render(_ctx, _cache, $props, $setup, $data, $options) {
with (_ctx) {
const { vShow: _vShow, resolveDirective: _resolveDirective, createVNode: _createVNode,
withDirectives: _withDirectives, openBlock: _openBlock, createBlock: _createBlock } = _Vue
const _directive_focus = _resolveDirective("focus")
return _withDirectives((_openBlock(), _createBlock("input", null, null, 512 /* NEED_PATCH */)), [
[_vShow, isShow],
[_directive_focus]
])
}
}
4.5 Как применять директивы в функциях рендеринга?
В дополнение к применению директив в шаблонах используйте ранее описанныйwithDirectivesфункцию, мы можем легко применить указанные инструкции в функции рендеринга:
<div id="app"></div>
<script>
const { createApp, h, vShow, defineComponent, withDirectives } = Vue
const Component = defineComponent({
data() {
return { value: true }
},
render() {
return [withDirectives(h('div', '我是阿宝哥'), [[vShow, this.value]])]
}
});
const app = Vue.createApp(Component)
app.mount('#app')
</script>
В этой статье в основном рассказывается, как настраивать директивы и как регистрировать глобальные и локальные директивы в Vue 3. Чтобы каждый мог глубже понять соответствующие знания о пользовательских инструкциях, брат Абао также проанализировал процесс регистрации и применения инструкций с точки зрения исходного кода. В последующих статьях брат Абао представит некоторые специальные инструкции, и, конечно, он также сосредоточится на принципе двусторонней привязки.Заинтересованные друзья не должны пропустить это.
Следуйте «Дорога полного стека», чтобы прочитать 4 бесплатные электронные книги (всего более 30 000 загрузок) и 9 руководств по Vue 3 для продвинутых пользователей.Друзья, которые хотят вместе изучать Vue 3.0, могут добавить Abaoge WeChat —— semlinker.