Vue高級--雙向綁定原理

1.原理

vue數(shù)據(jù)雙向綁 定是通過數(shù)據(jù)劫持結(jié)合發(fā)布者-訂閱者模式的方式,通過 **Object.defineProperty() ** 劫持各個屬性的 setter , getter , 在數(shù)據(jù)變動時發(fā)布消息給訂閱者沛简,觸發(fā)相應(yīng)的監(jiān)聽回調(diào)齐鲤。

2. 實(shí)現(xiàn)思路

要實(shí)現(xiàn)mvvm 的雙向綁定,必須實(shí)現(xiàn)以下幾點(diǎn):

  • 1 實(shí)現(xiàn)一個數(shù)據(jù)監(jiān)聽器 Observer椒楣,能夠?qū)?shù)據(jù)對象的所有屬性進(jìn)行監(jiān)聽给郊,如果變動,可拿到最新值并通知訂閱者捧灰。
  • 2 實(shí)現(xiàn)一個指令解析器 Compile丑罪,對每個元素節(jié)點(diǎn)的指令進(jìn)行掃描和解析,根據(jù)指令模板替換數(shù)據(jù)凤壁,以及綁定相應(yīng)的更新函數(shù)吩屹。
  • 3 實(shí)現(xiàn)一個 Watcher,作為連接 Observer 和 Compile 的橋梁拧抖,能夠訂閱并收到每個屬性變動的通知煤搜,執(zhí)行指令綁定的相應(yīng)回調(diào)函數(shù),從而更新視圖唧席。
  • 4 實(shí)現(xiàn)一個可以容納訂閱者的消息訂閱器 Dep 擦盾,訂閱器 Dep 主要負(fù)責(zé)收集訂閱器,然后在屬性變化的時候執(zhí)行對象的 訂閱者的更新函數(shù)淌哟。
  • 5 實(shí)現(xiàn)MVVM 入口函數(shù)迹卢,整合以上三者。

上述流程如圖所示:


image.png

3. 實(shí)現(xiàn)Observer 數(shù)據(jù)監(jiān)聽器

Observer 是一個數(shù)據(jù)監(jiān)聽器徒仓,其核心方法就是 Object.defineProperty()腐碱。如果要對所有屬性都進(jìn)行監(jiān)聽的話,那么可以通過遞歸方法遍歷所有屬性值掉弛,并對其進(jìn)行 Object.defineProperty()處理症见,如下代碼:

  • Observer.js
// 1.實(shí)現(xiàn)Observer觀察者
function Observer(data) {
  this.data = data;
  this.walk(data);
}

Observer.prototype = {
  constructor: Observer,
  walk: function (data) {
    let self = this;
    Object.keys(data).forEach(key => {
      self.defineReacctive(self.data, key, data[key])
    })
  },
  defineReacctive: function (data, key, val) {
    observe(val);//監(jiān)聽子屬性
    Object.defineProperty(data, key, {
      enumerable: true,//可枚舉
      configurable: false,//不能再define
      get: function () {
        return val;
      },
      set: function (newVal) {
        if (val === newVal) return;
        console.log('監(jiān)聽到的值發(fā)生變化了', val, '-->', newVal);
        val = newVal;
      }
    })
  }
}

function observe(data, vm) {
  if (!data || typeof data !== 'object') {
    return;
  }
  return new Observer(data)
}

4. 實(shí)現(xiàn)Dep 消息訂閱器

設(shè)計(jì)過程中,需要創(chuàng)建一個可以容納訂閱者的消息訂閱器 Dep殃饿,訂閱器 Dep主要負(fù)責(zé) 收集訂閱器Watcher谋作,然后在屬性變化的時候執(zhí)行對象訂閱者的更新函數(shù)。

  • Dep.js
// 實(shí)現(xiàn)消息訂閱器
function Dep() {
  this.subs = [];
}
Dep.prototype = {
  addSub: function (sub) {
    this.subs.push(sub)
  },
  notify: function () {
    this.subs.forEach(function (sub) {
      sub.update();
    })
  }
}
Dep.target = null;
  • 結(jié)合Dep 和Observer

將訂閱器添加的訂閱者 設(shè)計(jì)在Observer的 getter里面乎芳,這是為了讓 Watcher 初始化進(jìn)行觸發(fā)遵蚜,在 Observer的setter函數(shù)里面帖池,如果數(shù)據(jù)變化,就會通知所有訂閱者吭净,訂閱者們就會執(zhí)行對象的更新函數(shù)睡汹。一個比較完整的Observer 已經(jīng)實(shí)現(xiàn)了。

