我最近在寫 Vue 進階的內容。在這個過程中,有些人問我看 Vue 源碼需要有哪些準備嗎?所以也就有了這篇計劃之外的文章。
當你想學習 Vue 源碼的時候,需要有紮實的 JavaScript 基礎,下面羅列的只是其中的一部分比較具有代表性的知識點。如果你還不具備 JavaScript 基礎的話,建議不要急着看 Vue 源碼,這樣你會很容易放棄的。
我會從以下 7 點來展開:
- Flow 基本語法
- 發佈/訂閱模式
- Object.defineProperty
- ES6+ 語法
- 原型鏈、閉包
- 函數柯里化
- event loop
必要知識儲備
需要注意的是這篇文章每個點不會講的特別詳細,我這裏就是把一些知識點歸納一下。每個詳細的點仍需自己花時間學習。
Flow 基本語法
相信看過 Vue、Vuex 等源碼的人都知道它們使用了 Flow 靜態類型檢查工具。
我們知道 JavaScript 是弱類型的語言,所以我們在寫代碼的時候容易出現一些始料未及的問題。也正是因爲這個問題,纔出現了 Flow 這個靜態類型檢查工具。
這個工具可以改變 JavaScript 是弱類型的語言的情況,可以加入類型的限制,提高代碼質量。
// 未使用 Flow 限制
function sum(a, b) {
return a + b;
}
// 使用 Flow 限制 a b 都是 number 類型。
function sum(a: number, b:number) {
return a + b;
}
基礎檢測類型
Flow 支持原始數據類型,有如下幾種:
boolean
number
string
null
void( 對應 undefined )
在定義變量的同時在關鍵的地方聲明類型,使用如下:
let str:string = 'str';
// 重新賦值
str = 3 // 報錯
複雜類型檢測
Flow 支持複雜類型檢測,有如下幾種:
Object
Array
Function
自定義的 Class
需要注意直接使用 flow.js,JavaScript 是無法在瀏覽器端運行的,必須藉助 babel 插件,vue 源碼中使用的是 babel-preset-flow-vue 這個插件,並且在 babelrc 進行配置。
詳細的 Flow 語法可以看以下資料:
這裏推薦兩個資料
- 官方文檔:flow.org/en/
- Flow 的使用入門:zhuanlan.zhihu.com/p/26204569
發佈/訂閱模式
我們知道 Vue 是內部是實現了雙向綁定機制,使得我們不用再像從前那樣還要自己操作 DOM 了。
其實 Vue 的雙向綁定機制採用數據劫持結合發佈/訂閱模式實現的: 通過 Object.defineProperty()
來劫持各個屬性的 setter,getter
,在數據變動時發佈消息給訂閱者,觸發相應的監聽回調。
我發現有的人把觀察者模式和發佈/訂閱模式混淆一談,其實訂閱模式有一個調度中心,對訂閱事件進行統一管理。而觀察者模式可以隨意註冊事件,調用事件。
我畫了一個大概的流程圖,用來說明觀察者模式和發佈/訂閱模式。如下:
這塊我會在接下的文章中詳細講到,這裏先給出一個概念,感興趣的可以自己查找資料,也可等我的文章出爐。
其實我們對這種模式再熟悉不過了,但可能你自己也沒發現:
let div = document.getElementById('#div');
div.addEventListener('click', () => {
console.log("div 被點擊了一下")
})
可以思考下上面的事件綁定執行的一個過程,你應該會有共鳴。
函數柯里化
數據雙向綁定基礎:Object.defineProperty()
一、數據屬性
數據屬性包含一個數據值的位置。這個位置可以讀取和寫入值。數據屬性有 4 個描述他行爲的特性:
屬性 | 描述 |
---|---|
Configurable | 能否用 delete 刪除屬性從而重新定義屬性。默認爲 true |
Enumerable | 能否通過 for-in 遍歷,即是否可枚舉。默認爲 true |
Writable | 是否能修改屬性的值。默認爲 true |
Value | 包含這個屬性的數據值,讀寫屬性的時候其實就在這裏讀寫。默認爲 undefined |
如果你想要修改上述 4 個默認的數據屬性,就需要使用 ECMAScript 的 Object.defineProperty() 方法。
該方法包含3個參數:屬性所在的對象,屬性名,描述符對象。描述符對象的屬性必須在上述 4 個屬性中。
var person = {
name: '',
};
// 不能修改屬性的值
Object.defineProperty(person, "name",{
writable: false,
value: "小生方勤"
});
console.log(person.name); // "小生方勤"
person.name = "方勤";
console.log(person.name); // "小生方勤"
二、訪問器屬性
訪問器屬性不包含數據值,他們包含一對 getter
和 setter
函數(非必須)。在讀寫訪問器屬性的值的時候,會調用相應的 getter
和 setter
函數,而我們的 vue 就是在 getter
和 setter
函數中增加了我們需要的操作。
需要注意的是【value 或 writable】一定不能和【get 或 set】共存。
訪問器屬性有以下 4 個特性:
特性 | 描述 |
---|---|
Configurable | 能否用 delete 刪除屬性從而重新定義屬性。默認爲 true |
Enumerable | 能否通過 for-in 遍歷,即是否可枚舉。默認爲 true |
get | 讀取屬性時調用的函數,默認 undefined |
set | 寫入屬性時調用的函數,默認 undefined |
接下來給個例子:
var person = {
_name : "小生方勤"
};
Object.defineProperty(person, "name", {
//注意 person 多定義了一個 name 屬性
set: function(value){
this._name = "來自 setter : " + value;
},
get: function(){
return "來自 getter : " + this._name;
}
});
console.log( person.name ); // 來自 getter : 小生方勤
person.name = "XSFQ";
console.log( person._name ); // 來自 setter : XSFQ
console.log( person.name ); // 來自 getter : 來自 setter : XSFQ
如果之前都不清楚有 Object.defineProperty() 方法,建議你看《JavaScript 高級程序設計》的 139 - 144 頁。
額外講講 Object.create(null)
我們在源碼隨處可以
this.set = Object.create(null)
這樣的賦值。爲什麼這樣做呢?這樣寫的好處就是不需要考慮原型鏈上的屬性,可以真正的創建一個純淨的對象。
首先 Object.create 可以理解爲繼承一個對象,它是 ES5 的一個特性,對於舊版瀏覽器需要做兼容,基本代碼如下:
if (!Object.create) {
Object.create = function (o) {
function F() {} // 定義了一個隱式的構造函數
F.prototype = o;
return new F(); // 其實還是通過new來實現的
};
}
ES6+ 語法
其實這點應該是默認你需要知道的,不過鑑於之前有人問過我一些相關的問題,我稍微講一下。
export default
和 export
的區別
- 在一個文件或模塊中
export
可以有多個,但export default
僅有一個 - 通過
export
方式導出,在導入時要加 { },而export default
則不需要
1.export
//a.js
export const str = "小生方勤";
//b.js
import { str } from 'a'; // 導入的時候需要花括號
2.export default
//a.js
const str = "小生方勤";
export default str;
//b.js
import str from 'a'; // 導入的時候無需花括號
export default const a = 1;
這樣寫是會報錯的喲。
箭頭函數
這個一筆帶過:
- 箭頭函數中的 this 指向是固定不變的,即是在定義函數時的指向
- 而普通函數中的 this 指向時變化的,即是在使用函數時的指向
class 繼承
Class 可以通過 extends
關鍵字實現繼承,這比 ES5 的通過修改原型鏈實現繼承,要清晰和方便很多。
class staff {
constructor(){
this.company = "ABC";
this.test = [1,2,3];
}
companyName(){
return this.company;
}
}
class employee extends staff {
constructor(name,profession){
super();
this.employeeName = name;
this.profession = profession;
}
}
// 將父類原型指向子類
let instanceOne = new employee("Andy", "A");
let instanceTwo = new employee("Rose", "B");
instanceOne.test.push(4);
// 測試
console.log(instanceTwo.test); // [1,2,3]
console.log(instanceOne.companyName()); // ABC
// 通過 Object.getPrototypeOf() 方法可以用來從子類上獲取父類
console.log(Object.getPrototypeOf(employee) === staff)
// 通過 hasOwnProperty() 方法來確定自身屬性與其原型屬性
console.log(instanceOne.hasOwnProperty('test')) // true
// 通過 isPrototypeOf() 方法來確定原型和實例的關係
console.log(staff.prototype.isPrototypeOf(instanceOne)); // true
super
關鍵字,它在這裏表示父類的構造函數,用來新建父類的 this
對象。
- 子類必須在
constructor
方法中調用super
方法,否則新建實例時會報錯。這是因爲子類沒有自己的this
對象,而是繼承父類的this
對象,然後對其進行加工。- 只有調用
super
之後,纔可以使用this
關鍵字,否則會報錯。這是因爲子類實例的構建,是基於對父類實例加工,只有super
方法才能返回父類實例。
`super` 雖然代表了父類 `A` 的構造函數,但是返回的是子類 `B` 的實例,即` super` 內部的 `this ` 指的是 `B`,因此 `super()` 在這裏相當於 A.prototype.constructor.call(this)
ES5 和 ES6 實現繼承的區別
ES5 的繼承,實質是先創造子類的實例對象 this
,然後再將父類的方法添加到 this
上面(Parent.apply(this)
)。
ES6 的繼承機制完全不同,實質是先創造父類的實例對象 this
(所以必須先調用 super()
方法),然後再用子類的構造函數修改 this
。
proxy
對最新動態瞭解的人就會知道,在下一個版本的 Vue 中,會使用 proxy
代替 Object.defineProperty
完成數據劫持的工作。
尤大說,這個新的方案會使初始化速度加倍,於此同時內存佔用減半。
proxy 對象的用法:
var proxy = new Proxy(target, handler);
new Proxy() 即生成一個 Proxy 實例。target 參數表示所要攔截的目標對象,handler 參數也是一個對象,用來定製攔截行爲。
var proxy = new Proxy({}, {
get: function(obj, prop) {
console.log('get 操作')
return obj[prop];
},
set: function(obj, prop, value) {
console.log('set 操作')
obj[prop] = value;
}
});
proxy.num = 2; // 設置 set 操作
console.log(proxy.num); // 設置 get 操作 // 2
除了 get 和 set 之外,proxy 可以攔截多達 13 種操作。
注意,proxy 的最大問題在於瀏覽器支持度不夠,IE 完全不兼容。
倘若你基本不瞭解 ES6, 推薦下面這個教程:
阮一峯 ECMAScript 6 入門:es6.ruanyifeng.com/
原型鏈、閉包
原型鏈
因爲之前我特意寫了一篇文章來解釋原型鏈,所以這裏就不在講述了:
閉包
這裏我先放一段 Vue 源碼中的 once 函數。這就是閉包調用 —— 函數作爲返回值:
/**
* Ensure a function is called only once.
*/
export function once (fn: Function): Function {
let called = false
return function () {
if (!called) {
called = true
fn.apply(this, arguments)
}
}
}
這個函數的作用就是確保函數只調用一次。
爲什麼只會調用一次呢? 因爲函數調用完成之後,其執行上下文環境不會被銷燬,所以 called 的值依然在那裏。
閉包到底是什麼呢。《JavaScript 高級程序設計》的解釋是:
閉包是指有權訪問另一個函數作用域中的變量的函數。創建閉包的常見方式,就是在一個函數內部創建另一個函數。
簡單講,閉包就是指有權訪問另一個函數作用域中的變量的函數。
給兩段代碼,如果你知道他們的運行結果,那麼說明你是瞭解閉包的:
// 第一段
var num = 20;
function fun(){
var num = 10;
return function con(){
console.log( this.num )
}
}
var funOne = fun();
funOne(); // 20
// 第二段
var num = 20;
function fun(){
var num = 10;
return function con(){
console.log( num )
}
}
var funOne = fun();
funOne(); // 10
函數柯里化
所謂"柯里化",就是把一個多參數的函數,轉化爲單參數函數。
先說說我之前遇到過得一個面試題:
如何使
add(2)(3)(4)(5)()
輸出 14
在那次面試的時候,我還是不知道柯里化這個概念的,所以當時我沒答上。後來我才知道這可以用函數柯里化來解,即:
function add(num){
var sum=0;
sum= sum+num;
return function tempFun(numB){
if(arguments.length===0){
return sum;
}else{
sum= sum+ numB;
return tempFun;
}
}
}
那這和 Vue 有什麼關係呢?當然是有關係的:
我們是否經常這樣寫判斷呢?
if( A ){
// code
}else if( B ){
// code
}
這個寫法沒什麼問題,可是在重複的出現這種相同的判斷的時候。這個就顯得有點不那麼智能了。這個時候函數柯里化就可以排上用場了。
因爲 Vue 可以在不同平臺運行,所以也會存在上面的那種判斷。這裏利用柯里化的特點,通過 createPatchFunction 方法把一些參數提前保存,以便複用。
// 這樣不用每次調用 patch 的時候都傳遞 nodeOps 和 modules
export function createPatchFunction (backend) {
// 省略好多代碼
return function patch (oldVnode, vnode, hydrating, removeOnly) {
// 省略好多代碼
}
}
event loop
四個概念:
- 同步任務:即在主線程上排隊執行的任務,只有前一個任務執行完畢,才能執行後一個任務。
- 異步任務:指的是不進入主線程,某個異步任務可以執行了,該任務纔會進入主線程執行。
- macrotask:主要場景有:主代碼塊、setTimeout、setInterval等
- microtask:主要場景有:Promise、process.nextTick等。
這一點網上教程已經很多了,再因爲篇幅的問題,這裏就不詳細說了。
推薦一篇文章,說的很細緻:
JavaScript 執行機制:juejin.im/post/59e85e…
總結
這篇文章講到這裏就結束了。不過有一點我需要在說一篇,這篇文章的定位並不是面面俱到的將所有知識都講一遍,這不現實我也沒這個能力。
我只是希望通過這篇文章告訴大家一個觀點,要想看源碼,一些必備的 JavaScript 基礎知識必須要紮實,否則你會舉步維艱。
願你每天都有進步。