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

Vue作為當(dāng)前國內(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對應(yīng)視圖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-指令以及模板字面量等,并為相應(yīng)屬性添加訂閱者Watcher和回調(diào)函數(shù)

1.2 基本步驟如下

  1. Vue包括datatemplate兩部分,分別對應(yīng)Model與View
  2. 通過observedata的每一個屬性和其子屬性添加gettersetter
  3. 通過Dep實例來管理訂閱者,其中data的每一個屬性擁有一個Dep實例(dataDep實例為一對多的關(guān)系)
  4. 通過compile解析模板template,分析出那些是data的屬性并創(chuàng)建Watcher實例,添加至屬性對應(yīng)Dep實例中
  5. 當(dāng)data屬性值發(fā)生變化時,即調(diào)用屬性的getter時會觸發(fā)Dep實例的notify方法,接著出發(fā)Watcher實例的update方法,刷新視圖(Dep實例與Watcher實例同樣是一對多的關(guān)系`)
  6. 當(dāng)視圖數(shù)據(jù)發(fā)生變化時,改變data對應(yīng)屬性值,繼續(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;
    }
  },
  // 獲取屬性值(當(dāng)表達式為不只是key,而是一各需要運算的語句是如何處理?)
  getVMVal(vm, exp) {
    let src = vm;
    exp.split(".").forEach(k => {
      src = src[k];
    });
    return src;
  },
  // 設(shè)置屬性值
  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) {
  // 唯一標(biāo)識
  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作為唯一標(biāo)識 避免重復(fù)訂閱
    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)系作者
  • 序言:七十年代末敬锐,一起剝皮案震驚了整個濱河市辞嗡,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌滞造,老刑警劉巖续室,帶你破解...
    沈念sama閱讀 221,430評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異谒养,居然都是意外死亡挺狰,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,406評論 3 398
  • 文/潘曉璐 我一進店門买窟,熙熙樓的掌柜王于貴愁眉苦臉地迎上來丰泊,“玉大人,你說我怎么就攤上這事始绍⊥海” “怎么了?”我有些...
    開封第一講書人閱讀 167,834評論 0 360
  • 文/不壞的土叔 我叫張陵亏推,是天一觀的道長学赛。 經(jīng)常有香客問我,道長吞杭,這世上最難降的妖魔是什么盏浇? 我笑而不...
    開封第一講書人閱讀 59,543評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮芽狗,結(jié)果婚禮上绢掰,老公的妹妹穿的比我還像新娘。我一直安慰自己童擎,他們只是感情好滴劲,可當(dāng)我...
    茶點故事閱讀 68,547評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著顾复,像睡著了一般班挖。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上捕透,一...
    開封第一講書人閱讀 52,196評論 1 308
  • 那天聪姿,我揣著相機與錄音碴萧,去河邊找鬼乙嘀。 笑死末购,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的虎谢。 我是一名探鬼主播盟榴,決...
    沈念sama閱讀 40,776評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼婴噩!你這毒婦竟也來了擎场?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,671評論 0 276
  • 序言:老撾萬榮一對情侶失蹤几莽,失蹤者是張志新(化名)和其女友劉穎迅办,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體章蚣,經(jīng)...
    沈念sama閱讀 46,221評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡站欺,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,303評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了纤垂。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片矾策。...
    茶點故事閱讀 40,444評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖峭沦,靈堂內(nèi)的尸體忽然破棺而出贾虽,到底是詐尸還是另有隱情,我是刑警寧澤吼鱼,帶...
    沈念sama閱讀 36,134評論 5 350
  • 正文 年R本政府宣布蓬豁,位于F島的核電站,受9級特大地震影響菇肃,放射性物質(zhì)發(fā)生泄漏庆尘。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,810評論 3 333
  • 文/蒙蒙 一巷送、第九天 我趴在偏房一處隱蔽的房頂上張望驶忌。 院中可真熱鬧,春花似錦笑跛、人聲如沸付魔。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,285評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽几苍。三九已至,卻和暖如春陈哑,著一層夾襖步出監(jiān)牢的瞬間妻坝,已是汗流浹背伸眶。 一陣腳步聲響...
    開封第一講書人閱讀 33,399評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留刽宪,地道東北人厘贼。 一個月前我還...
    沈念sama閱讀 48,837評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像圣拄,于是被迫代替她去往敵國和親嘴秸。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,455評論 2 359

推薦閱讀更多精彩內(nèi)容