<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

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

          共 7488字,需瀏覽 15分鐘

           ·

          2021-04-01 14:53

          作者:九旬
          來源:SegmentFault 思否社區(qū)



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

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

          https://jsrun.net/RMIKp/embedded/all/light

          MVVM 框架主要包含兩個(gè)方面,數(shù)據(jù)變化更新視圖,視圖變化更新數(shù)據(jù)。

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

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

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

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

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

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

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

          1. 實(shí)現(xiàn)一個(gè)監(jiān)聽器 Observer,用來劫持并監(jiān)聽所有屬性,如果發(fā)生變動(dòng),則通知訂閱者。
          2. 實(shí)現(xiàn)一個(gè)訂閱者 Watcher,當(dāng)接到屬性變化的通知時(shí),執(zhí)行對(duì)應(yīng)的函數(shù),然后更新視圖,使用 Dep 來收集這些 Watcher。
          3. 實(shí)現(xiàn)一個(gè)解析器 Compile,用于掃描和解析的節(jié)點(diǎn)的相關(guān)指令,并根據(jù)初始化模板以及初始化相應(yīng)的訂閱器。

          顯示一個(gè) Observer

          Observer 是一個(gè)數(shù)據(jù)監(jiān)聽器,核心方法是利用 Object.defineProperty() 通過遞歸的方式對(duì)所有屬性都添加 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ù)添加檢測(cè)
          function defineReactive(data, key, val) {
          observe(val); // 遞歸遍歷所有子屬性
          let dep = new Dep(); // 新建一個(gè)dep
          Object.defineProperty(data, key, {
          enumerable: true,
          configurable: true,
          get: function() {
          if (Dep.target) {
          // 判斷是否需要添加訂閱者,僅第一次需要添加,之后就不用了,詳細(xì)看Watcher函數(shù)
          dep.addSub(Dep.target); // 添加一個(gè)訂閱者
          }
          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)聽對(duì)象的所有屬性
          function observe(data) {
          if (!data || typeof data !== "object") {
          return; // 如果不是對(duì)象就return
          }
          Object.keys(data).forEach(function(key) {
          defineReactive(data, key, data[key]);
          });
          }
          // Dep 負(fù)責(zé)收集訂閱者,當(dāng)屬性發(fā)生變化時(shí),觸發(fā)更新函數(shù)。
          function Dep() {
          this.subs = {};
          }
          Dep.prototype = {
          addSub: function(sub) {
          this.subs.push(sub);
          },
          notify: function() {
          this.subs.forEach((sub) => sub.update());
          },
          };
          思路分析中,需要有一個(gè)可以容納訂閱者消息訂閱器 Dep,用于收集訂閱者,在屬性發(fā)生變化時(shí)執(zhí)行對(duì)應(yīng)的更新函數(shù)。
          從代碼上看,將訂閱器 Dep 添加在 getter 里,是為了讓 Watcher 初始化時(shí)觸發(fā),,因此,需要判斷是否需要訂閱者。
          在 setter 中,如果有數(shù)據(jù)發(fā)生變化,則通知所有的訂閱者,然后訂閱者就會(huì)更新對(duì)應(yīng)的函數(shù)。
          到此為止,一個(gè)比較完整的 Observer 就完成了,接下來開始設(shè)計(jì) Watcher.

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

          訂閱者 Watcher 需要在初始化的時(shí)候?qū)⒆约禾砑拥接嗛喥?Dep 中,我們已經(jīng)知道監(jiān)聽器 Observer 是在 get 時(shí)執(zhí)行的 Watcher 操作,所以只需要在 Watcher 初始化的時(shí)候觸發(fā)對(duì)應(yīng)的 get 函數(shù)去添加對(duì)應(yīng)的訂閱者操作即可。
          那給如何觸發(fā) get 呢?因?yàn)槲覀円呀?jīng)設(shè)置了 Object.defineProperty(),所以只需要獲取對(duì)應(yīng)的屬性值就可以觸發(fā)了。
          我們只需要在訂閱者 Watcher 初始化的時(shí)候,在 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;
          },
          };
          到此為止, 簡(jiǎn)單的額 Watcher 設(shè)計(jì)完畢,然后將 Observer 和 Watcher 關(guān)聯(lián)起來,就可以實(shí)現(xiàn)一個(gè)簡(jiǎn)單的的雙向綁定了。
          因?yàn)檫€沒有設(shè)計(jì)解析器 Compile,所以可以先將模板數(shù)據(jù)寫死。
          將代碼轉(zhuǎn)化為 ES6 構(gòu)造函數(shù)的寫法,預(yù)覽試試。
          https://jsrun.net/8SIKp/embedded/all/light
          這段代碼因?yàn)闆]有實(shí)現(xiàn)編譯器而是直接傳入了所綁定的變量,我們只在一個(gè)節(jié)點(diǎn)上設(shè)置一個(gè)數(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ù)綁定,但是整個(gè)過程都沒有解析 DOM 節(jié)店,而是固定替換的,所以接下來要實(shí)現(xiàn)一個(gè)解析器來做數(shù)據(jù)的解析和綁定工作。
          解析器 compile 的實(shí)現(xiàn)步驟:
          1. 解析模板指令,并替換模板數(shù)據(jù),初始化視圖。
          2. 將模板指定對(duì)應(yīng)的節(jié)點(diǎn)綁定對(duì)應(yīng)的更新函數(shù),初始化相應(yīng)的訂閱器。
          為了解析模板,首先需要解析 DOM 數(shù)據(jù),然后對(duì)含有 DOM 元素上的對(duì)應(yīng)指令進(jìn)行處理,因此整個(gè) DOM 操作較為頻繁,可以新建一個(gè) 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;
          }
          接下來需要遍歷各個(gè)節(jié)點(diǎn),對(duì)含有相關(guān)指令和模板語法的節(jié)點(diǎn)進(jìn)行特殊處理,先進(jìn)行最簡(jiǎ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ù),對(duì)所有的子節(jié)點(diǎn)進(jìn)行判斷,如果節(jié)點(diǎn)是文本節(jié)點(diǎn)切匹配{{}}這種形式的指令,則進(jìn)行編譯處理,初始化對(duì)應(yīng)的參數(shù)。
          然后需要對(duì)當(dāng)前參數(shù)生成一個(gè)對(duì)應(yīng)的更新函數(shù)訂閱器,在數(shù)據(jù)發(fā)生變化時(shí)更新對(duì)應(yīng)的 DOM。
          這樣就完成了解析、初始化、編譯三個(gè)過程了。
          接下來改造一個(gè) myVue 就可以使用模板變量進(jìn)行雙向數(shù)據(jù)綁定了。
          https://jsrun.net/K4IKp/embedded/all/light
          添加解析事件
          添加完 compile 之后,一個(gè)數(shù)據(jù)雙向綁定就基本完成了,接下來就是在 Compile 中添加更多指令的解析編譯,比如 v-model、v-on、v-bind 等。
          添加一個(gè) 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)屬性,然后判斷屬性是否是指令屬性,如果是在做對(duì)應(yīng)的處理(事件就去監(jiān)聽事件、數(shù)據(jù)就去監(jiān)聽數(shù)據(jù)..)

          完整版 myVue

          在 MyVue 中添加 mounted 方法,在所有操作都做完時(shí)執(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;
          },
          });
          }
          }
          然后就可以測(cè)試使用了。
          https://jsrun.net/Y4IKp/embedded/all/light
          總結(jié)一下流程,回頭在哪看一遍這個(gè)圖,是不是清楚很多了。
          內(nèi)容參考:
          https://ustbhuangyi.github.io/vue-analysis/v2/reactive/
          https://juejin.cn/book/6844733705089449991/section/6844733705227862023
          https://www.cnblogs.com/canfoo/p/6891868.html
          https://github.com/AnsonZnl/KKB/blob/master/Vue/04Vue%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%9001/REDME.md
          https://github.com/AnsonZnl/vue-study/tree/web26/kvue



          點(diǎn)擊左下角閱讀原文,到 SegmentFault 思否社區(qū) 和文章作者展開更多互動(dòng)和交流,掃描下方”二維碼“或在“公眾號(hào)后臺(tái)回復(fù)“ 入群 ”即可加入我們的技術(shù)交流群,收獲更多的技術(shù)文章~

          - END -


          瀏覽 65
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  91深夜网站 | 久久伊人AV | 日日撸夜夜撸 | 97综合| 国产免费资源 |