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 { 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
}

View File

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

View File

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