Observer.prototype = {
  constructor: Observer,
  walk: function (data) {
    let self = this;
    Object.keys(data).forEach(key => {
      self.defineReacctive(self.data, key, data[key])
    })
  },
  defineReacctive: function (data, key, val) {
    let dep = new Dep();
    observe(val);//監(jiān)聽子屬性
    Object.defineProperty(data, key, {
      enumerable: true,//可枚舉
      configurable: false,//不能再define
      get: function () {
        //將訂閱者Wather賦予給 Dep.target攒钳,每個訂閱者都是不一樣的
        Dep.target && dep.addSub(Dep.target);//在這里添加一個訂閱器
        return val;
      },
      set: function (newVal) {
        if (val === newVal) return;
        console.log('監(jiān)聽到的值發(fā)生變化了', val, '-->', newVal);
        val = newVal;
        dep.notify();//通知所有訂閱者
      }
    })
  }
}

5. 實(shí)現(xiàn)Compile 編譯指令

Compile主要做的事情是解析模板指令帮孔,將模板中的變量替換成數(shù)據(jù),然后初始化渲染頁面不撑,并將每個指定對應(yīng)的節(jié)點(diǎn)綁定更新函數(shù)文兢,添加監(jiān)聽數(shù)據(jù)的Watcher訂閱者,一旦數(shù)據(jù)有變動焕檬,收到通知姆坚,更新視圖,如圖所示:

image.png
  • Compile.js 代碼如下:
// 編譯指令
function Compile(el, vm) {
  this.$vm = vm;
  this.$el = this.isElementNode(el) ? el : document.querySelector(el);
  if (this.$el) {
    this.$fragment = this.nodeFragment(this.$el);
    this.init();
    this.$el.appendChild(this.$fragment);
  }
}
Compile.prototype = {
  constructor: Compile,
  nodeFragment: function (el) {
    let fragment = document.createDocumentFragment();
    let child;

    //將原生節(jié)點(diǎn)拷貝到fragment
    while (child = el.firstChild) {
      fragment.appendChild(child);
    }
    return fragment;
  },
  init: function () {
    this.compileElement(this.$fragment);
  },
  compileElement: function (el) {
    let childNodes = el.childNodes;
    let self = this;
    [].slice.call(childNodes).forEach(function (node) {
      let text = node.textContent;
      let reg = /\{\{(.*)\}\}/; //匹配 {{}}

      if (self.isElementNode(node)) {//元素節(jié)點(diǎn)
        self.compile(node)
      } else if (self.isTextNode(node) && reg.test(text)) { //文本節(jié)點(diǎn)且 {{}}
        self.compileText(node, RegExp.$1.trim());
      }

      if (node.childNodes && node.childNodes.length) {//擁有孩子節(jié)點(diǎn)实愚,繼續(xù)遞歸
        self.compileElement(node)
      }
    })
  },
  compile: function (node) {
    let nodeAttrs = node.attributes;
    let self = this;

    [].slice.call(nodeAttrs).forEach(function (attr) {
      let attrName = attr.name;
      if (self.isDirective(attrName)) {
        let exp = attr.value;
        let dir = attrName.substring(2);

        if (self.isEventDirective(dir)) {//事件指令
          compileUtil.eventHandler(node, self.$vm, exp, dir);
        } else {//普通指令
          compileUtil[dir] && compileUtil[dir](node, self.$vm, exp)
        }

        node.removeAttribute(attrName);
      }
    })
  },

  compileText: function (node, exp) {
    compileUtil.text(node, this.$vm, exp);
  },
  isDirective: function (attr) {//普通指令
    return attr.indexOf('v-') == 0;
  },
  isEventDirective: function (dir) {//事件指令
    return dir.indexOf('on') === 0;
  },
  isElementNode: function (node) {//元素節(jié)點(diǎn)
    return node.nodeType == 1;
  },
  isTextNode: function (node) {//文本節(jié)點(diǎn)
    return node.nodeType == 3;
  },


}
//指定處理集合
let compileUtil = {
  text: function (node, vm, exp) {
    this.bind(node, vm, exp, 'text');
  },
  html: function (node, vm, exp) {
    this.bind(node, vm, exp, 'html');
  },
  model: function (node, vm, exp) {
    this.bind(node, vm, exp, 'model');
    let self = this;
    let val = this._getVMVal(vm, exp);
    node.addEventListener('input', function (e) {
      let newVal = e.target.value;
      if (val === newVal) return;
      console.log(newVal);
      self._setVMVal(vm, exp, newVal);
      val = newVal
    })
  },
  class: function (node, vm, exp) {
    this.bind(node, vm, exp, 'class');
  },
  bind: function (node, vm, exp, dir) {
    let updateFn = updater[dir + 'Updater'];
    updateFn && updateFn(node, this._getVMVal(vm, exp));
    new Watcher(vm, exp, function (value, oldValue) {
      updateFn && updateFn(node, value, oldValue)
    })
  },

  //事件處理
  eventHandler: function (node, vm, exp, dir) {
    let eventType = dir.split(':')[1];
    let fn = vm.$options.methods && vm.$options.methods[exp];
    if (eventType && fn) {
      node.addEventListener(eventType, fn.bind(vm), false);
    }
  },
  _getVMVal:function(vm,exp) {
    let val = vm;
    exp = exp.split('.');
    exp.forEach(function(k) {
      val = val._data[k]
    })
    return val;
  },
  _setVMVal: function(vm,exp,value) {
    let val = vm;
    exp = exp.split('.');
    exp.forEach(function(k,i) {
      //非最后一個key兼呵,更新val的值
      if(i<exp.length -1) {
        val = val._data[k];
      } else {
        val._data[k] = value
      }
    })
  },
}

