vue.js響應式原理解析與實現—實現v-model與{{}}指令

              小編:管理員 74閱讀 2022.08.19

              我們已經分析了vue.js是通過Object.defineProperty以及發布訂閱模式來進行數據劫持和監聽,并且實現了一個簡單的demo。今天,我們就基于上一節的代碼,來實現一個MVVM類,將其與html結合在一起,并且實現v-model以及{{}}語法。

              tips:本節新增代碼(去除注釋)在一百行左右。使用的Observer和Watcher都是延用上一節的代碼,沒有修改。

              接下來,讓我們一步步來,實現一個MVVM類。

              構造函數

              首先,一個MVVM的構造函數如下(和vue.js的構造函數一樣):

              class MVVM {
                constructor({ data, el }) {
                  this.data = data;
                  this.el = el;
                  this.init();
                  this.initDom();
                }
              }
              復制

              和vue.js一樣,有它的data屬性以及el元素。

              初始化操作

              vue.js可以通過this.xxx的方法來直接訪問this.data.xxx的屬性,這一點是怎么做到的呢?其實答案很簡單,它是通過Object.defineProperty來做手腳,當你訪問this.xxx的時候,它返回的其實是this.data.xxx。當你修改this.xxx值的時候,其實修改的是this.data.xxx的值。具體可以看如下代碼:

              class MVVM {
                constructor({ data, el }) {
                  this.data = data;
                  this.el = el;
                  this.init();
                  this.initDom();
                }
                // 初始化
                init() {
                  // 對this.data進行數據劫持
                  new Observer(this.data);
                  // 傳入的el可以是selector,也可以是元素,因此我們要在這里做一層處理,保證this.$el的值是一個元素節點
                  this.$el = this.isElementNode(this.el) ? this.el : document.querySelector(this.el);
                  // 將this.data的屬性都綁定到this上,這樣用戶就可以直接通過this.xxx來訪問this.data.xxx的值
                  for (let key in this.data) {
                    this.defineReactive(key);
                  }
                }
                  
                defineReactive(key) {
                  Object.defineProperty(this, key, {
                    get() {
                      return this.data[key];
                    },
                    set(newVal) {
                      this.data[key] = newVal;
                    }
                  })
                }
                // 是否是屬性節點
                isElementNode(node) {
                  return node.nodeType === 1;
                }
              }
              復制

              在完成初始化操作后,我們需要對this.$el的節點進行編譯。目前我們要實現的語法有v-model和{{}}語法,v-model這個屬性只可能會出現在元素節點的attributes里,而{{}}語法則是出現在文本節點里。

              fragment

              在對節點進行編譯之前,我們先考慮一個現實問題:如果我們在編譯過程中直接操作DOM節點的話,每一次修改DOM都會導致DOM的回流或重繪,而這一部分性能損耗是很沒有必要的。因此,我們可以利用fragment,將節點轉化為fragment,然后在fragment里編譯完成后,再將其放回到頁面上。

              class MVVM {
                constructor({ data, el }) {
                  this.data = data;
                  this.el = el;
                  this.init();
                  this.initDom();
                }
                
                initDom() {
                  const fragment = this.node2Fragment();
                  this.compile(fragment);
                  // 將fragment返回到頁面中
                  document.body.appendChild(fragment);
                }
                // 將節點轉為fragment,通過fragment來操作DOM,可以獲得更高的效率
                // 因為如果直接操作DOM節點的話,每次修改DOM都會導致DOM的回流或重繪,而將其放在fragment里,修改fragment不會導致DOM回流和重繪
                // 當在fragment一次性修改完后,在直接放回到DOM節點中
                node2Fragment() {
                  const fragment = document.createDocumentFragment();
                  let firstChild;
                  while(firstChild = this.$el.firstChild) {
                    fragment.appendChild(firstChild);
                  }
                  return fragment;
                }
              }
              復制實現v-model

              在將node節點轉為fragment后,我們來對其中的v-model語法進行編譯。

              由于v-model語句只可能會出現在元素節點的attributes里,因此,我們先判斷該節點是否為元素節點,若為元素節點,則判斷其是否是directive(目前只有v-model),若都滿足的話,則調用CompileUtils.compileModelAttr來編譯該節點。

              編譯含有v-model的節點主要有兩步:

              1. 為元素節點注冊input事件,在input事件觸發的時候,更新vm(this.data)上對應的屬性值。
              2. 對v-model依賴的屬性注冊一個Watcher函數,當依賴的屬性發生變化,則更新元素節點的value。
              class MVVM {
                constructor({ data, el }) {
                  this.data = data;
                  this.el = el;
                  this.init();
                  this.initDom();
                }
                
                initDom() {
                  const fragment = this.node2Fragment();
                  this.compile(fragment);
                  // 將fragment返回到頁面中
                  document.body.appendChild(fragment);
                }
                
                compile(node) {
                  if (this.isElementNode(node)) {
                    // 若是元素節點,則遍歷它的屬性,編譯其中的指令
                    const attrs = node.attributes;
                    Array.prototype.forEach.call(attrs, (attr) => {
                      if (this.isDirective(attr)) {
                        CompileUtils.compileModelAttr(this.data, node, attr)
                      }
                    })
                  }
                  // 若節點有子節點的話,則對子節點進行編譯
                  if (node.childNodes && node.childNodes.length > 0) {
                    Array.prototype.forEach.call(node.childNodes, (child) => {
                      this.compile(child);
                    })
                  }
                }
                // 是否是屬性節點
                isElementNode(node) {
                  return node.nodeType === 1;
                }
                // 檢測屬性是否是指令(vue的指令是v-開頭)
                isDirective(attr) {
                  return attr.nodeName.indexOf('v-') >= 0;
                }
              }
              
              const CompileUtils = {
                // 編譯v-model屬性,為元素節點注冊input事件,在input事件觸發的時候,更新vm對應的值。
                // 同時也注冊一個Watcher函數,當所依賴的值發生變化的時候,更新節點的值
                compileModelAttr(vm, node, attr) {
                  const { value: keys, nodeName } = attr;
                  node.value = this.getModelValue(vm, keys);
                  // 將v-model屬性值從元素節點上去掉
                  node.removeAttribute(nodeName);
                  node.addEventListener('input', (e) => {
                    this.setModelValue(vm, keys, e.target.value);
                  });
                    
                  new Watcher(vm, keys, (oldVal, newVal) => {
                    node.value = newVal;
                  });
                },
                /* 解析keys,比如,用戶可以傳入
                *  <input v-model="obj.name" />
                *  這個時候,我們在取值的時候,需要將"obj.name"解析為data[obj][name]的形式來獲取目標值
                */
                parse(vm, keys) {
                  keys = keys.split('.');
                  let value = vm;
                  keys.forEach(_key => {
                    value = value[_key];
                  });
                  return value;
                },
                // 根據vm和keys,返回v-model對應屬性的值
                getModelValue(vm, keys) {
                  return this.parse(vm, keys);
                },
                // 修改v-model對應屬性的值
                setModelValue(vm, keys, val) {
                  keys = keys.split('.');
                  let value = vm;
                  for(let i = 0; i < keys.length - 1; i++) {
                    value = value[keys[i]];
                  }
                  value[keys[keys.length - 1]] = val;
                },
              }
              復制實現{{}}語法

              {{}}語法只可能會出現在文本節點中,因此,我們只需要對文本節點做處理。如果文本節點中出現{{key}}這種語句的話,我們則對該節點進行編譯。在這里,我們可以通過下面這個正則表達式來對文本節點進行處理,判斷其是否含有{{}}語法。

              const textReg = /\{\{\s*\w+\s*\}\}/gi; // 檢測{{name}}語法
              console.log(textReg.test('sss'));
              console.log(textReg.test('aaa{{  name  }}'));
              console.log(textReg.test('aaa{{  name  }} {{ text }}'));
              復制

              若含有{{}}語法,我們則可以對其處理,由于一個文本節點可能出現多個{{}}語法,因此編譯含有{{}}語法的文本節點主要有以下兩步:

              1. 找出該文本節點中所有依賴的屬性,并且保留原始文本信息,根據原始文本信息還有屬性值,生成最終的文本信息。比如說,原始文本信息是"test {{test}} {{name}}",那么該文本信息依賴的屬性有this.data.test和this.data.name,那么我們可以根據原本信息和屬性值,生成最終的文本。
              2. 為該文本節點所有依賴的屬性注冊Watcher函數,當依賴的屬性發生變化的時候,則更新文本節點的內容。
              class MVVM {
                constructor({ data, el }) {
                  this.data = data;
                  this.el = el;
                  this.init();
                  this.initDom();
                }
                
                initDom() {
                  const fragment = this.node2Fragment();
                  this.compile(fragment);
                  // 將fragment返回到頁面中
                  document.body.appendChild(fragment);
                }
                
                compile(node) {
                  const textReg = /\{\{\s*\w+\s*\}\}/gi; // 檢測{{name}}語法
                  if (this.isTextNode(node)) {
                    // 若是文本節點,則判斷是否有{{}}語法,如果有的話,則編譯{{}}語法
                    let textContent = node.textContent;
                    if (textReg.test(textContent)) {
                      // 對于 "test{{test}} {{name}}"這種文本,可能在一個文本節點會出現多個匹配符,因此得對他們統一進行處理
                      // 使用 textReg來對文本節點進行匹配,可以得到["{{test}}", "{{name}}"]兩個匹配值
                      const matchs = textContent.match(textReg);
                      CompileUtils.compileTextNode(this.data, node, matchs);
                    }
                  }
                  // 若節點有子節點的話,則對子節點進行編譯
                  if (node.childNodes && node.childNodes.length > 0) {
                    Array.prototype.forEach.call(node.childNodes, (child) => {
                      this.compile(child);
                    })
                  }
                }
                // 是否是文本節點
                isTextNode(node) {
                  return node.nodeType === 3;
                }
              }
              
              const CompileUtils = {
                reg: /\{\{\s*(\w+)\s*\}\}/, // 匹配 {{ key }}中的key
                // 編譯文本節點,并注冊Watcher函數,當文本節點依賴的屬性發生變化的時候,更新文本節點
                compileTextNode(vm, node, matchs) {
                  // 原始文本信息
                  const rawTextContent = node.textContent;
                  matchs.forEach((match) => {
                    const keys = match.match(this.reg)[1];
                    console.log(rawTextContent);
                    new Watcher(vm, keys, () => this.updateTextNode(vm, node, matchs, rawTextContent));
                  });
                  this.updateTextNode(vm, node, matchs, rawTextContent);
                },
                // 更新文本節點信息
                updateTextNode(vm, node, matchs, rawTextContent) {
                  let newTextContent = rawTextContent;
                  matchs.forEach((match) => {
                    const keys = match.match(this.reg)[1];
                    const val = this.getModelValue(vm, keys);
                    newTextContent = newTextContent.replace(match, val);
                  })
                  node.textContent = newTextContent;
                }
              }
              復制結語

              這樣,一個具有v-model和{{}}功能的MVVM類就已經完成了。代碼地址點擊這里。有興趣的小伙伴可以上去看下(也可以star or fork下哈哈哈)。

              這里也有一個簡單的樣例(忽略樣式)。

              接下來的話,可能會繼續實現computed屬性,v-bind方法,以及支持在{{}}里面放表達式。如果覺得這個文章對你有幫助的話,麻煩點個贊,嘻嘻。

              最后,貼上所有的代碼:

              class Observer {
                constructor(data) {
                  // 如果不是對象,則返回
                  if (!data || typeof data !== 'object') {
                    return;
                  }
                  this.data = data;
                  this.walk();
                }
              
                // 對傳入的數據進行數據劫持
                walk() {
                  for (let key in this.data) {
                    this.defineReactive(this.data, key, this.data[key]);
                  }
                }
                // 創建當前屬性的一個發布實例,使用Object.defineProperty來對當前屬性進行數據劫持。
                defineReactive(obj, key, val) {
                  // 創建當前屬性的發布者
                  const dep = new Dep();
                  /*
                  * 遞歸對子屬性的值進行數據劫持,比如說對以下數據
                  * let data = {
                  *   name: 'cjg',
                  *   obj: {
                  *     name: 'zht',
                  *     age: 22,
                  *     obj: {
                  *       name: 'cjg',
                  *       age: 22,
                  *     }
                  *   },
                  * };
                  * 我們先對data最外層的name和obj進行數據劫持,之后再對obj對象的子屬性obj.name,obj.age, obj.obj進行數據劫持,層層遞歸下去,直到所有的數據都完成了數據劫持工作。
                  */
                  new Observer(val);
                  Object.defineProperty(obj, key, {
                    get() {
                      // 若當前有對該屬性的依賴項,則將其加入到發布者的訂閱者隊列里
                      if (Dep.target) {
                        dep.addSub(Dep.target);
                      }
                      return val;
                    },
                    set(newVal) {
                      if (val === newVal) {
                        return;
                      }
                      val = newVal;
                      new Observer(newVal);
                      dep.notify();
                    }
                  })
                }
              }
              
              // 發布者,將依賴該屬性的watcher都加入subs數組,當該屬性改變的時候,則調用所有依賴該屬性的watcher的更新函數,觸發更新。
              class Dep {
                constructor() {
                  this.subs = [];
                }
              
                addSub(sub) {
                  if (this.subs.indexOf(sub) < 0) {
                    this.subs.push(sub);
                  }
                }
              
                notify() {
                  this.subs.forEach((sub) => {
                    sub.update();
                  })
                }
              }
              
              Dep.target = null;
              
              // 觀察者
              class Watcher {
                /**
                 *Creates an instance of Watcher.
                 * @param {*} vm
                 * @param {*} keys
                 * @param {*} updateCb
                 * @memberof Watcher
                 */
                constructor(vm, keys, updateCb) {
                  this.vm = vm;
                  this.keys = keys;
                  this.updateCb = updateCb;
                  this.value = null;
                  this.get();
                }
              
                // 根據vm和keys獲取到最新的觀察值
                get() {
                  // 將Dep的依賴項設置為當前的watcher,并且根據傳入的keys遍歷獲取到最新值。
                  // 在這個過程中,由于會調用observer對象屬性的getter方法,因此在遍歷過程中這些對象屬性的發布者就將watcher添加到訂閱者隊列里。
                  // 因此,當這一過程中的某一對象屬性發生變化的時候,則會觸發watcher的update方法
                  Dep.target = this;
                  this.value = CompileUtils.parse(this.vm, this.keys);
                  Dep.target = null;
                  return this.value;
                }
              
                update() {
                  const oldValue = this.value;
                  const newValue = this.get();
                  if (oldValue !== newValue) {
                    this.updateCb(oldValue, newValue);
                  }
                }
              }
              
              class MVVM {
                constructor({ data, el }) {
                  this.data = data;
                  this.el = el;
                  this.init();
                  this.initDom();
                }
              
                // 初始化
                init() {
                  // 對this.data進行數據劫持
                  new Observer(this.data);
                  // 傳入的el可以是selector,也可以是元素,因此我們要在這里做一層處理,保證this.$el的值是一個元素節點
                  this.$el = this.isElementNode(this.el) ? this.el : document.querySelector(this.el);
                  // 將this.data的屬性都綁定到this上,這樣用戶就可以直接通過this.xxx來訪問this.data.xxx的值
                  for (let key in this.data) {
                    this.defineReactive(key);
                  }
                }
              
                initDom() {
                  const fragment = this.node2Fragment();
                  this.compile(fragment);
                  document.body.appendChild(fragment);
                }
                // 將節點轉為fragment,通過fragment來操作DOM,可以獲得更高的效率
                // 因為如果直接操作DOM節點的話,每次修改DOM都會導致DOM的回流或重繪,而將其放在fragment里,修改fragment不會導致DOM回流和重繪
                // 當在fragment一次性修改完后,在直接放回到DOM節點中
                node2Fragment() {
                  const fragment = document.createDocumentFragment();
                  let firstChild;
                  while(firstChild = this.$el.firstChild) {
                    fragment.appendChild(firstChild);
                  }
                  return fragment;
                }
              
                defineReactive(key) {
                  Object.defineProperty(this, key, {
                    get() {
                      return this.data[key];
                    },
                    set(newVal) {
                      this.data[key] = newVal;
                    }
                  })
                }
              
                compile(node) {
                  const textReg = /\{\{\s*\w+\s*\}\}/gi; // 檢測{{name}}語法
                  if (this.isElementNode(node)) {
                    // 若是元素節點,則遍歷它的屬性,編譯其中的指令
                    const attrs = node.attributes;
                    Array.prototype.forEach.call(attrs, (attr) => {
                      if (this.isDirective(attr)) {
                        CompileUtils.compileModelAttr(this.data, node, attr)
                      }
                    })
                  } else if (this.isTextNode(node)) {
                    // 若是文本節點,則判斷是否有{{}}語法,如果有的話,則編譯{{}}語法
                    let textContent = node.textContent;
                    if (textReg.test(textContent)) {
                      // 對于 "test{{test}} {{name}}"這種文本,可能在一個文本節點會出現多個匹配符,因此得對他們統一進行處理
                      // 使用 textReg來對文本節點進行匹配,可以得到["{{test}}", "{{name}}"]兩個匹配值
                      const matchs = textContent.match(textReg);
                      CompileUtils.compileTextNode(this.data, node, matchs);
                    }
                  }
                  // 若節點有子節點的話,則對子節點進行編譯。
                  if (node.childNodes && node.childNodes.length > 0) {
                    Array.prototype.forEach.call(node.childNodes, (child) => {
                      this.compile(child);
                    })
                  }
                }
                
                // 是否是屬性節點
                isElementNode(node) {
                  return node.nodeType === 1;
                }
                // 是否是文本節點
                isTextNode(node) {
                  return node.nodeType === 3;
                }
              
                isAttrs(node) {
                  return node.nodeType === 2;
                }
                // 檢測屬性是否是指令(vue的指令是v-開頭)
                isDirective(attr) {
                  return attr.nodeName.indexOf('v-') >= 0;
                }
              
              }
              
              const CompileUtils = {
                reg: /\{\{\s*(\w+)\s*\}\}/, // 匹配 {{ key }}中的key
                // 編譯文本節點,并注冊Watcher函數,當文本節點依賴的屬性發生變化的時候,更新文本節點
                compileTextNode(vm, node, matchs) {
                  // 原始文本信息
                  const rawTextContent = node.textContent;
                  matchs.forEach((match) => {
                    const keys = match.match(this.reg)[1];
                    console.log(rawTextContent);
                    new Watcher(vm, keys, () => this.updateTextNode(vm, node, matchs, rawTextContent));
                  });
                  this.updateTextNode(vm, node, matchs, rawTextContent);
                },
                // 更新文本節點信息
                updateTextNode(vm, node, matchs, rawTextContent) {
                  let newTextContent = rawTextContent;
                  matchs.forEach((match) => {
                    const keys = match.match(this.reg)[1];
                    const val = this.getModelValue(vm, keys);
                    newTextContent = newTextContent.replace(match, val);
                  })
                  node.textContent = newTextContent;
                },
                // 編譯v-model屬性,為元素節點注冊input事件,在input事件觸發的時候,更新vm對應的值。
                // 同時也注冊一個Watcher函數,當所依賴的值發生變化的時候,更新節點的值
                compileModelAttr(vm, node, attr) {
                  const { value: keys, nodeName } = attr;
                  node.value = this.getModelValue(vm, keys);
                  // 將v-model屬性值從元素節點上去掉
                  node.removeAttribute(nodeName);
                  new Watcher(vm, keys, (oldVal, newVal) => {
                    node.value = newVal;
                  });
                  node.addEventListener('input', (e) => {
                    this.setModelValue(vm, keys, e.target.value);
                  });
                },
                /* 解析keys,比如,用戶可以傳入
                *  let data = {
                *    name: 'cjg',
                *    obj: {
                *      name: 'zht',
                *    },
                *  };
                *  new Watcher(data, 'obj.name', (oldValue, newValue) => {
                *    console.log(oldValue, newValue);
                *  })
                *  這個時候,我們需要將keys解析為data[obj][name]的形式來獲取目標值
                */
                parse(vm, keys) {
                  keys = keys.split('.');
                  let value = vm;
                  keys.forEach(_key => {
                    value = value[_key];
                  });
                  return value;
                },
                // 根據vm和keys,返回v-model對應屬性的值
                getModelValue(vm, keys) {
                  return this.parse(vm, keys);
                },
                // 修改v-model對應屬性的值
                setModelValue(vm, keys, val) {
                  keys = keys.split('.');
                  let value = vm;
                  for(let i = 0; i < keys.length - 1; i++) {
                    value = value[keys[i]];
                  }
                  value[keys[keys.length - 1]] = val;
                },
              }
              復制
              關聯標簽:
              秋霞久久老子无码午夜精品,欧洲av成本人在线观看免费,亚洲精品无码国模av