diff --git a/packages/runtime-core/src/directives.ts b/packages/runtime-core/src/directives.ts index 839b17ad1..052138a83 100644 --- a/packages/runtime-core/src/directives.ts +++ b/packages/runtime-core/src/directives.ts @@ -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 = ( @@ -47,9 +48,13 @@ export type FunctionDirective = DirectiveHook export type Directive = ObjectDirective | FunctionDirective -type DirectiveModifiers = Record +export type DirectiveModifiers = Record -const valueCache = new WeakMap>() +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, - instance: ComponentInternalInstance, - directive: Directive, - value?: unknown, - arg?: string, - modifiers: DirectiveModifiers = EMPTY_OBJ -) { - let valueCacheForDir = valueCache.get(directive)! - if (!valueCacheForDir) { - valueCacheForDir = new WeakMap() - 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 +) // 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 = {} + 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 } diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts index c59c62102..3293315c1 100644 --- a/packages/runtime-core/src/vnode.ts +++ b/packages/runtime-core/src/vnode.ts @@ -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 { children: NormalizedChildren component: ComponentInternalInstance | null suspense: SuspenseBoundary | 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 diff --git a/packages/runtime-dom/src/directives/vModel.ts b/packages/runtime-dom/src/directives/vModel.ts index 20dfb9189..6c3092770 100644 --- a/packages/runtime-dom/src/directives/vModel.ts +++ b/packages/runtime-dom/src/directives/vModel.ts @@ -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 = { 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 = { @@ -126,8 +131,10 @@ export const vModelRadio: ObjectDirective = { 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) + } } }