Description
在上面主要谈了下vue整个compile编译过程,其实最主要任务就是提取节点属性、根据属性创建成对应的指令directive实例并保存到this.directives
数组中,并在执行生成的linker的时候,将this.directives
中新的指令进行初始化绑定_bind
;那这儿主要谈下directive相关的知识;
在前面说过,自定义组件的渲染其实也是通过指令的方式完成的,那这儿就以组件渲染过程来进行说明,如下组件:
// hello.vue
<template>
<div class="hello">
<h1>hello, welcome here</h1>
</div>
</template>
// app.vue
<hello @click.stop="hello" style="color: red" class="hello1" :class="{'selected': true}"></hello>
对于自定义组件的整个编译过程,在前面已经说过了,在这就不说了,主要说下如何通过指令将真正的html添加到对应的文档中;
首先,new directive
其实主要是对指令进行初始化配置,就不多谈;
主要说下其中this._bind
方法,它是指令初始化后绑定到对应元素的方法;
// remove attribute
if (
// 只要不是cloak指令那就从dom的attribute里移除
// 是cloak指令但是已经编译和link完成了的话,那也还是可以移除的
// 如移出":class"、":style"等
(name !== 'cloak' || this.vm._isCompiled) &&
this.el && this.el.removeAttribute
) {
var attr = descriptor.attr || ('v-' + name)
this.el.removeAttribute(attr)
}
这儿主要移出元素上添加的自定义指令,如v-if
、v-show
等;所以当我们使用控制台去查看dom元素时,其实是看不到写在代码中的自定义指令属性;但是不包括v-cloak
,因为这个在css中需要使用;
// html
<div v-cloak>
{{ message }}
</div>
// css
[v-cloak] {
display: none;
}
// copy def properties
// 不采用原型链继承,而是直接extend定义对象到this上,来扩展Directive实例
// 将不同指令一些特殊的函数或熟悉合并到实例化的directive里
var def = descriptor.def
if (typeof def === 'function') {
this.update = def
} else {
extend(this, def)
}
这儿主要说下extend(this, def)
,descriptor主要是指令的一些描述信息:
指令描述对象,以v-bind:href.literal="mylink"为例:
{
arg:"href",
attr:"v-bind:href.literal",
def:Object,// v-bind指令的定义
expression:"mylink", // 表达式,如果是插值的话,那主要用到的是下面的interp字段
filters:undefined
hasOneTime:undefined
interp:undefined,// 存放插值token
modifiers:Object, // literal修饰符的定义
name:"bind" //指令类型
raw:"mylink" //未处理前的原始属性值
}
而,def其实就是指令对应的配置信息;也就是我们在写指令时配置的数据,如下指令:
<template>
<div class="hello">
<h1 v-demo="demo">hello {{ msg }} welcome here</h1>
<!-- <h3 v-if="show" >this is v-if</h3> -->
</div>
</template>
<script>
export default {
created() {
setInterval(()=> {
this.demo += 1;
}, 1000)
},
data () {
return {
msg: 'Hello World!',
show: false,
demo: 1
}
},
directives: {
demo: {
bind: function() {
this.el.setAttribute('style', 'color: green');
},
update: function(value) {
if(value % 2) {
this.el.setAttribute('style', 'color: green');
} else {
this.el.setAttribute('style', 'color: red');
}
}
}
}
}
</script>
它对应的descriptor
就是:
descriptor = {
arg: undefined,
attr: "v-demo",
def: {
bind: function() {}, // 上面定义的bind
update: function() {} // 上面定义的update
},
expression:"demo",
filters: undefined,
modifiers: {},
name: 'demo'
}
接着上面的,使用extend(this, def)
就将def中定义的方法或属性就复制到实例化指令对象上面;好供后面使用;
// initial bind
if (this.bind) {
this.bind()
}
这就是执行上面刚刚保存的bind
方法;当执行此方法时,上面就会执行
this.el.setAttribute('style', 'color: green');
将字体颜色改为绿色;
// 下面这些判断是因为许多指令比如slot component之类的并不是响应式的,
// 他们只需要在bind里处理好dom的分发和编译/link即可然后他们的使命就结束了,生成watcher和收集依赖等步骤根本没有
// 所以根本不用执行下面的处理
if (this.literal) {
} else if (
(this.expression || this.modifiers) &&
(this.update || this.twoWay) &&
!this._checkStatement()
) {
var watcher = this._watcher = new Watcher(
this.vm,
this.expression,
this._update, // callback
{
filters: this.filters,
twoWay: this.twoWay,
deep: this.deep,
preProcess: preProcess,
postProcess: postProcess,
scope: this._scope
}
)
}
而这儿就是对需要添加双向绑定的指令添加watcher
;对应watcher后面再进行详细说明; 可以从上看出,传入了this._update
方法,其实也就是当数据变化时,就会执行this._update
方法,而:
var dir = this
if (this.update) {
// 处理一下原本的update函数,加入lock判断
this._update = function (val, oldVal) {
if (!dir._locked) {
dir.update(val, oldVal)
}
}
} else {
this._update = function() {}
}
其实也就是执行上面的descriptor.def.update
方法,所以当值变化时,会触发我们自定义指令时定义的update方法,而发生颜色变化;
这是指令最主要的代码部分;其他的如下:
// 获取指令的参数, 对于一些指令, 指令的元素上可能存在其他的attr来作为指令运行的参数
// 比如v-for指令,那么元素上的attr: track-by="..." 就是参数
// 比如组件指令,那么元素上可能写了transition-mode="out-in", 诸如此类
this._setupParams();
// 当一个指令需要销毁时,对其进行销毁处理;此时,如果定义了unbind方法,也会在此刻调用
this._teardown();
而对于每个指令的处理原理,可以看其对应源码;如v-show
源码:
// src/directives/public/show.js
import { getAttr, inDoc } from '../../util/index'
import { applyTransition } from '../../transition/index'
export default {
bind () {
// check else block
var next = this.el.nextElementSibling
if (next && getAttr(next, 'v-else') !== null) {
this.elseEl = next
}
},
update (value) {
this.apply(this.el, value)
if (this.elseEl) {
this.apply(this.elseEl, !value)
}
},
apply (el, value) {
if (inDoc(el)) {
applyTransition(el, value ? 1 : -1, toggle, this.vm)
} else {
toggle()
}
function toggle () {
el.style.display = value ? '' : 'none'
}
}
}
可以从上面看出在初始化页面绑定时,主要获取后面兄弟元素是否使用v-else
; 如果使用,将元素保存到this.elseEl
中,而当值变化执行update
时,主要执行了this.apply
;而最终只是执行了下面代码:
el.style.display = value ? '' : 'none'
从而达到隐藏或者展示元素的效果;