博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
3天学写mvvm框架[三]:浏览器端渲染
阅读量:6964 次
发布时间:2019-06-27

本文共 4146 字,大约阅读时间需要 13 分钟。

此前为了学习Vue的源码,我决定自己动手写一遍简化版的Vue。现在我将我所了解到的分享出来。如果你正在使用Vue但还不了解它的原理,或者正打算阅读Vue的源码,希望这些分享能对你了解Vue的运行原理有所帮助。

目标

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

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

参考:

转载地址:http://qtwsl.baihongyu.com/

你可能感兴趣的文章
动态网站维护基本命令
查看>>
透视表提取不反复记录(2)-每一个物品的全部分类
查看>>
[Whole Web] [AngularJS + Grunt] Using ng-html2js to Convert Templates into JavaScript
查看>>
基于jQuery/CSS3实现拼图效果的相册插件
查看>>
【问题解决】小数点前面不显示0的问题
查看>>
ios学习笔记(二)第一个应用程序--Hello World
查看>>
项目优化经验分享(七)敏捷开发
查看>>
如何解决adb devices 端口被占用的问题zz
查看>>
Maven学习总结(四)——Maven核心概念——转载
查看>>
实践一些js中的prototype, __proto__, constructor
查看>>
怎么用CIFilter给图片加上各种各样的滤镜_2
查看>>
android:关于主工程和library project
查看>>
CodeForces 2A Winner
查看>>
Window环境配置Mongodb
查看>>
Project facet Java version 1.8 is not supported解决记录
查看>>
制作和unity调用动态链接库dll文件
查看>>
N-Gram
查看>>
操作可能会破坏运行时稳定性的解决办法
查看>>
exsi6.0远程修改密码
查看>>
Header和Cookie相关内容
查看>>