前言
在编写vue
项目的过程中,指令应该是相当的熟悉的了
Vue 指令是一种特殊的 HTML
属性,具有 v-
前缀,用于在模板中声明性地绑定数据并对 DOM
进行操作。指令可以被绑定到 HTML
元素、组件和相应的模板语法中。
在 Vue
中,指令本质上就是实现了一个自定义操作的 JS
函数,该函数接受两个参数:绑定元素 (el) 和指令对象 (binding)。
指令对象包含了一些指令相关的信息,例如指令名称、表达式、修饰符等。
官方的v指令都有哪些
官方指令一览
指令 |
描述 |
条件指令 |
用于做条件渲染,包括整个if家庭(v-if 、v-else 、v-else-if ) |
v-text |
用于对整个节点赋值文本内容 |
v-html |
用于更新元素的innerHTML |
v-show |
用于切换一个元素的显示与隐藏 |
v-for |
类似于array.map() ,用于将一个列表数据转换为元素列表 |
v-on |
缩写:@ ,用于捆绑事件监听 |
v-bind |
缩写:: ,用于将变量与属性进行捆绑,使得属性参数化 |
v-model |
在表单控件或者组件上创建双向绑定 |
v-slot |
缩写:# ,提供具名插槽或需要接收 prop 的插槽 |
v-pre |
跳过这个元素和它的子元素的编译过程 |
v-cloak |
保持在元素上直到关联实例结束编译,然后才展示 |
v-once |
只渲染组件以及子组件一次,可用于提升渲染性能 |
指令的构成
一个指令对象一般包含有 👇 几个钩子函数:
- bind: 只调用一次,指令第一次绑定到元素时调用,一般可以在该方法中进行初始化设置动作;
- inserted: 被绑定元素插入到父节点时调用;
- update: 所在组件的VNode更新时调用,但可能发生在其子VNode更新之前,指令的值可能发生了改变,也可能没有;
- componentUpdated: 指令所在组件的VNode及其子VNode全部更新后调用;
- unbind: 指令与元素解绑时调用。
如何使用指令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| <template> <div> <p v-my-directive="value"></p> </div> </template>
<script> export default { data() { return { value: 'Hello, World!' } }, directives: { 'my-directive'(el, binding) { el.innerText = binding.value } } } </script>
|
我们自定义了一个名为 my-directive
的指令,并将其绑定到一个 p 标签上。
当组件渲染时,Vue 会自动调用 my-directive
函数,并传入该标签的元素对象和指令对象。
在该函数内部,我们可以对元素进行操作,例如将元素的 innerText
设置为指令表达式的值。
指令的解析与执行过程
😕 那么这个指令的执行过程是怎么来的呢?
我们做一个大胆的猜想,既然他是直接操作的DOM来处理的,那么在vue的渲染阶段,应该是可以看到关于指令的影子的,最后,我们在patch.js中找到关于关于指令的执行流程
钩子函数的顺序确定
1 2 3
| import baseModules from 'core/vdom/modules/index' export const patch: Function = createPatchFunction({nodeOps, modules})
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| export function createPatchFunction(backend){ const hooks = ['create', 'activate', 'update', 'remove', 'destroy'] let i, j const cbs = {} const { modules, nodeOps } = backend for(i = 0; i < hooks.length; ++ i){ cbs[hooks[i]] = [] for(j = 0; j < modules.length; ++j){ if(isDef(modules[j][hooks[i]])){ cbs[hooks[i]].push(modules[j][hooks[i]]) } } } }
|
然后,我们发现这里所缓存下来的cbs
都在各个与指令的钩子函数相关的地方有调用到:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| function invokeCreateHooks(vnode, insertedVnodeQueue){ for(let i = 0;i < cbs.create.length; ++i){ cbs.create[i](emptyNode, vnode) } }
function invokeDestroyHook(vnode){ let i, j for(i = 0;i < cbs.destroy.length; ++i) cbs.destroy[i](vnode) }
function patchVnode(...){ for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode) }
|
👉 因此,指令的钩子函数对应的触发契机也就确定下来了!!
钩子函数的被执行过程
那么这个钩子函数的执行过程是怎样的呢?请看下述的相关代码:
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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) } }
|
🌠 可以看出,最终都调用的_update(oldVnode, vnode)
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| 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){ callHook(dir, 'bind', vnode, oldVnode) }else{ dir.oldValue = oldDir.value dir.oldArg = oldDir.arg callHook(dir, 'update', vnode, oldVnode) } } }
|
这也就是bind
与update
钩子方法执行的契机,通过对比新旧虚拟node中的统一名字的指令,来判断是新增还是更新操作!
😕 那么另外的insert
以及componentUpdated
钩子方法,是在什么时候被调用的呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| for(key in newDirs){ if(!oldDir){ if(dir.def && dir.def.inserted){ dirsWithInsert.push(dir) }else{ 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]) { callHook(oldDirs[key], 'unbind', oldVnode, oldVnode, isDestroy) } } }
|
🌠 从上面的关于指令的执行过程的分析可以得知,指令其实就是一个个的具有特定意义的函数钩子所组成的函数对象集合体,它在节点的渲染期间对待操作的节点进行DOM级别的操作,从而更新的界面上!!
个别指令分析
本文从常用的指令中挑出几个来进行详细的说明,从而更加深入地理解指令设计的合理!
v-if
v-if
是一个特殊的指令,他并非在执行render()
渲染函数的时候去对应生成指令的,而是在解析html的时候就已经完成条件渲染的准备工作,如下所示:
1 2 3 4 5 6 7 8 9 10
| (function anonymous() { with (this) { return _c('div', { attrs: { "id": "app" } }, [(showMe) ? _c('p', [_v("我是被隐藏的文本")]) : _e()]) } } )
|
也就是说v-if
是生成的三目运算符方式来执行的js代码!!!
v-show
频繁的切换一个节点元素可见或者隐藏!
假如是我们自己来实现的话,应该是控制这个节点元素的display
css属性,通过控制none
以及原始属性,来达到控制一个元素显隐的效果
1 2 3 4 5 6 7 8 9 10 11 12 13
| export default{ bind(el, {value}, vnode){ const originalDislay = el.__vOriginalDisplay = el.style.display === 'none' ? '' : el.style.display if(value){ vnode.data.show = true enter(vnode, () => { el.style.display = originalDisplay }) }else{ el.style.display = value ? originalDisplay : 'none' } } }
|
从上面我们可以看出就是单纯的display
状态切换的过程!!
😕 当我们在模版中使用了指令的时候,vue将其解析成为如下的代码字符串:
👉 然后再转换为vnode来进行渲染操作,我们所传递给指令的参数
、指令修饰符
、指令参数
,都一一成为对应的指令对象的属性!
v-model
如何自定义自己的指令
1、声明指令,重载其bind
等相关钩子方法,在各个钩子方法中各自去实现对应的效果;
2、注册该指令,通过Vue.directive()
或者在组件内部声明该directives
属性;
3、在模版html中使用指令