此前为了学习Vue的源码,我决定本身动手写一遍简化版的Vue。如今我将我所了解到的分享出来。若是你正在使用Vue但还不了解它的原理,或者正打算阅读Vue的源码,但愿这些分享能对你了解Vue的运行原理有所帮助。vue
经过以前的实践,咱们已经实现了数据变更的监听与模板的解析,今天咱们就将把二者结合起来,完成浏览器端的渲染工做。git
首先咱们来编写类:Vue
。github
Vue
的构造函数将接受多个参数,包括:数组
在构造函数中,咱们将先设定根元素为$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
,它会优先访问实例的数据,若是数据不存在,再来访问方法等: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 */});
复制代码
_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.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
的一部分原理。
参考: