perf: improve directive runtime performance
This commit is contained in:
parent
6c7787db7b
commit
07ce2c5fa7
@ -12,7 +12,7 @@ return withDirectives(h(comp), [
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { VNode } from './vnode'
|
import { VNode } from './vnode'
|
||||||
import { isFunction, EMPTY_OBJ, makeMap } from '@vue/shared'
|
import { isFunction, EMPTY_OBJ, makeMap, EMPTY_ARR } from '@vue/shared'
|
||||||
import { warn } from './warning'
|
import { warn } from './warning'
|
||||||
import { ComponentInternalInstance } from './component'
|
import { ComponentInternalInstance } from './component'
|
||||||
import { currentRenderingInstance } from './componentRenderUtils'
|
import { currentRenderingInstance } from './componentRenderUtils'
|
||||||
@ -25,6 +25,7 @@ export interface DirectiveBinding {
|
|||||||
oldValue: any
|
oldValue: any
|
||||||
arg?: string
|
arg?: string
|
||||||
modifiers: DirectiveModifiers
|
modifiers: DirectiveModifiers
|
||||||
|
dir: ObjectDirective
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DirectiveHook<T = any> = (
|
export type DirectiveHook<T = any> = (
|
||||||
@ -47,9 +48,13 @@ export type FunctionDirective<T = any> = DirectiveHook<T>
|
|||||||
|
|
||||||
export type Directive<T = any> = ObjectDirective<T> | FunctionDirective<T>
|
export type Directive<T = any> = ObjectDirective<T> | FunctionDirective<T>
|
||||||
|
|
||||||
type DirectiveModifiers = Record<string, boolean>
|
export type DirectiveModifiers = Record<string, boolean>
|
||||||
|
|
||||||
const valueCache = new WeakMap<Directive, WeakMap<any, any>>()
|
export type VNodeDirectiveData = [
|
||||||
|
unknown,
|
||||||
|
string | undefined,
|
||||||
|
DirectiveModifiers
|
||||||
|
]
|
||||||
|
|
||||||
const isBuiltInDirective = /*#__PURE__*/ makeMap(
|
const isBuiltInDirective = /*#__PURE__*/ makeMap(
|
||||||
'bind,cloak,else-if,else,for,html,if,model,on,once,pre,show,slot,text'
|
'bind,cloak,else-if,else,for,html,if,model,on,once,pre,show,slot,text'
|
||||||
@ -61,56 +66,35 @@ export function validateDirectiveName(name: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyDirective(
|
const directiveToVnodeHooksMap = /*#__PURE__*/ [
|
||||||
props: Record<any, any>,
|
'beforeMount',
|
||||||
instance: ComponentInternalInstance,
|
'mounted',
|
||||||
directive: Directive,
|
'beforeUpdate',
|
||||||
value?: unknown,
|
'updated',
|
||||||
arg?: string,
|
'beforeUnmount',
|
||||||
modifiers: DirectiveModifiers = EMPTY_OBJ
|
'unmounted'
|
||||||
) {
|
].reduce(
|
||||||
let valueCacheForDir = valueCache.get(directive)!
|
(map, key: keyof ObjectDirective) => {
|
||||||
if (!valueCacheForDir) {
|
const vnodeKey = `onVnode` + key[0].toUpperCase() + key.slice(1)
|
||||||
valueCacheForDir = new WeakMap<VNode, any>()
|
const vnodeHook = (vnode: VNode, prevVnode: VNode | null) => {
|
||||||
valueCache.set(directive, valueCacheForDir)
|
const bindings = vnode.dirs!
|
||||||
|
const prevBindings = prevVnode ? prevVnode.dirs! : EMPTY_ARR
|
||||||
|
for (let i = 0; i < bindings.length; i++) {
|
||||||
|
const binding = bindings[i]
|
||||||
|
const hook = binding.dir[key]
|
||||||
|
if (hook != null) {
|
||||||
|
if (prevVnode != null) {
|
||||||
|
binding.oldValue = prevBindings[i].value
|
||||||
}
|
}
|
||||||
|
hook(vnode.el, binding, vnode, prevVnode)
|
||||||
if (isFunction(directive)) {
|
|
||||||
directive = {
|
|
||||||
mounted: directive,
|
|
||||||
updated: directive
|
|
||||||
} as ObjectDirective
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const key in directive) {
|
|
||||||
const hook = directive[key as keyof ObjectDirective]!
|
|
||||||
const hookKey = `onVnode` + key[0].toUpperCase() + key.slice(1)
|
|
||||||
const vnodeHook = (vnode: VNode, prevVNode: VNode | null) => {
|
|
||||||
let oldValue
|
|
||||||
if (prevVNode != null) {
|
|
||||||
oldValue = valueCacheForDir.get(prevVNode)
|
|
||||||
valueCacheForDir.delete(prevVNode)
|
|
||||||
}
|
}
|
||||||
valueCacheForDir.set(vnode, value)
|
}
|
||||||
hook(
|
map[key] = [vnodeKey, vnodeHook]
|
||||||
vnode.el,
|
return map
|
||||||
{
|
|
||||||
instance: instance.renderProxy,
|
|
||||||
value,
|
|
||||||
oldValue,
|
|
||||||
arg,
|
|
||||||
modifiers
|
|
||||||
},
|
},
|
||||||
vnode,
|
{} as Record<string, [string, Function]>
|
||||||
prevVNode
|
|
||||||
)
|
)
|
||||||
}
|
|
||||||
const existing = props[hookKey]
|
|
||||||
props[hookKey] = existing
|
|
||||||
? [].concat(existing, vnodeHook as any)
|
|
||||||
: vnodeHook
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Directive, value, argument, modifiers
|
// Directive, value, argument, modifiers
|
||||||
export type DirectiveArguments = Array<
|
export type DirectiveArguments = Array<
|
||||||
@ -121,15 +105,40 @@ export type DirectiveArguments = Array<
|
|||||||
>
|
>
|
||||||
|
|
||||||
export function withDirectives(vnode: VNode, directives: DirectiveArguments) {
|
export function withDirectives(vnode: VNode, directives: DirectiveArguments) {
|
||||||
const instance = currentRenderingInstance
|
const internalInstance = currentRenderingInstance
|
||||||
if (instance !== null) {
|
if (internalInstance === null) {
|
||||||
vnode.props = vnode.props || {}
|
__DEV__ && warn(`withDirectives can only be used inside render functions.`)
|
||||||
for (let i = 0; i < directives.length; i++) {
|
return
|
||||||
const [dir, value, arg, modifiers] = directives[i]
|
}
|
||||||
applyDirective(vnode.props, instance, dir, value, arg, modifiers)
|
const instance = internalInstance.renderProxy
|
||||||
|
const props = vnode.props || (vnode.props = {})
|
||||||
|
const bindings = vnode.dirs || (vnode.dirs = new Array(directives.length))
|
||||||
|
const injected: Record<string, true> = {}
|
||||||
|
for (let i = 0; i < directives.length; i++) {
|
||||||
|
let [dir, value, arg, modifiers = EMPTY_OBJ] = directives[i]
|
||||||
|
if (isFunction(dir)) {
|
||||||
|
dir = {
|
||||||
|
mounted: dir,
|
||||||
|
updated: dir
|
||||||
|
} as ObjectDirective
|
||||||
|
}
|
||||||
|
bindings[i] = {
|
||||||
|
dir,
|
||||||
|
instance,
|
||||||
|
value,
|
||||||
|
oldValue: void 0,
|
||||||
|
arg,
|
||||||
|
modifiers
|
||||||
|
}
|
||||||
|
// inject onVnodeXXX hooks
|
||||||
|
for (const key in dir) {
|
||||||
|
if (!injected[key]) {
|
||||||
|
const { 0: hookName, 1: hook } = directiveToVnodeHooksMap[key]
|
||||||
|
const existing = props[hookName]
|
||||||
|
props[hookName] = existing ? [].concat(existing, hook as any) : hook
|
||||||
|
injected[key] = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (__DEV__) {
|
|
||||||
warn(`withDirectives can only be used inside render functions.`)
|
|
||||||
}
|
}
|
||||||
return vnode
|
return vnode
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@ import { ShapeFlags } from './shapeFlags'
|
|||||||
import { isReactive } from '@vue/reactivity'
|
import { isReactive } from '@vue/reactivity'
|
||||||
import { AppContext } from './apiApp'
|
import { AppContext } from './apiApp'
|
||||||
import { SuspenseBoundary } from './suspense'
|
import { SuspenseBoundary } from './suspense'
|
||||||
|
import { DirectiveBinding } from './directives'
|
||||||
|
|
||||||
export const Fragment = Symbol(__DEV__ ? 'Fragment' : undefined)
|
export const Fragment = Symbol(__DEV__ ? 'Fragment' : undefined)
|
||||||
export const Portal = Symbol(__DEV__ ? 'Portal' : undefined)
|
export const Portal = Symbol(__DEV__ ? 'Portal' : undefined)
|
||||||
@ -66,6 +67,7 @@ export interface VNode<HostNode = any, HostElement = any> {
|
|||||||
children: NormalizedChildren<HostNode, HostElement>
|
children: NormalizedChildren<HostNode, HostElement>
|
||||||
component: ComponentInternalInstance | null
|
component: ComponentInternalInstance | null
|
||||||
suspense: SuspenseBoundary<HostNode, HostElement> | null
|
suspense: SuspenseBoundary<HostNode, HostElement> | null
|
||||||
|
dirs: DirectiveBinding[] | null
|
||||||
|
|
||||||
// DOM
|
// DOM
|
||||||
el: HostNode | null
|
el: HostNode | null
|
||||||
@ -200,6 +202,7 @@ export function createVNode(
|
|||||||
children: null,
|
children: null,
|
||||||
component: null,
|
component: null,
|
||||||
suspense: null,
|
suspense: null,
|
||||||
|
dirs: null,
|
||||||
el: null,
|
el: null,
|
||||||
anchor: null,
|
anchor: null,
|
||||||
target: null,
|
target: null,
|
||||||
@ -247,6 +250,7 @@ export function cloneVNode(vnode: VNode, extraProps?: Data): VNode {
|
|||||||
dynamicProps: vnode.dynamicProps,
|
dynamicProps: vnode.dynamicProps,
|
||||||
dynamicChildren: vnode.dynamicChildren,
|
dynamicChildren: vnode.dynamicChildren,
|
||||||
appContext: vnode.appContext,
|
appContext: vnode.appContext,
|
||||||
|
dirs: vnode.dirs,
|
||||||
|
|
||||||
// these should be set to null since they should only be present on
|
// these should be set to null since they should only be present on
|
||||||
// mounted VNodes. If they are somehow not null, this means we have
|
// mounted VNodes. If they are somehow not null, this means we have
|
||||||
|
@ -66,7 +66,10 @@ export const vModelText: ObjectDirective<
|
|||||||
addEventListener(el, 'change', onCompositionEnd)
|
addEventListener(el, 'change', onCompositionEnd)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeUpdate(el, { value, modifiers: { trim, number } }) {
|
beforeUpdate(el, { value, oldValue, modifiers: { trim, number } }) {
|
||||||
|
if (value === oldValue) {
|
||||||
|
return
|
||||||
|
}
|
||||||
if (document.activeElement === el) {
|
if (document.activeElement === el) {
|
||||||
if (trim && el.value.trim() === value) {
|
if (trim && el.value.trim() === value) {
|
||||||
return
|
return
|
||||||
@ -107,15 +110,17 @@ export const vModelCheckbox: ObjectDirective<HTMLInputElement> = {
|
|||||||
|
|
||||||
function setChecked(
|
function setChecked(
|
||||||
el: HTMLInputElement,
|
el: HTMLInputElement,
|
||||||
{ value }: DirectiveBinding,
|
{ value, oldValue }: DirectiveBinding,
|
||||||
vnode: VNode
|
vnode: VNode
|
||||||
) {
|
) {
|
||||||
// store the v-model value on the element so it can be accessed by the
|
// store the v-model value on the element so it can be accessed by the
|
||||||
// change listener.
|
// change listener.
|
||||||
;(el as any)._modelValue = value
|
;(el as any)._modelValue = value
|
||||||
el.checked = isArray(value)
|
if (isArray(value)) {
|
||||||
? looseIndexOf(value, vnode.props!.value) > -1
|
el.checked = looseIndexOf(value, vnode.props!.value) > -1
|
||||||
: !!value
|
} else if (value !== oldValue) {
|
||||||
|
el.checked = !!value
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const vModelRadio: ObjectDirective<HTMLInputElement> = {
|
export const vModelRadio: ObjectDirective<HTMLInputElement> = {
|
||||||
@ -126,10 +131,12 @@ export const vModelRadio: ObjectDirective<HTMLInputElement> = {
|
|||||||
assign(getValue(el))
|
assign(getValue(el))
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
beforeUpdate(el, { value }, vnode) {
|
beforeUpdate(el, { value, oldValue }, vnode) {
|
||||||
|
if (value !== oldValue) {
|
||||||
el.checked = looseEqual(value, vnode.props!.value)
|
el.checked = looseEqual(value, vnode.props!.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const vModelSelect: ObjectDirective<HTMLSelectElement> = {
|
export const vModelSelect: ObjectDirective<HTMLSelectElement> = {
|
||||||
// use mounted & updated because <select> relies on its children <option>s.
|
// use mounted & updated because <select> relies on its children <option>s.
|
||||||
|
Loading…
Reference in New Issue
Block a user