关于前端:ChatGPT教我用200行代码写一个简版Vue框架

5次阅读

共计 27146 个字符,预计需要花费 68 分钟才能阅读完成。

AI 是将来最好的老师

最近,我正在筹备一份对于 Vue 根底的学习材料。期间我突发奇想:是否利用当初热门的 ChatGPT 帮我创立学习内容?其实 Vue 自身不难学,特地是根底用法,然而,如果你想深刻把握 Vue,就要通过浏览 Vue 的源码来理解其原理。然而,不是每个人都有足够的工夫和急躁浏览 Vue 的源码。于是我花了一个多小时与 ChatGPT 对话,疏导它生成一些展现 Vue 原理的示例代码。在这个过程中,我只是充当一个提问者的角色,让 ChatGPT 依据我的问题生成我想要的代码。后果让我有些惊喜,但也不齐全出其不意。鉴于目前 AI 疾速倒退的趋势,我置信到了 2024 年,将会有很多职业被 AI 所代替,只管那时 AI 还不能齐全取代老师,然而它有后劲成为一个私人辅教工具。我置信,将来并不是 AI 取代人类,而是懂得用 AI 的人取代不懂用 AI 的人。接下来,就让咱们一起见识这位 AI 助教的程度吧。

本篇文章次要讲以下内容:

  • 让 AI 列出 Vue 从入门到高级的学习纲要
  • 尝试让 AI 给出能解释 Vue 原理的代码示例
  • 开始学习 Vue 生命周期的原理
  • 学习 Vue 模板语法和指令的原理
  • 学习 Vue 数据监听和计算属性的原理
  • 学习 Vue 事件处理和办法的原理
  • 学习 Vue 插槽(slot)的原理
  • 用 200 行代码实现一个简版 Vue 框架

让 AI 列出学习 Vue 的纲要

为了系统地学习 Vue,须要先制订一个学习纲要。联合我对 ChatGPT 的理解,给它提的第一个问题,就失去比较满意的回复,以下就是我提的问题原文:

请列出一个 Vue.js 从入门到高级的学习纲要

ChatGPT 的答复如下:

当波及 Vue.js 的学习纲要时,上面是一个从入门到高级的学习路线,涵盖了 Vue.js 的外围概念和高级主题:

  1. HTML、CSS 和 JavaScript 根底

    • HTML 元素和标签
    • CSS 选择器和款式
    • JavaScript 根本语法和概念
  2. Vue.js 入门

    • Vue.js 的介绍和特点
    • Vue.js 的装置和设置
    • Vue 实例和生命周期钩子
    • Vue 模板语法和指令
    • 数据绑定和计算属性
    • 事件处理和办法
  3. Vue 组件

    • 组件的基本概念和组成
    • 组件的注册和应用
    • 组件的通信形式(props、事件和 vuex)
    • 动静组件和异步组件
    • 插槽(slot)和作用域插槽
  4. Vue 路由

    • Vue Router 的装置和配置
    • 路由的根本用法(导航、参数传递)
    • 嵌套路由和命名路由
    • 路由守卫和导航解析
  5. Vue 状态治理(Vuex)

    • Vuex 的外围概念和应用场景
    • Vuex 的装置和配置
    • State、Mutation、Action 和 Getter
    • 模块化和命名空间
  6. Vue 进阶

    • 自定义指令
    • 过滤器(Filter)
    • 混入(Mixin)
    • 插件(Plugin)
    • 自定义过渡和动画
    • 服务器端渲染(SSR)
  7. Vue 与内部库的集成

    • 应用 Axios 进行 HTTP 申请
    • 应用 Vue 与第三方库(如 Chart.js、Google Maps 等)进行集成
    • 应用 Vue 和 Webpack 进行我的项目构建
  8. Vue 最佳实际和性能优化

    • 组件的拆分和复用
    • 正当应用计算属性和侦听器
    • 防止不必要的渲染
    • 异步组件和代码拆分
    • 优化网络申请和数据加载
  9. Vue 测试

    • 单元测试和端到端测试的概念
    • 应用 Vue Test Utils 进行单元测试
    • 应用 Cypress 进行端到端测试
  10. Vue 我的项目实战

    • 基于 Vue.js 构建一个残缺的应用程序
    • 应用 Vue Router 和 Vuex 进行状态治理
    • 集成其余库和工具
    • 测试和优化应用程序

请留神,这个学习纲要只是一个领导,你能够依据本人的学习进度和需要进行调整和扩大。此外,学习 Vue.js 最好通过实际我的项目和浏览官网文档来加深了解和把握。

以上内容是 ChatGPT 生成的,它具备肯定的普适性,但我并不筹备依照这个纲要来讲,我心愿大家通过浏览 Vue 的源码,来学习和把握 Vue 的用法。比方,很多人都感觉 Vue 数据双向绑定的用法很简略,但要让本人来模仿实现数据双向绑定却不简略。学习 Vue 的用法须要把握其原理次要有以下四个起因(这些起因也是由 ChatGPT 生成的):

  • 深刻了解框架 :通过学习 Vue 的原理,你将更好地了解 Vue 框架的设计和工作原理。这包含 Vue 的响应式数据绑定、虚构 DOM、组件化架构等外围概念。了解这些原理能够帮忙你更好地应用 Vue,更高效地开发和调试应用程序。
  • 自定义扩大和优化 :把握 Vue 的原理使你可能进行自定义扩大和优化。你能够依据我的项目需要自定义 Vue 的指令、插件、过滤器等,以及优化性能、缩小资源耗费等方面。了解原理能够让你更灵便地应答各种开发场景。
  • 故障排查和调试 :在开发过程中,难免会遇到一些问题和谬误。如果你理解 Vue 的原理,你能够更快地定位和解决问题。了解原理能够帮忙你深刻理解 Vue 的外部机制,辨认潜在的谬误起源,并采取正确的解决办法。
  • 跨框架学习和迁徙 :把握 Vue 的原理能够为你日后学习其余前端框架奠定根底。许多前端框架在概念和原理上有相似之处,因而,一旦你把握了 Vue 的原理,你将更容易了解和学习其余框架,如 React、Angular 等。此外,如果你须要迁徙我的项目到其余框架,了解原理能够帮忙你更好地了解新框架的工作形式。

让 AI 给出解释 Vue 原理的代码示例

该怎么向 ChatGPT 发问,能力让它生成模仿实现 Vue 某个个性的示例代码?依照以往教训,起初我是这么发问的:

请给一个简略的 JS 代码实现的例子,阐明 Vue 的生命周期及生命周期钩子的实现原理

ChatGPT 的答复如下:

以下是一个简略的 JavaScript 代码示例,展现了 Vue 的生命周期及生命周期钩子的实现原理:

<!DOCTYPE html>
<html>
<head>
  <title>Vue Lifecycle Example</title>
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
  <div id="app">
    <p>{{message}}</p>
    <button @click="updateMessage">Update Message</button>
  </div>

  <script>
    var app = new Vue({
      el: '#app',
      data: {message: 'Hello, Vue!'},
      beforeCreate: function() {console.log('beforeCreate hook');
      },
      created: function() {console.log('created hook');
      },
      beforeMount: function() {console.log('beforeMount hook');
      },
      mounted: function() {console.log('mounted hook');
      },
      beforeUpdate: function() {console.log('beforeUpdate hook');
      },
      updated: function() {console.log('updated hook');
      },
      beforeDestroy: function() {console.log('beforeDestroy hook');
      },
      destroyed: function() {console.log('destroyed hook');
      },
      methods: {updateMessage: function() {this.message = 'Updated message';}
      }
    });
  </script>
</body>
</html>

显然,这只是 Vue 生命周期的用法示例,并不是咱们想要的模仿 Vue 生命周期原理的示例。所以,把握 AI 的提醒技巧很重要。将来,AI 提醒师可能会成为一种职业,学好 AI 提醒技巧就像当下应用智能手机一样,将成为一种生存技能。


用 AI 学习 Vue 生命周期的原理

后面的代码曾经让咱们理解如何应用 Vue 的生命周期。为了便于模仿,咱们应用 Vue 的选项式 API,并且对生命周期的用法做一点调整,假如模仿用法的示例代码如下所示:

var app = new Vue({
  el: '#app',
  data: {message: 'Hello, Vue!'},
  beforeCreate: function() {console.log('beforeCreate hook');
  },
  created: function() {console.log('created hook');
  },
  beforeMount: function() {console.log('beforeMount hook');
  },
  mounted: function() {console.log('mounted hook');
  },
  beforeUpdate: function() {console.log('beforeUpdate hook');
  },
  updated: function() {console.log('updated hook');
  },
  render: function() {return '<p>' + this.message + '</p>';}
});

