最近偷懶好久沒有寫博客了,一直想繼續(xù)Vue學(xué)習(xí)系列,想深入Vue源碼來寫。結(jié)果發(fā)現(xiàn)自己層次不夠,對js的理解差好多。所以一直想寫一直擱置著。最近重新振作決心看完Vue源碼,并且以我們這類前端小白的角度來一步步弄懂Vue源碼。
PS:以下文章為筆記類,記錄了本人在看源碼過程中的一些問題和感悟。
Vue源碼的本質(zhì)是什么
Vue.js 本質(zhì)上就是一個包含各種邏輯的一個function。而我們通常初始化Vue的過程就是實例化的過程。
var vm = new Vue({})
話不多說,老規(guī)矩用代碼說話!
讓我們來對Vue進(jìn)行打?。?/p>
var vm = new Vue({
...
})
console.log(Vue)
console.log(vm)
打印結(jié)果如圖:

這里可以看到Vue$3這個方法,就是這個方法對Vue對象的構(gòu)造函數(shù)了。其實這很簡單,我們自己都可以造出一個Vue$4
function Vue$4(options) {
this.options = options
}
Vue$4.prototype.name = "小東西"
Vue$4.prototype.age = 27
console.log(new Vue$4("很好"))
顯示結(jié)果如圖

綜上所述,Vue對象的本質(zhì)就是一個function,與我們的Vue$4的不同之處只在于邏輯的多與少。
必須理解Object對象
在Vue的源碼中,出場率最多的應(yīng)該就數(shù)Object對象的使用上了??梢赃@么說,不懂Object都沒法往下看代碼。以下是源碼中用到的比較多的。
- Object.defineProperty 直接在一個對象上定義一個新屬性,或者修改一個對象的現(xiàn)有屬性, 并返回這個對象。
- Object.create 使用指定的原型對象及其屬性去創(chuàng)建一個新的對象。
- Object.keys 返回一個由一個給定對象的自身可枚舉屬性組成的數(shù)組,數(shù)組中屬性名的排列順序和使用 for...in 循環(huán)遍歷該對象時返回的順序一致 (兩者的主要區(qū)別是 一個 for-in 循環(huán)還會枚舉其原型鏈上的屬性)。
- Object.prototype Object的原型對象。
- Object.freeze 凍結(jié)一個對象,凍結(jié)指的是不能向這個對象添加新的屬性,不能修改其已有屬性的值,不能刪除已有屬性,以及不能修改該對象已有屬性的可枚舉性、可配置性、可寫性。也就是說,這個對象永遠(yuǎn)是不可變的。該方法返回被凍結(jié)的對象。
那么,Vue中哪個是Object呢?我們繼續(xù)試驗:
console.log(typeof Vue)
console.log(typeof new Vue())
輸出結(jié)果
function
object
結(jié)果顯示,使用new來創(chuàng)建的Vue實例就是個對象,所以一切對Object的操作行為都是針對Vue實例對象的。
理解setter和getter
在網(wǎng)上看Vue的評論是經(jīng)常會聽到說
Vue無非就是setter和getter方法的運用而已
這讓我等新手一臉懵逼,這里我們就來認(rèn)識認(rèn)識setter和getter。
當(dāng)我們在獲取一個Vue實例data中的某個對象,如果你用console打印出來會發(fā)現(xiàn),對象屬性中除了常規(guī)的對象屬性和proto對象之外還會多一個set和一個get方法,所謂的setter和getter就是它們。Vue給每一個對象屬性都添加了Observer觀察數(shù)據(jù)的獲取和修改。好吧,貼代碼一睹真容。
function defineReactive (
obj,
key,
val,
customSetter,
shallow
) {
var dep = new Dep();
// Object.getOwnPropertyDescriptor() 方法返回指定對象上一個自有屬性對應(yīng)的屬性描述符。
//(自有屬性指的是直接賦予該對象的屬性,不需要從原型鏈上進(jìn)行查找的屬性)
// 對象、屬性名稱、描述~
var property = Object.getOwnPropertyDescriptor(obj, key);
// 當(dāng)且僅當(dāng)該屬性的 configurable 為 true 時,該屬性描述符才能夠被改變,同時該屬性也能從對應(yīng)的對象上被刪除。默認(rèn)為 false。
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
var getter = property && property.get;
var setter = property && property.set;
var childOb = !shallow && observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
var value = getter ? getter.call(obj) : val;
if (Dep.target) { // Watcher
dep.depend();
if (childOb) {
childOb.dep.depend();
if (Array.isArray(value)) {
dependArray(value);
}
}
}
return value
},
set: function reactiveSetter (newVal) {
var value = getter ? getter.call(obj) : val;
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if ("development" !== 'production' && customSetter) {
customSetter(); // 自定義setter
}
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = !shallow && observe(newVal);
dep.notify();
}
});
}
代碼太長?懵逼了?沒關(guān)系,我們自己來造一個簡單的setter和getter來了解一下。
其實用的就是Object.defineProperty方法中就有set和get。
- get 一個給屬性提供 getter 的方法,如果沒有 getter 則為 undefined。該方法返回值被用作屬性值。默認(rèn)為 undefined。
- set 一個給屬性提供 setter 的方法,如果沒有 setter 則為 undefined。該方法將接受唯一參數(shù),并將該參數(shù)的新值分配給該屬性。默認(rèn)為 undefined。
好了,寫代碼~
var obj = {}
var mValue = "abc"
Object.defineProperty(obj, "_name", {
configurable: false,
enumerable: false,
get: function reactiveGetter () {
return mValue
},
set: function reactiveSetter (val) {
mValue = val
}
})
obj._name = "rose"
console.log(obj)
到這里我們打印log,對象屬性的set和get方法就出現(xiàn)了。

