【Vue原理】Directives - 源码版

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

若是你以为排版难看,请点击 下面连接 或者 拉到 下面关注公众号也能够吧数组

【Vue原理】Directives - 源码版 bash

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

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

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

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

updateDirectives 的源码不是很短,其中还涉及其余方法,不打算一次性放出来,打算一块一块分解地讲,因此 源码会被我分红不少块ui

今天咱们以两个问题开始this

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

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

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

image

会被解析成下面的渲染函数,看下其中的 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,他就是获取钩子的幕后黑手。

先看做用,再看源码

一、遍历本节点全部的指令,逐个从组件中获取

二、把获取的钩子添加到 遍历到的当前指令上

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 两个钩子就行了

一、inserted

inserted 是在DOM 插入父节点以后才触发的,而 处理 inserted 是在 DOM 插入以前,全部这里不可能直接触发,只能是先保存起来,等到 节点被插入以后再触发

因此,inserted 分为 保存和 执行两个步骤,咱们按两个步骤来看源码

保存钩子

下面保存 inserted 钩子的源码能够当作三步

一、保存进数组 dirsWithInsert

二、组装成函数 callInsert

三、合并到 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 的源码能够当作 两步

一、把全部含有 insert 函数的节点,保存到 insertedVnodeQueue

二、全部节点插入完毕,遍历 insertedVnodeQueue ,执行其中节点的 insert 函数

注意,insert 不是 inserted 哦,只是逻辑上 insert 包含 inserted

大概的函数调用逻辑以下


复制代码

image

二、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);        
   }
}
复制代码

举个栗子走下流程

image

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

image

公众号