咱们调整的中央,就是用 render 办法代替 methods,目标是将 render 办法返回的内容渲染到 ID 为 app 的 DOM 元素上。在 render 办法里会获取 this.message 的内容,该内容为在 data 对象里定义的 message 属性的值。

大家先思考一下如何实现上述性能,重点在于实现 Vue 配置对象的 el、data、beforeCreate、created、beforeMount、mounted、beforeUpdate、updated、render 属性。

接下来咱们看 ChatGPT 怎么实现上述属性,以下是我给 ChatGPT 的提醒语:

请给出一段代码,模仿实现 Vue.js 相似生命周期成果的示例代码

ChatGPT 的答复如下:

以下是一个简略的 JavaScript 代码示例,模仿实现相似 Vue.js 生命周期的成果:

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._proxyData();

  if (typeof options.created === 'function') {options.created.call(this);
  }

  this.$mount(options.el);
}

Vue.prototype.$mount = function(el) {this.$el = document.querySelector(el);

  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.render = function() {if (typeof this.$options.render === 'function') {this.$el.innerHTML = this.$options.render.call(this);
  }
};

// 应用示例
var app = new Vue({
  el: '#app',
  data: {message: 'Hello, Vue!'},
  beforeCreate: function() {console.log('beforeCreate hook');
  },
  created: function() {console.log('created hook');
  },
  beforeMount: function() {console.log('beforeMount hook');
  },
  mounted: function() {console.log('mounted hook');
  },
  beforeUpdate: function() {console.log('beforeUpdate hook');
  },
  updated: function() {console.log('updated hook');
  },
  render: function() {return '<p>' + this.message + '</p>';}
});

在这个示例中,咱们通过定义一个 Vue 构造函数来模仿 Vue.js 的生命周期。构造函数接管一个配置对象作为参数,并在实例化过程中触发各个生命周期钩子函数。

在构造函数中,咱们首先判断配置对象中是否定义了 beforeCreatecreated 钩子函数,并在相应的机会调用。而后,咱们解决数据对象,读取配置对象里的 data 属性,如果 data 是函数则执行获取其返回值,如果是对象则间接赋值,而后通过 _proxyData 办法将数据对象中的属性代理到 Vue 实例上,以便实现数据的双向绑定。

接下来,在 $mount 办法中,咱们判断配置对象中是否定义了 beforeMountmounted 钩子函数,并在相应的机会调用。在 $mount 办法中,咱们调用 render 办法将模板渲染到指定的 DOM 元素上。

_proxyData 办法中,咱们应用 Object.defineProperty 将数据对象的属性代理到 Vue 实例上,并在属性的 set 办法中触发 beforeUpdaterenderupdated 钩子,意味着只有数据对象的属性发生变化,就会触发视图更新。

render 办法中,咱们判断配置对象中是否定义 render 函数,通过该函数获取渲染的内容,再赋值给 $el 元素的 innerHTML 属性,其中 $el 元素在 $mount 办法中应用 document.querySelector 初始化。

最初,咱们应用示例中的配置对象来实例化 Vue,通过模仿渲染触发生命周期钩子函数,实现相似 Vue.js 的各个生命周期的成果。

上述针对代码的解释也是由 ChatGPT 生成的,我只是补充了一些阐明。可见 ChatGPT 这方面能力曾经不亚于业余私教,它不仅节俭我很多备课工夫,甚至某些局部比我讲得还好。

以上 JavaScript 代码还不能间接运行,咱们将该段代码保留为 lifecycle.js 文件,而后创立一个 index.html 网页并援用该文件,网页代码如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vue</title>
  </head>
  <body>
    <div id="app"></div>
    <script src="./lifecycle.js"></script>
  </body>
</html>

这样,咱们间接关上 index.html 就能运行 lifecycle.js,而后在浏览器查看渲染后果,并在控制台查看日志输入。另外,咱们能够在管制输出 app.message = 'ChatGPT' 来验证数据绑定以及页面更新机制。


用 AI 学习 Vue 模板语法和指令的原理

依照下面的学习门路,咱们在学习模板语法和指令之前,同样先给出便于模仿的用法示例代码:

var app = new Vue({
  el: '#app',
  data: {
    message: 'Hello, Vue!',
    inputValue: 'ChatGPT'
  },
  template: `
    <div>
      <p>{{message}}</p>
      <input v-model="inputValue" type="text">
      <p v-text="inputValue"></p>
    </div>
  `
});

咱们重点关注以下波及模板和指令的 HTML 片段:

<div>
  <p>{{message}}</p>
  <input v-model="inputValue" type="text">
  <p v-text="inputValue"></p>
</div>

这个 HTML 片段自身就是模板的内容,外面应用了一个模板语法和两个指令,模板语法是 {{message}},指令别离是 v-modelv-text

同样的,大家先思考一下本人如何实现,而后再看 ChatGPT 怎么实现模板语法和指令,以下是我给的提醒语:

请给出一段代码,模仿实现 Vue.js 相似模板语法和指令的示例代码

ChatGPT 的答复如下:

以下是一个简略的 JavaScript 代码示例,模仿实现相似 Vue.js 的模板语法和指令:

function Vue(options) {
  this.$options = options;
  this._data = typeof options.data === 'function' ? options.data() : options.data;
  this._compileTemplate();}

Vue.prototype._compileTemplate = function() {
  var self = this;
  var template = this.$options.template || '';

  var evalExpression = function(expression) {with (self._data) return eval(expression);
  }

  var compiledTemplate = template.replace(/\{\{(.*?)\}\}/g, function(match, expression) {var value = evalExpression(expression);
    return value !== undefined ? value : '';
  });

  var element = document.querySelector(this.$options.el);
  element.innerHTML = compiledTemplate.trim();

  element.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;
    });
  });

  element.querySelectorAll('[v-text]').forEach(function(element) {var value = element.getAttribute('v-text');
    element.textContent = self._data[value];
  });
};

