簡易mvvm庫的設計實現

前言

mvvm模式即model-view-viewmodel模式簡稱,單項/雙向數據綁定的實現,讓前端開發者們從繁雜的dom事件中解脫出來,很方便的處理數據和ui之間的聯動。
本文將從vue的雙向數據綁定入手,剖析mvvm庫設計的核心代碼與思路。javascript

需求整理與分析

整理

  • 數據一旦改變則更新數據對應的ui前端

  • ui改變則觸發事件改變ui對應的數據vue

分析

  • 經過dom節點的指令獲取刷新函數,用來刷新指定的ui。java

  • 實現一個橋接的方法,讓刷新函數和須要的數據關聯起來node

  • 監聽數據變化,數據改變後經過橋接方法調用刷新函數git

  • ui改變觸發對應的dom事件在改變特定的數據github

實現思路

  • 實現observer,從新定義data,爲data上每一個屬性增長setter,getter以監聽數據的變化正則表達式

  • 實現compile,掃描模版template,提取每一個dom節點中的指令信息segmentfault

  • 實現directive,經過指令信息是實例化對應的directive實例,不一樣類型的directive擁有不一樣的刷新函數update數組

  • 實現watcher,讓observer的屬性監聽函數與directive的update函數作一一對應,以實現數據變化後更新視圖

模塊劃分

MVVM目前劃分爲observer,compile,directive,watcher四個模塊

數據監聽模塊observer

經過es5規範中的object.defineProperty方式實現對數據的監聽
實現思路:
遞歸遍歷data,將data下面全部屬性都加上set,get方法,以實現對全部屬性的攔截.
注意:對象可能含有數組屬性,數組的內置有push,pop,splice等方法改變內部數據.
此時作法是改變數組的原型鏈,在原型鏈中增長一層自定義的push,pop,splice方法作攔截,這些方法裏面加上咱們本身的回調函數,而後在調用原生的push,pop,splice等方法.
具體能夠看我上一篇文章js對象監聽實現
observer.js代碼

export function Observer(obj) {
    this.$observe = function(_obj) {
        var type = Object.prototype.toString.call(_obj);
        if (type == '[object Object]') {
            this.$observeObj(_obj);
        } else if (type == '[object Array]') {
            this.$cloneArray(_obj);
        }
    };

    this.$observeObj = function(obj) {
        var t = this;
        Object.keys(obj).forEach(function(prop) {
            var val = obj[prop];
            defineProperty(obj, prop, val);
            if (prop != '__observe__') {
                t.$observe(val);
            }
        });
    };

    this.$cloneArray = function(a_array) {
        var ORP = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
        var arrayProto = Array.prototype;
        var newProto = Object.create(arrayProto);
        ORP.forEach(function(prop) {
            Object.defineProperty(newProto, prop, {
                value: function(newVal) {
                    var dep = a_array.__observe__;
                    var re=arrayProto[prop].apply(a_array, arguments);
                    dep.notify();
                    return re;
                },
                enumerable: false,
                configurable: true,
                writable: true
            });
        });
        a_array.__proto__ = newProto;
    };

    this.$observe(obj, []);
}

var addObserve = function(val) {
    if (!val || typeof val != 'object') {
        return;
    }
    var dep = new Dep();
    if (isArray(val)) {
        val.__observe__ = dep;
        return dep;
    }

}

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();
            }
        }
    });
}

編譯模塊compiler

實現思路:
1.將模版template上的dom遍歷一遍,將其存入文檔碎片frag
2.遍歷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);
          })
        }
      }
    };
    
    var paserNode = function (model, node) {
  var attributes = node.attributes || [];
  var direct_array = [];
  var scope = {
    parentNode: node.parentNode,
    nextNode: node.nextElementSibling,
    el: node,
    model: model,
    direct_array: direct_array
  };

  attributes = toArray(attributes);
  var textContent = node.textContent;
  var attrs = [];
  var vfor;

  attributes.forEach(attr => {
    var name = attr.name;
    if (isDirective(name)) {
      if (name == 'v-for') {
        vfor = attr;
      } else {
        attrs.push(attr);
      }
      removeAttribute(node, name);
    }
  });

  //bug  nodeType=3
  var textValue = stringParse(textContent);
  if (textValue) {
    attrs.push({
      name: 'v-text',
      value: textValue
    });
    node.textContent = '';
  }

  if (vfor) {
    scope.attrs = attrs;
    attrs = [vfor];
  }

  attrs.forEach(function (attr) {
    var name = attr.name;
    var val = attr.value;
    var directiveType = 'v' + /v-(\w+)/.exec(name)[1];
    var Directive = directives[directiveType];
    if (Directive) {
      direct_array.push(new Directive(val, scope));
    }
  });
};

var isDirective = function (attr) {
  return /v-(\w+)/.test(attr)
};

var isScript = function isScript(el) {
  return el.tagName === 'SCRIPT' && (
      !el.hasAttribute('type') ||
      el.getAttribute('type') === 'text/javascript'
    )
}

指令模塊directive

  • 指令信息如:v-text,v-for,v-model等。

  • 每種指令信息須要的初始化動做以及指令的刷新函數update均可能不同,因此咱們把它抽象出來單獨作一個模塊。固然也有公用的如公共屬性,統一的watcher實例化,unbind.

  • update函數則具體定義所屬指令如何渲染ui

如簡單的vtext指令的update函數以下:

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

結構圖

圖片描述

數據訂閱模塊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();
            }
        }
    });
}

流程圖

簡單的流程圖以下:
圖片描述

效果圖

圖片描述

代碼地址

總結

本文基本對mvvm庫的需求整理,拆分,以及對拆分模塊的逐一實現來達到總體雙向綁定功能的實現,固然目前市場上的mvvm庫功能毫不止於此,本文只是略舉我的認爲的核心代碼。若是思路和實現上的問題,也請各位斧正,謝謝閱讀!

相關文章
相關標籤/搜索