主頁 > 前端設計 > [書籍翻譯] 《JavaScript并發編程》第三章 使用Promises實作同步

[書籍翻譯] 《JavaScript并發編程》第三章 使用Promises實作同步

2020-09-14 10:46:17 前端設計

本文是我翻譯《JavaScript Concurrency》書籍的第三章 使用Promises實作同步,該書主要以Promises、Generator、Web workers等技術來講解JavaScript并發編程方面的實踐,

完整書籍翻譯地址:https://github.com/yzsunlei/javascript_concurrency_translation ,由于能力有限,肯定存在翻譯不清楚甚至翻譯錯誤的地方,歡迎朋友們提issue指出,感謝,

Promises幾年前就在JavaScript類別庫中實作了,這一切都始于Promises/A+規范,這些類別庫的實作都有它們自己的形式,直到最近(確切地說是ES6),Promises規范才被JavaScript語言納入,如標題那樣 - 它幫助我們實作同步原則,

在本章中,我們將首先簡單介紹Promises中各種術語,以便更容易理解本章的后面部分內容,然后,通過各種方式,我們將使用Promises來解決目前的一些問題,并讓并發處理更容易,準備好了嗎?

Promise相關術語

在我們深入研究代碼之前,讓我們花一點時間確保我們牢牢掌握Promises有關的術語,有Promise實體,但是還有各種狀態和方法,如果我們能夠弄清楚Promise這些術語,那么后面的章節會更易理解,這些解釋簡短易懂,所以如果您已經使用過Promises,您可以快速看下這些術語,就當復習下,

Promise

顧名思義,Promise是一種承諾,將Promise視為尚不存在的值的代理,Promise讓我們更好的撰寫并發代碼,因為我們知道值會在將來某個時刻存在,并且我們不必撰寫大量的狀態檢查樣板代碼,

狀態(State)

Promises總是處于以下三種狀態之一:

? 等待:這是Promise創建后的第一個狀態,它一直處于等待狀態,直到它完成或被拒絕,

? 完成:該Promise值已經處理完成,并能為它提供then()回呼函式,

? 拒絕:處理Promise的值出了問題,現在沒有資料,

Promise狀態的一個有趣特性是它們只轉換一次,它們要么從等待狀態到完成,要么從等待狀態到被拒絕,一旦它們進行了這種狀態轉換,后面就會鎖定在這種狀態,

執行器(Executor)

執行器函式負責以某種方式決議值并將處于等待狀態,創建Promise后立即呼叫此函式,它需要兩個引數:resolver函式和rejector函式,

決議器(Resolver)

決議器是一個作為引數傳遞給執行器函式的函式,實際上,這非常方便,因為我們可以將決議器函式傳遞給另一個函式,依此類推,呼叫決議器函式的位置并不重要,但是當它被呼叫時,Promise會進入一個完成狀態,狀態的這種改變將觸發then()回呼 - 這些我們將在后面看到,

拒絕器(Rejector)

拒絕器與決議器相似,它是傳遞給執行器函式的第二個引數,可以從任何地方呼叫,當它被呼叫時,Promise從等待狀態改變到拒絕狀態,這種狀態的改變將呼叫錯誤回呼函式,如果有的話,會傳遞給then()或catch(),

Thenable

如果物件具有接受完成回呼和拒絕回呼作為引數的then()方法,則該物件就是Thenable,換句話說,Promise是Thenable,但是在某些情況下,我們可能希望實作特定的決議語意,

完成和拒絕Promises

如果上一節剛剛介紹的幾個術語聽起來讓你困惑,那別擔心,從本節開始,我們將看到所有這些Promises術語的應用實踐,在這里,我們將展示一些簡單的Promise解決和拒絕的示例,

完成Promises

決議器是一個函式,顧名思義,它完成了我們的Promise,這不是完成Promise的唯一方法 - 我們將在后面探索更高級的方式,但到目前為止,這種方法是最常見的,它作為第一個引數傳遞給執行器函式,這意味著執行器可以通過簡單地呼叫決議器直接完成Promise,但這并不怎么實用,不是嗎?

更常見的情況是Promise執行器函式設定即將發生的異步操作 - 例如撥打網路電話,然后,在這些異步操作的回呼函式中,我們可以完成這個Promise,在我們的代碼中傳遞一個決議函式,剛開始可能感覺有點違反直覺,但是一旦我們開始使用它們就會發現很有意義,

決議器函式是一個相對Promise來說比較難懂的函式,它只能完成一次Promise,我們可以呼叫決議器很多次,但只在第一次呼叫會改變Promise的狀態,下面是一個圖描述了Promise的可能狀態;它還顯示了狀態之間是如何變化的:

image060.gif

現在,我們來看一些Promise代碼,在這里,我們將完成一個promise,它會呼叫then()完成回呼函式:

//我們的Promise使用的執行器函式,
//第一個引數是決議器函式,在1秒后呼叫完成Promise,
function executor(resolve) {
	setTimeout(resolve, 1000);
}

//我們Promise的完成回呼函式,
//這個簡單地在我們的執行程式函式運行后,停止那個定時器,
function fulfilled() {
	console.timeEnd('fulfillment');
}

//創建promise,并立即運行,
//然后啟動一個定時器來查看呼叫完成函式需要多長時間,
var promise = new Promise(executor);
promise.then(fulfilled);
console.time('fulfillment');

我們可以看到,決議器函式被呼叫時fulfilled()函式會被呼叫,執行器實際上并不呼叫決議器,相反,它將決議器函式傳遞給另一個異步函式 - setTimeout(),執行器并不是我們試圖去弄清楚的異步代碼,可以將執行器視為一種協調程式,它編排異步操作并確定何時執行Promise,

前面的示例未決議任何值,當某個操作的呼叫者需要確認它成功或失敗時,這是一個有效的用例,相反,讓我們這次嘗試決議一個值,如下所示:

//我們的Promise使用的執行函式,
//創建Promise后,設定延時一秒鐘呼叫"resolve()",
//并決議回傳一個字串值 - "done!",
function executor(resolve) {
	setTimeout(() => {
		resolve('done!');
	}, 1000);
}

//我們Promise的完成回呼接受一個值引數,
//這個值將傳遞到決議器,
function fulfilled(value) {
	console.log('resolved', value);
}

//創建我們的Promise,提供執行程式和完成回呼函式,
var promise = new Promise(executor);
promise.then(fulfilled);

