【Vue原理】Directives - 源碼版

寫文章不容易,點個讚唄兄弟 專一 Vue 源碼分享,文章分爲白話版和 源碼版,白話版助於理解工做原理,源碼版助於瞭解內部詳情,讓咱們一塊兒學習吧 研究基於 Vue版本 【2.5.17】node

若是你以爲排版難看,請點擊 下面連接 或者 拉到 下面關注公衆號也能夠吧數組

【Vue原理】Directives - 源碼版 bash

咦,上一篇咱們已經講過白話版啦,主要的邏輯你們應該也清楚了的,今天咱們就直接開幹源碼。有興趣讀源碼的同窗,但願對大家有幫助哦~函數

沒看過白話版的,仍是先別看源碼版了,那麼多代碼看了估計會懵逼...post

首先,上一篇說過,Vue 會在DOM 建立以後,插入父節點以前。對DOM綁定的事件和屬性等進行處理,其中包含指令。學習

Vue 有專門的方法來處理指令,這個方法是 updateDirectives,其做用,獲取指令鉤子,和對不一樣鉤子進行不一樣處理。測試

updateDirectives 的源碼不是很短,其中還涉及其餘方法,不打算一次性放出來,打算一塊一塊分解地講,因此 源碼會被我分紅不少塊ui

今天咱們以兩個問題開始this

一、怎麼獲取到設置的指令鉤子spa

二、內部怎麼調用鉤子函數

還有,模板上指令會被解析成數組,好比下面這個模板

image

會被解析成下面的渲染函數,看下其中的 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

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

大概的函數調用邏輯以下


複製代碼

image

二、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);        
   }
}
複製代碼

舉個栗子走下流程

image

須要更新的時候,調用順序

image

公衆號
相關文章
相關標籤/搜索