寫文章不容易,點個讚唄兄弟 專一 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); } }
舉個栗子走下流程
須要更新的時候,調用順序