共计 5381 个字符,预计需要花费 14 分钟才能阅读完成。
写文章不容易,点个赞呗兄弟
专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧
研究基于 Vue 版本 【2.5.17】
如果你觉得排版难看,请点击 下面链接 或者 拉到 下面 关注公众号 也可以吧
【Vue 原理】Directives – 源码版
咦,上一篇我们已经讲过白话版啦,主要的逻辑大家应该也清楚了的,今天我们就直接开干源码。有兴趣读源码的同学,希望对你们有帮助哦~
没看过白话版的,还是先别看源码版了,那么多代码看了估计会懵逼 …
首先,上一篇说过,Vue 会在 DOM 创建之后,插入父节点之前。对 DOM 绑定的事件和属性等进行处理,其中包含指令。
Vue 有专门的方法来处理指令,这个方法是 updateDirectives,其作用,获取指令钩子,和对不同钩子进行不同处理。
updateDirectives 的源码不是很短,其中还涉及其他方法,不打算一次性放出来,打算一块一块分解地讲,所以 源码会被我分成很多块
今天我们以两个问题开始
1、怎么获取到设置的指令钩子
2、内部怎么调用钩子函数
还有,模板上指令会被解析成数组,比如下面这个模板
会被解析成下面的渲染函数,看下其中的 directives,这就是指令被解析成的终极形态了。下面 updateDirectives 方法处理指令,处理的就是这个数组
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,他就是获取钩子的幕后黑手。
先看作用,再看源码
1、遍历本节点所有的指令,逐个从组件中获取
2、把获取的钩子添加到 遍历到的当前指令上
function normalizeDirectives$1(dirs, vm) {var res = {};
var i, dir;
for (i = 0; i < dirs.length; i++) {dir = dirs[i];
res[dir.name] = dir;
dir.def = vm.$options['directives'][dir.name];
}
return res
}
最后返回的是什么呢,举个例子看下
比如开始处理的指令数组是下面
directives: [{
name: "test",
rawName: "v-test"
}]
v-test 的钩子函数是
new Vue({
directives:{
test:{bind(){...},
inserted(){...},
.... 等其他钩子
}
}
})
经过 normalizeDirectives$1,就会返回下面这个
directives: [{
name: "test",
rawName: "v-test",
def:{bind(){...},
.... 等其他钩子
}
}]
好的,拿到了钩子,那我们下一步就是要处理钩子了!
怎么调用钩子
哈哈,看过白话版的,就知道这里不同的钩子的处理流程大概是什么样子,今天,这里是不会重复去描述啦,大概放些源码,供大家去学习。
bind、update、unbind 都是直接触发的,没有什么好讲的,触发的代码我已经标蓝了
function updateDirectives(oldVnode, vnode) {
// 如果旧节点为空,表示这是新创建的
var isCreate = oldVnode === emptyNode;
// 如果新节点为空,表示要销毁
var isDestroy = vnode === emptyNode;
var key, oldDir, dir;
for (key in newDirs) {oldDir = oldDirs[key];
dir = newDirs[key];
if (!oldDir) {dir.def.bind(vnode.elm, dir, vnode, oldVnode, isDestroy)
...inserted 处理
} else {dir.def.update(vnode.elm, dir, vnode, oldVnode, isDestroy)
...componentUpdated 处理
}
}
...
...inserted 和 componentUpdated 处理
...
if (!isCreate) {for (key in oldDirs) {if (!newDirs[key]) {oldDirs[key].def.unbind(vnode.elm,
dir, vnode, oldVnode, isDestroy)
}
}
}
}
重点我们讲 inserted 和 componentUpdated 两个钩子就好了
1、inserted
inserted 是在 DOM 插入父节点之后才触发的,而 处理 inserted 是在 DOM 插入之前,所有这里不可能直接触发,只能是先保存起来,等到 节点被插入之后再触发
所以,inserted 分为 保存和 执行两个步骤,我们按两个步骤来看源码
保存钩子
下面保存 inserted 钩子的源码可以看成三步
1、保存进数组 dirsWithInsert
2、组装成函数 callInsert
3、合并到 insert 钩子
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) {dirsWithInsert.push(dir);
}
}
}
if (dirsWithInsert.length) {var callInsert = function() {for (var i = 0; i < dirsWithInsert.length; i++) {callHook$1(dirsWithInsert[i], 'inserted', vnode, oldVnode);
}
};
if (isCreate) {
// 把 callInsert 和本节点的 insert 合并起来
vnode.data.hook['insert'] = callInsert
} else {callInsert();
}
}
}
执行钩子
通过白话版的测试我们已经知道,inserted 钩子是所有节点都插入完毕之后才触发的,而不是插入一个节点就触发一次
现在我们从头探索这个执行的流程
页面初始化,调用 patch 处理根节点,开始插入页面的步骤,其中会不断遍历子节点
function patch(oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {var insertedVnodeQueue=[]
if(需要更新){... 省略...}
// 不是更新,而是页面初始化
else{
// 其中会不断地遍历子节点,递归秭归等....
createElm(vnode,insertedVnodeQueue,...);
invokeInsertHook(vnode, insertedVnodeQueue);
}
return vnode.elm
}
上面的 createElm 会创建本节点以及其后代节点,然后插入到父节点中
等到 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);
}
此时,invokeInsertHook 开始执行,invokeInsertHook 是统一调用 inserted 钩子的地方。
function invokeInsertHook(vnode, insertedVnodeQueue) {for (var i = 0; i < insertedVnodeQueue.length; ++i) {insertedVnodeQueue[i].data.hook.insert(queue[i]);
}
}
因为 patch 只会在 根节点调用一次,invokeInsertHook 只在 patch 中调用
所以 inserted 才会在所有节点都插入父节点完毕之后,统一触发,而不是一个个来。
收集节点
invokeCreateHooks 用于调用各种函数处理事件、属性、指令等
也是在这里添加节点到 insertedVnodeQueue
function invokeCreateHooks(vnode, insertedVnodeQueue) {
// 其中会执行 updateDirectives...
for (var i$1 = 0; i$1 < cbs.create.length; ++i$1) {cbs.create[i$1](emptyNode, vnode);
}
i = vnode.data.hook;
// 保存含有 insert 函数的节点
if (isDef(i) && isDef(i.insert)) {insertedVnodeQueue.push(vnode);
}
}
然后,执行 inserted 的源码可以看成 两步
1、把所有含有 insert 函数的节点,保存到 insertedVnodeQueue
2、所有节点插入完毕,遍历 insertedVnodeQueue,执行其中节点的 insert 函数
注意,insert 不是 inserted 哦,只是逻辑上 insert 包含 inserted
大概的函数调用逻辑如下
2、componentUpdated
这个钩子和 inserted 差不多,只是执行的流程不一样
同样分为保存和执行两段源码
保存钩子
function updateDirectives(oldVnode, vnode) {
// 如果旧节点为空,表示这是新创建的
var isCreate = oldVnode === emptyNode;
var dirsWithPostpatch = [];
var key, oldDir, dir;
for (key in newDirs) {oldDir = oldDirs[key];
dir = newDirs[key];
if (!oldDir) {....}
else {if (dir.def && dir.def.componentUpdated) {dirsWithPostpatch.push(dir);
}
}
}
// 把指令 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);
}
}
举个栗子走下流程
需要更新的时候,调用顺序