實現(xiàn)類似Vue的 簡單雙向數(shù)據(jù)綁定

Vue作為當前國內(nèi)使用廣泛的前端MVVM框架,其中的雙向數(shù)據(jù)綁定大大減少了前端代碼維護數(shù)值變化的難度,顯得高效而神秘,那么今天就來解開其神秘面紗!動手實現(xiàn)簡單的雙向數(shù)據(jù)綁定,本項目源碼請猛戳這里。

1. 實現(xiàn)簡單的vue雙向數(shù)據(jù)綁定

animate.gif

1.1 基本原理

  • 首先看原理圖如下

    data-binding.png

  • 其中主要部分及其功能(首字母大寫為課實例化的類,小寫為函數(shù))

  1. MVVM即Vue實例,主要包括datatemplate兩部分(其他暫不考慮)
  2. data對象數(shù)據(jù)模型Model,template對應視圖View
  3. observe為數(shù)據(jù)劫持模塊,主要實現(xiàn)數(shù)據(jù)的gettersetter,并為屬性綁定訂閱者,在屬性值發(fā)生變化是通知訂閱者
  4. Watcher為訂閱者,通過depend將自己添加至訂閱者管理模塊Dep實例中,主要實現(xiàn)屬性值變化時調(diào)用回調(diào)函數(shù)更新視圖
  5. Dep為訂閱者管理模塊,是建立observeWatcher的橋梁.通過notify通知所有訂閱者數(shù)據(jù)發(fā)生變化
  6. compile為模板解析模塊,解析v-指令以及模板字面量等,并為相應屬性添加訂閱者Watcher和回調(diào)函數(shù)