邏輯運算符的使用
在源碼中有很多邏輯運算符的使用,有些運用的很巧妙。這里也科普下吧~
首先知道下可以轉(zhuǎn)換成false的值,如下:
- null
- NaN
- 0
- 空字符串("")
- undefined
用法一:判斷條件返回true或者false
這是最基本的用法。
if (a & a.master & a.master.name) {} // 如果這三個屬性都為true值,執(zhí)行if邏輯
if (a || b) {} // 如果a或者b為true值,執(zhí)行if邏輯。
用法二:判斷并返回條件對象
-
&&如果幾個條件都為true,則返回最后一個條件。 -
||幾個條件從前往后逐一判斷,如果那個條件為true,返回該條件,否則返回最后一個條件。
var getter = property && property.get; // 如果兩個屬性都存在,將property.get賦值給getter
e && e.__ob__ && e.__ob__.dep.depend(); // 如果三個屬性都存在,執(zhí)行第三條語句的方法
var res = assets[id] || assets[camelizedId] || assets[PascalCaseId];
// 給res賦值,如果assets[id]為true,則將其傳res;
// 如果assets[camelizedId]為true,將其傳給res;
// 如果前兩者都為false,將assets[PascalCaseId]傳給res。
var strat = strats[key] || defaultStrat;
然后,我對各種情況做了試驗得出以下結(jié)論:
-
&&判斷中,判斷值都為true,返回最后一個判斷值;判斷值中有false
值,返回第一個false值。 -
||判斷中,判斷值都為true,返回第一個判斷值;判斷值中有true值也有false值,返回第一個為true的判斷值;如果判斷值都為false,返回最后面的false值。
注意:這里所說的返回 false 值不一定是 Boolean 類型的 false,也可能是 0、null 等非值,詳見上文。
用法三:使用兩個非
兩個感嘆號會確保參數(shù)為非值時只能為false,不會是0、空字符串、undefined等非值。
if (options) {
this.deep = !!options.deep;
this.user = !!options.user;
this.lazy = !!options.lazy;
this.sync = !!options.sync;
} else {
this.deep = this.user = this.lazy = this.sync = false;
}
數(shù)組操作
復(fù)制
先看一段代碼。
var a = [ 'jack', 'rose', 'wade' ]
var b = a
b[1] = 'marry'
console.log(a)
console.log(b)
最后的結(jié)果是a和b的數(shù)組第二個值都變成了marry,原因就是b并不是獲得了數(shù)組內(nèi)容,而只是指向了a,a和b其實是一回事。如果我們需要復(fù)制,可以這么寫:
var b = a.slice()
查閱MDN可知:
slice() 方法返回一個從開始到結(jié)束(不包括結(jié)束)選擇的數(shù)組的一部分淺拷貝到一個新數(shù)組對象,原始數(shù)組不會被修改。
清空
想要將非空數(shù)組的內(nèi)容清空,最快捷的方式如下:
arr.length = 0
合并
直接引用MDN的例子。
concat() 方法用于合并兩個或多個數(shù)組。此方法不會更改現(xiàn)有數(shù)組,而是返回一個新數(shù)組。
var arr1 = ['a', 'b', 'c'];
var arr2 = ['d', 'e', 'f'];
var arr3 = arr1.concat(arr2);
// arr3 is a new array [ "a", "b", "c", "d", "e", "f" ]
遍歷
呃,使用forEach方式來遍歷數(shù)組是我剛知道的事 - -(之前都是用的for方法……),順便記錄下。
arr.forEach(function(obj){
// 數(shù)組循環(huán)
})
幾種特殊的寫法
在代碼中有些代碼片段有些看不懂,于是在SF上進(jìn)行了提問:看Vue源碼,有兩段代碼寫法不知是何意思,求指教~。感謝大家的幫助,這里總結(jié)下。
第一段是一段在{}里的代碼。
{
dataDef.set = function (newData) {
warn(
'Avoid replacing instance root $data. ' +
'Use nested data properties instead.',
this
);
};
propsDef.set = function () {
warn("$props is readonly.", this);
};
}
這段代碼,有的朋友說是塊級作用域、隔離作用域。不過另一種說法更可信。那就是我所看到的vue是編譯完的的代碼。源代碼其實在大括號前是有條件的。
if (process.env.NODE_ENV !== 'production') {
...
再來看下面的代碼
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global.Vue = factory());
}(this, (function () { 'use strict';
})))
這段代碼是Vue開頭的一段代碼,它有兩個知識點。
- 立即執(zhí)行函數(shù) —— 定義函數(shù)并立即執(zhí)行,寫法有
(function(){})()或者(function(){}())的形式。 - 由于過去前端沒有模塊系統(tǒng),使用script標(biāo)簽引入的js腳本共享同一個作用域,如果不把代碼包起來,很容易產(chǎn)生作用域污染、變量沖突的問題。
PS:這兩個問題真是網(wǎng)友的結(jié)論,不一定完善。有空我會查資料證實~
其他知識點
- Arguments —— 傳給函數(shù)的參數(shù)數(shù)組
- new運算符 —— 通過構(gòu)造函數(shù)創(chuàng)建對象
-
"development" !== 'production’的作用 —— webpack打包判斷執(zhí)行環(huán)境是不是生產(chǎn)環(huán)境,如果是生產(chǎn)環(huán)境會壓縮并且沒有提示警告之類的東西 - instanceof —— 驗證實例對象是否為該構(gòu)造函數(shù)new出來的。
- in關(guān)鍵字 —— 判斷某個值是否在數(shù)組或?qū)ο笾小?/li>
- Proxy對象 —— 創(chuàng)建某個對象,并定義一些行為給該對象。
- 字符串的
charAt是獲取第幾個字符,而slice方法是截取某段字符。 - delete關(guān)鍵字 —— 用于刪除對象中的某個關(guān)鍵字。
-
call()方法 —— 函數(shù)的調(diào)用,第一個參數(shù)為this,之后為函數(shù)定義參數(shù)。試驗了下
fun(a, b)和fun.call(this, a, b)兩種寫法效果一致。另外還可以看下 apply()方法
PS:都是從MDN中找到的資料,多查MDN讓我受益良多。
未完待續(xù)
還在學(xué)習(xí)Vue源碼中……看了很多文章、也看了一遍源碼。內(nèi)容太多,千頭萬緒,容我理清之后,用自己的文字把Vue的源碼學(xué)習(xí)記錄分享出來。一些值得記錄下來的知識點和心得會繼續(xù)在本文中更新。
最后,想學(xué)習(xí)Vue源碼的同學(xué)可以去買《Vue.js權(quán)威指南》這本書,雖然許多章節(jié)內(nèi)容和官網(wǎng)是重復(fù)的,不過源碼解析部分值得一看。我也正配合著這本書和源碼在學(xué)習(xí)Vue。