【Vue原理】Directives - 源码版

栏目: 编程语言 · 发布时间: 4年前

内容简介:写文章不容易,点个赞呗兄弟 专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧 研究基于 Vue版本如果你觉得排版难看,请点击

写文章不容易,点个赞呗兄弟 专注 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 - 源码版

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Practical Vim, Second Edition

Practical Vim, Second Edition

Drew Neil / The Pragmatic Bookshelf / 2015-10-31 / USD 29.00

Vim is a fast and efficient text editor that will make you a faster and more efficient developer. It’s available on almost every OS, and if you master the techniques in this book, you’ll never need an......一起来看看 《Practical Vim, Second Edition》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具