日韩av黄I国产麻豆传媒I国产91av视频在线观看I日韩一区二区三区在线看I美女国产在线I麻豆视频国产在线观看I成人黄色短片

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 前端技术 > vue >内容正文

vue

一个简单案例的Vue2.0源码

發(fā)布時間:2023/11/20 vue 72 coder
生活随笔 收集整理的這篇文章主要介紹了 一个简单案例的Vue2.0源码 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

本文學習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'] = newValobj.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._renderProxyvm._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._renderProxyvm._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源码的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。

91在线操 | 精品国产一区二区三区男人吃奶 | 日韩成人免费电影 | 久草在线免费色站 | 国产黄a三级三级三级三级三级 | 亚洲更新最快 | 天天摸天天弄 | 黄色影院在线免费观看 | 96久久 | 成人欧美在线 | 中文字幕一区在线 | 五月婷在线观看 | 在线观看av的网站 | 日本中文在线观看 | 99久久99久久精品国产片 | 99热精品免费观看 | 亚洲国产欧美一区二区三区丁香婷 | 四虎永久免费在线观看 | 中国精品一区二区 | 久久69精品久久久久久久电影好 | 日韩欧美有码在线 | 色综合天天综合 | 亚洲涩涩一区 | 国产精品美女久久久 | 在线观看91 | 四虎影院在线观看av | 精品成人在线 | 五月花激情 | 久久理论视频 | 国产精品一区二区三区电影 | 麻豆视频免费播放 | 成人丝袜 | 超碰在线网| 国产91亚洲 | 日韩电影在线观看一区 | 国产成人精品亚洲 | 毛片在线网 | 国产涩涩网站 | 欧美日韩亚洲精品在线 | 成人午夜影院在线观看 | 日韩精品在线视频 | 精品国产自| 午夜色场 | 免费三级黄色 | 成人免费观看视频大全 | 国产黄色精品 | 成人av电影免费观看 | 97在线观看免费观看高清 | 麻豆国产精品永久免费视频 | 欧美精品黑人性xxxx | 国产又粗又猛又爽又黄的视频免费 | 欧美va天堂va视频va在线 | 国产黄免费在线观看 | 久久不射影院 | 久久福利综合 | 免费在线观看av网址 | 免费在线看成人av | 黄色国产大片 | 香蕉精品在线观看 | 成年人在线观看 | 久久久久女人精品毛片 | 免费久久精品视频 | 韩国中文三级 | 特级xxxxx欧美 | 欧美激情在线网站 | 国产高清av免费在线观看 | 九九精品无码 | 黄色大片视频网站 | 国产一卡久久电影永久 | 欧美日韩在线视频观看 | 精品国产成人av在线免 | 久久美女视频 | 男女视频久久久 | 午夜视频黄 | 日韩欧美在线视频一区二区三区 | 国产999免费视频 | 一本一本久久a久久精品综合小说 | av免费观看高清 | 欧美在线观看视频一区二区 | 亚洲最大av在线播放 | 色五月情| 国产高清日韩欧美 | 国产亚洲精品成人av久久影院 | 久久免费精彩视频 | 爱爱av网| a黄色一级| 国产成人亚洲精品自产在线 | 天天摸天天操天天爽 | 一区二区中文字幕在线播放 | 日日爽天天 | 亚洲精品乱码久久久久久蜜桃91 | 永久免费精品视频 | 伊在线视频 | 在线观看黄网站 | 亚洲精品高清视频 | 一区二区影院 | 一区二区三区中文字幕在线观看 | 高清av免费看 | www.五月婷| 天天综合久久综合 | 久久这里只有精品23 | 亚欧日韩av | av电影一区二区 | 98福利在线 | 亚洲综合成人av | 安徽妇搡bbbb搡bbbb | 狠狠干电影 | 亚洲国产黄色片 | 91看片在线免费观看 | 精品国自产在线观看 | 黄色在线看网站 | 欧美日韩国产一区 | 久久综合色8888 | 免费在线看成人av | 国产成人av片 | 精品女同一区二区三区在线观看 | 欧美精品久久久久久久久久 | 丁香六月天婷婷 | 激情婷婷av | 亚洲女同videos | 99热精品久久 | 亚洲午夜av电影 | 国产成人一区二区三区影院在线 | 99久久精品国产网站 | 五月色综合| 国产日韩在线播放 | 色婷婷综合久久久中文字幕 | 超碰av在线播放 | 亚洲一区日韩在线 | 黄在线免费看 | 超碰在线最新地址 | 丁香六月激情 | 亚洲精品美女久久 | 精品国产91亚洲一区二区三区www | 日韩欧美在线视频一区二区 | 日韩在线观| 一本—道久久a久久精品蜜桃 | 亚洲成人精品 | 久久天堂亚洲 | 国产在线视频不卡 | 欧美日韩高清在线观看 | 四虎成人精品永久免费av | 91精品国产自产老师啪 | 国产成人精品电影久久久 | 国产视频久| 日本中文字幕网站 | 中文字幕在线视频一区 | 美女黄色网在线播放 | 中午字幕在线 | 夜夜干夜夜 | 成人亚洲网 | 91在线观看欧美日韩 | 成人小电影在线看 | 一本一道久久a久久综合蜜桃 | 亚洲国产精品视频在线观看 | 免费国产视频 | 免费在线观看成年人视频 | 成人av一区二区在线观看 | 国产美女黄网站免费 | 久久久久亚洲精品国产 | 蜜桃视频成人在线观看 | 免费看一级片 | av丝袜制服| 97爱爱爱 | 国产美腿白丝袜足在线av | 91在线精品播放 | 国产精品福利无圣光在线一区 | 国产精品涩涩屋www在线观看 | 在线免费观看黄色小说 | 国产精品乱码久久久久 | 麻花天美星空视频 | 成人黄在线 | 伊人激情综合 | 在线免费高清视频 | 狠狠色伊人亚洲综合成人 | 亚洲综合在线视频 | 亚洲精品国偷拍自产在线观看 | 在线国产视频一区 | 人人看人人爱 | 我要看黄色一级片 | 999久久国产精品免费观看网站 | 国产成人精品网站 | 色婷婷欧美 | 99午夜| 2019精品手机国产品在线 | 操操操天天操 | 一区二区三区四区五区在线 | 国产1区2 | 一级性av| 日韩在观看线 | 99色在线播放 | 日韩欧美大片免费观看 | 久久91久久久久麻豆精品 | 在线观看黄色av | 欧美黑人性猛交 | 婷婷色伊人| 最新国产精品亚洲 | 久久精品这里热有精品 | 欧美,日韩 | 怡红院av久久久久久久 | 天天射天天射天天射 | 精品一区 在线 | 麻豆免费在线视频 | av片中文| 日韩视频一二三区 | 亚洲精品午夜国产va久久成人 | 国产成人av | 国产99久久九九精品免费 | 日韩三级免费观看 | 欧美另类亚洲 | 91网页版在线观看 | 色片网站在线观看 | 久久乐九色婷婷综合色狠狠182 | 日本少妇高清做爰视频 | 亚洲成色 | 国产精品乱码高清在线看 | 国产在线视频一区 | 探花系列在线 | www.久热 | 色狠狠久久av五月综合 | 色婷婷天天干 | 久草免费在线视频观看 | 一级黄色免费网站 | 欧美日韩精品在线播放 | 久99久在线视频 | 中文字幕欧美日韩va免费视频 | 91高清不卡 | 在线亚洲小视频 | 亚洲九九| 91中文视频 | 久久久影片 | 亚洲动漫在线观看 | 日韩欧美精品一区二区 | 国产福利中文字幕 | 日韩黄色av网站 | www.国产视频 | 美女中文字幕 | 免费亚洲黄色 | 国产麻豆剧传媒免费观看 | 色福利网站 | 高清在线一区二区 | 欧美大香线蕉线伊人久久 | 五月天com| 欧美一级片免费在线观看 | 九热精品 | 999电影免费在线观看2020 | 中文在线字幕免费观看 | 国产一级视频在线免费观看 | 激情五月婷婷综合 | 国产乱老熟视频网88av | 久久久久久久久久久久影院 | 免费在线一区二区三区 | 日韩在线大片 | 不卡的av在线 | 日本中文字幕电影在线免费观看 | 日韩中文在线字幕 | 国内精品在线观看视频 | 日韩一区精品 | 在线观看中文字幕2021 | 色吊丝在线永久观看最新版本 | 国产手机视频在线 | 91看片在线免费观看 | 亚洲影视九九影院在线观看 | 91成人在线观看高潮 | 99视频在线免费看 | 欧美激情第一区 | 久久久久北条麻妃免费看 | 狠狠色网 | 在线观看资源 | 午夜影院先 | 成年人网站免费观看 | 欧美色综合天天久久综合精品 | 精品一区二区免费视频 | 亚洲综合视频在线 | 成人av免费在线播放 | 综合伊人av| 免费观看91 | 久久福利电影 | 日日躁天天躁 | 天天干,天天射,天天操,天天摸 | 中文乱幕日产无线码1区 | 欧美乱淫视频 | 美女网站视频久久 | 在线视频 影院 | 在线视频 国产 日韩 | 久久久www成人免费毛片麻豆 | 国产精品免费麻豆入口 | 97国产精品亚洲精品 | 欧美日在线 | 在线 欧美 日韩 | 美州a亚洲一视本频v色道 | 黄色软件在线看 | 99视频网址 | 五月婷婷亚洲 | 欧美日韩中文字幕在线视频 | 国产精品毛片一区二区 | 国产一卡二卡四卡国 | 久久综合久久综合这里只有精品 | 手机在线中文字幕 | 精品在线免费观看 | 九九精品毛片 | 亚洲精品免费视频 | 最新国产精品拍自在线播放 | 成人黄色在线看 | 97超级碰碰碰视频在线观看 | www.在线观看视频 | 国产精品久久久久久久久久久久午夜 | 又黄又爽又湿又无遮挡的在线视频 | 天天爱综合 | 亚洲欧洲精品在线 | 欧美国产日韩一区二区三区 | 成人av电影在线观看 | 青青草国产在线 | 色99中文字幕 | 国产精品扒开做爽爽的视频 | 99免在线观看免费视频高清 | 在线免费观看黄色av | 九九亚洲精品 | 国模精品一区二区三区 | 国产精品mm | 韩国视频一区二区三区 | 黄网站a| 天天色天天综合 | 免费看成人片 | 手机在线日韩视频 | 天天干天天操天天射 | 国产91精品看黄网站在线观看动漫 | 中文字幕丝袜一区二区 | 成人永久视频 | 国产精品一区二区三区四 | 日韩视频a| 一区二区三区视频网站 | 久久久久久久久国产 | 91精品免费 | 亚洲国产人午在线一二区 | 国产男男gay做爰 | 国产黄色片免费看 | 精品国产a | 中文av日韩 | 久久久噜噜噜久久久 | 欧美日韩国产一区二区在线观看 | 亚洲色视频 | 婷婷久久网站 | 在线观看国产一区 | 国产日韩中文字幕在线 | 国产精品久久久视频 | 亚洲免费不卡 | www.在线观看视频 | 亚洲国产精品成人精品 | 日日干日日操 | 在线播放精品一区二区三区 | 国产精品乱码久久久 | a成人在线| 国产精品区二区三区日本 | 亚洲精品在线看 | 激情欧美丁香 | 久久资源总站 | 最新av免费| 国产视频日韩视频欧美视频 | 久草国产在线 | 亚洲人成在| 五月天婷婷狠狠 | 日本精品视频网站 | 精品国产一区二区三区男人吃奶 | 国产精品淫 | 啪啪小视频网站 | 国产91av视频在线观看 | 九九热在线视频免费观看 | 亚洲高清在线精品 | 欧美一级久久久久 | 欧美一区二区三区四区夜夜大片 | 日韩成人一级大片 | 看片的网址| av色图天堂网 | www.超碰97.com| 欧洲精品视频一区 | 亚洲狠狠 | 国产一级电影免费观看 | 成人9ⅰ免费影视网站 | 成人av午夜 | 91一区二区三区在线观看 | 91成人在线看 | 天天爽天天做 | 91麻豆网站 | av日韩在线网站 | 91av手机在线观看 | 99色亚洲 | 精品视频在线免费 | 91亚洲夫妻| 91福利在线观看 | 国产精品密入口果冻 | 这里有精品在线视频 | 国产精品欧美精品 | 国产日韩欧美视频 | 91尤物国产尤物福利在线播放 | 天天射狠狠干 | a在线播放 | 欧美日韩一区二区免费在线观看 | 99视频在线看 | 911国产| 一级黄色片在线播放 | 1区2区视频 | 夜夜视频 | 不卡的av在线播放 | 中文字幕在线播放一区二区 | 精品国产一区二区三区日日嗨 | 在线免费视频a | 亚洲人成免费 | 就操操久久 | 国产精品xxxx18a99 | 日韩精品91偷拍在线观看 | 久久精视频 | 亚洲精品国产视频 | 成人精品影视 | 成人在线免费视频 | 一区二区三区观看 | 亚洲成av | 日韩欧美视频二区 | sm免费xx网站 | 亚洲国产成人在线播放 | 日本久久久久 | 日韩精品高清不卡 | 最新日韩中文字幕 | 最新中文字幕在线观看视频 | 五月婷网站 | 天天做天天爱夜夜爽 | 亚洲国产三级 | 国产免费嫩草影院 | 激情综合五月天 | 一本一本久久aa综合精品 | 国产男女无遮挡猛进猛出在线观看 | 久久精品欧美一 | 亚洲精品乱码久久久一二三 | 久久久影院一区二区三区 | 伊人久久国产精品 | 免费亚洲片 | 国产精品久久一区二区无卡 | 国产精品99页 | 欧美日韩在线观看一区二区三区 | 不卡的av在线 | 中文字幕日韩有码 | 亚洲精品久 | 黄在线免费看 | 亚洲人片在线观看 | 日本久久影视 | 天天爽天天射 | www.五月天激情 | 日日干综合| 亚洲免费黄色 | 免费亚洲婷婷 | 激情视频免费在线 | 亚洲精品高清视频在线观看 | 中文字幕在线观看完整 | 中文字幕亚洲精品日韩 | 91丨九色丨蝌蚪丨对白 | 国产91aaa| 日韩在线视频观看免费 | 久久人人爽爽人人爽人人片av | 婷婷久久综合九色综合 | 国产视频欧美视频 | 悠悠av资源片 | 久久中文字幕导航 | 日本色小说视频 | 不卡av在线免费观看 | 国产精品人人做人人爽人人添 | www黄在线| 久久精品国产亚洲精品2020 | 免费亚洲一区二区 | 天天鲁一鲁摸一摸爽一爽 | 国产高清不卡一区二区三区 | 天天摸天天舔天天操 | 午夜婷婷综合 | 最新日韩视频在线观看 | av在线播放亚洲 | 日韩系列在线 | 亚洲 综合 专区 | 高清av网站 | 园产精品久久久久久久7电影 | 在线国产欧美 | 日韩系列在线观看 | 精品久久久久久国产91 | 色婷婷激婷婷情综天天 | 中文字幕精品久久 | 少妇bbbb搡bbbb桶 | 天天爽夜夜爽人人爽曰av | 人人狠狠综合久久亚洲婷 | 午夜视频亚洲 | 美女精品网站 | 99爱在线观看 | 91mv.cool在线观看 | 青草视频在线看 | 一级免费av| 97在线公开视频 | 国产一级淫片免费看 | 国产福利精品视频 | 婷婷亚洲激情 | 久草网站在线 | 天天综合网天天 | 国产成人精品一区二 | 欧美亚洲久久 | 色偷偷网站视频 | 超碰在线免费福利 | 日韩中文字幕国产精品 | 欧美日本在线观看视频 | 国产永久免费高清在线观看视频 | 久久精品aaa | 91在线看视频免费 | 天天天天天干 | 日日夜夜精品免费 | 麻豆视频在线观看免费 | 国产精品一区二区久久 | 国产亚洲成av片在线观看 | 五月婷网| 国产99免费| 国产精品久久久久久久久久久久久久 | 黄在线免费观看 | 久久亚洲私人国产精品va | av在线影片| 深爱激情站 | 日日弄天天弄美女bbbb | 日韩精品一区二区在线视频 | 色综合天天色综合 | 亚洲做受高潮欧美裸体 | 日韩中文字幕亚洲一区二区va在线 | 国产精品综合久久久久久 | 日本黄色大片儿 | 久久国产二区 | 91.精品高清在线观看 | 国产精品毛片一区二区在线看 | 日韩午夜一级片 | 麻豆一二三精选视频 | 99久久精品国产一区 | 日日操天天爽 | 欧美婷婷色 | 亚洲国产中文字幕在线 | 国产精品va在线观看入 | 亚洲精品久久久蜜桃 | 国产不卡精品 | 国产一区二区不卡视频 | 亚洲精品综合久久 | 91视频免费视频 | 成人免费在线播放 | 国产精品麻豆视频 | 91麻豆精品国产91久久久久久 | 日韩黄在线观看 | 国产69精品久久久久久久久久 | 亚洲尺码电影av久久 | 精品国内自产拍在线观看视频 | 美女视频一区二区 | 国产精品视频在线看 | www.夜夜草| 久久久影视 | 国产专区在线看 | 欧美黑人巨大xxxxx | 国产视频精品免费 | 狠狠色噜噜狠狠狠狠2022 | 蜜桃av观看 | 国产精品免费视频网站 | 91麻豆精品国产91久久久更新时间 | 欧美一级淫片videoshd | 天天综合在线观看 | 成人免费观看大片 | 片黄色毛片黄色毛片 | 亚洲成人999 | 精品在线二区 | 精品国产三级a∨在线欧美 免费一级片在线观看 | 九九亚洲视频 | 丁香影院在线 | 成人av.com| 视频在线播放国产 | 中文字幕电影网 | 黄色软件视频网站 | 91精品视频导航 | 超级碰99 | 免费a级黄色毛片 | 一区二区三区免费网站 | 欧美久久久久久久久久久久久 | 久久久久色 | 精品自拍sae8—视频 | 西西444www大胆无视频 | 国产99久久九九精品免费 | 黄色三级网站在线观看 | 亚洲欧美日韩一二三区 | 中文字幕电影高清在线观看 | 日韩免费观看av | 亚洲激情六月 | 久久精品国产亚洲精品 | 中国美女一级看片 | 久久久精品欧美一区二区免费 | 成人在线视频免费 | 国产精品毛片一区视频播不卡 | 国产精品2020 | 在线观看视频97 | 天天插天天狠 | 免费人成在线观看网站 | 久久一久久 | 国产乱老熟视频网88av | 丁香六月网 | 91免费观看| 久久精品视频免费 | 丁香六月天 | av在线电影播放 | 国产一二区免费视频 | 91视频一8mav | 日本丶国产丶欧美色综合 | 日韩在线电影一区二区 | 欧美日产在线观看 | 在线看片91 | 亚洲视频高清 | 女人18片| 四季av综合网站 | 深爱五月激情五月 | 一级性视频 | 黄色午夜网站 | 99午夜| 久操视频在线播放 | 成年人免费在线观看网站 | 美女久久99 | 亚洲色五月| 国产小视频国产精品 | 四虎成人网 | 人人干天天干 | 在线欧美a| 99se视频在线观看 | 中文字幕成人在线观看 | 九九九在线观看视频 | 免费福利在线播放 | 免费一级片在线观看 | 91av中文字幕 | 91色蜜桃| 国产一区二区日本 | 日韩性xxxx | 在线免费观看国产视频 | 亚洲免费国产视频 | 色婷婷av国产精品 | 精品国产诱惑 | 国产一级视屏 | 亚洲精品视频在线 | 免费网站黄色 | 国产伦精品一区二区三区免费 | 看国产黄色片 | 日本高清xxxx | 黄色一集片 | avcom在线 | www.天天干.com | 久草在线免费看视频 | 91香蕉视频在线下载 | 天天干夜夜干 | 国产精品孕妇 | 亚洲在线免费视频 | 在线免费观看黄 | 成人在线观看免费 | 亚洲精品色视频 | 成人理论在线观看 | 国产精品福利久久久 | 色亚洲激情| 一区二区久久久久 | 中文亚洲欧美日韩 | 在线天堂亚洲 | 久久99国产精品视频 | 午夜aaaa | 中文av日韩 | 国产美女网站在线观看 | 亚洲成av片人久久久 | 91精品国自产在线 | 亚洲无人区小视频 | 韩国一区二区在线观看 | 99精品乱码国产在线观看 | 亚洲精品乱码久久久久久蜜桃91 | 国产午夜精品一区二区三区嫩草 | 一区二区三区在线免费观看视频 | 成人免费大片黄在线播放 | 免费a网址 | 日韩免费电影网 | 久久高清av | 一区二区三区精品久久久 | 日本黄色免费大片 | 三级黄色网址 | 欧美日韩国产精品爽爽 | 国产视频久久久久 | 五月天婷亚洲天综合网鲁鲁鲁 | av亚洲产国偷v产偷v自拍小说 | 欧美视频二区 | 亚洲va欧美va人人爽 | 日韩精品首页 | 久久久久 免费视频 | 国产精品一区二区久久精品爱涩 | 亚洲免费精品一区二区 | 成人永久在线 | 五月激情久久久 | 日韩欧美一区二区在线播放 | 夜夜夜夜猛噜噜噜噜噜初音未来 | 成人中文字幕+乱码+中文字幕 | 国产不卡视频在线播放 | 97在线观看免费观看 | 久久伊99综合婷婷久久伊 | 久久国产精品成人免费浪潮 | 久久婷婷一区二区三区 | av丝袜在线 | 欧美日韩另类在线观看 | 久久精品国产亚洲精品2020 | 黄色特一级| 人人狠狠综合久久亚洲婷 | 国产一区二区免费看 | 91av在线精品| 亚洲资源在线观看 | 中文字幕免费国产精品 | 91成人精品一区在线播放69 | 一区二区三区在线观看 | 五月天丁香视频 | 欧美日韩免费看 | 国产生活一级片 | 久久精品人 | 99久高清在线观看视频99精品热在线观看视频 | 五月网婷婷 | 日韩av不卡在线播放 | 日韩高清在线不卡 | 国产精品久久久久久久久久久久 | 久久99国产精品免费 | 91精品久久久久久久久久入口 | 99热官网| 国产精品青青 | 992tv在线观看| 国产精品岛国久久久久久久久红粉 | 久久天天躁夜夜躁狠狠躁2022 | 精品女同一区二区三区在线观看 | 亚洲春色综合另类校园电影 | 免费毛片一区二区三区久久久 | www.亚洲激情.com | 人人澡av | 99 色| 国产精品久久久久久久久久久久冷 | 亚洲一级黄色 | 国产一区二区在线观看免费 | 九九久久国产精品 | 日韩在线短视频 | 五月天电影免费在线观看一区 | 91精品国产92久久久久 | 久久精品亚洲精品国产欧美 | 特级aaa毛片| 91激情视频在线观看 | 在线视频区 | 久久综合九色综合97婷婷女人 | 亚洲色图色 | 人人插人人射 | 热re99久久精品国产99热 | 中文字幕一区二区在线播放 | 国产色就色| 久久免费成人精品视频 | 亚洲精品午夜久久久久久久久久久 | 高清av免费看 | 99精品久久99久久久久 | 在线观看中文字幕网站 | 成人在线免费观看网站 | 91精品视频一区 | 精品免费一区二区三区 | 日韩视频1 | 美女精品 | 97在线免费视频观看 | 天天躁天天狠天天透 | 久草视频精品 | 色91在线视频 | 91福利试看 | 99久久精品久久久久久动态片 | 久久久www免费电影网 | 91成人免费看 | 国产综合91| 日韩有码欧美 | 99热国内精品 | 欧美激情精品久久久久久 | 在线日本看片免费人成视久网 | 国产精品久久一区二区无卡 | 精品久久久一区二区 | 黄色片网站 | 国产精品毛片一区二区 | 天天色天天搞 | 日韩a在线播放 | 久久人91精品久久久久久不卡 | 色97在线| 菠萝菠萝蜜在线播放 | 91丨九色丨蝌蚪丰满 | 97国产大学生情侣白嫩酒店 | 欧美日韩国产一区二 | 国产亚洲精品综合一区91 | a黄色片在线观看 | 色播五月激情综合网 | 亚洲综合激情小说 | 五月婷婷久久综合 | 久久精品一级片 | 亚洲成人精品久久 | 久久久精品视频成人 | 最近乱久中文字幕 | 五月开心网 | 国产 av 日韩 | 激情综合色综合久久综合 | 九七视频在线 | 亚洲精品久久视频 | 久久美女高清视频 | 亚洲天堂网视频 | 国产成人一级 | www黄色| 麻豆综合网| 久久久久综合网 | 麻豆91精品 | 99九九免费视频 | 久久精品国产久精国产 | 欧美一区二区在线 | www一起操 | 美女国产精品 | 亚洲高清在线视频 | 欧美 日韩 国产 中文字幕 | 99久久国产免费,99久久国产免费大片 | 狠狠色丁香婷婷综合橹88 | 久久久久久久久黄色 | 亚洲精品国产精品国自产观看 | 国产精品原创av片国产免费 | 国产视频一二三 | 激情在线免费视频 | 久久综合色综合88 | 人人狠狠 | 欧洲在线免费视频 | 久久久久免费精品视频 | av天天干 | 成人一区二区三区在线 | 国产精品久久久久一区二区三区共 | 色综合中文字幕 | 狠狠色丁香婷婷综合 | 成人精品一区二区三区电影免费 | 日韩中文字幕第一页 | 国产又粗又猛又爽又黄的视频免费 | 91香蕉视频黄 | 免费中文字幕视频 | 日本在线观看一区二区 | 色婷婷www| 日韩r级在线 | 国产精品一区二区三区视频免费 | 日日操网| 97超碰资源总站 | 夜夜操狠狠操 | 国产一区二区在线看 | 国产精品久久久久久久久久不蜜月 | 亚洲综合欧美精品电影 | 在线免费观看麻豆视频 | a视频免费 | 在线观看成人 | 国产九九精品 | 欧美日一级片 | 天天操天天干天天操天天干 | 香蕉影院在线观看 | 色先锋av资源中文字幕 | 不卡的av在线播放 | 久久综合狠狠综合 | 午夜色站 | 久久国语 | 日韩精品电影在线播放 | 亚洲一区在线看 | 狠狠色丁香久久婷婷综合五月 | 天天色天| 中文字幕在线字幕中文 | 国产精品毛片久久久久久久久久99999999 | 91尤物国产尤物福利在线播放 | 久久久久免费精品国产小说色大师 | 久久久久亚洲a | 国产精品va在线播放 | 激情电影在线观看 | 日韩激情在线视频 | 日韩视频一区二区在线 | 成人免费视频播放 | 日本福利视频在线 | 美女视频永久黄网站免费观看国产 | 精品一区二区三区在线播放 | 亚洲精品美女久久17c | a黄色 | 亚洲精品中文在线资源 | 婷婷伊人网 | 麻豆国产视频下载 | 丁香影院在线 | 久久国产精品免费观看 | 亚洲黄色在线播放 | 中文字幕日本特黄aa毛片 | 国产精品久久久亚洲 | 密桃av在线 | 在线观看的a站 | 久久国产精品视频免费看 | 色中色亚洲 | 国产精品久久久久久久久免费看 | jizz999| 三级在线视频观看 | 欧美日韩不卡一区 | 婷婷丁香六月 | 在线日韩精品视频 | 丝袜美腿av | 日韩一区二区在线免费观看 | 日韩黄色免费在线观看 | 伊人资源站 | 日本韩国精品一区二区在线观看 | 国内精品久久影院 | 国产精品日韩在线播放 | 国产成人在线观看免费 | 久久热首页 | 成人国产精品久久久 | 免费av视屏 | 国产区精品在线观看 | 日韩欧美亚洲 | 欧美国产精品久久久久久免费 | 亚洲九九九在线观看 | 91视频大全| 2023国产精品自产拍在线观看 | 婷婷国产一区二区三区 | 天天插综合 | 又黄又爽又无遮挡的视频 | 看毛片的网址 | 久久成人国产精品入口 | 中文在线天堂资源 | 国产一级久久 | 成人一级 | 色www.| 国产麻豆精品95视频 | 日日夜夜操av | 日韩黄在线观看 | 狠狠插狠狠操 | 不卡的av在线播放 | 免费在线色 | 久久综合给合久久狠狠色 | 日本三级吹潮在线 | 黄色网中文字幕 | a午夜电影 | 最新日韩在线观看视频 | 夜夜夜夜夜夜操 | 欧美在线a视频 | 欧美日在线观看 | 日韩久久精品一区二区三区下载 | 天天综合网天天综合色 | 久久免费电影网 | 免费aa大片| 精品视频一区在线观看 | 麻豆影视网站 | 亚洲国产日韩av | 欧美一级免费黄色片 | 国产精品二区在线 | 中文字幕视频网站 | 亚洲国产精品视频在线观看 | 一区二区中文字幕在线播放 | 欧美激情在线看 | 久久久精品欧美一区二区免费 | 一级c片| 欧美久草视频 | 丁香午夜| 在线黄色av | 久久短视频 | 高清国产午夜精品久久久久久 | 免费观看成年人视频 | 国产精品美女www爽爽爽视频 | 精品久久久久久电影 | 操操爽| 亚洲欧美国产精品18p | 在线观看日韩精品视频 | 蜜臀久久99精品久久久无需会员 | 国产 日韩 在线 亚洲 字幕 中文 | 在线 高清 中文字幕 | 最近更新好看的中文字幕 | 国产亚洲人成网站在线观看 | 99热精品国产 | 午夜av不卡 | 97视频在线观看成人 | 激情婷婷av | 日韩视频a | 精品免费一区 | 99精品视频免费全部在线 | 欧美日韩在线精品一区二区 | 免费观看的av网站 | 亚洲韩国一区二区三区 | 久久久午夜精品福利内容 | 最近更新的中文字幕 | 91色影院| 一区 二区电影免费在线观看 | 视频在线一区 | 久久精品国产亚洲精品2020 | 黄色福利网站 | 国产在线高清视频 | 黄色毛片视频免费观看中文 | 欧美午夜精品久久久久久孕妇 | 日韩经典一区二区三区 | 午夜av剧场 | 九九热只有这里有精品 | 亚洲欧洲成人精品av97 | 国产激情免费 | 日韩久久精品一区二区三区 | 久久人人97超碰国产公开结果 | av在线看网站 | 欧美日韩a视频 | 6080yy午夜一二三区久久 | 日韩免费播放 | 国产精品自产拍在线观看桃花 | 狠狠干夜夜操 | 91探花系列在线播放 | 色婷婷av一区二 | 久久成人综合 |