前言
vue目前是前端使用頻率較高的一套前端mvvm框架之一,提供了數據的響應式
、watch
、computed
等極爲方便的功能及api,那麼,vue到底是如何實現這些功能的呢?在探究vue源碼之前,必須瞭解以下幾點javascript的基本內容,通過了解這些內容,你可以更加輕鬆的閱讀vue源碼。
flow 類型檢測
Flow就是JavaScript的靜態類型檢查工具,由Facebook團隊於2014年的Scale Conference上首次提出。該庫的目標在於檢查JavaScript中的類型錯誤,開發者通常不需要修改代碼即可使用,故使用成本很低。同時,它也提供額外語法支持,使得開發者能更大程度地發揮Flow的作用。總結一句話:將javascript從弱類型語言變成了強類型語言。
基礎檢測類型
Flow支持原始數據類型,其中void對應js中的undefined,基本有如下幾種:
boolean
number
string
null
void
在定義變量的同時,只需要在關鍵的地方聲明想要的類型,基本使用如下:
let str:number = 1;
let str1:string = 'a';
// 重新賦值
str = 'd' // error
str1 = 3 // error
複雜類型檢測
Flow支持複雜類型檢測,基本有如下幾種:
Object
Array
Function
自定義Class
基本使用如下示例代碼:
// Object 定義
let o:Object = {
key: 123
}
//聲明瞭Object的key
let o2:{key:string} = {
key: '111'
}
// Array 定義
//基於基本類似的數組,數組內都是相同類型
let numberArr:number[] = [12,3,4,5,2];
//另一個寫法
let numberAr2r:Array<number> = [12,3,2,3];
let stringArr:string[] = ['12','a','cc'];
let booleanArr:boolean[] = [true,true,false];
let nullArr:null[] = [null,null,null];
let voidArr:void[] = [ , , undefined,void(0)];
//數組內包含各個不同的類型數據
//第4個原素沒有聲明,則可以是任意類型
let arr:[number,string,boolean] = [1,'a',true,function(){},];
Function定義寫法如下,vue源碼中出現頻率最多的:
/**
* 聲明帶類型的函數
* 這裏是聲明一個函數fn,規定了自己需要的參數類型和返回值類型。
*/
function fn(arg:number,arg2:string):Object{
return {
arg,
arg2
}
}
/**
* vue源碼片段
* src/core/instance/lifecycle.js
*/
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
// 省略
}
自定義的class,聲明一個自定義類,然後用法如同基本類型,基本代碼如下:
/**
* vue源碼片段
* src/core/observer/index.js
*/
export class Observer {
value: any;
dep: Dep;
vmCount: number;
constructor (value: any) {
// 省略
}
}
直接使用flow.js,javascript是無法在瀏覽器端運行的,必須藉助babel插件,vue源碼中使用的是babel-preset-flow-vue這個插件,並且在babelrc進行配置,片段代碼如下:
// package.json 文件
// 省略
"devDependencies": {
// 省略
"babel-preset-flow-vue": "^1.0.0"
}
// 省略
// babelrc 文件
{
"presets": ["es2015", "flow-vue"],
"plugins": ["transform-vue-jsx", "syntax-dynamic-import"],
"ignore": [
"dist/*.js",
"packages/**/*.js"
]
}
對象
這裏只對對象的創建、對象上的屬性操作相關、getter/setter方法、對象標籤等進行再分析,對於原型鏈以及原型繼承原理不是本文的重要內容。
創建對象
一般創建對象有以下三種寫法,基本代碼如下:
// 第一種 最簡單的寫法
let obj = { a: 1 }
obj.a // 1
typeof obj.toString // 'function'
// 第二種
let obj2 = Object.create({ a: 1 })
obj2.a // 1
typeof obj2.toString // 'function'
// 第三種
let obj3 = Object.create(null)
typeof obj3.toString // 'undefined'
圖解基本如下:
Object.create可以理解爲繼承一個對象,它是ES5的一個新特性,對於舊版瀏覽器需要做兼容,基本代碼如下(vue使用ie9+瀏覽器,所以不需要做兼容處理):
if (!Object.create) {
Object.create = function (o) {
function F() {} //定義了一個隱式的構造函數
F.prototype = o;
return new F(); //其實還是通過new來實現的
};
}
其中,在vue源碼中會看見使用Object.create(null)
來創建一個空對象,其好處不用考慮會和原型鏈上的屬性重名問題,vue代碼片段如下:
// src/core/global-api/index.js
// 再Vue上定義靜態屬性options並且賦值位空對象,ASSET_TYPES是在vue上定義的'component','directive','filter'等屬性
Vue.options = Object.create(null)
ASSET_TYPES.forEach(type => {
Vue.options[type + 's'] = Object.create(null)
})
屬性操作相關
其實在創建對象的同時,對象上會默認設置當前對象的枚舉類型值,如果不設置,默認所有枚舉類型均爲false,那麼如何定義對象並且設置枚舉類型值呢?主要使用到的是ES5的新特性Object.defineProperty
。
Object.defineProperty(obj,prop,descriptor)
中的descriptor
有如下幾種參數:
- configurable 當且僅當該屬性的 configurable 爲 true 時,該屬性描述符才能夠被改變,同時該屬性也能從對應的對象上被刪除。默認爲 false
- enumerable 當且僅當該屬性的enumerable爲true時,該屬性才能夠出現在對象的枚舉屬性中。默認爲 false
- value 該屬性對應的值。可以是任何有效的 JavaScript 值(數值,對象,函數等)。默認爲 undefined。
- writable 當且僅當該屬性的writable爲true時,value才能被賦值運算符改變。默認爲 false。
- get 一個給屬性提供 getter 的方法,如果沒有 getter 則爲 undefined。當訪問該屬性時,該方法會被執行,方法執行時沒有參數傳入,但是會傳入this對象(由於繼承關係,這裏的this並不一定是定義該屬性的對象)。默認爲 undefined。
- set 一個給屬性提供 setter 的方法,如果沒有 setter 則爲 undefined。當屬性值修改時,觸發執行該方法。該方法將接受唯一參數,即該屬性新的參數值。默認爲 undefined
注意:在 descriptor 中不能同時設置訪問器 (get 和 set) 和 value。
完整示例代碼如下:
Object.defineProperty(obj,prop,
configurable: true,
enumerable: true,
writable: true,
value: '',
get: function() {
},
set: function() {
}
)
通過使用Object.getOwnPropertyDescriptor
來查看對象上屬性的枚舉類型值,具體使用相關示例代碼如下:
// 如果不設置枚舉類型,默認都是false
let obj = {}
Object.defineProperty(obj, 'name', {
value : "wqzwh"
})
Object.getOwnPropertyDescriptor(obj, 'name')
// {value: "wqzwh", writable: false, enumerable: false, configurable: false}
let obj2 = {}
Object.defineProperty(obj2, 'name', {
enumerable: true,
writable: true,
value : "wqzwh"
})
Object.getOwnPropertyDescriptor(obj2, 'name')
// {value: "wqzwh", writable: true, enumerable: true, configurable: false}
通過Object.keys()
來獲取對象的key,必須將enumerable
設置爲true才能獲取,否則返回是空數組,代碼如下:
let obj = {}
Object.defineProperty(obj, 'name', {
enumerable: true,
value : "wqzwh"
})
Object.keys(obj) // ['name']
通過propertyIsEnumerable
可以判斷定義的對象是否可枚舉,代碼如下:
let obj = {}
Object.defineProperty(obj, 'name', {
value : "wqzwh"
})
obj.propertyIsEnumerable('name') // false
let obj = {}
Object.defineProperty(obj, 'name', {
enumerable: true,
value : "wqzwh"
})
obj.propertyIsEnumerable('name') // true
通過hasOwnProperty
來檢測一個對象是否含有特定的自身屬性;和 in 運算符不同,該方法會忽略掉那些從原型鏈上繼承到的屬性。代碼如下:
// 使用Object.defineProperty創建對象屬性
let obj = {}
Object.defineProperty(obj, 'name', {
value : "wqzwh",
enumerable: true
})
let obj2 = Object.create(obj)
obj2.age = 20
for (key in obj2) {
console.log(key); // age, name
}
for (key in obj2) {
if (obj2.hasOwnProperty(key)) {
console.log(key); // age
}
}
// 普通創建屬性
let obj = {}
obj.name = 'wqzwh'
let obj2 = Object.create(obj)
obj2.age = 20
for (key in obj2) {
console.log(key); // age, name
}
for (key in obj2) {
if (obj2.hasOwnProperty(key)) {
console.log(key); // age
}
}
注意:如果繼承的對象屬性是通過Object.defineProperty創建的,並且enumerable未設置成true,那麼for in依然不能枚舉出原型上的屬性。(感謝 @SunGuoQiang123 同學指出錯誤問題,已經做了更改)
getter/setter方法
通過get/set
方法來檢測屬性變化,基本代碼如下:
function foo() {}
Object.defineProperty(foo.prototype, 'z',
{
get: function(){
return 1
}
}
)
let obj = new foo();
console.log(obj.z) // 1
obj.z = 10
console.log(obj.z) // 1
這個是z
屬性是foo.prototype
上的屬性並且有get
方法,對於第二次通過obj.z = 10
並不會在obj
本身創建z
屬性,而是直接原型觸發上的get
方法。
圖解基本如下:
Object.defineProperty
中的configurable
、enumerable
、writable
、value
、get
、set
幾個參數相互之間的關係到底如何呢?可以用一張圖來清晰說明:
對象標籤
其實創建對象的同時都會附帶一個__proto__
的原型標籤,除了使用Object.create(null)
建立對象以外,代碼如下:
let obj = {x: 1, y: 2}
obj.__proto__.z = 3
console.log(obj.z) // 3
Object.preventExtensions
方法用於鎖住對象屬性,使其不能夠拓展,也就是不能增加新的屬性,但是屬性的值仍然可以更改,也可以把屬性刪除,Object.isExtensible
用於判斷對象是否可以被拓展,基本代碼如下:
let obj = {x : 1, y : 2};
Object.isExtensible(obj); // true
Object.preventExtensions(obj);
Object.isExtensible(obj); // false
obj.z = 1;
obj.z; // undefined, add new property failed
Object.getOwnPropertyDescriptor(obj, 'x');
// Object {value: 1, writable: true, enumerable: true, configurable: true}
Object.seal
方法用於把對象密封,也就是讓對象既不可以拓展也不可以刪除屬性(把每個屬性的configurable設爲false),單數屬性值仍然可以修改,Object.isSealed
由於判斷對象是否被密封,基本代碼如下:
let obj = {x : 1, y : 2};
Object.seal(obj);
Object.getOwnPropertyDescriptor(obj, 'x');
// Object {value: 1, writable: true, enumerable: true, configurable: false}
Object.isSealed(obj); // true
Object.freeze
完全凍結對象,在seal的基礎上,屬性值也不可以修改(每個屬性的wirtable也被設爲false),Object.isFrozen
判斷對象是否被凍結,基本代碼如下:
let obj = {x : 1, y : 2};
Object.freeze(obj);
Object.getOwnPropertyDescriptor(obj, 'x');
// Object {value: 1, writable: false, enumerable: true, configurable: false}
Object.isFrozen(obj); // true
DOM自定義事件
在介紹這個命題之前,先看一段vue源碼中的model的指令,打開platforms/web/runtime/directives/model.js
,片段代碼如下:
/* istanbul ignore if */
if (isIE9) {
// http://www.matts411.com/post/internet-explorer-9-oninput/
document.addEventListener('selectionchange', () => {
const el = document.activeElement
if (el && el.vmodel) {
trigger(el, 'input')
}
})
}
// 省略
function trigger (el, type) {
const e = document.createEvent('HTMLEvents')
e.initEvent(type, true, true)
el.dispatchEvent(e)
}
其中document.activeElement
是當前獲得焦點的元素,可以使用document.hasFocus()
方法來查看當前元素是否獲取焦點。
對於標準瀏覽器,其提供了可供元素觸發的方法:element.dispatchEvent(). 不過,在使用該方法之前,我們還需要做其他兩件事,及創建和初始化。因此,總結說來就是:
document.createEvent()
event.initEvent()
element.dispatchEvent()
createEvent()
方法返回新創建的Event
對象,支持一個參數,表示事件類型,具體見下表:
參數 事件接口 初始化方法
HTMLEvents HTMLEvent initEvent()
MouseEvents MouseEvent initMouseEvent()
UIEvents UIEvent initUIEvent()
initEvent()
方法用於初始化通過DocumentEvent
接口創建的Event
的值。支持三個參數:initEvent(eventName, canBubble, preventDefault)
. 分別表示事件名稱,是否可以冒泡,是否阻止事件的默認操作。
dispatchEvent()
就是觸發執行了,上文vue源碼中的el.dispatchEvent(e),
參數e表示事件對象,是createEvent()
方法返回的創建的Event
對象。
那麼這個東東具體該怎麼使用呢?例如自定一個click方法,代碼如下:
// 創建事件.
let event = document.createEvent('HTMLEvents');
// 初始化一個點擊事件,可以冒泡,無法被取消
event.initEvent('click', true, false);
let elm = document.getElementById('wq')
// 設置事件監聽.
elm.addEventListener('click', (e) => {
console.log(e)
}, false);
// 觸發事件監聽
elm.dispatchEvent(event);
數組擴展方法
every方法/some方法
接受兩個參數,第一個是函數(接受三個參數:數組當前項的值、當前項在數組中的索引、數組對象本身
),第二個參數是執行第一個函數參數的作用域對象,也就是上面說的函數中this所指向的值,如果不設置默認是undefined。
這兩種方法都不會改變原數組
- every(): 該方法對數組中的每一項運行給定函數,如果該函數對每一項都返回 true,則返回true。
- some(): 該方法對數組中的每一項運行給定函數,如果該函數對任何一項返回 true,則返回true。
示例代碼如下:
let arr = [ 1, 2, 3, 4, 5, 6 ];
console.log( arr.some( function( item, index, array ){
console.log( 'item=' + item + ',index='+index+',array='+array );
return item > 3;
}));
console.log( arr.every( function( item, index, array ){
console.log( 'item=' + item + ',index='+index+',array='+array );
return item > 3;
}));
some方法是碰到一個返回true的值時候就返回了,並沒有繼續往下運行,而every也一樣,第一個值就是一個false,所以後面也沒有進行下去的必要了,就直接返回結果了。
getBoundingClientRect
該方法返回一個矩形對象,其中四個屬性:left、top、right、bottom
,分別表示元素各邊與頁面上邊和左邊的距離,x、y
表示左上角定點的座標位置。
通過這個方法計算得出的left、top、right、bottom、x、y
會隨着視口區域內滾動操作而發生變化,如果你需要獲得相對於整個網頁左上角定位的屬性值,那麼只要給top、left屬性值加上當前的滾動位置。
爲了跨瀏覽器兼容,請使用 window.pageXOffset 和 window.pageYOffset 代替 window.scrollX 和 window.scrollY。不能訪問這些屬性的腳本可以使用下面的代碼:
// For scrollX
(((t = document.documentElement) || (t = document.body.parentNode))
&& typeof t.scrollLeft == 'number' ? t : document.body).scrollLeft
// For scrollY
(((t = document.documentElement) || (t = document.body.parentNode))
&& typeof t.scrollTop == 'number' ? t : document.body).scrollTop
在IE中,默認座標從(2,2)開始計算,導致最終距離比其他瀏覽器多出兩個像素,代碼如下:
document.documentElement.clientTop; // 非IE爲0,IE爲2
document.documentElement.clientLeft; // 非IE爲0,IE爲2
// 所以爲了保持所有瀏覽器一致,需要做如下操作
functiongGetRect (element) {
let rect = element.getBoundingClientRect();
let top = document.documentElement.clientTop;
let left= document.documentElement.clientLeft;
return{
top: rect.top - top,
bottom: rect.bottom - top,
left: rect.left - left,
right: rect.right - left
}
}
performance
vue中片段源碼如下:
if (process.env.NODE_ENV !== 'production') {
const perf = inBrowser && window.performance
/* istanbul ignore if */
if (
perf &&
perf.mark &&
perf.measure &&
perf.clearMarks &&
perf.clearMeasures
) {
mark = tag => perf.mark(tag)
measure = (name, startTag, endTag) => {
perf.measure(name, startTag, endTag)
perf.clearMarks(startTag)
perf.clearMarks(endTag)
perf.clearMeasures(name)
}
}
}
performance.mark
方法在瀏覽器的性能條目緩衝區中創建一個具有給定名稱的緩衝區,performance.measure
在瀏覽器的兩個指定標記(分別稱爲起始標記和結束標記)之間的性能條目緩衝區中創建一個命名,測試代碼如下:
let _uid = 0
const perf = window.performance
function testPerf() {
_uid++
let startTag = `test-mark-start:${_uid}`
let endTag = `test-mark-end:${_uid}`
// 執行mark函數做標記
perf.mark(startTag)
for(let i = 0; i < 100000; i++) {
}
// 執行mark函數做標記
perf.mark(endTag)
perf.measure(`test mark init`, startTag, endTag)
}
測試結果可以在谷歌瀏覽器中的Performance
中監測到,效果圖如下:
瀏覽器中performance處理模型基本如下(更多具體參數說明):
Proxy相關
get方法
get
方法用於攔截某個屬性的讀取操作,可以接受三個參數,依次爲目標對象、屬性名和 proxy 實例本身(嚴格地說,是操作行爲所針對的對象),其中最後一個參數可選。
攔截對象屬性的讀取,比如proxy.foo和proxy['foo']
基本使用如下:
let person = {
name: "張三"
};
let proxy = new Proxy(person, {
get: (target, property) => {
if (property in target) {
return target[property];
} else {
throw new ReferenceError("Property \"" + property + "\" does not exist.");
}
}
});
proxy.name // "張三"
proxy.age // 拋出一個錯誤
如果一個屬性不可配置(configurable)且不可寫(writable),則 Proxy 不能修改該屬性,否則通過 Proxy 對象訪問該屬性會報錯。示例代碼如下:
const target = Object.defineProperties({}, {
foo: {
value: 123,
writable: false,
configurable: false
},
});
const handler = {
get(target, propKey) {
return 'abc';
}
};
const proxy = new Proxy(target, handler);
proxy.foo // TypeError: Invariant check failed
has方法
此方法可以接受兩個參數,分別是目標對象、需查詢的屬性名,主要攔截如下幾種操作:
- 屬性查詢: foo in proxy
- 繼承屬性查詢: foo in Object.create(proxy)
- with 檢查: with(proxy) { (foo); }
- Reflect.has()
如果原對象不可配置或者禁止擴展,這時has攔截會報錯。基本示例代碼如下:
let obj = { a: 10 };
Object.preventExtensions(obj);
let p = new Proxy(obj, {
has: function(target, prop) {
return false;
}
});
'a' in p // TypeError is thrown
has
攔截只對in
運算符生效,對for...in
循環不生效。基本示例代碼如下:
let stu1 = {name: '張三', score: 59};
let stu2 = {name: '李四', score: 99};
let handler = {
has(target, prop) {
if (prop === 'score' && target[prop] < 60) {
console.log(`${target.name} 不及格`);
return false;
}
return prop in target;
}
}
let oproxy1 = new Proxy(stu1, handler);
let oproxy2 = new Proxy(stu2, handler);
'score' in oproxy1
// 張三 不及格
// false
'score' in oproxy2
// true
for (let a in oproxy1) {
console.log(oproxy1[a]);
}
// 張三
// 59
for (let b in oproxy2) {
console.log(oproxy2[b]);
}
// 李四
// 99
使用with
關鍵字的目的是爲了簡化多次編寫訪問同一對象的工作,基本寫法如下:
let qs = location.search.substring(1);
let hostName = location.hostname;
let url = location.href;
with (location){
let qs = search.substring(1);
let hostName = hostname;
let url = href;
}
使用with
關鍵字會導致代碼性能降低,使用let
定義變量相比使用var
定義變量能提高一部分性能,示例代碼如下:
// 不使用with
function func() {
console.time("func");
let obj = {
a: [1, 2, 3]
};
for (let i = 0; i < 100000; i++) {
let v = obj.a[0];
}
console.timeEnd("func");// 1.310302734375ms
}
func();
// 使用with並且使用let定義變量
function funcWith() {
console.time("funcWith");
const obj = {
a: [1, 2, 3]
};
with (obj) {
let a = obj.a
for (let i = 0; i < 100000; i++) {
let v = a[0];
}
}
console.timeEnd("funcWith");// 14.533935546875ms
}
funcWith();
// 使用with
function funcWith() {
console.time("funcWith");
var obj = {
a: [1, 2, 3]
};
with (obj) {
for (var i = 0; i < 100000; i++) {
var v = a[0];
}
}
console.timeEnd("funcWith");// 52.078857421875ms
}
funcWith();
js引擎在代碼執行之前有一個編譯階段,在不使用with
關鍵字的時候,js引擎知道a是obj上的一個屬性,它就可以靜態分析代碼來增強標識符的解析,從而優化了代碼,因此代碼執行的效率就提高了。使用了with關鍵字後,js引擎無法分辨出a變量是局部變量還是obj的一個屬性,
因此,js引擎在遇到with
關鍵字後,它就會對這段代碼放棄優化,所以執行效率就降低了。
使用has
方法攔截with
關鍵字,示例代碼如下:
let stu1 = {name: '張三', score: 59};
let handler = {
has(target, prop) {
if (prop === 'score' && target[prop] < 60) {
console.log(`${target.name} 不及格`);
return false;
}
return prop in target;
}
}
let oproxy1 = new Proxy(stu1, handler);
function test() {
let score
with(oproxy1) {
return score
}
}
test() // 張三 不及格
在使用with
關鍵字時候,主要是因爲js引擎在解析代碼塊中變量的作用域造成的性能損失,那麼我們可以通過定義局部變量來提高其性能。修改示例代碼如下:
// 修改後
function funcWith() {
console.time("funcWith");
const obj = {
a: [1, 2, 3]
};
with (obj) {
let a = obj.a
for (let i = 0; i < 100000; i++) {
let v = a[0];
}
}
console.timeEnd("funcWith");// 1.7109375ms
}
funcWith();
但是在實際使用的時候在with
代碼塊中定義局部變量不是很可行,那麼刪除頻繁查找作用域的功能應該可以提高代碼部分性能,經測試運行時間幾乎相同,修改代碼如下:
function func() {
console.time("func");
let obj = {
a: [1, 2, 3]
};
let v = obj.a[0];
console.timeEnd("func");// 0.01904296875ms
}
func();
// 修改後
function funcWith() {
console.time("funcWith");
const obj = {
a: [1, 2, 3]
};
with (obj) {
let v = a[0];
}
console.timeEnd("funcWith");// 0.028076171875ms
}
funcWith();
配上has函數後執行效果如何呢,片段代碼如下:
// 第一段代碼其實has方法沒用,只是爲了對比使用
console.time("測試");
let stu1 = {name: '張三', score: 59};
let handler = {
has(target, prop) {
if (prop === 'score' && target[prop] < 60) {
console.log(`${target.name} 不及格`);
return false;
}
return prop in target;
}
}
let oproxy1 = new Proxy(stu1, handler);
function test(oproxy1) {
return {
render: () => {
return oproxy1.score
}
}
}
console.log(test(oproxy1).render()) // 張三 不及格
console.timeEnd("測試"); // 0.719970703125ms
console.time("測試");
let stu1 = {name: '張三', score: 59};
let handler = {
has(target, prop) {
if (prop === 'score' && target[prop] < 60) {
console.log(`${target.name} 不及格`);
return false;
}
return prop in target;
}
}
let oproxy1 = new Proxy(stu1, handler);
function test(oproxy1) {
let score
return {
render: () => {
with(oproxy1) {
return score
}
}
}
}
console.log(test(oproxy1).render()) // 張三 不及格
console.timeEnd("測試"); // 0.760009765625ms
vue中使用with
關鍵字的片段代碼如下,主要通過proxy
來攔截AST
語言樹中涉及到的變量以及方法,並且判斷是否AST
語言樹中是否存在爲定義的變量及方法,至於爲什麼vue
會使用with
關鍵字,具體可以點擊查看
export function generate (
ast: ASTElement | void,
options: CompilerOptions
): CodegenResult {
const state = new CodegenState(options)
const code = ast ? genElement(ast, state) : '_c("div")'
return {
render: `with(this){return ${code}}`,
staticRenderFns: state.staticRenderFns
}
}
outerHTML
打開platforms/web/entry-runtime-width-compile.js
,查看getOuterHTML
方法,片段代碼如下:
function getOuterHTML (el: Element): string {
if (el.outerHTML) {
return el.outerHTML
} else {
const container = document.createElement('div')
container.appendChild(el.cloneNode(true))
return container.innerHTML
}
}
由於在IE9-11中SVG
標籤元素是沒有innerHTML
和outerHTML
這兩個屬性,所以會有else
之後的語句
總結
以上主要是在閱讀源碼時,發現不是很明白的api以及一些方法,每個人可以根據自己的實際情況選擇性閱讀。
學如逆水行舟,不進則退