3天学写mvvm框架[三]:浏览器端渲染

栏目: 后端 · 发布时间: 5年前

内容简介:通过之前的实践,我们已经实现了数据变动的监听与模板的解析,今天我们就将把两者结合起来,完成浏览器端的渲染工作。首先我们来编写类:

通过之前的实践,我们已经实现了数据变动的监听与模板的解析,今天我们就将把两者结合起来,完成浏览器端的渲染工作。

Vue类

首先我们来编写类: Vue

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 ,它会优先访问实例的数据,如果数据不存在,再来访问方法等:

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 中的方法绑定到实例上:

Object.keys(methods).forEach((key) => {
  this[key] = methods[key].bind(proxyObj);
});
复制代码

最后我们将调用 watch 方法,传入的求值函数 updateComponent 将完成渲染工作,同时收集依赖,以便在数据变动时重新渲染:

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 ,以便进行后续的比较更新:

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

参考:


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

JUnit Recipes中文版

JUnit Recipes中文版

陈浩等译 / 电子工业 / 2006-9 / 69.00元

《JUnit Recipes中文版:程序员实用测试技巧》主要介绍了在Java开发中使用JUnit进行单元测试的各种方法、原则、技巧与实践。本书出自开发一线专家之手,本着实用的原则,涵盖各类Java开发中应用JUnit的实用技巧,内容丰富、全面深入;无论对于需要应用JUnit进行单元测试的一线Java开发人员,还是JUnit入门、进阶者,本书都是一本不可多得的实用指南。这本书介绍了大量的JUnit实......一起来看看 《JUnit Recipes中文版》 这本书的介绍吧!

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具