共计 6780 个字符,预计需要花费 17 分钟才能阅读完成。
背景
看了一些自定义指令的文章,然而探索其原理的文章却不多见,所以我决定水一篇。
如何自定义指令?
其实对于这个问题官网文档上曾经有了很好的示例的,咱们先来温故一下。
除了外围性能默认内置的指令 (v-model
和 v-show
),Vue 也容许注册自定义指令。留神,在 Vue2.0 中,代码复用和形象的次要模式是组件。然而,有的状况下,你依然须要对一般 DOM 元素进行底层操作,这时候就会用到自定义指令。举个聚焦输入框的例子,如下:
当页面加载时,该元素将取得焦点 (留神:autofocus
在挪动版 Safari
上不工作)。事实上,只有你在关上这个页面后还没点击过任何内容,这个输入框就该当还是处于 聚焦状态。当初让咱们用指令来实现这个性能:
// 注册一个全局自定义指令 `v-focus`
Vue.directive('focus', {
// 当被绑定的元素插入到 DOM 中时……
inserted: function (el) {
// 聚焦元素
el.focus()}
})
如果想注册部分指令,组件中也承受一个 directives
的选项:
directives: {
focus: {
// 指令的定义
inserted: function (el) {el.focus()
}
}
}
而后你能够在模板中任何元素上应用新的 v-focus property
,如下:
<input v-focus>
指令外部提供的钩子函数
一个指令定义对象能够提供如下几个钩子函数 (均为可选):
bind:
只调用一次,指令第一次绑定到元素时调用。在这里能够进行一次性的初始化设置。inserted:
被绑定元素插入父节点时调用 (仅保障父节点存在,但不肯定已被插入文档中)。update:
所在组件的 VNode 更新时调用,然而可能产生在其子 VNode 更新之前。指令的值可能产生了扭转,也可能没有。然而你能够通过比拟更新前后的值来疏忽不必要的模板更新 (具体的钩子函数参数见下)。componentUpdated:
指令所在组件的 VNode 及 其子 VNode全副更新后调用。unbind:
只调用一次,指令与元素解绑时调用。
钩子函数参数
指令钩子函数会被传入以下参数:
el:
指令所绑定的元素,能够用来间接操作DOM
。binding:
一个对象,蕴含以下 property:name:
指令名,不包含v-
前缀。value:
指令的绑定值,例如:v-my-directive="1 + 1"
中,绑定值为 2。oldValue:
指令绑定的前一个值,仅在update
和componentUpdated
钩子中可用。无论值是否扭转都可用。expression:
字符串模式的指令表达式。例如v-my-directive="1 + 1"
中,表达式为"1 + 1"
。arg:
传给指令的参数,可选。例如v-my-directive:foo
中,参数为 “foo”。modifiers:
一个蕴含修饰符的对象。例如:v-my-directive.foo.bar
中,修饰符对象为{foo: true, bar: true}
。vnode:
Vue
编译生成的虚构节点。能够参考官网的 VNode API 来理解更多详情。oldVnode:
上一个虚构节点,仅在update
和componentUpdated
钩子中可用。
除了 el
之外,其它参数都应该是 只读的 ,切勿进行批改。如果须要在 钩子之间共享数据,倡议通过元素的 dataset
来进行。
咱们来看一个 demo
,
<div id="hook-arguments-example" v-demo:foo.a.b="message"></div>
Vue.directive('demo', {bind: function (el, binding, vnode) {
var s = JSON.stringify
el.innerHTML =
'name:' + s(binding.name) + '<br>' +
'value:' + s(binding.value) + '<br>' +
'expression:' + s(binding.expression) + '<br>' +
'argument:' + s(binding.arg) + '<br>' +
'modifiers:' + s(binding.modifiers) + '<br>' +
'vnode keys:' + Object.keys(vnode).join(',')
}
})
new Vue({
el: '#hook-arguments-example',
data: {message: 'hello!'}
})
来看一下渲染的后果:
name: "demo"
value: "hello!"
expression: "message"
argument: "foo"
modifiers: {"a":true,"b":true}
vnode keys: tag, data, children, text, elm, ns, context, fnContext, fnOptions, fnScopeId, key, componentOptions, componentInstance, parent, raw, isStatic, isRootInsert, isComment, isCloned, isOnce, asyncFactory, asyncMeta, isAsyncPlaceholder
指令实现原理解析
通过下面官网的例子和咱们平时的 coding,咱们基本上理解了 vue 的指令是如何应用的了,接下来咱们从 源码 的视角来解析其实现的原理。
Vue.directive 的定义:
function initAssetRegisters(Vue) {ASSET_TYPES.forEach(function (type) {Vue[type] = function (id, definition) {if (!definition) {return this.options[type + 's'][id]
} else {if (type === 'component') {validateComponentName(id);
}
if (type === 'component' && isPlainObject(definition)) {
definition.name = definition.name || id;
definition = this.options._base.extend(definition);
}
if (type === 'directive' && typeof definition === 'function') {
// Tip: 兼容传参
definition = {
bind: definition,
update: definition
};
}
// Tip: 贮存一个 ['component', 'directive', 'filter']
this.options[type + 's'][id] = definition;
return definition
}
};
});
}
参考 前端进阶面试题具体解答
其实这个办法比较简单,就是在vm.options.directives
挂载了一个映射,比方 vm.$options.directives.demo = {xxx}
,咱们要看看这个指令是如何失效的。
在没有下一步对源码进行剖析之前,咱们也能大略猜测出自定义指令是如何实现的。在模板编译阶段,从元素的属性中解析到指令属性,在不同生命周期元素阶段调用自定指令中不同的自定义逻辑。接下来配合源码来剖析一下,将这个指令解析和失效分为三个阶段:模板编译阶段, 生成 VNode 阶段, 以及生成实在 Dom 的 patch 阶段。
咱们以上面的代码片段为例:
<div id="hook-arguments-example" v-demo:foo.a.b="message"></div>
模板编译阶段
对模板编译不相熟的同学能够去回顾一下,这个阶段大抵做了什么。这里不去具体介绍了,只关注指令这一部分。指令是元素的属性的一部分,所以在解析标签元素时,会被放入 Ele Ast
这个对象的 attrs
属性中。上述的示例,会被解析为这样:
[{name: "id", value: "hook-arguments-example", start: 5, end: 32},
{name: "v-demo:foo.a.b", value: "message", start: 33, end: 57}
]
在匹配到完结标签时,会进一步解决这些属性,比方:如果是指令的话,会被解决为 directives
挂载到这个 Ele Ast
对象上。
具体的流程如下,在 endTagMatch
匹配到完结标签的时候,会去调用解决完结标签的 parseEndTag
函数,在这个函数外部回去调用 parseHtml
的配置项的 options.end
,其中又回去调用 closeElement
。
function closeElement(element) {
// ...
if (!inVPre && !element.processed) {element = processElement(element, options);
}
// ...
}
留神这里的 processElement
办法,次要是对解析过程中的元素进行各种加工。咱们来看一下 processElement
的代码。
function processElement(element, options) {processKey(element);
processRef(element);
processSlotContent(element);
processSlotOutlet(element);
processComponent(element);
// ...
processAttrs(element);
return element
}
次要针对一堆元素属性的解决办法,咱们须要关注 processAttrs
办法,它是解决指令和修饰符相干的办法。咱们我看一下 processAttrs
的伪代码:
function processAttrs(el) {
var list = el.attrsList;
var i, l, name, rawName, value, modifiers, syncGen, isDynamic;
for (i = 0, l = list.length; i < l; i++) {name = rawName = list[i].name;
value = list[i].value;
// Tip: 解析指令 dirRE = /^v-|^@|^:|^#/;
if (dirRE.test(name)) {
// ...
if (bindRE.test(name)) {
// 解决 v-bind 情景
if ((modifiers && modifiers.prop) || (!el.component && platformMustUseProp(el.tag, el.attrsMap.type, name)
)) {addProp(el, name, value, list[i], isDynamic);
} else {addAttr(el, name, value, list[i], isDynamic);
}
} else if (onRE.test(name)) {
// 解决 v-on 情景
addHandler(el, name, value, modifiers, false, warn$2, list[i], isDynamic);
} else {
// 解决 惯例指令情景
// Tip:给被解析到的元素,增加 directives 属性
addDirective(el, name, rawName, value, arg, isDynamic, modifiers, list[i]);
}
} else {// ... 解决 literal attribute(文字属性)
}
}
}
这里有个 for
循环去 Ele Ast
的 attrsList
,而后依照不同的正则去解析他们,别离解决 v-bind
,v-on
以及 v-xx
的情景。对于自定义的指令会通过 addDirective
给 Ele Ast
增加 directives 属性,如下:
directives = [
{arg: "foo" , end: 57, isDynamicArg: false, modifiers: { a: true, b: true}, name: "demo", rawName: "v-demo:foo.a.b", start: 33, value: "message"
}
]
在模板解析的第一段阶段指令解析为上述模样。在模板解析的第二阶段 generate
将解析失去的 Ele Ast
生成产生 vNode
的函数字符串。自定义指令也转化为上面的模式了,成为 _c
函数的第二个参数了。
"{directives:[{name:"demo",rawName:"v-demo:foo.a.b",value:(message),expression:"message",arg:"foo",modifiers:{"a":true,"b":true}}],attrs:
{"id":"hook-arguments-example"}}"
生成 vNode 阶段
在这个 render
函数生成 vNode
的阶段,生面的指令字符串会被挂载到 vNode.data.directives
属性下,
vNode.data.directives = [{
arg: "foo"
expression: "message"
modifiers: {a: true, b: true}
name: "demo"
rawName: "v-demo:foo.a.b"
value: "hello!"
}]
生成实在 Dom 的 patch 阶段
在这个由 vNode
生成实在 Dom
的阶段,createElm
会去调用 invokeCreateHooks
(调用 crate
阶段所须要的函数),会去调用 updateDirectives
函数,这外面最终会去调用 _update
咱们来看下代码:
function _update(oldVnode, vnode) {
var isCreate = oldVnode === emptyNode;
// Tip: 获取到全局上自定义的指令函数
var oldDirs = normalizeDirectives$1(oldVnode.data.directives, oldVnode.context);
var newDirs = normalizeDirectives$1(vnode.data.directives, vnode.context);
for (key in newDirs) {if (!oldDir) {
// new directive, bind
callHook$1(dir, 'bind', vnode, oldVnode);
if (dir.def && dir.def.inserted) {dirsWithInsert.push(dir);
}
} else {callHook$1(dir, 'update', vnode, oldVnode);
}
}
if (dirsWithInsert.length) {var callInsert = function () {for (var i = 0; i < dirsWithInsert.length; i++) {callHook$1(dirsWithInsert[i], 'inserted', vnode, oldVnode);
}
};
if (isCreate) {mergeVNodeHook(vnode, 'insert', callInsert);
} else {callInsert();
}
}
if (dirsWithPostpatch.length) {mergeVNodeHook(vnode, 'postpatch', function () {for (var i = 0; i < dirsWithPostpatch.length; i++) {callHook$1(dirsWithPostpatch[i], 'componentUpdated', vnode, oldVnode);
}
});
}
if (!isCreate) {for (key in oldDirs) {if (!newDirs[key]) {
// no longer present, unbind
callHook$1(oldDirs[key], 'unbind', oldVnode, oldVnode, isDestroy);
}
}
}
}
在 _update
中,normalizeDirectives$1
很重要,是它将咱们一开始全局自定义的指令函数对应到以后的节点上。此外,在不同的生命周期也会根据不同的条件去调用不同自定义指令函数。比方,不存在 oldDir
,就会去调用初始化的bind
。
总结
没有设想中的那么神秘,从一开始的 Vue.directive
全局函数的定义以及文档中给不同钩子函数的定义和灌入的参数,咱们就有了大略的思路了。通过对自定义指令实现的一步步探索,对整个 vue
的流程有了更进一步的理解。此外让我印象粗浅的是整个代码逻辑的组织,值得咱们去进去开掘和学习。