指令是带有 v- 前缀的特殊特性,当表达式的值改变时,将其产生的连带影响,响应式地做用于 DOM。
Vue2.0 内置了形如v-bind、v-on等指令,若是须要对普通 DOM 元素进行底层操做还可使用自定义指令。
javascript
在 Vue2.0 中,能够经过自定义指令对普通 DOM 元素进行底层操做。一个指令定义对象能够提供以下几个钩子函数:
html
一、bind:指令第一次绑定到元素时调用,只调用一次。
二、inserted:被绑定元素插入父节点时调用。
三、update:所在组件的VNode更新时调用。
四、componentUpdated:指令所在组件的VNode及其子VNode所有更新后调用。
五、unbind:指令与元素解绑时调用,只调用一次。
前端
经过以下示例来阐述源码中对自定义指令的处理过程:
vue
<body>
<div id="app"></div>
</body>
<script> let vm = new Vue({ el: '#app', template: '<div>' + '<input v-focus>' + '</div>', directives: { focus: { inserted: function (el) { el.focus() } } } }) </script>
复制代码
指令、组件和过滤器在Vue中称为资源。能够经过全局API进行全局注册,也能够经过具体选项进行局部注册。组件的全局注册和组件注册在《组件》一文中详细阐述过,指令的处理与之相似,这里简要说明。
自定义指令全局注册的方法以下所示:
java
Vue.directive = function (id, definition) {
if (!definition) {
return this.options.directives[id]
} else {
if (typeof definition === 'function') {
definition = { bind: definition, update: definition };
}
Vue.options.directives[id] = definition;
return definition
}
}
复制代码
Vue.directive 方法功能比较简单:将自定义指令的名字与配置对象转化成 Vue.options.directives 对象上的键值对。当配置对象为函数时,将该函数当成 bind 与 update 的钩子函数内容来处理,这是由于Vue提供了这种函数简写的方式,在《选项合并》中有过详细阐述。
使用 directives 选项来注册指令,会将自定义指令信息存储在当前组件实例的 $options.directives 对象上。
node
带有自定义指令的标签在生成AST时,会调用 processElement 函数对自定义指令进行处理。
express
function processElement (element,options) {
/* ... */
processAttrs(element);
return element
}
复制代码
processElement 函数会将标签上的属性解析到元素对象的 attrsList 与 attrsMap 属性中,而后调用 processAttrs 函数处理标签上的属性。若是有指令属性,则将其放入到元素对象的 directives 数组属性中。
模板编译的 codegen 阶段,在执行 genData 时会根据 el.directives 将指令信息存入到 el.data 字符串中。
渲染函数最终根据标签名称el.tag、标签数据el.data、子节点children共同生成。实例中的模板通过编译后生成的渲染函数以下所示:
数组
_c(
'div',
[
_c(
'input',
{
directives:[
{
name:"focus",
rawName:"v-focus"
}
]
}
)
]
)
复制代码
调用 Vue.prototype._render 方法生成VNode,本质是经过调用渲染函数来完成的。渲染函数中的 _c() 是 createElement 的别称,在函数内部经过调用 _createElement 函数来生成VNode。
app
function _createElement (context,tag,data,children,normalizationType){
/* ... */
if (config.isReservedTag(tag)) {
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,undefined, undefined, context
);
}
/* ... */
}
复制代码
根据示例的渲染函数生成的VNode以下所示:
dom
vnode = {
tag: "div",
children: [
{
tag: "input",
data: {
directives: [
{
name: "focus",
rawName: "v-focus"
}
]
}
/* 省略其它属性 */
}
]
/* 省略其它属性 */
}
复制代码
在 patch 过程当中,会调用 createElm 函数来生成真实DOM并插入到DOM树中。
function createElm (/* ... */){
/* 省略... */
createChildren(vnode, children, insertedVnodeQueue);
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue);
}
insert(parentElm, vnode.elm, refElm);
/* 省略... */
}
function invokeCreateHooks (vnode, insertedVnodeQueue) {
for (var i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, vnode);
}
i = vnode.data.hook;
if (isDef(i)) {
if (isDef(i.create)) { i.create(emptyNode, vnode); }
if (isDef(i.insert)) { insertedVnodeQueue.push(vnode); }
}
}
复制代码
关于 cbs 中各阶段的钩子函数的详细阐述可参看《Virtual DOM》。
cbs = {
create: [
/* 省略... */
function updateDirectives (oldVnode, vnode) {/*省略具体代码*/}
]
/* 省略... */
}
复制代码
在 updateDirectives 方法中,若是虚拟DOM的 data.directives 属性存在,会调用内部方法 _update 。该方法比较很重要,自定义指令提供的钩子都在该函数中进行处理,下面分步详细解读该函数:
function _update (oldVnode, vnode) {
var isCreate = oldVnode === emptyNode;
var isDestroy = vnode === emptyNode;
var oldDirs = normalizeDirectives(oldVnode.data.directives, oldVnode.context);
var newDirs = normalizeDirectives(vnode.data.directives, vnode.context);
var dirsWithInsert = [];
var dirsWithPostpatch = [];
/* 省略... */
}
复制代码
函数首先定义一些变量,变量的具体含义以下所示:
isCreate:指令所在的元素节点是否被建立。
isDestroy:指令所在的元素节点是否被销毁。
oldDirs:旧元素节点上的指令。
newDirs:新元素节点上的指令。
dirsWithInsert:拥有 inserted 钩子函数的指令。
dirsWithPostpatch:拥有 componentUpdated 钩子函数的指令。
在 _update 函数中,会调用 callHook 来调用具体的钩子函数。
function callHook (dir, hook, vnode, oldVnode, isDestroy) {
var 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"));
}
}
}
复制代码
接着说 _update 函数,在定义变量以后处理新VNode存在的状况。代码以下所示:
function _update (oldVnode, vnode) {
/* 省略... */
var key, oldDir, dir;
for (key in newDirs) {
oldDir = oldDirs[key];
dir = newDirs[key];
if (!oldDir) {
callHook(dir, 'bind', vnode, oldVnode);
if (dir.def && dir.def.inserted) {
dirsWithInsert.push(dir);
}
} else {
dir.oldValue = oldDir.value;
dir.oldArg = oldDir.arg;
callHook(dir, 'update', vnode, oldVnode);
if (dir.def && dir.def.componentUpdated) {
dirsWithPostpatch.push(dir);
}
}
}
/* 省略... */
}
复制代码
当新VNode存在而旧VNode不存在时,说明新VNode是新建立的,未与自定义指令绑定,此时第一次绑定调用 bind 钩子函数,如有 inserted 钩子函数,则将指令存入 dirsWithInsert 数组。
当新VNode和旧VNode都存在时,说明是在进行VNode更新。此时调用 update 钩子函数,如有 componentUpdated 钩子函数,则将指令存入 dirsWithPostpatch 数组。
而后是对 inserted 与 componentUpdated 钩子函数的处理:
function _update (oldVnode, vnode) {
/* 省略... */
if (dirsWithInsert.length) {
var callInsert = function () {
for (var 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', function () {
for (var i = 0; i < dirsWithPostpatch.length; i++) {
callHook(dirsWithPostpatch[i], 'componentUpdated', vnode, oldVnode);
}
});
}
/* 省略... */
}
复制代码
mergeVNodeHook 函数接收三个参数:def、 hookKey、hook。若是第一个参数 def 是VNode类型,则会替换成 def.data.hook。mergeVNodeHook 的功能是:若是def[hookKey] 不存在,则直接调用hook,若是存在则将hook合并存储起来,在后续合适时机调用。
由代码能够看出,对指令 inserted 钩子函数的处理是:若VNode是新建立的,则会把 dirsWithInsert 数组中的函数追加到 vnode.data.hook.insert 中执行。若是是更新VNode,则直接执行钩子函数。
对指令 componentUpdated 钩子函数的处理是:使用 mergeVNodeHook 函数进行处理,等待后面子组件所有更新完成后调用。
function _update (oldVnode, vnode) {
/* 省略... */
if (!isCreate) {
for (key in oldDirs) {
if (!newDirs[key]) {
callHook(oldDirs[key], 'unbind', oldVnode, oldVnode, isDestroy);
}
}
}
}
复制代码
_update 函数的最后是对 unbind 钩子函数的处理,在旧VNode存在而新VNode不存在时,即指令与元素解绑时调用 unbind 钩子函数。
使用 v-bind 指令能够动态地绑定一个或多个特性,或一个组件 prop 到表达式,v-bind 指令能够简写为 :。由于字符串拼接麻烦且易错,在将 v-bind 用于 class 和 style 时,Vue 作了专门的加强。因此 v-bind 指令的使用分为三种状况:普通属性、class、style。
示例代码以下所示:
<body>
<div id="app"></div>
</body>
<script></script>
复制代码
在模板编译的 parse 阶段会调用 processElement 函数,在该函数的最后分别调用 transforms 数组中的函数来解析 v-bind 绑定的 class 和 style,最后用 processAttrs 函数来解析 v-bind 绑定的普通属性。
function processElement (element,options) {
/* 省略... */
for (var i = 0; i < transforms.length; i++) {
element = transforms[i](element, options) || element;
}
processAttrs(element);
return element
}
transforms = [
function transformNode (el, options) {
/* ... */
if(staticClass){el.staticClass=JSON.stringify(staticClass);}
var classBinding = getBindingAttr(el, 'class', false);
if(classBinding){el.classBinding = classBinding;}
},
function transformNode (el, options) {
/* ... */
var styleBinding = getBindingAttr(el, 'style', false);
if (styleBinding) {el.styleBinding = styleBinding;}
}
]
复制代码
通过 processElement 函数处理后,v-bind 绑定的普通属性会存入元素节点的 attrs 属性中,class 与 style 会分别存入 classBinding 与 styleBinding 中。
ast = {
tag: "div",
children: [
{
tag: "div",
hasBindings: true,
attrs: [{/* 省略属性id详情 */}],
attrsList: [{/* 省略属性id详情 */}],
attrsMap: {
:class: "{ red: isRed }",
:style: "{ fontSize: size + 'px' }",
v-bind:id: "id"
},
rawAttrsMap: {
/* 省略属性v-bind:id、:class、:style详情 */
}
styleBinding: "{ fontSize: size + 'px' }",
classBinding: "{ red: isRed }"
/* 省略其它属性... */
}
]
/* 省略其它属性... */
}
复制代码
在模板编译的 codegen 阶段会调用 genElement 函数,并在该函数中调用 genData 函数来将 v-bind 绑定的普通属性、class 与 style 合并到 data 中。示例最终生成的渲染函数以下所示:
_c(
'div',
[
_c(
'div',
{
class:{ red: isRed },
style:({ fontSize: size + 'px' }),
attrs:{"id":id}
},
[_v("666")]
)
]
)
复制代码
渲染函数通过 Vue.prototype._render 函数处理后生成 VNode,_c() 函数的第二个参数会处理成元素标签 VNode 的 data 属性。
vnode = {
tag: "div",
children: [
{
tag: "div",
data: {
attrs: {id: 123}
class: {red: true}
style: {fontSize: "24px"}
}
/* 省略其它属性... */
}
]
/* 省略其它属性... */
}
复制代码
在 patch 阶段,会的调用 createElm 函数生成真实 DOM,在createElm 函数中生成真实 DOM 后会调用 invokeCreateHooks 来对 data 中的数据进行处理。
function createElm (/*...*/){
/*...*/
var data = vnode.data;
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue);
}
/*...*/
}
function invokeCreateHooks (vnode, insertedVnodeQueue) {
for (var i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, vnode);
}
i = vnode.data.hook;
if (isDef(i)) {
if(isDef(i.create)){i.create(emptyNode, vnode);}
if(isDef(i.insert)){insertedVnodeQueue.push(vnode);}
}
}
复制代码
关于 cbs 的具体组成能够查看《Virtual DOM》 一文,跟 v-bind 相关的部分以下所示:
cbs = {
create: [
function updateAttrs (oldVnode, vnode) {/*省略具体代码*/},
function updateClass (oldVnode, vnode) {/*省略具体代码*/},
function updateStyle (oldVnode, vnode) {/*省略具体代码*/},
/* updateDOMProps函数做用是更新一些特殊的属性: 不能经过 setAttribute 设置, 而是应该直接经过 DOM 元素设置的属性。 好比:value、checked等 */
function updateDOMProps (oldVnode, vnode) {/*省略具体代码*/},
/* 省略其它函数 */
]
/* 省略其它属性 */
复制代码
使用 v-bind 指令修饰符 .prop 绑定的属性会放入 vnode.data.domProps 中,使用 updateDOMProps 进行处理,这里省略具体处理逻辑。
对普通属性的处理函数 updateAttrs 逻辑比较简单:对比新旧VNode,来决定增长仍是删除属性,增长属性调用原生DOM的 setAttribute 方法,删除属性调用原生DOM的 removeAttribute 方法。在该函数中有对 IE 的兼容处理。
function updateAttrs (oldVnode, vnode) {
/* 省略... */
var oldAttrs = oldVnode.data.attrs || {};
var attrs = vnode.data.attrs || {};
for (key in attrs) {
cur = attrs[key];
old = oldAttrs[key];
if (old !== cur) {
setAttr(elm, key, cur);
}
}
for (key in oldAttrs) {
if (isUndef(attrs[key])) {
if (isXlink(key)) {
elm.removeAttributeNS(xlinkNS, getXlinkProp(key));
} else if (!isEnumeratedAttr(key)) {
elm.removeAttribute(key);
}
}
}
/* 省略... */
}
复制代码
对 class 处理的函数 updateClass 逻辑是:将Vue中使用的静态类staticClass 与使用响应式数据相关的 dynamicClass 统一块儿来,而后和普通属性同样调用 DOM 原生方法 setAttribute 添加类名。
function updateClass (oldVnode, vnode) {
/* 省略.... */
var cls = genClassForVnode(vnode);
var transitionClass = el._transitionClasses;
if (isDef(transitionClass)) {
cls = concat(cls, stringifyClass(transitionClass));
}
if (cls !== el._prevClass) {
el.setAttribute('class', cls);
el._prevClass = cls;
}
}
复制代码
对 style 处理的函数 updateStyle 有一点比较特殊,设置 style 属性时是调用 dom.style.setProperty 方法。
v-bind 指令中还有一点须要注意:能够添加 .sync 修饰符对一个 prop 进行“双向绑定”。.sync 修饰符实质上是语法糖,会扩展成一个更新父组件绑定值的 v-on 侦听器,v-on 指令将在下一节详细介绍。
// 语法糖
<Child v-bind:val.sync = parentVal></Child>
// 至关于下面代码
<Child v-bind:val = parentVal
@updateVal = "parentVal.a=$event">
</Child>
复制代码
在 Vue 中用 v-on 指令监听 DOM 事件,并在触发时运行一些 JavaScript 代码。由于事件指令使用较多,Vue提供了简写形式:@。
示例代码以下所示:
<body>
<div id="app"></div>
</body>
<script> let Child = { template: `<div @click="changeVal">点击</div>`, props: ['val'], methods: { changeVal () { this.$emit('updateVal', ++this.val.a) } } } let vm = new Vue({ el: '#app', template: `<div> <Child v-bind:val = parentVal @updateVal = "parentVal.a=$event" @mouseover.native = "printMsg" ></Child> <div v-on:mouseover = "showMsg" @mouseout.stop = "hideMsg"> {{parentVal.a}} </div> <div>{{message}}</div> </div>`, data() { return { parentVal: { a: 1 }, message: '离开' } }, methods: { printMsg (){console.log(this.message)}, showMsg () { this.message = '进入' }, hideMsg () { this.message = '离开' }, }, components: { Child } }) </script>
复制代码
在模板编译的 parse 阶段,会调用 processAttrs 处理事件属性。通过 processAttrs 函数处理后会为不带 native 修饰符的节点添加 events 属性,events 对象包含各个事件信息,其中修饰符存储在事件对象的 modifiers 属性中。对于在组件上带 native 修饰符的DOM事件则存储在 nativeEvents 属性中。
通过 parse 生成的 AST 以下所示:
ast = {
tag: "div",
children: [
{
tag: "Child",
attrs: {
name: "val",
value: "parentVal"
/* 省略其它属性 */
},
events: {
updateVal: {
value: "parentVal.a=$event"
/* 省略其它属性 */
}
},
nativeEvents: {
mouseover: {/* 省略具体属性 */}
}
},
{
tag: "div",
events: {
mouseout: {
value: "hideMsg"
modifiers: {stop: true}
/* 省略其它属性 */
},
mouseover: {
value: "showMsg"
/* 省略其它属性 */
}
}
}
/* 省略其它子节点 */
]
/* 省略其它属性 */
}
复制代码
在 codegen 阶段调用 genElement 生成渲染函数字符串时,会调用 genData 方法将普通事件信息存储到 data.on 属性中,将组件上的DOM原生事件存储到 data.nativeOn 属性中。最终生成以下渲染函数:
_c(
'div',
[
_c(
'Child',
{
attrs:{"val":parentVal},
on:{
"updateVal":function($event){
parentVal.a=$event
}
},
nativeOn:{
"mouseover":function($event){
return printMsg($event)
}
}
}
),
_c(
'div',
{
on:{
"mouseover":showMsg,
"mouseout":function($event){
$event.stopPropagation();
return hideMsg($event)
}
}
},
/* 省略子节点渲染函数 */
)
/* 省略其它子节点渲染函数 */
],
1
)
复制代码
在生成VNode的过程当中会调用 _createElement 函数生成元素 VNode,具体实现是经过调用 new VNode() 完成的。VNode() 构造函数会根据类型的不一样而作出不一样处理,若是是元素VNode则将事件信息直接放到 data 属性上,若是是组件VNode则将其放到 componentOptions.listeners 上,对于组件上的原生DOM属性,则将其从 data.nativeOn 复制到 data.on 上。
根据渲染函数生成的VNode以下所示:
vnode = {
tag: "div",
children: [
{
tag: "vue-component-1-Child",
data: {
attrs: {},
on: {
mouseover: function($event){
return printMsg($event)
}
},
nativeOn: {
mouseover: function($event){
return printMsg($event)
}
},
hook: {
destroy: function(){},
init: function(){},
insert: function(){},
prepatch: function(){}
}
},
componentOptions: {
tag: "Child",
listeners: {
updateVal: function($event){parentVal.a=$event}
}
/* 省略其它属性 */
}
/* 省略其它属性 */
},
{
tag: "div",
data: {
on: {
mouseout: function($event){
$event.stopPropagation();
return hideMsg($event)
},
mouseover: function () { [native code] }
}
}
/* 省略其它属性 */
}
/* 省略其它子节点 */
]
/* 省略其它属性 */
}
复制代码
在 patch 阶段对元素标签上 v-on 的处理跟前面提到的自定义指令、v-bind相似,最终会调用 cbs.create.updateDOMListeners 来处理事件。
function updateDOMListeners (oldVnode, vnode) {
/* ... */
var on = vnode.data.on || {};
var oldOn = oldVnode.data.on || {};
target = vnode.elm;
normalizeEvents(on);
updateListeners(on,oldOn,add,remove,createOnceHandler,vnode.context);
target = undefined;
}
function updateListeners(on,oldOn,add,remove,createOnceHandler,vm){
var name, def, cur, old, event;
for (name in on) {
def = cur = on[name];
old = oldOn[name];
event = normalizeEvent(name);
if (isUndef(cur)) {
/* ... */
} else if (isUndef(old)) {
if (isUndef(cur.fns)) {
cur = on[name] = createFnInvoker(cur, vm);
}
if (isTrue(event.once)) {
cur = on[name] = createOnceHandler(event.name, cur, event.capture);
}
add(event.name, cur, event.capture, event.passive, event.params);
} else if (cur !== old) {
old.fns = cur;
on[name] = old;
}
}
for (name in oldOn) {
if (isUndef(on[name])) {
event = normalizeEvent(name);
remove(event.name, oldOn[name], event.capture);
}
}
}
复制代码
updateDOMListeners 函数做用是提取出新旧VNode的 on 属性,规范化新节点的 on 属性后调用 updateListeners 函数来处理。
updateListeners 主要功能是对比新旧VNode,若是新节点须要添加事件就调用 add 方法,本质是调用原生DOM的 addEventListener 方法为元素添加事件监听。若是新节点须要移除事件,就调用 remove,本质是调用原生DOM的 removeEventListener 方法删除元素上的事件监听。另外,updateListeners 函数中也有对各类修饰符的处理。
在 patch 阶段对组件上 v-on 的处理分为两种:对原生DOM事件的处理、自定义事件的处理。原生DOM事件存储在 data.on 上,所以处理方式与元素标签的状况同样。
在《选项合并》中,讲述实例初始化方法 Vue.prototype._init 时跳过了处理组件的代码:
Vue.prototype._init = function (options) {
/* 省略.... */
if (options && options._isComponent) {
initInternalComponent(vm, options);
}
/* 省略.... */
}
function initInternalComponent (vm, options) {
var opts = vm.$options = Object.create(vm.constructor.options);
var parentVnode = options._parentVnode;
/* 省略.... */
var vnodeComponentOptions = parentVnode.componentOptions;
opts._parentListeners = vnodeComponentOptions.listeners;
/* 省略.... */
}
复制代码
通过 initInternalComponent 函数处理后会将父组件的 componentOptions.listeners 赋值给子组件的 _parentListeners 属性。在子组件调用初始化事件函数 initEvents 时会处理 listeners。
function initEvents (vm) {
vm._events = Object.create(null);
vm._hasHookEvent = false;
var listeners = vm.$options._parentListeners;
if (listeners) {
updateComponentListeners(vm, listeners);
}
}
function updateComponentListeners(vm,listeners,oldListeners){
target = vm;
updateListeners(listeners,oldListeners||{},add,remove,createOnceHandler,vm);
target = undefined;
}
复制代码
从上述代码中能够看出,自定义事件的处理最终是经过 updateListeners 函数来完成的:
function updateListeners(on,oldOn,add,remove,createOnceHandler,vm){
var name, def, cur, old, event;
for (name in on) {
def = cur = on[name];
old = oldOn[name];
event = normalizeEvent(name);
if (isUndef(cur)) {
/* 省略... */
} else if (isUndef(old)) {
if (isUndef(cur.fns)) {
cur = on[name] = createFnInvoker(cur, vm);
}
if (isTrue(event.once)) {
cur = on[name] = createOnceHandler(event.name, cur, event.capture);
}
add(event.name,cur,event.capture,event.passive,event.params);
} else if (cur !== old) {
old.fns = cur;
on[name] = old;
}
}
for (name in oldOn) {
if (isUndef(on[name])) {
event = normalizeEvent(name);
remove(event.name,oldOn[name],event.capture);
}
}
}
复制代码
自定义事件与原生DOM事件处理的最大不一样就是调用的添加事件函数 add() 与删除事件函数 remove()不一样。
function add (event, fn) {
target.$on(event, fn);
}
function remove (event, fn) {
target.$off(event, fn);
}
复制代码
自定义事件的添加与删除最终是调用了实例方法 $on 与 $off 来完成的,自定义事件的触发是调用实例方法 $emit 来完成,这些实例方法都是暴露出来的API,其实现原理在下一篇文章详述。
Vue 使用 v-for 指令完成基于源数据屡次渲染元素或模板块的功能,循环渲染的数据源能够是数组或者对象,2.6版本以后数据源也能够是可迭代的值,好比原生的 Map 和 Set。
示例代码以下所示:
<body>
<div id="app"></div>
</body>
<script> let vm = new Vue({ el: '#app', template: `<div> <div v-for="(item,index) in colors" :key="index"> {{index}}:{{item}} </div><div v-for="(item,name,index) in object" :key="name"> {{index}}:{{name}}:{{item}} </div> </div>`, data() { return { colors: ['red','blue','green'], object: { title: 'How to do lists in Vue', author: 'Jane Doe', publishedAt: '2016-04-10' } } } }) </script>
复制代码
通过模板编译处理后,生成以下渲染函数:
_c(
'div',
[
_l(
(colors),
function(item,index){
return _c(
'div',
{key:index},
[_v("\n"+_s(index)+":"+_s(item)+"\n")]
)
}
),
_l(
(object),
function(item,name,index){
return _c(
'div',
{key:name},
[_v("\n "+_s(index)+":"+_s(name)+":"+_s(item)+"\n")]
)
}
)
],
2
)
复制代码
能够看到,v-for 所在的模板最终会转化成 _l() 函数,_l() 函数是 renderList 的别称。
function renderList (val, render) {
var ret, i, l, keys, key;
if (Array.isArray(val) || typeof val === 'string') {
ret = new Array(val.length);
for (i = 0, l = val.length; i < l; i++) {
ret[i] = render(val[i], i);
}
} else if (typeof val === 'number') {
ret = new Array(val);
for (i = 0; i < val; i++) {
ret[i] = render(i + 1, i);
}
} else if (isObject(val)) {
if (hasSymbol && val[Symbol.iterator]) {
ret = [];
var iterator = val[Symbol.iterator]();
var result = iterator.next();
while (!result.done) {
ret.push(render(result.value, ret.length));
result = iterator.next();
}
} else {
keys = Object.keys(val);
ret = new Array(keys.length);
for (i = 0, l = keys.length; i < l; i++) {
key = keys[i];
ret[i] = render(val[key], key, i);
}
}
if (!isDef(ret)) {
ret = [];
}
(ret)._isVList = true;
return ret
}
复制代码
renderList 函数主要功能是生成 VNode 数组,其中具体 VNode 的生成依然是经过 _c() 函数来完成的。
由上述代码能够看出,v-for 指令能够处理的数据源类型有四种:数组、数字、可迭代对象与普通对象。
v-if 指令根据表达式的值的真假条件渲染元素。若是元素是 <template> ,将提出它的内容做为条件块。v-else 指令来表示 v-if 的“else 块”,v-else-if 指令充当 v-if 的“else-if 块”,能够连续使用。
示例代码以下所示:
<body>
<div id="app"></div>
</body>
<script> let vm = new Vue({ el: '#app', template: `<div> <div v-if="type === 'A'">A</div> <div v-else-if="type === 'B'">B</div> <div v-else>C</div> <div v-if="color === 'blue'">blue</div> </div>`, data() { return { type: 'A', color: 'blue' } } }) </script>
复制代码
在模板编译的 parse 阶段,会使用 processIfConditions 函数处理条件渲染指令的内容。
function processIfConditions (el, parent) {
var prev = findPrevElement(parent.children);
if (prev && prev.if) {
addIfCondition(prev, {
exp: el.elseif,
block: el
});
} else {
/* 省略警告信息 */
}
}
复制代码
生成的 AST 以下所示:
ast = {
tag: "div",
type: 1,
children: [
{
type: 1,
tag: "div",
if: "type === 'A'",
ifProcessed: true,
ifConditions: [
{
exp: "type === 'A'",
block: {/* 省略具体 */}
},
{
exp: "type === 'B'",
block: {/* 省略具体 */}
},
{
exp: undefined,
block: {/* 省略具体 */}
}
]
},
{
type: 1,
tag: "div",
if: "color === 'blue'",
ifProcessed: true,
ifConditions: [
{
exp: "color === 'blue'",
block: {/* 省略具体 */}
}
]
}
]
/* 省略其它属性 */
}
复制代码
在模板编译的 codegen 阶段,会调用 genIf 函数处理 v-if 所在的标签:
function genIf(el,state,altGen,altEmpty){
el.ifProcessed = true;
return genIfConditions(el.ifConditions.slice(),state,altGen,altEmpty)
}
function genIfConditions(conditions,state,altGen,altEmpty){
if (!conditions.length) {
return altEmpty || '_e()'
}
var condition = conditions.shift();
if (condition.exp) {
return ("(" + (condition.exp) + ")?" + (genTernaryExp(condition.block)) + ":" + (genIfConditions(conditions, state, altGen, altEmpty)))
} else {
return ("" + (genTernaryExp(condition.block)))
}
function genTernaryExp (el) {
return altGen
? altGen(el, state)
: el.once
? genOnce(el, state)
: genElement(el, state)
}
}
复制代码
从代码中能够看出,v-if 指令会转化成三目运算符的形式,最终生成的渲染函数以下所示:
_c(
'div',
[
(type === 'A')?_c('div',[_v("A")]):(type === 'B')?_c('div',[_v("B")]):_c('div',[_v("C")]),
_v(" "),
(color === 'blue')?_c('div',[_v("blue")]):_e()
]
)
复制代码
带有 v-if 指令的模板会编译成根据数据源真假值来调用具体辅助方法的渲染函数,v-if 会根据数据源真假值来决定是否渲染该节点,这一点与 v-show 不一样。
v-show 指令根据表达式之真假值,切换元素的 display CSS 属性。当条件变化时该指令触发过渡效果。
v-if 是“真正”的条件渲染,由于它会确保在切换过程当中条件块内的事件监听器和子组件适当地被销毁和重建。v-show 就简单得多:无论初始条件是什么,元素老是会被渲染,而且只是简单地基于 CSS 进行切换。
示例代码以下所示:
<body>
<div id="app"></div>
</body>
<script></script>
复制代码
在模板编译和生成VNode的过程当中,v-show指令与自定义指令的过程同样,示例生成的渲染函数以下所示:
_c(
'div',
[
_c(
'h1',
{
directives:[
{
name:"show",
rawName:"v-show",
value:(hello),
expression:"hello"
}
]
},
[_v("Hello")]
),
_v(" "),
_c(
'h1',
{
directives:[
{
name:"show",
rawName:"v-show",
value:(world),
expression:"world"
}
]
},
[_v("World")]
)
]
)
复制代码
在调用处理指令的钩子函数 updateDirectives 时,v-show 指令有所不一样,至关于 v-show 内部实现了自定义指令的 bind、update、unbind 三个阶段的钩子函数。
export default {
bind (el, { value }, vnode) {
vnode = locateNode(vnode)
const transition = vnode.data && vnode.data.transition
const originalDisplay = el.__vOriginalDisplay =
el.style.display === 'none' ? '' : el.style.display
if (value && transition) {
vnode.data.show = true
enter(vnode, () => {
el.style.display = originalDisplay
})
} else {
el.style.display = value ? originalDisplay : 'none'
}
},
update (el, { value, oldValue }, vnode) {
if (!value === !oldValue) return
vnode = locateNode(vnode)
const transition = vnode.data && vnode.data.transition
if (transition) {
vnode.data.show = true
if (value) {
enter(vnode, () => {
el.style.display = el.__vOriginalDisplay
})
} else {
leave(vnode, () => {
el.style.display = 'none'
})
}
} else {
el.style.display = value ? el.__vOriginalDisplay : 'none'
}
},
unbind (el,binding,vnode,oldVnode,isDestroy){
if (!isDestroy) {
el.style.display = el.__vOriginalDisplay
}
}
}
复制代码
从上述代码能够看到,v-show 指令仅仅是经过调用 DOM.style.display 的值来显示和隐藏DOM元素。关于 v-show 指令触发过渡效果的原理在《内置组件》一文中已经阐述过。
v-model 指令用于在表单控件或者组件上建立双向绑定,所谓双向绑定是指除了数据驱动视图改变外,DOM视图的改变也会引发数据的改变。
v-model 指令能够在表单控件和组件上使用,表单控件包含有:<input>、<select>、<textarea>,能够在指令后添加修饰符:
.lazy:取代 input 监听 change 事件。
.number:输入字符串转为有效的数字。
.trim:输入首尾空格过滤。
v-model 会忽略全部表单元素的 value、checked、selected 特性的初始值而老是将 Vue 实例的数据做为数据来源。v-model 在内部为不一样的输入元素使用不一样的属性并抛出不一样的事件:
一、text 和 textarea 元素使用 value 属性和 input 事件。
二、checkbox 和 radio 使用 checked 属性和 change 事件。
三、select 字段将 value 做为 prop 并将 change 做为事件。
v-model 指令表单元素上的使用示例以下所示:
<input v-model="message" placeholder="edit me">
<p>Message is: {{ message }}</p>
<input type="checkbox" id="checkbox" v-model="checked">
<label for="checkbox">{{ checked }}</label>
复制代码
在组件上使用 v-model 默认会利用名为 value 的 prop 和名为 input 的事件,可是像单选框、复选框等类型的输入控件可能会将 value 特性用于不一样的目的。model 选项能够用来避免这样的冲突:
Vue.component('base-checkbox', {
model: {
prop: 'checked',
event: 'change'
},
props: {
checked: Boolean
},
template: ` <input type="checkbox" v-bind:checked="checked" v-on:change="$emit('change', $event.target.checked)"> `
})
<base-checkbox v-model="lovingVue"></base-checkbox> 复制代码
这里的 lovingVue 的值将会传入这个名为 checked 的 prop。同时当 <base-checkbox> 触发一个 change 事件并附带一个新的值的时候,这个 lovingVue 的属性将会被更新。
尽管 model 选项中已经声明了 prop 属性,可是仍须要在组件的 props 选项里声明 checked 这个 prop。
这里借用官网的示例来阐述 v-model 指令在表单元素:
<body>
<div id="app"></div>
</body>
<script></script>
复制代码
在模板编译的 parse 阶段,v-model 与前面讲的指令同样,会被 processAttrs 函数将其放入到元素对象的 directives 数组属性中。而后在 codegen 阶段调用 genDirectives 函数来处理指令:
function genDirectives (el, state) {
var dirs = el.directives;
if (!dirs) { return }
var res = 'directives:[';
var hasRuntime = false;
var i, l, dir, needRuntime;
for (i = 0, l = dirs.length; i < l; i++) {
dir = dirs[i];
needRuntime = true;
var gen = state.directives[dir.name];
if (gen) {
needRuntime = !!gen(el, dir, state.warn);
}
if (needRuntime) {
hasRuntime = true;
res += "{name:\"" + (dir.name) + "\",rawName:\"" + (dir.rawName) + "\"" + (dir.value ? (",value:(" + (dir.value) + "),expression:" + (JSON.stringify(dir.value))) : '') + (dir.arg ? (",arg:" + (dir.isDynamicArg ? dir.arg : ("\"" + (dir.arg) + "\""))) : '') + (dir.modifiers ? (",modifiers:" + (JSON.stringify(dir.modifiers))) : '') + "},";
}
}
if (hasRuntime) {
return res.slice(0, -1) + ']'
}
}
复制代码
v-model 指令比较特殊的地方在于 state.directives.model 函数是真实存在的,也就是说 gen 的值为 true。
var gen = state.directives.model;
复制代码
state.directives.model 函数以下所示:
function model (el,dir,_warn) {
warn = _warn;
var value = dir.value;
var modifiers = dir.modifiers;
var tag = el.tag;
var type = el.attrsMap.type;
/* 省略警告信息 */
if (el.component) {
genComponentModel(el, value, modifiers);
return false
} else if (tag === 'select') {
genSelect(el, value, modifiers);
} else if (tag === 'input' && type === 'checkbox') {
genCheckboxModel(el, value, modifiers);
} else if (tag === 'input' && type === 'radio') {
genRadioModel(el, value, modifiers);
} else if (tag === 'input' || tag === 'textarea') {
genDefaultModel(el, value, modifiers);
} else if (!config.isReservedTag(tag)) {
genComponentModel(el, value, modifiers);
return false
} else {
/* 省略警告信息 */
}
return true
}
复制代码
示例代码 tag 值为 input,所以会调用 genDefaultModel 方法:
function genDefaultModel (el,value,modifiers) {
var type = el.attrsMap.type;
/* 省略v-bind与v-model值有冲突的警告信息 */
var ref = modifiers || {};
var lazy = ref.lazy;
var number = ref.number;
var trim = ref.trim;
var needCompositionGuard = !lazy && type !== 'range';
var event = lazy
? 'change'
: type === 'range'
? RANGE_TOKEN
: 'input';
var valueExpression = '$event.target.value';
if (trim) {
valueExpression = "$event.target.value.trim()";
}
if (number) {
valueExpression = "_n(" + valueExpression + ")";
}
var code = genAssignmentCode(value, valueExpression);
if (needCompositionGuard) {
code = "if($event.target.composing)return;" + code;
}
addProp(el, 'value', ("(" + value + ")"));
addHandler(el, event, code, null, true);
if (trim || number) {
addHandler(el, 'blur', '$forceUpdate()');
}
}
复制代码
genDefaultModel 函数会根据指令的修饰符来进行分别处理,该函数的核心代码以下所示:
addProp(el, 'value', ("(" + value + ")"));
addHandler(el, event, code, null, true);
复制代码
其实不只仅是 genDefaultModel 函数,model函数中处理其他几种状况的函数本质也是调用,addProp 与 addHandler 函数,这两个函数分别将 v-model 绑定的值放入 el.props 与 el.events 中,进而转化成对 v-bind 与 v-on 指令的处理。示例代码生成的渲染函数以下所示:
_c(
'div',
[
_c(
'input',
{
directives:[
{
name:"model",
rawName:"v-model",
value:(message),
expression:"message"
}
],
attrs:{
"placeholder":"edit me"
},
domProps:{
"value":(message)
},
on:{
"input":function($event){
if($event.target.composing)return;
message=$event.target.value
}
}
}
),
_v(" "),
_c('p',[_v("Message is: "+_s(message))])
]
)
复制代码
由此能够看出:v-model 本质上一个语法糖,在模板编译的阶段会被拆分,分别被当作v-bind与v-on指令处理。
依旧借用官网实例来阐述 v-model 指令在组件上的使用状况:
<body>
<div id="app"></div>
</body>
<script></script>
复制代码
在模板编译的 codegen 阶段依旧是调用 genDirectives 函数,与在表单元素上状况不一样的在 model 中最终会调用 genComponentModel 方法:
function genComponentModel(el,value,modifiers) {
var ref = modifiers || {};
var number = ref.number;
var trim = ref.trim;
var baseValueExpression = '$$v';
var valueExpression = baseValueExpression;
if (trim) {
valueExpression =
"(typeof " + baseValueExpression + " === 'string'" +
"? " + baseValueExpression + ".trim()" +
": " + baseValueExpression + ")";
}
if (number) {
valueExpression = "_n(" + valueExpression + ")";
}
var assignment = genAssignmentCode(value, valueExpression);
el.model = {
value: ("(" + value + ")"),
expression: JSON.stringify(value),
callback: ("function (" + baseValueExpression + ") {" + assignment + "}")
};
}
复制代码
通过 genComponentModel 函数处理后父组件节点上会添加 model 属性。在 parse 后续阶段会调用 genData 函数,其中有对节点含有 model 属性状况的处理:
function genData (el, state) {
var data = '{';
/* 省略... */
if (el.model) {
data += "model:{value:" + (el.model.value) + ",callback:" + (el.model.callback) + ",expression:" + (el.model.expression) + "},";
}
/* 省略... */
return data
}
复制代码
示例代码最终生成的渲染函数以下所示:
_c(
'div',
[
_c(
'base-checkbox',
{
model:{
value:(lovingVue),
callback:function ($$v) {
lovingVue=$$v
},
expression:"lovingVue"
}
}
),
_v(" "),
_c('div',[_v(_s(lovingVue))])
],
1
)
复制代码
在根据渲染函数生成 VNode 的过程当中,会调用 createComponent 函数生成组件类型VNode:
function createComponent(Ctor,data,context,children,tag){
/* 省略... */
if (isDef(data.model)) {
transformModel(Ctor.options, data);
}
/* 省略... */
}
复制代码
若组件渲染函数第二个参数对象上有 model 属性时会调用 transformModel 函数进行处理:
function transformModel (options, data) {
var prop = (options.model && options.model.prop) || 'value';
var event = (options.model && options.model.event) || 'input'
;(data.attrs || (data.attrs = {}))[prop] = data.model.value;
var on = data.on || (data.on = {});
var existing = on[event];
var callback = data.model.callback;
if (isDef(existing)) {
if (
Array.isArray(existing)
? existing.indexOf(callback) === -1
: existing !== callback
) {
on[event] = [callback].concat(existing);
}
} else {
on[event] = callback;
}
}
复制代码
transformModel 函数将 data.model.value 赋值给 data.props、将 data.model.callback 赋值给 data.on。data.props 与 data.on 中的属性名是由 model 选项的值来决定,若是不传该选项则默认 prop 为 value,event 为 input。
由上可知,v-model 在组件上使用时最终也会转化成 v-bind 与 v-on 状况,只是与 v-model 在表单元素上使用时在模板编译阶段转化不一样,在组件上使用时是在生成 VNode 阶段转换的。
自定义指令用于对普通 DOM 元素进行底层操做,全局注册会将自定义指令信息存储在 Vue.options.directives 对象上,局部注册会将信息存储在组件实例的 $options.directives 对象上。在根据 VNode 生成真实DOM过程当中,会在合适的时机调用不一样的钩子函数。
v-bind指令的使用分为三种状况:普通属性、class、style。普通属性与class是经过原生DOM的 setAttribute 与 removeAttribute方法添加和移除的;而设置 style 属性时是调用原生DOM的 style.setProperty 方法。
v-on指令用于绑定事件监听器,原生DOM事件主要经过原生的 addEventListener 与 removeEventListener 方法来添加和删除的。自定义事件是利用 Vue 定义的事件中心来实现的。
v-for指令基于源数据屡次渲染元素或模板块,其实现思路是在渲染函数生成VNode时,根据循环条件来生成多个 VNode。
v-if指令根据表达式的值的真假条件渲染元素,v-if 指令生成的渲染函数是三目运算符的形式,会根据数据的真假条件来生成对应的VNode。
v-show指令只是简单地切换元素的 CSS 属性 display,其内部实现至关于实现了 bind、update、unbind 钩子函数的自定义指令。若在 Transition 组件上使用则调用 enter 与 leave 函数完成过渡效果。
v-model指令用于在表单控件或者组件上建立双向绑定,其本质是一个语法糖,会转换成 v-bind 与 v-on 指令处理。在表单元素上使用时,这种转化在模板编译阶段进行;在组件上使用时,是在根据渲染函数生成 VNode 阶段进行。
欢迎关注公众号:前端桃花源,互相交流学习!