天天看點

JS源碼分析│簡易mvvm庫的設計實作

作者:刀哥(朱建)

前言:mvvm模式即model-view-viewmodel模式簡稱,單項/雙向資料綁定的實作,讓前端開發者們從繁雜的dom事件中解脫出來,很友善的處理資料和ui之間的關聯。本文将從vue的雙向資料綁定入手,剖析mvvm庫設計的核心代碼與思路。

1、需求整理與分析

需求:

  • 資料一旦改變則更新資料對應的ui
  • ui改變則觸發事件改變ui對應的資料

分析:

  • 通過dom節點的指令擷取重新整理函數,用來重新整理指定的ui。
  • 實作一個橋接的方法,讓重新整理函數和需要的資料關聯起來。
  • 監聽資料變化,資料改變後通過橋接方法調用重新整理函數。
  • ui改變觸發對應的dom事件在改變特定的資料。

2、實作思路

  • 實作observer,重新定義data,為data上每個屬性增加setter,getter以監聽資料的變化。
  • 實作compile,掃描模版template,提取每個dom節點中的指令資訊。
  • 實作directive,通過指令資訊是執行個體化對應的directive執行個體,不同類型的directive擁有不同的重新整理函數update。
  • 實作watcher,讓observer的屬性監聽函數與directive的update函數做一一對應,以實作資料變化後更新視圖。

3、子產品劃分

MVVM目前劃分為observer,compile,directive,watcher四個子產品。

4、資料監聽子產品observer

通過es5規範中的object.defineProperty方式實作對資料的監聽。

5、實作思路

遞歸周遊data,将data下面所有屬性都加上set,get方法,以實作對所有屬性的攔截.

注意:對象可能含有數組屬性,數組的内置有push,pop,splice等方法改變内部資料.

此時做法是改變數組的原型鍊,在原型鍊中增加一層自定義的push,pop,splice方法做攔截,這些方法裡面加上我們自己的回調函數,然後在調用原生的push,pop,splice等方法。

export function defineProperty(obj, prop, val) {
if (prop == '__observe__') {

    return;

}

val = val || obj[prop];

var dep = new Dep();

obj.__observe__ = dep;

var childDep = addObserve(val);

Object.defineProperty(obj, prop, {

    get: function() {

        var target = Dep.target;

        if (target) {

            dep.addSub(target);

            if (childDep) {

                childDep.addSub(target);

            }

        }

        return val;

    },

    set: function(newVal) {

        if(newVal!=val){

            val = newVal;

            dep.notify();

        }

    }

});
}           

6、編譯子產品compiler

實作思路:

  • 将模版template上的dom周遊一遍,将其存入文檔碎片frag
  • 周遊frag,通過attributes擷取節點的屬性資訊,在通過正規表達式過濾屬性資訊,進而拿到元素節點和文檔節點的指令資訊
var complieTemplate = function (nodes, model) {

if ((nodes.nodeType == 1 || nodes.nodeType == 11) && !isScript(nodes)) {
paserNode(model, nodes);

if (nodes.hasChildNodes()) {

  nodes.childNodes.forEach(node=> {

    complieTemplate(node, model);

  })

}
}

};           

7、指令子產品directive

指令資訊如:v-text,v-for,v-model等。

每種指令資訊需要的初始化動作以及指令的重新整理函數update都可能不一樣,是以我們把它抽象出來單獨做一個子產品。當然也有公用的如公共屬性,統一的watcher執行個體化,unbind.

update函數則具體定義所屬指令如何渲染ui,如簡單的vtext指令的update函數如下:

vt.update = function (textContent) {
this.el.textContent = textContent;
};           

9、結構圖

JS源碼分析│簡易mvvm庫的設計實作

9、資料訂閱子產品watcher

watcher的功能是讓directive和observer子產品關聯起來。初始化的時候做兩件事:

  • 将directive子產品的update函數當參數傳入,并将其存入自身update屬性中。
  • 調用getValue,進而擷取對象data的特定屬性值,進而觸發一次之前在observer定義的屬性函數的getter方法。

由于在defineProperty函數中定義的dep變量在setter和getter函數裡有引用,使dep變量處于閉包狀态沒有釋放,此時在getter方法中通過判斷Depend.target的存在,來擷取訂閱者watcher,通過釋出者dep儲存起來。資料的每個屬性都有一個唯一的的dep變量,記錄着所有訂閱者watcher的資訊,一旦屬性有變化,調用setter函數的時候觸發dep.notify(),通知所有已訂閱的watcher,進而執行所有與該屬性關聯的重新整理函數,最後更新指定的ui。

watcher 初始化部分代碼:

Depend.target = this;

this.value = this.getValue();

Depend.target = null;           

observer.js 屬性定義代碼:

export function defineProperty(obj, prop, val) {
if (prop == '__observe__') {

    return;

}

val = val || obj[prop];

var dep = new Dep();

obj.__observe__ = dep;

var childDep = addObserve(val);

Object.defineProperty(obj, prop, {

    get: function() {

        var target = Dep.target;

        if (target) {

            dep.addSub(target);

            if (childDep) {

                childDep.addSub(target);

            }

        }

        return val;

    },

    set: function(newVal) {

        if(newVal!=val){

            val = newVal;

            dep.notify();

        }

    }

});
}           

10、流程圖

JS源碼分析│簡易mvvm庫的設計實作

11、總結

文基本對mvvm庫的需求整理,拆分,以及對拆分子產品的逐一實作來達到整體雙向綁定功能的實作,當然目前市場上的mvvm庫功能絕不止于此,本文隻是略舉個人認為的核心代碼。如果思路和實作上的問題,也請各位斧正,謝謝閱讀!

原代碼:

https://github.com/laughing-pic-zhu/mvvm

想要深入了解的同學可以通路

數瀾社群

,和大家一起讨論學習~

繼續閱讀