国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 編程 > JavaScript > 正文

vue.js動態數據綁定學習筆記

2019-11-19 16:33:11
字體:
來源:轉載
供稿:網友

對于vue.js的動態數據綁定,經過反復地看源碼和博客講解,總算能夠理解它的實現了,心累~ 分享一下學習成果,同時也算是做個記錄。完整代碼GitHub地址:https://github.com/hanrenguang/Dynamic-data-binding。也可以到倉庫的 README 閱讀本文,容我厚臉皮地求 star,求 follow。

整體思路

不知道有沒有同學和我一樣,看著vue的源碼卻不知從何開始,真叫人頭大。硬生生地看了observer, watcher, compile這幾部分的源碼,只覺得一臉懵逼。最終,從這里得到啟發,作者寫得很好,值得一讀。

關于動態數據綁定呢,需要搞定的是 Dep , Observer , Watcher , Compile 這幾個類,他們之間有著各種聯系,想要搞懂源碼,就得先了解他們之間的聯系。下面來理一理:

  • Observer 所做的就是劫持監聽所有屬性,當有變動時通知 Dep
  • Watcher 向 Dep 添加訂閱,同時,屬性有變化時,Observer 通知 Dep,Dep 則通知 Watcher
  • Watcher 得到通知后,調用回調函數更新視圖
  • Compile 則是解析所綁定元素的 DOM 結構,對所有需要綁定的屬性添加 Watcher 訂閱

由此可以看出,當屬性發生變化時,是由Observer -> Dep -> Watcher -> update view,Compile 在最開始解析 DOM 并添加 Watcher 訂閱后就功成身退了。

從程序執行的順序來看的話,即 new Vue({}) 之后,應該是這樣的:先通過 Observer 劫持所有屬性,然后 Compile 解析 DOM 結構,并添加 Watcher 訂閱,再之后就是屬性變化 -> Observer -> Dep -> Watcher -> update view,接下來就說說具體的實現。

從new一個實例開始談起

網上的很多源碼解讀都是從 Observer 開始的,而我會從 new 一個MVVM實例開始,按照程序執行順序去解釋或許更容易理解。先來看一個簡單的例子:

<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>test</title></head><body> <div class="test">  <p>{{user.name}}</p>  <p>{{user.age}}</p> </div> <script type="text/javascript" src="hue.js"></script> <script type="text/javascript">  let vm = new Hue({   el: '.test',   data: {    user: {     name: 'Jack',     age: '18'    }   }  }); </script></body></html>

接下來都將以其為例來分析。下面來看一個簡略的 MVVM 的實現,在此將其命名為 hue。為了方便起見,為 data 屬性設置了一個代理,通過 vm._data 來訪問 data 的屬性顯得麻煩且冗余,通過代理,可以很好地解決這個問題,在注釋中也有說明。添加完屬性代理后,調用了一個 observe 函數,這一步做的就是 Observer 的屬性劫持了,這一步具體怎么實現,暫時先不展開。先記住他為 data 的屬性添加了 getter 和 setter。

function Hue(options) { this.$options = options || {}; let data = this._data = this.$options.data,  self = this; Object.keys(data).forEach(function(key) {  self._proxyData(key); }); observe(data); self.$compile = new Compile(self, options.el || document.body);}// 為 data 做了一個代理,// 訪問 vm.xxx 會觸發 vm._data[xxx] 的getter,取得 vm._data[xxx] 的值,// 為 vm.xxx 賦值則會觸發 vm._data[xxx] 的setterHue.prototype._proxyData = function(key) { let self = this; Object.defineProperty(self, key, {  configurable: false,  enumerable: true,  get: function proxyGetter() {   return self._data[key];  },  set: function proxySetter(newVal) {   self._data[key] = newVal;  } });};

再往下看,最后一步 new 了一個 Compile,下面我們就來講講 Compile。

Compile

new Compile(self, options.el || document.body) 這一行代碼中,第一個參數是當前 Hue 實例,第二個參數是綁定的元素,在上面的示例中為class為 .test 的div。

關于 Compile,這里只實現最簡單的 textContent 的綁定。而 Compile 的代碼沒什么難點,很輕易就能讀懂,所做的就是解析 DOM,并添加 Watcher 訂閱。關于 DOM 的解析,先將根節點 el 轉換成文檔碎片 fragment 進行解析編譯操作,解析完成后,再將 fragment 添加回原來的真實 DOM 節點中。來看看這部分的代碼:

function Compile(vm, el) { this.$vm = vm; this.$el = this.isElementNode(el)  ? el  : document.querySelector(el); if (this.$el) {  this.$fragment = this.node2Fragment(this.$el);  this.init();  this.$el.appendChild(this.$fragment); }}Compile.prototype.node2Fragment = function(el) { let fragment = document.createDocumentFragment(),  child; // 也許有同學不太理解這一步,不妨動手寫個小例子觀察一下他的行為 while (child = el.firstChild) {  fragment.appendChild(child); } return fragment;};Compile.prototype.init = function() { // 解析 fragment this.compileElement(this.$fragment);};

以上面示例為例,此時若打印出 fragment,可觀察到其包含兩個p元素:

<p>{{user.name}}</p><p>{{user.age}}</p>

下一步就是解析 fragment,直接看代碼及注釋吧:

Compile.prototype.compileElement = function(el) { let childNodes = Array.from(el.childNodes),  self = this; childNodes.forEach(function(node) {  let text = node.textContent,   reg = //{/{(.*)/}/}/;  // 若為 textNode 元素,且匹配 reg 正則  // 在上例中會匹配 '{{user.name}}' 及 '{{user.age}}'  if (self.isTextNode(node) && reg.test(text)) {   // 解析 textContent,RegExp.$1 為匹配到的內容,在上例中為 'user.name' 及 'user.age'   self.compileText(node, RegExp.$1);  }  // 遞歸  if (node.childNodes && node.childNodes.length) {   self.compileElement(node);  } });};Compile.prototype.compileText = function(node, exp) { // this.$vm 即為 Hue 實例,exp 為正則匹配到的內容,即 'user.name' 或 'user.age' compileUtil.text(node, this.$vm, exp);};let compileUtil = { text: function(node, vm, exp) {  this.bind(node, vm, exp, 'text'); }, bind: function(node, vm, exp, dir) {  // 獲取更新視圖的回調函數  let updaterFn = updater[dir + 'Updater'];  // 先調用一次 updaterFn,更新視圖  updaterFn && updaterFn(node, this._getVMVal(vm, exp));  // 添加 Watcher 訂閱  new Watcher(vm, exp, function(value, oldValue) {   updaterFn && updaterFn(node, value, oldValue);  }); }, // 根據 exp,獲得其值,在上例中即 'vm.user.name' 或 'vm.user.age' _getVMVal: function(vm, exp) {  let val = vm;  exp = exp.trim().split('.');  exp.forEach(function(k) {   val = val[k];  });  return val; }};let updater = { // Watcher 訂閱的回調函數 // 在此即更新 node.textContent,即 update view textUpdater: function(node, value) {  node.textContent = typeof value === 'undefined'   ? ''   : value; }};

正如代碼中所看到的,Compile 在解析到 {{xxx}} 后便添加了 xxx 屬性的訂閱,即 new Watcher(vm, exp, callback)。理解了這一步后,接下來就需要了解怎么實現相關屬性的訂閱了。先從 Observer 開始談起。

Observer

從最簡單的情況來考慮,即不考慮數組元素的變化。暫時先不考慮 Dep 與 Observer 的聯系。先看看 Observer 構造函數:

function Observer(data) { this.data = data; this.walk(data);}Observer.prototype.walk = function(data) { const keys = Object.keys(data); // 遍歷 data 的所有屬性 for (let i = 0; i < keys.length; i++) {  // 調用 defineReactive 添加 getter 和 setter  defineReactive(data, keys[i], data[keys[i]]); }};

接下來通過 Object.defineProperty 方法給所有屬性添加 getter 和 setter,就達到了我們的目的。屬性有可能也是對象,因此需要對屬性值進行遞歸調用。

function defineReactive(obj, key, val) { // 對屬性值遞歸,對應屬性值為對象的情況 let childObj = observe(val); Object.defineProperty(obj, key, {  enumerable: true,  configurable: true,  get: function() {   // 直接返回屬性值   return val;  },  set: function(newVal) {   if (newVal === val) {    return;   }   // 值發生變化時修改閉包中的 val,   // 保證在觸發 getter 時返回正確的值   val = newVal;   // 對新賦的值進行遞歸,防止賦的值為對象的情況   childObj = observe(newVal);  } });}

最后補充上 observe 函數,也即 Hue 構造函數中調用的 observe 函數:

function observe(val) { // 若 val 是對象且非數組,則 new 一個 Observer 實例,val 作為參數 // 簡單點說:是對象就繼續。 if (!Array.isArray(val) && typeof val === "object") {  return new Observer(val); }}

這樣一來就對 data 的所有子孫屬性(不知有沒有這種說法。。)都進行了“劫持”。顯然到目前為止,這并沒什么用,或者說如果只做到這里,那么和什么都不做沒差別。于是 Dep 上場了。我認為理解 Dep 與 Observer 和 Watcher 之間的聯系是最重要的,先來談談 Dep 在 Observer 里做了什么。

Observer & Dep

在每一次 defineReactive 函數被調用之后,都會在閉包中新建一個 Dep 實例,即 let dep = new Dep()。Dep 提供了一些方法,先來說說 notify 這個方法,它做了什么事?就是在屬性值發生變化的時候通知 Dep,那么我們的代碼可以增加如下:

function defineReactive(obj, key, val) { let childObj = observe(val); const dep = new Dep(); Object.defineProperty(obj, key, {  enumerable: true,  configurable: true,  get: function() {   return val;  },  set: function(newVal) {   if (newVal === val) {    return;   }   val = newVal;   childObj = observe(newVal);   // 發生變動   dep.notify();  } });}

如果僅考慮 Observer 與 Dep 的聯系,即有變動時通知 Dep,那么這里就算完了,然而在 vue.js 的源碼中,我們還可以看到一段增加在 getter 中的代碼:

// ...get: function() { if (Dep.target) {  dep.depend(); } return val;}// ...

這個 depend 方法呢,它又做了啥?答案是為閉包中的 Dep 實例添加了一個 Watcher 的訂閱,而 Dep.target 又是啥?他其實是一個 Watcher 實例,???一臉懵逼,先記住就好,先看一部份的 Dep 源碼:

// 標識符,在 Watcher 中有用到,先不用管let uid = 0;function Dep() { this.id = uid++; this.subs = [];}Dep.prototype.depend = function() { // 這一步相當于做了這么一件事:this.subs.push(Dep.target) // 即添加了 Watcher 訂閱,addDep 是 Watcher 的方法 Dep.target.addDep(this);};// 通知更新Dep.prototype.notify = function() { // this.subs 的每一項都為一個 Watcher 實例 this.subs.forEach(function(sub) {  // update 為 Watcher 的一個方法,更新視圖  // 沒錯,實際上這個方法最終會調用到 Compile 中的 updaterFn,  // 也即 new Watcher(vm, exp, callback) 中的 callback  sub.update(); });};// 在 Watcher 中調用Dep.prototype.addSub = function(sub) { this.subs.push(sub);};// 初始時引用為空Dep.target = null;

也許看到這還是一臉懵逼,沒關系,接著往下。大概有同學會疑惑,為什么要把添加 Watcher 訂閱放在 getter 中,接下來我們來說說這 Watcher 和 Dep 的故事。

Watcher & Dep

先讓我們回顧一下 Compile 做的事,解析 fragment,然后給相應屬性添加訂閱:new Watcher(vm, exp, cb)。new 了這個 Watcher 之后,Watcher 怎么辦呢,就有了下面這樣的對話:

Watcher:hey Dep,我需要訂閱 exp 屬性的變動。

Dep:這我可做不到,你得去找 exp 屬性中的 dep,他能做到這件事。

Watcher:可是他在閉包中啊,我無法和他聯系。

Dep:你拿到了整個 Hue 實例 vm,又知道屬性 exp,你可以觸發他的 getter 啊,你在 getter 里動些手腳不就行了。

Watcher:有道理,可是我得讓 dep 知道是我訂閱的啊,不然他通知不到我。

Dep:這個簡單,我幫你,你每次觸發 getter 前,把你的引用告訴 Dep.target 就行了。記得辦完事后給 Dep.target 置空。

于是就有了上面 getter 中的代碼:

// ...get: function() { // 是否是 Watcher 觸發的 if (Dep.target) {  // 是就添加進來  dep.depend(); } return val;}// ...

現在再回頭看看 Dep 部分的代碼,是不是好理解些了。如此一來, Watcher 需要做的事情就簡單明了了:

function Watcher(vm, exp, cb) { this.$vm = vm; this.cb = cb; this.exp = exp; this.depIds = new Set(); // 返回一個用于獲取相應屬性值的函數 this.getter = parseGetter(exp.trim()); // 調用 get 方法,觸發 getter this.value = this.get();}Watcher.prototype.get = function() { const vm = this.$vm; // 將 Dep.target 指向當前 Watcher 實例 Dep.target = this; // 觸發 getter let value = this.getter.call(vm, vm); // Dep.target 置空 Dep.target = null; return value;};Watcher.prototype.addDep = function(dep) { const id = dep.id; if (!this.depIds.has(id)) {  // 添加訂閱,相當于 dep.subs.push(this)  dep.addSub(this);  this.depIds.add(id); }};function parseGetter(exp) { if (/[^/w.$]/.test(exp)) {  return; } let exps = exp.split("."); return function(obj) {  for (let i = 0; i < exps.length; i++) {   if (!obj)    return;   obj = obj[exps[i]];  }  return obj; };}

最后還差一部分,即 Dep 通知變化后,Watcher 的處理,具體的函數調用流程是這樣的:dep.notify() -> sub.update(),直接上代碼:

Watcher.prototype.update = function() { this.run();};Watcher.prototype.run = function() { let value = this.get(); let oldVal = this.value; if (value !== oldVal) {  this.value = value;  // 調用回調函數更新視圖  this.cb.call(this.$vm, value, oldVal); }};

結語

到這就算寫完了,本人水平有限,若有不足之處歡迎指出,一起探討。

參考資料

https://github.com/DMQ/mvvm

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持武林網。

發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
主站蜘蛛池模板: 荣昌县| 郧西县| 广昌县| 革吉县| 元阳县| 抚顺县| 塘沽区| 江油市| 兴安盟| 应城市| 泽普县| 蒙自县| 大足县| 日照市| 寿宁县| 永康市| 曲沃县| 邻水| 凉城县| 周至县| 南充市| 柯坪县| 嘉善县| 汾阳市| 北宁市| 定襄县| 山西省| 韶关市| 德格县| 德州市| 巩义市| 蒙自县| 团风县| 嵊泗县| 波密县| 平湖市| 杨浦区| 德保县| 中宁县| 门源| 新营市|