let updater = {
  textUpdater: function(node,value) {
    node.textContent = typeof value == 'undefined'? '' : value
  },
  htmlUpdater: function(node,value) {
    node.innerHtml = typeof value == 'undefined'? '' : value
  },
  classUpdater: function(node,value,oldValue) {
    let className = node.className;
    className = className.replace(oldValue,'').replace(/\s$/,'');
    let space = className && String(value)? ' ': '';
    node.className = className + space +value;
  },
  modelUpdater: function(node,value) {
    node.value = typeof value == 'undefined'? '' : value
  },
}

6. 實(shí)現(xiàn)Watcher 訂閱者

Watcher 訂閱者作為 Observer 和 Compile 之間通信的橋梁,主要做的事情是:
1.在自身實(shí)例化時往屬性訂閱器Dep 里面添加自己:在Dep.target上緩存訂閱器腊敲,通過觸發(fā) getter方法击喂,把自己添加到getter方法里面,添加成功后去掉Dep.target碰辅。
2.自身必須有一個update方法懂昂。
3.待屬性變動dep.notice()通知時,能調(diào)用自身的update方法没宾,并觸發(fā)Compil中綁定的回調(diào)凌彬。

function MVVM(options) {
  this.$options = options || {};
  let data = this._data = this.$options.data;
  let self = this;
  observe(data,self);
  this.$compile = new Compile(options.el || document.body, self)
}

從上面代碼可看出監(jiān)聽的數(shù)據(jù)對象是options.data,每次需要更新視圖循衰,則必須通過let vm = new MVVM({data:{name: 'zs'}}); vm._data.name = 'li';铲敛,這樣的方式來改變數(shù)據(jù)。
顯然不符合我們一開始的期望会钝,我們所期望的調(diào)用方式應(yīng)該是這樣的:
let vm = new MVVM({data:{name: 'zs'}}); vm.name = 'li';
所以這里我們需要給MVVM實(shí)例添加一個屬性代理的方法伐蒋,使訪問vm的屬性代理可訪問 vm._data的屬性,改造后的代碼如下:

function MVVM(options) {
  this.$options = options || {};
  console.log(this);
  let data = this._data = this.$options.data;
  let self = this;
  //數(shù)據(jù)代理
  //實(shí)現(xiàn) vm.xxx -> vm._data.xxx
  Object.keys(data).forEach(function (key) {
    self._proxyData(key)
  });
  this._initComputed();
  observe(data);
  this.$compile = new Compile(options.el || document.body, this)
}

