Vue中指令的实现机制
2021-10-31
| 2023-2-19
0  |  0 分钟
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的指令hook
 function 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本质上就是一个语法糖.
本质上就是往组件上传递了一个名为modelValueprop, 它的值是往组件传入的数据data.
另外, 还在组件上监听了一个名为onUpdate:modelValue的自定义事件, 事件执行的时候会将执行的参数赋值给数据data
在vue3中, 支持传递v-model的参数, 这样, 我们就可以对组件传入多个v-model了 , 类似如下:
<ChildComponent v-mode:title="pageTitle" v-model:content="pageContent">

总结

 
源码
  • Vue
  • Vue源码
  • Directive
  • 原型链Getter/Setter的屏蔽效应Vue中的依赖注入实现机制
    目录