Vue自定义指令介绍及原理

Vue自定义指令

Vue指令:

在使用Vue框架进行前端开发时,我们经常会使用一些特殊指令来快速实现一些效果或功能。
常见指令如:v-bind、v-if (v-else)、v-show、v-html等等都是一些比较常用的指令
使用如下:

<span v-if=true v-html="htmlContext || '--'"></span>
<span v-show=true> show html </span>

由于本文主要介绍自定义指令相关的一些知识,所以对于Vue自带指令就不做过多赘述了
在这情况下,Vue官方也推出了一种编写自定义指令的方法。我们可以定义开发我们自定义的模版指令,来对一些特殊的需求效果功能进行开发与实现
在Vue的官方文档中是如下描述的:

vue自定义指令官方文档在这里插入图片描述

自定义指令不只支持全局注册,也支持在组件中进行注册并引用,具体操作在文档中都有详细描述

Vue自定义指令

Vue自定义指令机制包含五个钩子函数,每个钩子函数包含四个回调参数
钩子函数简单来说可以称作为生命周期,如果对生命周期有过了解或应用应该会比较容易理解

Vue2钩子函数:

bind:只调用一次,组件初始化时会调用该钩子函数
inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。可以通过比较更新前后的值来忽略不必要的模板更新
componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用
unbind:只调用一次,指令与元素解绑时调用

Vue3钩子函数:

setup() - 新增,对应之前的 Vue2 的 created、beforeCreate。
onBeforeMount - 组件挂载到节点之前执行
onMounted - 组件挂载完成后执行
onBeforeUpdate:组件更新之前执行
onUpdated - 组件更新完成之后执行
onBeforeUnmount - 组件卸载之前执行
onUnmounted:组件卸载完成后执行
特殊:(onActivated、onDeactivated、onErrorCaptured)
onActivated: keep-alive中的组件,会多出两个生命周期钩子函数,被激活时执行
onDeactivated: 组件切换时执行,当前组件消亡时执行
onErrorCaptured: 捕获子组件、孙组件异常时被调用执行

四个函数的回调参数:el、binding、vnode、oldVnode
el:指令所绑定的元素
binding(对象):
name(指令名称)、value(指令的绑定值)、oldValue(指令绑定的前一个值)、
expression(绑定指令的表达式)、arg(绑定的指令参数)、
modifiers(一个包含修饰符的对象。例如:v-my-directive.foo.bar 中,修饰符对象为 { foo: true, bar: true })
vnode:即Vue虚拟DOM生成的虚拟节点,细节可学习Vue虚拟DOM原理
oldVnode:上一个虚拟节点,仅在 update 和 componentUpdated 钩子函数中可用

接下来上一个简单的组件注册自定义指令的例子:
效果为一渲染input元素后即添加焦点,可辅助进行表单校验

// html
<input v-focus> 

//  JS
    directives: {
      focus: {
        // 指令的定义
        inserted: function (el,binding,vnode,oldVnode) {
          el.focus();
          // 展示回调值输出
          console.log('el',el);
          console.log('binding',binding);
          console.log('vnode',vnode);
          console.log('oldVnode',oldVnode);
        }
      }
    }

输出如下:
Vue自定义指令输出回调参数

Vue自定义指令原理:

以上介绍了Vue自定义指令的相关介绍以及用法
接下来将集中分享一下关于Vue自定义指令的实现原理
首先我们知道,
Vue一个机制首先要在 vm.$options 上进行挂载 => 而后进行正则解析处理 => 最后生成虚拟dom

这个的流程就不在此赘述了,有时间会有集中出一篇关于这方面的文章
接下来我们直接分析patchDom的过程
源码链接:Vue自定义指令源码 Github
为了页面不占用太大篇幅,源码就不全额摘抄了,下面将集中分析部分关键原理

/* @flow */

import { emptyNode } from 'core/vdom/patch'
import { resolveAsset, handleError } from 'core/util/index'
import { mergeVNodeHook } from 'core/vdom/helpers/index'

