Vue2.x 的雙向綁定原理及實(shí)現(xiàn)

Vue 數(shù)據(jù)雙向綁定原理

Vue 是利用的 Object.defineProperty()方法進(jìn)行的數(shù)據(jù)劫持,利用 set嫡秕、get 來檢測數(shù)據(jù)的讀寫禁荒。

<iframe width="100%" height="300" src="http://jsrun.net/RMIKp/embedded/all/light" allowfullscreen="allowfullscreen" frameborder="0"></iframe>

MVVM 框架主要包含兩個方面海渊,數(shù)據(jù)變化更新視圖痊班,視圖變化更新數(shù)據(jù)勤婚。

視圖變化更新數(shù)據(jù)摹量,如果是像 input 這種標(biāo)簽涤伐,可以使用 oninput 事件..

數(shù)據(jù)變化更新視圖可以使用 Object.definProperty()的 set 方法可以檢測數(shù)據(jù)變化,當(dāng)數(shù)據(jù)改變就會觸發(fā)這個函數(shù)缨称,然后更新視圖凝果。

實(shí)現(xiàn)過程

我們知道了如何實(shí)現(xiàn)雙向綁定了,首先要對數(shù)據(jù)進(jìn)行劫持監(jiān)聽睦尽,所以我們需要設(shè)置一個 Observer 函數(shù)器净,用來監(jiān)聽所有屬性的變化。

如果屬性發(fā)生了變化当凡,那就要告訴訂閱者 watcher 看是否需要更新數(shù)據(jù)山害,如果訂閱者有多個,則需要一個 Dep 來收集這些訂閱者沿量,然后在監(jiān)聽器 observer 和 watcher 之間進(jìn)行統(tǒng)一管理浪慌。

還需要一個指令解析器 compile,對需要監(jiān)聽的節(jié)點(diǎn)和屬性進(jìn)行掃描和解析朴则。

因此权纤,流程大概是這樣的:

  1. 實(shí)現(xiàn)一個監(jiān)聽器 Observer,用來劫持并監(jiān)聽所有屬性乌妒,如果發(fā)生變動汹想,則通知訂閱者。
  2. 實(shí)現(xiàn)一個訂閱者 Watcher撤蚊,當(dāng)接到屬性變化的通知時古掏,執(zhí)行對應(yīng)的函數(shù),然后更新視圖侦啸,使用 Dep 來收集這些 Watcher槽唾。
  3. 實(shí)現(xiàn)一個解析器 Compile,用于掃描和解析的節(jié)點(diǎn)的相關(guān)指令匹中,并根據(jù)初始化模板以及初始化相應(yīng)的訂閱器夏漱。
仿Vue導(dǎo)圖.png

顯示一個 Observer

Observer 是一個數(shù)據(jù)監(jiān)聽器,核心方法是利用 Object.defineProperty()通過遞歸的方式對所有屬性都添加 setter顶捷、getter 方法進(jìn)行監(jiān)聽挂绰。

var library = {
  book1: {
    name: "",
  },
  book2: "",
};
observe(library);
library.book1.name = "vue權(quán)威指南"; // 屬性name已經(jīng)被監(jiān)聽了,現(xiàn)在值為:“vue權(quán)威指南”
library.book2 = "沒有此書籍"; // 屬性book2已經(jīng)被監(jiān)聽了,現(xiàn)在值為:“沒有此書籍”

// 為數(shù)據(jù)添加檢測
function defineReactive(data, key, val) {
  observe(val); // 遞歸遍歷所有子屬性
  let dep = new Dep(); // 新建一個dep
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function() {
      if (Dep.target) {
        // 判斷是否需要添加訂閱者葵蒂,僅第一次需要添加交播,之后就不用了,詳細(xì)看Watcher函數(shù)
        dep.addSub(Dep.target); // 添加一個訂閱者
      }
      return val;
    },
    set: function(newVal) {
      if (val == newVal) return; // 如果值未發(fā)生改變就return
      val = newVal;
      console.log(
        "屬性" + key + "已經(jīng)被監(jiān)聽了践付,現(xiàn)在值為:“" + newVal.toString() + "”"
      );
      dep.notify(); // 如果數(shù)據(jù)發(fā)生變化秦士,就通知所有的訂閱者。
    },
  });
}

