【Vue原理】Directives - 源碼版

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

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

【Vue原理】Directives - 源碼版 函數

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

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

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

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

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

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

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

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

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

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

公衆號

相關文章
相關標籤/搜索