3天學寫mvvm框架[三]:瀏覽器端渲染

此前爲了學習Vue的源碼,我決定本身動手寫一遍簡化版的Vue。如今我將我所瞭解到的分享出來。若是你正在使用Vue但還不瞭解它的原理,或者正打算閱讀Vue的源碼,但願這些分享能對你瞭解Vue的運行原理有所幫助。vue

目標

經過以前的實踐,咱們已經實現了數據變更的監聽與模板的解析,今天咱們就將把二者結合起來,完成瀏覽器端的渲染工做。git

Vue類

首先咱們來編寫類:Vuegithub

Vue的構造函數將接受多個參數,包括:數組

  • el:實例的渲染將以此做爲父節點。
  • data:一個函數,運行後將返回一個對象/數組,做爲實例的數據。
  • tpl: 實例的模板字符串。
  • methods:實例的方法。

在構造函數中,咱們將先設定根元素爲$el,而後調用咱們以前寫的parseHtmlgenerateRender並最終生成Function實例做爲咱們的渲染函數render,同時使用proxy來建立可觀察的數據:瀏覽器

class Vue {
  constructor({ el, data, tpl, methods }) {
    // set render
    if (el instanceof Element) {
      this.$el = el;
    } else {
      this.$el = document.querySelector(el);
    }
    const ast = parseHtml(tpl);
    const renderCode = generateRender(ast);
    this.render = new Function(renderCode);

    // set data
    this.data = proxy(data.call(this));

    ...
  }

  ...
}
複製代碼

這裏,咱們將再次使用proxy來建立一個代理。在Vue中,例如data方法建立了{ a: 1 }這樣的數據,咱們能夠經過this.a而非相似this.data.a來訪問。爲了支持這樣更簡潔地訪問數據,咱們但願提供一個對象,同時提供對數據的訪問以及其餘內容例如方法的訪問,同時又保持proxy對於新鍵值對的設置的靈活性,所以我這裏採起的方式是建立一個新的proxy,它會優先訪問實例的數據,若是數據不存在,再來訪問方法等:bash

const proxyObj = new Proxy(this, {
  get(target, key) {
    if (key in target.data) return target.data[key];
    return target[key];
  },
  set(target, key, value) {
    if (!(key in target.data) && key in target) {
      target[key] = value;
    } else {
      target.data[key] = value;
    }
    return true;
  },
  has(target, key) {
    return (key in target) || (key in target.data);
  },
});
this._proxyObj = proxyObj;
複製代碼

接下去,咱們將methods中的方法綁定到實例上:app

Object.keys(methods).forEach((key) => {
  this[key] = methods[key].bind(proxyObj);
});
複製代碼

最後咱們將調用watch方法,傳入的求值函數updateComponent將完成渲染工做,同時收集依賴,以便在數據變更時從新渲染:dom

const updateComponent = () => {
  this._update(this._render());
};

watch(updateComponent, () => {/* noop */});
複製代碼

渲染與v-dom

_render方法將調用render來建立一棵由VNode節點組成的樹,或稱之爲v-dom函數

class VNode {
  constructor(tag, text, attrs, children) {
    this.tag = tag;
    this.text = text;
    this.attrs = attrs;
    this.children = children;
  }
}

class Vue {
  ...

  _render() {
    return this.render.call(this._proxyObj);
  }

  _c(tag, attrs, children) {
    return new VNode(tag, null, attrs, children);
  }

  _v(text) {
    return new VNode(null, text, null, null);
  }
}
複製代碼

_update方法將根據是否已經建立過舊的v-dom來判斷是進行建立過程仍是比較更新過程(patch),隨後咱們須要保存本次建立的v-dom,以便進行後續的比較更新:oop

_update(vNode) {
  const preVode = this.preVode;
  if (preVode) {
    patch(preVode, vNode);
  } else {
    this.preVode = vNode;
    this.$el.appendChild(build(vNode));
  }
}
複製代碼

建立過程將遍歷整個v-dom,使用document.createTextNodedocument.createElement來建立dom元素,並將其保存在VNode節點上,用以以後進行更新:

const build = function (vNode) {
  if (vNode.text) return vNode.$el = document.createTextNode(vNode.text);
  if (vNode.tag) {
    const $el = document.createElement(vNode.tag);
    handleAttrs(vNode, $el);
    vNode.children.forEach((child) => {
      $el.appendChild(build(child));
    });
    return vNode.$el = $el;
  }
};
const handleAttrs = function ({ attrs }, $el, preAttrs = {}) {
  if (preAttrs.class !== attrs.class || preAttrs['v-class'] !== attrs['v-class']) {
    let clsStr = '';
    if (attrs.class) clsStr += attrs.class;
    if (attrs['v-class']) clsStr += ' ' + attrs['v-class'];
    $el.className = clsStr;
  }
  if (attrs['v-on-click'] !== preAttrs['v-on-click']) { // 這裏匿名函數老是會不等的
    if (attrs['v-on-click']) $el.onclick = attrs['v-on-click'];
  }
};
複製代碼

因爲咱們還不支持v-ifv-forcomponent組件等等,所以咱們能夠認爲更新後的v-dom在結構上是一致的,這樣就大大簡化了比較更新的過程。咱們只須要遍歷新老兩顆v-dom,在patch方法中傳入對應的新老VNode節點,若是存在不一樣的屬性,便進行跟新就能夠了:

const patch = function (preVode, vNode) {
  if (preVode.tag === vNode.tag) {
    vNode.$el = preVode.$el;
    if (vNode.text) {
      if (vNode.text !== preVode.text) vNode.$el.textContent = vNode.text;
    } else {
      vNode.$el = preVode.$el;
      preVode.children.forEach((preChild, i) => { // TODO:
        patch(preChild, vNode.children[i]);
      });
      handleAttrs(vNode, vNode.$el, preVode.attrs);
    }
  } else {
    // 由於結構是同樣的,所以暫時沒必要考慮
  }
};
複製代碼

最後,咱們暴露一個方法來返回新建的Vue實例所綁定的_proxyObj對象,咱們就能夠經過這個對象來改變實例數據或是調用實例的方法等了:

Vue.new = function (opts) {
  return new Vue(opts)._proxyObj;
};
複製代碼

總結

咱們經過3次實踐,完成了數據監聽、模板解析以及最後的渲染。固然這只是一個很是簡陋的demo,容錯性有限、支持的功能也很是有限。

也許以後我還會更新這一系列的文章,加入計算屬性的支持、組件的支持、v-ifv-forv-model等directive的支持、templatekeep-alivecomponent等組件,等等。

最後謝謝您閱讀本文,但願有幫助到您理解Vue的一部分原理。

參考:

相關文章
相關標籤/搜索