资讯专栏INFORMATION COLUMN

【Vue原理】Directives - 源码版

draveness / 1592人阅读

摘要:对绑定的事件和属性等进行处理,其中包含指令。有专门的方法来处理指令,这个方法是,其作用,获取指令钩子,和对不同钩子进行不同处理。

写文章不容易,点个赞呗兄弟
专注 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);        
   }
}

举个栗子走下流程

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/105267.html

相关文章

  • Vue原理】Compile - 源码 之 generate 节点数据拼接

    摘要:写文章不容易,点个赞呗兄弟专注源码分享,文章分为白话版和源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究基于版本如果你觉得排版难看,请点击下面链接或者拉到下面关注公众号也可以吧原理源码版之节点数据拼接上一篇我们 写文章不容易,点个赞呗兄弟 专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究...

    fizz 评论0 收藏0
  • Vue原理】Mixins - 源码

    写文章不容易,点个赞呗兄弟专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究基于 Vue版本 【2.5.17】 如果你觉得排版难看,请点击 下面链接 或者 拉到 下面关注公众号也可以吧 【Vue原理】Mixins - 源码版 今天探索的是 mixins 的源码,mixins 根据不同的选项类型会做不同的处理 篇幅会有些长,...

    gotham 评论0 收藏0
  • Vue原理】VModel - 源码 之 表单元素绑定流程

    摘要:首先,兄弟,容我先说几句涉及源码很多,篇幅很长,我都已经分了上下三篇了,依然这么长,但是其实内容都差不多一样,但是我还是毫无保留地给你了。 写文章不容易,点个赞呗兄弟专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究基于 Vue版本 【2.5.17】 如果你觉得排版难看,请点击 下面链接 或者 拉到 下面关注公众号也...

    sarva 评论0 收藏0
  • Vue原理】Compile - 源码 之 generate 节点拼接

    摘要:还原的难度就在于变成模板了,因为其他的什么等是原封不动的哈哈,可是直接照抄最后鉴于本人能力有限,难免会有疏漏错误的地方,请大家多多包涵,如果有任何描述不当的地方,欢迎后台联系本人,有重谢 写文章不容易,点个赞呗兄弟 专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究基于 Vue版本 【2.5.17】 如果你觉得排版...

    macg0406 评论0 收藏0
  • Vue原理】Component - 源码 之 创建组件VNode

    摘要:写文章不容易,点个赞呗兄弟专注源码分享,文章分为白话版和源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究基于版本如果你觉得排版难看,请点击下面链接或者拉到下面关注公众号也可以吧原理源码版之创建组件今天就要开启我 写文章不容易,点个赞呗兄弟专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究基于...

    hover_lew 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<