溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

怎么理解JavaScript數(shù)據(jù)雙向綁定

發(fā)布時間:2021-11-15 15:24:27 來源:億速云 閱讀:158 作者:iii 欄目:web開發(fā)

本篇內(nèi)容介紹了“怎么理解JavaScript數(shù)據(jù)雙向綁定”的有關(guān)知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領(lǐng)大家學(xué)習(xí)一下如何處理這些情況吧!希望大家仔細(xì)閱讀,能夠?qū)W有所成!

模板引擎實現(xiàn)原理

對于頁面渲染,一般分為服務(wù)器端渲染和瀏覽器端渲染。一般來說服務(wù)器端吐html頁面的方式渲染速度更快、更利于SEO,但是瀏覽器端渲染更利于提高開發(fā)效率和減少維護成本,是一種相關(guān)舒服的前后端協(xié)作模式,后端提供接口,前端做視圖和交互邏輯。前端通過Ajax請求數(shù)據(jù)然后拼接html字符串或者使用js模板引擎、數(shù)據(jù)驅(qū)動的框架如Vue進行頁面渲染。

在ES6和Vue這類框架出現(xiàn)以前,前端綁定數(shù)據(jù)的方式是動態(tài)拼接html字符串和js模板引擎。模板引擎起到數(shù)據(jù)和視圖分離的作用,模板對應(yīng)視圖,關(guān)注如何展示數(shù)據(jù),在模板外頭準(zhǔn)備的數(shù)據(jù),  關(guān)注那些數(shù)據(jù)可以被展示。模板引擎的工作原理可以簡單地分成兩個步驟:模板解析 / 編譯(Parse /  Compile)和數(shù)據(jù)渲染(Render)兩部分組成,當(dāng)今主流的前端模板有三種方式:

  • String-based templating (基于字符串的parse和compile過程)

  • Dom-based templating (基于Dom的link或compile過程)

  • Living templating (基于字符串的parse 和 基于dom的compile過程)

String-based templating

怎么理解JavaScript數(shù)據(jù)雙向綁定

基于字符串的模板引擎,本質(zhì)上依然是字符串拼接的形式,只是一般的庫做了封裝和優(yōu)化,提供了更多方便的語法簡化了我們的工作?;驹砣缦拢?/p>

典型的庫:

  • art-template

  • mustache.js

  • doT

之前的一篇文章中我介紹了js模板引擎的實現(xiàn)思路,感興趣的朋友可以看看這里:JavaScript進階學(xué)習(xí)(一)——  基于正則表達(dá)式的簡單js模板引擎實現(xiàn)。這篇文章中我們利用正則表達(dá)式實現(xiàn)了一個簡單的js模板引擎,利用正則匹配查找出模板中{{}}之間的內(nèi)容,然后替換為模型中的數(shù)據(jù),從而實現(xiàn)視圖的渲染。

