Vue底層學習5——插值文本編譯與依賴收集

全手打原創,轉載請標明出處:https://www.cnblogs.com/dreamsqin/p/15044033.html, 多謝,=。=~(如果對你有幫助的話請幫我點個贊啦)

作爲一個Web前端開發人員,使用Vue框架進行項目開發已經有一陣子,掐指一算,是時候認真探索一下Vue的底層了,以前的瞭解比較偏理論,這一次打算在弄清基本原理的前提下自己手寫Vue中的核心部分,也許這樣我纔敢說自己“深入理解”了Vue。上篇簡述了整個編譯器原理並擬定了三項編譯目標,完成編譯器框架搭建,在遍歷Dom子節點時實現分流處理,本篇主要實現第一個目標插值文本編譯和依賴收集~

插值文本編譯

由上一篇提供的demo2可以得到如下的運行結果:

但實際上我們想要展示的是各個變量對應的值,而不是變量名,所以需要編譯Dom中的插值變量,並將其替換爲對應的值,這裏新建一個compileText方法實現:

/*** compile.js ***/
// new Compile(el, vm)

class Compile{
  constructor(el, vm) {
    // 需要遍歷的Dom節點
    this.$el = document.querySelector(el);
    // 數據緩存
    this.$vm = vm;

    // 編譯
    if (this.$el) {
      // 提取指定節點中的內容,提高效率,減少Dom操作
      this.$fragment = this.node2Fragment(this.$el);
      // 執行編譯
      this.compile(this.$fragment);
      // 將編譯完的html追加至$el
      this.$el.appendChild(this.$fragment);
    }
  }

  // 提取指定Dom節點中的代碼片段
  node2Fragment(el) {
    const fragment = document.createDocumentFragment();
    // 將el中的所有子元素移動至fragment中
    let child = null;
    while(child = el.firstChild) {
      fragment.appendChild(child);
    }
    return fragment;
  }

  // 編譯過程
  compile(el) {
    const childNodes = el.childNodes;
    Array.from(childNodes).forEach(node => {
      // 類型判斷
      if (this.isElement(node)) {
        // 節點
        console.log('編譯節點' + node.nodeName);
      } else if(this.isInterpolation(node)) {
        // 編譯插值文本
        this.compileText(node);
      }

      // 遞歸子節點
      if (node.childNodes && node.childNodes.length > 0) {
        this.compile(node);
      }
    })
  }

  isElement(node) {
    return node.nodeType === 1;
  }

  isInterpolation(node) {
    return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent);
  }

  // 插值文本編譯
  compileText(node) {
    node.textContent = this.$vm.$data[RegExp.$1];
  }
}

需要特別注意的是RegExp.$1的巧用,在做子節點分流時我們通過正則表達式對插值文本進行了匹配分組,所以在執行compileText方法時我們可以通過RegExp.$1獲取到分組中的內容,也就是插值括號{{}}中的變量,例如namelocationlocationAgain,然後通過傳遞的Vue實例this.$vm獲取到$data中的屬性變量值,再對節點內容進行替換操作,最終運行結果如下:

可以看到頁面中的變量成功被替換,但這種方式只會初始化一次,當變量值發生改變時,頁面中展示的內容是不會同步變更的,可以利用demo2(源碼可參見《Vue底層學習4——編譯器框架搭建》)中created方法的延遲賦值操作測試一下,我們在MVue的構造函數中執行一下created方法:

/*** MVue.js ***/
// new MVue({ data: {...} })

class MVue {
  constructor(options) {
    // 數據緩存
    this.$options = options;
    this.$data = options.data;

    // 數據遍歷
    this.observe(this.$data);

    new Compile(options.el, this);

    // created執行
    if (options.created) {
      options.created.call(this);
    }
  }
}

調用時使用call綁定this指向是爲了方便在Vue實例的created方法中輕鬆使用this訪問當前的Vue實例對象,例如我們日常用this.data去訪問實例的數據屬性。created執行後結果如下:

開始啦成功打印,但name的重新賦值並沒有同步更新至頁面,與上面的猜想一致。其主要原因是沒有做依賴收集,也就是之前MVue.jsconstructor 中模擬Watcher 激活getter的部分,除此之外,我們編譯器中還需要一個更新函數,之前Watcherupdate方法都是通過console實現視圖更新的預留,這些事還是得編譯器來完成。

更新函數

觸發更新的操作有很多,視圖中不僅僅只有插值文本,還有一系列的v-指令或者事件,所以我們需要抽象出一個更新函數供所有的觸發調用,在編譯器中定義一個更新函數update,它接收4個參數,分別表示需要更新的節點當前的Vue實例屬性標識觸發更新的指令標識