// 監(jiān)聽對象的所有屬性
function observe(data) {
  if (!data || typeof data !== "object") {
    return; // 如果不是對象就return
  }
  Object.keys(data).forEach(function(key) {
    defineReactive(data, key, data[key]);
  });
}
// Dep 負(fù)責(zé)收集訂閱者永高,當(dāng)屬性發(fā)生變化時隧土,觸發(fā)更新函數(shù)。
function Dep() {
  this.subs = {};
}
Dep.prototype = {
  addSub: function(sub) {
    this.subs.push(sub);
  },
  notify: function() {
    this.subs.forEach((sub) => sub.update());
  },
};

思路分析中命爬,需要有一個可以容納訂閱者消息訂閱器 Dep曹傀,用于收集訂閱者,在屬性發(fā)生變化時執(zhí)行對應(yīng)的更新函數(shù)饲宛。

從代碼上看皆愉,將訂閱器 Dep 添加在 getter 里,是為了讓 Watcher 初始化時觸發(fā)艇抠,幕庐,因此,需要判斷是否需要訂閱者家淤。

在 setter 中异剥,如果有數(shù)據(jù)發(fā)生變化,則通知所有的訂閱者媒鼓,然后訂閱者就會更新對應(yīng)的函數(shù)届吁。

到此為止,一個比較完整的 Observer 就完成了绿鸣,接下來開始設(shè)計 Watcher.

實(shí)現(xiàn) Watcher

訂閱者 Watcher 需要在初始化的時候?qū)⒆约禾砑拥接嗛喥?Dep 中疚沐,我們已經(jīng)知道監(jiān)聽器 Observer 是在 get 時執(zhí)行的 Watcher 操作,所以只需要在 Watcher 初始化的時候觸發(fā)對應(yīng)的 get 函數(shù)去添加對應(yīng)的訂閱者操作即可潮模。

那給如何觸發(fā) get 呢亮蛔?因?yàn)槲覀円呀?jīng)設(shè)置了 Object.defineProperty(),所以只需要獲取對應(yīng)的屬性值就可以觸發(fā)了擎厢。

我們只需要在訂閱者 Watcher 初始化的時候究流,在 Dep.target 上緩存下訂閱者,添加成功之后在將其去掉就可以了动遭。

function Watcher(vm, exp, cb) {
  this.cb = cb;
  this.vm = vm;
  this.exp = exp;
  this.value = this.get(); // 將自己添加到訂閱器的操作
}

Watcher.prototype = {
  update: function() {
    this.run();
  },
  run: function() {
    var value = this.vm.data[this.exp];
    var oldVal = this.value;
    if (value !== oldVal) {
      this.value = value;
      this.cb.call(this.vm, value, oldVal);
    }
  },
  get: function() {
    Dep.target = this; // 緩存自己芬探,用于判斷是否添加watcher。
    var value = this.vm.data[this.exp]; // 強(qiáng)制執(zhí)行監(jiān)聽器里的get函數(shù)
    Dep.target = null; // 釋放自己
    return value;
  },
};

到此為止厘惦, 簡單的額 Watcher 設(shè)計完畢偷仿,然后將 Observer 和 Watcher 關(guān)聯(lián)起來,就可以實(shí)現(xiàn)一個簡單的的雙向綁定了。

因?yàn)檫€沒有設(shè)計解析器 Compile酝静,所以可以先將模板數(shù)據(jù)寫死节榜。

將代碼轉(zhuǎn)化為 ES6 構(gòu)造函數(shù)的寫法,預(yù)覽試試别智。

<iframe width="100%" height="400" src="http://jsrun.net/8SIKp/embedded/all/light" allowfullscreen="allowfullscreen" frameborder="0"></iframe>

這段代碼因?yàn)闆]有實(shí)現(xiàn)編譯器而是直接傳入了所綁定的變量宗苍,我們只在一個節(jié)點(diǎn)上設(shè)置一個數(shù)據(jù)(name)進(jìn)行綁定,然后在頁面上進(jìn)行 new MyVue薄榛,就可以實(shí)現(xiàn)雙向綁定了讳窟。

并兩秒后進(jìn)行值得改變,可以看到蛇数,頁面也發(fā)生了變化挪钓。

// MyVue
proxyKeys(key) {
    var self = this;
    Object.defineProperty(this, key, {
        enumerable: false,
        configurable: true,
        get: function proxyGetter() {
            return self.data[key];
        },
        set: function proxySetter(newVal) {
            self.data[key] = newVal;
        }
    });
}

上面這段代碼的作用是將 this.data 的 key 代理到 this 上是越,使得我可以方便的使用 this.xx 就可以取到 this.data.xx耳舅。

實(shí)現(xiàn) Compile

雖然上面實(shí)現(xiàn)了雙向數(shù)據(jù)綁定,但是整個過程都沒有解析 DOM 節(jié)店倚评,而是固定替換的浦徊,所以接下來要實(shí)現(xiàn)一個解析器來做數(shù)據(jù)的解析和綁定工作。

解析器 compile 的實(shí)現(xiàn)步驟:

  1. 解析模板指令天梧,并替換模板數(shù)據(jù)盔性,初始化視圖。
  2. 將模板指定對應(yīng)的節(jié)點(diǎn)綁定對應(yīng)的更新函數(shù)呢岗,初始化相應(yīng)的訂閱器冕香。

為了解析模板,首先需要解析 DOM 數(shù)據(jù)后豫,然后對含有 DOM 元素上的對應(yīng)指令進(jìn)行處理悉尾,因此整個 DOM 操作較為頻繁,可以新建一個 fragment 片段挫酿,將需要的解析的 DOM 存入 fragment 片段中在進(jìn)行處理构眯。

function nodeToFragment(el) {
  var fragment = document.createDocumentFragment();
  var child = el.firstChild;
  while (child) {
    // 將Dom元素移入fragment中
    fragment.appendChild(child);
    child = el.firstChild;
  }
  return fragment;
}

接下來需要遍歷各個節(jié)點(diǎn),對含有相關(guān)指令和模板語法的節(jié)點(diǎn)進(jìn)行特殊處理早龟,先進(jìn)行最簡單模板語法處理惫霸,使用正則解析“{{變量}}”這種形式的語法。

function compileElement (el) {
    var childNodes = el.childNodes;
    var self = this;
    [].slice.call(childNodes).forEach(function(node) {
        var reg = /\{\{(.*)\}\}/; // 匹配{{xx}}
        var text = node.textContent;
        if (self.isTextNode(node) && reg.test(text)) {  // 判斷是否是符合這種形式{{}}的指令
            self.compileText(node, reg.exec(text)[1]);
        }
        if (node.childNodes && node.childNodes.length) {
            self.compileElement(node);  // 繼續(xù)遞歸遍歷子節(jié)點(diǎn)
        }
    });
},
function compileText (node, exp) {
    var self = this;
    var initText = this.vm[exp];
    updateText(node, initText);  // 將初始化的數(shù)據(jù)初始化到視圖中
    new Watcher(this.vm, exp, function (value) {  // 生成訂閱器并綁定更新函數(shù)
        self.updateText(node, value);
    });
},
function updateText (node, value) {
    node.textContent = typeof value == 'undefined' ? '' : value;
}

獲取到最外層的節(jié)點(diǎn)后葱弟,調(diào)用 compileElement 函數(shù)壹店,對所有的子節(jié)點(diǎn)進(jìn)行判斷,如果節(jié)點(diǎn)是文本節(jié)點(diǎn)切匹配{{}}這種形式的指令芝加,則進(jìn)行編譯處理硅卢,初始化對應(yīng)的參數(shù)。

然后需要對當(dāng)前參數(shù)生成一個對應(yīng)的更新函數(shù)訂閱器,在數(shù)據(jù)發(fā)生變化時更新對應(yīng)的 DOM老赤。

這樣就完成了解析轮洋、初始化、編譯三個過程了抬旺。

接下來改造一個 myVue 就可以使用模板變量進(jìn)行雙向數(shù)據(jù)綁定了弊予。

<iframe width="100%" height="400" src="http://jsrun.net/K4IKp/embedded/all/light" allowfullscreen="allowfullscreen" frameborder="0"></iframe>

添加解析事件

添加完 compile 之后,一個數(shù)據(jù)雙向綁定就基本完成了开财,接下來就是在 Compile 中添加更多指令的解析編譯汉柒,比如 v-model、v-on责鳍、v-bind 等碾褂。

添加一個 v-model 和 v-on 解析:

function compile(node) {
  var nodeAttrs = node.attributes;
  var self = this;
  Array.prototype.forEach.call(nodeAttrs, function(attr) {
    var attrName = attr.name;
    if (isDirective(attrName)) {
      var exp = attr.value;
      var dir = attrName.substring(2);
      if (isEventDirective(dir)) {
        // 事件指令
        self.compileEvent(node, self.vm, exp, dir);
      } else {
        // v-model 指令
        self.compileModel(node, self.vm, exp, dir);
      }
      node.removeAttribute(attrName); // 解析完畢,移除屬性
    }
  });
}
// v-指令解析
function isDirective(attr) {
  return attr.indexOf("v-") == 0;
}
// on: 指令解析
function isEventDirective(dir) {
  return dir.indexOf("on:") === 0;
}

上面的 compile 函數(shù)是用于遍歷當(dāng)前 dom 的所有節(jié)點(diǎn)屬性历葛,然后判斷屬性是否是指令屬性正塌,如果是在做對應(yīng)的處理(事件就去監(jiān)聽事件、數(shù)據(jù)就去監(jiān)聽數(shù)據(jù)..)

完整版 myVue

在 MyVue 中添加 mounted 方法恤溶,在所有操作都做完時執(zhí)行乓诽。

class MyVue {
  constructor(options) {
    var self = this;
    this.data = options.data;
    this.methods = options.methods;
    Object.keys(this.data).forEach(function(key) {
      self.proxyKeys(key);
    });
    observe(this.data);
    new Compile(options.el, this);
    options.mounted.call(this); // 所有事情處理好后執(zhí)行mounted函數(shù)
  }
  proxyKeys(key) {
    // 將this.data屬性代理到this上
    var self = this;
    Object.defineProperty(this, key, {
      enumerable: false,
      configurable: true,
      get: function getter() {
        return self.data[key];
      },
      set: function setter(newVal) {
        self.data[key] = newVal;
      },
    });
  }
}

然后就可以測試使用了。

<iframe width="100%" height="400" src="http://jsrun.net/Y4IKp/embedded/all/light" allowfullscreen="allowfullscreen" frameborder="0"></iframe>

總結(jié)一下流程咒程,回頭在哪看一遍這個圖鸠天,是不是清楚很多了。


vue2.x流程圖2.png

參考

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末帐姻,一起剝皮案震驚了整個濱河市稠集,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌饥瓷,老刑警劉巖剥纷,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異扛伍,居然都是意外死亡筷畦,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進(jìn)店門刺洒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來鳖宾,“玉大人,你說我怎么就攤上這事逆航《ξ模” “怎么了?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵因俐,是天一觀的道長拇惋。 經(jīng)常有香客問我周偎,道長,這世上最難降的妖魔是什么撑帖? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任蓉坎,我火速辦了婚禮,結(jié)果婚禮上胡嘿,老公的妹妹穿的比我還像新娘蛉艾。我一直安慰自己,他們只是感情好衷敌,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布勿侯。 她就那樣靜靜地躺著,像睡著了一般缴罗。 火紅的嫁衣襯著肌膚如雪助琐。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天面氓,我揣著相機(jī)與錄音兵钮,去河邊找鬼。 笑死侧但,一個胖子當(dāng)著我的面吹牛矢空,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播禀横,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼粥血!你這毒婦竟也來了柏锄?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤复亏,失蹤者是張志新(化名)和其女友劉穎趾娃,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體缔御,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡抬闷,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了耕突。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片笤成。...
    茶點(diǎn)故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖眷茁,靈堂內(nèi)的尸體忽然破棺而出炕泳,到底是詐尸還是另有隱情,我是刑警寧澤上祈,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布培遵,位于F島的核電站浙芙,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏籽腕。R本人自食惡果不足惜嗡呼,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望皇耗。 院中可真熱鬧晤锥,春花似錦、人聲如沸廊宪。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽箭启。三九已至壕翩,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間傅寡,已是汗流浹背放妈。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留荐操,地道東北人芜抒。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像托启,于是被迫代替她去往敵國和親宅倒。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評論 2 355

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