【Vue2.0源码学习】指令篇-Vue自定义指令

这篇具有很好参考价值的文章主要介绍了【Vue2.0源码学习】指令篇-Vue自定义指令。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

1. 前言

Vue中,除了Vue本身为我们提供的一些内置指令之外,Vue还支持用户自定义指令。并且用户有两种定义指令的方式:一种是使用全局API——Vue.directive来定义全局指令,这种方式定义的指令会被存放在Vue.options['directives']中;另一种是在组件内的directive选项中定义专为该组件使用的局部指令,这种方式定义的指令会被存放在vm.$options['directives']中。

可以看到,无论是使用哪一种方式定义的指令它都是将定义好的指令存放在指定的地方,而并不能让指令生效。那么定义的指令什么时候才会生效呢?或者说它是如何生效的呢?本篇文章就来带你探究自定义指令如何生效的内部原理。

2. 何时生效

我们知道,指令是作为标签属性写在模板中的HTML标签上的,那么又回到那句老话了,既然是写在模板中的,那它必然会经过模板编译,编译之后会产生虚拟DOM,在虚拟DOM渲染更新时,除了更新节点的内容之外,节点上的一些指令、事件等内容也需要更新。另外,我们还知道,虚拟DOM节点的更新不只是更新一个已有的节点,也有可能是创建一个新的节点,还有可能是删除一个节点等等,这些都叫做虚拟DOM节点的更新,那么既然虚拟DOM节点更新的概念这么大,那到底该什么时候处理指令的相关逻辑,执行指令函数,让指令生效呢?

其实,在虚拟DOM渲染更新的时候,它在执行相关操作的同时,还会在每个阶段触发相应的钩子函数,我们只需监听不同的钩子函数,就可以在虚拟DOM渲染更新的不同阶段做一些额外的事情。下表给出了虚拟DOM在渲染更新的不同阶段所触发的不同的钩子函数及其触发时机:

钩子函数名称 触发时机 回调参数
init 已创建VNode,在patch期间发现新的虚拟节点时被触发 VNode
create 已基于VNode创建了DOM元素 emptyNode和VNode
activate keep-alive组件被创建 emptyNode和innerNode
insert VNode对应的DOM元素被插入到父节点中时被触发 VNode
prepatch 一个VNode即将被patch之前触发 oldVNode和VNode
update 一个VNode更新时触发 oldVNode和VNode
postpatch 一个VNode被patch完毕时触发 oldVNode和VNode
destory 一个VNode对应的DOM元素从DOM中移除时或者它的父元素从DOM中移除时触发 VNode
remove 一个VNode对应的DOM元素从DOM中移除时触发。与destory不同的是,如果是直接将该VNode的父元素从DOM中移除导致该元素被移除,那么不会触发 VNode和removeCallback

所以我们只需在恰当的阶段监听对应的钩子函数来处理指令的相关逻辑,从而就可以使指令生效了。

现在我们来设想一下,在什么阶段处理指令的逻辑会比较合适?仔细想一下,当一个节点被创建成DOM元素时,如果这个节点上有指令,那此时得处理指令逻辑,让指令生效;当一个节点被更新时,如果节点更新之前没有指令,而更新之后有了指令,或者是更新前后节点上的指令发生了变化,那此时得处理指令逻辑,让指令生效;另外,当节点被移除时,那节点上的指令自然也就没有用了,此时还得处理指令逻辑。

基于以上设想,我们得出一个结论:在虚拟DOM渲染更新的createupdatedestory阶段都得处理指令逻辑,所以我们需要监听这三个钩子函数来处理指令逻辑。事实上,Vue也是这么做的,代码如下:

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

可以看到,分别监听了这三个钩子函数,当虚拟DOM渲染更新的时候会触发这三个钩子函数,从而就会执行updateDirectives函数,在该函数内部就会去处理指令的相关逻辑,我们在下面会详细分析该函数内部是如何处理指令逻辑。

3. 指令钩子函数

Vue对于自定义指令定义对象提供了几个钩子函数,这几个钩子函数分别对应着指令的几种状态,一个指令从第一次被绑定到元素上到最终与被绑定的元素解绑,它会经过以下几种状态:

  • bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
  • inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
  • update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前
  • componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
  • unbind:只调用一次,指令与元素解绑时调用。

有了每个状态的钩子函数,这样我们就可以让指令在不同状态下做不同的事情。

例如,我们想让指令所绑定的输入框一插入到 DOM 中,输入框就获得焦点,那么,我们就可以这样定义指令:

// 注册一个全局自定义指令 `v-focus`
Vue.directive('focus', {
    // 当被绑定的元素插入到 DOM 中时……
    inserted: function (el) {
        // 聚焦元素
        el.focus()
    }
})

