一起學(xué)習(xí)、手寫MVVM框架

vue中的數(shù)據(jù)雙向綁定,其實(shí)一句話就可以說(shuō)清楚了:利用 Object.defineProperty(),來(lái)劫持各個(gè)屬性的setter/getter,并且把內(nèi)部解耦為 Observer, Dep, 并使用 Watcher 相連。
那根據(jù)這句話我們可以把整一個(gè)簡(jiǎn)單的MVVM框架粗分為以下四個(gè)模塊:
1.模板編譯(Compile)
2.數(shù)據(jù)劫持(Observer)
3.訂閱發(fā)布(Dep)
4.觀察者(Watcher)
我們就根據(jù)這四個(gè)模塊來(lái)分析、手寫一個(gè)MVVM框架。
想看源碼的,請(qǐng)直接下滑到最后。

MVVM類

和Vue類似,我們構(gòu)建一個(gè)MVVM類,通過(guò)new指令創(chuàng)建一個(gè)MVVM實(shí)例,并傳入一個(gè)類型為對(duì)象的參數(shù)option,包含當(dāng)前實(shí)例的作用域el和模板綁定的數(shù)據(jù)data。

class MVVM {
  constructor(options) {
    // 掛載實(shí)例
    this.$el = options.el;
    this.$data = options.data;

    // 編譯模板
    if(this.$el) {
      // 數(shù)據(jù)劫持 把對(duì)象的所有屬性 改成帶set 和 get 方法的
      new Observer(this.$data)
      
      // 將數(shù)據(jù)代理到實(shí)例上,直接操作實(shí)例即可,不需要通過(guò)vm.$data來(lái)進(jìn)行操作
      this.proxyData(this.$data)
      // 用數(shù)據(jù)和元素進(jìn)行編譯
      new Compile(this.$el, this)
    }
  }

  proxyData(data) {
    Object.keys(data).forEach(key => {
      Object.defineProperty(this, key, {
        get() {
          return data[key]
        },
        set(newValue) {
          data[key] = newValue
        }
      })
    })
  }
} 

MVVM類整合了所有的模塊,作為連接CompileObserver的橋梁。

模板編譯(Compile)

Compile

compile在編譯模板的時(shí)候,其實(shí)是從指令和文本兩個(gè)方面來(lái)處理的。

class Compile {
  constructor(el, vm) {
    // 判斷是否為DOM,若不是,自己獲取
    this.el = this.isElementNode(el) ? el : document.querySelector(el);
    this.vm = vm;
    if (this.el) {
      // 1. 將真實(shí)DOM放進(jìn)內(nèi)存中
      let fragment = this.node2fragment(this.el);
      // 2. 開(kāi)始編譯 提取想要的元素節(jié)點(diǎn) v-model 和 文本節(jié)點(diǎn) {{}}
      this.compile(fragment);
      // 3. 將編譯好的 fragment 重新放回頁(yè)面
      this.el.appendChild(fragment);
    }
  }

  /**
   * 輔助方法
   * 是否為元素節(jié)點(diǎn)
   * @isElementNode
   * 是否為指令
   * @isDirective
   */
  isElementNode(node) {
    return node.nodeType === 1;
  }
  isDirective(name) {
    return name.includes("v-");
  }

  /**
   * 核心方法
   */
  compileElement(node) {
    // v-model  v-text
    let attrs = node.attributes; // 取出當(dāng)前節(jié)點(diǎn)的屬性
    Array.from(attrs).forEach(attr => {
      let attrName = attr.name;
      if (this.isDirective(attrName)) {
        // 判斷屬性名是否包含 v-model

        // 取到對(duì)應(yīng)的值,放到節(jié)點(diǎn)中
        let expr = attr.value;
        let [, type] = attrName.split("-");  //解構(gòu)賦值v-model-->model

        // 調(diào)用對(duì)應(yīng)的編譯方法, 編譯哪個(gè)節(jié)點(diǎn),用數(shù)據(jù)替換掉表達(dá)式
        CompileUtil[type](node, this.vm, expr);
      }
    });
  }

  compileText(node) {
    let expr = node.textContent; // 取出文本中的內(nèi)容
    let reg = /\{\{([^]+)\}\}/g; // {{a}} {} {{c}}
    if (reg.test(expr)) {
      // 調(diào)用編譯文本的方法,編輯哪個(gè)節(jié)點(diǎn),用數(shù)據(jù)替換掉表達(dá)式
      CompileUtil["text"](node, this.vm, expr);
    }
  }

