最近,我正在準備一份關於 Vue 基礎的學習材料。期間我突發奇想:能否利用現在熱門的 ChatGPT 幫我建立學習內容?其實 Vue 本身不難學,特別是基礎用法,但是,如果你想深入掌握 Vue,就要通過閱讀 Vue 的原始碼來了解其原理。然而,不是每個人都有足夠的時間和耐心閱讀 Vue 的原始碼。於是我花了一個多小時與 ChatGPT 對話,引導它生成一些展示 Vue 原理的範例程式碼。在這個過程中,我只是充當一個提問者的角色,讓 ChatGPT 根據我的問題生成我想要的程式碼。結果讓我有些驚喜,但也不完全出乎意料。鑑於目前 AI 快速發展的趨勢,我相信到了 2024 年,將會有很多職業被 AI 所替代,儘管那時 AI 還不能完全取代老師,但是它有潛力成為一個私人輔教工具。我相信,未來並不是 AI 取代人類,而是懂得用 AI 的人取代不懂用 AI 的人。接下來,就讓我們一起見識這位 AI 助教的水平吧。
本篇文章主要講以下內容:
為了系統地學習 Vue,需要先制定一個學習大綱。結合我對 ChatGPT 的瞭解,給它提的第一個問題,就得到比較滿意的回覆,以下就是我提的問題原文:
請列出一個Vue.js從入門到高階的學習大綱
ChatGPT 的回答如下:
當涉及Vue.js的學習大綱時,下面是一個從入門到高階的學習路線,涵蓋了Vue.js的核心概念和高階主題:
請注意,這個學習大綱只是一個指導,你可以根據自己的學習進度和需求進行調整和擴充套件。此外,學習Vue.js最好通過實踐專案和閱讀官方檔案來加深理解和掌握。
以上內容是 ChatGPT 生成的,它具備一定的普適性,但我並不準備按照這個大綱來講,我希望大家通過閱讀 Vue 的原始碼,來學習和掌握 Vue 的用法。比如,很多人都覺得 Vue 資料雙向繫結的用法很簡單,但要讓自己來模擬實現資料雙向繫結卻不簡單。學習 Vue 的用法需要掌握其原理主要有以下四個原因(這些原因也是由 ChatGPT 生成的):
深入理解框架:通過學習 Vue 的原理,你將更好地理解 Vue 框架的設計和工作原理。這包括 Vue 的響應式資料繫結、虛擬 DOM、元件化架構等核心概念。理解這些原理可以幫助你更好地使用 Vue,更高效地開發和偵錯應用程式。
自定義擴充套件和優化:掌握 Vue 的原理使你能夠進行自定義擴充套件和優化。你可以根據專案需求自定義 Vue 的指令、外掛、過濾器等,以及優化效能、減少資源消耗等方面。理解原理可以讓你更靈活地應對各種開發場景。
故障排查和偵錯:在開發過程中,難免會遇到一些問題和錯誤。如果你瞭解 Vue 的原理,你可以更快地定位和解決問題。理解原理可以幫助你深入瞭解 Vue 的內部機制,識別潛在的錯誤來源,並採取正確的解決方法。
跨框架學習和遷移:掌握 Vue 的原理可以為你日後學習其他前端框架奠定基礎。許多前端框架在概念和原理上有相似之處,因此,一旦你掌握了 Vue 的原理,你將更容易理解和學習其他框架,如 React、Angular 等。此外,如果你需要遷移專案到其他框架,理解原理可以幫助你更好地理解新框架的工作方式。
該怎樣向 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 提示技巧就像當下使用智慧手機一樣,將成為一種生存技能。
前面的程式碼已經讓我們瞭解如何使用 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 的生命週期。建構函式接收一個設定物件作為引數,並在範例化過程中觸發各個生命週期勾點函數。在建構函式中,我們首先判斷設定物件中是否定義了
beforeCreate
和created
勾點函數,並在相應的時機呼叫。然後,我們處理資料物件,讀取設定物件裡的data
屬性,如果data
是函數則執行獲取其返回值,如果是物件則直接賦值,然後通過_proxyData
方法將資料物件中的屬性代理到Vue
範例上,以便實現資料的雙向繫結。接下來,在
$mount
方法中,我們判斷設定物件中是否定義了beforeMount
和mounted
勾點函數,並在相應的時機呼叫。在$mount
方法中,我們呼叫render
方法將模板渲染到指定的 DOM 元素上。在
_proxyData
方法中,我們使用Object.defineProperty
將資料物件的屬性代理到Vue
範例上,並在屬性的set
方法中觸發beforeUpdate
、render
和updated
勾點,意味著只要資料物件的屬性發生變化,就會觸發檢視更新。在
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'
來驗證資料繫結以及頁面更新機制。
按照上面的學習路徑,我們在學習模板語法和指令之前,同樣先給出便於模擬的用法範例程式碼:
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-model
和 v-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-model
和 v-text
指令的內容,但實際偵錯過程中並不能獲取。於是我拆解成兩個語句:querySelectorAll('[v-model]')
和 querySelectorAll('[v-text]')
分別獲取指令內容。
由此可見,我們還不能完成信任 ChatGPT 生成的程式碼。如果發現程式碼有問題,你可以向它提出質疑,它會說抱歉然後重新生成新的程式碼,更新後的程式碼是否正確,依然需要你再次驗證。
接下來要學習的仍是比較基礎的資料監聽和計算屬性,其實前面在生命週期的例子裡已經包含了資料雙向繫結,資料監聽和計算屬性的原理跟資料雙向繫結很類似。
同樣的,我們先給出便於模擬的用法範例程式碼:
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
從範例程式碼可以看出,設定物件新增了 computed
和 watch
屬性,分別定義了計算屬性 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
建構函式,它接收一個包含data
、computed
和watch
的選項物件。在
_proxyData
方法中,我們使用Object.defineProperty
為Vue
範例上的屬性定義了get
和set
方法,將其代理到_data
物件上。在
_createComputed
方法中,我們通過遍歷computed
物件,為每個計算屬性定義了get
方法,使其能夠被當做普通屬性使用。在
_createWatchers
方法中,我們通過遍歷watch
物件,為每個屬性使用Object.defineProperty
監聽_data
物件中該屬性的變化,並在變化時觸發回撥函數。最後,我們使用範例中的設定物件來範例化
Vue
,並傳入計算屬性和監聽屬性的函數,通過執行一段指令碼讀取和更改設定屬性的值,實現了類似 Vue.js 的資料監聽和計算屬性的效果。
與之前一樣,以上程式碼並不完全是 ChatGPT 生成的。在原始生成的程式碼中,監聽屬性需要執行 app.$watch('message', ...)
這樣的語句,即呼叫 app
的 $watch
方法來新增監聽函數。修正後的監聽方式更符合 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
的程式碼,然後我再將兩段程式碼融合在一起,才得到上面的範例程式碼。
最後,我們來學習 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>
為了實現的程式碼簡化起見,HelloComponent
和 slot
兩個節點依然保留。大家先思考一下自己如何實現,然後再看 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
建構函式,它接收一個包含data
、template
和components
的選項物件。在
_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
物件也是我補上的。
前面我們已經模擬了 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
屬性。<HelloComponent v-on:greet="greetMessage">
來宣告監聽 HelloComponent
元件的 greet
事件,事件會觸發 app 應用設定物件裡的 greetMessage
方法。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
、$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
方法系結相同的監聽函數。
以上模擬 Vue 原理的範例程式碼都是按 Vue 的 Option 選項式 API 方式編寫的,與當前我們常用的 Composition 組合式 API 有所不同。這也許跟我使用的 ChatGPT 版本只能獲取 2021 年以前的資料有關,但這並不妨礙我們利用它學習 Vue 的用法、理解 Vue 的原理。
本篇文章涉及的內容都比較基礎,Vue 還有很多高階特性和用法,我們都可以藉助 AI 輔導我們學習。當然,在學習的過程中,我們要時刻注意 AI 的回答並不完全正確,需要自己通過實踐逐一甄別。在 Vue 的實戰開發過程中,我們同樣可以藉助 AI 來定位分析問題,畢竟它不知疲倦,脾氣又好,是不可多得的好老師。
本篇文章所有原始碼和範例工程都在 OpenTiny
站點,請存取 https://github.com/opentiny/ai-vue/
聯絡我們: