*/
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'
oldValue: any
arg?: string
modifiers: DirectiveModifiers
+ dir: ObjectDirective
}
export type DirectiveHook<T = any> = (
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'
}
}
-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<
>
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
}
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
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> = {
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)
+ }
}
}