在模板中使用该指令,如下:

<input v-focus>

可以看到,我们在定义该指令的时候,我们将获取焦点的逻辑写在了inserted钩子函数里面,这样就保证了当被绑定的元素插入到父节点时,获取焦点的逻辑就会被执行。

同理,我们也可以在一个指令中设置多个钩子函数,从而让一个指令在不同状态下做不同的事。

OK,有了这个概念之后,接下来我们就来分析指令是如何生效的。

4. 如何生效

在虚拟DOM篇中我们知道了,当虚拟DOM渲染更新的时候会触发createupdatedestory这三个钩子函数,从而就会执行updateDirectives函数来处理指令的相关逻辑,执行指令函数,让指令生效。所以,探究指令如何生效的问题就是分析updateDirectives函数的内部逻辑。

updateDirectives函数的定义位于源码的src/core/vdom/modules/directives.js文件中,如下:

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

可以看到,该函数的内部是判断了如果新旧VNode中只要有一方涉及到了指令,那就调用_update方法去处理指令逻辑。

_update方法定义如下:

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)

  const dirsWithInsert = []
  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]) {
        // no longer present, unbind
        callHook(oldDirs[key], 'unbind', oldVnode, oldVnode, isDestroy)
      }
    }
  }
}

可以看到,该方法内首先定义了一些变量,如下:

const isCreate = oldVnode === emptyNode
const isDestroy = vnode === emptyNode
const oldDirs = normalizeDirectives(oldVnode.data.directives, oldVnode.context)
const newDirs = normalizeDirectives(vnode.data.directives, vnode.context)

const dirsWithInsert = []
const dirsWithPostpatch = []
  • isCreate:判断当前节点vnode对应的旧节点oldVnode是不是一个空节点,如果是的话,表明当前节点是一个新创建的节点。
  • isDestroy:判断当前节点vnode是不是一个空节点,如果是的话,表明当前节点对应的旧节点将要被销毁。
  • oldDirs:旧的指令集合,即oldVnode中保存的指令。
  • newDirs:新的指令集合,即vnode中保存的指令。
  • dirsWithInsert:保存需要触发inserted指令钩子函数的指令列表。
  • dirsWithPostpatch:保存需要触发componentUpdated指令钩子函数的指令列表。

另外,你可能还看到了在定义新旧指令集合的变量中调用了normalizeDirectives函数,其实该函数是用来模板中使用到的指令从存放指令的地方取出来,并将其格式进行统一化,其定义如下:

function normalizeDirectives (dirs,vm):  {
  const res = Object.create(null)
  if (!dirs) {
    return res
  }
  let i, dir
  for (i = 0; i < dirs.length; i++) {
    dir = dirs[i]
    if (!dir.modifiers) {
      dir.modifiers = emptyModifiers
    }
    res[getRawDirName(dir)] = dir
    dir.def = resolveAsset(vm.$options, 'directives', dir.name, true)
  }
  return res
}

以第三章节中的v-focus指令为例,通过normalizeDirectives函数取出的指令会变成如下样子:

{
    'v-focus':{
        name : 'focus' ,  // 指令的名称
        value : '',       // 指令的值
        arg:'',           // 指令的参数
        modifiers:{},     // 指令的修饰符
        def:{
            inserted:fn
        }
    }
}

OK,言归正传,获取到oldDirsnewDirs之后,接下来要做的事情就是对比这两个指令集合并触发对应的指令钩子函数。

首先,循环newDirs,并分别从oldDirsnewDirs取出当前循环到的指令分别保存在变量oldDirdir中,如下:

let key, oldDir, dir
for (key in newDirs) {
    oldDir = oldDirs[key]
    dir = newDirs[key]
}

然后判断当前循环到的指令名key在旧的指令列表oldDirs中是否存在,如果不存在,说明该指令是首次绑定到元素上的一个新指令,此时调用callHook触发指令中的bind钩子函数,接着判断如果该新指令在定义时设置了inserted钩子函数,那么将该指令添加到dirsWithInsert中,以保证执行完所有指令的bind钩子函数后再执行指令的inserted钩子函数,如下:

// 判断当前循环到的指令名`key`在旧的指令列表`oldDirs`中是否存在,如果不存在,那么说明这是一个新的指令
if (!oldDir) {
    // new directive, bind
    // 触发指令中的`bind`钩子函数
    callHook(dir, 'bind', vnode, oldVnode)
    // 如果定义了inserted 时的钩子函数 那么将该指令添加到dirsWithInsert中
    if (dir.def && dir.def.inserted) {
        dirsWithInsert.push(dir)
    }
}

如果当前循环到的指令名key在旧的指令列表oldDirs中存在时,说明该指令在之前已经绑定过了,那么这一次的操作应该是更新指令。

