天天看點

160行代碼仿Vue實作極簡雙向綁定[詳細注釋]

前言

現在的前端面試不管你用的什麼架構,總會問你這個架構的雙向綁定機制,有的甚至要求你現場實作一個雙向綁定出來,那對于沒有好好研究過這方面知識的同學來說,當然是很難的,接下來本文用160行代碼帶你實作一個極簡的雙向綁定機制。如果喜歡的話可以點波贊/關注,支援一下,希望大家看完本文可以有所收獲。

本文是在

面試題:你能寫一個Vue的雙向資料綁定嗎?

的基礎上仔細研究+改動,并添加了詳細注釋,而成的。

160行代碼仿Vue實作極簡雙向綁定[詳細注釋]

demo位址:

codepen:

仿Vue極簡雙向綁定

Github:

了解Object.defineProperty():

這個API是實作雙向綁定的核心,最主要的作用是重寫資料的

get

set

方法。

使用方式:

let obj = {
      singer: "周傑倫"
    };
    let value = "青花瓷";
    Object.defineProperty(obj, "music", {
      // value: '七裡香', // 設定屬性的值 下面設定了get set函數 是以這裡不能設定
      configurable: false, // 是否可以删除屬性 預設不能删除
      // writable: true,  // 是否可以修改對象 下面設定了get set函數 是以這裡不能設定
      enumerable: true, // music是否可以被枚舉 預設是不能被枚舉(周遊)
      //  get,set設定時不能設定writable和value,要一對一對設定,交叉設定/同時存在 就會報錯
      get() {
        // 擷取obj.music的時候就會調用get方法
        // let value = "強行設定get的傳回值"; // 打開注釋 讀取屬性永遠都是‘強行設定get的傳回值’
        return value;
      },
      set(val) {
        // 将修改的值重新賦給song
        value = val;
      }
    });
    console.log(obj.music); // 青花瓷
    delete obj.music; // configurable設為false 删除無效
    console.log(obj.music); // 青花瓷
    obj.music = "聽媽媽的話"; 
    console.log(obj.music); // 聽媽媽的話
    for (let key in obj) {
      // 預設情況下通過defineProperty定義的屬性是不能被枚舉(周遊)的
      // 需要設定enumerable為true才可以 否則隻能拿到singer 屬性
      console.log(key); // singer, music
    }
           

示例demo:

對,這裡有個

demo

畫一下重點:

  • get,set設定時不能設定writable和value, 他們是一對情侶的存在,交叉設定或同時存在,會報錯
  • 通過

    defineProperty

    設定的屬性,預設不能删除,不能周遊,當然你可以通過設定更改他們。
  • get、set 是函數,可以做的事情很多。

相容性:IE 9,Firefox 4, Chorme 5,Opera 11.6,Safari 5.1

更詳細的可以看一下

MDN

實作思路:

mvvm系列的雙向綁定,關鍵步驟:

  1. 實作資料監聽器Observer,用

    Object.defineProperty()

    重寫資料的get、set,值更新就在set中通知訂閱者更新資料。
  2. 實作模闆編譯Compile,深度周遊dom樹,對每個元素節點的指令模闆進行替換資料以及訂閱資料。
  3. 實作Watch用于連接配接Observer和Compile,能夠訂閱并收到每個屬性變動的通知,執行指令綁定的相應回調函數,進而更新視圖。
  4. mvvm入口函數,整合以上三者。

流程圖:

160行代碼仿Vue實作極簡雙向綁定[詳細注釋]

這部分講的很清楚,現在有點懵逼也沒關系,看完代碼,自己copy下來玩一玩之後,回頭再看實作思路,相信會有收獲的。

具體代碼實作:

html結構:

<div id="app">
    <input type="text" v-model="name">
    <h3 v-bind="name"></h3>
    <input type="text" v-model="testData1">
    <h3>{{ testData1 }}</h3>
    <input type="text" v-model="testData2">
    <h3>{{ testData2 }}</h3>
</div>
           

看到這個模闆,相信用過Vue的同學都不會陌生。

調用方法:

采用類Vue方式來使用雙向綁定:

window.onload = function () {
    var app = new myVue({
        el: '#app', // dom
        data: { // 資料
            testData1: '仿Vue',
            testData2: '極簡雙向綁定',
            name: 'OBKoro1'
        }
    })
}
           

建立myVue函數:

實際上這裡是我們實作思路中的第四步,用于整合資料監聽器

this._observer()

、指令解析器

this._compile()

以及連接配接Observer和Compile的_watcherTpl的watch池。

function myVue(options = {}) {  // 防止沒傳,設一個預設值
        this.$options = options; // 配置挂載
        this.$el = document.querySelector(options.el); // 擷取dom
        this._data = options.data; // 資料挂載
        this._watcherTpl = {}; // watcher池
        this._observer(this._data); // 傳入資料,執行函數,重寫資料的get set
        this._compile(this.$el); // 傳入dom,執行函數,編譯模闆 釋出訂閱
    };
           

Watcher函數:

這是實作思路中的第三步,因為下方資料監聽器

_observer()

需要用到Watcher函數,是以這裡就先講了。

像實作思路中所說的,這裡起到了連接配接Observer和Compile的作用:

  1. 在模闆編譯_compile()階段釋出訂閱
  2. 在指派操作的時候,更新視圖