1.2 基本步驟如下

  1. Vue包括datatemplate兩部分,分別對應Model與View
  2. 通過observedata的每一個屬性和其子屬性添加gettersetter
  3. 通過Dep實例來管理訂閱者,其中data的每一個屬性擁有一個Dep實例(dataDep實例為一對多的關(guān)系)
  4. 通過compile解析模板template,分析出那些是data的屬性并創(chuàng)建Watcher實例,添加至屬性對應Dep實例中
  5. data屬性值發(fā)生變化時,即調(diào)用屬性的getter時會觸發(fā)Dep實例的notify方法,接著出發(fā)Watcher實例的update方法,刷新視圖(Dep實例與Watcher實例同樣是一對多的關(guān)系`)
  6. 當視圖數(shù)據(jù)發(fā)生變化時,改變data對應屬性值,繼續(xù)步驟5,實現(xiàn)視圖刷新

2. 用法

<div id='wu-app'>
       <input type="text" v-model='text'>
       <br>
       <label for="">Input value:{{text}}</label>
       <br>
       <input type="button" v-on:click='btnClick' value='Click Me'>
</div>
<script>
    window.onload = function () {
        var app = new W.Wu({
            el: '#wu-app',
            data: {
                text: 'Hello World!'
            },
            methods: {
                btnClick(e) {
                    this.text = 'You clicked the button!'
                }
            }
        })
    }
</script>

3. 代碼分析

3.1 主模塊:入口

export function Wu(options) {
  this.$options = options;
  this.$data = options.data || {};
  this.$methods = options.methods || {};
  this.$watched = options.watched;

  // 將data和methods以及computed中的屬性方法代理在自己身上
  proxy(this, this.$data);
  proxy(this, this.$methods);
  
  // 初始化數(shù)據(jù)劫持
  observe(this.$data);
  // 模板解析
  compile(options.el || document.body, this);
}

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

function observe(data) {
  if (!data || typeof data !== "object") return;
  Object.keys(data).forEach(function(key) {
    let val = data[key],
      dep = new Dep();
    //觀察子屬性
    observe(val);
    Object.defineProperty(data, key, {
      configurable: true,
      enumerable: true,
      get: function() {
        // console.log(`i get ${key}:${val}`);
        //添加訂閱者
        Dep.target && dep.addSub(Dep.target);
        return val;
      },
      set: function(newVal) {
        // console.log(`i set ${key}:${val}-->${newVal}`);
        val = newVal;
        //通知所有訂閱者數(shù)據(jù)變更
        dep.notify();
      }
    });
  });
}

3.3 模板解析

let compileUtil = {
  elementNodeType: 1,
  textNodeType: 3,
  isDirective(attr) {
    let reg = /v-|:|@/;
    return reg.test(attr);
    return (
      attr.indexof("v-") == 0 ||
      attr.indexof(":") == 0 ||
      attr.indexof("@") == 0
    );
  },
  // 將原生節(jié)點拷貝到fragment
  node2Fragment(node) {
    let frag = document.createDocumentFragment();
    [].slice.call(node.childNodes).forEach(child => {
      frag.appendChild(child);
    });
    return frag;
  },
  // 更新回調(diào)函數(shù)
  update(node, dir, newVal, oldVal) {
    switch (dir) {
      case "model":
        node.value = typeof newVal === "undefined" ? "" : newVal;
        break;
      case "class":
        break;
      case "html":
        break;
      case "text":
        node.textContent = typeof newVal === "undefined" ? "" : newVal;
        break;
    }
  },
  // 獲取屬性值(當表達式為不只是key,而是一各需要運算的語句是如何處理?)
  getVMVal(vm, exp) {
    let src = vm;
    exp.split(".").forEach(k => {
      src = src[k];
    });
    return src;
  },
  // 設置屬性值
  setVMVal(vm, exp, val) {
    let src = vm,
      keys = exp.split(".");
    for (let i = 0; i < keys.length; i++) {
      const k = keys[i];
      if (i < keys.length - 1) {
        src = src[k];
      } else {
        src[k] = val;
      }
    }
    return src;
  },
  // 解析節(jié)點
  compileNode(node, vm) {
    [].slice.call(node.childNodes).forEach(child => {
      switch (child.nodeType) {
        // 節(jié)點
        case compileUtil.elementNodeType:
          let attrs = child.attributes;
          [].slice.call(attrs).forEach(attr => {
            // 判斷是否為內(nèi)部指令
            let attrName = attr.name;
            if (this.isDirective(attrName)) {
              let dir = attrName.split(/v-|:|@/).join(""),
                exp = attr.value;
              // 事件
              if (dir.substring(0, 2) === "on") {
                child.addEventListener(
                  dir.substring(2),
                  //注意 this 指向
                  this.getVMVal(vm, exp).bind(vm)
                );
              } else {
                // 其他指令model bind text等
                this.update(child, dir, this.getVMVal(vm, exp));
                // 訂閱者
                new Watcher(vm, exp, (newVal, oldVal) => {
                  // 更新回調(diào)
                  this.update(child, dir, newVal, oldVal);
                });
                // model
                if (dir === "model") {
                  let oldVal = this.getVMVal(vm, exp);
                  // 注冊對于表單輸入項的input事件
                  child.addEventListener("input", e => {
                    var newVal = e.target.value;
                    if (newVal !== oldVal) {
                      // 更改數(shù)值
                      this.setVMVal(vm, exp, newVal);
                    }
                  });
                }
              }
              // 移除指令
              // child.removeAttributes(attr);
            }
          });
          if (child.childNodes && child.childNodes.length > 0) {
            this.compileNode(child, vm);
          }
          break;
        // 文本
        case compileUtil.textNodeType:
          var text = child.textContent,
            reg = /\{\{(.*)\}\}/;
          if (reg.test(text)) {
            var exp = reg.exec(text)[1];
            this.update(child, "text", this.getVMVal(vm, exp));
            new Watcher(vm, exp, (newVal, oldVal) => {
              this.update(child, "text", newVal, oldVal);
            });
          }
          break;
      }
    });
  }
};
// 模板解析
function compile(template, vm) {
  let el =
    template.nodeType == compileUtil.elementNodeType
      ? template
      : document.querySelector(template); // 取出id為el的第一個節(jié)點作為容器

  if (el) {
    // 將原始節(jié)點存為fragment進行操作 減少頁面渲染次數(shù) 提升效率
    let fragment = compileUtil.node2Fragment(el);
    compileUtil.compileNode(fragment, vm);
    // 處理完后 重新添加至容器
    el.appendChild(fragment);
  }
}

3.4 訂閱者

let _uid = 0;
export function Watcher(vm, exp, cb) {
  // 唯一標識
  this.id = _uid++;
  this.vm = vm;
  this.exp = exp;
  this.cb = cb;
  this.value = this.depend();
}

Watcher.prototype = {
  constructor: Watcher,
  depend() {
    Dep.target = this;
    //通過觸發(fā)getter,添加自己為訂閱者
    var value = this.vm[this.exp];
    Dep.target = null;
    return value;
  },
  // 更新
  update() {
    let oldVal = this.value,
      newVal = this.vm[this.exp];
    if (oldVal !== newVal) {
      this.value = newVal;
      this.cb(newVal, oldVal);
    }
  }
};

3.5 訂閱者管理

export function Dep() {
  // 鍵值對 
  this.subs = new Map();
}
Dep.prototype = {
  constructor: Dep,
  // 添加訂閱者
  addSub(watcher) {
    // 通過訂閱者id作為唯一標識 避免重復訂閱
    this.subs.set(watcher.id, watcher);
  },
  // 通知訂閱者
  notify() {
    this.subs.forEach(watcher => {
      watcher.update();
    });
  }
};
Dep.target = null;

參考


如果您感覺有所幫助,或者有問題需要交流,歡迎留言評論,非常感謝!
前端菜鳥,還請多多關(guān)照!


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

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

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