首先,在dir上添加oldValue属性和oldArg属性,用来保存上一次指令的value属性值和arg属性值,然后调用callHook触发指令中的update钩子函数,接着判断如果该指令在定义时设置了componentUpdated钩子函数,那么将该指令添加到dirsWithPostpatch中,以保证让指令所在的组件的VNode及其子VNode全部更新完后再执行指令的componentUpdated钩子函数,如下:

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)
    }
}

最后,判断dirsWithInsert数组中是否有元素,如果有,则循环dirsWithInsert数组,依次执行每一个指令的inserted钩子函数,如下:

if (dirsWithInsert.length) {
    const callInsert = () => {
        for (let i = 0; i < dirsWithInsert.length; i++) {
            callHook(dirsWithInsert[i], 'inserted', vnode, oldVnode)
        }
    }
}

从上述代码中可以看到,并没有直接去循环执行每一个指令的inserted钩子函数,而是新创建了一个callInsert函数,当执行该函数的时候才会去循环执行每一个指令的inserted钩子函数。这又是为什么呢?

这是因为指令的inserted钩子函数必须在被绑定元素插入到父节点时调用,那么如果是一个新增的节点,如何保证它已经被插入到父节点了呢?我们之前说过,虚拟DOM在渲染更新的不同阶段会触发不同的钩子函数,比如当DOM节点在被插入到父节点时会触发insert函数,那么我们就知道了,当虚拟DOM渲染更新的insert钩子函数被调用的时候就标志着当前节点已经被插入到父节点了,所以我们要在虚拟DOM渲染更新的insert钩子函数内执行指令的inserted钩子函数。也就是说,当一个新创建的元素被插入到父节点中时虚拟DOM渲染更新的insert钩子函数指令的inserted钩子函数都要被触发。既然如此,那就可以把这两个钩子函数通过调用mergeVNodeHook方法进行合并,然后统一在虚拟DOM渲染更新的insert钩子函数中触发,这样就保证了元素确实被插入到父节点中才执行的指令的inserted钩子函数,如下:

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()
    }
}

同理,我们也需要保证指令所在的组件的VNode及其子VNode全部更新完后再执行指令的componentUpdated钩子函数,所以我们将虚拟DOM渲染更新的postpatch钩子函数和指令的componentUpdated钩子函数进行合并触发,如下:

if (dirsWithPostpatch.length) {
    mergeVNodeHook(vnode, 'postpatch', () => {
        for (let i = 0; i < dirsWithPostpatch.length; i++) {
            callHook(dirsWithPostpatch[i], 'componentUpdated', vnode, oldVnode)
        }
    })
}

最后,当newDirs循环完毕后,再循环oldDirs,如果某个指令存在于旧的指令列表oldDirs而在新的指令列表newDirs中不存在,那说明该指令是被废弃的,所以则触发指令的unbind钩子函数对指令进行解绑。如下:

if (!isCreate) {
    for (key in oldDirs) {
        if (!newDirs[key]) {
            // no longer present, unbind
            callHook(oldDirs[key], 'unbind', oldVnode, oldVnode, isDestroy)
        }
    }
}

以上就是指令生效的全部逻辑。所谓让指令生效,其实就是在合适的时机执行定义指令时所设置的钩子函数。

5. 总结

本篇文章介绍了关于自定义指令如何生效的相关内容。

首先,我们知道了如果一个DOM节点上绑定了指令,那么在这个DOM节点所对应虚拟DOM节点进行渲染更新的时候,不但会处理节点渲染更新的逻辑,还会处理节点上指令的相关逻辑。具体处理指令逻辑的时机是在虚拟DOM渲染更新的createupdatedestory阶段。

接着,我们介绍了Vue对于自定义指令定义对象提供了几个钩子函数,这几个钩子函数分别对应着指令的几种状态,我们可以根据实际的需求将指令逻辑写在合适的指令状态钩子函数中,比如,我们想让指令所绑定的元素一插入到DOM中就执行指令逻辑,那我们就应该把指令逻辑写在指令的inserted钩子函数中。

接着,我们逐行分析了updateDirectives函数,在该函数中就是对比新旧两份VNode上的指令列表,通过对比的异同点从而执行指令不同的钩子函数,让指令生效。

最后,一句话概括就是:所谓让指令生效,其实就是在合适的时机执行定义指令时所设置的钩子函数。文章来源地址https://www.toymoban.com/news/detail-492364.html

