function EventBus() {
this._events = {};
}
EventBus.prototype.on = function(eventName, callback) {
if (!this._events[eventName]) {
this._events[eventName] = [];
}
this._events[eventName].push(callback);
};
EventBus.prototype.emit = function(eventName, payload) {
if (this._events[eventName]) {
this._events[eventName].forEach(function(callback) {
callback(payload);
});
}
};
function Vue(options) {
this.$options = options;
if (typeof options.beforeCreate === 'function') {
options.beforeCreate.call(this);
}
this._data = typeof options.data === 'function' ? options.data() : options.data;
this._eventBus = new EventBus();
this._proxyData();
this._proxyMethods();
this._createComputed();
this._createWatchers();
if (typeof options.created === 'function') {
options.created.call(this);
}
this.$mount();
}
Vue.prototype.$render = function() {
if (typeof this.$options.render === 'function' && this.$options.el) {
this.$el = document.querySelector(this.$options.el);
this.$el.innerHTML = this.$options.render.call(this);
} else {
this._compileTemplate();
this._proxyComponents();
}
};
Vue.prototype.$mount = function() {
if (typeof this.$options.beforeMount === 'function') {
this.$options.beforeMount.call(this);
}
this.$render();
if (typeof this.$options.mounted === 'function') {
this.$options.mounted.call(this);
}
};
Vue.prototype._proxyData = function() {
var self = this;
Object.keys(this._data).forEach(function(key) {
Object.defineProperty(self, key, {
get: function() {
return self._data[key];
},
set: function(newValue) {
self._data[key] = newValue;
if (typeof self.$options.beforeUpdate === 'function') {
self.$options.beforeUpdate.call(self);
}
self.$render();
if (typeof self.$options.updated === 'function') {
self.$options.updated.call(self);
}
}
});
});
};
Vue.prototype._createComputed = function() {
var self = this;
var computed = this.$options.computed || {};
Object.keys(computed).forEach(function(key) {
Object.defineProperty(self, key, {
get: function() {
return computed[key].call(self);
}
});
});
};
Vue.prototype._createWatchers = function() {
var self = this;
var watch = this.$options.watch || {};
Object.keys(watch).forEach(function(key) {
var callback = watch[key]
var value = self._data[key];
Object.defineProperty(self._data, key, {
get: function() {
return value;
},
set: function(newValue) {
var oldValue = value
value = newValue;
callback.call(self, newValue, oldValue);
}
});
});
};
Vue.prototype._proxyMethods = function() {
var self = this;
var methods = this.$options.methods || {};
Object.keys(methods).forEach(function(key) {
self[key] = methods[key].bind(self);
});
};
Vue.prototype.$emit = function(eventName, payload) {
this._eventBus.emit(eventName, payload);
};
Vue.prototype.$on = function(eventName, callback) {
this._eventBus.on(eventName, callback);
};
Vue.prototype._compileTemplate = function() {
var self = this;
var el = this.$options.el
var template = this.$options.template || '';
var evalExpression = function(expression) {
with (self) return eval(expression);
}
var compiledTemplate = template.replace(/\{\{(.*?)\}\}/g, function(match, expression) {
var value = evalExpression(expression);
return value !== undefined ? value : '';
});
var element = el ? document.querySelector(el) : document.createElement('div');
element.innerHTML = compiledTemplate.trim();
this.$el = el ? element : element.childNodes[0];
this._handleDirective()
};
Vue.prototype._handleDirective = function() {
var self = this;
this.$el.querySelectorAll('[v-model]').forEach(function(element) {
var value = element.getAttribute('v-model');
element.value = self._data[value];
element.addEventListener('input', function(event) {
self._data[value] = event.target.value;
self.$emit(`update:${value}`, event.target.value);
});
});
this.$el.querySelectorAll('[v-text]').forEach(function(element) {
var value = element.getAttribute('v-text');
element.textContent = self._data[value];
self.$on(`update:${value}`, function(newValue) {
element.textContent = newValue;
});
});
};
Vue.prototype._proxyComponents = function() {
var self = this;
var components = this.$options.components || {};
Object.keys(components).forEach(function(componentName) {
var component = self[componentName] || new Vue(components[componentName]);
var isNewComponent = typeof self[componentName] === 'undefined';
self[componentName] = component;
self.$el.querySelectorAll(componentName).forEach(function(element) {
component.$el.querySelectorAll('slot').forEach(function(slot) {
slot.innerHTML = element.innerHTML;
});
element.innerHTML = component.$el.outerHTML;
isNewComponent && component.$options?.emits.forEach(function(event) {
var method = element.getAttribute('v-on:' + event);
if (typeof self[method] === 'function') {
component.$on(event, self[method]);
}
});
});
});
};
在 Vue
的构造函数里,我们做了几件事:处理生命周期钩子函数、创建 EventBus
实例、使用 _proxyData
、_proxyMethods
、_createComputed
、_createWatchers
方法将数据对象的属性、方法、计算属性、监听器代理或绑定到 Vue 实例上。
然后再调用 $mount
方法挂载组件,触发生命周期钩子函数并执行 $render
方法。在 $render
方法中,执行用户自定义的渲染函数,或者使用 _compileTemplate
、_proxyComponents
方法编译模板和解析子组件。
在 _proxyData
方法中,我们使用 Object.defineProperty
将数据对象的属性代理到 Vue
实例上,并在属性的 set
方法中触发 beforeUpdate
、 $render
和 updated
钩子,意味着只要数据对象的属性发生变化,就会触发视图更新。
在 _createComputed
方法中,我们通过遍历 computed
对象,为每个计算属性定义了 get
方法,使其能够被当做普通属性使用。
在 _createWatchers
方法中,我们通过遍历 watch
对象,为每个属性使用 Object.defineProperty
监听 _data
对象中该属性的变化,并在变化时触发回调函数。注意:在 set
方法中,与之前相比我们新增了 oldValue
参数。
在 _proxyMethods
方法中,我们将配置对象中的方法绑定到 Vue 实例上,以便在实例中可以直接访问和调用这些方法。
在 Vue
原型中,我们定义了 $emit
和 $on
方法。 $emit
方法用于抛出事件,接收两个参数:事件名和可选的数据载荷。 $on
方法用于监听事件,接收两个参数:事件名和回调函数。
在 _compileTemplate
方法中,我们首先获取配置对象中的模板字符串,并使用正则表达式匹配 {{ expression }}
的部分。然后,我们使用 eval
函数根据表达式动态求值,将值替换回模板字符串中。接下来,我们根据配置对象中的 el
属性获取对应的 DOM 元素,如果 DOM 元素不存在,我们就创建一个 div
元素代替,然后再将编译后的模板字符串赋值给该元素的 innerHTML
属性。接着给 Vue 实例设置 $el
属性并且调用 _handleDirective
方法处理指令。注意:前面如果用 div
元素代替,则需通过 childNodes[0]
排除该 div
元素。
在 _handleDirective
方法,我们通过 querySelectorAll
方法获取所有具有 v-model
属性的元素,并遍历每个元素。在遍历过程中,我们解析 model
指令,将元素的值设置为对应的数据属性值,并添加 input
事件监听器。注意:在 addEventListener
方法中,与之前相比我们新增了 $emit
动作,用来触发 update:inputValue
事件,从而实现 inputValue
完整的数据双向绑定。
接着,我们通过 querySelectorAll
方法获取所有具有 v-text
属性的元素,并遍历每个元素。在遍历过程中,我们解析 text
指令,将元素的文本内容设置为对应的数据属性值。注意:与之前相比我们新增了 $on
动作,用来监听 update:inputValue
事件,让文本内容随着 inputValue
的值变化而变化。
在 _proxyComponents
方法中,我们首先获取配置对象中的组件声明,然后遍历所有的组件,根据组件名称获取组件对象,创建该对象的 Vue
实例。注意:与之前相比我们会保存该对象到实例上,并优先从实例中获取已经创建好的对象。接着通过该实例的 $el
属性,遍历所有 slot
插槽,将原始的 innerHTML
设置为插槽的内容,并重新设置组件的 innerHTML
为实例 $el
元素的 outerHTML
内容。
最后,我们还新增了 v-on
的组件监听事件功能。首先,我们从组件配置对象里的 emits
数组获取组件抛出的所有事件名称,然后遍历该数组,判断 app 应用是否监听了该事件,如果从 app 应用的 self[method]
找到对应的监听函数,则给组件通过 $on
方法绑定该监听函数。注意:由于组件更新会触发多次 _proxyComponents
方法,因此必须判断 isNewComponent
是否为新创建的组件,防止重复用 $on
方法绑定相同的监听函数。