// new Watcher() 為this._compile()釋出訂閱+ 在this._observer()中set(指派)的時候更新視圖
 function Watcher(el, vm, val, attr) {
     this.el = el; // 指令對應的DOM元素
     this.vm = vm; // myVue執行個體
     this.val = val; // 指令對應的值 
     this.attr = attr; // dom擷取值,如value擷取input的值 / innerHTML擷取dom的值
     this.update(); // 更新視圖
 }
 Watcher.prototype.update = function () { 
     this.el[this.attr] = this.vm._data[this.val]; // 擷取data的最新值 指派給dom 更新視圖
 }
           

沒有看錯,代碼量就這麼多,可能需要把整個代碼連接配接起來,多看幾遍才能夠了解。

實作資料監聽器_observer():

實作思路中的第一步,用

Object.defineProperty()

周遊data重寫所有屬性的get set。

然後在給對象的某個屬性指派的時候,就會觸發set。

在set中我們可以監聽到資料的變化,然後就可以觸發watch更新視圖。

myVue.prototype._observer = function (obj) {
        var _this = this;
        Object.keys(obj).forEach(key => { // 周遊資料
            _this._watcherTpl[key] = { // 每個資料的訂閱池()
                _directives: []
            };
            var value = obj[key]; // 擷取屬性值
            var watcherTpl = _this._watcherTpl[key]; // 資料的訂閱池
            Object.defineProperty(_this._data, key, { // 雙向綁定最重要的部分 重寫資料的set get
                configurable: true,  // 可以删除
                enumerable: true, // 可以周遊
                get() {
                    console.log(`${key}擷取值:${value}`);
                    return value; // 擷取值的時候 直接傳回
                },
                set(newVal) { // 改變值的時候 觸發set
                    console.log(`${key}更新:${newVal}`);
                    if (value !== newVal) {
                        value = newVal;
                        watcherTpl._directives.forEach((item) => { // 周遊訂閱池 
                            item.update();
                            // 周遊所有訂閱的地方(v-model+v-bind+{{}}) 觸發this._compile()中釋出的訂閱Watcher 更新視圖  
                        });
                    }
                }
            })
        });
    }
           

實作Compile 模闆編譯

這裡是實作思路中的第三步,讓我們來總結一下這裡做了哪些事情:

  • 首先是深度周遊dom樹,周遊每個節點以及子節點。
  • 将模闆中的變量替換成資料,初始化渲染頁面視圖。
  • 把指令綁定的屬性添加到對應的訂閱池中
  • 一旦資料有變動,收到通知,更新視圖。
myVue.prototype._compile = function (el) {
      var _this = this, nodes = el.children; // 擷取app的dom
      for (var i = 0, len = nodes.length; i < len; i++) { // 周遊dom節點
          var node = nodes[i];
          if (node.children.length) {
              _this._compile(node);  // 遞歸深度周遊 dom樹
          }
          // 如果有v-model屬性,并且元素是INPUT或者TEXTAREA,我們監聽它的input事件    
          if (node.hasAttribute('v-model') && (node.tagName = 'INPUT' || node.tagName == 'TEXTAREA')) {
              node.addEventListener('input', (function (key) {
                  var attVal = node.getAttribute('v-model'); // 擷取v-model綁定的值
                  _this._watcherTpl[attVal]._directives.push(new Watcher( // 将dom替換成屬性的資料并釋出訂閱 在set的時候更新資料
                      node,
                      _this,
                      attVal,
                      'value'
                  ));
                  return function () {
                      _this._data[attVal] = nodes[key].value;  // input值改變的時候 将新值賦給資料 觸發set=>set觸發watch 更新視圖
                  }
              })(i));
          }
          if (node.hasAttribute('v-bind')) { // v-bind指令 
              var attrVal = node.getAttribute('v-bind'); // 綁定的data
              _this._watcherTpl[attrVal]._directives.push(new Watcher( // 将dom替換成屬性的資料并釋出訂閱 在set的時候更新資料
                  node,
                  _this,
                  attrVal,
                  'innerHTML'
              ))
          }
          var reg = /\{\{\s*([^}]+\S)\s*\}\}/g, txt = node.textContent;   // 正則比對{{}}
          if (reg.test(txt)) {
              node.textContent = txt.replace(reg, (matched, placeholder) => {
                  // matched比對的文本節點包括{{}}, placeholder 是{{}}中間的屬性名
                  var getName = _this._watcherTpl; // 所有綁定watch的資料
                  getName = getName[placeholder];  // 擷取對應watch 資料的值
                  if (!getName._directives) { // 沒有事件池 建立事件池
                      getName._directives = [];
                  }
                  getName._directives.push(new Watcher( // 将dom替換成屬性的資料并釋出訂閱 在set的時候更新資料
                      node,
                      _this,
                      placeholder,
                      'innerHTML'
                  ));
                  return placeholder.split('.').reduce((val, key) => {
                      return _this._data[key]; // 擷取資料的值 觸發get 傳回目前值 
                  }, _this.$el);
              });
          }
      }
  }
           

完整代碼&demo位址

GitHub完整代碼

結語

本文隻是一個簡單的實作雙向綁定的方法,主要目的是幫助各位同學了解mvvm架構的雙向綁定機制,也并沒有很完善,這裡還是有很多缺陷,比如:沒有實作資料的深度對資料進行

get

set

等。希望看完本文,大家能有所收獲。

原文釋出時間為:2018年06月25日

原文作者:OBKoro1

本文來源:

掘金

   如需轉載請聯系原作者

繼續閱讀