MVVM.prototype = {
  constructor: MVVM,
  $watch: function (key, options, cb) {
    new Watcher(this, key, cb);
  },
  _proxyData: function (key, setter, getter) {
    let self = this;
    setter = setter ||
      Object.defineProperty(self, key, {
        enumerable: true,
        configurable: false,
        get: function proxyGetter() {
          return self._data[key]
        },
        set: function proxySetter(newVal) {
          self._data[key] = newVal;
        }
      })
  },
  _initComputed: function () {//添加計(jì)算屬性
    let self = this;
    let computed = this.$options.computed;
    if (typeof computed === 'object') {
      Object.keys(computed).forEach(function (key) {
        Object.defineProperty(self, key, {
          get: typeof computed[key] === 'function' ? computed[key] : computed[key].get,
          set: function() {}
        })
      })
    }
  },
}

這里主要利用了 Object.defineProperty() 這個方法來劫持 vm實(shí)例對象的屬性的讀寫權(quán)顽素,使讀寫vm實(shí)例的屬性 轉(zhuǎn)成了 vm._data的屬性值咽弦。

7. html

<body>
  <div id="app">
    <h1 id="name">{{name}}</h1>
    <input type="text" v-model="msg">
    <p>{{msg}}</p>
    <button v-on:click="clickHandle">change</button>
  </div>
</body>
<script src="./Dep.js"></script>
<script src="./Observer.js"></script>
<script src="./Watcher.js"></script>
<script src="./Compile.js"></script>
<script src="./MVVM.js"></script>
<script>
  let vm = new MVVM({
    el: '#app',
    data: {
      msg: 'hello'
    },
    methods: {
      clickHandle: function() {
        this.msg = 'hi';
        console.log(this);
      }
    },
  })
</script>

效果圖:


image.png

最后,源碼都放在gitee上面了胁出,請點(diǎn)擊源碼

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市段审,隨后出現(xiàn)的幾起案子全蝶,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,482評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件抑淫,死亡現(xiàn)場離奇詭異绷落,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)始苇,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評論 2 382
  • 文/潘曉璐 我一進(jìn)店門砌烁,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人催式,你說我怎么就攤上這事函喉。” “怎么了荣月?”我有些...
    開封第一講書人閱讀 152,762評論 0 342
  • 文/不壞的土叔 我叫張陵管呵,是天一觀的道長。 經(jīng)常有香客問我哺窄,道長捐下,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,273評論 1 279
  • 正文 為了忘掉前任萌业,我火速辦了婚禮坷襟,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘生年。我一直安慰自己婴程,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,289評論 5 373
  • 文/花漫 我一把揭開白布晶框。 她就那樣靜靜地躺著排抬,像睡著了一般。 火紅的嫁衣襯著肌膚如雪授段。 梳的紋絲不亂的頭發(fā)上蹲蒲,一...
    開封第一講書人閱讀 49,046評論 1 285
  • 那天,我揣著相機(jī)與錄音侵贵,去河邊找鬼届搁。 笑死,一個胖子當(dāng)著我的面吹牛窍育,可吹牛的內(nèi)容都是我干的卡睦。 我是一名探鬼主播,決...
    沈念sama閱讀 38,351評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼漱抓,長吁一口氣:“原來是場噩夢啊……” “哼表锻!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起乞娄,我...
    開封第一講書人閱讀 36,988評論 0 259
  • 序言:老撾萬榮一對情侶失蹤瞬逊,失蹤者是張志新(化名)和其女友劉穎显歧,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體确镊,經(jīng)...
    沈念sama閱讀 43,476評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡士骤,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,948評論 2 324
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了蕾域。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片拷肌。...
    茶點(diǎn)故事閱讀 38,064評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖旨巷,靈堂內(nèi)的尸體忽然破棺而出巨缘,到底是詐尸還是另有隱情,我是刑警寧澤契沫,帶...
    沈念sama閱讀 33,712評論 4 323
  • 正文 年R本政府宣布带猴,位于F島的核電站,受9級特大地震影響懈万,放射性物質(zhì)發(fā)生泄漏拴清。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,261評論 3 307
  • 文/蒙蒙 一会通、第九天 我趴在偏房一處隱蔽的房頂上張望口予。 院中可真熱鬧,春花似錦涕侈、人聲如沸沪停。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽木张。三九已至,卻和暖如春端三,著一層夾襖步出監(jiān)牢的瞬間舷礼,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評論 1 262
  • 我被黑心中介騙來泰國打工郊闯, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留妻献,地道東北人。 一個月前我還...
    沈念sama閱讀 45,511評論 2 354
  • 正文 我出身青樓团赁,卻偏偏與公主長得像育拨,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子欢摄,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,802評論 2 345

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