export default {
  create: updateDirectives,
  update: updateDirectives,
  destroy: function unbindDirectives (vnode: VNodeWithData) {
    updateDirectives(vnode, emptyNode)
  }
}

function updateDirectives (oldVnode: VNodeWithData, vnode: VNodeWithData) {
  if (oldVnode.data.directives || vnode.data.directives) {
    _update(oldVnode, vnode)
  }
}

// 比较前后节点更新 相关流程
function _update (oldVnode, vnode) {
  // 是否为新节点 ? 
  const isCreate = oldVnode === emptyNode
  const isDestroy = vnode === emptyNode
  // 新旧节点的指令抽离
  const oldDirs = normalizeDirectives(oldVnode.data.directives, oldVnode.context)
  const newDirs = normalizeDirectives(vnode.data.directives, vnode.context)

  // inserted 需要触发钩子函数的元素列表
  const dirsWithInsert = []
  // componentUpdated
  const dirsWithPostpatch = []

  // 更新新旧节点 指令
  let key, oldDir, dir
  for (key in newDirs) {
    oldDir = oldDirs[key]
    dir = newDirs[key]
  // 不存在旧节点时,直接插入!
    if (!oldDir) {
      // new directive, bind
      callHook(dir, 'bind', vnode, oldVnode)
      if (dir.def && dir.def.inserted) {
        dirsWithInsert.push(dir)
      }
  // 存在旧节点时:更新      
    } else {
      // existing directive, update
      dir.oldValue = oldDir.value
      dir.oldArg = oldDir.arg
      callHook(dir, 'update', vnode, oldVnode)
      if (dir.def && dir.def.componentUpdated) {
        dirsWithPostpatch.push(dir)
      }
    }
  }

  if (dirsWithInsert.length) {
    const callInsert = () => {
      for (let i = 0; i < dirsWithInsert.length; i++) {
        callHook(dirsWithInsert[i], 'inserted', vnode, oldVnode)
      }
    }
    // 判断是否为新创建的节点,如果是,则使用该方法将钩子函数合并,并推迟执行
    if (isCreate) {
      mergeVNodeHook(vnode, 'insert', callInsert)
    } else {
      callInsert()
    }
  }

  if (dirsWithPostpatch.length) {
    mergeVNodeHook(vnode, 'postpatch', () => {
      for (let i = 0; i < dirsWithPostpatch.length; i++) {
        callHook(dirsWithPostpatch[i], 'componentUpdated', vnode, oldVnode)
      }
    })
  }
  // 判断该节点是否为新创建的虚拟节点?
  if (!isCreate) {
    // 遍历旧节点
    for (key in oldDirs) {
      // 寻找不存在的节点
      if (!newDirs[key]) {
        // 执行指令的unbind方法
        callHook(oldDirs[key], 'unbind', oldVnode, oldVnode, isDestroy)
      }
    }
  }
}

const emptyModifiers = Object.create(null)

function normalizeDirectives (
  dirs: ?Array<VNodeDirective>,
  vm: Component
): { [key: string]: VNodeDirective } {
  const res = Object.create(null)
  if (!dirs) {
    // $flow-disable-line
    return res
  }
  let i, dir
  for (i = 0; i < dirs.length; i++) {
    dir = dirs[i]
    if (!dir.modifiers) {
      // $flow-disable-line
      dir.modifiers = emptyModifiers
    }
    res[getRawDirName(dir)] = dir
    dir.def = resolveAsset(vm.$options, 'directives', dir.name, true)
  }
  // $flow-disable-line
  return res
}

// 获取指令name
function getRawDirName (dir: VNodeDirective): string {
  return dir.rawName || `${dir.name}.${Object.keys(dir.modifiers || {}).join('.')}`
}
// 用于循环调用钩子函数
function callHook (dir, hook, vnode, oldVnode, isDestroy) {
  const fn = dir.def && dir.def[hook]
  if (fn) {
    try {
      fn(vnode.elm, dir, vnode, oldVnode, isDestroy)
    } catch (e) {
      handleError(e, vnode.context, `directive ${dir.name} ${hook} hook`)
    }
  }
}
  • 1
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值