password
Created
Feb 19, 2023 03:52 PM
type
Post
status
Published
date
Oct 31, 2021
slug
summary
Vue中指令的实现机制
tags
Vue
Vue源码
Directive
category
源码
icon
自定义指令
Vue允许我们自定义指令作用到DOM上, 例如, 我们需要在进入页面时实现输入框自动聚焦, 我们可以自定义一个
v-focus
指令import Vue from 'vue' const app = Vue.createApp({}) // 注册全局v-focus指令 app.directive('focus', { // 挂载的勾子函数 mounted(el) { el.focus() } }) // 或者在组件内部局部注册 { // ... directives: { focus: { mounted(el) { el.focus() } } } // ... }
完成定义之后, 即可在模板中进行使用
<input v-focus />
指令本质上就是一个
javascript
对象, 对象上定义了一系列的钩子函数指令的注册
指令注册则是将指令的定义保存到相应的地方, 未来使用的时候可以从保存的地方拿到它
全局注册
全局注册的指令, 会挂载到
app.context
对象上, 供当前应用全局进行使用function createApp(rootComponent, rootProps = null) { const context = createAppContext() const app = { _component: rootComponent, _props: rootProps, directive(name, directive) { // 正式环境, 会校验directive的名称是否与内置的directive冲突 if ((process.env.NODE_ENV !== 'production')) { validateDirectiveName(name) } if (!directive) { // 没有第二个参数, 则获取对应的指令对象 return context.directives[name] } // 如果有重复注册, 会抛出警告 // ... context.directives[name] = directive return app } } return app }
局部注册
局部注册的指令, 保存在组件对象的定义中
指令的编译
先看下如果使用了指令, 编译后的js内容是怎么样的, 同样以
<input v-focus />
为例// ... export function render(_ctx, _cache, $props, $setup, $data, $options) { const _directive_focus = _resolveDirective('focus') return _withDirectives((_openBlock(), _createBlock('input', null, null, 512 /* NEED_PATCH */)), [ [_directive_focus] ]) } // ... // 如果不使用v-focus export function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(), _createBlock('input')) }
可见, 使用了指令的地方, 会使用
resolveDirective
来解析处理自定义指令, 同时将返回使用withDirectives
包裹, 并传入此自定义指令.function resolveDirective(name) { return resolveAsset('directives', name) } function resolveAsset(type, name, warnMissing = true) { // 获取当前渲染实例 const instance = currentRenderingInstance || currentInstance if (instance) { const Component = instance.type const res = // 局部注册 resolve(Component[type], name) || // 全局注册 resolve(instance.appContext[type], name) } } // 根据名称`name`来进行匹配, 匹配不到会用驼峰匹配一次, 再匹配不到会再用首字母大写匹配一次 function resolve(registry, name) { return (registry && ( registry[name] || registry[camelize(name)] || registry[capitalize(camelize(name))] ) ) }
withDirectives
主要则是给vnode
添加了一个dir
属性, 属性值是这个元素所设置的所有指令的对象数组.function withDirectives(vnode, directives) { const internalInstance = currentRenderingInstance if (internalInstance === null) { return vnode } const instance = internalInstance.proxy const bindings = vnode.dirs || (vnode.dirs = []) for(let i = 0; i < directives.length; i++) { let [dir, value, arg, modifiers = EMOTY_OBJ] = directives[i] if (isFunction(dir)) { dir = { mounted: dir, updated: dir } } bindings.push({ dir, instance, value, oldValue: void 0, arg, modifiers }) } return vnode }
指令的应用
组件挂载时
在元素挂载时, 挂载之前会触发
beforeMount
的指令hook挂载完成之后, 触发
mounted
的指令hookfunction mountElement(vnode, container, anchor, parentComponent, parentSuspense, isSVG, optimized) { if (dirs) { invokeDirectiveHook(vnode, null, parentComponent, 'beforeMount') } // 将创建的DOM元素节点挂载到container上 hostInsert(el, container, anchor) if (dirs) { queuePostRenderEffect(() => { invokeDirectiveHook(vnode, null, parentComponent, 'mounted') ]) } } function invokeDirectiveHook(vnode, prevVNode, instance, name) { const bindings = vnode.dirs const oldBindings = prevVNode && prevVNode.dirs // 遍历找到对应的指令对象 for(let i = 0; i < bindings.length; i++) { const binding = bindings[i] if (oldBindings) { binding.oldValue = oldBindings[i].value } const hook = binding.dir[name] if (hook) { // 找到了指令勾子, 就执行这个勾子 callWithAsyncErrorHandling(hook, instance, 8 /* DIRECTIVE_HOOK */, [ vnode.el, binding, vnode, prevVNode ]) } } }
组件更新时
const patchElement = (n1, n2, parentComponent, parentSuspense, isSVG, optimized) => { const el = (n2.el = n1.el) const oldProps = (n1 && n1.props) || EMPTY_OBJ const newProps = n2.props || EMPTY_OBJ const { dirs } = n2 // 更新Props patchProps(el, n2, oldProps, newProps, parentComponent, parentSuspense, isSVG) if (dirs) { invokeDirectiveHook(n2, n2, parentComponent, 'beforeUpdate') } // 更新子节点 patchChildren(n1, n2, el, null, parentComponent, parentSuspense, areChildrenSVG) if (dirs) { queuePoseRenderEffect(() => { invokeDirectiveHook(vnode, null, parentComponent, 'updated') }) } }
组件卸载时
const unmount = (vnode, parentComponent, parentSuspense, doRemove = false) { const shouldInvokeDirs = shapeFlag & 1 /* ELEMENT */ && dirs // 如果是组件, 则直接注销组件 if (shapeFlag & 6 /* COMPONENT */) { unmountComponent(vnode.component, parentSuspense, doRemove) } else { if (shapeFlag & 128 /* SUSPENSE */) { vnode.suspense.unmount(parentSuspense, doRemove) return } if (shouldInvokeDirs) { invokeDirectiveHook(vnode, null, parentComponent, 'beforeUnmount') } // 卸载子节点 if (dynamicChildren && (type !== Fragment || (patchFlag > 0 && patchFlag & 64 /* STABLE_FRAGMENT */))) { unmountChildren(dynamicChildren, parentComponent, parentSuspense) } else if (shapeFlag & 16 /* ARRAY_CHILDREN */) { unmountChildren(children, parentComponent, parentSuspense) } if (shapeFlag & 64 /* TELEPORT */ ) { vnode.type.remove(vnode, internals) } // 移除DOM节点 if (doRemove) { remove(vnode) } } if ((vnodeHook = props && props.onVnodeUnmounted) || shouldInvokeDirs) { queuePostRenderEffect(() => { vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode) if (shouldInvokeDirs) { invokeDirectiveHook(vnode, null, parentComponent, 'unmounted') } }, parentSuspense) } }
内置指令
v-model
v-model对不同的情况下绑定的内容是不一样的, v-model主要的对象是
input
select
textarea
及自定义组件我们以一个输入框同步绑定一个名为
searchText
, 组件为input
的双向绑定数据为例<input v-model="searchText" />
编译之后的结果如下:
export function render(_ctx, _cache, $props, $setup, $data, $options) { return _withDirectives((_openBlock(), _createBlock('input', { 'onUpdate:modelValue': $event => (_ctx.searchText = $event) }, null, 8 /* PROPS */, ['onUpdate:modelValue'])), [_vModelText, _ctx.searchText]) }
可见
v-model
也调用了与自定义指令一样的withDirective
函数,在withDirective中, 还额外传递了一个
onUpdate:modelValue
的函数同时, 绑定了一个
vModelText
的内置指令vModelText
const vModelText = { // 数据到DOM的流动 created(el, { value, modifiers: { lazy, trim, number } }, vnode) { // 将v-model绑定的值赋值给el.value, 这就是数据的单向流动 el.value = value == null ? '' : value // 获取props中的onUpdateModelValue属性 el._assign = getModelAssigner(vnode) const castToNumber = number || el.type === 'number' // 监听input标签的事件 // 根据是否配置lazy来决定是用change事件, 还是input事件 addEventListener(el, lazy ? 'change' : 'input', e => { if (e.target.composing) return let domValue = el.value if (trim) { domValue = domValue.trim() } else if (castToNumber) { domValue = toNumber(domValue) } el._assign(domValue) }) if (trim) { addEventListener(el, 'change', () => { el.value = el.value.trim() }) } if (!lazy) { // 非lazy的情况下, 处理中文输入法输入内容但是还没有转译成为中文等类似情景 // 此时会有一个target.composing的标识, 保证此类情况下不会频繁触发DOM的更新 addEventListener(el, 'compositionstart', onCompositionStart) addEventListener(el, 'compositionend', onCompositionEnd) } }, // DOM到数据的流动 beforeUpdate(el, { value, modifiers: { lazy, trim, number } }, vnode) { el._assign = getModelAssigner(vnode) if (document.activeElement === el) { if (trim && el.value.trim() === value) { return } if ((number || el.type === 'number') && toNumber(el.value) === value) { return } } const newValue = value == null ? '' : value if (el.value !== newValue) { el.value = newValue } } } const getModelAssigner = (vnode) => { const fn = vnode.props['onUpdate:modelValue'] return isArray(fn) ? value => invokeArrayFns(fn, value) : fn } function onCompositionStart(e) { e.target.composing = true } function onCompositionEnd(e) { const target = e.target if (target.composing) { target.composing = false trigger(target, 'input') } }
在自定义组件上作用v-model
假如我们有这样子的一个自定义组件
app.component('custom-input', { props: ['modelValue'], template: ` <input v-model="value" /> `, computed: { value: { get() { return this.modelValue }, set(value) { this.$emit('update:modelValue', value) } } } })
接下来, 我们使用这个自定义组件
<custom-input v-model="searchText" /> // 其实和下面等价 <custom-input :modelValue="searchText" @update:modelValue="$event=>{searchText=$event}" />
以下是两个相同的编译结果
// ... const _component_custom_input = _resolveComponent('custom-input') return (_openBlock(), _createBlock(_component_custom_input, { modelValue: _ctx.searchText, 'onUpdate:modelValue': $event => (_ctx.searchText=$event) }, null, 8 /* PROPS */, ['modelValue', 'onUpdate:modelValue']))
其实从这里可以看出:
v-model
本质上就是一个语法糖.本质上就是往组件上传递了一个名为
modelValue
的prop
, 它的值是往组件传入的数据data.另外, 还在组件上监听了一个名为
onUpdate:modelValue
的自定义事件, 事件执行的时候会将执行的参数赋值给数据data在vue3中, 支持传递
v-model
的参数, 这样, 我们就可以对组件传入多个v-model
了 , 类似如下:<ChildComponent v-mode:title="pageTitle" v-model:content="pageContent">