写文章不容易,点个赞呗兄弟
专注 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 函数的节点,保存到 insertedVnodeQueue2、所有节点插入完毕,遍历 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);           }}

举个栗子走下流程

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