【Vue原理】Directives - 源码版

写文章不容易,点个赞呗兄弟
专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧
研究基于 Vue版本 【2.5.17】

如果你觉得排版难看,请点击 下面链接 或者 拉到 下面关注公众号也可以吧

【Vue原理】Directives - 源码版

咦,上一篇我们已经讲过白话版啦,主要的逻辑大家应该也清楚了的,今天我们就直接开干源码。有兴趣读源码的同学,希望对你们有帮助哦~

没看过白话版的,还是先别看源码版了,那么多代码看了估计会懵逼...

首先,上一篇说过,Vue 会在DOM 创建之后,插入父节点之前。对DOM绑定的事件和属性等进行处理,其中包含指令。

Vue 有专门的方法来处理指令,这个方法是 updateDirectives,其作用,获取指令钩子,和对不同钩子进行不同处理。

updateDirectives 的源码不是很短,其中还涉及其他方法,不打算一次性放出来,打算一块一块分解地讲,所以 源码会被我分成很多块

今天我们以两个问题开始

1、怎么获取到设置的指令钩子

2、内部怎么调用钩子函数

还有,模板上指令会被解析成数组,比如下面这个模板

【Vue原理】Directives - 源码版

会被解析成下面的渲染函数,看下其中的 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

大概的函数调用逻辑如下

【Vue原理】Directives - 源码版

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);        
   }
}

举个栗子走下流程

【Vue原理】Directives - 源码版

需要更新的时候,调用顺序

【Vue原理】Directives - 源码版

【Vue原理】Directives - 源码版

相关推荐