perf: improve directive runtime performance

This commit is contained in:
Evan You 2019-10-26 16:00:07 -04:00
parent 6c7787db7b
commit 07ce2c5fa7
3 changed files with 86 additions and 66 deletions

View File

@ -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++) {
if (isFunction(directive)) { const binding = bindings[i]
directive = { const hook = binding.dir[key]
mounted: directive, if (hook != null) {
updated: directive if (prevVnode != null) {
} as ObjectDirective binding.oldValue = prevBindings[i].value
} }
hook(vnode.el, binding, vnode, prevVnode)
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(
vnode.el,
{
instance: instance.renderProxy,
value,
oldValue,
arg,
modifiers
},
vnode,
prevVNode
)
} }
const existing = props[hookKey] map[key] = [vnodeKey, vnodeHook]
props[hookKey] = existing return map
? [].concat(existing, vnodeHook as any) },
: vnodeHook {} as Record<string, [string, Function]>
} )
}
// 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
} }

View File

@ -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

View File

@ -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,8 +131,10 @@ export const vModelRadio: ObjectDirective<HTMLInputElement> = {
assign(getValue(el)) assign(getValue(el))
}) })
}, },
beforeUpdate(el, { value }, vnode) { beforeUpdate(el, { value, oldValue }, vnode) {
el.checked = looseEqual(value, vnode.props!.value) if (value !== oldValue) {
el.checked = looseEqual(value, vnode.props!.value)
}
} }
} }