// 应用示例
var app = new Vue({
  el: '#app',
  data: {
    message: 'Hello, Vue!',
    inputValue: 'ChatGPT'
  },
  template: `
    <div>
      <p>{{message}}</p>
      <input v-model="inputValue" type="text">
      <p v-text="inputValue"></p>
    </div>
  `
});

在这个示例中,咱们通过定义一个 Vue 构造函数来模仿 Vue.js 的模板语法和指令。构造函数接管一个配置对象作为参数,并在实例化过程中编译模板。

_compileTemplate 办法中,咱们首先获取配置对象中的模板字符串,并应用正则表达式匹配 {{expression}} 的局部。而后,咱们应用 eval 函数依据表达式动静求值,将值替换回模板字符串中。接下来,咱们依据配置对象中的 el 属性获取对应的 DOM 元素,并将编译后的模板字符串赋值给该元素的 innerHTML 属性。

而后,咱们通过 querySelectorAll 办法获取所有具备 v-model 属性的元素,并遍历每个元素。在遍历过程中,咱们解析 model 指令,将元素的值设置为对应的数据属性值,并增加 input 事件监听器,以实现 inputValue 简略的双向绑定。

接着,咱们通过 querySelectorAll 办法获取所有具备 v-text 属性的元素,并遍历每个元素。在遍历过程中,咱们解析 text 指令,将元素的文本内容设置为对应的数据属性值。留神:该指令咱们没有实现 inputValue 的双向绑定。

最初,咱们应用示例中的配置对象来实例化 Vue,并传入模板字符串。通过模仿解析模板和执行指令,咱们实现了相似 Vue.js 的模板语法和指令的成果,要查看成果请在浏览器中关上 index.html 页面。

以上代码并不齐全是 ChatGPT 生成的,因为在调试过程中,发现两个问题须要修改能力失常运行:

  • 第一个问题是 ChatGPT 想间接应用 eval 函数依据表达式动静求值,ChatGPT 生成的代码为 eval(expression),其中 expression 为字符串 "message",请留神 message 两边有空格。间接调用 eval 函数获取 message 的值会报错,因为全局对象里并没有定义 message 属性,而表达式 {{message}} 的本意是获取 this._data 对象的 message 属性值。为此我增加了一个 evalExpression 函数,通过 with 语句将 this._data 设置为全局对象,这样能力正确获取 this._data.message 的值。但 with 语句在严格模式下不倡议应用,因而并不是最优解,有趣味的同学能够钻研一下最优解。
  • 第二个问题是 ChatGPT 想应用 querySelectorAll('[v-]') 同时获取 v-modelv-text 指令的内容,但理论调试过程中并不能获取。于是我拆解成两个语句:querySelectorAll('[v-model]')querySelectorAll('[v-text]') 别离获取指令内容。

由此可见,咱们还不能实现信赖 ChatGPT 生成的代码。如果发现代码有问题,你能够向它提出质疑,它会说道歉而后从新生成新的代码,更新后的代码是否正确,仍然须要你再次验证。


用 AI 学习 Vue 数据监听和计算属性的原理

接下来要学习的仍是比拟根底的数据监听和计算属性,其实后面在生命周期的例子里曾经蕴含了数据双向绑定,数据监听和计算属性的原理跟数据双向绑定很相似。

同样的,咱们先给出便于模仿的用法示例代码:

var app = new Vue({
  data: {
    message: 'Hello, Vue!',
    firstName: 'John',
    lastName: 'Doe'
  },
  computed: {fullName: function() {return this.firstName + ' ' + this.lastName;}
  },
  watch: {message: function(newValue) {console.log('Message changed:', newValue);
    }
  }
});

console.log(app.message);       // Output: Hello, Vue!
app.message = 'Hello, Vue.js!'; // Output: Message changed: Hello, Vue.js!
console.log(app.message);       // Output: Hello, Vue.js!
console.log(app.fullName);      // Output: John Doe
app.message = 'New message';    // Output: Message changed: New message

从示例代码能够看出,配置对象新增了 computedwatch 属性,别离定义了计算属性 fullName,以及监听 message 属性变动的函数。

大家先思考一下本人如何实现,而后再看 ChatGPT 怎么实现数据监听和计算属性,以下是我给的提醒语:

请给出一段代码,模仿实现 Vue.js 相似数据监听和计算属性的示例代码

ChatGPT 的答复如下:

要模仿实现 Vue.js 相似数据监听和计算属性的性能,能够应用 Object.defineProperty 来定义属性的 get 和 set 办法,并应用 watch 函数来监听属性的变动。以下是一个示例代码:

function Vue(options) {
  this._data = options.data;
  this._computed = options.computed;
  this._watch = options.watch;

  this._proxyData();
  this._createComputed();
  this._createWatchers();}

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;
      }
    });
  });
};

Vue.prototype._createComputed = function() {
  var self = this;
  var computed = this._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._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) {
        value = newValue;
        callback.call(self, newValue);
      }
    });
  });
};

// 应用示例
var app = new Vue({
  data: {
    message: 'Hello, Vue!',
    firstName: 'John',
    lastName: 'Doe'
  },
  computed: {fullName: function() {return this.firstName + ' ' + this.lastName;}
  },
  watch: {message: function(newValue) {console.log('Message changed:', newValue);
    }
  }
});

console.log(app.message);       // Output: Hello, Vue!
app.message = 'Hello, Vue.js!'; // Output: Message changed: Hello, Vue.js!
console.log(app.message);       // Output: Hello, Vue.js!
console.log(app.fullName);      // Output: John Doe
app.message = 'New message';    // Output: Message changed: New message

在这个示例中,咱们定义了一个 Vue 构造函数,它接管一个蕴含 datacomputedwatch 的选项对象。

_proxyData 办法中,咱们应用 Object.definePropertyVue 实例上的属性定义了 getset 办法,将其代理到 _data 对象上。

_createComputed 办法中,咱们通过遍历 computed 对象,为每个计算属性定义了 get 办法,使其可能被当做一般属性应用。

_createWatchers 办法中,咱们通过遍历 watch 对象,为每个属性应用 Object.defineProperty 监听 _data 对象中该属性的变动,并在变动时触发回调函数。

最初,咱们应用示例中的配置对象来实例化 Vue,并传入计算属性和监听属性的函数,通过执行一段脚本读取和更改配置属性的值,实现了相似 Vue.js 的数据监听和计算属性的成果。

与之前一样,以上代码并不齐全是 ChatGPT 生成的。在原始生成的代码中,监听属性须要执行 app.$watch('message', ...) 这样的语句,即调用 app$watch 办法来增加监听函数。修改后的监听形式更合乎 Vue 的官网用法。


用 AI 学习 Vue 事件处理和办法的原理

事件处理在组件开发过程中是很重要的一环,接下来咱们模仿实现 Vue 的事件处理和办法的机制,以下是便于模仿的用法示例代码:

var app = new Vue({
  data: {message: 'Hello, Vue!'},
  methods: {greet: function() {this.$emit('greet', this.message);
    },
    updateMessage: function(newMessage) {this.message = newMessage;}
  },
});

app.$on('greet', function(message) {console.log('Greet:', message);
});

app.greet(); // Output: Greet: Hello, Vue!
app.updateMessage('Hello, World!');
app.greet(); // Output: Greet: Hello, World!

从示例代码能够看出,配置对象新增了 methods 属性,用于定义实例的办法,同时新增内置的 $emit$on 办法,用于抛出事件和监听事件。

大家先思考一下本人如何实现,而后再看 ChatGPT 怎么实现事件处理和办法,以下是我给的提醒语:

请给出一段代码,模仿实现 Vue.js 相似事件处理和办法的示例代码

ChatGPT 的答复如下:

以下是一个简略的 JavaScript 代码示例,模仿实现相似 Vue.js 的事件处理和办法:

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._data = typeof options.data === 'function' ? options.data() : options.data;
  this._methods = options.methods;
  this._eventBus = new EventBus();

  this._proxyData();
  this._proxyMethods();}

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;
      }
    });
  });
};

Vue.prototype._proxyMethods = function() {
  var self = this;
  var methods = this._methods;
  if (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);
};

// 应用示例
var app = new Vue({
  data: {message: 'Hello, Vue!'},
  methods: {greet: function() {this.$emit('greet', this.message);
    },
    updateMessage: function(newMessage) {this.message = newMessage;}
  },
});

app.$on('greet', function(message) {console.log('Greet:', message);
});

app.greet(); // Output: Greet: Hello, Vue!
app.updateMessage('Hello, World!');
app.greet(); // Output: Greet: Hello, World!

在这个示例中,咱们首先应用 EventBus 对象来模仿事件总线。EventBus 对象具备 on 办法用于监听事件和 emit 办法用于触发事件。

Vue 构造函数中,咱们创立了一个 EventBus 实例并将其赋值给 _eventBus 属性。

_proxyData 办法中,咱们应用 Object.defineProperty 将数据对象的属性代理到 Vue 实例上,以便实现数据的双向绑定。

_proxyMethods 办法中,咱们将配置对象中的办法绑定到 Vue 实例上,以便在实例中能够间接拜访和调用这些办法。

Vue 原型中,咱们定义了 $emit$on 办法。$emit 办法用于抛出事件,接管两个参数:事件名和可选的数据载荷。$on 办法用于监听事件,接管两个参数:事件名和回调函数。

最初,咱们创立了一个 Vue 实例,先在 Vue 的配置对象的 greet 办法应用 $emit 办法触发 greet 事件,以便触发回调函数并输入音讯。而后,在实例中应用 $on 办法监听 greet 事件,并且定义回调函数来输入音讯。通过这种形式,咱们模仿实现了相似 Vue.js 中抛出事件和接管事件的性能。

与之前一样,以上代码并不齐全是 ChatGPT 生成的。在原始生成的代码中,并没有模仿实现抛出事件和接管事件。我从新给出新的提醒语,ChatGPT 才生成 EventBus 的代码,而后我再将两段代码交融在一起,才失去下面的示例代码。


用 AI 学习 Vue 插槽(slot)的原理

最初,咱们来学习 Vue 的插槽。为了模仿插槽的用法,咱们还得先模仿 Vue 的组件申明,在组件里定义插槽,而后通过模板引入组件,并设置组件插槽的内容,以下是便于模仿的用法示例代码:

var HelloComponent = {data: function() {
    return {name: 'John'};
  },
  template: `
    <div>
      <h1>{{name}}</h1>
      <slot></slot>
    </div>
  `
};

var app = new Vue({
  el: '#app',
  data: {message: 'Hello, Vue!'},
  components: {HelloComponent},
  template: `
    <HelloComponent>
      <p>{{message}}</p>
    </HelloComponent>
  `
});

上述示例代码中,咱们申明了一个 HelloComponent 的组件,在该组件的模板中,咱们增加了一个 slot 插槽,而后在利用的模板里引入 HelloComponent 组件,并在该组件外面增加 <p>{{message}}</p> 作为插槽的内容。

代码运行的后果将生成如下 HTML 代码:

<div id="app">
  <hellocomponent>
    <div>
      <h1>John</h1>
      <slot>
        <p>Hello, Vue!</p>
      </slot>
    </div>
  </hellocomponent>
</div>

为了实现的代码简化起见,HelloComponentslot 两个节点仍然保留。大家先思考一下本人如何实现,而后再看 ChatGPT 怎么实现插槽,以下是我给的提醒语:

请给出一段代码,模仿实现 Vue.js 相似插槽(slot)的示例代码

ChatGPT 的答复如下:

要模仿实现 Vue.js 相似插槽(slot)的性能,能够在组件定义中应用非凡的标记来示意插槽,并在组件实例中通过相应的属性将内容传递到插槽中。以下是一个示例代码:

function Vue(options) {
  this.$options = options;
  this._data = typeof options.data === 'function' ? options.data() : options.data;
  this._components = options.components || {};

  this._proxyData();
  this._compileTemplate();
  this._proxyComponents();}

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;
      }
    });
  });
};

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];
};

Vue.prototype._proxyComponents = function() {
  var self = this;
  var components = this._components;

  Object.keys(components).forEach(function(componentName) {var component = new Vue(components[componentName]);
    self.$el.querySelectorAll(componentName).forEach(function(element) {component.$el.querySelectorAll('slot').forEach(function(slot) {slot.innerHTML = element.innerHTML;});
      element.innerHTML = component.$el.outerHTML;
    });
  });
};

// 应用示例
var HelloComponent = {data: function() {
    return {name: 'John'};
  },
  template: `
    <div>
      <h1>{{name}}</h1>
      <slot></slot>
    </div>
  `
};

var app = new Vue({
  el: '#app',
  data: {message: 'Hello, Vue!'},
  components: {HelloComponent},
  template: `
    <HelloComponent>
      <p>{{message}}</p>
    </HelloComponent>
  `
});

在这个示例中,咱们定义了一个 Vue 构造函数,它接管一个蕴含 datatemplatecomponents 的选项对象。

_proxyData 办法中,咱们应用 Object.defineProperty 将数据对象的属性代理到 Vue 实例上,以便实现数据的双向绑定。

_compileTemplate 办法中,咱们首先获取配置对象中的模板字符串,并应用正则表达式匹配 {{expression}} 的局部。而后,咱们应用 eval 函数依据表达式动静求值,将值替换回模板字符串中。留神:这里 eval 后面的 with 语句跟的是 self,而不是之前的 self._data,这是因为本示例中增加了 _proxyData 办法。接下来,咱们依据配置对象中的 el 属性获取对应的 DOM 元素,如果 DOM 元素不存在,咱们就创立一个 div 元素代替,而后再将编译后的模板字符串赋值给该元素的 innerHTML 属性。接着给 Vue 实例设置 $el 属性,留神:后面如果用 div 元素代替,则需通过 childNodes[0] 排除该 div 元素。

_proxyComponents 办法中,咱们首先获取配置对象中的组件申明,而后遍历所有的组件,依据组件名称获取组件对象,创立该对象的 Vue 实例,并通过该实例的 $el 属性,遍历所有 slot 插槽,将原始的 innerHTML 设置为插槽的内容,而后从新设置组件的 innerHTML 为实例 $el 元素的 outerHTML 内容。

最初,咱们先定义了一个 HelloComponent 对象,在该组件的模板中,增加了一个 slot 插槽。接着再创立一个 Vue 的实例,在配置对象的模板里引入 HelloComponent 组件,并在该组件外面增加插槽的内容。通过这种形式,咱们模仿实现了相似 Vue.js 中抛出事件和接管事件的性能。

同样的,以上代码并不齐全是 ChatGPT 生成的。在原始生成的代码中,还创立了一个 VueComponent 的构造函数,然而却没有实现插槽内容的替换,甚至除了应用示例之外,其余中央齐全没有插槽的相干代码,就连应用示例代码中的 components 对象也是我补上的。


用 200 行代码实现一个简版 Vue 框架

后面咱们曾经模仿了 Vue 的生命周期、模板语法、指令、双向绑定、数据监听、计算属性、事件处理、办法、组件、插槽,当初是时候把这些模仿的个性交融在一起,构建一个 Vue 框架的繁难版本。以下是便于模仿的用法示例代码:

var HelloComponent = {emits: ['greet'],
  data: function() {
    return {
      firstName: 'John',
      lastName: 'Doe'
    };
  },
  computed: {fullName: function() {return this.firstName + ' ' + this.lastName;}
  },
  updated: function() {this.$emit('greet', this.firstName);
  },
  template: `
    <div>
      <h1>{{fullName}}</h1>
      <slot></slot>
    </div>
  `
};

