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的一部分原理。

参考:

相关文章
相关标签/搜索