【Vue原理】Directives - 源码版
写文章不容易,点个赞呗兄弟
专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧
研究基于 Vue版本 【2.5.17】
如果你觉得排版难看,请点击 下面链接 或者 拉到 下面关注公众号也可以吧
咦,上一篇我们已经讲过白话版啦,主要的逻辑大家应该也清楚了的,今天我们就直接开干源码。有兴趣读源码的同学,希望对你们有帮助哦~
没看过白话版的,还是先别看源码版了,那么多代码看了估计会懵逼...
首先,上一篇说过,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); } }
举个栗子走下流程
需要更新的时候,调用顺序