1. Vue怎么获取到设置的指令钩子
以下面一段Vue模板为例
<div v-test v-test2></div>
以上的模板会被编译成渲染函数
with(this) {
return _c('div', {
directives: [{
name: 'test',
rawName: 'v-test'
}, {
name: 'test2',
rawName: 'v-test2'
}]
})
}
如何获取到我们设置的指令的钩子,入口函数为updateDirectives
, 用来获取所有的新指令和旧指令集合。
function updateDirectives(oldVnode, vnode) {
// 获取旧节点的指令
var oldDirs = normalizeDirectives$1(
oldVnode.data.directives,
oldVnode.context);
// 获取新节点的指令
var newDirs = normalizeDirectives$1(
vnode.data.directives,
vnode.context);
}
可以看到以上主要使用到了normalizeDirectives$1
函数,这个函数就是获取指令的核心方法
function normalizeDirectives$1(dirs, name) {
var res = {};
var i, dir;
// 遍历本节点的所有的指令,逐个从组建中获取
for (i = 0; i < dirs.length; i++) {
dir = dirs[i];
res[dir.name] = dir;
// 将获取到的钩子添加到当前指令上
dir.ref = vm.$options['directives'][dir.name];
}
return res;
}
经过以上这一步的处理,我们的directives
会变成一下这个样子
directives: [{
name: 'test',
rawName: 'v-test',
ref: {
bind() {},
unbind() {}
}
}]
2. 内部如何调用钩子函数
上面一篇文章已经说过,bind, update, unbind都是直接触发的。
- bind 当指令在新节点中存在,但是旧节点中不存在时,就会调用bind
- update 当指令在新旧节点中都存在时,会调用update
- unbind 当指令在新节点中不存在,旧节点中存在时,会调用unbind
下面主要来看inserted
和componentUpdated
inserted是在DOM插入父节点之后才触发,而处理inserted是在DOM插入之前,所以这里不可能直接触发,inserted分为保存和执行两个步骤
function updateDirectives(oldVnode, vnode) {
// 如果旧节点为空,表示这是新创建的
var isCreate = oldVnode === emptyNode;
var dirsWithInsert = [];
var key, oldDir, dir;
for (key in newDirs) {
oldDir = oldDirs[key];
dir = newDirs[key];
if (!oldDir) {
if (dir.def && dir.def.inserted) {
// 将所有的inserted钩子保存到dirsWithInsert数组
dirsWithInsert.push(dir);
}
}
}
if (dirsWithInsert.length) {
// 用所有的inserted钩子组装成函数 callInserted
var callInsert = function() {
for (var i = 0; i < dirsWithInsert.length; i++) {
callHook$1(dirsWithInsert[i], 'inserted', vnode, oldVnode);
}
};
// 合并到hook中的insert
if (isCreate) {
// 把callInsert 和本节点的 insert 合并起来
vnode.data.hook['insert'] = callInsert
} else {
callInsert();
}
}
}
当页面初始化时,调用patch处理根节点,开始插入页面的步骤,其中会不断遍历子节点
function patch(oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
var insertedVnodeQueue=[]
if(需要更新){...省略...}
// 不是更新,而是页面初始化
else{
// 其中会不断地遍历子节点,递归等....
createElm(vnode,insertedVnodeQueue,...);
invokeInsertHook(vnode, insertedVnodeQueue);
}
return vnode.elm
}
上面的createElement会创建本节点以及其后代节点,然后插入到父节点中,等到createElm执行完,所有的节点都已经插入完毕了
function createElm(
vnode,insertedVnodeQueue,
parentElm,refElm
){
vnode.elm = document.createElement(vnode.tag);
// 不断遍历子节点,递归调用 createElm
if (Array.isArray(children)) {
for (var i = 0; i < children.length; ++i) {
createElm(children[i], insertedVnodeQueue,
vnode.elm, null, true, children, i);
}
}
// 处理本节点的事件,属性等,其中包含对指令的处理
invokeCreateHooks(vnode, insertedVnodeQueue);
// 插入 本DOM 到父节点中
insert(parentElm, vnode.elm, refElm);
}
整个createElm之后,会调用invokeInsertHook,这就是调用insert的时机,我们可以看看invokeInsertHook做了什么
// 将之前存储的insert钩子一次性全部触发
function invokeInsertHook(vnode, insertedVnodeQueue) {
for (var i = 0; i < insertedVnodeQueue.length; ++i) {
insertedVnodeQueue[i].data.hook.insert(queue[i]);
}
}
componentUpdated
也是分为保存和执行两段源码
// 把指令componentUpdated的函数 和本节点的 postpatch 合并起来
if (dirsWithPostpatch.length) {
vnode.data.hook['postpatch'] = function() {
for (var i = 0; i < dirsWithPostpatch.length; i++) {
callHook$1(dirsWithPostpatch[i],
'componentUpdated', vnode, oldVnode);
}
});
}
componentUpdated钩子是更新一个节点就马上执行,更新一个节点的意思是包括其内部子节点的,更新一个节点也会调用patch
function patch(oldVnode, vnode) {
if(需要更新){
patchVnode(oldVnode, vnode)
}
return vnode.elm
}
function patchVnode(oldVnode, vnode){
// 递归调用 patchVnode 更新子节点
updateChildren(oldVnode, vnode,.....);
// 执行本节点的 postpatch
if (isDef(i = data.hook) && isDef(i = i.postpatch)) {
i(oldVnode, vnode);
}
}