两百行代码实现简易版本的vue框架

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 方法绑定相同的监听函数。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值