我們可以看到這段代碼與前面的例子非常相似,區別在于我們的決議器函式實際上是在傳遞給setTimeout()的回呼函式的閉包內呼叫的,這是因為我們正在決議一個字串值,還有一個將被決議的引數值傳遞給我們的fulfilled()函式,

拒絕promises

Promise執行器函式并不總是按期望進行,當出現問題時,我們需要拒絕promise,這是從等待狀態轉換到另一個可能的狀態,這不是進入一個完成狀態而是進入一個被拒絕的狀態,這會導致執行不同的回呼,與完成回呼函式是分開的,值得慶幸的是,拒絕Promise的機制與完成Promise非常相似,我們來看看這是如何實作的:

//此執行器在延時一秒后拒絕Promise,
//它使用拒絕回呼函式來改變狀態,
//并傳遞拒絕的引數值到回呼函式,
function executor(resolve, reject) {
	setTimeout(() => {
		reject('Failed');
	}, 1000);
}

//用作拒絕回呼的函式,
//它接收提供拒絕的引數值,
function rejected(reason) {
	console.error(reason);
}

//創建promise,并運行執行器,
//使用“catch()”方法來接收拒絕回呼函式,
var promise = new Promise(executor);
promise.catch(rejected);

這段代碼看起來和在上一節中看到的代碼非常相似,我們設定了超時,并且我們拒絕了它而不是完成它,這是使用rejector函式完成的,并作為第二個引數傳遞給執行器,

我們使用catch()方法而不是then()方法來設定拒絕回呼函式,我們將在本章后面看到then()方法如何用于同時處理完成和拒絕回呼函式,此示例中的拒絕回呼函式僅將失敗原因列印出來,通常情況下提供此回傳值很重要,當我們完成promise時,回傳值也是常見的,盡管不是必需的,另一方面,對于拒絕函式,一般也很少有情況僅僅通過回呼函式輸出拒絕原因,

讓我們看下另一個例子,它捕獲執行器中拋出的例外,并為拒絕回呼函式提供更有意義的報錯原因:

//此promise執行程式拋出錯誤,
//并呼叫拒絕回呼函式輸出錯誤資訊,
new Promise(() => {
	throw new Error('Problem executing promise');
}).catch((reason) => {
	console.error(reason);
});

//此promise執行程式捕獲錯誤,
//并呼叫拒絕回呼函式輸出更有意義的錯誤資訊,
new Promise((resolve, reject) => {
	try {
		var size = this.name.length;
	} catch (error) {
		reject(error instanceof TypeError ? 'Missing "name" property' : error);
	}
}).catch((reason) => {
	console.error(reason);
});

前一個例子中第一個Promise的有趣之處在于它確實改變了狀態,即使我們沒有使用resolve()或reject()明確地改變promise的狀態,然而,最終改變promise的狀態是很重要的; 我們將在下一節中探討這個話題,

空Promises

盡管事實上執行器函式傳遞了一個完成回呼函式和拒絕回呼函式,但并不保證promise將改變狀態,有些情況下,promise只是掛起,并沒有觸發完成回呼也沒有觸發拒絕回呼,這可能并沒有什么問題,事實上,簡單的promises,就很容易發現和修復沒有回應的promises,然而,隨著我們進入更復雜的場景后,一個promise的完成回呼可以作為其他幾個promise的回呼結果,如果一個promises不能完成或拒絕,然后整個流程將崩潰,這種情況除錯起來是非常麻煩的;下面的圖可以很清楚的看到這個情況:

image061.gif

在圖中,我們可以看到哪個promise導致依賴的promise掛起,但通過除錯代碼來解決這個問題并不容易,現在讓我們看看導致promise掛起的執行函式:

//這個promise能夠正常運行執行器函式,
//但“then()”回呼函式永遠不會被執行,
new Promise(() => {
	console.log('executing promise');
}).then(() => {
	console.log('never called');
});

//此時,我們并不知道promise出了什么問題
console.log('finished executing, promise hangs');

但是,是否有一種更安全的方式來處理這種不確定性呢?在我們的代碼中,我們不需要掛起無需完成或拒絕的執行函式,讓我們來實作一個執行器包裝函式,像一個安全網那樣讓過長時間還沒完成的promises執行拒絕回呼函式,這將揭開解決不好處理的promise場景的神秘面紗:

//promise執行器函式的包裝器,
//在給定的超時時間后拋出錯誤,
function executorWrapper(func, timeout) {
	//這是實際呼叫的函式,
	//它需要決議器函式和拒絕器函式作為引數,
	return function executor(resolve, reject) {
		//設定我們的計時器,
		//當時間到達時,我們可以使用超時訊息拒絕promise,
		var timer = setTimeout(() => {
			reject('Promise timed out after $?? {timeout} MS');
		}, timeout);
		
		//呼叫我們原來的執行器包裝函式,
		//我們實際上也包裝了完成回呼函式
		//和拒絕回呼函式,所以當
		//執行者呼叫它們時,會清除定時器,
		func((value) => {
            clearTimeout(timer);
            resolve(value);
        }, (value) => {
            clearTimeout(timer);
            reject(value);
        });
    };
}

//這個promise執行后超時,
//超時錯誤訊息傳遞給拒絕回呼,
new Promise(executorWrapper((resolve, reject) => {
	setTimeout(() => {
		resolve('done');
	}, 2000);
}, 1000)).catch((reason) => {
	console.error(reason);
});

//這個promise執行后按預期運行,
//在定時結束之前呼叫“resolve()”,
new Promise(executorWrapper((resolve, reject) => {
	setTimeout(() => {
		resolve(true);
	}, 500);
}, 1000)).then((value) => {
	console.log('resolved', value);
});

對promises作出改進

既然我們已經很好地理解了promises的執行機制,本節將詳細介紹如何使用promises來解決特定問題,通常,這意味著當promises完成或被拒絕時,我們會達到我們某些目的,

我們將首先查看JavaScript解釋器中的任務佇列,以及這些對我們的決議回呼函式的意義,然后,我們將考慮使用promise的結果資料,處理錯誤,創建更好的抽象來回應promises,以及thenables,讓我們開始吧,

處理任務佇列

JavaScript任務佇列的概念在“第2章,JavaScript運行模型”中提到過,它的主要職責是初始化新的執行背景關系堆疊,這是常見的任務佇列,然而,還有另一種佇列,這是專用于執行promises回呼的,這意味著,如果他們都存在時,演算法會從這些佇列中選擇一個任務執行,

