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

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

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

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

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

参考:


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

查看所有标签

猜你喜欢:

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

Letting Go of the Words

Letting Go of the Words

Janice (Ginny) Redish / Morgan Kaufmann / 2007-06-11 / USD 49.95

"Redish has done her homework and created a thorough overview of the issues in writing for the Web. Ironically, I must recommend that you read her every word so that you can find out why your customer......一起来看看 《Letting Go of the Words》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

在线进制转换器
在线进制转换器

各进制数互转换器

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

HTML 编码/解码