写文章不容易,点个赞呗兄弟 专一 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工做原理,源码版助于了解内部详情,让咱们一块儿学习吧 研究基于 Vue版本 【2.5.17】node
若是你以为排版难看,请点击 下面连接 或者 拉到 下面关注公众号也能够吧数组
咦,上一篇咱们已经讲过白话版啦,主要的逻辑你们应该也清楚了的,今天咱们就直接开干源码。有兴趣读源码的同窗,但愿对大家有帮助哦~post
没看过白话版的,仍是先别看源码版了,那么多代码看了估计会懵逼...学习
首先,上一篇说过,Vue 会在DOM 建立以后,插入父节点以前。对DOM绑定的事件和属性等进行处理,其中包含指令。测试
Vue 有专门的方法来处理指令,这个方法是 updateDirectives,其做用,获取指令钩子,和对不一样钩子进行不一样处理。this
updateDirectives 的源码不是很短,其中还涉及其余方法,不打算一次性放出来,打算一块一块分解地讲,因此 源码会被我分红不少块code
今天咱们以两个问题开始component
一、怎么获取到设置的指令钩子orm
二、内部怎么调用钩子函数
还有,模板上指令会被解析成数组,好比下面这个模板
会被解析成下面的渲染函数,看下其中的 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 是在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 大概的函数调用逻辑以下
这个钩子和 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); } }
举个栗子走下流程
须要更新的时候,调用顺序