一个简单案例的Vue2.0源码
本文學習vue2.0源碼,主要從new Vue()時發(fā)生了什么和頁面的響應式更新2個維度了解Vue.js的原理。以一個簡單的vue代碼為例,介紹了這個代碼編譯運行的流程,在流程中原始DOM的信息會被解析轉換,存在不同的對象中。其中關鍵的對象有el、template、ast、code、render、render function和vnode等。本文對vue源碼每一個關鍵細節(jié)的位置都進行了記錄。
vue源碼的理解需要一些js基礎,先介紹js的相關基礎。
1.基礎知識
1.1 Chrome瀏覽器架構
1.1.1 Chrome架構簡介
Javascript是一種無類型的語言,所以很靈活,但編譯運行比較耗時。現(xiàn)在主流的Javascript引擎有V8、JavaScriptCore等。Javascript引擎只是瀏覽器中的一個小部分。比如,chromium瀏覽器的架構如圖1[1],它包含渲染引擎Blink(Webkit)和V8引擎,Blink(Webkit)引擎與javaScript引擎相互提供了接口(如圖2[2]),由它們協(xié)作完成HTML的渲染。Chomium瀏覽器中和Blink并列的模塊還有GPU/CommandBuffer(硬件加速架構)、沙箱模型、CC(Chromium Compositor)、IPC、UI等。在這些模塊之上是”Content模塊“和”Content API“,它們將下面的渲染機制、安全機制和插件機制等隱藏起來,提供一個接口層。該接口會被上層模塊”Chromium瀏覽器“、”Content Shell“等使用;它可以被其它項目比如CEF(Chromium Embedded Framework)、Opera瀏覽器等使用。
”Chromium瀏覽器“、”Content Shell“是構建在”Content API“之上的兩個”瀏覽器“,Chromium具有瀏覽器完整的功能,也就是我們編譯出來能看到的瀏覽器式樣。”Content Shell“是使用Content API來包裝的一層簡單的”殼“,但是它也是一個簡單的”瀏覽器“,用戶可以使用Content模塊來渲染和顯示網頁內容。Content Shell的作用很明顯,其一可以用來測試Content模塊很多功能的正確性,例如渲染、硬件加速等;其二是一個參考,可以被很多外部的項目參考來開發(fā)基于”Content API“的瀏覽器或各種類型的項目。上面還有一個部分是”Androdi WebView“,他是為了滿足Android系統(tǒng)上的WebView而設計的,其思想是利用Chromium的實現(xiàn)來替換原來Android系統(tǒng)默認的WebView。
圖1 chromium模塊結構圖
圖2 渲染引擎和JavaScript引擎的關系
1.1.2 Chrome中HTML渲染流程
HTML渲染指將 HTML,CSS 和 JavaScript 轉換為屏幕上的像素的過程,它由渲染引擎和JavaScript引擎協(xié)作完成。HTML渲染流程如圖3[3]所示,可以將這個過程分為5個步驟[4]。HTML解釋器、CSS解釋器和布局都是渲染引擎中的模塊。
- HTML解釋器處理 HTML 標記并構造 DOM 樹;
- CSS解釋器處理 CSS 并構建 CSSOM 樹;同時JavaScript引擎編譯運行JavaScript腳本;
- 將 DOM 和 CSSOM 組合成一個 Render 樹。在DOM建立的時候,渲染引擎接受來自CSS解釋器的樣式信息,構建一個新的內部繪圖模型,創(chuàng)建RenderObject樹;
- 在Render樹上運行布局(渲染引擎中的模塊)以計算每個節(jié)點的幾何體。
- 將各個節(jié)點繪制到屏幕上。
圖3 渲染引擎的一般渲染過程及各階段依賴的其他模塊
1.1.3 V8中JavaScript的編譯流程
Netscape于1995年開發(fā)了Javascript,主要目的是處理一些輸入框校驗,這些檢驗在之前都是由后端語言(比如Perl)來實現(xiàn)的。在客戶端處理基本的校驗是非常令人興奮的;當時電話調制解調器盛行,訪問服務器的速度很慢,訪問時需要很大的耐心。從那以后,JavaScript逐漸成長為市場上每個瀏覽器的重要特性。JavaScript不再僅僅實現(xiàn)簡單的數(shù)據(jù)校驗,如今與瀏覽器窗口和內容的方方面面都有交互。JavaScript被認為是全能的語言,可是實現(xiàn)復雜的運算和交互,包含閉包、lambda表達式,甚至是元編程(metaprogramming)等特性。[5]JavaScript設計的時候,并不適用來開發(fā)大工程和性能要求非常的場景。但隨著javascript使用越來越廣泛,功能越來越豐富,對javascript的性能的要求也越來越高。
Javascript是一種無類型或動態(tài)類型的語言,在編譯的時候不知道變量的類型。相比較而言,C++或Java等語言都是靜態(tài)類型語言,它們在編譯的時候知道變量的類型。早期javascript的編譯流程如圖4所示;先將源代碼編譯成抽象語法樹,然后抽象語法樹上解釋執(zhí)行。早期的javascipt編譯器執(zhí)行如Demo1的代碼時,獲取對象obj中的屬性b,是通過在obj對象中搜索變量標識符”b“實現(xiàn)的。而在Demo2和Demo3中,java運行字節(jié)碼(或本地代碼)時,獲取包含同樣數(shù)據(jù)的對象obj的屬性b,只需要將對象obj所在的地址向右偏移4個字節(jié)(int的大小)即可。對對象屬性的訪問時非常頻繁的,相比于java中通過偏移量來訪問值,使用2個匯編指令就能完成;在javascript中通過屬性名匹配訪問數(shù)據(jù)的值,性能會差很多。[6]
圖4 早期Javascript的編譯流程
var obj ={a:1,b:2}
console.log(obj.b) //2
Demo1 JavaScript獲取對象obj的屬性b的值
public class Obj {
public int a;
public int b;
public Obj(int a, int b) {
this.a = a;
this.b = b;
}
}
Demo2 Java中定義類Obj
public class test {
public static void main(String[] args) {
Obj obj = new Obj(1, 2);
System.out.println(obj.b); //2
}
}
Demo3 Java中訪問對象obj的屬性b
為了提高javascript的編譯性能,眾多工程師借鑒了java和C++編譯器的思想,嘗試對javascript進行改進。隨著java虛擬機的JIT技術引入,現(xiàn)在的做法是將抽象語法樹轉成中間表示(字節(jié)碼),然后通過JIT技術轉成本地代碼(匯編代碼),這能夠大大提高執(zhí)行效率,如圖5。當然也有些直接從抽象語法樹生成本地代碼的JIT技術,例如V8。V8中使用了特殊的方式來表示數(shù)據(jù)類型。[106]
圖5 JavaScript編譯流程改進
源代碼編譯時,先生成抽象語法樹(ast)是編譯器的通常做法,java編譯器、C++編譯器中都包含這個步驟。在3.1節(jié)中,vue框架在將template解析生成Vnode時,也先將template解析抽象語法樹。Demo4所示的javascript代碼編譯生成的ast樹如圖6所示[7]。
if(typeof a == "undefined" ) {
a = 0;
} else {
a = a;
}
alert(a);
Demo4 一段簡單的js代碼
圖6 根據(jù)js代碼生成的抽象語法樹(ast)
1.2 不同瀏覽器的差異
1.2.1 渲染引擎和JavaScript引擎
不同的瀏覽器使用的渲染引擎和JavaScript引擎有所不同。目前主流的瀏覽器有Chomium、FireFox、Edge、Safri等,它們使用的渲染引擎和Javascript引擎如圖7[8][9]。Chromium剛開始使用Webkit引擎,后來從Webkit中創(chuàng)建了Blink分支,這主要有2方面原因:1)Chromium使用了不同于其它基于Webkit的瀏覽器的多進程架構,支持多進程架構增加了Webkit和Chromium社區(qū)的復雜性,阻礙了集體前進的步伐;2)這使得有機會進行其它性能提升方案的開放式調查,Chromium的用戶和開發(fā)者希望Chromium盡可能地快。比如,他們希望可以有盡可能多的瀏覽器任務并發(fā)執(zhí)行,以使主線程有空閑執(zhí)行應用代碼。他們已經取得了巨大的進展,比如減少了JavaScript和layout對頁面滾動的影響,并使得越來越多的CSS動畫以60fps的速度運行,即使JavaScript正在做其它繁重的工作[10]。JavaScirpt引擎V8相比于JavaScriptCore,效率大大提升,后來Node.js也是基于V8引擎的。
| 瀏覽器 | 渲染引擎 | Javascript引擎 |
|---|---|---|
| Chromium | 早期:Webkit,后來:Blink | V8 |
| Safari | Webkit | JavascriptCore |
| Edge | 早期:EdgeHTML,后來:Blink | Chakra |
| IE | Trident | Chakra |
| FireFox | Gecko | Spider Monkey |
圖7 不同瀏覽器的渲染引擎和JavaScript引擎
1.2.2 JavaScript規(guī)范的統(tǒng)一
Netscape公司于1994年首次發(fā)布Netscape Navigator,并于1995年開發(fā)了JavaScript。1995年,微軟首次發(fā)布了IE(Internet Explorer),這導致了和NetScape的瀏覽器大戰(zhàn)。微軟對Navigator的解釋器進行逆向工程(Reverse engineering),創(chuàng)建了自己的腳本語言JScipt。2000年,IE的市場份額達到了95%。2004年,Netscape的繼承者Mozilla發(fā)布了FireFox瀏覽器。在2005年,Mozilla加入了ECMA國際組織。2008年,Google首次發(fā)布了Chrome瀏覽器,使用了JavaScript引擎V8,比其他JavaScript引擎都要快。2008年,這些不同的瀏覽器組織在Oslo的會議上相聚并達成最終的協(xié)議[11],同年五大主要瀏覽器(IE、FireFox、Safari、Chrome和Opera)全部開始遵守ECMAScript3規(guī)范[12]。
1.3 JavaScript的一些特性
JavaScript的語法從C和其它類似C的語言(比如Java和Perl)中借鑒了很多。熟悉這些語言的開發(fā)者可以輕松掌握JavaScript的寬松語法[11]。筆者也沒有專門學過js這門語言,因為它看起來和java很像。雖然JavaScript與java在名字、語法和標準庫上都很相似,但其實兩者是不同的語言,在設計上也有巨大的差異[14]。在閱讀Vue2.0源碼時,發(fā)現(xiàn)JavaScript有一些特性是java中沒有的,比如原型、閉包等,在這先簡單介紹一下這些特性。
1.3.1 原型(prototype)
1.3.1.1 原型
prototype是JavaScript語言的一個重要特性。Demo5是一個使用prototype的簡單案例,定義函數(shù)A并設置了函數(shù)原型的屬性值name后,函數(shù)A的原型實例a1和a2會共享這個屬性值。當一個函數(shù)(構造器)創(chuàng)建的時候,它的prototype屬性也會被創(chuàng)建。默認所有的prototype都會有一個constructor屬性,它指向prototype所屬的函數(shù)。當函數(shù)(構造器)創(chuàng)建新實例時,實例中會有一個內在指針指向函數(shù)(構造器)原型。在ECMA-262中,這個指針被稱為[[prototype]]。在script中沒有標準方式獲取[[prototype]]的值,但是Firefox、Safari、Chrome和Edge等在每個對象上都加上了__proto__屬性,通過__proto__獲取[[prototype]]的值。[15]圖8中展示了函數(shù)、函數(shù)原型和原型實例的關系。依據(jù)圖8的關系,Demo6中的結果是顯然的。
function A(){}
A.prototype.name = 'jack'
a1 = new A()
a2 = new A()
console.log(a1.name) //jack
console.log(a2.name) //jack
Demo5一個使用prototype的簡單案例
圖8 函數(shù)、函數(shù)原型和原型實例的關系
console.log(A.prototype.constructor === A);//true
console.log(a1.__proto__ === A.prototype);//true
Demo6 函數(shù)、函數(shù)原型和原型實例的關系
也可以在Chrome瀏覽器的控制臺查看對象a1的屬性,在控制臺中a1的屬性如圖9。圖中第一個[[Prototype]]表示對象a1指向的原型A Prototype,它的constructor為f A()。第二個[[Prototype]]指向原型A Prototype的原型Object Prototype,該原型的constructor為f Object()。Object Prototype不是其它原型的實例,所以它下面沒有[[Prototype]]。多個[[Prototype]]構成原型鏈[47],原型鏈中的原型的關系如圖10所示。根據(jù)圖10中關系,Demo7的結果是顯而易見的。原型實例會共享原型鏈中的所有屬性。
圖9 Chrome控制臺中對象a1的屬性
圖10 函數(shù)A的一個簡單的原型鏈
console.log(a1.__proto__ === A.prototype) //true
console.log(A.prototype.__proto__ === Object.prototype) //true
console.log(Object.prototype.__proto__ === null) //true
Demo7 原型鏈中各對象屬性之間的關系
在javascirpt中的原型概念可以與java做類比。如圖11,js中原型實例看為java中的對象;js中函數(shù)(構造器)看為是java類中的構造方法;js的函數(shù)原型看成java類中的靜態(tài)成員變量和方法;js的原型鏈看為java類的繼承。java類中的靜態(tài)成員變量和方法在第一次使用該類時加載到方法區(qū),函數(shù)原型在函數(shù)(構造器)創(chuàng)建的同時被創(chuàng)建。java中使用構造函數(shù)創(chuàng)建對象時,對象中有內在指針指向方法區(qū)的類;函數(shù)(構造器)創(chuàng)建新實例時,實例中有一個內在指針指向函數(shù)(構造器)原型。js中如果函數(shù)原型是其它原型的實例,該函數(shù)原型會共享其它原型中的屬性,構成原型鏈;java中子類會繼承父類的屬性和方法。
| js | java |
|---|---|
| 原型實例 | 對象 |
| 函數(shù)(構造器) | 類中的構造方法 |
| 函數(shù)原型 1)當一個函數(shù)(構造器)創(chuàng)建的時候,它的函數(shù)原型也會被創(chuàng)建 2)當函數(shù)(構造器)創(chuàng)建新實例時,實例中會有一個內在指針指向函數(shù)(構造器)原型 |
類中的靜態(tài)成員變量和方法 1)類中的靜態(tài)成員變量和方法在第一次使用該類時加載到方法區(qū) 2)使用構造函數(shù)創(chuàng)建對象時,對象中有內在指針指向方法區(qū)的類 |
| 原型鏈 | 類的繼承 |
圖11 將js中的原型與java的語法做類比
1.3.1.2 函數(shù)和函數(shù)原型繼承
在上一節(jié)中,其實基于prototype構成的原型鏈實現(xiàn)了函數(shù)原型的繼承,它非常類似于java中類的繼承。除了基于原型鏈的繼承,還有很多其它方式能實現(xiàn)繼承[16]。下面以Demo8所示的父函數(shù)Animal和子函數(shù)Cat為例,介紹實現(xiàn)函數(shù)和函數(shù)原型繼承的幾種方式。
function Animal(){
this.species = "動物";
}
Animal.prototype.food = "肉類"
function Cat(name,color){
this.name = name;
this.color = color;
}
Demo8 父函數(shù)Animal和子函數(shù)Cat
1)基于prototype的繼承
Demo9展示了通過prototype實現(xiàn)構造函數(shù)和函數(shù)原型繼承,案例中子函數(shù)的原型和父函數(shù)的原型構成原型鏈。
function extend(child,parent){
child.prototype = new parent();
child.prototype.constructor = child;
return child;
}
extend(Cat,Animal);
var cat1 = new Cat("大毛","黃色");
alert(cat1.species); // 動物
alert(cat1.food); //肉類
Demo9 通過原型鏈實現(xiàn)構造函數(shù)和函數(shù)原型繼承
2)構造函數(shù)綁定
也可以使用構造函數(shù)綁定實現(xiàn)構造函數(shù)的繼承。如Demo10,使用call或apply方法,將父對象的構造函數(shù)綁定在子對象上,即可實現(xiàn)構造函數(shù)的繼承。
function Cat(name,color){
Animal.apply(this, arguments);
this.name = name;
this.color = color;
}
var cat1 = new Cat("大毛","黃色");
alert(cat1.species); // 動物
Demo10 通過構造函數(shù)綁定實現(xiàn)構造函數(shù)繼承
3)拷貝繼承
如Demo11,將父函數(shù)Parent的原型中的屬性,逐一拷貝給子函數(shù)Child的原型中的屬性,可以實現(xiàn)函數(shù)原型的繼承。
function extend(Child, Parent) {
var p = Parent.prototype;
var c = Child.prototype;
for (var i in p) {
c[i] = p[i];
}
}
extend(Cat,Animal);
var cat1 = new Cat("大毛","黃色");
alert(cat1.food); //肉類
Demo11 通過函數(shù)原型拷貝實現(xiàn)函數(shù)原型繼承
1.3.1.3 疑問
在上面1.3.1.1節(jié)中在Chrome控制臺中查看了原型實例的屬性,對__proto__字段還有疑問。如圖12,第二個[[Prototype]]表示的是Object Prototype,它的__proto__字段應為null;圖中的__proto__表示的顯然不是Object Prototype的原型,它表示的是對象a1的原型。既然__proto__表示的是對象a1的原型,那應該和第一個[[Prototype]]處于同一層級,為何放在第二個[[Prototype]]的下一層級呢?
圖12 原型實例a1(1.3.1.1節(jié))的屬性
1.3.2 閉包(closure)
1.3.2.1 執(zhí)行上下文作用域
執(zhí)行上下文(execution context)是JavaScript中非常重要的概念,也可簡稱為上下文。變量或函數(shù)的執(zhí)行上下文定義了可以訪問哪些數(shù)據(jù)。每個執(zhí)行上下文都關聯(lián)一個variable object,它包含上下文中的變量和函數(shù)[17]。
全局上下文位于最外層。在web瀏覽器中,全局上下文指window對象。當執(zhí)行上下文執(zhí)行完后,執(zhí)行上下文連帶包含在其中的函數(shù)和變量一起銷毀。window對象在關閉應用的時候會銷毀,比如關閉web頁面或關閉瀏覽器。每個函數(shù)調用都有自己的執(zhí)行上下文。當函數(shù)執(zhí)行完畢后,函數(shù)的執(zhí)行上下文會銷毀[17]。
當函數(shù)定義時,它的作用域鏈被創(chuàng)建,并預加載 global variable object,并將作用域鏈保存到[[scope]]中。當函數(shù)執(zhí)行時,函數(shù)的執(zhí)行上下文被創(chuàng)建,并基于函數(shù)的[[scope]]創(chuàng)建上下文的作用域鏈。之后,函數(shù)的activation object被創(chuàng)建并添加到上下文的作用域鏈中。如果函數(shù)定義在其它函數(shù)中,在函數(shù)定義時,函數(shù)的作用域鏈也會預加載其它函數(shù)的activation object。函數(shù)執(zhí)行上下文中的作用域鏈包含global variable object,本函數(shù)的activation object,還可能包含其它函數(shù)的activation object。[118]以Demo12所示的簡單案例進行說明。當代碼執(zhí)行到Afunc中swapFunc函數(shù)時,如圖13的執(zhí)行上下文swapFunc execution context和作用域鏈Scope Chain被創(chuàng)建。Scope Chain中包含Global variable object、AFunc activation object和swapFunc activation object。最靠前的是當前代碼執(zhí)行的上下文中的swapFunc activation object。然后包含當前執(zhí)行上下文的上一層執(zhí)行上下文中的Afunc activation object。然后是上上一層執(zhí)行上下文的activation object,直到global context中的Global variable object。函數(shù)和變量的標識符取值是通過在scope chain中搜索標識符名稱,搜索是從scope chain的最前面開始的[17]。
var a = 1;
function AFunc() {
let b = 2;
function swapFunc() {
let temp = b;
b = a;
a = temp;
// a,b,temp都可以獲取到
}
// a,b可以獲取到
swapFunc();
}
// 只能獲取到變量a
AFunc();
Demo12 函數(shù)swapFunc可以訪問在swapFunc之外聲明的變量a和變量b
圖13 函數(shù)swapFunc執(zhí)行上下文中的作用域鏈
1.3.2.2閉包
將上一節(jié)Demo12的代碼稍作調整,在AFunc函數(shù)中最后一行代碼前加個return,如Demo13。執(zhí)行AFunc()會返回swapFunc函數(shù),用變量BFunc接收。當swapFunc函數(shù)從AFunc()返回的時候,swapFunc函數(shù)的作用域鏈被初始化為包含函數(shù)AFunc的variable object和Global variable object,如圖13所示。執(zhí)行函數(shù)BFunc()時,它可以訪問AFunc activation object中的變量b和Global variable object中的變量a。也就是說,當函數(shù)AFunc執(zhí)行完后,AFunc的activation object不能被銷毀,因為函數(shù)swapFunc的作用域鏈對其有引用。當函數(shù)AFunc執(zhí)行完后,他的作用域鏈被銷毀;但是activation object仍然在內存中,直到函數(shù)swapFunc銷毀時才會被銷毀,swapFunc的銷毀可以通過將swapFunc的值設置null,等待垃圾回收任務回收它。[18]當一個函數(shù)可以訪問當前函數(shù)的activation object之外的變量時,這個函數(shù)被成為閉包(closure)[19]。Demo13中的函數(shù)swapFunc和AFunc都是閉包,swapFunc可以訪問AFunc variable object和Global variable object中的變量;AFunc可以訪問Global activation object中的變量。由于閉包中包含其它函數(shù)的activation object,可能會比非閉包的函數(shù)占用更多的內存,比如Demo13中的BFunc函數(shù)。所以在實際使用時,應盡量在必要的時候才使用閉包。
var a = 1;
function AFunc() {
let b = 2;
function swapFunc() {
let temp = b;
b = a;
a = temp;
// a,b,temp都可以獲取到,swapFunc是一個閉包
}
// a,b可以獲取到,AFunc是一個閉包
return swapFunc();
}
// 只能獲取到變量a
BFunc = AFunc();
BFunc();//BFunc執(zhí)行時可以訪問函數(shù)之外變量a和b,BFunc是一個閉包函數(shù)
BFunc = null;//將BFunc設置為null,等待垃圾回收任務回收
Demo13 函數(shù)BFunc是一個閉包
1.3.2.2 一些閉包的案例[20]
1)案例1:斐波那契數(shù)列
如Demo14,在函數(shù)makeFab中定義了函數(shù)inner,inner訪問了makeFab activation object中的變量last和current。執(zhí)行makeFab()會返回inner函數(shù),用變量fab接收。當inner函數(shù)從makeFab()返回的時候,inner函數(shù)的作用域鏈(scope chain)被初始化為包含函數(shù)makeFab 的activation object。由于函數(shù)inner的scope chain對函數(shù)makeFab的activation object有引用,在makeFab執(zhí)行完后,makeFab的activation object不會被銷毀。每次執(zhí)行fab函數(shù)后,變量last和current不會被銷毀,下一次執(zhí)行fab函數(shù)時,會在上次的執(zhí)行結果的基礎上進行計算。
function makeFab () {
let last = 1, current = 1
return function inner() {
[current, last] = [current + last, current]
return last
}
}
let fab = makeFab()
console.log(fab()) // 1
console.log(fab()) // 2
console.log(fab()) // 3
console.log(fab()) // 5
Demo14 斐波那契數(shù)列
2)案例2:防抖節(jié)流函數(shù)
在web頁面的上下文中運行如Demo2的案例,如果你在輸入框輸入一個字母或數(shù)字,0.5s后控制臺將輸出這個字符。如果你在輸入框連續(xù)輸入,且輸入的間隔小于0.5s,那么在停止輸入的0.5s后,控制臺輸出輸入框中的信息。這可以實現(xiàn)輸入框的字符聯(lián)想或自動搜索功能,并避免過于頻繁的后端請求。
Demo15中使用了clearTimeout方法。clearTimeout是一個全局方法,它的參數(shù)是setTimeout()返回的timeoutId,調用clearTimeout會取消setTimeout建立的timeout任務[21]。
<body>
<input type="text">
</body>
<script>
function?debounce?(func, time)?{
let?timer =?0
return?function?(...args)?{
timer && clearTimeout(timer)
timer = setTimeout(()?=>?{
timer =?0
func.apply(this, args)?},
time)
}
}
input = document.getElementsByTagName("input")[0];
input.onkeypress = debounce(
function?()?{
console.log(input.value)?//事件處理邏輯
},
500)
</script>
Demo15 防抖節(jié)流函數(shù)
3)案例3:優(yōu)雅解決按鈕多次連續(xù)點擊
當用戶點擊按鈕向后端發(fā)送請求時,用戶可能會多次連續(xù)點擊。如果每次點擊都觸發(fā)一次請求,可能會出現(xiàn)上一次請求還未返回,又觸發(fā)下一次請求的情況。多次請求一方面會消耗服務端資源;另一方面可能會導致數(shù)據(jù)意外錯誤,比如重復創(chuàng)建表單記錄。Demo16中通過使用lock標記字段解決了這個問題,每次發(fā)送請求前將lock置為true,請求返回將lock置為false,如果點擊按鈕時上一次請求尚未返回,此時lock為true,函數(shù)直接返回,不會發(fā)送新的請求。其中匿名函數(shù)function(postParams)就是一個閉包。
let clickButton = (
function () {
let lock = false
return function (postParams) {
if (lock) return lock = true?// 使用axios發(fā)送請求?
lock = true
axios.post('urlxxx', postParams).then(
// 表單提交成功?
).catch(error => {
// 表單提交出錯?
console.log(error)
}).finally(() => {
// 不管成功失敗 都解鎖?
lock = false
})
}
})()
button.addEventListener('click', clickButton)
Demo16 優(yōu)雅解決按鈕多次連續(xù)點擊
為了避免每個點擊函數(shù)都使用lock標記字段,可以使用裝飾器。如Demo17,使用裝飾器函數(shù)singleClick,當manuDone為true時,可以手動設置函數(shù)done的觸發(fā)時間。當調用test()函數(shù)時,會每隔1s觸發(fā)調用一次print函數(shù),第一次調用print函數(shù)時將lock置為true,同時調用singleClick函數(shù)的參數(shù)func函數(shù),函數(shù)中進行了控制臺輸出,并設置了2s后觸發(fā)done函數(shù),done函數(shù)將lock置為false。從第一次test函數(shù)中的timeout任務執(zhí)行,調用print函數(shù),print函數(shù)中執(zhí)行singleClick函數(shù)的參數(shù)func函數(shù),在調用func函數(shù)時將lock置為true的總耗時大于2s。所以第2次test函數(shù)中的timeout任務執(zhí)行(1s后),和第3次test函數(shù)中的timeout任務執(zhí)行(2s后)時lock仍為false,調用print函數(shù)時,函數(shù)直接返回,不會進行singleClick函數(shù)的參數(shù)func函數(shù)的調用。第4次(3s)后lock已置為true,此時函數(shù)singleClick中func函數(shù)可以正常調用,并在控制臺輸出相應的數(shù)字。所以控制臺輸出數(shù)字的時間間隔為3s,輸出數(shù)字的間隔為3。
function singleClick(func, manuDone = false) {
let lock = false
return function (...args) {
if (lock) return lock = true
lock = true
let done = () => lock = false
if (manuDone)
return func.call(this, ...args, done)
let promise = func.call(this, ...args)
promise ? promise.finally(done) : done()
return promise
}
}
let print = singleClick(
function (i, done) {
console.log('print is called', i)
setTimeout(done, 2000)
}, true)
function test() {
for (let i = 0; i < 10; i++) {
setTimeout(() => {
print(i)
}, i * 1000)
}
}
test();
//print is called 0
//print is called 3
//print is called 6
//print is called 9
Demo17 裝飾器函數(shù)singleClick
使用如Demo17的裝飾器函數(shù)singleClick對Demo16進行改造,得到Demo18。只需在裝飾器函數(shù)singleClick中使用lock字段,不用每個點擊事件函數(shù)clickButton中使用lock字段。當點擊按鈕時,如果上一次請求尚未返回,不會發(fā)送新的請求。其中singleClick函數(shù)的返回值以及done函數(shù)都是閉包。
let clickButton = singleClick(function (postParams) {
if (!checkForm()) return
return axios.post('urlxxx', postParams).then(
// 表單提交成功?
).catch(error => {
// 表單提交出錯?
console.log(error)
})
})
button.addEventListener('click', clickButton)
Demo18 使用裝飾器函數(shù)singeClick解決按鈕多次連續(xù)點擊
4)案例4:使用閉包模擬“封裝”特性
“封裝”是面向對象的特性之一,所謂“封裝”,即一個對象對外隱藏了其內部的一些屬性或者方法的實現(xiàn)細節(jié),外界僅能通過暴露的接口操作該對象。js是比較“*”的語言,所以并沒有類似Java語言那樣提供私有變量或私有方法的定義方式,不過利用閉包,卻可以很好地模擬這個特性。比如游戲開發(fā)中,玩家對象身上通常會有一個經驗屬性,假設為exp,"打怪"、“做任務”、“使用經驗書”等都會增加exp這個值,而在升級的時候又會減掉exp的值,把exp直接暴露給各處業(yè)務來操作顯然是很糟糕的。Demo19中使用閉包將exp隱藏起來,只能通過getExp()和changeExp()函數(shù)操作。
function makePlayer() {
let exp = 0
// 經驗值?
return {
getExp() {
return exp
}, changeExp(delta, sReason = '') {?// log(xxx),記錄變動日志?
exp += delta
}
}
}
let p = makePlayer()
console.log(p.getExp())// 0
p.changeExp(2000)
console.log(p.getExp())?// 2000
Demo19 使用閉包模擬“封裝”特性
1.3.3 其它特性
1.3.3.1 Object.defineProperty[22]
ECMA-262通過屬性的內部屬性描述了屬性的特征。規(guī)范中的這些內部屬性用于JavaScript引擎的實現(xiàn),無法直接通過JavaScript訪問到。屬性名使用兩對中括號括起來以表示其是內部屬性,比如[[Enumerable]]。屬性分為數(shù)據(jù)屬性(data properties)和訪問屬性(access properties)2種,它們具有不同的內部屬性。
1)數(shù)據(jù)屬性
數(shù)據(jù)屬性包含數(shù)據(jù)值的地址([[Value]])。可以從該地址中讀取和寫入value值。數(shù)據(jù)屬性包含4個屬性:
[[Configurable]]—表明屬性能否通過delete刪除,該屬性的內部屬性能否被修改,或該數(shù)據(jù)屬性能否被修改為訪問屬性。默認值為true。
[[Enumerable]]—表明屬性能否在 for…in 循環(huán)和 Object.keys() 中被枚舉。默認值為true。
[[Writable]]—表明屬性的value能否被修改。默認值為true。
[[Value]]—屬性的value。這是一個屬性的value被讀取和寫入的地址。默認值為undefined。
當一個屬性被添加到對象中時,屬性的[[Configurable]]、[[Enumerable]]和[[Writable]]等內部屬性被設置為true,同時[[Value]]屬性被設置為賦予的值。比如Demo20中,person的name屬性被創(chuàng)建并賦值”jack“。這表示[[Value]]被設置為”jack“,對屬性值的修改也會存在[[Value]]中。你可以使用Object.defineProperty()來修改默認的屬性值。這個方法由3個參數(shù),擬修改或新增的屬性所屬的對象,屬性名以及descriptor對象。descriptor對象有configurable、enumerable、 writable和value等4個屬性。你可以修改這些屬性值。對descriptor中的屬性值的修改會影響后續(xù)對屬性或descriptor中屬性的操作。如Demo21,當configurable設置為false,表明屬性不能通過delete刪除,該屬性的除writable之外的屬性不能被修改,或該數(shù)據(jù)屬性不能被修改為訪問屬性。
let person = {
name: "jack"
};
Demo20 定義person對象
let person = {};
Object.defineProperty(person, "name", {
configurable: false,//表明屬性不能通過delete刪除,該屬性的除writable之外的內部屬性不能被修改,或該數(shù)據(jù)屬性不能被修改為訪問屬性
Enumerable: false, //表明屬性不能在 for…in 循環(huán)和 Object.keys() 中被枚舉
writable: false, //表明屬性的value不能被修改
value: "jack"
});
/*驗證configurable: false的作用*/
delete person.name; //false
console.log(person.name);//"jack"
//throw an error,"Uncaught TypeError: Cannot redefine property: name"
Object.defineProperty(person, "name", {
configurable: true,//由fase為true
Enumerable: false,
writable: false,
value: "jack"
});
//throw an error,"Uncaught TypeError: Cannot redefine property: name"
Object.defineProperty(person, "name", {
configurable: true,
Enumerable: false,
get() { //由數(shù)據(jù)屬性修改為訪問屬性
return this.name;
}
});
/*驗證Enumerable: false的作用*/
console.log(Object.keys(person));//[]
/*驗證writable: false的作用*/
console.log(person.name); // "jack"
person.name = "rose";
console.log(person.name); // "jack"
Demo21 通過defineProperty修改屬性的數(shù)據(jù)屬性
2)訪問屬性
訪問屬性不包含數(shù)據(jù)值的地址[[value]]。它包含getter和setter函數(shù)。當一個訪問屬性被讀取,getter函數(shù)被調用;當被寫入,setter函數(shù)被調用,setter函數(shù)的入參是新寫入的value值。訪問屬性包含4個屬性:
[[Configurable]]—表明屬性能否通過delete刪除,該屬性的屬性能否被修改,或該訪問屬性能否被修改為訪數(shù)據(jù)屬性。默認值為true。
[[Enumerable]]—表明屬性能否在 for…in 循環(huán)和 Object.keys() 中被枚舉。默認值為true。
[[Get]]—屬性被讀取時調用該函數(shù)。默認值為undefined。
[[Set]]—屬性被寫入時調用該函數(shù)。默認值為undefined。
你可以使用Object.defineProperty()來修改默認的屬性值。如Demo22,當我們寫入name屬性時,函數(shù)set被調用。
let person = {_name:"jack",cnt:0};
Object.defineProperty(person, "name", {
configurable:true,
enumerable:true,
get() {
return this._name;
},
set(newValue) {
if(newValue){
this._name = newValue
this.cnt ++;
}
}
});
//屬性寫入時調用set()函數(shù)
person.name = "rose";
//屬性寫入時調用get()函數(shù)
console.log(person._name); // rose
console.log(person.cnt);//1
Demo22 通過defineProperty修改屬性的訪問屬性
3)定義多屬性
如果你想定義一個對象中的多個屬性,ECMAScript提供了Object.defineProperties()方法。如Demo23所示,通過Object.defineProperties()定義個對象person中的_name、cnt和name等多個屬性。
Object.defineProperties(person, {
_name:{
value:"jack"
},
cnt:{
value:0
},
name:{
configurable:true,
enumerable:true,
get() {
return this._name;
},
set(newValue) {
if(newValue){
this._name = "rose"
this.cnt ++;
}
}
}
});
Demo23 通過Object.defineProperties定義對象的多個屬性
4)讀取descriptor的屬性值
通過difineProperty定義的屬性,屬性的descriptor可以通過Object.getOwnPropertyDescriptor獲取。也可以通過Object.getOwnPropertyDescriptors一次性獲取所有屬性的descriptor。如Demo24,基于Demo23中的定義的對象person,先使用Object.getOwnPropertyDescriptor()獲取了屬性name的descriptor;然后使用Object.getOwnPropertyDescriptors獲取了person對象的所有屬性的descriptor。
let descriptor = Object.getOwnPropertyDescriptor(person,"name");
console.log(descriptor.configurable);//true
console.log(descriptor.enumerable);//true
console.log(typeof descriptor.get);//"function"
console.log(typeof descriptor.set);//"function"
console.log(Object.getOwnPropertyDescriptors(person));
//{
// cnt: {
// configurable: true
// enumerable: true
// value: 0
// writable: true
// },
// name: {
// configurable: true
// enumerable: true
// get: ? get()
// set: ? set(newValue)
// },
// _name: {
// configurable: true
// enumerable: true
// value: "jack"
// writable: true
// }
//}
Demo24 通過Object的getOwnPropertyDescriptor和getOwnPropertyDescriptors方法獲取屬性的descriptor
1.3.3.2 Object.create(o)[23]
Object.create(o)返回一個以o為原型的對象。
function a(){}
var b = Object.create(a.prototype);
console.log( b.__proto__ === a.prototype ); //true
Demo25 通過Object.create()創(chuàng)建對象
1.3.3.3 字符串方法[24]
Demo26列舉了字符串的部分方法。
//slice(start,end):截取字符串起始索引與結束索引之間的部分,作為新字符串返回
var str = "Apple, Banana, Mango";
var res = str.slice(7,13);
console.log(res); //Banana
Demo26 字符串的部分方法
1.3.3.4 數(shù)組方法[25]
Demo27 列舉了數(shù)組的部分方法。
//拼接數(shù)組:splice() 方法可用于向數(shù)組添加新項:
var fruits = ["Banana", "Orange", "Apple", "Mango"];
fruits.splice(2, 0, "Lemon", "Kiwi");
console.log(fruits); //Banana,Orange,Lemon,Kiwi,Apple,Mango
//位移元素 unshift() 方法(在開頭)向數(shù)組添加新元素,并“反向位移”舊元素:
var fruits = ["Banana", "Orange", "Apple", "Mango"];
fruits.unshift("Lemon"); // 向 fruits 添加新元素 "Lemon"
console.log(fruits); //Lemon,Banana,Orange,Apple,Mango
Demo27 數(shù)組的部分方法
1.3.3.5 with(this)
動態(tài)創(chuàng)建的函數(shù)中,可以使用with(ObjName),比如with(this)或with(document)(document是DOM中的對象)。with(ObjName)后代碼塊的作用域鏈得到增強,對象ObjName(比如this或document)中的函數(shù)或變量可以像本地變量一樣被訪問[26]。如Demo28中,vm是一個vue實例,使用with(vm)后的代碼塊的作用域得到增強,可以在代碼塊中直接訪問vm實例中的函數(shù)_c。
在vue源碼中render函數(shù)中使用了with(this),this指vue實例代理;Demo29是一個簡化的示例。如Demo29,在vue示例代理的handler中定義了方法has(),handler中的方法has()像一個攔截器一樣;在調用代理的方法時,會優(yōu)先調用handler中定義的,如果handler中未定義再調用實例中的。Proxy的使用詳見1.3.4.1節(jié)。在with(vueProxy)代碼塊中調用方法時,js引擎內部會先調用has(vueProxy,方法名)。比如調用方法_d時,js引擎內部會先調用方法has(vueProxy,"_d"),has函數(shù)在handler中定義了,直接調用handler中的has函數(shù),控制臺會輸出該方法在實例中未定義,并拋出錯誤。
function Vue(){
}
vm = new Vue();
vm._c = function createElement(){}
with(vm){
_c();
}
Demo28 使用with(objName)增強代碼塊的作用域
function Vue(){
}
vue = new Vue();
vue._c = function createElement(){}
//為vue實例創(chuàng)建代理vueProxy
const handler = {
has(target, key){
const has = key in target
if (!has){
console.log("Property or method "+ key +" is not defined on the instance")
}
return has;
}
}
vueProxy = new Proxy(vue,handler)
with(vueProxy){
_c();//方法調用時,js引擎內部會調用has(vueProxy,"_c")函數(shù)
_d();//方法調用時,js引擎內部會調用has(vueProxy,"_d")函數(shù)
}
//瀏覽器控制臺輸出:
//Property or method _d is not defined on the instance
//error: Uncaught ReferenceError: _d is not defined at <anonymous>
Demo29 vue源碼的render函數(shù)中使用with(this)的一個簡化案例
1.3.3.6 MessageChannel
MessageChannel實例有2個端口port1和port2,代表2個通信終端。它可以通過將port以參數(shù)的形式傳遞到worker中,使父頁面和worker通過channel進行通信[27]。
可以使用postMessage在主頁面環(huán)境和worker環(huán)境進行往返通信。瀏覽器可以通過worker在主頁面環(huán)境之外分配一個獨立的子環(huán)境,worker和線程有很多相同的特征,worker環(huán)境可以和主環(huán)境平行地執(zhí)行代碼[28]。如Demo31,文件main.js中創(chuàng)建了factorialWorker.js的worker,使用postMessage與worker環(huán)境進行通信,worker環(huán)境接受到信息后又通過postMessage通信回來。
如果要通過channel進行通信,可以通過MessageChannel實現(xiàn)。如Demo33,文件main.js中創(chuàng)建了worker.js的worker,使用postMessage將MessagePort(值為[channel.port1])發(fā)送worker;Demo32中的woker接收到信息中的MessagePort,并為MessagePort設置message Handler。Demo33中使用終端port2通過channel發(fā)送信息,Demo32中的終端port1接受到信息后,再通過channel發(fā)出新的信息;Demo33中的終端port2接收到信息,并輸出到控制臺。
MessageChannel也可以在同一個js文件中使用。如Demo34,終端port1發(fā)送信息,終端port2接受到信息并輸出到控制臺。但終端port1接收到信息是異步執(zhí)行的,異步執(zhí)行的原理與setTimeout相似,如Demo35。
function factorial(n) {
let result = 1;
while(n) { result *= n--; }
return result;
}
self.onmessage = ({data}) => {
self.postMessage(`${data}! = ${factorial(data)}`);
};
Demo30 work環(huán)境中的factorialworker.js,使用postMessage通信
const factorialWorker = new Worker('./factorialWorker.js');
factorialWorker.onmessage = ({data}) => console.log(data);
factorialWorker.postMessage(5);
factorialWorker.postMessage(7);
factorialWorker.postMessage(10);
// 控制臺輸出:
// 5! = 120
// 7! = 5040
// 10! = 3628800
Demo31 主環(huán)境中的main.js,使用postMessage通信
let messagePort = null;
function factorial(n) {
let result = 1;
while(n) { result *= n--; }
return result;
}
// Set message handler on global object
self.onmessage = ({ports}) => {
if (!messagePort) {
messagePort = ports[0];
self.onmessage = null;
// Set message handler on global object
messagePort.onmessage = ({data}) => {
// Subsequent messages send data
messagePort.postMessage(`${data}! = ${factorial(data)}`);
};
}
}
Demo32 worker環(huán)境中的worker.js,基于MessageChannel通信
const channel = new MessageChannel();
const factorialWorker = new Worker('./worker.js');
// Send the MessagePort object to the worker.
factorialWorker.postMessage(null, [channel.port1]);
channel.port2.onmessage = ({data}) => console.log(data);
channel.port2.postMessage(5);
//控制臺輸出:
// 5! = 120
Demo33 主環(huán)境中的main.js,基于MessageChannel通信
const channel = new MessageChannel();
channel.port2.onmessage = ({data}) => console.log(data);
channel.port1.postMessage(1);
//控制臺輸出:
//1
Demo34 在同一個js文件中基于MessageChannel通信
setTimeout(() => {
console.log("setTimeout_1");
}, 0);
//使用MessageChannel
const channel = new MessageChannel()
channel.port2.onmessage = ()=>{console.log("onmessage")}
channel.port1.postMessage(1)
setTimeout(() => {
console.log("setTimeout_2");
}, 0);
console.log("After setTimeout");
//控制臺輸出:
//After setTimeout
//setTimeout_1
//onmessage
//setTimeout_2
Demo35 MessageChannel接收到信息后是異步執(zhí)行的
1.3.3.7 call和apply
call和apply是函數(shù)的兩個額外方法,可以通過call和apply方法進行函數(shù)調用。call和apply方法可以接收一個特殊的參數(shù),這個參數(shù)在函數(shù)內部可以通過this引用。apply和call方法的功能相同,只是接收的參數(shù)不同[29]。apply方法有2個參數(shù),第一個參數(shù)表示函數(shù)內部通過this可引用的對象,第二個參數(shù)是一個數(shù)組或arguments對象,如Demo36。你可以在函數(shù)內部使用arguments獲取函數(shù)所有的參數(shù),也可以根據(jù)索引獲取指定的第幾個參數(shù),比如使用argumnets[0]獲取第一個參數(shù)。arguments在引擎內部是一個數(shù)組,但它不是一個Array實例[30]。call方法的參數(shù)個數(shù)不確定,第一個參數(shù)表示函數(shù)內部通過this可引用的對象,第二個及后面的參數(shù)是直接傳遞給函數(shù)的參數(shù),如Demo37。
function sum(num1, num2) {
return num1 + num2;
}
function callSum1(num1, num2) {
return sum.apply(this, arguments); // passing in arguments object
}
function callSum2(num1, num2) {
return sum.apply(this, [num1, num2]); // passing in array
}
console.log(callSum1(10, 10)); // 20
console.log(callSum2(10, 10)); // 20
Demo36 apply方法有2個參數(shù)
let o = {
num: 10
};
function sum(num1, num2) {
return this.num + num1 + num2;
}
num = sum.apply(o, num1,num2) //passing arguments directly
console.log(num); // 30
Demo37 call方法的參數(shù)個數(shù)不確定
1.3.4 es6的一些特性
1.3.4.1 Proxy[51]
proxy通過Proxy構造器創(chuàng)建。Proxy構造器有2個參數(shù),target對象和handler對象。如Demo38,通過Proxy構造器創(chuàng)建了proxy對象,構造器的第一個參數(shù)是被代理的對象target,第二參數(shù)handler為空,所有對proxy的操作都會到達target對象。handler的主要目的是允許自定義trap,它像”基本操作攔截器“一樣。當這些基本操作被proxy調用時,會直接調用proxy中的trap函數(shù),也就是說你可以對基本操作進行攔截和修改。如Demo38,在使用proxy[property], proxy.property, 或Object.create(proxy)[property]來訪問屬性時,會進行基本操作get(),這些基本操作會調用proxy中定義的trap函數(shù)get(),而不會調用javascript引擎中的基本操作get()。基本操作get()不是ECMAScript對象可以直接調用的方法。
const target = {
id: 'target',
};
//handler為空對象
let handler = {
};
let proxy = new Proxy(target, handler);
console.log(proxy.id); // target
//handler中添加方法屬性
handler = {
get() {
return 'target override';
},
};
proxy = new Proxy(target, handler);
console.log(proxy.id); // target override
Demo38 為對象創(chuàng)建代理,可以在handler中自定義方法
1.3.4.2 let
1)let和var的主要差別
let的使用和var很像,最主要的差別是let是塊作用域的,而var是函數(shù)作用域的[31]。
變量的作用域是指變量定義的代碼所在的區(qū)域。全局變量是全局作用域,它可以定義在JavaScript代碼的任何地方;函數(shù)變量是函數(shù)作用域,它只能定義在函數(shù)體中。函數(shù)的參數(shù)被認為是本地變量,它只能定義在函數(shù)體中。在函數(shù)體中,本地變量的優(yōu)先級高于同名的全局變量[48],如Demo39。
var scope = "global";
function checkscope() {
var scope = "local";
return scope;
}
checkscope() //local
Demo39 本地變量的優(yōu)先級高于全局變量
在一些類似C語言的編程語言中,每個使用{}括起來的代碼塊都有自己的作用域,變量不能在作用域外被訪問。ECMAScript6以前的javascript是沒有塊作用域的,使用的是函數(shù)作用域。函數(shù)中定義的變量是函數(shù)作用域,變量可以在整個函數(shù)以及函數(shù)的子函數(shù)中訪問,如Demo40。
function checkscope() {
var scope = "local scope";
return function nested() {
return scope;
}
}
checkscope() //local scope
Demo40 var變量可以在整個函數(shù)和整個函數(shù)的子函數(shù)中訪問
ECMAScript6中新增了let關鍵字,它是塊作用域的。如Demo41,代碼塊{}中使用let聲明了變量a,在代碼塊外訪問變量a,控制臺會報變量a未定義的錯誤。這與var關鍵字不同,var是函數(shù)作用域的。塊作用域是函數(shù)作用域的子作用域。在for循環(huán)的迭代變量的聲明中,使用var會有迭代變量指向同一個變量的問題[41]。如Demo42,setTimeout是異步執(zhí)行的,在for循環(huán)執(zhí)行完畢后,開始執(zhí)行setTimeout的任務。由于var是函數(shù)作用域的,所以5個setTimeout任務中引用的變量i是同一函數(shù)作用域下的變量,此時變量i的值為5,所以控制臺的輸出結果為5,5,5,5,5。使用let聲明可以解決這個問題,如Demo43,在for循環(huán)執(zhí)行完畢后,開始執(zhí)行setTimeout任務。由于let是塊作用域的,所以5個setTimeout任務中引用的變量i是不同塊作用域下的,它們的值是不同的,所以控制臺輸出結果為0,1,2,3,4。
{
let a = 1;
}
console.log(a) //Uncaught ReferenceError: a is not defined
Demo41 let是塊作用域的
for (var i = 0; i < 5; ++i) {
setTimeout(() => console.log(i), 0)
}
//你期望的控制臺可能是0, 1, 2, 3, 4
//控制臺輸出:
//5, 5, 5, 5, 5
Demo42 在for循環(huán)使用var聲明迭代變量
for (let i = 0; i < 5; ++i) {
setTimeout(() => console.log(i), 0)
}
//控制臺輸出:
//0, 1, 2, 3, 4
Demo43 在for循環(huán)使用let聲明迭代變量
2)let和var的其它差別
使用var變量時會有變量提升的現(xiàn)象[42]。如Demo44,函數(shù)f()中使用var聲明了局部變量scope,它的作用域是函數(shù)f()。在局部變量聲明之前,就可以對局部變量進行訪問,但此時局部變量尚未賦值,所以第一個輸出結果為”undefined“。在變量聲明之前就可以對變量進行訪問,被成為變量提升現(xiàn)象。
var scope = "global";
function f() {
console.log(scope); // "undefined", 而不是 "global"
var scope = "local";
console.log(scope); // "local"
}
f();
Demo44 var變量的變量提升
let與var不同,但也有類似于變量提升的現(xiàn)象[45]。如Demo45,在塊變量scope聲明之前,對scope的訪問會拋出錯誤,而不是”outerBlock“。在塊變量scope聲明之前,對變量scope的訪問就會受影響,是類似于變量提升的現(xiàn)象。var出現(xiàn)變量提升現(xiàn)象,let出現(xiàn)類似于變量提升的現(xiàn)象,原因是var變量在編譯后是undefined的狀態(tài),而let變量在編譯后是uninitialized的狀態(tài)[13]。如Demo44,當函數(shù)f()編譯完成開始執(zhí)行后,第一行輸出變量scope時,函數(shù)變量scope的值為undefined,所以控制臺輸出undefined。如Demo45,當代碼塊編譯完成開始執(zhí)行后,第一行輸出變量scope時,塊變量scope的值處于initialized的狀態(tài),此時的變量無法被訪問,所以控制臺輸出錯誤信息。
let scope = "outerBlock";
{
console.log(scope); //Uncaught ReferenceError: Cannot access 'scope' before initialization,
//而不是 "outerBlock"
let scope = "block";
console.log(scope); // "block"
}
Demo45 let變量類似于變量提升的現(xiàn)象
由于上述原因,Demo46在執(zhí)行時也會拋出錯誤。這是由于在編譯完成開始執(zhí)行代碼塊時,塊變量x處于uninitialized的狀態(tài),此時訪問塊變量會報錯。在for循環(huán)的循環(huán)語句中使用let時沒有這個問題。在for循環(huán)中使用let聲明迭代變量的初始值時,初始值表達式在當前變量作用域外進行計算。如Demo47,for循環(huán)可以正常輸出塊變量x的值;迭代變量的初始值表達式是在塊作用域外計算的。使用let塊語句時也沒有這個問題。如Demo48,let塊語句由包含變量聲明和初始值表達式的塊和代碼塊組成;變量和初始值被放在()中,緊接著是由{}括起來的代碼塊。let塊語句中的初始值表達式并不是代碼塊的一部分,初始值表達式在代碼塊作用域外執(zhí)行。[49]
let x = 1;
{
let x = x + 1; //Uncaught ReferenceError: Cannot access 'x' before initialization
console.log(x);
}
Demo46 let變量存在類似于變量提升的現(xiàn)象
let x = 1;
for(let x = x + 1; x < 5; x ++){
console.log(x); //2,3,4
}
Demo47 for循環(huán)迭代變量的初始值表達式是在塊作用域之外計算的
let x = 1;
let (x = x + 1){
console.log(x + 1);//3
}
console.log(x + 1);//2
Demo48 let塊語句中的初始值表達式是在塊作用域之外計算的
不可以在同一個塊作用域下使用let重復聲明同一個變量,當使用let和var混合聲明同名變量也是不允許的[41]。如Demo49,當重復聲明同名變量時,會報出SyntaxError的錯誤。
var name;
var name;
let age;
let age; // SyntaxError; identifier 'age' has already been declared
//使用let和var混合聲明變量
var name2;
let name2; // SyntaxError
let age2;
var age2; // SyntaxError
Demo49 不能使用let重復聲明同一個變量
1.3.4.3 const[31]
const具有l(wèi)et的一些特點,但是用const聲明變量時必須有一個初始值,且變量的值不能更改。嘗試修改變量的值將會拋出運行時錯誤,如Demo50。除了這點不同之外,const基本具有l(wèi)et的其它特點。比如const也是塊作用域的,const變量不可重復聲明等,如Demo51。
const a;//Uncaught SyntaxError: Missing initializer in const declaration
const a = 1;
a = 2; //Uncaught TypeError: Assignment to constant variable
Demo50 const變量的值不能修改
// 不允許重復聲明變量
const name = 'Matt';
const name = 'Nicholas'; // SyntaxError
// const時塊作用域的
const name = 'Matt';
if (true) {
const name = 'Nicholas';
}
console.log(name); // Matt
Demo51 const的一些特性
1.3.4.4 Class
Class是ECMSScript6中引入的新語法結構,所以你對它可能不熟悉。在ECMAScript6之前使用prototype和constructor也能模仿類的行為,但語法顯得冗長和混亂。盡管ECMAScript6中的Class具有典型的面向對象編程的特征,但底層仍然使用prototype和constructor的概念。[32]如Demo52,相比于使用構造器和原型模仿類的行為,使用ES6的Class定義類的語法更簡潔清晰。
//使用構造器和原型模仿類的行為
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.toString = function () {
return '(' + this.x + ', ' + this.y + ')';
};
// 使用ES6的Class定義類
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
Demo52 使用Class定義類
與function類型類似,定義class主要有2種方式,class聲明和class表達式,如Demo53。
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
const point = class{
constructor(x, y) {
this.x = x;
this.y = y;
}
}
Demo53 定義Class的2種方式
class中可以包含構造器方法、實例方法、getter、setter和靜態(tài)類方法等,如Demo54。class包含的這些部分不是必須的,一個空class也是合法的。
class Point {
constructor(x, y) { //構造器方法
this.x = x;
this.y = y;
}
toString() { //實例方法
return '(' + this.x + ', ' + this.y + ')';
}
set coordinate(value){ //setter
[this.x, this.y] = value.split(",");
}
get coordinate(){ //getter
return this.x + "," + this.y;
}
static locate() { //靜態(tài)方法
console.log('class', this);
}
locate(){
console.log('instance',this);
}
}
let point = new Point(10,20);
console.log(point.toString());//(10,20)
point.coordinate = "20,30";
console.log(point.coordinate);//20,30
point.locate();//instance, Point{x:'20',y:'30'}
Point.locate();//class,class Point {}
Demo54 一個包含構造器方法、實例方法、getter、setter和靜態(tài)類方法的class類定義
1.4 vue源碼簡介
1.4.1 flow和typescript
javascript是動態(tài)類型的語言,javascript代碼在運行時,變量被賦予不同的值可能會改變變量的類型。因為變量的類型沒有限制,開發(fā)時可能會有很多類型錯誤,這些錯誤在運行時才會發(fā)現(xiàn)。對于大型項目的開發(fā)來說,會降低開發(fā)效率。為了避免JavaScript中動態(tài)類型的問題,我們需要通過其它語言來寫我們的項目,然后將其編譯成JavaScript[33]。我們需要一種作為JavaScript擴展的語言,來限制變量的類型。臉書的flow和微軟的typescript都提供了JavaScript的靜態(tài)類型擴展。
vue2.0中使用的是臉書的flow[34],在調試的時候需要下載flow插件。typescirpt在vue3.0中全面使用。typescript和flow具有很多的語言特性[35],筆者也只是了解,不過并不影響源碼的閱讀。
1.4.2 vue源碼構建
在github上下載vue2.5.16的源碼[36]。源碼構建使用npm run build命令(Node.js版本為v14.15.1),在構建前需要先使用npm install安裝模塊。如圖14,npm run build實際上執(zhí)行的是node scripts/build.js命令。查看build.js,其中獲取了./config文件的所有build配置,如Demo55。./config中部分構建配置如Demo56,其中包含名稱為“web-full-dev”和”weex-factory“的構建配置,后者的屬性weex為true,構建的目標文件中包含“weex”。使用npm run build命令構建時,會過濾掉目標文件中包含“weex”的構建配置,如Demo55,這些構建配置不會進行構建。其它正常構建的配置在構建時,比如“web-full-dev”配置構建時,源碼中包含的if(__WEEX__)判斷,該判斷為false,所以生成的目標代碼中直接不再包含if(__WEEX__)相關的代碼,如Demo57和Demo58。Weex 是阿里巴巴發(fā)起的跨平臺用戶界面開發(fā)框架,同時也正在 Apache 基金會進行項目孵化,Weex 允許你使用 Vue 語法開發(fā)不僅僅可以運行在瀏覽器端,還能被用于開發(fā) iOS 和 Android 上的原生應用的組件[46]。npm run build構建完成后,會生成多個文件,本文的案例中使用的是vue.js。
圖14 vue源碼的構建命令npm run build
//文件目錄:\vue-2.5.16\scripts\build.js
//引入config.js中的所有構建配置
let builds = require('./config').getAllBuilds()
// filter builds via command line arg
if (process.argv[2]) {
const filters = process.argv[2].split(',')
builds = builds.filter(b => {
return filters.some(f => b.output.file.indexOf(f) > -1 || b._name.indexOf(f) > -1)
})
} else {
//過濾掉構建的目標文件中包含"weex"的構建配置
// filter out weex builds by default
builds = builds.filter(b => {
return b.output.file.indexOf('weex') === -1
})
}
build(builds)
Demo55 vue源碼構建的build.js文件
//文件目錄:\vue-2.5.16\scripts\config.js
const builds = {
// Runtime+compiler development build (Browser)
'web-full-dev': {
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.js'),
format: 'umd',
env: 'development',
alias: { he: './entity-decoder' },
banner
},
// Weex runtime factory
'weex-factory': {
weex: true,
entry: resolve('weex/entry-runtime-factory.js'),
//構建的目標文件中包含weex
dest: resolve('packages/weex-vue-framework/factory.js'),
format: 'cjs',
plugins: [weexFactoryPlugin]
}
}
function genConfig (name) {
const opts = builds[name]
const config = {
//此處省略部分代碼
output: {
file: opts.dest,
format: opts.format,
banner: opts.banner,
name: opts.moduleName || 'Vue'
}
}
//此處省略部分代碼
return config
}
if (process.env.TARGET) {
module.exports = genConfig(process.env.TARGET)
} else {
exports.getBuild = genConfig
exports.getAllBuilds = () => Object.keys(builds).map(genConfig)
}
Demo56 vue源碼構建的config.js文件
export function createPatchFunction (backend) {
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
const tag = vnode.tag
if (isDef(tag)) {
//vue源碼中包含if(__WEEX__)判斷
if (__WEEX__) {
// in Weex, the default insertion order is parent-first.
// List items can be optimized to use children-first insertion
// with append="tree".
const appendAsTree = isDef(data) && isTrue(data.appendAsTree)
if (!appendAsTree) {
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
insert(parentElm, vnode.elm, refElm)
}
createChildren(vnode, children, insertedVnodeQueue)
if (appendAsTree) {
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
insert(parentElm, vnode.elm, refElm)
}
} else {
createChildren(vnode, children, insertedVnodeQueue)
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
insert(parentElm, vnode.elm, refElm)
}
}
}
}
Demo57 vue源碼中的if(__WEEX__)判斷
function createPatchFunction (backend) {
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
var tag = vnode.tag;
if (isDef(tag)) {
/* istanbul ignore if */
{
createChildren(vnode, children, insertedVnodeQueue);
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue);
}
insert(parentElm, vnode.elm, refElm);
}
}
}
}
Demo58 生成的vue.js中不包含if(__WEEX__)判斷
1.4.3 vue.js的調試
vue源碼有很多細節(jié),對細節(jié)不理解時,可以先debug調試看下。vue.js調試只需要以下2個簡單的步驟:
- 在github上下載vue2.5.16的源碼[36];將源碼中dist文件夾下的vue.js放在前端項目的lib文件夾下;通過如圖15的方式在html文件中引入vue.js。
圖15在html文件中引入vue.js
- 在瀏覽器中訪問html頁面。打開F12開發(fā)者工具,如圖16在需要調試的代碼位置上打上斷點。刷新頁面,即可進入斷點開始調試。調試時,可在控制臺輸出相應變量的值,如圖17。
圖16 在vue.js源碼打斷點進行調試
圖17 vue.js調試時,可在控制臺輸出變量的值
2.流程圖繪制
查看vue.js源碼,里面有很多功能。本文只考慮如Demo58的簡單案例,在chrome瀏覽器中執(zhí)行時所經歷的流程。重點描述從new Vue到生成操作DOM的原生js的完整流程,對vue.js有個大概的認識。在流程中會有一些對象,這些對象記錄了從el(new Vue時參數(shù)中的屬性)到vnode的完整流程,它們是el、template、element,ast,函數(shù)render,vnode等。vnode是指虛擬DOM,他包含真實DOM的信息,但這些信息尚未更改到真實DOM上。將vnode和舊的vnode(與真實DOM信息相同)進行對比,只將有差異的節(jié)點(元素)更新到真實DOM上。
vue.js是一個js框架,它會將new Vue編譯成操作DOM的原生js代碼,以實現(xiàn)頁面的變更。同時,new Vue中的data(model)更新后,其掛載的DOM(view)是實時更新的,即Vue的視圖更新是響應式的。這依賴于vue借助原生的js函數(shù)defineProperty,將data中的屬性定義為訪問屬性,在讀取屬性時調用get函數(shù),在寫入屬性時調用set()函數(shù)。在獲取屬性值時將當前vue實例的Wather實例添加到Dep實例的subs中;在修改屬性值時,獲取subs中的Watcher實例,并觸發(fā)Watcher的update方法,重新執(zhí)行函數(shù)render生成vnode,并將vnode和舊的vnode進行對比,將有差異的節(jié)點更新到真實DOM上。每個vue實例都對應一個 Watcher 實例[37],如果有多個vue實例,只會觸發(fā)data所屬的vue實例的Watcher的update方法,對DOM的更新只涉vue實例掛載的DOM區(qū)域。
Demo58中案例的代碼執(zhí)行,這其中有一些過程,下面從vue.js源碼層面進行詳細介紹。如果你對代碼邏輯分支不清楚,或變量的數(shù)據(jù)格式不清楚,可通過代碼調試的方式,先大概熟悉代碼邏輯和變量的數(shù)據(jù)格式。
<body>
<div id = "app" >
<div >{{ username[0] }}</div>
</div>
<script src="./lib/vue.js"></script>
<script>
new Vue({
//指定掛載的 DOM 區(qū)域
el: '#app',
//指定 model 數(shù)據(jù)源
data: {
username: ['張三']
}
});
</script>
</body>
Demo58 一個使用vue.js的簡單案例
2.1 從new Vue到DOM操作
如Demo58的簡單案例在編譯運行時,從new Vue到生成操作DOM的原生js會經過一系列步驟。流程圖1展示了源代碼中先后執(zhí)行了哪些函數(shù)或方法。在流程圖的左側展示的函數(shù)參數(shù)的傳遞以及返回值的返回。流程圖的右側展示了函數(shù)的定義,包含定義全局的函數(shù)常量或在函數(shù)原型中添加函數(shù)。函數(shù)參數(shù)的傳遞及返回值返回的流程中,經歷從el到vnode的轉變,主要經過了如圖中①-⑤的步驟,涉及的變量依次為el、template、ast、code、render、render function和vnode。在3.1節(jié)中將依次介紹這些變量的含義和在實際運行中的數(shù)據(jù)格式。流程圖中每個流程節(jié)點最上方是代碼的所屬文件,文件地址是指在未構建的vue2.5.16源碼的相對地址。
流程從new Vue到生成vnode,然后會執(zhí)行patch方法。new Vue流程中舊vnode是由未解析的原始DOM生成的,它的nodeType值為1。在執(zhí)行到如流程圖2所示的步驟2中的patch方法時,不會調用patchVnode方法由根節(jié)點開始從上至下進行patch,而是直接調用createElm方法。在createElm時分為4個步驟,創(chuàng)建元素、創(chuàng)建子元素、調用鉤子函數(shù)修改元素屬性和將元素添加到父元素下;這些步驟會生成操作DOM的原生js代碼,以對DOM進行操作。創(chuàng)建子元素是遞歸的,當vnode沒有子節(jié)點時,遞歸終止。createElm方法調用后,會移除舊Vnode對應的DOM元素在父元素中移除。
流程圖1 從new Vue到生成操作DOM的原生js
流程圖2 new Vue進行patch操作時直接調用createElm函數(shù)
2.2 響應式更新
對vue實例中data數(shù)據(jù)的更新是響應式的,當data更新后,vue實例掛載的DOM是實時更新的,即頁面視圖是實時更新的。響應式更新主要是使用Object.defineProperty(詳見1.3.3.1)將data中的屬性定義為訪問屬性,在讀取屬性時調用get函數(shù),在寫入屬性時調用set函數(shù)來實現(xiàn)的。如流程圖3的“調用defineReactive函數(shù)”步驟,在get函數(shù)中將當前vue實例的Watcher實例添加到dep.subs中(官方表述為Watcher進行依賴收集[37]),在set函數(shù)中獲取subs中的Watcher實例,并通知Wacher實例調用update方法。后文中Watcher實例用watcher表示,Dep實例用dep表示,Observer實例用observer表示。
流程圖3中,在new Vue后會調用vue的初始化方法,初始化方法中調用initState函數(shù),經過幾個步驟后,到達“調用observe函數(shù)”步驟。在“調用observe函數(shù)”步驟,會創(chuàng)建Observer實例,到達“執(zhí)行Observer構造器”步驟。在“執(zhí)行Observer構造器”步驟中,一方面創(chuàng)建Dep實例,并將this(Observer實例)、dep和value關聯(lián)起來,可以通過value.__ob__.dep找到value的observer的dep;另一方面會判斷value是否是數(shù)組,如果是數(shù)組,則重寫”push“、”pop“等方法(詳見3.2節(jié)),并逐個以數(shù)組元素為參數(shù)調用observe()函數(shù)。如果不是數(shù)組,則調用walk方法,在walk方法中,以obj的逐個鍵為參數(shù)調用defineReactive函數(shù)。在defineReactive函數(shù)中,一方面創(chuàng)建了被閉包(set和get函數(shù))引用的dep,將被閉包引用的dep簡稱為閉包dep;另一方面以屬性值為參數(shù)調用observe函數(shù),并返回childOb。在get函數(shù)中,調用閉包dep的depend方法將當前vue實例的watcher添加到閉包dep.subs中;并且如果childOb存在,則向childOb的dep.subs中添加當前vue實例的watcher,并且如果屬性值是一個數(shù)組,則向每一個數(shù)組元素的observer的dep.subs中添加當前vue實例的watcher。在流程圖4的set函數(shù)中,以newVal為參數(shù)調用observe函數(shù),返回值用childOb接收;并且調用dep.notify方法,通知到observer的dep.subs中的每一個watcher。
在4種情況下會調用observe函數(shù),首次到達“調用observe函數(shù)”步驟時以data為參數(shù)調用;在步驟”執(zhí)行Observer構造器“中,判斷value不是數(shù)組時以value為參數(shù)調用;判斷value是數(shù)組時依次以每個數(shù)組元素為參數(shù)調用;修改屬性值調用set函數(shù)時以屬性值為參數(shù)調用。調用observe函數(shù),重新定義個每個對象屬性的set和get函數(shù)。在get函數(shù)中,將watcher添加到閉包dep.subs中;在set函數(shù)中,通知閉包dep.subs中的所有watcher調用update方法。通過obj['property'] = newVal、obj.property = newVal, 或Object.create(obj).property = newVal的方式修改屬性 ,每個屬性的修改都會調用該屬性的set函數(shù),通知所有watcher調用update方法,進行DOM更新。
但如果屬性值是數(shù)組,調用obj[property].push(newVal)或obj[property][0] = newVal是不會調用屬性property的set函數(shù),這在如Demo59的代碼中也有說明,代碼中dependArray函數(shù)是在步驟”調用defineReactive函數(shù)“中調用的。在步驟”調用defineReactive函數(shù)“的get函數(shù)中,會判斷childOb是否存在,如果存在則向childOb的dep.subs中添加當前vue實例的watcher,這里dep不是閉包dep,是childOb(屬性值的observer)的dep。如果value是數(shù)組,如Demo59,會為每個數(shù)組元素的observer的dep.subs中添加當前Vue實例的watcher,這里dep也不是閉包dep,是observer的dep。observer的dep是在步驟”執(zhí)行Observer構造器“中創(chuàng)建的,當value是數(shù)組時,執(zhí)行數(shù)組的”push“、”pop“等函數(shù)時會通知數(shù)組的observer的dep.subs中的watcher(詳見3.2節(jié)),如果value數(shù)組的元素還是數(shù)組,執(zhí)行子數(shù)組的”push“、”pop“等函數(shù)也會通知子數(shù)組的observer的dep.subs中的watcher。但通過obj[property][0] = newVal不會通知watcher調用update方法,即此時DOM不會實時更新,這是vue2.0的一個Bug。vue3.0沒有這個問題。
//所屬文件:\core\observer\index.js
/**
* Collect dependencies on array elements when the array is touched, since
* we cannot intercept array element access like property getters.
*/
function dependArray (value: Array<any>) {
for (let e, i = 0, l = value.length; i < l; i++) {
e = value[i]
e && e.__ob__ && e.__ob_class Observer_.dep.depend()
if (Array.isArray(e)) {
dependArray(e)
}
}
}
Demo59 dependArray函數(shù)
對如Demo58所示的案例進行修改,添加了“點擊”按鈕。如Demo60,當點擊”點擊“按鈕時,會通過3種方式修改data中username的值。第一種方式this.username = ['李四']會調用屬性username的set函數(shù),通知閉包dep.subs中的watcher調用update方法。第二種方式this.username.unshift('李四')會通知數(shù)組['張三']的observer的dep.subs中的watcher調用update方法。第三種方式this.username.unshift('李四')不會通知watcher,這是vue2.0的bug,vue3.0沒有這個問題。如果username的值為數(shù)組套數(shù)組,比如[['張三']],那么this.username[0].unshift('李四')也會通知數(shù)組['張三']的observer的dep.subs中的watcher。
如流程圖4,通過第一種方式this.username = ['李四']修改username的值之后,會調用屬性的set函數(shù)。在set函數(shù)中,會通知閉包dep中的watcher調用update方法。在update方法中,判斷this.sync是否為true,為true表示同步更新,否則時異步更新(詳見3.3節(jié))。流程圖4中假定this.sync為true,會調用this.run()方法。后續(xù)步驟包括調用render函數(shù)生成新的Vnode等,最后調用vm.__patch__方法。
<body>
<div id = "app" >
<div >{{ username[0] }}</div>
<button v-on:click="handleClick">點擊</button>
</div>
<script src="./lib/vue.js"></script>
<script>
new Vue({
//指定掛載的 DOM 區(qū)域
el: '#app',
//指定 model 數(shù)據(jù)源
data: {
username: ['張三']
},
methods: {
handleClick() {
this.username = ['李四'] //會實時更新
// this.username.unshift('李四') //會實時更新
// this.username[0] = ['李四'] //不會實時更新
}
}
});
</script>
</body>
Demo60 點擊按鈕時修改this.username的值時,視圖是實時更新的
流程圖3 new Vue時將當前vue實例的watcher添加到閉包dep.subs中
流程圖4 從數(shù)據(jù)對象修改通知watcher到調用watcher.update方法到調用vm.__patch__方法
vm.__patch__方法的調用如流程圖5。經過一些步驟后,開始patchVnode函數(shù)。在patchVnode函數(shù)中,先對Vnode和舊Vnode進行patch,然后對它們的子節(jié)點進行patch。如果Vnode和舊Vnode完全相同,則不需修改,否則將Vnode的信息更新到DOM上。Vnode的子節(jié)點和舊Vnode的子節(jié)點同時存在時,會調用updateChildren函數(shù)(詳見3.4節(jié));如果不同時存在,則直接將增加或移除的子節(jié)點更新到DOM上。updateChildren函數(shù)中,會判斷vnode和舊vnode是否存在同類型的子節(jié)點(sameVnode()函數(shù)判斷),如果存在,則以同類型的子節(jié)點為參數(shù)遞歸調用patchVnode函數(shù);否則將增加或移除的節(jié)點更新到DOM上。增加節(jié)點時,會調用createElm()函數(shù),createElm()函數(shù)的定義詳見2.1節(jié)的流程圖2,他會通過遞歸創(chuàng)建子節(jié)點,實現(xiàn)節(jié)點和其所有子節(jié)點相應DOM元素的創(chuàng)建。
流程圖5 數(shù)據(jù)修改時調用patch函數(shù)時,會調用patchVnode函數(shù)逐個節(jié)點進行patch
3.重要細節(jié)
3.1 templat解析生成render[50]
2.1節(jié)介紹了new Vue到DOM操作的完整流程。流程中涉及的對象依次為el、template、ast、code、render、render function和vnode。在2.1節(jié)的流程圖1中,el在new Vue()時作為構造器參數(shù)中的屬性,它的值為”#app“。通過調用getOutHTML(el)獲取template,ast、render function和vnode等也在后續(xù)的流程生成。從el到生成vnode的流程圖可表示為圖18。其中序號對應2.1節(jié)的流程圖1中的步驟。
圖18 從el到vnode的流程
3.1.1 template
通過debug如第2節(jié)Demo58中的案例,template的值如圖19所示。
圖19 簡單案例Demo58在debug時template的值
從el到template的步驟,如2.1節(jié)圖1的步驟①。步驟中如Demo61的函數(shù)Vue.prototype.$mount執(zhí)行,先獲取id為el的值”#app“的元素,然后獲取元素的outerHTML屬性,outerHTML即是template的值。
//所屬文件:\platforms\web\entry-runtime-with-compiler.js
Vue.prototype.$mount = function (el,hydrating){
el = el && query(el) //根據(jù)id獲取元素
const options = this.$options
template = getOuterHTML(el)//獲取元素的outerHTML屬性
const { render, staticRenderFns }= compileToFunctions(template, {
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters,comments
}, this);
options.render = render
options.staticRenderFns = staticRenderFns
return mount.call(this, el, hydrating);
}
Demo61 流程圖1中的步驟①
3.1.2 ast
通過debug,ast的結構如圖20所示。
圖20 簡單案例Demo58在debug時ast的值
ast是抽象語法樹[45],在1.1.3節(jié)也有介紹過。將生成ast對象轉換為抽象語法樹如圖21所示`[38][39]`。
圖21 ast對象轉換成的抽象語法樹
將template解析成ast的步驟,如2.1節(jié)流程圖1的步驟②。在parse時會使用正則表達式按順序從頭到尾匹配template字符串中HTML的開始標簽和結束標簽,解析流程如圖22。先匹配到開始標簽<div id = "app">,解析該標簽并存入棧中。然后匹配到開始標簽<div>,解析該標簽并存入棧中,入棧時判斷棧不為空,將當前標簽和棧頭部的標簽建立父子關系。然后解析到第一個文本,文本元素不用入棧,建立文本元素和棧頭部標簽的父子關系。然后解析到第一個結束標簽,將棧頭部的標簽彈出,此時棧中只剩開始標簽<div id = "app">。然后解析到第二個結束標簽,將棧頭部的標簽彈出。此時,template解析完畢,第一個開始標簽作為root節(jié)點返回。總的來說,在parse時會使用正則表達式按順序從頭到尾匹配template字符串中HTML的開始標簽、結束標簽和文本,開始標簽和文本會被解析為元素對象(如Demo62);在解析的過程中會建立元素間的父子關系,解析后的元素構成ast樹,第一個開始標簽作為root節(jié)點返回(如Demo63)。其實template解析時遇到結束標簽,從棧中彈出一個開始標簽,是為了方便建立標簽間的父子關系的。這個例子沒有說明這一點,以Demo64的例子進行說明。
圖22 將template解析成ast的流程示意圖(入棧的標簽實際已解析為元素對象)
//所屬文件:\compiler\parser\html-parser.js
export function parseHTML (html, options) {
while (html) {//按順序從頭到尾匹配template字符串中HTML的開始標簽、結束標簽和文本
if (!lastTag || !isPlainTextElement(lastTag)) {
let textEnd = html.indexOf('<')
if (textEnd === 0) {
// End tag:
const endTagMatch = html.match(endTag)//匹配結束標簽
if (endTagMatch) {
const curIndex = index
advance(endTagMatch[0].length)//html中已匹配的部分截取掉
parseEndTag(endTagMatch[1], curIndex, index)//解析結束標簽
continue
}
// Start tag:
const startTagMatch = parseStartTag()//匹配開始標簽,html中已匹配的部分截取掉
if (startTagMatch) {
handleStartTag(startTagMatch)//解析開始標簽為element
if (shouldIgnoreFirstNewline(lastTag, html)) {
advance(1)
}
continue
}
}
let text, rest, next
if (textEnd >= 0) {//匹配文本
rest = html.slice(textEnd)
while (
!endTag.test(rest) &&
!startTagOpen.test(rest) &&
!comment.test(rest) &&
!conditionalComment.test(rest)
) {
// < in plain text, be forgiving and treat it as text
next = rest.indexOf('<', 1)
if (next < 0) break
textEnd += next
rest = html.slice(textEnd)
}
text = html.substring(0, textEnd) //獲取文本
advance(textEnd)//html中已匹配的部分截取掉
}
if (options.chars && text) {
options.chars(text)
}
}
}
}
Demo62 template解析之parseHTML函數(shù)
//所屬文件:\compiler\parser\index.js
export function parse (
template: string,
options: CompilerOptions
): ASTElement | void {
parseHTML(template, {
//解析到開始標簽時會執(zhí)行
start (tag, attrs, unary) {
if (!root) {
root = element //將第一個開始標簽設置為ast的根節(jié)點
checkRootConstraints(root)
}
if (currentParent && !element.forbidden) {
if (element.elseif || element.else) {
processIfConditions(element, currentParent)
} else if (element.slotScope) { // scoped slot
currentParent.plain = false
const name = element.slotTarget || '"default"'
;(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element
} else {
currentParent.children.push(element) //在currentParent的子標簽中添加剛入棧的標簽
element.parent = currentParent //將剛入棧的標簽的父標簽設置為currentParent
}
}
if (!unary) {
currentParent = element//設置currentParent為剛入棧標簽
stack.push(element)
} else {
closeElement(element)
}
}
//解析到結束標簽時執(zhí)行
end () {
// remove trailing whitespace
const element = stack[stack.length - 1]
const lastNode = element.children[element.children.length - 1]
if (lastNode && lastNode.type === 3 && lastNode.text === ' ' && !inPre) {
element.children.pop()
}
// pop stack
stack.length -= 1 //彈出棧首標簽
currentParent = stack[stack.length - 1]//設置currentParent為彈棧后的棧首標簽
closeElement(element)
},
}
//解析到文本時會執(zhí)行
chars (text: string) {
const children = currentParent.children
text = inPre || text.trim()
? isTextTag(currentParent) ? text : decodeHTMLCached(text)
// only preserve whitespace if its not right after a starting tag
: preserveWhitespace && children.length ? ' ' : ''
if (text) {
let res
if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {
children.push({
type: 2,
expression: res.expression,
tokens: res.tokens,
text
})
} else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
children.push({ //將文本元素添加為棧頂標簽的子元素
type: 3,
text
})
}
}
}
return root;//返回根節(jié)點
}
Demo63 template解析之parse函數(shù)
在解析如Demo64的template時,在parse時會使用正則表達式按順序從頭到尾匹配template字符串中HTML的開始標簽和結束標簽,解析流程如圖23。先匹配到開始標簽<div id = "parent">,解析該標簽并存入棧中。然后匹配到開始標簽 <div id = "child1">,解析該標簽并存入棧中,入棧時判斷棧不為空,將當前標簽和棧頭部的標簽建立父子關系,如demo63。然后匹配到結束標簽</div>,將棧頭部的標簽彈出,此時棧中只剩開始標簽<div id = "parent">。然后匹配到開始標簽<div id = "child2">,解析該標簽并存入棧中,入棧時判斷棧不為空,將當前標簽和棧頭部的標簽建立父子關系。然后匹配到第二個結束標簽</div>,將棧頭部的標簽彈出,此時棧中只剩開始標簽<div id = "parent">。然后匹配最后一個</div>,匹配完成后,將棧頭部的標簽彈出。此時,template解析完畢,第一個開始標簽作為root節(jié)點返回。可以看出,在template解析時遇到結束標簽,從棧中彈出相應的開始標簽,是為了方便建立標簽間的父子關系的。
<div id = "parent">
<div id = "child1"></div>
<div id = "child2"></div>
</div>
Demo64 說明解析template時使用棧結構的原因的案例
圖23 如Demo64的template解析為ast的流程示意圖(入棧的標簽實際已解析為元素對象)
3.1.3 render function
通過debug,2.1節(jié)圖1中的步驟③中code的值如圖24所示,步驟④中render的值如圖25所示。
圖24 簡單案例Demo58在debug時code的值
圖25 簡單案例Demo58在debug時res.render的值
由ast樹解析為render function經歷了如第2.1節(jié)圖1中的步驟③和④。步驟③中調用函數(shù)generate,根據(jù)ast生成code,render是code的屬性。函數(shù)generate的代碼如Demo65所示,它調用了如Demo66的函數(shù)genElement;genElement中調用了如Demo67的函數(shù)genChildren,genChildern中會調用函數(shù)gen;函數(shù)gen實際值為如Demo68的函數(shù)genNode,genNode會判斷節(jié)點類型,如果是開始標簽節(jié)點,則調用genElement,形成遞歸,如果是文本節(jié)點,則調用如Demo69的函數(shù)genText。當節(jié)點沒有子節(jié)點或節(jié)點為文本節(jié)點時,遞歸會終止。過程中主要的函數(shù)調用可以表示為圖26。generate函數(shù)生成的render字符串,在2.1節(jié)圖1步驟④中,被轉換為render function。
圖26 由ast生成render時genElement函數(shù)的遞歸調用示意圖
//所屬文件:\compiler\codegen\index.js
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
}
}
Demo65 generate函數(shù)
//所屬文件:\compiler\codegen\index.js
export function genElement (el: ASTElement, state: CodegenState): string {
if (el.staticRoot && !el.staticProcessed) {
return genStatic(el, state)
} else if (el.once && !el.onceProcessed) {
return genOnce(el, state)
} else if (el.for && !el.forProcessed) {
return genFor(el, state)
} else if (el.if && !el.ifProcessed) {
return genIf(el, state)
} else if (el.tag === 'template' && !el.slotTarget) {
return genChildren(el, state) || 'void 0'
} else if (el.tag === 'slot') {
return genSlot(el, state)
} else {
// component or element
let code
if (el.component) {
code = genComponent(el.component, el, state)
} else {
const data = el.plain ? undefined : genData(el, state)
const children = el.inlineTemplate ? null : genChildren(el, state, true)//調用genChildren
code = `_c('${el.tag}'${
data ? `,${data}` : '' // data
}${
children ? `,${children}` : '' // children
})`
}
// module transforms
for (let i = 0; i < state.transforms.length; i++) {
code = state.transforms[i](el, code)
}
return code
}
}
Demo66 genElement函數(shù)
//所屬文件:\compiler\codegen\index.js
export function genChildren (
el: ASTElement,
state: CodegenState,
checkSkip?: boolean,
altGenElement?: Function,
altGenNode?: Function
): string | void {
const children = el.children
if (children.length) { //沒有children時遞歸終止
const el: any = children[0]
// optimize single v-for
if (children.length === 1 &&
el.for &&
el.tag !== 'template' &&
el.tag !== 'slot'
) {
return (altGenElement || genElement)(el, state)
}
const normalizationType = checkSkip
? getNormalizationType(children, state.maybeComponent)
: 0
const gen = altGenNode || genNode //參數(shù)altGenNode為null,取genNode
return `[${children.map(c => gen(c, state)).join(',')}]${ //調用gen(取genNode)函數(shù)
normalizationType ? `,${normalizationType}` : ''
}`
}
}
Demo67 genChildren函數(shù)
//所屬文件:\compiler\codegen\index.js
function genNode (node: ASTNode, state: CodegenState): string {
if (node.type === 1) {//類型為開始標簽節(jié)點
return genElement(node, state) //調用genElemnt,形成遞歸調用
} if (node.type === 3 && node.isComment) {
return genComment(node)
} else {//類型為文本節(jié)點
return genText(node) //如果是文本節(jié)點,遞歸終止
}
}
Demo68 genNode函數(shù)
//所屬文件:\compiler\codegen\index.js
export function genText (text: ASTText | ASTExpression): string {
return `_v(${text.type === 2
? text.expression // no need for () because already wrapped in _s()
: transformSpecialNewlines(JSON.stringify(text.text))
})`
}
Demo69 genText函數(shù)
3.1.4 vnode
通過debug,vnode的結構如圖27所示。vnode的結構很長,圖中未全部顯示。vnode的結構可簡化為Demo70。
圖27 簡單案例Demo58在debug時vnode的值
{
children:[{
tag:"div"
children:[{
text:"張三"
}]
}]
data:{
attrs:{id: 'app'}
}
tag:"div"
}
Demo70 對vnode簡化后的對象
由render function生成vnode如2.1節(jié)流程圖1中的步驟⑤,相關代碼如Demo71所示。Demo71中的render是一個匿名函數(shù)(3.1.3節(jié)圖5),render函數(shù)的函數(shù)體是with(this){return _c('div',{attrs:{"id":"app"}},[_c('div',[_v(_s(username[0]))])])},該函數(shù)體會在執(zhí)行render.call()時執(zhí)行。其中_c指createElement函數(shù)(如Demo73),_v指createTextVNode函數(shù)(如Demo72),_s指是toString函數(shù)(如Demo72),對函數(shù)體替換后得到with(this){return createElement('div',{attrs:{"id":"app"}}, [createElement('div',[createTextVNode(toString(username[0]))])])}。
//所屬文件:\core\instance\render.js
Vue.prototype._render = function (): VNode {
vnode = render.call(vm._renderProxy, vm.$createElement)
}
Demo71 流程圖1中的步驟⑤
//所屬文件:\core\instance\render-helpers\index.js
export function installRenderHelpers (target: any) {
target._o = markOnce
target._n = toNumber
target._s = toString
target._l = renderList
target._t = renderSlot
target._q = looseEqual
target._i = looseIndexOf
target._m = renderStatic
target._f = resolveFilter
target._k = checkKeyCodes
target._b = bindObjectProps
target._v = createTextVNode
target._e = createEmptyVNode
target._u = resolveScopedSlots
target._g = bindObjectListeners
}
Demo72 _v、_s等函數(shù)的定義
//所屬文件:\core\instance\render.js
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
Demo73 函數(shù)_c的定義
函數(shù)體with(this){return createElement(vm,'div',{attrs:{"id":"app"}},[createElement(vm,'div', [createTextVNode(toString(username[0]))])])}開始執(zhí)行。先執(zhí)行函數(shù)createTextVNode(toString(username[0])),函數(shù)createTextVNode的定義如Demo74。由于函數(shù)是在with(this)的代碼塊中,執(zhí)行時作用域得到加強,username[0]指this.username[0];this指調用render.call方法時的第一個參數(shù)vm._renderProxy,vm._renderProxy是vue實例vm的代理,故username[0]的值為‘張三’。函數(shù)執(zhí)行后創(chuàng)建Vnode實例vnode,vnode作為返回值返回,它的節(jié)點信息如Demo75。
//所屬文件:\core\vdom\vnode.js
export function createTextVNode (val: string | number) {
return new VNode(undefined, undefined, undefined, String(val))
}
Demo74 createTextVNode函數(shù)
{
text:"張三"
}
Demo75 對生成的vnode簡化后的對象構成
然后執(zhí)行表達式createElement(vm,'div',[createTextVNode(toString(username[0]))]),createTextVNode(toString(username[0]))剛剛已執(zhí)行,它的返回值是vnode。替換表達式中已執(zhí)行部分,得到createElement(vm,'div',[vnode]),其中vnode的信息如Demo75。表達式執(zhí)行時,調用如Demo76的createElement方法,createElement方法中調用如Demo77的_createElement方法,_createElement中創(chuàng)建了新的Vnode實例vnode,它的children是參數(shù)中的[vnode]。新的vnode作為返回值返回,它的節(jié)點信息如Demo78。
然后執(zhí)行表達式createElement(vm,'div',{attrs:{"id":"app"}},[createElement(vm,'div',[createTextVNode(toString(username[0]))])]),createElement(vm,'div',[createTextVNode(toString(username[0]))])剛剛已執(zhí)行,它的返回值是vnode。替換表達式中已執(zhí)行部分,得到createElement(vm,'div',{attrs:{"id":"app"}},[vnode]),其中vnode的信息如Demo10。表達式執(zhí)行時,調用Demo9的createElement方法,創(chuàng)建了新的Vnode實例vnode,它的children是參數(shù)中中的[vnode],它的data屬性值是參數(shù)中的{attrs:{"id":"app"}}。新的vnode作為返回值返回,它的節(jié)點信息如Demo70。
//所屬文件:\core\instance\render.js
export function createElement (
context: Component,
tag: any,
data: any,
children: any,
normalizationType: any,
alwaysNormalize: boolean
): VNode | Array<VNode> {
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children
children = data
data = undefined
}
if (isTrue(alwaysNormalize)) {
normalizationType = ALWAYS_NORMALIZE
}
return _createElement(context, tag, data, children, normalizationType)
}
Demo76 createElement函數(shù)
//所屬文件:\core\vdom\create-element.js
export function _createElement (
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
if (isDef(data) && isDef((data: any).__ob__)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n' +
'Always create fresh vnode data objects in each render!',
context
)
return createEmptyVNode()
}
// object syntax in v-bind
if (isDef(data) && isDef(data.is)) {
tag = data.is
}
if (!tag) {
// in case of component :is set to falsy value
return createEmptyVNode()
}
// warn against non-primitive key
if (process.env.NODE_ENV !== 'production' &&
isDef(data) && isDef(data.key) && !isPrimitive(data.key)
) {
if (!__WEEX__ || !('@binding' in data.key)) {
warn(
'Avoid using non-primitive value as key, ' +
'use string/number value instead.',
context
)
}
}
// support single function children as default scoped slot
if (Array.isArray(children) &&
typeof children[0] === 'function'
) {
data = data || {}
data.scopedSlots = { default: children[0] }
children.length = 0
}
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children)
}
let vnode, ns
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
if (config.isReservedTag(tag)) {
// platform built-in elements
vnode = new VNode( //1.創(chuàng)建Vnode節(jié)點
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
} else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// component
vnode = createComponent(Ctor, data, context, children, tag)
} else {
// unknown or unlisted namespaced elements
// check at runtime because it may get assigned a namespace when its
// parent normalizes children
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
} else {
// direct component options / constructor
vnode = createComponent(tag, data, context, children)
}
if (Array.isArray(vnode)) {
return vnode
} else if (isDef(vnode)) {
if (isDef(ns)) applyNS(vnode, ns)
if (isDef(data)) registerDeepBindings(data)
return vnode //2.返回vnode節(jié)點
} else {
return createEmptyVNode()
}
}
Demo77 _createElement函數(shù)
{
tag:"div"
children:[{
text:"張三"
}]
}
Demo78 對生成的vnode簡化后的對象構成
剛剛執(zhí)行的render.call()方法有2個參數(shù)(如Demo79),render.call()執(zhí)行時會調用函數(shù)render(如圖28)。render.call()的第一個參數(shù)表示函數(shù)render執(zhí)行時的this;第二個參數(shù)作為render函數(shù)的參數(shù),但render函數(shù)是一個匿名無參函數(shù),所以第二個參數(shù)沒有用到。render.call()的第一個參數(shù)為vm._renderProxy,vm._renderProxy的值是在_init方法執(zhí)行時賦予的,如Demo80。_init方法中調用了如Demo81的initProxy方法,它以hasHandler為參數(shù)創(chuàng)建vue實例的代理vm._renderProxy,hasHandler中定義了has方法。render函數(shù)的函數(shù)體是with(this)的代碼塊,代碼塊執(zhí)行時,會調用has方法判斷_c、_v、_s等函數(shù)在this中是否存在(參見1.3.3.5節(jié)),this指vm._renderProxy。由于vm._renderProxy的hasHander中定義了has方法,直接調用該has方法,不會調用javascript引擎內部的has方法。hasHandler中的has調用時,如果_c、_v、_s等函數(shù)在vm._renderProxy中不存在,則會給出錯誤警告。
//所屬文件:\core\instance\render.js
Vue.prototype._render = function (): VNode {
vnode = render.call(vm._renderProxy, vm.$createElement)
}
Demo79 流程圖1中的步驟⑤
圖28 簡單案例Demo1在debug時res.render的值
//所屬文件:\core\instance\init.js
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
}
}
Demo80 vue原型中的_init方法定義
//所屬文件: \core\instance\proxy.js
if (process.env.NODE_ENV !== 'production') {
initProxy = function initProxy (vm) {
if (hasProxy) {
// determine which proxy handler to use
const options = vm.$options
const handlers = options.render && options.render._withStripped
? getHandler
: hasHandler //此時,render尚未初始化,option.render為undefined,取值hasHandler
vm._renderProxy = new Proxy(vm, handlers)
} else {
vm._renderProxy = vm
}
const hasHandler = {
has (target, key) {
const has = key in target
const isAllowed = allowedGlobals(key) || key.charAt(0) === '_'
if (!has && !isAllowed) {
//如果屬性在對象中不存在,給出錯誤警告
warnNonPresent(target, key)
}
return has || !isAllowed
}
}
const getHandler = {
get (target, key) {
if (typeof key === 'string' && !(key in target)) {
warnNonPresent(target, key)
}
return target[key]
}
}
}
Demo81 initProxy函數(shù)
3.2 響應式更新之數(shù)組方法重寫[40]
如2.2節(jié)的流程圖3,步驟“執(zhí)行Observer構造器”的代碼如Demo82所示。當value是數(shù)組時,會調用protoAugment方法或copyAugment方法。這兩個方法的作用是什么呢?以protoAugment為例進行說明。在protoAugment函數(shù)中,修改value.__proto__為arrayMethods。arrayMethods的相關定義如Demo83。在Demo83中arrayMethods函數(shù)原型繼承了Array的函數(shù)原型,并重寫了”push“、”pop“等7個函數(shù),重寫后的函數(shù)除了執(zhí)行數(shù)組的原功能外,還會調用 ob.observeArray方法和ob.dep.notify方法。ob(this.__ob__)是在Demo82定義的。當調用value的這7個函數(shù)時,調用的其實是重寫后的函數(shù),比如push方法新增一個元素,則以新增元素(push方法入參時轉為數(shù)組格式)為參數(shù)調用observeArray函數(shù);并通知數(shù)組的observer的dep.subs中的所有的watcher調用update方法,dep.subs中的watcher是在步驟”調用defineReactive函數(shù)“的get函數(shù)中添加的(詳見2.2節(jié)第4段)。
//所屬文件:\core\observer\index.js
class Observer {
constructor (value: any) {
//observer的dep
this.dep = new Dep()
def(value, '__ob__', this)
if (Array.isArray(value)) {
const augment = hasProto//判斷瀏覽器是否支持__proto__屬性
? protoAugment//修改數(shù)組對象的__proto__屬性,覆蓋原有方法
: copyAugment//通過Object.defineProperty,覆蓋原有方法
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {
this.walk(value)
}
}
function protoAugment (target, src, keys) {
target.__proto__ = src;
}
Demo82 執(zhí)行Observer構造器,如果value是數(shù)組,重寫數(shù)組部分方法
//所屬文件:\core\observer\array.js
const arrayProto = Array.prototype
//arrayMethods繼承自arrayProto
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method]
//重寫arrayMethods的push、pop等方法
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
// 在數(shù)組中新增的元素的observer的dep.subs中添加當前屬性所屬對象的Watcher
if (inserted) ob.observeArray(inserted)
// 對dep.subs中的Watcher進行通知
ob.dep.notify()
return result
})
})
Demo83 創(chuàng)建繼承于Array的函數(shù)arrayMethods,并重寫部分方法
3.3 響應式更新之異步更新[41]
如2.2節(jié)所述,data中屬性值的更新是響應式的。當data中的屬性值更新,會通知相應的watcher調用update方法。watcher.update方法中對視圖的更新默認是異步的。如2.2節(jié)流程圖4,watcher的update方法中,this.sync默認為false,會調用queueWatcher(this)方法。如Demo84,函數(shù)queueWather中watcher被添加到queue中,然后調用nextTick函數(shù)。如Demo85,函數(shù)nextTick中添加回調函數(shù)flushSchedulerQueue到callback數(shù)組中,然后調用macroTimerFunc函數(shù)。如Demo86,函數(shù)macroTimerFunc在加載vue.js時創(chuàng)建,函數(shù)中異步調用flushCallbacks函數(shù)。如Demo87,flushCallbacks函數(shù)中逐個調用callback數(shù)組中的函數(shù),flushSchedulerQueue被調用。如Demo88,flushSchedulerQueue函數(shù)中遍歷queue中watcher,調用watcher的run方法,進行DOM更新。由于flushCallbacks函數(shù)是異步調用的,所以DOM更新也是異步的。
//所屬文件: \core\observer\scheduler.js
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher) //watcher被添加到queue中
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// queue the flush
if (!waiting) {
waiting = true
nextTick(flushSchedulerQueue)
}
}
}
Demo84 queueWatcher函數(shù)
//所屬文件:\core\util\next-tick.js
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
callbacks.push(() => { //回調函數(shù)flushSchedulerQueue添加到callback數(shù)組中
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
pending = true
if (useMacroTask) {
macroTimerFunc() //調用macroTimerFunc函數(shù)
} else {
microTimerFunc()
}
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
Demo85 nextTick函數(shù)
//所屬文件:\core\util\next-tick.js
// Determine (macro) task defer implementation.
// Technically setImmediate should be the ideal choice, but it's only available
// in IE. The only polyfill that consistently queues the callback after all DOM
// events triggered in the same loop is by using MessageChannel.
/* istanbul ignore if */
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
macroTimerFunc = () => {
setImmediate(flushCallbacks)
}
} else if (typeof MessageChannel !== 'undefined' && (
isNative(MessageChannel) ||
// PhantomJS
MessageChannel.toString() === '[object MessageChannelConstructor]'
)) {
//異步執(zhí)行flushCallbacks函數(shù)
const channel = new MessageChannel()
const port = channel.port2
channel.port1.onmessage = flushCallbacks
macroTimerFunc = () => {
port.postMessage(1)
}
} else {
/* istanbul ignore next */
macroTimerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
Demo86 macroTimerFunc函數(shù)在加載vue.js時定義
//所屬文件:\core\util\next-tick.js
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
Demo87 flushCallbacks函數(shù)
//所屬文件: \core\observer\scheduler.js
function flushSchedulerQueue () {
flushing = true
let watcher, id
// Sort queue before flush.
// This ensures that:
// 1. Components are updated from parent to child. (because parent is always
// created before the child)
// 2. A component's user watchers are run before its render watcher (because
// user watchers are created before the render watcher)
// 3. If a component is destroyed during a parent component's watcher run,
// its watchers can be skipped.
queue.sort((a, b) => a.id - b.id)
// do not cache length because more watchers might be pushed
// as we run existing watchers
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
id = watcher.id
has[id] = null
watcher.run() //調用watcher.run()方法,進行DOM更新
// in dev build, check and stop circular updates.
if (process.env.NODE_ENV !== 'production' && has[id] != null) {
circular[id] = (circular[id] || 0) + 1
if (circular[id] > MAX_UPDATE_COUNT) {
warn(
'You may have an infinite update loop ' + (
watcher.user
? `in watcher with expression "${watcher.expression}"`
: `in a component render function.`
),
watcher.vm
)
break
}
}
}
// keep copies of post queues before resetting state
const activatedQueue = activatedChildren.slice()
const updatedQueue = queue.slice()
resetSchedulerState()
// call component updated and activated hooks
callActivatedHooks(activatedQueue)
callUpdatedHooks(updatedQueue)
// devtool hook
/* istanbul ignore if */
if (devtools && config.devtools) {
devtools.emit('flush')
}
}
Demo88 flushSchedulerQueue函數(shù)
異步執(zhí)行是指當前線程行完成后,才會執(zhí)行異步的函數(shù)。實現(xiàn)異步執(zhí)行有多種方式。Demo86中函數(shù)macroTimerFunc異步調用flushCallbacks函數(shù)使用了3種方式。以setTimeout為例進行說明。如Demo89,setTimeout中的任務不會立馬執(zhí)行,任務會被添加到task任務中[42],直到調用setTimeout的線程終止后才會執(zhí)行[43],所以“After setTimeout”會在“setTimeout”之前輸出。MessageChannel中onmessage的異步執(zhí)行與setTimeout的原理相似,詳見1.3.3.6節(jié)。
如Demo90中所示html頁面,當點擊“點擊”按鈕后,在修改this.username的值時,只會將vue實例的watcher添加到queue中,不會立馬執(zhí)行DOM更新,所以consle.log輸出的值時修改前的”張三“。當?shù)诙涡薷膖his.username之時,watcher已被添加到queue中,不會重復添加。在handclick函數(shù)執(zhí)行完畢后,當前線程終止;開始執(zhí)行flushCallbacks函數(shù),函數(shù)只會執(zhí)行一次,然后遍歷queue中的watcher,調用watcher.run()方法。由于queue已去重,watcher.run方法只會被調用一次,而如果是同步執(zhí)行watcher.run會被調用2次,比較耗時。異步調用是從性能上考慮的。
setTimeout(()=>{console.log("setTimeout")}, 0);
console.log("After setTimeout");
//控制臺輸出:
//After setTimeout
//setTimeout
Demo89 setTimeout的異步執(zhí)行
<body>
<div id = "app" >
<div ref="test">{{ username[0] }}</div>
<button v-on:click="handleClick">點擊</button>
</div>
<script src="./lib/vue.js"></script>
<script>
new Vue({
//指定掛載的 DOM 區(qū)域
el: '#app',
//指定 model 數(shù)據(jù)源
data: {
username: ['張三']
},
methods: {
handleClick() {
this.username = ['李四'] //對DOM的更新是異步的,將watcher添加到queue中
console.log(this.$refs.test.innerText); //張三
this.username = ['王五'] //watcher已添加到queue中,不會重復添加
}
}
});
</script>
</body>
Demo90 一個異步更新DOM的簡單頁面
3.4 updateChildren()方法[44]
2.2節(jié)的流程圖5中,在patchVNode()時,如果oldCh和newCh都存在,則會調用updateChildren函數(shù)。updateChilren的源代碼如Demo91。代碼實現(xiàn)可概括為逐個判斷新子節(jié)點數(shù)組中的子節(jié)點是否存在同類型(sameVnode()函數(shù)判斷)的舊子節(jié)點。如果存在,則以新子節(jié)點和舊子節(jié)點為參數(shù)調用patchVNode。patchVNode后,如果舊子節(jié)點在舊子節(jié)點數(shù)組中的次序與新子節(jié)點在新子節(jié)點數(shù)組中的次序不同,則對相應的DOM元素的次序進行調整。比如,舊子節(jié)點數(shù)組的非首個節(jié)點與新節(jié)點數(shù)組的首個節(jié)點是同類型的,除了調用patchVNode之外,還要通過nodeOps.insertBefore()函數(shù)調整DOM中相應元素的次序。在逐個判斷新子節(jié)點數(shù)組的子節(jié)點是否存在同類型的舊子節(jié)點時,先對新子節(jié)點數(shù)組和舊子節(jié)點數(shù)組的首尾節(jié)點進行判斷,這在某些場景下可以提高運行效率。
//所屬文件: \core\vdom\patch.js
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
let oldStartIdx = 0
let newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
let oldKeyToIdx, idxInOld, vnodeToMove, refElm
// removeOnly is a special flag used only by <transition-group>
// to ensure removed elements stay in correct relative positions
// during leaving transitions
const canMove = !removeOnly
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(newCh)
}
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
//先將舊子節(jié)點數(shù)組和新子節(jié)點數(shù)組按首首、尾首、首尾、尾尾的方式進行同類型子節(jié)點匹配
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
//然后判斷新子節(jié)點數(shù)組的開始節(jié)點在舊子節(jié)點數(shù)組中是否存在同鍵節(jié)點
} else {
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
if (isUndef(idxInOld)) { // New element
//如果不存在同鍵節(jié)點,調用createElm函數(shù)
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
vnodeToMove = oldCh[idxInOld]
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
oldCh[idxInOld] = undefined
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
} else {
// 如果是不同類型的節(jié)點,調用createElm函數(shù)
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}
}
newStartVnode = newCh[++newStartIdx]
}
}
if (oldStartIdx > oldEndIdx) {
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
//添加節(jié)點
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {
//移除父元素parentElm中的子元素oldCh.elm
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
}
Demo91 函數(shù)updateChildren
參考文章:
[1] 朱永盛.WebKit技術內幕[M].北京:電子工業(yè)出版社,2014:46.
[2] 朱永盛.WebKit技術內幕[M].北京:電子工業(yè)出版社,2014:241.
[3] 朱永盛.WebKit技術內幕[M].北京:電子工業(yè)出版社,2014:14.
[4] 渲染頁面:瀏覽器的工作原理.developer.mozilla.org,檢索于2023-10-23.
[5] Matt Frisbie.Professional JavaScript for Web Developers[M].Indianapolis, Indiana.John Wiley & Sons, Inc,2020: 1.
[6] 朱永盛.WebKit技術內幕[M].北京:電子工業(yè)出版社,2014:238-240.
[7] Jackie Yin.深度剖析Javascript 引擎運行原理分析.知乎,2017,檢索于2023-11-17.
[8] Browser engine.WIKIPEDIA,檢索于2023-10-23.
[9] Introduction to Javascript Engines.www.geeksforgeeks.org,檢索于2023-11-17.
[10] Developer FAQ - Why Blink?.www.chromium.org,檢索于2023-11-17.
[11] JavaScript.WIKIPEDIA,檢索于2023-11-17.
[12] Matt Frisbie.Professional JavaScript for Web Developers[M].Indianapolis, Indiana.John Wiley & Sons, Inc,2020: 6.
[13] haoduoyu2099.從底層和內存角度透析Javascript 的執(zhí)行過程.CSDN.2023,檢索于2023-11-18
[14] Matt Frisbie.Professional JavaScript for Web Developers[M].Indianapolis, Indiana.John Wiley & Sons, Inc,2020: 25.
[15] Matt Frisbie.Professional JavaScript for Web Developers[M].Indianapolis, Indiana.John Wiley & Sons, Inc,2020: 274.
[16] 阮一峰.Javascript面向對象編程(二):構造函數(shù)的繼承.www.ruanyifeng.com,檢索于2023-11-17.
[17] Matt Frisbie.Professional JavaScript for Web Developers[M].Indianapolis, Indiana.John Wiley & Sons, Inc,2020: 106-107.
[18] Matt Frisbie.Professional JavaScript for Web Developers[M].Indianapolis, Indiana.John Wiley & Sons, Inc,2020: 379-380.
[19] Matt Frisbie.Professional JavaScript for Web Developers[M].Indianapolis, Indiana.John Wiley & Sons, Inc,2020: 376.
[20] 前端Q群282549184.JavaScript閉包應用介紹.簡書,檢索于2023-11-17.
[21] clearTimeout() global function.developer.mozilla.org,檢索于2023-11-17.
[22] Matt Frisbie.Professional JavaScript for Web Developers[M].Indianapolis, Indiana.John Wiley & Sons, Inc,2020: 252.
[23] JavaScript 對象定義.W3school,檢索于2023-11-17.
[24] JavaScript 字符串方法.W3school,檢索于2023-11-17.
[25] JavaScript 數(shù)組方法.W3school,檢索于2023-11-17.
[26] Matt Frisbie.Professional JavaScript for Web Developers[M].Indianapolis, Indiana.John Wiley & Sons, Inc,2020: 605.
[27] Matt Frisbie.Professional JavaScript for Web Developers[M].Indianapolis, Indiana.John Wiley & Sons, Inc,2020: 984.
[28] Matt Frisbie.Professional JavaScript for Web Developers[M].Indianapolis, Indiana.John Wiley & Sons, Inc,2020: 970.
[29] Matt Frisbie.Professional JavaScript for Web Developers[M].Indianapolis, Indiana.John Wiley & Sons, Inc,2020: 368.
[30] Matt Frisbie.Professional JavaScript for Web Developers[M].Indianapolis, Indiana.John Wiley & Sons, Inc,2020: 353.
[31] Matt Frisbie.Professional JavaScript for Web Developers[M].Indianapolis, Indiana.John Wiley & Sons, Inc,2020: 31-34.
[32] Matt Frisbie.Professional JavaScript for Web Developers[M].Indianapolis, Indiana.John Wiley & Sons, Inc,2020: 302.
[33] Comparing statically typed JavaScript implementations.blog.logrocket.com,檢索于2023-11-17.
[34] Getting Started.flow.org,檢索于2023-11-17.
[35] TypeScript for JavaScript Programmers.www.typescriptlang.org,檢索于2023-11-17.
[36] vuejs.v2.5.17-beta.0.Github,檢索于2023-11-17.
[37] 深入響應式原理.v2.cn.vuejs.org,檢索于2023-11-17.
[38] 郭方超.瀏覽器加載、解析、渲染的流程?.知乎,檢索于2023-11-17.
[39] Jackie Yin.HTML代碼是如何被解析成瀏覽器中的DOM對象的.知乎,檢索于2023-11-17.
[40] answershuto.數(shù)據(jù)綁定原理.Github,檢索于2023-11-17.
[41] answershuto.Vue.js異步更新DOM策略及nextTick.Github,檢索于2023-11-18.
[42] Using promises.developer.mozilla.org,檢索于2023-11-18.
[43] setTimeout() global function.developer.mozilla.org,檢索于2023-11-18.
[44] answershutoVirtualDOM與diff(Vue實現(xiàn)).Github,檢索于2023-11-18.
[45] Abstract syntax tree.WIKIPEDIA,檢索于2023-11-18.
[46] 原生渲染.v2.cn.vuejs.org,檢索于2023-11-17.
[47] Matt Frisbie.Professional JavaScript for Web Developers[M].Indianapolis, Indiana.John Wiley & Sons, Inc,2020: 289.
[48] David Flanagan.JavaScript: The Definitive Guide[M].sebastopol.O’Reilly Media, Inc,2011: 53-54
[49] David Flanagan.JavaScript: The Definitive Guide[M].sebastopol.O’Reilly Media, Inc,2011: 270-271
[50] answershuto.聊聊Vue的template編譯,檢索于2023-11-17.
[51] Matt Frisbie.Professional JavaScript for Web Developers[M].Indianapolis, Indiana.John Wiley & Sons, Inc,2020: 325.
總結
以上是生活随笔為你收集整理的一个简单案例的Vue2.0源码的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 手撕Vuex-安装模块方法
- 下一篇: 手撕Vue-Router-知识储备