  // 遞歸
  compile(fragment) {
    let childNodes = fragment.childNodes;
    Array.from(childNodes).forEach(node => {
      if (this.isElementNode(node)) {
        // 如果是元素的節(jié)點(diǎn),則繼續(xù)深入檢查
        // 編譯元素
        this.compileElement(node);
        this.compile(node);
      } else {
        // 文本節(jié)點(diǎn)
        // 編譯文本
        this.compileText(node);
      }
    });

    // Array.from()方法是將一個(gè)類數(shù)組對(duì)象或者可遍歷對(duì)象轉(zhuǎn)換成一個(gè)真正的數(shù)組
  }

  // 將el中的內(nèi)容全部放進(jìn)內(nèi)存中
  node2fragment(el) {
    // 文檔碎片 內(nèi)存中的 dom 節(jié)點(diǎn)
    let fragment = document.createDocumentFragment();
    let firstChild;
    // 把值賦給變量 取不到后返回null,null作為條件
    while ((firstChild = el.firstChild)) {
      // 使用appendChild() 方法從一個(gè)元素向另一個(gè)元素中移動(dòng)
      fragment.appendChild(firstChild);
    }

    return fragment; // 內(nèi)存中的節(jié)點(diǎn)
  }
}

CompileUtil

CompileUtil是一個(gè)對(duì)象工具,配合Copmpile使用。

let CompileUtil = {
  model(node, vm, expr) {
    let updateFn = this.updater["modelUpdater"];

    /**
     *
     * 這里應(yīng)該加一個(gè)監(jiān)控,數(shù)據(jù)變化了 應(yīng)該調(diào)用watch的callback
     * (這里只是記錄原始的值 watcher的update沒(méi)有執(zhí)行,只有屬性的set執(zhí)行的時(shí)候,才會(huì)執(zhí)行cb回調(diào),重新進(jìn)行真實(shí)數(shù)據(jù)綁定)
     *
     */
    new Watcher(vm, expr, newValue => {
      // 當(dāng)值變化后會(huì)調(diào)用cb 將新的值傳遞過(guò)來(lái)
      updateFn && updateFn(node, this.getVal(vm, expr));
    });

    node.addEventListener("input", e => {
      let newValue = e.target.value;

      //監(jiān)聽(tīng)輸入事件,將輸入的內(nèi)容設(shè)置到對(duì)應(yīng)數(shù)據(jù)上
      this.setVal(vm, expr, newValue);
    });
    updateFn && updateFn(node, this.getVal(vm, expr));
  },
  text(node, vm, expr) {
    // 文本處理
    let updateFn = this.updater["textUpdater"];
    let value = this.getTextVal(vm, expr);

    expr.replace(/\{\{((?:.|\r?\n)+?)\}\}/g, (...args) => {
      new Watcher(vm, args[1], newValue => {
        // 如果數(shù)據(jù)變化了,文本節(jié)點(diǎn)需要重新獲取依賴的屬性,更新文本中的內(nèi)容
        updateFn && updateFn(node, this.getTextVal(vm, expr));
      });
    });
    updateFn && updateFn(node, value);
  },
  getTextVal(vm, expr) {
    // 獲取編譯文本后的結(jié)果
    let value = this.parseText(expr);
    let result = '';
    value.tokens.forEach((item) => {
      if(item.hasOwnProperty('@binding')) {
        result += this.getVal(vm, item['@binding'])
      } else {
        result += item
      }
    })
    return result
  },
  parseText(text) {
    const tagRE = /\{\{((?:.|\r?\n)+?)\}\}/g;
    if (!tagRE.test(text)) {
      return;
    }
    const tokens = [];
    const rawTokens = [];
    let lastIndex = (tagRE.lastIndex = 0);
    let match, index, tokenValue;
    while ((match = tagRE.exec(text))) {
      index = match.index;
      // push text token
      if (index > lastIndex) {
        rawTokens.push((tokenValue = text.slice(lastIndex, index)));
        tokens.push(JSON.stringify(tokenValue));
      }
      // tag token 
      const exp = match[1].trim();
      tokens.push(`_s(${exp})`);
      rawTokens.push({ "@binding": exp });
      lastIndex = index + match[0].length;
    }

    if (lastIndex < text.length) {
      rawTokens.push((tokenValue = text.slice(lastIndex)));
      tokens.push(JSON.stringify(tokenValue));
    }
    return {
      expression: tokens.join("+"),
      tokens: rawTokens
    };
  },
  setVal(vm, expr, value) {
    expr = expr.split(".");
    return expr.reduce((prev, next, currentIndex) => {
      if (currentIndex === expr.length - 1) {
        return (prev[next] = value);
      }
      return prev[next];
    }, vm.$data);
  },
  getVal(vm, expr) {
    // 獲取實(shí)例上對(duì)應(yīng)的數(shù)據(jù)
    expr = expr.split("."); // {{message.a}} [message, a]

    // vm.$data.message => vm.$data.message.a
    return expr.reduce((prev, next) => {
      return prev[next.trim()];
    }, vm.$data);

    /**
     *  關(guān)于 reduce:
     * arr.reduce(callback,[initialValue])
     */
  },
  updater: {
    // 文本更新
    textUpdater(node, value) {
      node.textContent = value;
    },
    // 輸入框更新
    modelUpdater(node, value) {
      node.value = value;
    }
  }
};

