vue原始碼探究(第四彈)

dunne21發表於2021-09-09

vue原始碼探究(第四彈)

結束了上一part的資料代理,這一部分主要講講vue的模板解析,感覺這個有點難理解,而且內容有點多,hhh。

模板解析

廢話不多說,先從簡單的入手。

按照之前的套路,先舉一個例子:

<div id="test">
  <p>{{name}}</p></div><script type="text/javascript" class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="js/mvvm/compile.js"></script><script type="text/javascript" class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="js/mvvm/mvvm.js"></script><script type="text/javascript" class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="js/mvvm/observer.js"></script><script type="text/javascript" class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="js/mvvm/watcher.js"></script><script type="text/javascript">
  new MVVM({
    el: '#test',
    data: {
      name: '喵喵喵'
    }
  })
  // 這時候,我們的頁面還是渲染出 喵喵喵</script>

接下來講講內部的相關實現:

我們的MVVM中的建構函式中有什麼東西,可以解析我們的模板呢?

// 建立一個用來編譯模板的compile物件this.$compile = new Compile(options.el || document.body, this)

什麼是Compile?

一行一行註釋著解讀

function Compile(el, vm) {
  // 儲存vm
  this.$vm = vm;
  // 儲存el元素
  this.$el = this.isElementNode(el) ? el : document.querySelector(el);
  // 如果el元素存在  if (this.$el) {
    // 1. 取出el中所有子節點, 封裝在一個framgment物件中
    // 這裡的node2Fragment 就是將node -> 放入 Fragment中,documentFragment將node進行批次處理
    this.$fragment = this.node2Fragment(this.$el);
    // 2. 編譯fragment中所有層次子節點
    this.init();
    // 3. 將fragment新增到el中
    this.$el.appendChild(this.$fragment);
  }}Compile.prototype = {
  node2Fragment: function (el) {
    var fragment = document.createDocumentFragment(),
      child;

    // 將原生節點複製到fragment    while (child = el.firstChild) {
      fragment.appendChild(child);
    }

    return fragment;
  },

  init: function () {
    // 編譯fragment
    this.compileElement(this.$fragment);
  },

  compileElement: function (el) {
    // 得到所有子節點
    var childNodes = el.childNodes,
      // 儲存compile物件      me = this;
    // 遍歷所有子節點    [].slice.call(childNodes).forEach(function (node) {
      // 得到節點的文字內容
      var text = node.textContent;
      // 正則物件(匹配大括號表示式)
      var reg = /{{(.*)}}/;  // {{name}}
      // 這裡提出一個問題,為什麼這裡的正則匹配要用/{{(.*)}}/,而不是/{{.*}}/呢?
      // 其實/{{.*}}/就可以匹配到{{xxx}},這裡加一個()的意義是,用於.$1,來取得{{}}中的值,eg:name
      // 如果是元素節點      if (me.isElementNode(node)) {
        // 編譯元素節點的指令屬性
        me.compile(node);
        // 如果是一個大括號表示式格式的文字節點      } else if (me.isTextNode(node) && reg.test(text)) {
        // 編譯大括號表示式格式的文字節點
        me.compileText(node, RegExp.$1); // RegExp.$1: 表示式   name      }
      // 如果子節點還有子節點      if (node.childNodes && node.childNodes.length) {
        // 遞迴呼叫實現所有層次節點的編譯
        me.compileElement(node);
      }
    });
  },

  compile: function (node) {
    // 得到所有標籤屬性節點
    var nodeAttrs = node.attributes,      me = this;
    // 遍歷所有屬性    [].slice.call(nodeAttrs).forEach(function (attr) {
      // 得到屬性名: v-on:click
      var attrName = attr.name;
      // 判斷是否是指令屬性      if (me.isDirective(attrName)) {
        // 得到表示式(屬性值): test
        var exp = attr.value;
        // 得到指令名: on:click
        var dir = attrName.substring(2);
        // 事件指令        if (me.isEventDirective(dir)) {
          // 解析事件指令
          compileUtil.eventHandler(node, me.$vm, exp, dir);
        // 普通指令        } else {
          // 解析普通指令
          compileUtil[dir] && compileUtil[dir](node, me.$vm, exp);
        }

        // 移除指令屬性
        node.removeAttribute(attrName);
      }
    });
  },

  compileText: function (node, exp) {
    // 呼叫編譯工具物件解析
    compileUtil.text(node, this.$vm, exp);
  },

  isDirective: function (attr) {
    return attr.indexOf('v-') == 0;
  },

  isEventDirective: function (dir) {
    return dir.indexOf('on') === 0;
  },

  isElementNode: function (node) {
    return node.nodeType == 1;
  },

  isTextNode: function (node) {
    return node.nodeType == 3;
  }};// 指令處理集合
