整合營(yíng)銷服務(wù)商

          電腦端+手機(jī)端+微信端=數(shù)據(jù)同步管理

          免費(fèi)咨詢熱線:

          一文帶你了解JavaScript 函數(shù)式編程

          函數(shù)式編程在前端已經(jīng)成為了一個(gè)非常熱門的話題。在最近幾年里,我們看到非常多的應(yīng)用程序代碼庫(kù)里大量使用著函數(shù)式編程思想。

          本文將略去那些晦澀難懂的概念介紹,重點(diǎn)展示在 JavaScript 中到底什么是函數(shù)式的代碼、聲明式與命令式代碼的區(qū)別、以及常見(jiàn)的函數(shù)式模型都有哪些?更多優(yōu)質(zhì)文章請(qǐng)猛戳https://github.com/ljianshu/Blog

          一、什么是函數(shù)式編程

          函數(shù)式編程是一種編程范式,主要是利用函數(shù)把運(yùn)算過(guò)程封裝起來(lái),通過(guò)組合各種函數(shù)來(lái)計(jì)算結(jié)果。函數(shù)式編程意味著你可以在更短的時(shí)間內(nèi)編寫(xiě)具有更少錯(cuò)誤的代碼。舉個(gè)簡(jiǎn)單的例子,假設(shè)我們要把字符串 functional programmingisgreat變成每個(gè)單詞首字母大寫(xiě),我們可以這樣實(shí)現(xiàn):

          1. var string = 'functional programming is great';

          2. var result = string

          3. .split(' ')

          4. .map(v => v.slice(0, 1).toUpperCase + v.slice(1))

          5. .join(' ');

          上面的例子先用 split 把字符串轉(zhuǎn)換數(shù)組,然后再通過(guò) map 把各元素的首字母轉(zhuǎn)換成大寫(xiě),最后通過(guò) join 把數(shù)組轉(zhuǎn)換成字符串。整個(gè)過(guò)程就是 join(map(split(str))),體現(xiàn)了函數(shù)式編程的核心思想:通過(guò)函數(shù)對(duì)數(shù)據(jù)進(jìn)行轉(zhuǎn)換

          由此我們可以得到,函數(shù)式編程有兩個(gè)基本特點(diǎn):

          • 通過(guò)函數(shù)來(lái)對(duì)數(shù)據(jù)進(jìn)行轉(zhuǎn)換

          • 通過(guò)串聯(lián)多個(gè)函數(shù)來(lái)求結(jié)果

          二、對(duì)比聲明式與命令式

          • 命令式:我們通過(guò)編寫(xiě)一條又一條指令去讓計(jì)算機(jī)執(zhí)行一些動(dòng)作,這其中一般都會(huì)涉及到很多繁雜的細(xì)節(jié)。命令式代碼中頻繁使用語(yǔ)句,來(lái)完成某個(gè)行為。比如 for、if、switch、throw 等這些語(yǔ)句。

          • 聲明式:我們通過(guò)寫(xiě)表達(dá)式的方式來(lái)聲明我們想干什么,而不是通過(guò)一步一步的指示。表達(dá)式通常是某些函數(shù)調(diào)用的復(fù)合、一些值和操作符,用來(lái)計(jì)算出結(jié)果值。

          1. //命令式

          2. var CEOs = ;

          3. for(var i = 0; i < companies.length; i++){

          4. CEOs.push(companies[i].CEO)

          5. }


          6. //聲明式

          7. var CEOs = companies.map(c => c.CEO);

          從上面的例子中,我們可以看到聲明式的寫(xiě)法是一個(gè)表達(dá)式,無(wú)需關(guān)心如何進(jìn)行計(jì)數(shù)器迭代,返回的數(shù)組如何收集,它指明的是做什么,而不是怎么做。函數(shù)式編程的一個(gè)明顯的好處就是這種聲明式的代碼,對(duì)于無(wú)副作用的純函數(shù),我們完全可以不考慮函數(shù)內(nèi)部是如何實(shí)現(xiàn)的,專注于編寫(xiě)業(yè)務(wù)代碼。

          三、常見(jiàn)特性

          無(wú)副作用

          指調(diào)用函數(shù)時(shí)不會(huì)修改外部狀態(tài),即一個(gè)函數(shù)調(diào)用 n 次后依然返回同樣的結(jié)果。

          1. var a = 1;

          2. // 含有副作用,它修改了外部變量 a

          3. // 多次調(diào)用結(jié)果不一樣

          4. function test1 {

          5. a++

          6. return a;

          7. }


          8. // 無(wú)副作用,沒(méi)有修改外部狀態(tài)

          9. // 多次調(diào)用結(jié)果一樣

          10. function test2(a) {

          11. return a + 1;

          12. }

          透明引用

          指一個(gè)函數(shù)只會(huì)用到傳遞給它的變量以及自己內(nèi)部創(chuàng)建的變量,不會(huì)使用到其他變量。

          1. var a = 1;

          2. var b = 2;

          3. // 函數(shù)內(nèi)部使用的變量并不屬于它的作用域

          4. function test1 {

          5. return a + b;

          6. }

          7. // 函數(shù)內(nèi)部使用的變量是顯式傳遞進(jìn)去的

          8. function test2(a, b) {

          9. return a + b;

          10. }

          不可變變量

          指的是一個(gè)變量一旦創(chuàng)建后,就不能再進(jìn)行修改,任何修改都會(huì)生成一個(gè)新的變量。使用不可變變量最大的好處是線程安全。多個(gè)線程可以同時(shí)訪問(wèn)同一個(gè)不可變變量,讓并行變得更容易實(shí)現(xiàn)。由于 JavaScript 原生不支持不可變變量,需要通過(guò)第三方庫(kù)來(lái)實(shí)現(xiàn)。(如 Immutable.js,Mori 等等)

          1. var obj = Immutable({ a: 1 });

          2. var obj2 = obj.set('a', 2);

          3. console.log(obj); // Immutable({ a: 1 })

          4. console.log(obj2); // Immutable({ a: 2 })

          函數(shù)是一等公民

          我們常說(shuō)函數(shù)是JavaScript的"第一等公民",指的是函數(shù)與其他數(shù)據(jù)類型一樣,處于平等地位,可以賦值給其他變量,也可以作為參數(shù),傳入另一個(gè)函數(shù),或者作為別的函數(shù)的返回值。下文將要介紹的閉包、高階函數(shù)、函數(shù)柯里化和函數(shù)組合都是圍繞這一特性的應(yīng)用

          四、常見(jiàn)的函數(shù)式編程模型

          1.閉包(Closure)

          如果一個(gè)函數(shù)引用了自由變量,那么該函數(shù)就是一個(gè)閉包。何謂自由變量?自由變量是指不屬于該函數(shù)作用域的變量(所有全局變量都是自由變量,嚴(yán)格來(lái)說(shuō)引用了全局變量的函數(shù)都是閉包,但這種閉包并沒(méi)有什么用,通常情況下我們說(shuō)的閉包是指函數(shù)內(nèi)部的函數(shù))。

          閉包的形成條件:

          • 存在內(nèi)、外兩層函數(shù)

          • 內(nèi)層函數(shù)對(duì)外層函數(shù)的局部變量進(jìn)行了引用

          閉包的用途:可以定義一些作用域局限的持久化變量,這些變量可以用來(lái)做緩存或者計(jì)算的中間量等

          1. // 簡(jiǎn)單的緩存工具

          2. // 匿名函數(shù)創(chuàng)造了一個(gè)閉包

          3. const cache = (function {

          4. const store = {};


          5. return {

          6. get(key) {

          7. return store[key];

          8. },

          9. set(key, val) {

          10. store[key] = val;

          11. }

          12. }

          13. });

          14. console.log(cache) //{get: ?, set: ?}

          15. cache.set('a', 1);

          16. cache.get('a'); // 1

          上面例子是一個(gè)簡(jiǎn)單的緩存工具的實(shí)現(xiàn),匿名函數(shù)創(chuàng)造了一個(gè)閉包,使得 store 對(duì)象 ,一直可以被引用,不會(huì)被回收。

          閉包的弊端:持久化變量不會(huì)被正常釋放,持續(xù)占用內(nèi)存空間,很容易造成內(nèi)存浪費(fèi),所以一般需要一些額外手動(dòng)的清理機(jī)制。

          2.高階函數(shù)

          函數(shù)式編程傾向于復(fù)用一組通用的函數(shù)功能來(lái)處理數(shù)據(jù),它通過(guò)使用高階函數(shù)來(lái)實(shí)現(xiàn)。高階函數(shù)指的是一個(gè)函數(shù)以函數(shù)為參數(shù),或以函數(shù)為返回值,或者既以函數(shù)為參數(shù)又以函數(shù)為返回值

          高階函數(shù)經(jīng)常用于:

          • 抽象或隔離行為、作用,異步控制流程作為回調(diào)函數(shù),promises,monads等

          • 創(chuàng)建可以泛用于各種數(shù)據(jù)類型的功能

          • 部分應(yīng)用于函數(shù)參數(shù)(偏函數(shù)應(yīng)用)或創(chuàng)建一個(gè)柯里化的函數(shù),用于復(fù)用或函數(shù)復(fù)合。

          • 接受一個(gè)函數(shù)列表并返回一些由這個(gè)列表中的函數(shù)組成的復(fù)合函數(shù)。

          JavaScript 語(yǔ)言是原生支持高階函數(shù)的, 例如Array.prototype.map,Array.prototype.filter 和 Array.prototype.reduce 是JavaScript中內(nèi)置的一些高階函數(shù),使用高階函數(shù)會(huì)讓我們的代碼更清晰簡(jiǎn)潔。

          map

          map 方法創(chuàng)建一個(gè)新數(shù)組,其結(jié)果是該數(shù)組中的每個(gè)元素都調(diào)用一個(gè)提供的函數(shù)后返回的結(jié)果。map 不會(huì)改變?cè)瓟?shù)組。

          假設(shè)我們有一個(gè)包含名稱和種類屬性的對(duì)象數(shù)組,我們想要這個(gè)數(shù)組中所有名稱屬性放在一個(gè)新數(shù)組中,如何實(shí)現(xiàn)呢?

          1. // 不使用高階函數(shù)

          2. var animals = [

          3. { name: "Fluffykins", species: "rabbit" },

          4. { name: "Caro", species: "dog" },

          5. { name: "Hamilton", species: "dog" },

          6. { name: "Harold", species: "fish" },

          7. { name: "Ursula", species: "cat" },

          8. { name: "Jimmy", species: "fish" }

          9. ];

          10. var names = ;

          11. for (let i = 0; i < animals.length; i++) {

          12. names.push(animals[i].name);

          13. }

          14. console.log(names); //["Fluffykins", "Caro", "Hamilton", "Harold", "Ursula", "Jimmy"]

          1. // 使用高階函數(shù)

          2. var animals = [

          3. { name: "Fluffykins", species: "rabbit" },

          4. { name: "Caro", species: "dog" },

          5. { name: "Hamilton", species: "dog" },

          6. { name: "Harold", species: "fish" },

          7. { name: "Ursula", species: "cat" },

          8. { name: "Jimmy", species: "fish" }

          9. ];

          10. var names = animals.map(x=>x.name);

          11. console.log(names); //["Fluffykins", "Caro", "Hamilton", "Harold", "Ursula", "Jimmy"]

          filter

          filter 方法會(huì)創(chuàng)建一個(gè)新數(shù)組,其中包含所有通過(guò)回調(diào)函數(shù)測(cè)試的元素。filter 為數(shù)組中的每個(gè)元素調(diào)用一次 callback 函數(shù), callback 函數(shù)返回 true 表示該元素通過(guò)測(cè)試,保留該元素,false 則不保留。filter 不會(huì)改變?cè)瓟?shù)組,它返回過(guò)濾后的新數(shù)組。

          假設(shè)我們有一個(gè)包含名稱和種類屬性的對(duì)象數(shù)組。我們想要?jiǎng)?chuàng)建一個(gè)只包含狗(species: "dog")的數(shù)組。如何實(shí)現(xiàn)呢?

          1. // 不使用高階函數(shù)

          2. var animals = [

          3. { name: "Fluffykins", species: "rabbit" },

          4. { name: "Caro", species: "dog" },

          5. { name: "Hamilton", species: "dog" },

          6. { name: "Harold", species: "fish" },

          7. { name: "Ursula", species: "cat" },

          8. { name: "Jimmy", species: "fish" }

          9. ];

          10. var dogs = ;

          11. for (var i = 0; i < animals.length; i++) {

          12. if (animals[i].species === "dog") dogs.push(animals[i]);

          13. }

          14. console.log(dogs);

          1. // 使用高階函數(shù)

          2. var animals = [

          3. { name: "Fluffykins", species: "rabbit" },

          4. { name: "Caro", species: "dog" },

          5. { name: "Hamilton", species: "dog" },

          6. { name: "Harold", species: "fish" },

          7. { name: "Ursula", species: "cat" },

          8. { name: "Jimmy", species: "fish" }

          9. ];

          10. var dogs = animals.filter(x => x.species === "dog");

          11. console.log(dogs); // {name: "Caro", species: "dog"}

          12. // { name: "Hamilton", species: "dog" }

          reduce

          reduce 方法對(duì)調(diào)用數(shù)組的每個(gè)元素執(zhí)行回調(diào)函數(shù),最后生成一個(gè)單一的值并返回。reduce 方法接受兩個(gè)參數(shù):1)reducer 函數(shù)(回調(diào)),2)一個(gè)可選的 initialValue。

          假設(shè)我們要對(duì)一個(gè)數(shù)組的求和:

          1. // 不使用高階函數(shù)

          2. const arr = [5, 7, 1, 8, 4];

          3. let sum = 0;

          4. for (let i = 0; i < arr.length; i++) {

          5. sum = sum + arr[i];

          6. }

          7. console.log(sum);//25

          1. // 使用高階函數(shù)

          2. const arr = [5, 7, 1, 8, 4];

          3. const sum = arr.reduce((accumulator, currentValue) => accumulator + currentValue,0);

          4. console.log(sum)//25

          我們可以通過(guò)下圖,形象生動(dòng)展示三者的區(qū)別:

          3.函數(shù)柯里化

          柯里化又稱部分求值,柯里化函數(shù)會(huì)接收一些參數(shù),然后不會(huì)立即求值,而是繼續(xù)返回一個(gè)新函數(shù),將傳入的參數(shù)通過(guò)閉包的形式保存,等到被真正求值的時(shí)候,再一次性把所有傳入的參數(shù)進(jìn)行求值。

          1. // 普通函數(shù)

          2. function add(x,y){

          3. return x + y;

          4. }

          5. add(1,2); // 3

          6. // 函數(shù)柯里化

          7. var add = function(x) {

          8. return function(y) {

          9. return x + y;

          10. };

          11. };

          12. var increment = add(1);

          13. increment(2);// 3

          這里我們定義了一個(gè) add 函數(shù),它接受一個(gè)參數(shù)并返回一個(gè)新的函數(shù)。調(diào)用 add 之后,返回的函數(shù)就通過(guò)閉包的方式記住了 add 的第一個(gè)參數(shù)。那么,我們?nèi)绾蝸?lái)實(shí)現(xiàn)一個(gè)簡(jiǎn)易的柯里化函數(shù)呢?

          1. function curryIt(fn) {

          2. // 參數(shù)fn函數(shù)的參數(shù)個(gè)數(shù)

          3. var n = fn.length;

          4. var args = ;

          5. return function(arg) {

          6. args.push(arg);

          7. if (args.length < n) {

          8. return arguments.callee; // 返回這個(gè)函數(shù)的引用

          9. } else {

          10. return fn.apply(this, args);

          11. }

          12. };

          13. }

          14. function add(a, b, c) {

          15. return [a, b, c];

          16. }

          17. var c = curryIt(add);

          18. var c1 = c(1);

          19. var c2 = c1(2);

          20. var c3 = c2(3);

          21. console.log(c3); //[1, 2, 3]

          由此我們可以看出,柯里化是一種“預(yù)加載”函數(shù)的方法,通過(guò)傳遞較少的參數(shù),得到一個(gè)已經(jīng)記住了這些參數(shù)的新函數(shù),某種意義上講,這是一種對(duì)參數(shù)的“緩存”,是一種非常高效的編寫(xiě)函數(shù)的方法!

          4.函數(shù)組合 (Composition)

          前面提到過(guò),函數(shù)式編程的一個(gè)特點(diǎn)是通過(guò)串聯(lián)函數(shù)來(lái)求值。然而,隨著串聯(lián)函數(shù)數(shù)量的增多,代碼的可讀性就會(huì)不斷下降。函數(shù)組合就是用來(lái)解決這個(gè)問(wèn)題的方法。假設(shè)有一個(gè) compose 函數(shù),它可以接受多個(gè)函數(shù)作為參數(shù),然后返回一個(gè)新的函數(shù)。當(dāng)我們?yōu)檫@個(gè)新函數(shù)傳遞參數(shù)時(shí),該參數(shù)就會(huì)「流」過(guò)其中的函數(shù),最后返回結(jié)果。

          1. //兩個(gè)函數(shù)的組合

          2. var compose = function(f, g) {

          3. return function(x) {

          4. return f(g(x));

          5. };

          6. };


          7. //或者

          8. var compose = (f, g) => (x => f(g(x)));

          9. var add1 = x => x + 1;

          10. var mul5 = x => x * 5;

          11. compose(mul5, add1)(2);// =>15

          歡迎關(guān)注公眾號(hào)前端工匠,你的成長(zhǎng)我們一起見(jiàn)證!

          參考文章

          • 珠峰架構(gòu)課(推薦)

          • MDN文檔

          • What is Functional Programming?

          • So You Want to be a Functional Programmer

          • 理解 JavaScript 中的高階函數(shù)

          • 我所了解的函數(shù)式編程

          • JS函數(shù)式編程指南

          • JavaScript函數(shù)式編程(一)

          • 我眼中的 JavaScript 函數(shù)式編程

          覽器的“心”

          瀏覽器的“心”,說(shuō)的就是瀏覽器的內(nèi)核。在研究瀏覽器微觀的運(yùn)行機(jī)制之前,我們首先要對(duì)瀏覽器內(nèi)核有一個(gè)宏觀的把握。

          許多工程師因?yàn)闃I(yè)務(wù)需要,免不了需要去處理不同瀏覽器下代碼渲染結(jié)果的差異性。這些差異性正是因?yàn)闉g覽器內(nèi)核的不同而導(dǎo)致的——瀏覽器內(nèi)核決定了瀏覽器解釋網(wǎng)頁(yè)語(yǔ)法的方式。

          瀏覽器內(nèi)核可以分成兩部分:渲染引擎(Layout Engine 或者 Rendering Engine)和 JS 引擎。早期渲染引擎和 JS 引擎并沒(méi)有十分明確的區(qū)分,但隨著 JS 引擎越來(lái)越獨(dú)立,內(nèi)核也成了渲染引擎的代稱(下文我們將沿用這種叫法)。渲染引擎又包括了 HTML 解釋器、CSS 解釋器、布局、網(wǎng)絡(luò)、存儲(chǔ)、圖形、音視頻、圖片解碼器等等零部件。

          目前市面上常見(jiàn)的瀏覽器內(nèi)核可以分為這四種:Trident(IE)、Gecko(火狐)、Blink(Chrome、Opera)、Webkit(Safari)。

          大家最耳熟能詳?shù)目赡芫褪?Webkit 內(nèi)核了。很多同學(xué)可能會(huì)聽(tīng)說(shuō)過(guò) Chrome 的內(nèi)核就是 Webkit,殊不知 Chrome 內(nèi)核早已迭代為了 Blink。但是換湯不換藥,Blink 其實(shí)也是基于 Webkit 衍生而來(lái)的一個(gè)分支,因此,Webkit 內(nèi)核仍然是當(dāng)下瀏覽器世界真正的霸主。

          下面我們就以 Webkit 為例,對(duì)現(xiàn)代瀏覽器的渲染過(guò)程進(jìn)行一個(gè)深度的剖析。

          開(kāi)啟瀏覽器渲染“黑盒”

          什么是渲染過(guò)程?簡(jiǎn)單來(lái)說(shuō),渲染引擎根據(jù) HTML 文件描述構(gòu)建相應(yīng)的數(shù)學(xué)模型,調(diào)用瀏覽器各個(gè)零部件,從而將網(wǎng)頁(yè)資源代碼轉(zhuǎn)換為圖像結(jié)果,這個(gè)過(guò)程就是渲染過(guò)程(如下圖)。

          從這個(gè)流程來(lái)看,瀏覽器呈現(xiàn)網(wǎng)頁(yè)這個(gè)過(guò)程,宛如一個(gè)黑盒。在這個(gè)神秘的黑盒中,有許多功能模塊,內(nèi)核內(nèi)部的實(shí)現(xiàn)正是這些功能模塊相互配合協(xié)同工作進(jìn)行的。其中我們最需要關(guān)注的,就是HTML 解釋器CSS 解釋器圖層布局計(jì)算模塊視圖繪制模塊JavaScript 引擎這幾大模塊:

          1. HTML 解釋器:將 HTML 文檔經(jīng)過(guò)詞法分析輸出 DOM 樹(shù)。
          2. CSS 解釋器:解析 CSS 文檔, 生成樣式規(guī)則。
          3. 圖層布局計(jì)算模塊:布局計(jì)算每個(gè)對(duì)象的精確位置和大小。
          4. 視圖繪制模塊:進(jìn)行具體節(jié)點(diǎn)的圖像繪制,將像素渲染到屏幕上。
          5. JavaScript 引擎:編譯執(zhí)行 Javascript 代碼。

          瀏覽器渲染過(guò)程解析

          有了對(duì)零部件的了解打底,我們就可以一起來(lái)走一遍瀏覽器的渲染流程了。在瀏覽器里,每一個(gè)頁(yè)面的首次渲染都經(jīng)歷了如下階段(圖中箭頭不代表串行,有一些操作是并行進(jìn)行的,下文會(huì)說(shuō)明):

          • 解析 HTML

          在這一步瀏覽器執(zhí)行了所有的加載解析邏輯,在解析 HTML 的過(guò)程中發(fā)出了頁(yè)面渲染所需的各種外部資源請(qǐng)求。

          • 計(jì)算樣式

          瀏覽器將識(shí)別并加載所有的 CSS 樣式信息與 DOM 樹(shù)合并,最終生成頁(yè)面 render 樹(shù)(:after :before 這樣的偽元素會(huì)在這個(gè)環(huán)節(jié)被構(gòu)建到 DOM 樹(shù)中)。

          • 計(jì)算圖層布局

          頁(yè)面中所有元素的相對(duì)位置信息,大小等信息均在這一步得到計(jì)算。

          • 繪制圖層

          在這一步中瀏覽器會(huì)根據(jù)我們的 DOM 代碼結(jié)果,把每一個(gè)頁(yè)面圖層轉(zhuǎn)換為像素,并對(duì)所有的媒體文件進(jìn)行解碼。

          • 整合圖層,得到頁(yè)面

          最后一步瀏覽器會(huì)合并合各個(gè)圖層,將數(shù)據(jù)由 CPU 輸出給 GPU 最終繪制在屏幕上。(復(fù)雜的視圖層會(huì)給這個(gè)階段的 GPU 計(jì)算帶來(lái)一些壓力,在實(shí)際應(yīng)用中為了優(yōu)化動(dòng)畫(huà)性能,我們有時(shí)會(huì)手動(dòng)區(qū)分不同的圖層)。

          幾棵重要的“樹(shù)”

          上面的內(nèi)容沒(méi)有理解透徹?別著急,我們一起來(lái)捋一捋這個(gè)過(guò)程中的重點(diǎn)——樹(shù)!

          為了使渲染過(guò)程更明晰一些,我們需要給這些”樹(shù)“們一個(gè)特寫(xiě):

          1. DOM 樹(shù):解析 HTML 以創(chuàng)建的是 DOM 樹(shù)(DOM tree ):渲染引擎開(kāi)始解析 HTML 文檔,轉(zhuǎn)換樹(shù)中的標(biāo)簽到 DOM 節(jié)點(diǎn),它被稱為“內(nèi)容樹(shù)”。
          2. CSSOM 樹(shù):解析 CSS(包括外部 CSS 文件和樣式元素)創(chuàng)建的是 CSSOM 樹(shù)。CSSOM 的解析過(guò)程與 DOM 的解析過(guò)程是并行的
          3. 渲染樹(shù):CSSOM 與 DOM 結(jié)合,之后我們得到的就是渲染樹(shù)(Render tree )。
          4. 布局渲染樹(shù):從根節(jié)點(diǎn)遞歸調(diào)用,計(jì)算每一個(gè)元素的大小、位置等,給每個(gè)節(jié)點(diǎn)所應(yīng)該出現(xiàn)在屏幕上的精確坐標(biāo),我們便得到了基于渲染樹(shù)的布局渲染樹(shù)(Layout of the render tree)。
          5. 繪制渲染樹(shù): 遍歷渲染樹(shù),每個(gè)節(jié)點(diǎn)將使用 UI 后端層來(lái)繪制。整個(gè)過(guò)程叫做繪制渲染樹(shù)(Painting the render tree)。

          基于這些“樹(shù)”,我們?cè)偈崂硪环?/p>

          渲染過(guò)程說(shuō)白了,首先是基于 HTML 構(gòu)建一個(gè) DOM 樹(shù),這棵 DOM 樹(shù)與 CSS 解釋器解析出的 CSSOM 相結(jié)合,就有了布局渲染樹(shù)。最后瀏覽器以布局渲染樹(shù)為藍(lán)本,去計(jì)算布局并繪制圖像,我們頁(yè)面的初次渲染就大功告成了。

          之后每當(dāng)一個(gè)新元素加入到這個(gè) DOM 樹(shù)當(dāng)中,瀏覽器便會(huì)通過(guò) CSS 引擎查遍 CSS 樣式表,找到符合該元素的樣式規(guī)則應(yīng)用到這個(gè)元素上,然后再重新去繪制它。

          有心的同學(xué)可能已經(jīng)在思考了,查表是個(gè)花時(shí)間的活,我怎么讓瀏覽器的查詢工作又快又好地實(shí)現(xiàn)呢?OK,講了這么多原理,我們終于引出了我們的第一個(gè)可轉(zhuǎn)化為代碼的優(yōu)化點(diǎn)——CSS 樣式表規(guī)則的優(yōu)化!

          不做無(wú)用功:基于渲染流程的 CSS 優(yōu)化建議

          在給出 CSS 選擇器方面的優(yōu)化建議之前,先告訴大家一個(gè)小知識(shí):CSS 引擎查找樣式表,對(duì)每條規(guī)則都按從右到左的順序去匹配。 看如下規(guī)則:

          #myList li {}
          

          這樣的寫(xiě)法其實(shí)很常見(jiàn)。大家平時(shí)習(xí)慣了從左到右閱讀的文字閱讀方式,會(huì)本能地以為瀏覽器也是從左到右匹配 CSS 選擇器的,因此會(huì)推測(cè)這個(gè)選擇器并不會(huì)費(fèi)多少力氣:#myList 是一個(gè) id 選擇器,它對(duì)應(yīng)的元素只有一個(gè),查找起來(lái)應(yīng)該很快。定位到了 myList 元素,等于是縮小了范圍后再去查找它后代中的 li 元素,沒(méi)毛病。

          事實(shí)上,CSS 選擇符是從右到左進(jìn)行匹配的。我們這個(gè)看似“沒(méi)毛病”的選擇器,實(shí)際開(kāi)銷相當(dāng)高:瀏覽器必須遍歷頁(yè)面上每個(gè) li 元素,并且每次都要去確認(rèn)這個(gè) li 元素的父元素 id 是不是 myList,你說(shuō)坑不坑!

          說(shuō)到坑,不知道大家還記不記得這個(gè)經(jīng)典的通配符:

          * {}
          

          入門 CSS 的時(shí)候,不少同學(xué)拿通配符清除默認(rèn)樣式(我曾經(jīng)也是通配符用戶的一員)。但這個(gè)家伙很恐怖,它會(huì)匹配所有元素,所以瀏覽器必須去遍歷每一個(gè)元素!大家低頭看看自己頁(yè)面里的元素個(gè)數(shù),是不是心涼了——這得計(jì)算多少次呀!

          這樣一看,一個(gè)小小的 CSS 選擇器,也有不少的門道!好的 CSS 選擇器書(shū)寫(xiě)習(xí)慣,可以為我們帶來(lái)非常可觀的性能提升。根據(jù)上面的分析,我們至少可以總結(jié)出如下性能提升的方案:

          1. 避免使用通配符,只對(duì)需要用到的元素進(jìn)行選擇。
          2. 關(guān)注可以通過(guò)繼承實(shí)現(xiàn)的屬性,避免重復(fù)匹配重復(fù)定義。
          3. 少用標(biāo)簽選擇器。如果可以,用類選擇器替代,舉個(gè)例子:

          錯(cuò)誤示范:

          #myList li{}
          

          理想:

          .myList_li {}
          

          不要畫(huà)蛇添足,id 和 class 選擇器不應(yīng)該被多余的標(biāo)簽選擇器拖后腿。

          錯(cuò)誤示范

          .myList#title
          

          理想:

          #title
          

          減少嵌套。后代選擇器的開(kāi)銷是最高的,因此我們應(yīng)該盡量將選擇器的深度降到最低(最高不要超過(guò)三層),盡可能使用類來(lái)關(guān)聯(lián)每一個(gè)標(biāo)簽元素。

          搞定了 CSS 選擇器,萬(wàn)里長(zhǎng)征才剛剛開(kāi)始的第一步。但現(xiàn)在你已經(jīng)理解了瀏覽器的工作過(guò)程,接下來(lái)的征程對(duì)你來(lái)說(shuō)并不再是什么難題~

          告別阻塞:CSS 與 JS 的加載順序優(yōu)化

          說(shuō)完了過(guò)程,我們來(lái)說(shuō)一說(shuō)特性。

          HTML、CSS 和 JS,都具有阻塞渲染的特性。

          HTML 阻塞,天經(jīng)地義——沒(méi)有 HTML,何來(lái) DOM?沒(méi)有 DOM,渲染和優(yōu)化,都是空談。

          那么 CSS 和 JS 的阻塞又是怎么回事呢?

          CSS 的阻塞

          在剛剛的過(guò)程中,我們提到 DOM 和 CSSOM 合力才能構(gòu)建渲染樹(shù)。這一點(diǎn)會(huì)給性能造成嚴(yán)重影響:默認(rèn)情況下,CSS 是阻塞的資源。瀏覽器在構(gòu)建 CSSOM 的過(guò)程中,不會(huì)渲染任何已處理的內(nèi)容。即便 DOM 已經(jīng)解析完畢了,只要 CSSOM 不 OK,那么渲染這個(gè)事情就不 OK(這主要是為了避免沒(méi)有 CSS 的 HTML 頁(yè)面丑陋地“裸奔”在用戶眼前)。

          我們知道,只有當(dāng)我們開(kāi)始解析 HTML 后、解析到 link 標(biāo)簽或者 style 標(biāo)簽時(shí),CSS 才登場(chǎng),CSSOM 的構(gòu)建才開(kāi)始。很多時(shí)候,DOM 不得不等待 CSSOM。因此我們可以這樣總結(jié):

          CSS 是阻塞渲染的資源。需要將它盡早、盡快地下載到客戶端,以便縮短首次渲染的時(shí)間。

          事實(shí)上,現(xiàn)在很多團(tuán)隊(duì)都已經(jīng)做到了盡早(將 CSS 放在 head 標(biāo)簽里)和盡快(啟用 CDN 實(shí)現(xiàn)靜態(tài)資源加載速度的優(yōu)化)。這個(gè)“把 CSS 往前放”的動(dòng)作,對(duì)很多同學(xué)來(lái)說(shuō)已經(jīng)內(nèi)化為一種編碼習(xí)慣。那么現(xiàn)在我們還應(yīng)該知道,這個(gè)“習(xí)慣”不是空穴來(lái)風(fēng),它是由 CSS 的特性決定的。

          JS 的阻塞

          不知道大家注意到?jīng)]有,前面我們說(shuō)過(guò)程的時(shí)候,花了很多筆墨去說(shuō) HTML、說(shuō) CSS。相比之下,JS 的出鏡率也太低了點(diǎn)。

          這當(dāng)然不是因?yàn)?JS 不重要。而是因?yàn)椋谑状武秩具^(guò)程中,JS 并不是一個(gè)非登場(chǎng)不可的角色——沒(méi)有 JS,CSSOM 和 DOM 照樣可以組成渲染樹(shù),頁(yè)面依然會(huì)呈現(xiàn)——即使它死氣沉沉、毫無(wú)交互。

          JS 的作用在于修改,它幫助我們修改網(wǎng)頁(yè)的方方面面:內(nèi)容、樣式以及它如何響應(yīng)用戶交互。這“方方面面”的修改,本質(zhì)上都是對(duì) DOM 和 CSSDOM 進(jìn)行修改。因此 JS 的執(zhí)行會(huì)阻止 CSSOM,在我們不作顯式聲明的情況下,它也會(huì)阻塞 DOM。

          我們通過(guò)一個(gè)例子來(lái)理解一下這個(gè)機(jī)制:

          三個(gè) console 的結(jié)果分別為:

          注:本例僅使用了內(nèi)聯(lián) JS 做測(cè)試。感興趣的同學(xué)可以把這部分 JS 當(dāng)做外部文件引入看看效果——它們的表現(xiàn)一致。

          第一次嘗試獲取 id 為 container 的 DOM 失敗,這說(shuō)明 JS 執(zhí)行時(shí)阻塞了 DOM,后續(xù)的 DOM 無(wú)法構(gòu)建;第二次才成功,這說(shuō)明腳本塊只能找到在它前面構(gòu)建好的元素。這兩者結(jié)合起來(lái),“阻塞 DOM”得到了驗(yàn)證。再看第三個(gè) console,嘗試獲取 CSS 樣式,獲取到的是在 JS 代碼執(zhí)行前的背景色(yellow),而非后續(xù)設(shè)定的新樣式(blue),說(shuō)明 CSSOM 也被阻塞了。那么在阻塞的背后,到底發(fā)生了什么呢?

          我們前面說(shuō)過(guò),JS 引擎是獨(dú)立于渲染引擎存在的。我們的 JS 代碼在文檔的何處插入,就在何處執(zhí)行。當(dāng) HTML 解析器遇到一個(gè) script 標(biāo)簽時(shí),它會(huì)暫停渲染過(guò)程,將控制權(quán)交給 JS 引擎。JS 引擎對(duì)內(nèi)聯(lián)的 JS 代碼會(huì)直接執(zhí)行,對(duì)外部 JS 文件還要先獲取到腳本、再進(jìn)行執(zhí)行。等 JS 引擎運(yùn)行完畢,瀏覽器又會(huì)把控制權(quán)還給渲染引擎,繼續(xù) CSSOM 和 DOM 的構(gòu)建。 因此與其說(shuō)是 JS 把 CSS 和 HTML 阻塞了,不如說(shuō)是 JS 引擎搶走了渲染引擎的控制權(quán)

          現(xiàn)在理解了阻塞的表現(xiàn)與原理,我們開(kāi)始思考一個(gè)問(wèn)題。瀏覽器之所以讓 JS 阻塞其它的活動(dòng),是因?yàn)樗恢?JS 會(huì)做什么改變,擔(dān)心如果不阻止后續(xù)的操作,會(huì)造成混亂。但是我們是寫(xiě) JS 的人,我們知道 JS 會(huì)做什么改變。假如我們可以確認(rèn)一個(gè) JS 文件的執(zhí)行時(shí)機(jī)并不一定非要是此時(shí)此刻,我們就可以通過(guò)對(duì)它使用 defer 和 async 來(lái)避免不必要的阻塞,這里我們就引出了外部 JS 的三種加載方式。

          JS的三種加載方式

          • 正常模式:
          <script src="index.js"></script>
          

          這種情況下 JS 會(huì)阻塞瀏覽器,瀏覽器必須等待 index.js 加載和執(zhí)行完畢才能去做其它事情。

          • async 模式:
          <script async src="index.js"></script>
          

          async 模式下,JS 不會(huì)阻塞瀏覽器做任何其它的事情。它的加載是異步的,當(dāng)它加載結(jié)束,JS 腳本會(huì)立即執(zhí)行

          • defer 模式:
          <script defer src="index.js"></script>
          

          defer 模式下,JS 的加載是異步的,執(zhí)行是被推遲的。等整個(gè)文檔解析完成、DOMContentLoaded 事件即將被觸發(fā)時(shí),被標(biāo)記了 defer 的 JS 文件才會(huì)開(kāi)始依次執(zhí)行。

          從應(yīng)用的角度來(lái)說(shuō),一般當(dāng)我們的腳本與 DOM 元素和其它腳本之間的依賴關(guān)系不強(qiáng)時(shí),我們會(huì)選用 async;當(dāng)腳本依賴于 DOM 元素和其它腳本的執(zhí)行結(jié)果時(shí),我們會(huì)選用 defer。

          通過(guò)審時(shí)度勢(shì)地向 script 標(biāo)簽添加 async/defer,我們就可以告訴瀏覽器在等待腳本可用期間不阻止其它的工作,這樣可以顯著提升性能。

          tm是超文本標(biāo)記語(yǔ)言簡(jiǎn)稱,英文全名是“Hyper Text Markup Language”,是構(gòu)成網(wǎng)站網(wǎng)頁(yè)文檔主要語(yǔ)言,它是一種標(biāo)記語(yǔ)言而不是編程語(yǔ)言,是由一套標(biāo)記標(biāo)簽來(lái)描述網(wǎng)頁(yè)內(nèi)容,這些內(nèi)容主要由瀏覽器解釋(展示給網(wǎng)站訪客),同一個(gè)html文件如果不加處理對(duì)于不同的瀏覽器顯示不同的效果導(dǎo)致出現(xiàn)瀏覽器兼容問(wèn)題,處理瀏覽器兼容問(wèn)題其實(shí)就是要處理同一html文件在不同瀏覽器顯示不同效果的問(wèn)題。

          html從1993年誕生25年以來(lái)從當(dāng)初1.0發(fā)展現(xiàn)在5.0(html5),而html5經(jīng)過(guò)近幾年的發(fā)展現(xiàn)在已經(jīng)十分火熱了,現(xiàn)在網(wǎng)頁(yè)布局方法基本上都是“div+css”,傳統(tǒng)的table布局現(xiàn)在用的已經(jīng)很少只有一些特殊功能頁(yè)面才會(huì)用到table布局比如說(shuō)調(diào)查問(wèn)卷。下面就為大家介紹“學(xué)好html語(yǔ)言需要掌握什么?前端工程師需要明白什么?”

          打好基礎(chǔ)

          1、打好基礎(chǔ)

          這里說(shuō)的基礎(chǔ)主要是指熟練使用DIV+ CSS的基礎(chǔ)內(nèi)容,熟悉使用常用的標(biāo)簽div、p、span、ul、li等,熟練掌握css常用設(shè)置效果比如說(shuō)圖文混排、圖文環(huán)繞、背景設(shè)置、段落文字等,有興趣可以研究css高級(jí)使用方法(濾鏡、特殊定位等),只有熟練掌握好了這些內(nèi)容對(duì)于以后做工作才能得心應(yīng)手。

          持續(xù)學(xué)習(xí)

          2、持續(xù)學(xué)習(xí)(向大牛學(xué)習(xí))。

          基礎(chǔ)內(nèi)容一般7到10天就可以掌握,一百個(gè)個(gè)人寫(xiě)同一個(gè)頁(yè)面有一百種不同的方法,有的人可以用最少的代碼寫(xiě)出很復(fù)雜的效果而且兼容問(wèn)題很少,這就要求我們自己要不斷去琢磨方法和學(xué)習(xí)提高自己能力,最簡(jiǎn)單的辦法學(xué)習(xí)大牛們的代碼是怎么寫(xiě)的,最簡(jiǎn)單你可以拿四大門戶頁(yè)面代碼好好研究研究,如果能靜下心來(lái)去研究相信你會(huì)收獲不少。

          溝通

          3、與同事(特別是后端)溝通

          web開(kāi)發(fā)想要達(dá)到最終的結(jié)果,前端和后端必須需要相互溝通,打個(gè)比方前端頁(yè)面是貨架,而后端就是放在貨架上的商品,和同事溝通不僅僅可以使工作減少不必要的麻煩(特別是返工)也可以讓在溝通中學(xué)習(xí)更多的東西,所以多跟同事溝通對(duì)我們絕對(duì)是由很多益處的。

          清楚將來(lái)發(fā)展

          4、清楚將來(lái)發(fā)展。

          web前端可以分為初級(jí)、中級(jí)、高級(jí),初級(jí)絕對(duì)要熟練掌握div+css,中級(jí)要熟練掌握js,重點(diǎn)是掌握J(rèn)query、angularJS。高級(jí)要掌握掌握面向?qū)ο缶幊趟枷搿⒊跫?jí)網(wǎng)站安全知識(shí)、配合后臺(tái)開(kāi)發(fā)人員實(shí)現(xiàn)產(chǎn)品界面和功能等,也有一部分轉(zhuǎn)成專職程序員了,所以對(duì)于自己未來(lái)發(fā)展自己要規(guī)劃清楚,杜絕稀里糊涂,要一天比一天好。


          主站蜘蛛池模板: 久久久久99人妻一区二区三区 | 精品国产一区二区三区麻豆| 国产精品视频一区二区三区经| 国产成人精品一区在线 | 午夜无码视频一区二区三区| 亚洲AV无一区二区三区久久 | 国产精品久久久久久麻豆一区| 欧洲精品一区二区三区在线观看 | 无码人妻精品一区二区三区夜夜嗨 | 中文字幕VA一区二区三区 | 无码人妻一区二区三区av| 美女免费视频一区二区| 成人国产精品一区二区网站公司| 亚洲中文字幕无码一区| 狠狠色婷婷久久一区二区三区| 国产一区高清视频| 国产在线观看一区二区三区| 亚洲精品日韩一区二区小说| 精品人无码一区二区三区| 无码国产精品一区二区免费式芒果| 一区二区三区免费看| 国产成人综合精品一区| 成人免费一区二区无码视频| 国产精品视频无圣光一区| 麻豆va一区二区三区久久浪| 国产精品日韩一区二区三区| 国模精品一区二区三区视频| 亚洲av色香蕉一区二区三区蜜桃| 精品国产一区二区三区久久久狼| 97久久精品一区二区三区 | 婷婷国产成人精品一区二| 日韩一区二区精品观看| 亚洲熟女乱综合一区二区| 成人国产精品一区二区网站| 亚洲一区二区三区无码影院| 少妇人妻偷人精品一区二区| 亚洲乱码一区二区三区在线观看| 国产在线精品一区二区中文| 精品乱人伦一区二区三区| 国产肥熟女视频一区二区三区| 波多野结衣的AV一区二区三区 |