到了这里,关于【Vue2.0源码学习】指令篇-Vue自定义指令的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 前端学习--vue2--2--vue指令基础

    写在前面: 前置内容 - vue配置 vue指令只的是带有v-前缀的特殊标签属性 插值表达式{{}}是一种vue模板语法。 利用表达式进行差值,渲染到页面。 表达式可以是被求值的代码,js会计算 不能用差值表达式的 不存在的数据 {{data里面不存在的字段 js {{if}} 标签属性 span id=“

    2024年02月14日
    浏览(33)
  • 前端学习---vue2--指令修饰符详解

    写在前面: 前端感觉系统学起来还行,我也不晓得我是咋快速入门1个月就开始看实习公司代码的。然后现在开始系统复习,然后感觉有的封装的还可以,不过就是我不晓得,像这个指令修饰符,其实说逻辑难写,倒是也没有,就是简单不用自己写. 就像后端判断字符串是否为

    2024年02月14日
    浏览(24)
  • 【学习记录24】vue3自定义指令

    1、html部分  2、js部分 3、实现效果 1、html部分 2、js部分 在components下创建loading文件夹,在loading文件夹里创建directive.js  在main.js中全局注册指令 1、在components下创建loading文件夹,在loading文件夹里创建directive.js 2、在loading文件夹里创建loading.vue 3、在loading文件夹里放入一张G

    2024年01月19日
    浏览(26)
  • 【Vue2.0源码学习】全局API篇-Vue中全局API分析

    与实例方法不同,实例方法是将方法挂载到 Vue 的原型上,而全局API是直接在 Vue 上挂载方法。在 Vue 中,全局API一共有12个,分别是 Vue.extend 、 Vue.nextTick 、 Vue.set 、 Vue.delete 、 Vue.directive 、 Vue.filter 、 Vue.component 、 Vue.use 、 Vue.mixin 、 Vue.observable 、 Vue.version 。这12个API中有的

    2024年02月08日
    浏览(32)
  • 【Vue2.0源码学习】生命周期篇-初始化阶段(initInjections)

    本篇文章介绍生命周期初始化阶段所调用的第四个初始化函数—— initInjections 。从函数名字上来看,该函数是用来初始化实例中的 inject 选项的。说到 inject 选项,那必然离不开 provide 选项,这两个选项都是成对出现的,它们的作用是:允许一个祖先组件向其所有子孙后代注

    2024年02月09日
    浏览(40)
  • 【Vue2.0源码学习】生命周期篇-初始化阶段(initState)

    本篇文章介绍生命周期初始化阶段所调用的第五个初始化函数—— initState 。 从函数名字上来看,这个函数是用来初始化实例状态的,那么什么是实例的状态呢?在前面文章中我们略有提及,在我们日常开发中,在 Vue 组件中会写一些如 props 、 data 、 methods 、 computed 、 watc

    2024年02月09日
    浏览(39)
  • vue2 vue中的常用指令

    1.前端必备技能 2.岗位多,绝大互联网公司都在使用Vue 3.提高开发效率 4.高薪必备技能(Vue2+Vue3) 概念:Vue (读音 /vjuː/,类似于 view) 是一套 **构建用户界面 ** 的 渐进式 框架 Vue2官网:https://v2.cn.vuejs.org/ 1.什么是构建用户界面 基于数据 渲染出用户可以看到的 界面 2.什么是渐

    2024年02月11日
    浏览(28)
  • 20230628----重返学习-自定义指令的玩法和作用-对象新增属性不能响应的问题-Vue组件中的data属性-Vue生命周

    面试题:自定义指令的玩法和作用 面试题:Vue怎么用 vm.$set() 解决对象新增属性不能响应的问题 ? 面试题:Vue 组件中的 data 为什么必须是函数? 面试题:谈谈你对 Vue2 生命周期的理解? 面试题:简单说一下 $nextTick 的作用及实现原理? 面试题:computed 和 watch 的区别和运用

    2024年02月11日
    浏览(43)
  • vue2--1. 内容渲染指令 2. 属性绑定指令 3. 事件绑定 4. v-model 指令 5. 条件渲染指令

    2. 属性绑定指令 3. 事件绑定 4. v-model 指令 5. 条件渲染指令) List item 推荐大家安装的 VScode 中的 Vue 插件 Vue 3 Snippets https://marketplace.visualstudio.com/items?itemName=hollowtree.vue-snippets Vetur https://marketplace.visualstudio.com/items?itemName=octref.vetur 什么是 vue 构建用户界面 用 vue 往 html 页面中填充

    2024年02月05日
    浏览(44)
  • Vue2技能树(3)-声明式渲染、指令大全、生命周期函数

    👍 点赞,你的认可是我创作的动力! ⭐️ 收藏,你的青睐是我努力的方向! ✏️ 评论,你的意见是我进步的财富! Vue2技能树(1)-介绍、导入使用、响应式数据绑定、组件化开发 vue2技能树(2)-模板语法、vue的工具链、渐进式框架 Vue2技能树(3)-声明式渲染、指令大全、生命周

    2024年02月07日
    浏览(40)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包