var compileUtil = {
  // 解析: v-text/{{}}
  text: function (node, vm, exp) {
    this.bind(node, vm, exp, 'text');
  },
  // 解析: v-html
  html: function (node, vm, exp) {
    this.bind(node, vm, exp, 'html');
  },

  // 解析: v-model
  model: function (node, vm, exp) {
    this.bind(node, vm, exp, 'model');

    var me = this,      val = this._getVMVal(vm, exp);
    node.addEventListener('input', function (e) {
      var newValue = e.target.value;
      if (val === newValue) {
        return;
      }

      me._setVMVal(vm, exp, newValue);
      val = newValue;
    });
  },

  // 解析: v-class
  class: function (node, vm, exp) {
    this.bind(node, vm, exp, 'class');
  },

  // 真正用於解析指令的方法
  bind: function (node, vm, exp, dir) {
    /*實現初始化顯示*/
    // 根據指令名(text)得到對應的更新節點函式
    // 取到一個object的屬性,有2個方法,一個是obj. 一個是obj[]
    // 當我們要取得屬性是一個變數的時候,使用obj[]
    var updaterFn = updater[dir + 'Updater'];
    // 如果存在呼叫來更新節點
    updaterFn && updaterFn(node, this._getVMVal(vm, exp));

    // 建立表示式對應的watcher物件
    new Watcher(vm, exp, function (value, oldValue) {/*更新介面*/
      // 當對應的屬性值發生了變化時, 自動呼叫, 更新對應的節點
      updaterFn && updaterFn(node, value, oldValue);
    });
  },

  // 事件處理
  eventHandler: function (node, vm, exp, dir) {
    // 得到事件名/型別: click
    var eventType = dir.split(':')[1],
      // 根據表示式得到事件處理函式(從methods中): test(){}
      fn = vm.$options.methods && vm.$options.methods[exp];
    // 如果都存在    if (eventType && fn) {
      // 繫結指定事件名和回撥函式的DOM事件監聽, 將回撥函式中的this強制繫結為vm
      node.addEventListener(eventType, fn.bind(vm), false);
    }
  },

  // 得到表示式對應的value
  _getVMVal: function (vm, exp) {
    // 這裡為什麼要forEach呢?
    // 如果你的exp是a.b.c.c.d呢 就需要forEach 如果只是一層 當然不需要遍歷啦
    var val = vm._data;
    exp = exp.split('.');
    exp.forEach(function (k) {
      val = val[k];
    });
    return val;
  },

  _setVMVal: function (vm, exp, value) {
    var val = vm._data;
    exp = exp.split('.');
    exp.forEach(function (k, i) {
      // 非最後一個key,更新val的值      if (i < exp.length - 1) {
        val = val[k];
      } else {
        val[k] = value;
      }
    });
  }};// 包含多個用於更新節點方法的物件
var updater = {
  // 更新節點的textContent
  textUpdater: function (node, value) {
    node.textContent = typeof value == 'undefined' ? '' : value;
  },

  // 更新節點的innerHTML
  htmlUpdater: function (node, value) {
    node.innerHTML = typeof value == 'undefined' ? '' : value;
  },

  // 更新節點的className
  classUpdater: function (node, value, oldValue) {
    var className = node.className;
    className = className.replace(oldValue, '').replace(/s$/, '');

    var space = className && String(value) ? ' ' : '';

    node.className = className + space + value;
  },

  // 更新節點的value
  modelUpdater: function (node, value, oldValue) {
    node.value = typeof value == 'undefined' ? '' : value;
  }};

最後

未完待續...
接下來,還有一個更有趣的東西

下一章繼續~


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/1600/viewspace-2822853/,如需轉載,請註明出處,否則將追究法律責任。

相關文章