指令是vue提供的带有 v-前缀的特殊特性。
指令的职责是,当表达式的值变化时,将其产生的连带影响响应式的作用于DOM。
指令原理
在模板解析阶段,我们将指令解析到AST,然后使用AST生成代码字符串的过程中 实现 某些内置指令的功能, 最后在虚拟DOM渲染的过程中触发自定义指令的钩子函数使指令生效。
实现原理:
在模板解析阶段,将节点上的指令解析出来并添加到AST 的 directives属性中。
随后directives数据会传递到Vnode中,接着可以通过 vnode.data.directives获取一个阶段说绑定的指令。
最后当虚拟DOM进行修补时,会根据节点对比结果触发一些钩子函数。 更新指令的程序会监听 create、update、destroy钩子函数,
并在这三个钩子函数触发时 对 Vnode和oldVnode进行对比, 最终对比结果触发指令的钩子函数。
v-if指令
v-if是在模板编译阶段实现的。
if
else
模板编译后生成的代码字符串
(has)? _c('li', [_v("if")]) : _c('li', [_v("else")])
这段代码最终执行时,根据has变量的值来选择创建哪个节点
v-for指令
v-for指令也是模板编译的代码生成阶段实现的
v-for {{index}}
生成的代码字符串
_l((list), function(item, index){
return _c('li', [
_v("v-for " + _s(index))
])
})
_l是函数renderList的别名。当执行这段代码时, _l函数会循环变量 list并依次调用第二个参数 所传递 的函数。
v-on指令
v-on指令的作用是绑定事件监听器,事件类型有参数指定。 它用在普通元素上时,监听原生DOM事件;用在自定义元素组件上时,监听子组件触发的自定义事件。
从模板解析到生成Vnode,最终事件都会被保存到Vnode中。通过vnode.data.on得到一个节点注册的所有事件。
虚拟DOM在修补(patch)的过程中会根据不同的时机触发不同的钩子函数。
事件绑定相关的处理逻辑分别设置了create和update钩子函数,也就是说patch的过程中,当DOM创建或更新时,会触发事件绑定相关的处理逻辑。
create和update钩子函数都会执行updateDOMListener函数
相关代码如下
// 通过对比两个VNode的事件对象,来决定绑定原生DOM事件还是解绑原生DOM事件。
function updateDOMListeners (oldVnode: VNodeWithData, vnode: VNodeWithData) {
// 如果两个Vnode中事件对象都不存在,
// 说明上一次没有绑定任何事件, 这一次元素更新也没有新增事件绑定
if (!oldVnode.data.on && !vnode.data.on) {
return
}
// 获取新老Vnode上的事件
const on = vnode.data.on || {}
const oldOn = oldVnode.data.on || {}
// vnode.elm上保存vnode所对应的DOM元素
target = vnode.elm
// 更新事件监听器,通过对比on 与 oldOn. 判断调用add方法还是 remove方法执行绑定事件还是解绑事件,
updateListeners(on, oldOn, add, remove, createOnceHandler, vnode.context)
target = undefined
}
function add (
event: string,
handler: Function,
capture: boolean,
passive?: boolean,
params?: Array
) {
if (capture) {
console.log('Weex do not support event in bubble phase.')
return
}
target.addEvent(event, handler, params)
}
function remove (
event: string,
handler: any,
capture: any,
_target?: any
) {
(_target || target).removeEvent(event)
}
自定义指令的内部原理
虚拟DOM通过diff算法对比 两个 VNode之间的差异并更新真实的DOM节点。
在更新的过程中,可能是创建新的节点,可能是更新节点,也可能是删除节点。
虚拟DOM在渲染时,处理更新DOM内容外,还会触发钩子函数。
指令的处理逻辑封闭监听了create、update、destroy.
虚拟DOM在触发钩子函数时,下面代码对应的函数都会被执行。
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)
}
}
function _update (oldVnode, vnode) {
const isCreate = oldVnode === emptyNode // 是否是新创建的节点
const isDestroy = vnode === emptyNode // 是否新虚拟节点不存在
// normalizeDirectives 将模板中 使用的 指令 从用户注册的 自定义指令集合中 取出来
const oldDirs = normalizeDirectives(oldVnode.data.directives, oldVnode.context) // 旧的指令集合
const newDirs = normalizeDirectives(vnode.data.directives, vnode.context) // 新的指令集合
const dirsWithInsert = [] // 保存需要触发inserted 指令钩子的指令列表
const dirsWithPostpatch = [] // 保存需要触发 componentUpdated 钩子函数的指令列表
let key, oldDir, dir
// 循环新的指令集合
for (key in newDirs) {
oldDir = oldDirs[key]
dir = newDirs[key]
// 如果旧指令不存在,说明改指令首次绑定到元素
if (!oldDir) {
// 新指令 触发 bind 函数
callHook(dir, 'bind', vnode, oldVnode)
// 如果有inserted 方法 添加到dirsWithInsert。等待执行
if (dir.def && dir.def.inserted) {
dirsWithInsert.push(dir)
}
} else {
// 已经存在的指令 更新即可
dir.oldValue = oldDir.value
dir.oldArg = oldDir.arg
// 触发update钩子函数
callHook(dir, 'update', vnode, oldVnode)
if (dir.def && dir.def.componentUpdated) {
// 如果有componentUpdated方法 保存到 dirsWithPostpatch 等待执行
dirsWithPostpatch.push(dir)
}
}
}
// 执行 dirsWithInsert
if (dirsWithInsert.length) {
// callInsert 函数 会等到执行时, 才会依次调用每个指令的inserted方法
const callInsert = () => {
for (let i = 0; i < dirsWithInsert.length; i++) {
callHook(dirsWithInsert[i], 'inserted', vnode, oldVnode)
}
}
// 如果新 创建的 元素
if (isCreate) {
// mergeVNodeHook 可以将 一个钩子函数 与 虚拟节点 现有的 钩子函数合并。
// 这样 钩子函数的执行 推迟到被绑定的 元素插入 到父节点之后进行
mergeVNodeHook(vnode, 'insert', callInsert)
} else {
// 如果不是新的元素, 直接执行 即可
callInsert()
}
}
// componentUpdated 也需要将指令推迟到 指令所在组件的Vnode及其子Vnode全部更新之后调用
if (dirsWithPostpatch.length) {
// 虚拟DOM更新前 会触发 prepatch钩子函数
// 虚拟DOM更新中 会触发 update钩子函数
// 虚拟DOM更新后 会触发 postpatch钩子函数
mergeVNodeHook(vnode, 'postpatch', () => {
for (let i = 0; i < dirsWithPostpatch.length; i++) {
callHook(dirsWithPostpatch[i], 'componentUpdated', vnode, oldVnode)
}
})
}
// 如果是新创建,是不需要解绑的。
if (!isCreate) {
// 旧的存在,新的不存在。那么调用callHook 执行upbind方法即可
for (key in oldDirs) {
if (!newDirs[key]) {
// no longer present, unbind
callHook(oldDirs[key], 'unbind', oldVnode, oldVnode, isDestroy)
}
}
}
}