Promises具有內置的并發語意,而且有充分的理由,如果一個promise被用來確保某個值最終被決議,那么為對其作出回應的代碼賦予高優先級是有意義的,否則,當值到達時,處理它的代碼可能還要在其他任務后面等待很長的時間才能執行,讓我們撰寫一些代碼來演示下這些并發語意:

//創建5個promise,記錄它們的執行時間,
//以及當他們對回傳值做出回應的時間,
for (let i = 0; i < 5; i++) {
	new Promise((resolve) => {
		console.log('execting promise');
		resolve(i);
	}).then((value) => {
		console.log('resolved', i);
	});
}

//在任何promise完成回呼之前,這里會先被呼叫,
//因為堆疊任務需要在解釋器進入promise決議回呼佇列之前完成,
//當前5個“then()”回呼將被置后,
console.log('done executing');

//→
//execting promise
//execting promise
// ...
//done executing
//resolved 1
//resolved 2
// ...

拒絕回呼也遵循同樣的語意,

使用promise的回傳資料

到目前為止,我們已經在本章中看到了一些示例,其中決議器函式完成promise后并回傳值,傳遞給此函式的值是最終傳遞給完成回呼函式的值,通過讓執行程式設定任何異步操作的方法,例如setTimeout(),延時傳遞該值呼叫決議程式,但在這些例子中,呼叫者實際上并沒有等待任何值;我們只使用setTimeout()作為示例異步操作,讓我們看一下我們實際上沒有值的情況,異步網路請求需要獲取到它:

//用于從服務器獲取資源的通用函式,
//回傳一個promise,
function get(path) {
	return new Promise((resolve, reject) => {
		var request = new XMLHttpRequest();
		
		//promise決議資料加載后的JSON資料,
		request.addEventListener('load', (e) => {
			resolve(JSON.parse(e.target.responseText));
		});

		//當請求出錯時,promise執行拒絕回呼函式,
		request.addEventListener('error', (e) => {
			reject(e.target.statusText || '未知錯誤');
		});


		//如果請求被中止時,我們呼叫完成回呼函式
		request.addEventListener('abort', resolve);
		
		request.open('get', path);
		request.send();
	});
}

//我們可以直接附加我們的“then()”處理程式
//到“get()”,因為它回傳一個promise,
//在決議之前,這里使用的值是一個真正的異步操作,
//因為必須發請求遠程獲取值,
get('api.json').then((value) => {
	console.log('hello', value.hello);
});

使用像get()這樣的函式,它們不僅始侄訓傳像promise一樣的原生型別,而且還封裝了一些讓人討厭的異步細節,在我們的代碼中處理XMLHttpRequest物件并不令人愉快,我們已經簡化了可以回傳的各種情況,而不是總是必須為load,error和abort事件創建處理程式,我們只需要關心一個介面 - promise,這就是同步并發原則的全部內容,

錯誤回呼

有兩種方法可以對被拒絕的promise做出處理,換句話說,提供錯誤回呼,第一種方法是使用catch()方法,該方法使用單一回呼函式,另一種方法是將被拒絕的回呼函式作為then()的第二個引數傳遞,

將then()方法用來處理拒絕回呼函式在某些情況下表現的更好,它應該被用來替代catch()函式,第一個場景是撰寫promises和thenable物件可以互換的代碼,catch()方法不是thenable必要的一部分,第二個場景是當我們建立回呼鏈時,我們將在本章后面探討,

讓我們看一些代碼,它們比較了兩種為promises提供拒絕回呼函式的方法:

//這個promise執行器將隨機執行完成回呼或拒絕回呼
function executor(resolve, reject) {
	cnt++;
	Math.round(Math.random()) ? 
		resolve(`fulfilled promise ${cnt}`) :
		reject(`rejected promise ${cnt}`);
}

//讓“log()”和“error()”函式作為簡單回呼函式
var log = console.log.bind(console),
	error = console.error.bind(console),
	cnt = 0;

//創建一個promise,然后通過“catch()”方法傳入拒絕回呼,
new Promise(executor).then(log).catch(error);

//創建一個promise,然后通過“then()”方法傳入拒絕回呼,
new Promise(executor).then(log, error);

我們可以看到這兩種方法實際上非常相似,在代碼美觀上,也沒有哪個有真正的優勢,然而,當涉及到使用thenables時,then()方法有一個優勢,我們后面會看到,但是,由于我們實際上并沒有以任何方式使用promise實體,除了添加回呼之外,實際上沒有必要擔心catch()和then()用于注冊拒絕回呼,

始終回應

Promises最終總是結束于完成狀態或拒絕狀態,我們通常為每個狀態傳入不同的回呼函式,但是,我們很可能希望為這兩個狀態執行一些相同的操作,例如,如果使用promise的組件在promise等待時更改狀態,我們要確保在完成或拒絕promise后清除狀態,

我們可以用這樣的方式撰寫代碼:完成和拒絕狀態的每個回呼都去執行這些操作,或者他們每個都可以呼叫執行一些公用的清理函式,下面這種方式的示圖:

image065.gif

將清理任務分配給promise是否有意義,而不是將其分配給其它個別結果?這樣,在決議promise時運行的回呼函式專注于它需要對值執行的操作,而拒絕回呼則專注于處理錯誤,讓我們看看是否可以使用always()方法撰寫一些擴展promises的代碼:

//在promise原型上擴展使用“always()”方法,
//不管promise是完成還是拒絕,始侄訓呼叫給定的函式,
Promise.prototype.always = function(func) {
	return this.then(func, func);
};

//創建promise隨機完成或被拒絕,
var promise = new Promise((resolve, reject) => {
	Math.round(Math.random()) ? 
	resolve('fullfilled') : reject('rejected');
});

//傳遞promise完成和拒絕回呼,
promise.then((value) => {
	console.log(value);
}, (reason) => {
	console.error(reason);
});

//這個回呼函式總是會在上面的回呼執行之后呼叫,
promise.always((value) => {
	console.log('cleaning up...');
});

請注意,在這里順序很重要,如果我們在then()之前呼叫always(),那么函式仍然會運行,但它會在
回呼提供給then()之前運行,我們實際上可以在then()之前和之后都呼叫always(),以便在完成或拒絕回呼
之前以及之后運行代碼,

處理其他promises

到目前為止,我們在本章中看到的大多數promise都是由執行程式函式直接完成的,或者是當值準備完成時從異步操作中呼叫決議器的結果,像這樣傳遞回呼函式實際上非常靈活,例如,執行程式甚至不必執行任何任務,除了將決議器函式存盤在某處以便稍后呼叫它來決議promise,

當我們發現自己處于需要多個值的更復雜的同步場景時,這可能特別有用,這些值已經被傳遞給呼叫者,如果我們有處理回呼函式,我們就可以處理promise,讓我們看看,在存盤代碼的決議函式的多個promises,使每一個promise都可以在后面處理:

//存盤一系列決議器函式的串列,
var resolvers = [];

//在執行器中創建5個新的promise,
//決議器被推到了“resolvers”陣列,
//我們可以給每一個promise執行回呼,
for(let i = 0; i < 5; i++) {
	new Promise(() => {
		resolvers.push(resolve);
	}).then((value) => {
		console.log(`resolved ${i + 1}`, value);
	});
}

//設定一個2s之后延時運行函式,
//當它運行時,我們遍歷“決議器”陣列中的每一個決議器函式,
//并且傳入一個回傳值來呼叫它,
setTimeout(() => {
	for(resolver of resolvers) {
		resolver(true);
	}
}, 2000);

正如這個例子所表明的那樣,我們不必在executor函式內處理它們,事實上,我們甚至不需要在創建和設定執行程式和完成函式之后顯式參考promise實體,決議器函式已存盤在某處,它包含對promise的參考,

類Promise物件

Promise類是一種原生的JavaScript型別,但是,我們并不總是需要創建新的promise實體來實作相同的同步操作,我們可以使用靜態Promise.resolve()方法來決議這些物件,讓我們看看如何使用此方法:

//“Promise.resolve()”方法可以處理thenable物件,
//這是一個帶有“then()”方法的類似于執行器的物件,
//這個執行器將隨機完成或拒絕promise,
Promise.resolve({then: (resolve, reject) => {
	Math.round(Math.random()) ? resolve('fulfilled') : reject('rejected');

	//這個方法回傳一個promise,所以我們能夠
	//設定已完成和被拒絕的回呼函式,
}}).then((value) => {
	console.log('resolved', value);
}, (reason) => {
	console.error('reason', reason);
});

我們將在本章的最后一節中再次討論Promise.resolve()方法,以了解更多用例,

建立回呼鏈

我們在本章前面介紹的每種promise方法都會回傳promise,這允許我們在回傳值上再次呼叫這些方法,從而產生then().then()呼叫的鏈,依此類推,鏈式promise具有挑戰性的一個方面是promise方法回傳的是新實體,也就是說,我們將在本節中探討promise在一定程度上的不變性,

隨著我們的應用程式變得越來越大,并發性挑戰隨之增加,這意味著我們需要考慮更好的方法來利用原生同步語意,例如promises,正如JavaScript中的任何其他原始值一樣,我們可以將它們從函式傳遞給函式,我們必須以同樣的方式處理promises - 傳遞它們,并建立在回呼函式鏈上,

Promises只改變狀態一次

Promise初始時是等待狀態,并且它們結束于已完成或被拒絕的狀態,一旦promise轉變為其中一種狀態,它們就會鎖定在這種狀態,這有兩個有趣的副作用,

首先,多次嘗試完成或拒絕promise將被忽略,換句話說,決議器和拒絕器是冪等的 - 只有第一次呼叫對promise有影響,讓我們看看這代碼如何執行:

//此執行器函式嘗試決議promise兩次,
//但完成的回呼只呼叫一次,
new Promise((resolve, reject) => {
	resolve('fulfilled');
	resolve('fulfilled');
}).then((value) => {
	console.log('then', value);
});

//這個執行器函式嘗試拒絕promise兩次,
//但拒絕的回呼只呼叫一次,
new Promise((resolve, reject) => {
	reject('rejected');
	reject('rejected');
}).catch((reason) => {
	console.error('reason');
});

promises僅改變狀態一次的另一個含義是promise可以在添加完成或拒絕回呼之前處理,競爭條件,例如這個,是并發編程的殘酷現實,通常,回呼函式會在創建時添加到promise中,由于JavaScript是運行到完成的,因此在添加回呼之前,不會處理promise決議回呼的任務佇列,但是,如果promise立即在執行中決議怎么辦?如果將回呼添加到另一個JavaScript執行背景關系的promise中會怎樣?讓我們看看是否可以用一些代碼來更好地說明這些情況:

//此執行器函式立即決議promise,添加“then()”回呼時,
//promise已經決議了,但回呼函式仍然會使用已決議的值進行呼叫,
new Promise((resolve, reject) => {
	resolve('done');
	console.log('executor', 'resolved');
}).then((value) => {
	console.log('then', value);
});

//創建一個立即決議的新promise執行器函式,
var promise = new Promise((resolve, reject) => {
	resolve('done');
	console.log('executor', 'resolved');
});

//這個回呼是promise決議后就立即執行了,
promise.then((value) => {
	console.log('then 1', value);
});

//此回呼在promise決議后未添加到另一個的promise中,
//它仍然被立即呼叫并獲得已決議的值,
setTimeout(() => {
	promise.then((value) => {
		console.log('then 2', value);
	});
}, 1000);

此代碼說明了promises的一個非常重要的特性,無論何時將執行回呼添加到promise中,無論是處于暫時掛起狀態還是決議狀態,使用promise的代碼都不會更改,從表面上看,這似乎不是什么大不了的事,但是這種競爭條件檢查的型別需要更多的并發代碼來保護自己,相反,Promise原生語法為我們處理這個問題,我們可以開始將異步值視為原始型別,

不可改變的promises

promises并非真正不可改變,它們改變狀態,then()方法將回呼函式添加到promise,但是,有一些不可改變的promises特征值得在這里討論,因為它們會在某些情況下影響我們的promise代碼,

從技術上講,then()方法實際上并沒有改變promise物件,它創建了所謂的promise能力,它是一個參考promise的內部JavaScript記錄,以及我們添加的函式,因此,它不是JavaScript語言中的真正語法,

這是一張圖,說明當我們鏈接兩個或更多then()一起呼叫時會發生什么:

image069.gif

我們可以看到,then()方法不會回傳與背景關系一起呼叫的相同實體,相反,then()創建一個新的promise實體并回傳它,讓我們看一些代碼,來進一步的說明當我們使用then()將promises鏈接在一起時會發生的事情:

//創建一個立即決議的promise,
//并且存盤在“promise1”中,
var promise1 = new Promise((resolve, reject) => {
	resolve('fulfilled');
});

//使用“promise1”的“then()”方法創建一個
//新的promise實體,存盤在“promise2”中,
var promise2 = promise1.then((value) => {
	console.log('then 1', value);
	//→then 1 fulfilled
});

//為“promise2”創建一個“then()”回呼,這實際上
//創建第三個promise實體,但我們不用它做任何事情,
promise2.then((value) => {
	console.log('then 2', value);
	//→then 2 undefined
});

//確信“promise1”和“promise2”實際上是不同的物件
console.log('equal', promise1 === promise2);
//→equal false

我們可以清楚地看到這兩個創建promise的實體在這個例子中是獨立的promise物件,值得指出的是第二個promise執行前時,一定是它執行了第一個promise,但是,我們可以看到的是該值不會傳遞到第二個promise,我們將在下一節中解決此問題,

有多少個then()回呼,就有多少個promise物件

正如我們在上一節中看到的那樣,使用then()創建的promise將系結到它們的創建者,也就是說,當第一個promise完成時,系結它的promise也會完成,依此類推,但是,我們也發現了一個小問題,已決議的值不會使其傳遞到第一個回呼函式,這樣做的原因是為回應promise決議而運行的每個回呼都是第一個回呼的回傳值被送入第二個回呼,依此類推,我們的第一個回呼將值作為引數的原因是因為這在promise機制中顯然會發生的,

我們來看看另一個promise鏈示例,這一次,我們將顯式回傳回呼函式中的值:

//創建一個新promise隨機呼叫決議回呼或拒絕回呼,
new Promise((resolve, reject) => {
	Math.round(Math.random()) ?
	resolve('fulfilled') : reject('rejected');
}).then((value) => {
	//在完成原始promise時呼叫回傳值,
	//以防另一個promise鏈接到這一個,
	console.log('then 1', value); 
	return value;
}).catch((reason) => {
	//鏈接到第二個promise,
	//當拒絕回呼時執行,
	console.error('catch 1', reason);
}).then((value) => {
	//鏈接到第三個promise,
	//按預期得到值,并回傳值給任何下個promise回呼使用,
	console.log('then 2', value);
	return value;
}).catch((reason) => {
	//這里永不會被呼叫,
	//拒絕回呼不會通過promise鏈傳遞,
	console.error('catch 2', reason);
});

這看起來不錯,我們可以看到已決議的值通過promise鏈傳遞,有一個例外 - 拒絕回呼不會向后傳遞,相反,只有鏈中的第一個promise拒絕回呼會執行,其余的promise回呼只是完成,而不是拒絕,這意味著最后一個catch()回呼永遠不會運行,

當我們以這種方式將promise鏈接在一起時,我們的執行回呼函式需要能夠處理錯誤條件,例如,已決議的值可能具有error屬性,可以檢查其具體問題,

promises傳遞

在本節中,我們講講promise作為原始值的用法,我們經常用原始值做的事情是將它們作為引數傳遞給函式,并從函式中回傳它們,promise和其他原生語法之間的關鍵區別在于我們如何使用它們,其他值是始終都存在,而promise的值到未來某個時間點才存在,因此,我們需要通過回呼函式定義一些操作程序,當值獲得時去執行,

promises的好處是用于提供這些回呼函式的介面小巧且一致,當我們將值與將作用于它的代碼耦合時,我們不需要再去自主創造同步機制,這些單元可以像任何其他值一樣在我們的應用程式中運用,并且并發語意是常見的,這是幾個promise函式相互傳遞的示圖:

image071.gif

在這個函式堆疊呼叫結束時,我們得到一個完成幾個promise的決議的promise物件,整個promise鏈是從第一個promise完成而開始的,比如何遍歷promise鏈的機制??更重要的是所有這些函式都可以自由使用這個promise傳遞的值而不影響其他函式,

在這里有兩個并發原則,首先,我們通過執行異步操作僅只能處理該值一次; 每個回呼函式都可以自由使用此決議值,其次,我們在抽象同步機制方面做得很好,換句話說,代碼并沒有帶有很多重復代碼,讓我們看看傳遞promise的代碼實際的樣子:

//簡單實用的工具函式,
//將多個較小的函陣列合成一個函式,
function compose(...funcs) {
	return function(value) {
		var result = value;
		
		for(let func of funcs) {
			result = func(value);
		}
		return result;
	};
}

//接受一個promise或一個完成值,
//如果這是一個promise,它添加了一個“then()”回呼并回傳一個新的promise,
//否則,它會執行“update”并回傳值,
function updateFirstName(value) {
	if (value instanceof Promise) {
		return value.then(updateFirstName);
	}

	console.log('first name', value.first); 
	return value;
}

//與上面的函式類似,
//只是它執行不同的UI“update”,
function updateLastName(value) {
	if (value instanceof Promise) {
		return value.then(updateLastName);
	} 

	console.log('last name', value.last); 
	return value;
}

//與上面的函式類似,除了它
//只是它執行不同的UI“update”,
function updateAge(value) {
	if (value instanceof Promise) {
		return value.then(updateAge);
	}

	console.log('age', value.age);
	return value;
}

//一個promise物件,
//它在延時一秒鐘之后,
//攜帶一個資料物件完成promise,
var promise = new Promise((resolve, reject) => {
	setTimeout(() => {
		resolve({
			first: 'John',
			last: 'Smith',
			age: 37
		});
	});
}, 1000);

//我們組裝一個“update()”函式來更新各種UI組件,
var update = compose(
	updateFirstName,
	updateLastName,
	updateAge
);

//使用promise呼叫我們的更新函式,
update(promise);

這里的關鍵函式是我們的更新函式 - updateFirstName(),updateLastName()和updateAge(),他們非常靈活,接受一個promise或promise回傳值,如果這些函式中的任何一個將promise作為引數,它們會通過添加then()回呼函式來回傳新的promise,請注意,它添加了相同的函式,updateFirstName()將添加updateFirstName()作為回呼,當回呼觸發時,它將與此次用于更新UI的普通物件一起使用,因此,promise如果失敗,我們可以繼續更新UI,

promise檢查每個函式都需要三行,這并不是非常突兀的,最終結果是易讀且靈活的代碼,順序無關緊要; 我們可以用不同的順序包裝我們的update()函式,并且UI組件都將以相同的方式更新,我們可以將普通物件直接傳遞給update(),一切都會同樣執行,看起來不像并發代碼的并發代碼是我們在這里取得的重大成功,

同步多個promises

在本章前面,我們已經探究了單個promise實體,它決議一個值,觸發回呼,并可能傳遞給其他promises處理,在本節中,我們將介紹幾種靜態Promise方法,它們可以幫助我們處理需要同步多個promise值的情況,

首先,我們將處理我們開發的組件需要同步訪問多個異步資源的情況,然后,我們將看一下不常見的情況,如異步操作在處理之前由于UI中發生的事件而變得沒有意義,

等待promises

在我們等待處理多個promise的情況下,也許是將多個資料源轉換后提供給一個UI組件使用,我們可以使用Promise.all()方法,它將promise實體的集合作為輸入,并回傳一個新的promise實體,僅當完成了所有輸入的promise時,才會回傳一個新實體,

then()函式是我們為Promise提供的創建新promise的回呼,給出一組決議值作為輸入,這些值對應于索引輸入promise的位置,這是一個非常強大的同步機制,它可以幫助我們實作同步并發原則,因為它隱藏了所有的處理記錄,

我們不需要幾個回呼,讓每個回呼都協調它們所系結的promise狀態,我們只需一個回呼,它具有我們需要的所有決議資料,這個示例展示如何同步多個promise:

//用于發送“GET”HTTP請求的工具函式,
//并回傳帶有已決議的資料的promise,
function get(path) {
	return new Promise((resolve, reject) => {
		var request = new XMLHttpRequest();
		
		//當資料加載時,完成決議了JSON資料的promise
		request.addEventListener('load', (e) => {
			resolve(JSON.parse(e.target.responseText));
		});

		//當請求出錯時,
		//promise被適當的原因拒絕,
		request.addEventListener('error', (e) => {
			reject(e.target.statusText || 'unknown error');
		});

		//如果請求被中止,我們繼續完成處理請求 
		request.addEventListener('abort', resolve);
		
		request.open('get', path);
		request.send();
	});
}


//保存我們的請求promises,
var requests = [];

//發出5個API請求,并將相應的5個
//promise放在“requests”陣列中,
for (let i = 0; i < 5; i++) {
	requests.push(get('api.json'));
}

//使用“Promise.all()”讓我們傳入一個陣列promises,
//當所有promise完成時,回傳一個已經完成的新promise,
//我們的回呼得到一個陣列對應于promises的已決議值,
Promise.all(requests).then((values) => {
	console.log('first', values.map(x => x[0])); 
	console.log('second', values.map(x => x[1]));
});

取消promises

到目前為止,我們在本書中已看到的XHR請求具有中止請求的處理程式,這是因為我們可以手動中止請求并阻止任何load回呼函式運行,需要此功能的典型場景是用戶單擊取消按鈕,或導航到應用程式的其他部分,從而使請求變得毫無意義,

如果我們是要在抽象promise上更上一層樓,在同樣的原則也適用,而一些可能發生的并發操作的執行讓promise變得毫無意義,promises和XHR請求的程序中之間的區別,是前者沒有abort()方法,最后我們要做的一件事是在我們的promise回呼中開始引入可能并不必要的取消邏輯,

Promise.race()方法在這里可以幫助我們,顧名思義,該方法回傳一個新的promise,它由第一個要決議的輸入promise決定,這可能你聽的不多,但實作Promise.race()的邏輯并不容易,它實際上是同步原則,隱藏了應用程式代碼中的并發復雜性,我們來看看這個方法是怎么可以幫助我們處理因用戶互動而取消的promise:

//用于取消資料請求的決議器??函式,
var cancelResolver;

//一個簡單的“常量”值,用于處理取消promise
var CANCELED = {};

//我們的UI組件
var buttonLoad = document.querySelector('button.load'),
	buttonCancel = document.querySelector('button.cancel');

//請求資料,回傳一個promise,
function getDataPromise() {
	//創建取消promise,
	//執行器傳入“resolve”函式為“cancelResolver”,
	//所以它稍后可以被呼叫,
	var cancelPromise = new Promise((resolve) => {
		cancelResolver = resolve;
	});

	//我們實際想要的資料
	//這通常是一個HTTP請求,
	//但我們在這里使用setTimeout()簡單模擬一下,
	var dataPromise = new Promise((resolve) => {
		setTimeout(() => {
			resolve({hello: 'world'});
		}, 3000);
	});

	//“Promise.race()”方法回傳一個新的promise,
	//并且無論輸入promise是什么,它都可以完成處理
	return Promise.race([cancelPromise, dataPromise]);
}

//單擊取消按鈕時,我們使用
//“cancelResolver()”函式來處理取消promise
buttonCancel.addEventListener('click', () => {
	cancelResolver(CANCELLED);
});

//單擊加載按鈕時,我們使用
//“getDataPromise()”發出請求獲取資料,
buttonLoad.addEventListener('click', () => {
	buttonLoad.disabled = true;
	getDataPromise().then((value) => {
		buttonLoad.disabled = false;
		//promise得到了執行,但那是因為
		//用戶取消了請求,所以我們這里
		//通過回傳CANCELED “constant”退出,
		//否則,我們有資料可以使用,
		if (Object.is(value, CANCELED)) {
			return value;
		}
		
		console.log('loaded data', value);
	});
});

作為練習,嘗試想象一個更復雜的場景,其中dataPromise是由Promise.all()創建的promise,我們的
cancelResolver()函式可以一次取消許多復雜的異步操作,

沒有執行器的promises

在最后一節中,我們將介紹Promise.resolve()和Promise.reject()方法,我們已經在本章前面看到Promise.resolve()如何處理thenable物件,它還可以直接處理值或其他promises,當我們實作一個可能同步也可能異步的函式時,這些方法會派上用場,這不是我們想要使用具有模糊并發語意函式的情況,

例如,這是一個可能同步也可能異步的函式,讓人感到迷惑,幾乎肯定會在以后出現錯誤:

//一個示例函式,它可能從快取中回傳“value”,
//也可能通過“fetchs”異步獲取值,
function getData(value) {
	//如果它存在于快取中,我們直接回傳這個值
	var index = getData.cache.indexOf(value);
	if(index > -1) {
		return getData.cache[index];
	}

	//否則,我們必須通過“fetch”異步獲取它,
	//這個“resolve()”呼叫通常是會在網路發起請求的回呼函式
	return new Promise((resolve) => {
		getData.cache.push(value);
		resolve(value);
	});
}

//創建快取,
getData.cache = [];

console.log('getting foo', getData('foo'));
//→getting foo Promise
console.log('getting bar', getData('bar'));
//→getting bar Promise
console.log('getting foo', getData('foo'));
//→getting foo foo

我們可以看到最后一次呼叫回傳的是快取值,而不是一個promise,這很直觀,因為我們不需要通過promise獲取最終的值,我們已經擁有這個值!問題是我們讓使用getData()函式的任何代碼表現出不一致性,也就是說,呼叫getData()的代碼需要處理并發語意,此代碼不是并發的,讓我們通過引入Promise.resolve()來改變它:

//一個示例函式,它可能從快取中回傳“value”,
//也可能通過“fetchs”異步獲取值,
function getData(value) {
	var cache = getData.cache;
	//如果這個函式沒有快取,
	//那就拒絕promise,
	if(!Array.isArray(cache)) {
		return Promise.reject('missing cache');
	}

	//如果它存在于快取中,
	//我們直接使用快取的值回傳完成的promise
	var index = getData.cache.indexOf(value);
	
	if (index > -1) {
		return Promise.resolve(getData.cache[index]);
	}

	//否則,我們必須通過“fetch”異步獲取它,
	//這個“resolve()”呼叫通常是會在網路發起請求的回呼函式
	return new Promise((resolve) => {
		getData.cache.push(value);
		resolve(value);
	});
}

//創建快取,
getData.cache = [];

//每次呼叫“getData()”回傳都是一致的,
//甚至當使用同步值時,
//它們仍然回傳得到決議完成的promise,
getData('foo').then((value) => {
	console.log('getting foo', `“${value}”`);
}, (reason) => {
	console.error(reason);
});

getData('bar').then((value) => {
	console.log('getting bar', `“${value}”`);
}, (reason) => {
	console.error(reason);
});

getData('foo').then((value) => {
	console.log('getting foo', `“${value}”`);
}, (reason) => {
	console.error(reason);
});

這樣更好,使用Promise.resolve()和Promise.reject(),任何使用getData()的代碼默認都是并發的,即使資料獲取操作是同步的,

小結

本章介紹了ES6中引入的Promise物件的大量細節內容,以幫助JavaScript程式員處理困擾該語言多年的同步問題,大量的使用異步回呼,這會產生回呼地獄,因而我們要盡量避免它,

Promise通過實作一個足以解決任何值的通用介面來幫助我們處理同步問題,promise總是處于三種狀態之一 - 等待,完成或拒絕,并且它們只會改變一次狀態,當這些狀態發生改變時,將觸發回呼,promise有一個執行器函式,其作用是設定使用promise的異步操作resolver函式或rejector函式來改變promise的狀態,

promise帶來的大部分價值在于它們如何幫助我們簡化復雜的場景,因為,如果我們只需處理一個運行帶有決議值回呼的異步操作,那么使用promises就不值得,這是不常見的情況,常見的情況是幾個異步操作,每個操作都需要決議回傳值;并且這些值需要同步處理和轉換,Promises有方法幫助我們這樣做,因此,我們能夠更好地將同步并發原則應用于我們的代碼,

在下一章中,我們將介紹另一個新引入的語法 - Generator,與promises類似,生成器是幫助我們應用另一個并發原則的機制 - 保護,

最后補充下書籍章節目錄

  • 《JavaScript并發編程》第一章 JavaScript并發簡介
  • 《JavaScript并發編程》第二章 JavaScript運行模型
  • 《JavaScript并發編程》第三章 使用Promises實作同步
  • 《JavaScript并發編程》第四章 使用Generators實作惰性計算
  • 《JavaScript并發編程》第五章 使用Web Workers
  • 《JavaScript并發編程》第六章 實用的并發
  • 《JavaScript并發編程》第七章 抽取并發邏輯

另外還有講解兩章nodeJs后端并發方面的,和一章專案實戰方面的,這里就不再貼了,有興趣可轉向https://github.com/yzsunlei/javascript_concurrency_translation查看,

轉載請註明出處,本文鏈接:https://www.uj5u.com/qianduan/34042.html

標籤:HTML5

上一篇:git使用

下一篇:基于MUI框架+HTML5PLUS 開發 iOS和Android 應用程式(APP)

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • vue移動端上拉加載

    可能做得過于簡單或者比較low,請各位大佬留情,一起探討技術 ......

    uj5u.com 2020-09-10 04:38:07 more
  • 優美網站首頁,頂部多層導航

    一個個人用的瀏覽器首頁,可以把一下常用的網站放在這里,平常打開會比較方便。 第一步,HTML代碼 <script src=https://www.cnblogs.com/szharf/p/"js/jquery-3.4.1.min.js"></script> <div id="navigate"> <ul> <li class="labels labels_1"> ......

    uj5u.com 2020-09-10 04:38:47 more
  • 頁面為要加<!DOCTYPE html>

    最近因為寫一個js函式,需要用到$(window).height(); 由于手寫demo的時候,過于自信,其實對前端方面的認識也不夠體系,用文本檔案直接敲出來的html代碼,第一行沒有加上<!DOCTYPE html> 導致了$(window).height();的結果直接是整個document的高 ......

    uj5u.com 2020-09-10 04:38:52 more
  • WordPress網站程式手動升級要做好資料備份

    WordPress博客網站程式在進行升級前,必須要做好網站資料的備份,這個問題良家佐言是遇見過的;在剛開始接觸WordPress博客程式的時候,因為升級問題和博客網站的修改的一些嘗試,良家佐言是吃盡了苦頭。因為購買的是西部數碼的空間和域名,每當佐言把自己的WordPress博客網站搞到一塌糊涂的時候 ......

    uj5u.com 2020-09-10 04:39:30 more
  • WordPress程式不能升級為5.4.2版本的原因

    WordPress是一款個人博客系統,受到英文博客愛好者和中文博客愛好者的追捧,并逐步演化成一款內容管理系統軟體;它是使用PHP語言和MySQL資料庫開發的,用戶可以在支持PHP和MySQL資料庫的服務器上使用自己的博客。每一次WordPress程式的更新,就會牽動無數WordPress愛好者的心, ......

    uj5u.com 2020-09-10 04:39:49 more
  • 使用CSS3的偽元素進行首字母下沉和首行改變樣式

    網頁中常見的一種效果,首字改變樣式或者首行改變樣式,效果如下圖。 代碼: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, ......

    uj5u.com 2020-09-10 04:40:09 more
  • 關于a標簽的講解

    什么是a標簽? <a> 標簽定義超鏈接,用于從一個頁面鏈接到另一個頁面。 <a> 元素最重要的屬性是 href 屬性,它指定鏈接的目標。 a標簽的語法格式:<a href=https://www.cnblogs.com/summerxbc/p/"指定要跳轉的目標界面的鏈接">需要展示給用戶看見的內容</a> a標簽 在所有瀏覽器中,鏈接的默認外觀如下: 未被訪問的鏈接帶 ......

    uj5u.com 2020-09-10 04:40:11 more
  • 前端輪播圖

    在需要輪播的頁面是引入swiper.min.js和swiper.min.css swiper.min.js地址: 鏈接:https://pan.baidu.com/s/15Uh516YHa4CV3X-RyjEIWw 提取碼:4aks swiper.min.css地址 鏈接:https://pan.b ......

    uj5u.com 2020-09-10 04:40:13 more
  • 如何設定html中的背景圖片(全屏顯示,且不拉伸)

    1 <style>2 body{background-image:url(https://uploadbeta.com/api/pictures/random/?key=BingEverydayWallpaperPicture); 3 background-size:cover;background ......

    uj5u.com 2020-09-10 04:40:16 more
  • Java學習——HTML詳解(上)

    HTML詳解 初識HTML Hyper Text Markup Language(超文本標記語言) 1 <!--DOCTYPE:告訴瀏覽器我們要使用什么規范--> 2 <!DOCTYPE html> 3 <html lang="en"> 4 <head> 5 <!--meta 描述性的標簽,描述一些 ......

    uj5u.com 2020-09-10 04:40:33 more
最新发布
  • 我的第一個NPM包:panghu-planebattle-esm(胖虎飛機大戰)使用說明

    好家伙,我的包終于開發完啦 歡迎使用胖虎的飛機大戰包!! 為你的主頁添加色彩 這是一個有趣的網頁小游戲包,使用canvas和js開發 使用ES6模塊化開發 效果圖如下: (覺得圖片太sb的可以自己改) 代碼已開源!! Git: https://gitee.com/tang-and-han-dynas ......

    uj5u.com 2023-04-20 07:59:23 more
  • 生產事故-走近科學之消失的JWT

    入職多年,面對生產環境,盡管都是小心翼翼,慎之又慎,還是難免捅出簍子。輕則滿頭大汗,面紅耳赤。重則系統停擺,損失資金。每一個生產事故的背后,都是寶貴的經驗和教訓,都是專案成員的血淚史。為了更好地防范和遏制今后的各類事故,特開此專題,長期更新和記錄大大小小的各類事故。有些是親身經歷,有些是經人耳傳口授 ......

    uj5u.com 2023-04-18 07:55:04 more
  • 記錄--Canvas實作打飛字游戲

    這里給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 打開游戲界面,看到一個畫面簡潔、卻又富有挑戰性的游戲。螢屏上,有一個白色的矩形框,里面不斷下落著各種單詞,而我需要迅速地輸入這些單詞。如果我輸入的單詞與螢屏上的單詞匹配,那么我就可以獲得得分;如果我輸入的單詞錯誤或者時間過長,那么我就會輸 ......

    uj5u.com 2023-04-04 08:35:30 more
  • 了解 HTTP 看這一篇就夠

    在學習網路之前,了解它的歷史能夠幫助我們明白為何它會發展為如今這個樣子,引發探究網路的興趣。下面的這張圖片就展示了“互聯網”誕生至今的發展歷程。 ......

    uj5u.com 2023-03-16 11:00:15 more
  • 藍牙-低功耗中心設備

    //11.開啟藍牙配接器 openBluetoothAdapter //21.開始搜索藍牙設備 startBluetoothDevicesDiscovery //31.開啟監聽搜索藍牙設備 onBluetoothDeviceFound //30.停止監聽搜索藍牙設備 offBluetoothDevi ......

    uj5u.com 2023-03-15 09:06:45 more
  • canvas畫板(滑鼠和觸摸)

    <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>canves</title> <style> #canvas { cursor:url(../images/pen.png),crosshair; } #canvasdiv{ bo ......

    uj5u.com 2023-02-15 08:56:31 more
  • 手機端H5 實作自定義拍照界面

    手機端 H5 實作自定義拍照界面也可以使用 MediaDevices API 和 <video> 標簽來實作,和在桌面端做法基本一致。 首先,使用 MediaDevices.getUserMedia() 方法獲取攝像頭媒體流,并將其傳遞給 <video> 標簽進行渲染。 接著,使用 HTML 的 < ......

    uj5u.com 2023-01-12 07:58:22 more
  • 記錄--短視頻滑動播放在 H5 下的實作

    這里給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 短視頻已經無數不在了,但是主體還是使用 app 來承載的。本文講述 H5 如何實作 app 的視頻滑動體驗。 無聲勝有聲,一圖頂百辯,且看下圖: 網址鏈接(需在微信或者手Q中瀏覽) 從上圖可以看到,我們主要實作的功能也是本文要講解的有: ......

    uj5u.com 2023-01-04 07:29:05 more
  • 一文讀懂 HTTP/1 HTTP/2 HTTP/3

    從 1989 年萬維網(www)誕生,HTTP(HyperText Transfer Protocol)經歷了眾多版本迭代,WebSocket 也在期間萌芽。1991 年 HTTP0.9 被發明。1996 年出現了 HTTP1.0。2015 年 HTTP2 正式發布。2020 年 HTTP3 或能正... ......

    uj5u.com 2022-12-24 06:56:02 more
  • 【HTML基礎篇002】HTML之form表單超詳解

    ??一、form表單是什么

    ??二、form表單的屬性

    ??三、input中的各種Type屬性值

    ??四、標簽 ......

    uj5u.com 2022-12-18 07:17:06 more