再次認(rèn)識(shí)到正則表達(dá)式的重要性。
在處理{{}}模板引擎的時(shí)候,遇到一個(gè)bug,在一個(gè)DOM節(jié)點(diǎn)里,如果有個(gè)有多個(gè){{}}{{}}會(huì)顯示為undefined,后來(lái)仔細(xì)閱讀了vueJs的源碼,借鑒其中parseText()方法,進(jìn)行處理,得以解決。

數(shù)據(jù)劫持(Observer)

什么是數(shù)據(jù)劫持?

在訪問(wèn)或修改對(duì)象的某個(gè)屬性時(shí),通過(guò)一段代碼攔截這個(gè)行為,進(jìn)行額外的操作,或者修改返回的結(jié)果。

數(shù)據(jù)劫持的作用是什么?

它是雙向數(shù)據(jù)綁定的核心方法,通過(guò)劫持對(duì)象屬性的settergetter操作,監(jiān)聽(tīng)數(shù)據(jù)的變化,同時(shí)也是后期ES6中很多語(yǔ)法糖底層實(shí)現(xiàn)的核心方法。

使用Object.defineProperty()做數(shù)據(jù)劫持,有什么弊端?

1、不能監(jiān)聽(tīng)數(shù)組的變化
2、必須遍歷對(duì)象的每個(gè)屬性
3、必須深層遍歷嵌套的對(duì)象

MVVM中的數(shù)據(jù)劫持

class Observer {
  constructor(data) {
    this.observe(data)
  }

  observe(data) {
    // 要對(duì)這個(gè)data數(shù)據(jù),將原有的屬性改成set和get的形式

    // defineProperty針對(duì)的是對(duì)象
    if(!data || typeof data !== 'object') {
      return
    }

    // 將數(shù)據(jù)一一劫持,先獲取到data的key和value
    Object.keys(data).forEach(key => {
      // 定義響應(yīng)式變化
      this.defineReactive(data, key, data[key])
      this.observe(data[key]) //深度遞歸劫持
    })

    // 關(guān)于Object.keys() 返回一個(gè)包含對(duì)象的屬性名稱的數(shù)組
  }

  // 定義響應(yīng)式
  defineReactive(obj, key, value) {
    let that = this;
    let dep = new Dep(); // 每個(gè)變化的數(shù)據(jù) 都會(huì)對(duì)應(yīng)一個(gè)數(shù)組,這個(gè)數(shù)組是存放所有更新的操作

    Object.defineProperty(obj, key, {
      enumerable: true,     // 是否能在for...in循環(huán)中遍歷出來(lái)或在Object.keys中列舉出來(lái)
      configurable: true,   // false,不可修改、刪除目標(biāo)屬性或修改屬性性以下特性
      get() {
        Dep.target && dep.addSub(Dep.target)
        return value;
      },
      set(newValue) {
        if(newValue != value) {
          that.observe(newValue);  // 如果設(shè)置的是對(duì)象,繼續(xù)劫持
          value = newValue;
          dep.notify(); //通知所有人 數(shù)據(jù)更新了
        }
      }
    })
  }
}

訂閱發(fā)布(Dep)

其實(shí)發(fā)布訂閱說(shuō)白了就是把要執(zhí)行的函數(shù)統(tǒng)一存儲(chǔ)在一個(gè)數(shù)組subs中管理,當(dāng)達(dá)到某個(gè)執(zhí)行條件時(shí),循環(huán)這個(gè)數(shù)組并執(zhí)行每一個(gè)成員。

class Dep {
  constructor() {
    // 訂閱數(shù)組
    this.subs = [];
  }

  // 添加訂閱
  addSub(watcher) {
    this.subs.push(watcher);
  }

  // 將消息通知給所有人
  notify() {
    this.subs.forEach(watcher => watcher.update());
  }
}

觀察者(Watcher)

Watcher 類的作用是,獲取更改前的值存儲(chǔ)起來(lái),并創(chuàng)建一個(gè) update 實(shí)例方法,當(dāng)值被更改時(shí),執(zhí)行實(shí)例的 callback 以達(dá)到視圖的更新。

class Watcher{  // 因?yàn)橐@取 oldValue,所以需要“數(shù)據(jù)”和“表達(dá)式”
  constructor(vm, expr, cb) {
    this.vm = vm;
    this.expr = expr;
    this.cb = cb;

    // 先獲取 oldValue 保存下來(lái)
    this.value = this.get();
  }

  getVal(vm, expr) {
    expr = expr.split('.');

    return expr.reduce((prev, next) => {
      return prev[next.trim()]
    }, vm.$data);
  }

  get() {
    // 在取值之前先將 watcher 保存到 Dep 上
    Dep.target = this;
    let value = this.getVal(this.vm, this.expr);
    Dep.target = null;
    return value;
  }

  // 對(duì)外暴露的方法,如果值改變就可以調(diào)用這個(gè)方法來(lái)更新
  update() {
    let newValue = this.getVal(this.vm, this.expr);
    let oldValue = this.value;
    if (newValue != oldValue) {
      this.cb(newValue);
    }
  }
}

最后

最后當(dāng)然是要檢測(cè)一下,我們的寫的代碼是不是能正常運(yùn)行。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Document</title>
  </head>
  <body>
    <div id="app">
      <!-- 雙向數(shù)據(jù)綁定 靠的是表單 -->
      <input type="text" v-model="message.a" />
      <div>{{ message.a }} 啦啦啦</div>
      {{ message.a }}
      {{ b }}
    </div>
  </body>
</html>
<script src="observer.js"></script>
<script src="watcher.js"></script>
<script src="compile.js"></script>
<script src="dep.js"></script>
<script src="mvvm.js"></script>
<script>
  let vm = new MVVM({
    el: "#app",
    data: {
      message: { a: "wlf" },
      b: "biubiubiu"
    }
  });
</script>

總結(jié)

我們根據(jù)下圖(參考《深入淺出vue.js》),將整個(gè)流程再梳理一遍:


流程圖.jpg

new MVVM() 后, MVVM 會(huì)進(jìn)行初始化即實(shí)例化MVVM,在這個(gè)過(guò)程中,模板綁定的數(shù)據(jù)data通過(guò)Observer數(shù)據(jù)劫持,轉(zhuǎn)換成了getter/setter的形式,來(lái)監(jiān)聽(tīng)數(shù)據(jù)的變化,當(dāng)被設(shè)置的對(duì)象被讀取的時(shí)候會(huì)執(zhí)行getter函數(shù),當(dāng)它被賦值的時(shí)候會(huì)執(zhí)行setter函數(shù)。

當(dāng)頁(yè)面渲染的時(shí)候,會(huì)讀取所需對(duì)象的值,這個(gè)時(shí)候會(huì)觸發(fā)getter函數(shù)從而將Watcher添加到Dep中進(jìn)行依賴收集,添加訂閱。

當(dāng)對(duì)象的值發(fā)生變化時(shí),會(huì)觸發(fā)對(duì)應(yīng)的setter函數(shù),setter會(huì)調(diào)用dep.notify()通知之前依賴收集得到的 Dep 中的每一個(gè) Watcher,也就是遍歷subs這個(gè)數(shù)組,告訴它們自己的值改變了,需要重新渲染視圖。這時(shí)候這些 Watcher就會(huì)開(kāi)始調(diào)用 update() 來(lái)更新視圖。

源碼地址:
https://github.com/lostimever/MVVM

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容