In order to learn the source code of Vue, I decided to write a simplified version of Vue myself. Now I share what I’ve learned. If you’re using Vue but don’t know how it works, or if you’re looking to read the source code for Vue, hopefully these shares will help you understand how Vue works.
The target
Through the previous practice, we have realized the monitoring of data changes and template parsing, today we will combine the two, complete the browser side of the rendering work.
Vue class
First, let’s write the class: Vue.
The Vue constructor will take multiple arguments, including:
- El: This will be the parent node for instance rendering.
- Data: a function that returns an object/array as the instance’s data.
- TPL: Template string for the instance.
- Methods: instance methods.
In the constructor, we will first set the root element to $el, then call parseHtml and generateRender we wrote earlier, and finally generate the Function instance as our render Function, while using proxy to create observable data:
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);
// setdata this.data = proxy(data.call(this)); . }... }Copy the code
Here, we will use proxy again to create a proxy. In Vue, for example, the data method creates data like {a: 1} that can be accessed through this.a instead of something like this.data.a. To support such a simple access to the data, we want to provide an object, while providing access to data, and other content such as the access method, while maintaining the proxy Settings for new key/value pair of flexibility, so I here’s approach is to create a new proxy, it will preferential access to the instance data, if the data is not present, Access methods, etc.
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(! (keyin target.data) && key in target) {
target[key] = value;
} else {
target.data[key] = value;
}
return true;
},
has(target, key) {
return (key in target) || (key intarget.data); }}); this._proxyObj = proxyObj;Copy the code
Next, we bind methods from Methods to instances:
Object.keys(methods).forEach((key) => {
this[key] = methods[key].bind(proxyObj);
});
Copy the code
Finally, we call the Watch method, and the passed evaluation function updateComponent does the rendering and collects dependencies to re-render if the data changes:
const updateComponent = () => {
this._update(this._render());
};
watch(updateComponent, () => {/* noop */});
Copy the code
Rendering and v – dom
The _render method calls render to create a tree of vNodes, or 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) {
returnnew VNode(null, text, null, null); }}Copy the code
The _update method determines whether to create or patch based on whether the old V-DOM has been created, and then we need to save the created V-DOM for subsequent comparison updates:
_update(vNode) {
const preVode = this.preVode;
if (preVode) {
patch(preVode, vNode);
} else {
this.preVode = vNode;
this.$el.appendChild(build(vNode)); }}Copy the code
The creation process iterates through the V-DOM, using document.createTextNode and Document. createElement to create the DOM element and store it on the VNode for later updates:
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'}) {// Here anonymous functions are always unequalif (attrs['v-on-click']) $el.onclick = attrs['v-on-click']; }};Copy the code
Since we don’t yet support v-if, V-for, or Component components, etc., we can assume that the updated V-DOM is structurally consistent, which greatly simplifies the process of comparing updates. We just need to traverse the old and new V-Dom and pass in the corresponding old and new VNodes in the patch method. If there are different attributes, we can update them:
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{// Because the structure is the same, so do not worry about}};Copy the code
Finally, we expose a method that returns the _proxyObj object bound to the newly created Vue instance. We can use this object to change instance data or call instance methods, etc:
Vue.new = function (opts) {
return new Vue(opts)._proxyObj;
};
Copy the code
conclusion
We completed data listening, template parsing and finally rendering through three practices. Of course, this is a very rudimentary demo, with limited fault tolerance and limited functionality.
Maybe I’ll update this series to include support for computed attributes, component support, directive support for v-if, V-for, and V-model, template, keep-alive, and component, and so on.
Finally, thank you for reading this article, and I hope it helped you understand some of the principles of Vue.
Reference:
- Vue