我最近在寫 Vue 進階的內容。在這個過程當中,有些人問我看 Vue 源碼須要有哪些準備嗎?因此也就有了這篇計劃以外的文章。前端
當你想學習 Vue 源碼的時候,須要有紮實的 JavaScript 基礎,下面羅列的只是其中的一部分比較具備表明性的知識點。若是你還不具有 JavaScript 基礎的話,建議不要急着看 Vue 源碼,這樣你會很容易放棄的。vue
我會從如下 7 點來展開:node
Flow 基本語法ios
發佈/訂閱模式es6
Object.defineProperty面試
ES6+ 語法npm
原型鏈、閉包瀏覽器
函數柯里化babel
須要注意的是這篇文章每一個點不會講的特別詳細,我這裏就是把一些知識點概括一下。每一個詳細的點仍需本身花時間學習。閉包
相信看過 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 語法能夠看如下資料:
這裏推薦兩個資料
官方文檔:https://flow.org/en/
咱們知道 Vue 是內部是實現了雙向綁定機制,使得咱們不用再像從前那樣還要本身操做 DOM 了。
其實 Vue 的雙向綁定機制採用數據劫持結合發佈/訂閱模式實現的: 經過 Object.defineProperty() 來劫持各個屬性的 setter,getter,在數據變更時發佈消息給訂閱者,觸發相應的監聽回調。
我發現有的人把觀察者模式和發佈/訂閱模式混淆一談,其實訂閱模式有一個調度中心,對訂閱事件進行統一管理。而觀察者模式能夠隨意註冊事件,調用事件。
我畫了一個大概的流程圖,用來講明觀察者模式和發佈/訂閱模式。以下:
這塊我會在接下的文章中詳細講到,這裏先給出一個概念,感興趣的能夠本身查找資料,也可等個人文章出爐。
其實咱們對這種模式再熟悉不過了,但可能你本身也沒發現:
let div = document.getElementById('#div'); div.addEventListener('click', () => { console.log("div 被點擊了一下") })
能夠思考下上面的事件綁定執行的一個過程,你應該會有共鳴。
數據屬性包含一個數據值的位置。這個位置能夠讀取和寫入值。數據屬性有 4 個描述他行爲的特性:
若是你想要修改上述 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 個特性:
接下來給個例子:
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 頁。
咱們在源碼隨處能夠 this.set=Object.create(null) 這樣的賦值。爲何這樣作呢?這樣寫的好處就是不須要考慮原型鏈上的屬性,能夠真正的建立一個純淨的對象。
首先 Object.create 能夠理解爲繼承一個對象,它是 ES5 的一個特性,對於舊版瀏覽器須要作兼容,基本代碼以下:
if (!Object.create) { Object.create = function (o) { function F() {} // 定義了一個隱式的構造函數 F.prototype = o; return new F(); // 其實仍是經過new來實現的 }; }
其實這點應該是默認你須要知道的,不過鑑於以前有人問過我一些相關的問題,我稍微講一下。
exportdefault 和 export 的區別
在一個文件或模塊中 export 能夠有多個,但 exportdefault 僅有一個
經過 export 方式導出,在導入時要加 { },而 exportdefault 則不須要
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'; // 導入的時候無需花括號
exportdefaultconsta=1; 這樣寫是會報錯的喲。
這個一筆帶過:
箭頭函數中的 this 指向是固定不變的,便是在定義函數時的指向
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 的繼承,實質是先創造子類的實例對象 this,而後再將父類的方法添加到 this 上面( Parent.apply(this))。
ES6 的繼承機制徹底不一樣,實質是先創造父類的實例對象 this (因此必須先調用 super() 方法),而後再用子類的構造函數修改 this。
對最新動態瞭解的人就會知道,在下一個版本的 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 入門:http://es6.ruanyifeng.com/
由於以前我特地寫了一篇文章來解釋原型鏈,因此這裏就不在講述了:
原型鏈:http://www.javashuo.com/article/p-skwfkoft-kn.html
這裏我先放一段 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)() 輸出 9
在那次面試的時候,我仍是不知道柯里化這個概念的,因此當時我沒答上。後來我才知道這能夠用函數柯里化來解,即:
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) { // 省略好多代碼 } }
四個概念:
同步任務:即在主線程上排隊執行的任務,只有前一個任務執行完畢,才能執行後一個任務
異步任務:指的是不進入主線程,某個異步任務能夠執行了,該任務纔會進入主線程執行
macrotask:主要場景有 主代碼塊、setTimeout、setInterval 等
這一點網上教程已經不少了,再由於篇幅的問題,這裏就不詳細說了。
推薦一篇文章,說的很細緻:
JavaScript 執行機制:http://www.javashuo.com/article/p-cmtsxzha-h.html
這篇文章講到這裏就結束了。不過有一點我須要在說一篇,這篇文章的定位並非面面俱到的將全部知識都講一遍,這不現實我也沒這個能力。
我只是但願經過這篇文章告訴你們一個觀點,要想看源碼,一些必備的 JavaScript 基礎知識必需要紮實,不然你會舉步維艱。
願你天天都有進步。
最近總有朋友問我 Vue 相關的問題,所以接下來我會輸出 9 篇 Vue 相關的文章,但願對你們有必定的幫助。我會保持在 7 到 10 天更新一篇。
【前端詞典】Vuex 注入 Vue 生命週期的過程(完成)
【前端詞典】淺析 Vue 響應式原理
【前端詞典】新老 VNode 進行 patch 的過程
【前端詞典】如何開發功能組件並上傳 npm
【前端詞典】從這幾個方面優化你的 Vue 項目
【前端詞典】從 Vue-Router 設計講前端路由發展
【前端詞典】在項目中如何正確的使用 Webpack
【前端詞典】Vue 服務端渲染
建議你關注個人公衆號,第一時間就能夠接收最新的文章。
若是你想加羣交流,也能夠添加有點智能的機器人,自動拉你進羣:
隨意吧,開心就好