写文章不容易,点个赞呗兄弟
专注 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);
}
}
举个栗子走下流程
需要更新的时候,调用顺序