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 { isFunction, EMPTY_OBJ, makeMap } from '@vue/shared'
|
||||
import { isFunction, EMPTY_OBJ, makeMap, EMPTY_ARR } from '@vue/shared'
|
||||
import { warn } from './warning'
|
||||
import { ComponentInternalInstance } from './component'
|
||||
import { currentRenderingInstance } from './componentRenderUtils'
|
||||
@ -25,6 +25,7 @@ export interface DirectiveBinding {
|
||||
oldValue: any
|
||||
arg?: string
|
||||
modifiers: DirectiveModifiers
|
||||
dir: ObjectDirective
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
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(
|
||||
'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(
|
||||
props: Record<any, any>,
|
||||
instance: ComponentInternalInstance,
|
||||
directive: Directive,
|
||||
value?: unknown,
|
||||
arg?: string,
|
||||
modifiers: DirectiveModifiers = EMPTY_OBJ
|
||||
) {
|
||||
let valueCacheForDir = valueCache.get(directive)!
|
||||
if (!valueCacheForDir) {
|
||||
valueCacheForDir = new WeakMap<VNode, any>()
|
||||
valueCache.set(directive, valueCacheForDir)
|
||||
}
|
||||
|
||||
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)
|
||||
const directiveToVnodeHooksMap = /*#__PURE__*/ [
|
||||
'beforeMount',
|
||||
'mounted',
|
||||
'beforeUpdate',
|
||||
'updated',
|
||||
'beforeUnmount',
|
||||
'unmounted'
|
||||
].reduce(
|
||||
(map, key: keyof ObjectDirective) => {
|
||||
const vnodeKey = `onVnode` + key[0].toUpperCase() + key.slice(1)
|
||||
const vnodeHook = (vnode: VNode, prevVnode: VNode | null) => {
|
||||
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)
|
||||
}
|
||||
}
|
||||
valueCacheForDir.set(vnode, value)
|
||||
hook(
|
||||
vnode.el,
|
||||
{
|
||||
instance: instance.renderProxy,
|
||||
value,
|
||||
oldValue,
|
||||
arg,
|
||||
modifiers
|
||||
},
|
||||
vnode,
|
||||
prevVNode
|
||||
)
|
||||
}
|
||||
const existing = props[hookKey]
|
||||
props[hookKey] = existing
|
||||
? [].concat(existing, vnodeHook as any)
|
||||
: vnodeHook
|
||||
}
|
||||
}
|
||||
map[key] = [vnodeKey, vnodeHook]
|
||||
return map
|
||||
},
|
||||
{} as Record<string, [string, Function]>
|
||||
)
|
||||
|
||||
// Directive, value, argument, modifiers
|
||||
export type DirectiveArguments = Array<
|
||||
@ -121,15 +105,40 @@ export type DirectiveArguments = Array<
|
||||
>
|
||||
|
||||
export function withDirectives(vnode: VNode, directives: DirectiveArguments) {
|
||||
const instance = currentRenderingInstance
|
||||
if (instance !== null) {
|
||||
vnode.props = vnode.props || {}
|
||||
for (let i = 0; i < directives.length; i++) {
|
||||
const [dir, value, arg, modifiers] = directives[i]
|
||||
applyDirective(vnode.props, instance, dir, value, arg, modifiers)
|
||||
const internalInstance = currentRenderingInstance
|
||||
if (internalInstance === null) {
|
||||
__DEV__ && warn(`withDirectives can only be used inside render functions.`)
|
||||
return
|
||||
}
|
||||
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
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ import { ShapeFlags } from './shapeFlags'
|
||||
import { isReactive } from '@vue/reactivity'
|
||||
import { AppContext } from './apiApp'
|
||||
import { SuspenseBoundary } from './suspense'
|
||||
import { DirectiveBinding } from './directives'
|
||||
|
||||
export const Fragment = Symbol(__DEV__ ? 'Fragment' : undefined)
|
||||
export const Portal = Symbol(__DEV__ ? 'Portal' : undefined)
|
||||
@ -66,6 +67,7 @@ export interface VNode<HostNode = any, HostElement = any> {
|
||||
children: NormalizedChildren<HostNode, HostElement>
|
||||
component: ComponentInternalInstance | null
|
||||
suspense: SuspenseBoundary<HostNode, HostElement> | null
|
||||
dirs: DirectiveBinding[] | null
|
||||
|
||||
// DOM
|
||||
el: HostNode | null
|
||||
@ -200,6 +202,7 @@ export function createVNode(
|
||||
children: null,
|
||||
component: null,
|
||||
suspense: null,
|
||||
dirs: null,
|
||||
el: null,
|
||||
anchor: null,
|
||||
target: null,
|
||||
@ -247,6 +250,7 @@ export function cloneVNode(vnode: VNode, extraProps?: Data): VNode {
|
||||
dynamicProps: vnode.dynamicProps,
|
||||
dynamicChildren: vnode.dynamicChildren,
|
||||
appContext: vnode.appContext,
|
||||
dirs: vnode.dirs,
|
||||
|
||||
// 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
|
||||
|
@ -66,7 +66,10 @@ export const vModelText: ObjectDirective<
|
||||
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 (trim && el.value.trim() === value) {
|
||||
return
|
||||
@ -107,15 +110,17 @@ export const vModelCheckbox: ObjectDirective<HTMLInputElement> = {
|
||||
|
||||
function setChecked(
|
||||
el: HTMLInputElement,
|
||||
{ value }: DirectiveBinding,
|
||||
{ value, oldValue }: DirectiveBinding,
|
||||
vnode: VNode
|
||||
) {
|
||||
// store the v-model value on the element so it can be accessed by the
|
||||
// change listener.
|
||||
;(el as any)._modelValue = value
|
||||
el.checked = isArray(value)
|
||||
? looseIndexOf(value, vnode.props!.value) > -1
|
||||
: !!value
|
||||
if (isArray(value)) {
|
||||
el.checked = looseIndexOf(value, vnode.props!.value) > -1
|
||||
} else if (value !== oldValue) {
|
||||
el.checked = !!value
|
||||
}
|
||||
}
|
||||
|
||||
export const vModelRadio: ObjectDirective<HTMLInputElement> = {
|
||||
@ -126,8 +131,10 @@ export const vModelRadio: ObjectDirective<HTMLInputElement> = {
|
||||
assign(getValue(el))
|
||||
})
|
||||
},
|
||||
beforeUpdate(el, { value }, vnode) {
|
||||
el.checked = looseEqual(value, vnode.props!.value)
|
||||
beforeUpdate(el, { value, oldValue }, vnode) {
|
||||
if (value !== oldValue) {
|
||||
el.checked = looseEqual(value, vnode.props!.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user