/*** compile.js ***/
// new Compile(el, vm)

class Compile{
  // 更新函數
  update(node, vm, exp, dir) {
    const updateFn = this[dir + 'Updater'];
    // 如果存在就執行,實現初始化
    updateFn && updateFn(node, vm.$data[exp]);
  }
}

updateFn的執行只能達到初始化的作用,跟上述compileText函數實現的效果一致,但當數據變更時想要同步更新,就需要做依賴收集,跟之前模擬的一樣,我們需要創建一個Watcher實例,接收3個參數,分別表示當前的Vue實例屬性標識當屬性變更時執行的更新回調函數

/*** compile.js ***/
// new Compile(el, vm)

class Compile{
  // 更新函數
  update(node, vm, exp, dir) {
    const updateFn = this[dir + 'Updater'];
    // 如果存在就執行,實現初始化
    updateFn && updateFn(node, vm.$data[exp]);
    
    // 依賴收集
    new Watcher(vm, exp, function(value) {
      updateFn && updateFn(node, value);
    });
  }
}

那麼對於插值文本的更新我們就需要創建一個對應的更新函數textUpdater,並且之前用於插值文本編譯的compileText函數就需要做對應的變更:

/*** compile.js ***/
// new Compile(el, vm)

class Compile{
  // 更新函數
  update(node, vm, exp, dir) {
    const updateFn = this[dir + 'Updater'];
    // 如果存在就執行,實現初始化
    updateFn && updateFn(node, vm.$data[exp]);
    
    // 依賴收集
    new Watcher(vm, exp, function(value) {
      updateFn && updateFn(node, value);
    });
  }
  
  // 插值文本更新
  textUpdater(node, value) {
    node.textContent = value;
  }
  
  // 插值文本編譯
  compileText(node) {
    this.update(node, this.$vm, RegExp.$1, 'text');
  }
}

可以看到以前我們在模擬依賴收集時,實例化Watcher時是不會傳參的,但是現在接收了3個參數,所以需要同步修改MVue中的Watcher類,並通過Watcher拿到的Vue實例及屬性標識激活getter實現依賴收集:

/*** MVue.js ***/
class Watcher {
  constructor(vm, exp, cb) {
    // 數據緩存
    this.$vm = vm;
    this.$key = exp;
    this.$cb = cb;

    // 將當前Watcher的實例指定到Dep靜態屬性target
    Dep.target = this;
    
    // 激活屬性的getter,添加依賴
    this.$vm.$data[this.$key];
    // 置空,防止重複添加
    Dep.target = null;
  }

  update() {
    // 預留視圖更新
    console.log('數據更新了,需要我們更新視圖');
  }
}

那麼現在預留的視圖更新就可以直接執行傳入的cb回調了,並綁定其中的this指向爲當前的Vue實例,同時將修改後的值作爲參數傳遞進去:

/*** MVue.js ***/
class Watcher {
  constructor(vm, exp, cb) {
    // 數據緩存
    this.$vm = vm;
    this.$key = exp;
    this.$cb = cb;

    // 將當前Watcher的實例指定到Dep靜態屬性target
    Dep.target = this;
    
    // 激活屬性的getter,添加依賴
    this.$vm.$data[this.$key];
    // 置空,防止重複添加
    Dep.target = null;
  }

  update() {
    // 視圖更新
    this.$cb.call(this.$vm, this.$vm.$data[this.$key]);
  }
}

爲了方便我們獲取和設置data中的屬性,我們可以做一層代理,將data屬性掛載到Vue的實例上,實現通過Vue實例就可以直接訪問或設置data屬性:

/*** MVue.js ***/
// new MVue({ data: {...} })

class MVue {
  constructor(options) {...}

  observe(data) {
    // 確定data存在並且爲對象
    if (!data || typeof data !== 'object') {
      return;
    }

    // 遍歷data對象
    Object.keys(data).forEach(key => {
        // 重寫對象屬性的getter和setter,實現數據的響應化
        this.defineReactive(data, key, data[key]);
        
        // 代理data中的屬性到Vue實例上
        this.proxyData(key);
    })
  }

  defineReactive(obj, key, val) {...}

  proxyData(key) {
    Object.defineProperty(this, key, {
      get: function() {
        return this.$data[key];
      },
      set: function(newVal) {
        this.$data[key] = newVal;
      }
    })
  }
}

接下來就可以把代碼中通過this.$vm.$data訪問或設置data中屬性的操作修改爲this.$vm直接進行訪問和設置,修改後的代碼就不貼出來了,全局搜索一下~

下面就是見證奇蹟的時刻,再次運行一下demo2,效果如下,1.5s左右後視圖被同步更新了:

參考資料

1、Vue源碼:https://github.com/vuejs/vue

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章