函數(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ù)把運(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):
var string = 'functional programming is great';
var result = string
.split(' ')
.map(v => v.slice(0, 1).toUpperCase + v.slice(1))
.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é)果
命令式:我們通過(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é)果值。
//命令式
var CEOs = ;
for(var i = 0; i < companies.length; i++){
CEOs.push(companies[i].CEO)
}
//聲明式
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ù)代碼。
指調(diào)用函數(shù)時(shí)不會(huì)修改外部狀態(tài),即一個(gè)函數(shù)調(diào)用 n 次后依然返回同樣的結(jié)果。
var a = 1;
// 含有副作用,它修改了外部變量 a
// 多次調(diào)用結(jié)果不一樣
function test1 {
a++
return a;
}
// 無(wú)副作用,沒(méi)有修改外部狀態(tài)
// 多次調(diào)用結(jié)果一樣
function test2(a) {
return a + 1;
}
指一個(gè)函數(shù)只會(huì)用到傳遞給它的變量以及自己內(nèi)部創(chuàng)建的變量,不會(huì)使用到其他變量。
var a = 1;
var b = 2;
// 函數(shù)內(nèi)部使用的變量并不屬于它的作用域
function test1 {
return a + b;
}
// 函數(shù)內(nèi)部使用的變量是顯式傳遞進(jìn)去的
function test2(a, b) {
return a + b;
}
指的是一個(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 等等)
var obj = Immutable({ a: 1 });
var obj2 = obj.set('a', 2);
console.log(obj); // Immutable({ a: 1 })
console.log(obj2); // Immutable({ a: 2 })
我們常說(shuō)函數(shù)是JavaScript的"第一等公民",指的是函數(shù)與其他數(shù)據(jù)類型一樣,處于平等地位,可以賦值給其他變量,也可以作為參數(shù),傳入另一個(gè)函數(shù),或者作為別的函數(shù)的返回值。下文將要介紹的閉包、高階函數(shù)、函數(shù)柯里化和函數(shù)組合都是圍繞這一特性的應(yīng)用
如果一個(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ì)算的中間量等。
// 簡(jiǎn)單的緩存工具
// 匿名函數(shù)創(chuàng)造了一個(gè)閉包
const cache = (function {
const store = {};
return {
get(key) {
return store[key];
},
set(key, val) {
store[key] = val;
}
}
});
console.log(cache) //{get: ?, set: ?}
cache.set('a', 1);
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ī)制。
函數(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 方法創(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)呢?
// 不使用高階函數(shù)
var animals = [
{ name: "Fluffykins", species: "rabbit" },
{ name: "Caro", species: "dog" },
{ name: "Hamilton", species: "dog" },
{ name: "Harold", species: "fish" },
{ name: "Ursula", species: "cat" },
{ name: "Jimmy", species: "fish" }
];
var names = ;
for (let i = 0; i < animals.length; i++) {
names.push(animals[i].name);
}
console.log(names); //["Fluffykins", "Caro", "Hamilton", "Harold", "Ursula", "Jimmy"]
// 使用高階函數(shù)
var animals = [
{ name: "Fluffykins", species: "rabbit" },
{ name: "Caro", species: "dog" },
{ name: "Hamilton", species: "dog" },
{ name: "Harold", species: "fish" },
{ name: "Ursula", species: "cat" },
{ name: "Jimmy", species: "fish" }
];
var names = animals.map(x=>x.name);
console.log(names); //["Fluffykins", "Caro", "Hamilton", "Harold", "Ursula", "Jimmy"]
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)呢?
// 不使用高階函數(shù)
var animals = [
{ name: "Fluffykins", species: "rabbit" },
{ name: "Caro", species: "dog" },
{ name: "Hamilton", species: "dog" },
{ name: "Harold", species: "fish" },
{ name: "Ursula", species: "cat" },
{ name: "Jimmy", species: "fish" }
];
var dogs = ;
for (var i = 0; i < animals.length; i++) {
if (animals[i].species === "dog") dogs.push(animals[i]);
}
console.log(dogs);
// 使用高階函數(shù)
var animals = [
{ name: "Fluffykins", species: "rabbit" },
{ name: "Caro", species: "dog" },
{ name: "Hamilton", species: "dog" },
{ name: "Harold", species: "fish" },
{ name: "Ursula", species: "cat" },
{ name: "Jimmy", species: "fish" }
];
var dogs = animals.filter(x => x.species === "dog");
console.log(dogs); // {name: "Caro", species: "dog"}
// { name: "Hamilton", species: "dog" }
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ù)組的求和:
// 不使用高階函數(shù)
const arr = [5, 7, 1, 8, 4];
let sum = 0;
for (let i = 0; i < arr.length; i++) {
sum = sum + arr[i];
}
console.log(sum);//25
// 使用高階函數(shù)
const arr = [5, 7, 1, 8, 4];
const sum = arr.reduce((accumulator, currentValue) => accumulator + currentValue,0);
console.log(sum)//25
我們可以通過(guò)下圖,形象生動(dòng)展示三者的區(qū)別:
柯里化又稱部分求值,柯里化函數(shù)會(huì)接收一些參數(shù),然后不會(huì)立即求值,而是繼續(xù)返回一個(gè)新函數(shù),將傳入的參數(shù)通過(guò)閉包的形式保存,等到被真正求值的時(shí)候,再一次性把所有傳入的參數(shù)進(jìn)行求值。
// 普通函數(shù)
function add(x,y){
return x + y;
}
add(1,2); // 3
// 函數(shù)柯里化
var add = function(x) {
return function(y) {
return x + y;
};
};
var increment = add(1);
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ù)呢?
function curryIt(fn) {
// 參數(shù)fn函數(shù)的參數(shù)個(gè)數(shù)
var n = fn.length;
var args = ;
return function(arg) {
args.push(arg);
if (args.length < n) {
return arguments.callee; // 返回這個(gè)函數(shù)的引用
} else {
return fn.apply(this, args);
}
};
}
function add(a, b, c) {
return [a, b, c];
}
var c = curryIt(add);
var c1 = c(1);
var c2 = c1(2);
var c3 = c2(3);
console.log(c3); //[1, 2, 3]
由此我們可以看出,柯里化是一種“預(yù)加載”函數(shù)的方法,通過(guò)傳遞較少的參數(shù),得到一個(gè)已經(jīng)記住了這些參數(shù)的新函數(shù),某種意義上講,這是一種對(duì)參數(shù)的“緩存”,是一種非常高效的編寫(xiě)函數(shù)的方法!
前面提到過(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é)果。
//兩個(gè)函數(shù)的組合
var compose = function(f, g) {
return function(x) {
return f(g(x));
};
};
//或者
var compose = (f, g) => (x => f(g(x)));
var add1 = x => x + 1;
var mul5 = x => x * 5;
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è)深度的剖析。
什么是渲染過(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 引擎這幾大模塊:
有了對(duì)零部件的了解打底,我們就可以一起來(lái)走一遍瀏覽器的渲染流程了。在瀏覽器里,每一個(gè)頁(yè)面的首次渲染都經(jīng)歷了如下階段(圖中箭頭不代表串行,有一些操作是并行進(jìn)行的,下文會(huì)說(shuō)明):
在這一步瀏覽器執(zhí)行了所有的加載解析邏輯,在解析 HTML 的過(guò)程中發(fā)出了頁(yè)面渲染所需的各種外部資源請(qǐng)求。
瀏覽器將識(shí)別并加載所有的 CSS 樣式信息與 DOM 樹(shù)合并,最終生成頁(yè)面 render 樹(shù)(:after :before 這樣的偽元素會(huì)在這個(gè)環(huán)節(jié)被構(gòu)建到 DOM 樹(shù)中)。
頁(yè)面中所有元素的相對(duì)位置信息,大小等信息均在這一步得到計(jì)算。
在這一步中瀏覽器會(huì)根據(jù)我們的 DOM 代碼結(jié)果,把每一個(gè)頁(yè)面圖層轉(zhuǎn)換為像素,并對(duì)所有的媒體文件進(jìn)行解碼。
最后一步瀏覽器會(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ū)分不同的圖層)。
上面的內(nèi)容沒(méi)有理解透徹?別著急,我們一起來(lái)捋一捋這個(gè)過(guò)程中的重點(diǎn)——樹(shù)!
為了使渲染過(guò)程更明晰一些,我們需要給這些”樹(shù)“們一個(gè)特寫(xiě):
基于這些“樹(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)化!
在給出 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é)出如下性能提升的方案:
錯(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ō)并不再是什么難題~
說(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 的阻塞又是怎么回事呢?
在剛剛的過(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 的特性決定的。
不知道大家注意到?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í)行完畢才能去做其它事情。
<script async src="index.js"></script>
async 模式下,JS 不會(huì)阻塞瀏覽器做任何其它的事情。它的加載是異步的,當(dāng)它加載結(jié)束,JS 腳本會(huì)立即執(zhí)行。
<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ī)劃清楚,杜絕稀里糊涂,要一天比一天好。
*請(qǐng)認(rèn)真填寫(xiě)需求信息,我們會(huì)在24小時(shí)內(nèi)與您取得聯(lián)系。