var app = new Vue({
  el: '#app',
  data: {
    message: 'Hello, Vue!',
    inputValue: 'ChatGPT'
  },
  watch: {message: function(newValue, oldValue) {console.log('Message changed:', oldValue, '->', newValue);
    },
    inputValue: function(newValue, oldValue) {console.log('InputValue changed:', oldValue, '->', newValue);
    }
  },
  methods: {greetMessage: function(message) {this.$emit('greet', message);
    },
    updateMessage: function(newMessage) {this.message = newMessage;}
  },
  components: {HelloComponent},
  beforeCreate: function() {console.log('beforeCreate hook');
  },
  created: function() {console.log('created hook');
  },
  beforeMount: function() {console.log('beforeMount hook');
  },
  mounted: function() {console.log('mounted hook');
  },
  beforeUpdate: function() {console.log('beforeUpdate hook');
  },
  updated: function() {console.log('updated hook');
  },
  template: `
    <div>
      <HelloComponent v-on:greet="greetMessage">
        <p>{{message}}</p>
      </HelloComponent>
      <input v-model="inputValue" type="text">
      <p v-text="inputValue"></p>
    </div>
  `
});

app.$on('greet', function(message) {console.log('Greet:', message);
});

app.inputValue = 'OpenAI'
app.HelloComponent.firstName = 'Tom';
app.updateMessage('Hello, World!');

这段代码在后面的根底上增加了新性能,比方 app.HelloComponent.firstName 利用能够通过组件名获取子组件的实例、v-on:greet 监听子组件的事件等。特地是对于 greet 事件,产生的连环动作顺次是:

  • HelloComponent 组件的生命周期 updated 中抛出 greet 事件,事件的参数为 firstName 属性。
  • 在 app 利用的模板里通过 <HelloComponent v-on:greet="greetMessage"> 来申明监听 HelloComponent 组件的 greet 事件,事件会触发 app 利用配置对象里的 greetMessage 办法。
  • 在 app 利用的 greetMessage 办法中再次往外抛 greet 事件,由利用的实例通过 app.$on('greet', ...) 监听 greet 事件,输入 firstName 的值。

以上代码运行的后果,输入的 HTML 页面代码如下:

<div id="app">
  <div>
    <hellocomponent v-on:greet="greetMessage">
      <div>
        <h1>Tom Doe</h1>
        <slot>
          <p>Hello, World!</p>
        </slot>
      </div>
    </hellocomponent>
    <input v-model="inputValue" type="text" />
    <p v-text="inputValue">OpenAI</p>
  </div>
</div>

控制台输入的后果如下。另外,能够在控制台输出 app.inputValue = 123 等形式察看数据双向绑定的成果。

beforeCreate hook
created hook
beforeMount hook
mounted hook
InputValue changed: ChatGPT  ->  OpenAI
beforeUpdate hook
updated hook
Greet: Tom
Message changed: Hello, Vue!  ->  Hello, World!
beforeUpdate hook
updated hook

以下就是本篇文章的精髓,只需 200 行代码实现的简版 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$renderupdated 钩子,意味着只有数据对象的属性发生变化,就会触发视图更新。

_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 办法绑定雷同的监听函数。


总结

以上模仿 Vue 原理的示例代码都是按 Vue 的 Option 选项式 API 形式编写的,与以后咱们罕用的 Composition 组合式 API 有所不同。这兴许跟我应用的 ChatGPT 版本只能获取 2021 年以前的材料无关,但这并不障碍咱们利用它学习 Vue 的用法、了解 Vue 的原理。

本篇文章波及的内容都比拟根底,Vue 还有很多高级个性和用法,咱们都能够借助 AI 辅导咱们学习。当然,在学习的过程中,咱们要时刻留神 AI 的答复并不完全正确,须要本人通过实际逐个甄别。在 Vue 的实战开发过程中,咱们同样能够借助 AI 来定位剖析问题,毕竟它不知疲倦,脾气又好,是不可多得的好老师。

本篇文章所有源代码和示例工程都在 OpenTiny 站点,请拜访 https://github.com/opentiny/ai-vue/

分割咱们:

  • 官网公众号:OpenTiny
  • OpenTiny 官网:https://opentiny.design/
  • Vue 组件库:https://github.com/opentiny/tiny-vue(欢送 Star 🌟)
  • Angular 组件库:https://github.com/opentiny/ng(欢送 Star 🌟)
正文完
 0