var template = function(tpl, data) {    var re = /{{(.+?)}}/g,      cursor = 0,      reExp = /(^( )?(var|if|for|else|switch|case|break|{|}|;))(.*)?/g,          code = 'var r=[];\n';       // 解析html    function parsehtml(line) {      // 單雙引號轉(zhuǎn)義,換行符替換為空格,去掉前后的空格      line = line.replace(/('|")/g, '\\$1').replace(/\n/g, ' ').replace(/(^\s+)|(\s+$)/g,"");      code +='r.push("' + line + '");\n';    }        // 解析js代碼            function parsejs(line) {        // 去掉前后的空格      line = line.replace(/(^\s+)|(\s+$)/g,"");      code += line.match(reExp)? line + '\n' : 'r.push(' + 'this.' + line + ');\n';    }              // 編譯模板    while((match = re.exec(tpl))!== null) {      // 開始標(biāo)簽  {{ 前的內(nèi)容和結(jié)束標(biāo)簽 }} 后的內(nèi)容      parsehtml(tpl.slice(cursor, match.index));      // 開始標(biāo)簽  {{ 和 結(jié)束標(biāo)簽 }} 之間的內(nèi)容      parsejs(match[1]);      // 每一次匹配完成移動指針      cursor = match.index + match[0].length;    }    // ***一次匹配完的內(nèi)容    parsehtml(tpl.substr(cursor, tpl.length - cursor));    code += 'return r.join("");';    return new Function(code.replace(/[\r\t\n]/g, '')).apply(data);  }

源代碼:http://jsfiddle.net/zhaomenghuan/bw468orv/embedded/

現(xiàn)在ES6支持了模板字符串,我們可以用比較簡單的代碼就可以實現(xiàn)類似的功能:

const template = data => `    <p>name: ${data.name}</p>    <p>age: ${data.profile.age}</p>    <ul>      ${data.skills.map(skill => `        <li>${skill}</li>      `).join('')}    </ul>`     const data = {    name: 'zhaomenghuan',    profile: { age: 24 },    skills: ['html5', 'javascript', 'android']  }     document.body.innerHTML = template(data)

Dom-based templating

怎么理解JavaScript數(shù)據(jù)雙向綁定

Dom-based templating  則是從DOM的角度去實現(xiàn)數(shù)據(jù)的渲染,我們通過遍歷DOM樹,提取屬性與DOM內(nèi)容,然后將數(shù)據(jù)寫入到DOM樹中,從而實現(xiàn)頁面渲染。一個簡單的例子如下:

function MVVM(opt) {    this.dom = document.querySelector(opt.el);    this.data = opt.data || {};    this.renderDom(this.dom);  }     MVVM.prototype = {    init: {      sTag: '{{',      eTag: '}}'    },    render: function (node) {      var self = this;      var sTag = self.init.sTag;      var eTag = self.init.eTag;         var matchs = node.textContent.split(sTag);      if (matchs.length){        var ret = '';        for (var i = 0; i < matchs.length; i++) {          var match = matchs[i].split(eTag);          if (match.length == 1) {              ret += matchs[i];          } else {              ret = self.data[match[0]];          }          node.textContent = ret;        }      }    },    renderDom: function(dom) {      var self = this;         var attrs = dom.attributes;      var nodes = dom.childNodes;         Array.prototype.forEach.call(attrs, function(item) {        self.render(item);      });         Array.prototype.forEach.call(nodes, function(item) {        if (item.nodeType === 1) {          return self.renderDom(item);        }        self.render(item);      });    }  }     var app = new MVVM({    el: '#app',    data: {      name: 'zhaomenghuan',      age: '24',      color: 'red'    }  });

源代碼:http://jsfiddle.net/zhaomenghuan/6e3yg6Lq/embedded/

頁面渲染的函數(shù) renderDom  是直接遍歷DOM樹,而不是遍歷html字符串。遍歷DOM樹節(jié)點屬性(attributes)和子節(jié)點(childNodes),然后調(diào)用渲染函數(shù)render。當(dāng)DOM樹子節(jié)點的類型是元素時,遞歸調(diào)用遍歷DOM樹的方法。根據(jù)DOM樹節(jié)點類型一直遍歷子節(jié)點,直到文本節(jié)點。

render的函數(shù)作用是提取{{}}中的關(guān)鍵詞,然后使用數(shù)據(jù)模型中的數(shù)據(jù)進行替換。我們通過textContent獲取Node節(jié)點的nodeValue,然后使用字符串的split方法對nodeValue進行分割,提取{{}}中的關(guān)鍵詞然后替換為數(shù)據(jù)模型中的值。

DOM 的相關(guān)基礎(chǔ)

注:元素類型對應(yīng)NodeType

元素類型NodeType
元素1
屬性2
文本3
注釋8
文檔9

childNodes 屬性返回包含被選節(jié)點的子節(jié)點的  NodeList。childNodes包含的不僅僅只有html節(jié)點,所有屬性,文本、注釋等節(jié)點都包含在childNodes里面。children只返回元素如input,  span, script, div等,不會返回TextNode,注釋。

數(shù)據(jù)雙向綁定實現(xiàn)原理

js模板引擎可以認(rèn)為是一個基于MVC的結(jié)構(gòu),我們通過建立模板作為視圖,然后通過引擎函數(shù)作為控制器實現(xiàn)數(shù)據(jù)和視圖的綁定,從而實現(xiàn)實現(xiàn)數(shù)據(jù)在頁面渲染,但是當(dāng)數(shù)據(jù)模型發(fā)生變化時,視圖不能自動更新;當(dāng)視圖數(shù)據(jù)發(fā)生變化時,模型數(shù)據(jù)不能實現(xiàn)更新,這個時候雙向數(shù)據(jù)綁定應(yīng)運而生。檢測視圖數(shù)據(jù)更新實現(xiàn)數(shù)據(jù)綁定的方法有很多種,目前主要分為三個流派,Angular使用的是臟檢查,只在特定的事件下才會觸發(fā)視圖刷新,Vue使用的是Getter/Setter機制,而React則是通過  Virtual DOM 算法檢查DOM的變動的刷新機制。

本文限于篇幅和內(nèi)容在此只探討一下 Vue.js 數(shù)據(jù)綁定的實現(xiàn),對于 angular 和 react 后續(xù)再做說明,讀者也可以自行閱讀源碼。Vue  監(jiān)聽數(shù)據(jù)變化的機制是把一個普通 JavaScript 對象傳給 Vue 實例的 data 選項,Vue 將遍歷此對象所有的屬性,并使用  Object.defineProperty 把這些屬性全部轉(zhuǎn)為 getter/setter。Vue 2.x 對 Virtual DOM  進行了支持,這部分內(nèi)容后續(xù)我們再做探討。

引子

為了更好的理解Vue中視圖和數(shù)據(jù)更新的機制,我們先看一個簡單的例子:

var o = {    a: 0  }  Object.defineProperty(o, "b", {    get: function () {      return this.a + 1;    },    set: function (value) {      this.a = value / 2;    }  });  console.log(o.a); // "0"  console.log(o.b); // "1"     // 更新o.a  o.a = 5;  console.log(o.a); // "5"  console.log(o.b); // "6"     // 更新o.b  o.b = 10;  console.log(o.a); // "5"  console.log(o.b); // "6"

這里我們可以看出對象o的b屬性的值依賴于a屬性的值,同時b屬性值的變化又可以改變a屬性的值,這個過程相關(guān)的屬性值的變化都會影響其他相關(guān)的值進行更新。反過來我們看看如果不使用Object.defineProperty()方法,上述的問題通過直接給對象屬性賦值的方法實現(xiàn),代碼如下

var o = {    a: 0  }      o.b = o.a + 1;  console.log(o.a); // "0"  console.log(o.b); // "1"     // 更新o.a  o.a = 5;  o.b = o.a + 1;  console.log(o.a); // "5"  console.log(o.b); // "6"     // 更新o.b  o.b = 10;  o.a = o.b / 2;  o.b = o.a + 1;  console.log(o.a); // "5"  console.log(o.b); // "6"

很顯然使用Object.defineProperty()方法可以更方便的監(jiān)聽一個對象的變化。當(dāng)我們的視圖和數(shù)據(jù)任何一方發(fā)生變化的時候,我們希望能夠通知對方也更新,這就是所謂的數(shù)據(jù)雙向綁定。既然明白這個道理我們就可以看看Vue源碼中相關(guān)的處理細(xì)節(jié)。

Object.defineProperty()

Object.defineProperty()方法可以直接在一個對象上定義一個新屬性,或者修改一個已經(jīng)存在的屬性, 并返回這個對象。

語法:Object.defineProperty(obj, prop, descriptor)

參數(shù):

  • obj:需要定義屬性的對象。

  • prop:需被定義或修改的屬性名。

  • descriptor:需被定義或修改的屬性的描述符。

返回值:返回傳入函數(shù)的對象,即***個參數(shù)obj

該方法重點是描述,對象里目前存在的屬性描述符有兩種主要形式:數(shù)據(jù)描述符和存取描述符。數(shù)據(jù)描述符是一個擁有可寫或不可寫值的屬性。存取描述符是由一對  getter-setter 函數(shù)功能來描述的屬性。描述符必須是兩種形式之一;不能同時是兩者。

數(shù)據(jù)描述符和存取描述符均具有以下可選鍵值:

  • configurable:當(dāng)且僅當(dāng)該屬性的 configurable 為 true 時,該屬性才能夠被改變,也能夠被刪除。默認(rèn)為 false。

  • enumerable:當(dāng)且僅當(dāng)該屬性的 enumerable 為 true 時,該屬性才能夠出現(xiàn)在對象的枚舉屬性中。默認(rèn)為 false。

數(shù)據(jù)描述符同時具有以下可選鍵值:

  • value:該屬性對應(yīng)的值??梢允侨魏斡行У?JavaScript 值(數(shù)值,對象,函數(shù)等)。默認(rèn)為 undefined。

  • writable:當(dāng)且僅當(dāng)僅當(dāng)該屬性的writable為 true 時,該屬性才能被賦值運算符改變。默認(rèn)為 false。

存取描述符同時具有以下可選鍵值:

  • get:一個給屬性提供 getter 的方法,如果沒有 getter 則為  undefined。該方法返回值被用作屬性值。默認(rèn)為undefined。

  • set:一個給屬性提供 setter 的方法,如果沒有 setter 則為  undefined。該方法將接受唯一參數(shù),并將該參數(shù)的新值分配給該屬性。默認(rèn)為undefined。

我們可以通過Object.defineProperty()方法精確添加或修改對象的屬性。比如,直接賦值創(chuàng)建的屬性默認(rèn)情況是可以枚舉的,但是我們可以通過Object.defineProperty()方法設(shè)置enumerable屬性為false為不可枚舉。

var obj = {    a: 0,    b: 1  }  for (var prop in obj) {    console.log(`obj.${prop} = ${obj[prop]}`);  }

結(jié)果:

"obj.a = 0"  "obj.b = 1"

我們通過Object.defineProperty()修改如下:

var obj = {    a: 0,    b: 1  }  Object.defineProperty(obj, 'b', {    enumerable: false  })  for (var prop in obj) {    console.log(`obj.${prop} = ${obj[prop]}`);  }

結(jié)果:

"obj.a = 0"

這里需要說明的是我們使用Object.defineProperty()默認(rèn)情況下是enumerable屬性為false,例如:

var obj = {    a: 0  }  Object.defineProperty(obj, 'b', {    value: 1  })  for (var prop in obj) {    console.log(`obj.${prop} = ${obj[prop]}`);  }

結(jié)果:

"obj.a = 0"

其他描述屬性使用方法類似,不做贅述。Vue源碼core/util/lang.jsS中定義了這樣一個方法:

/**  * Define a property.  */  export function def (obj: Object, key: string, val: any, enumerable?: boolean) {    Object.defineProperty(obj, key, {      value: val,      enumerable: !!enumerable,      writable: true,      configurable: true    })  }

Object.getOwnPropertyDescriptor()

Object.getOwnPropertyDescriptor()  返回指定對象上一個自有屬性對應(yīng)的屬性描述符。(自有屬性指的是直接賦予該對象的屬性,不需要從原型鏈上進行查找的屬性)

語法:Object.getOwnPropertyDescriptor(obj, prop)

參數(shù):

  • obj:在該對象上查看屬性

  • prop:一個屬性名稱,該屬性的屬性描述符將被返回

返回值:如果指定的屬性存在于對象上,則返回其屬性描述符(property descriptor),否則返回  undefined。可以訪問“屬性描述符”內(nèi)容,例如前面的例子:

var o = {    a: 0  }                Object.defineProperty(o, "b", {    get: function () {      return this.a + 1;    },    set: function (value) {      this.a = value / 2;    }  });                var des = Object.getOwnPropertyDescriptor(o,'b');  console.log(des);  console.log(des.get);

Vue源碼分析

本次我們主要分析一下Vue 數(shù)據(jù)綁定的源碼,這里我直接將 Vue.js 1.0.28 版本的代碼稍作刪減拿過來進行,2.x 的代碼基于 flow  靜態(tài)類型檢查器書寫的,代碼除了編碼風(fēng)格在整體結(jié)構(gòu)上基本沒有太大改動,所以依然基于 1.x 進行分析,對于存在差異的部分加以說明。

怎么理解JavaScript數(shù)據(jù)雙向綁定

監(jiān)聽對象變動

// 觀察者構(gòu)造函數(shù)  function Observer (value) {    this.value = value    this.walk(value)  }     // 遞歸調(diào)用,為對象綁定getter/setter  Observer.prototype.walk = function (obj) {    var keys = Object.keys(obj)    for (var i = 0, l = keys.length; i < l; i++) {      this.convert(keys[i], obj[keys[i]])    }  }     // 將屬性轉(zhuǎn)換為getter/setter  Observer.prototype.convert = function (key, val) {    defineReactive(this.value, key, val)  }     // 創(chuàng)建數(shù)據(jù)觀察者實例  function observe (value) {    // 當(dāng)值不存在或者不是對象類型時,不需要繼續(xù)深入監(jiān)聽    if (!value || typeof value !== 'object') {      return    }    return new Observer(value)  }     // 定義對象屬性的getter/setter  function defineReactive (obj, key, val) {    var property = Object.getOwnPropertyDescriptor(obj, key)    if (property && property.configurable === false) {      return    }       // 保存對象屬性預(yù)先定義的getter/setter    var getter = property && property.get    var setter = property && property.set       var childOb = observe(val)    Object.defineProperty(obj, key, {      enumerable: true,      configurable: true,      get: function reactiveGetter () {        var value = getter ? getter.call(obj) : val        console.log("訪問:"+key)        return value      },      set: function reactiveSetter (newVal) {        var value = getter ? getter.call(obj) : val        if (newVal === value) {          return        }        if (setter) {          setter.call(obj, newVal)        } else {          val = newVal        }        // 對新值進行監(jiān)聽        childOb = observe(newVal)        console.log('更新:' + key + ' = ' + newVal)      }    })  }

定義一個對象作為數(shù)據(jù)模型,并監(jiān)聽這個對象。

let data = {    user: {      name: 'zhaomenghuan',      age: '24'    },    address: {      city: 'beijing'    }  }  observe(data)     console.log(data.user.name)  // 訪問:user  // 訪問:name     data.user.name = 'ZHAO MENGHUAN'  // 訪問:user  // 更新:name = ZHAO MENGHUAN

效果如下:

怎么理解JavaScript數(shù)據(jù)雙向綁定

監(jiān)聽數(shù)組變動

上面我們通過Object.defineProperty把對象的屬性全部轉(zhuǎn)為 getter/setter  從而實現(xiàn)監(jiān)聽對象的變動,但是對于數(shù)組對象無法通過Object.defineProperty實現(xiàn)監(jiān)聽。Vue  包含一組觀察數(shù)組的變異方法,所以它們也將會觸發(fā)視圖更新。

const arrayProto = Array.prototype  const arrayMethods = Object.create(arrayProto)     function def(obj, key, val, enumerable) {    Object.defineProperty(obj, key, {      value: val,      enumerable: !!enumerable,      writable: true,      configurable: true    })  }     // 數(shù)組的變異方法  ;[    'push',    'pop',    'shift',    'unshift',    'splice',    'sort',    'reverse'  ]  .forEach(function (method) {    // 緩存數(shù)組原始方法    var original = arrayProto[method]    def(arrayMethods, method, function mutator () {      var i = arguments.length      var args = new Array(i)      while (i--) {        args[i] = arguments[i]      }      console.log('數(shù)組變動')      return original.apply(this, args)    })  })

Vue.js 1.x 在Array.prototype原型對象上添加了$set 和 $remove方法,在2.X后移除了,使用全局 API Vue.set  和 Vue.delete代替了,后續(xù)我們再分析。

定義一個數(shù)組作為數(shù)據(jù)模型,并對這個數(shù)組調(diào)用變異的七個方法實現(xiàn)監(jiān)聽。

let skills = ['JavaScript', 'Node.js', 'html5']  // 原型指針指向具有變異方法的數(shù)組對象  skills.__proto__ = arrayMethods     skills.push('java')  // 數(shù)組變動  skills.pop()  // 數(shù)組變動

效果如下:

怎么理解JavaScript數(shù)據(jù)雙向綁定

我們將需要監(jiān)聽的數(shù)組的原型指針指向我們定義的數(shù)組對象,這樣我們的數(shù)組在調(diào)用上面七個數(shù)組的變異方法時,能夠監(jiān)聽到變動從而實現(xiàn)對數(shù)組進行跟蹤。

對于__proto__屬性,在ES2015中正式被加入到規(guī)范中,標(biāo)準(zhǔn)明確規(guī)定,只有瀏覽器必須部署這個屬性,其他運行環(huán)境不一定需要部署,所以 Vue  是先進行了判斷,當(dāng)__proto__屬性存在時將原型指針__proto__指向具有變異方法的數(shù)組對象,不存在時直接將具有變異方法掛在需要追蹤的對象上。

我們可以在上面Observer觀察者構(gòu)造函數(shù)中添加對數(shù)組的監(jiān)聽,源碼如下:

const hasProto = '__proto__' in {}  const arrayKeys = Object.getOwnPropertyNames(arrayMethods)     // 觀察者構(gòu)造函數(shù)  function Observer (value) {    this.value = value    if (Array.isArray(value)) {      var augment = hasProto        ? protoAugment        : copyAugment      augment(value, arrayMethods, arrayKeys)      this.observeArray(value)    } else {      this.walk(value)    }  }     // 觀察數(shù)組的每一項  Observer.prototype.observeArray = function (items) {    for (var i = 0, l = items.length; i < l; i++) {      observe(items[i])    }  }     // 將目標(biāo)對象/數(shù)組的原型指針__proto__指向src  function protoAugment (target, src) {    target.__proto__ = src  }     // 將具有變異方法掛在需要追蹤的對象上  function copyAugment (target, src, keys) {    for (var i = 0, l = keys.length; i < l; i++) {      var key = keys[i]      def(target, key, src[key])    }  }

原型鏈

對于不了解原型鏈的朋友可以看一下我這里畫的一個基本關(guān)系圖:

怎么理解JavaScript數(shù)據(jù)雙向綁定

  • 原型對象是構(gòu)造函數(shù)的prototype屬性,是所有實例化對象共享屬性和方法的原型對象;

  • 實例化對象通過new構(gòu)造函數(shù)得到,都繼承了原型對象的屬性和方法;

  • 原型對象中有個隱式的constructor,指向了構(gòu)造函數(shù)本身。

Object.create

Object.create 使用指定的原型對象和其屬性創(chuàng)建了一個新的對象。

const arrayProto = Array.prototype  const arrayMethods = Object.create(arrayProto)

這一步是通過 Object.create  創(chuàng)建了一個原型對象為Array.prototype的空對象。然后通過Object.defineProperty方法對這個對象定義幾個變異的數(shù)組方法。有些新手可能會直接修改  Array.prototype 上的方法,這是很危險的行為,這樣在引入的時候會全局影響Array  對象的方法,而使用Object.create實質(zhì)上是完全了一份拷貝,新生成的arrayMethods對象的原型指針__proto__指向了Array.prototype,修改arrayMethods  對象不會影響Array.prototype。

基于這種原理,我們通常會使用Object.create 實現(xiàn)類式繼承。

// 實現(xiàn)繼承  var extend = function(Child, Parent) {      // 拷貝Parent原型對象      Child.prototype = Object.create(Parent.prototype);      // 將Child構(gòu)造函數(shù)賦值給Child的原型對象      Child.prototype.constructor = Child;  }     // 實例  var Parent = function () {      this.name = 'Parent';  }  Parent.prototype.getName = function () {      return this.name;  }  var Child = function () {      this.name = 'Child';  }  extend(Child, Parent);  var child = new Child();  console.log(child.getName())

發(fā)布-訂閱模式

在上面一部分我們通過Object.defineProperty把對象的屬性全部轉(zhuǎn)為 getter/setter 以及  數(shù)組變異方法實現(xiàn)了對數(shù)據(jù)模型變動的監(jiān)聽,在數(shù)據(jù)變動的時候,我們通過console.log打印出來提示了,但是對于框架而言,我們相關(guān)的邏輯如果直接寫在那些地方,自然是不夠優(yōu)雅和靈活的,這個時候就需要引入常用的設(shè)計模式去實現(xiàn),vue.js采用了發(fā)布-訂閱模式。發(fā)布-訂閱模式主要是為了達(dá)到一種“高內(nèi)聚、低耦合”的效果。

Vue的Watcher訂閱者作為Observer和Compile之間通信的橋梁,能夠訂閱并收到每個屬性變動的通知,執(zhí)行指令綁定的相應(yīng)回調(diào)函數(shù),從而更新視圖。

/**  * 觀察者對象  */  function Watcher(vm, expOrFn, cb) {      this.vm = vm      this.cb = cb      this.depIds = {}      if (typeof expOrFn === 'function') {          this.getter = expOrFn      } else {          this.getter = this.parseExpression(expOrFn)      }      this.value = this.get()  }     /**  * 收集依賴  */  Watcher.prototype.get = function () {      // 當(dāng)前訂閱者(Watcher)讀取被訂閱數(shù)據(jù)的***更新后的值時,通知訂閱者管理員收集當(dāng)前訂閱者      Dep.target = this      // 觸發(fā)getter,將自身添加到dep中      const value = this.getter.call(this.vm, this.vm)      // 依賴收集完成,置空,用于下一個Watcher使用      Dep.target = null      return value  }     Watcher.prototype.addDep = function (dep) {      if (!this.depIds.hasOwnProperty(dep.id)) {          dep.addSub(this)          this.depIds[dep.id] = dep      }  }     /**  * 依賴變動更新  *  * @param {Boolean} shallow  */  Watcher.prototype.update = function () {      this.run()  }     Watcher.prototype.run = function () {      var value = this.get()      if (value !== this.value) {          var oldValue = this.value          this.value = value          // 將newVal, oldVal掛載到MVVM實例上          this.cb.call(this.vm, value, oldValue)      }  }     Watcher.prototype.parseExpression = function (exp) {      if (/[^\w.$]/.test(exp)) {          return      }      var exps = exp.split('.')            return function(obj) {          for (var i = 0, len = exps.length; i < len; i++) {              if (!obj) return              obj = obj[exps[i]]          }          return obj      }  }

Dep  是一個數(shù)據(jù)結(jié)構(gòu),其本質(zhì)是維護了一個watcher隊列,負(fù)責(zé)添加watcher,更新watcher,移除watcher,通知watcher更新。

let uid = 0     function Dep() {      this.id = uid++      this.subs = []  }     Dep.target = null     /**  * 添加一個訂閱者  *  * @param {Directive} sub  */  Dep.prototype.addSub = function (sub) {      this.subs.push(sub)  }     /**  * 移除一個訂閱者  *  * @param {Directive} sub  */  Dep.prototype.removeSub = function (sub) {      let index = this.subs.indexOf(sub);      if (index !== -1) {          this.subs.splice(index, 1);      }  }     /**  * 將自身作為依賴添加到目標(biāo)watcher  */  Dep.prototype.depend = function () {      Dep.target.addDep(this)  }     /**  * 通知數(shù)據(jù)變更  */  Dep.prototype.notify = function () {      var subs = toArray(this.subs)      // stablize the subscriber list first      for (var i = 0, l = subs.length; i < l; i++) {          // 執(zhí)行訂閱者的update更新函數(shù)          subs[i].update()      }  }

模板編譯

compile主要做的事情是解析模板指令,將模板中的變量替換成數(shù)據(jù),然后初始化渲染頁面視圖,并將每個指令對應(yīng)的節(jié)點綁定更新函數(shù),添加監(jiān)聽數(shù)據(jù)的訂閱者,一旦數(shù)據(jù)有變動,收到通知,更新視圖。

怎么理解JavaScript數(shù)據(jù)雙向綁定

這種實現(xiàn)和我們講到的Dom-based  templating類似,只是更加完備,具有自定義指令的功能。在遍歷節(jié)點屬性和文本節(jié)點的時候,可以編譯具備{{}}表達(dá)式或v-xxx的屬性值的節(jié)點,并且通過添加  new Watcher()及綁定事件函數(shù),監(jiān)聽數(shù)據(jù)的變動從而對視圖實現(xiàn)雙向綁定。

MVVM實例

在數(shù)據(jù)綁定初始化的時候,我們需要通過new Observer()來監(jiān)聽數(shù)據(jù)模型變化,通過new  Compile()來解析編譯模板指令,并利用Watcher搭起Observer和Compile之間的通信橋梁。

/**  * @class 雙向綁定類 MVVM  * @param {[type]} options [description]  */  function MVVM(options) {      this.$options = options || {}      // 簡化了對data的處理      let data = this._data = this.$options.data      // 監(jiān)聽數(shù)據(jù)      observe(data)      new Compile(options.el || document.body, this)  }     MVVM.prototype.$watch = function (expOrFn, cb) {      new Watcher(this, expOrFn, cb)  }

為了能夠直接通過實例化對象操作數(shù)據(jù)模型,我們需要為MVVM實例添加一個數(shù)據(jù)模型代理的方法:

MVVM.prototype._proxy = function (key) {      Object.defineProperty(this, key, {          configurable: true,          enumerable: true,          get: () => this._data[key],          set: (val) => {              this._data[key] = val          }      })  }

至此我們可以通過一個小例子來說明本文的內(nèi)容:

<div id="app">      <h4>{{user.name}}</h4>      <input type="text" v-model="modelValue">      <p>{{modelValue}}</p>  </div>  <script>      let vm = new MVVM({          el: '#app',          data: {              modelValue: '',              user: {                  name: 'zhaomenghuan',                  age: '24'              },              address: {                  city: 'beijing'              },              skills: ['JavaScript', 'Node.js', 'html5']          }      })            vm.$watch('modelValue', val => console.log(`watch modelValue :${val}`))  </script>

“怎么理解JavaScript數(shù)據(jù)雙向綁定”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識可以關(guān)注億速云網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實用文章!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI