]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
perf: improve directive runtime performance
authorEvan You <yyx990803@gmail.com>
Sat, 26 Oct 2019 20:00:07 +0000 (16:00 -0400)
committerEvan You <yyx990803@gmail.com>
Sat, 26 Oct 2019 20:00:07 +0000 (16:00 -0400)
packages/runtime-core/src/directives.ts
packages/runtime-core/src/vnode.ts
packages/runtime-dom/src/directives/vModel.ts

index 839b17ad1fb038818a43d3b97dd01dab61c53198..052138a83ef460cd69f9609346c48a70cc101020 100644 (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
 }
index c59c62102e264b29dc6801d4f830ed79494fd589..3293315c170397b0f8dc10ba521b92fbae750f31 100644 (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
index 20dfb9189e2ae386b376d9ac90e5d15421d16057..6c30927707074534eaad65b6932a33861640091b 100644 (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)
+    }
   }
 }