内容简介:通过之前的实践,我们已经实现了数据变动的监听与模板的解析,今天我们就将把两者结合起来,完成浏览器端的渲染工作。首先我们来编写类:
通过之前的实践,我们已经实现了数据变动的监听与模板的解析,今天我们就将把两者结合起来,完成浏览器端的渲染工作。
Vue类
首先我们来编写类: Vue
。
Vue
的构造函数将接受多个参数,包括:
- el:实例的渲染将以此作为父节点。
- data:一个函数,运行后将返回一个对象/数组,作为实例的数据。
- tpl: 实例的模板字符串。
- methods:实例的方法。
在构造函数中,我们将先设定根元素为 $el
,然后调用我们之前写的 parseHtml
和 generateRender
并最终生成 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.createTextNode
和 document.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-if
、 v-for
或 component
组件等等,因此我们可以认为更新后的 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-if
、 v-for
和 v-model
等directive的支持、 template
、 keep-alive
与 component
等组件,等等。
最后谢谢您阅读本文,希望有帮助到您理解 Vue
的一部分原理。
参考:
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- iOS 渲染框架
- 如何选择正确的后端渲染框架:Next, Nuxt, Nest?
- Next.js 6.0.0 发布,React 服务器端渲染框架
- Next.js 6.0.1 发布,React 应用的后端渲染框架
- Next.js 6.0.2 发布,React 应用的后端渲染框架
- Next.js 6.1.1 发布,React 应用的后端渲染框架
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。