ฉันอยู่ภายใต้การแสดงผลของ JavaScript นั้นแบบอะซิงโครนัสเสมอ อย่างไรก็ตามฉันได้เรียนรู้ว่ามีสถานการณ์ที่มันไม่ได้ (เช่นการโกง DOM) มีการอ้างอิงที่ดีทุกที่เกี่ยวกับเวลาที่จะซิงโครนัสและเมื่อใดที่มันจะไม่ตรงกัน? jQuery มีผลกับเรื่องนี้หรือไม่?
ฉันอยู่ภายใต้การแสดงผลของ JavaScript นั้นแบบอะซิงโครนัสเสมอ อย่างไรก็ตามฉันได้เรียนรู้ว่ามีสถานการณ์ที่มันไม่ได้ (เช่นการโกง DOM) มีการอ้างอิงที่ดีทุกที่เกี่ยวกับเวลาที่จะซิงโครนัสและเมื่อใดที่มันจะไม่ตรงกัน? jQuery มีผลกับเรื่องนี้หรือไม่?
คำตอบ:
JavaScript มีการซิงโครนัสและเธรดเดียวเสมอ หากคุณกำลังรันบล็อกโค้ด JavaScript บนหน้าเว็บจะไม่มีการเรียกใช้ JavaScript อื่นในหน้านั้นอีก
JavaScript เป็นแบบอะซิงโครนัสเฉพาะในแง่ที่สามารถทำการโทรเช่น Ajax การเรียกใช้ Ajax จะหยุดดำเนินการและรหัสอื่น ๆ จะสามารถดำเนินการจนกว่าการโทรกลับ (สำเร็จหรืออื่น ๆ ) ณ จุดที่โทรกลับจะทำงานพร้อมกัน จะไม่มีรหัสอื่นทำงานในจุดนี้ มันจะไม่ขัดจังหวะรหัสอื่น ๆ ที่กำลังทำงานอยู่
ตัวจับเวลาจาวาสคริปต์ทำงานด้วยการเรียกกลับแบบเดียวกัน
การอธิบาย JavaScript แบบอะซิงโครนัสอาจทำให้เข้าใจผิด มันแม่นยำยิ่งกว่าที่จะบอกว่า JavaScript นั้นมีการซิงโครนัสและเธรดเดี่ยวพร้อมกลไกการโทรกลับที่หลากหลาย
jQuery มีตัวเลือกในการโทร Ajax เพื่อทำให้การซิงโครไนซ์ (พร้อมasync: false
ตัวเลือก) ผู้เริ่มต้นอาจถูกล่อลวงให้ใช้สิ่งนี้อย่างไม่ถูกต้องเพราะจะช่วยให้รูปแบบการเขียนโปรแกรมแบบดั้งเดิมมากขึ้นซึ่งคนอาจคุ้นเคยมากกว่า สาเหตุที่เป็นปัญหาคือตัวเลือกนี้จะบล็อกJavaScript ทั้งหมดในหน้าจนกว่าจะเสร็จสิ้นรวมถึงตัวจัดการเหตุการณ์และตัวจับเวลาทั้งหมด
JavaScript เป็นเธรดเดี่ยวและมีรูปแบบการดำเนินการแบบซิงโครนัส เธรดเดี่ยวหมายความว่าคำสั่งเดียวจะถูกดำเนินการในแต่ละครั้ง ซิงโครนัสหมายถึงทีละครั้งเช่นรหัสหนึ่งบรรทัดจะถูกดำเนินการในเวลาเพื่อให้รหัสปรากฏ ดังนั้นใน JavaScript สิ่งหนึ่งที่เกิดขึ้นในแต่ละครั้ง
บริบทการดำเนินการ
เอ็นจิ้น JavaScript โต้ตอบกับเอนจิ้นอื่น ๆ ในเบราว์เซอร์ ในกองการประมวลผล JavaScript มีบริบทส่วนกลางที่ด้านล่างและเมื่อเราเรียกใช้ฟังก์ชันเครื่องยนต์ JavaScript จะสร้างบริบทการดำเนินการใหม่สำหรับฟังก์ชั่นที่เกี่ยวข้อง เมื่อฟังก์ชั่นที่เรียกใช้ออกจากบริบทการดำเนินการของมันจะโผล่ขึ้นมาจากสแต็คจากนั้นบริบทการดำเนินการต่อไปคือผุดและอื่น ๆ ...
ตัวอย่างเช่น
function abc()
{
console.log('abc');
}
function xyz()
{
abc()
console.log('xyz');
}
var one = 1;
xyz();
ในโค้ดด้านบนบริบทการดำเนินการทั่วโลกจะถูกสร้างขึ้นและในบริบทนี้var one
จะถูกเก็บไว้และค่าของมันจะเป็น 1 ... เมื่อเรียก xyz () ถูกเรียกจากนั้นบริบทการดำเนินการใหม่จะถูกสร้างขึ้นและหากเรากำหนดตัวแปรใด ๆ ในฟังก์ชัน xyz ตัวแปรเหล่านั้นจะถูกเก็บไว้ในบริบทการดำเนินการของ xyz () ในฟังก์ชั่น xyz เราเรียกใช้ abc () จากนั้นบริบทการดำเนินการ abc () จะถูกสร้างและวางในสแต็กการดำเนินการ ... ตอนนี้เมื่อ abc () เสร็จสิ้นบริบทของมันจะถูกผุดจากสแต็กดังนั้นบริบท xyz () สแต็คและบริบททั่วโลกจะถูกตอก ...
ตอนนี้เกี่ยวกับการเรียกกลับไม่ตรงกัน; อะซิงโครนัสหมายถึงมากกว่าหนึ่งครั้ง
เช่นเดียวกับสแต็คการดำเนินการมีคิวงานอีเว้นท์ เมื่อเราต้องการได้รับแจ้งเกี่ยวกับเหตุการณ์บางอย่างในเอ็นจิน JavaScript เราสามารถฟังเหตุการณ์นั้นและเหตุการณ์นั้นถูกวางในคิว ตัวอย่างเช่นเหตุการณ์คำขอ Ajax หรือเหตุการณ์คำขอ HTTP
เมื่อใดก็ตามที่การประมวลผลสแต็กต์ว่างเปล่าดังแสดงในตัวอย่างโค้ดด้านบนเอ็นจิ้น JavaScript จะดูคิวเหตุการณ์เป็นระยะและดูว่ามีเหตุการณ์ใด ๆ ที่ต้องได้รับการแจ้งเตือน ตัวอย่างเช่นในคิวมีสองเหตุการณ์คำขอ ajax และคำขอ HTTP นอกจากนี้ยังดูว่ามีฟังก์ชันที่จำเป็นต้องเรียกใช้ในทริกเกอร์เหตุการณ์นั้นหรือไม่ ... ดังนั้นเครื่องมือ JavaScript จะได้รับแจ้งเกี่ยวกับเหตุการณ์และรู้หน้าที่ที่เกี่ยวข้องเพื่อดำเนินการกับเหตุการณ์นั้น ... ดังนั้นเครื่องมือ JavaScript จึงเรียกใช้ ฟังก์ชั่นการจัดการในกรณีตัวอย่างเช่น AjaxHandler () จะถูกเรียกและชอบเสมอเมื่อฟังก์ชั่นที่ถูกเรียกใช้บริบทการดำเนินการของมันจะถูกวางไว้ในบริบทการดำเนินการและตอนนี้การดำเนินการฟังก์ชั่นเสร็จสิ้นและคำขอ ajax เหตุการณ์ ... เมื่อ AjaxHandler () เสร็จสิ้นการประมวลผลสแต็กจะว่างเปล่าดังนั้นเอ็นจินจะดูคิวเหตุการณ์อีกครั้งและเรียกใช้ฟังก์ชันตัวจัดการเหตุการณ์ของคำขอ HTTP ซึ่งอยู่ในคิวถัดไป เป็นสิ่งสำคัญที่ต้องจำไว้ว่าคิวเหตุการณ์จะถูกประมวลผลเฉพาะเมื่อสแตกการเรียกใช้งานว่าง
ตัวอย่างเช่นดูรหัสด้านล่างอธิบายการประมวลผลสแต็กและการจัดการคิวเหตุการณ์โดยเครื่องมือ Javascript
function waitfunction() {
var a = 5000 + new Date().getTime();
while (new Date() < a){}
console.log('waitfunction() context will be popped after this line');
}
function clickHandler() {
console.log('click event handler...');
}
document.addEventListener('click', clickHandler);
waitfunction(); //a new context for this function is created and placed on the execution stack
console.log('global context will be popped after this line');
และ
<html>
<head>
</head>
<body>
<script src="program.js"></script>
</body>
</html>
ตอนนี้เรียกใช้เว็บเพจและคลิกที่หน้าและดูผลลัพธ์บนคอนโซล ผลลัพธ์จะเป็น
waitfunction() context will be popped after this line
global context will be emptied after this line
click event handler...
เอ็นจิ้น JavaScript กำลังเรียกใช้โค้ดแบบซิงโครนัสตามที่อธิบายไว้ในส่วนบริบทการดำเนินการเบราว์เซอร์จะวางสิ่งต่าง ๆ ในคิวเหตุการณ์แบบอะซิงโครนัส ดังนั้นฟังก์ชั่นที่ใช้เวลานานในการดำเนินการจึงสามารถขัดขวางการจัดการเหตุการณ์ได้ สิ่งที่เกิดขึ้นในเบราว์เซอร์เช่นเหตุการณ์ได้รับการจัดการด้วยวิธีนี้โดย JavaScript หากมีผู้ฟังที่ควรจะรันเอ็นจินจะรันมันเมื่อสแตกการเรียกใช้งานว่างเปล่า และเหตุการณ์จะถูกดำเนินการตามลำดับที่เกิดขึ้นดังนั้นส่วนอะซิงโครนัสจึงเกี่ยวกับสิ่งที่เกิดขึ้นนอกเครื่องยนต์นั่นคือสิ่งที่เครื่องยนต์ควรทำเมื่อเหตุการณ์ภายนอกเกิดขึ้น
ดังนั้น JavaScript จึงซิงโครไนซ์เสมอ
JavaScript เป็นแบบเธรดเดียวและตลอดเวลาที่คุณทำงานกับการเรียกใช้โค้ดไหลแบบซิงโครนัสปกติ
ตัวอย่างที่ดีของพฤติกรรมแบบอะซิงโครนัสที่ JavaScript สามารถมีได้คือเหตุการณ์ (การโต้ตอบผู้ใช้ผลลัพธ์คำขอ Ajax ฯลฯ ) และตัวจับเวลาโดยทั่วไปการกระทำที่อาจเกิดขึ้นได้ตลอดเวลา
ฉันขอแนะนำให้คุณดูบทความต่อไปนี้:
บทความนั้นจะช่วยให้คุณเข้าใจธรรมชาติของเธรดเดียวของ JavaScript และการทำงานของตัวจับเวลาภายในและการทำงานของ JavaScript แบบอะซิงโครนัส
สำหรับคนที่เข้าใจจริงๆว่า JS ทำงานอย่างไรคำถามนี้อาจดูไม่ดีนักอย่างไรก็ตามคนส่วนใหญ่ที่ใช้ JS ไม่มีความเข้าใจในระดับลึก (และไม่จำเป็นต้องใช้มัน) และสำหรับพวกเขานี่เป็นจุดที่ค่อนข้างสับสนฉันจะ พยายามตอบจากมุมมองนั้น
JS ซิงโครนัสในวิธีการใช้รหัส แต่ละบรรทัดจะทำงานหลังจากบรรทัดก่อนที่จะเสร็จสมบูรณ์และถ้าบรรทัดนั้นเรียกใช้ฟังก์ชันหลังจากนั้นจะเสร็จสมบูรณ์ ect ...
จุดหลักของความสับสนเกิดขึ้นจากข้อเท็จจริงที่ว่าเบราว์เซอร์ของคุณสามารถบอก JS ให้เพิ่มรหัสได้ตลอดเวลา (simmlar ถึงวิธีที่คุณสามารถเพิ่มรหัส JS เพิ่มเติมในหน้าจากคอนโซล) ตัวอย่างเช่น JS มีฟังก์ชันการโทรกลับที่มีวัตถุประสงค์เพื่ออนุญาตให้ JS BEHAVE แบบอะซิงโครนัสเพื่อให้ส่วนอื่น ๆ ของ JS สามารถทำงานได้ในขณะที่รอฟังก์ชั่น JS ที่ถูกดำเนินการ (IE GET
โทร) เพื่อกลับคำตอบ เบราว์เซอร์มีคำตอบ ณ จุดนั้นลูปเหตุการณ์ (เบราว์เซอร์) จะดำเนินการโค้ด JS ที่เรียกใช้ฟังก์ชันการโทรกลับ
เนื่องจากอีเวนต์วนรอบ (เบราว์เซอร์) สามารถป้อน JS เพิ่มเติมที่จะดำเนินการ ณ จุดใด ๆ ในแง่นั้น JS นั้นแบบอะซิงโครนัส (สิ่งหลักที่จะทำให้เบราว์เซอร์ใส่รหัส JS คือหมดเวลาโทรกลับและเหตุการณ์)
ฉันหวังว่านี่จะชัดเจนพอที่จะเป็นประโยชน์กับใครบางคน
คำว่า "อะซิงโครนัส" สามารถใช้ในความหมายที่แตกต่างกันเล็กน้อยทำให้เกิดคำตอบที่ขัดแย้งกันดูเหมือนที่นี่ในขณะที่พวกเขาไม่ได้จริง Wikipedia บน Asynchronyมีคำจำกัดความนี้:
อะซิงโครนัสในการเขียนโปรแกรมคอมพิวเตอร์หมายถึงเหตุการณ์ที่เกิดขึ้นโดยไม่ขึ้นกับโฟลว์ของโปรแกรมหลักและวิธีจัดการกับเหตุการณ์ดังกล่าว สิ่งเหล่านี้อาจเป็นเหตุการณ์ "ภายนอก" เช่นการมาถึงของสัญญาณหรือการกระทำที่เกี่ยวข้องกับโปรแกรมที่เกิดขึ้นพร้อมกันกับการทำงานของโปรแกรมโดยไม่ต้องมีการบล็อกโปรแกรมเพื่อรอผลลัพธ์
รหัสที่ไม่ใช่ JavaScript สามารถจัดคิวกิจกรรม "นอก" ดังกล่าวต่อคิวเหตุการณ์ของ JavaScript บางส่วน แต่นั่นก็เท่าที่มันจะไป
ไม่มีการขัดจังหวะการเรียกใช้โค้ด JavaScript ภายนอกเพื่อรันโค้ด JavaScript อื่น ๆ ในสคริปต์ของคุณ ชิ้นส่วนของ JavaScript จะถูกดำเนินการอย่างใดอย่างหนึ่งหลังจากที่อื่น ๆ และลำดับจะถูกกำหนดโดยลำดับของเหตุการณ์ในแต่ละคิวเหตุการณ์และลำดับความสำคัญของคิวเหล่านั้น
ตัวอย่างเช่นคุณสามารถมั่นใจได้อย่างแน่นอนว่าจะไม่มีการเรียกใช้ JavaScript (ในสคริปต์เดียวกัน) ในขณะที่โค้ดต่อไปนี้กำลังทำงาน:
let a = [1, 4, 15, 7, 2];
let sum = 0;
for (let i = 0; i < a.length; i++) {
sum += a[i];
}
กล่าวอีกนัยหนึ่งไม่มีการจองล่วงหน้าใน JavaScript สิ่งที่อาจจะอยู่ในคิวเหตุการณ์การประมวลผลเหตุการณ์เหล่านั้นจะต้องรอจนกว่าโค้ดดังกล่าวจะทำงานจนเสร็จ ข้อกำหนด EcmaScript ระบุไว้ในหัวข้อ 8.4 งานและคิวงาน :
การเรียกใช้งานสามารถเริ่มต้นได้เฉพาะเมื่อไม่มีบริบทการดำเนินการที่กำลังดำเนินอยู่และสแต็กบริบทการดำเนินการว่างเปล่า
ตามที่คนอื่นเขียนไปแล้วมีหลายสถานการณ์ที่อะซิงโครนัสเข้ามาเล่นใน JavaScript และมักเกี่ยวข้องกับคิวเหตุการณ์ซึ่งจะส่งผลให้มีการเรียกใช้ JavaScript เมื่อไม่มีการเรียกใช้โค้ด JavaScript อื่นเท่านั้น:
setTimeout()
: ตัวแทน (เช่นเบราว์เซอร์) จะวางเหตุการณ์ในคิวเหตุการณ์เมื่อหมดเวลาแล้ว การตรวจสอบเวลาและการวางเหตุการณ์ในคิวเกิดขึ้นโดยรหัสที่ไม่ใช่ JavaScript ดังนั้นคุณสามารถจินตนาการได้ว่าสิ่งนี้เกิดขึ้นควบคู่ไปกับการใช้รหัส JavaScript ที่มีศักยภาพ แต่การโทรกลับที่จัดไว้ให้setTimeout
สามารถดำเนินการได้เมื่อรหัส JavaScript ที่กำลังดำเนินการอยู่ในปัจจุบันทำงานจนเสร็จสิ้นและกำลังอ่านคิวเหตุการณ์ที่เหมาะสม
fetch()
: เอเจนต์จะใช้ฟังก์ชัน OS เพื่อดำเนินการตามคำขอ HTTP และตรวจสอบการตอบสนองขาเข้าใด ๆ อีกครั้งงานที่ไม่ใช่ JavaScript นี้อาจทำงานคู่ขนานกับรหัส JavaScript บางอย่างที่ยังคงทำงานอยู่ แต่ขั้นตอนการแก้ปัญหาสัญญาซึ่งจะแก้ไขสัญญาที่ส่งคืนโดยfetch()
สามารถดำเนินการได้เมื่อ JavaScript ที่ดำเนินการอยู่ในปัจจุบันเสร็จสิ้นลงเท่านั้น
requestAnimationFrame()
: เอ็นจิ้นการแสดงผลของเบราว์เซอร์ (ที่ไม่ใช่ JavaScript) จะวางเหตุการณ์ในคิว JavaScript เมื่อพร้อมที่จะทำการทาสี เมื่อประมวลผลเหตุการณ์ JavaScript แล้วฟังก์ชันการเรียกกลับจะดำเนินการ
queueMicrotask()
: วางเหตุการณ์ในคิว microtask ทันที การเรียกกลับจะถูกดำเนินการเมื่อสแตกการโทรว่างเปล่าและเหตุการณ์นั้นถูกใช้ไป
มีตัวอย่างอีกมากมาย แต่ฟังก์ชั่นเหล่านี้มีให้โดยสภาพแวดล้อมของโฮสต์ไม่ใช่โดย Core EcmaScript มีแกน ECMAScript Promise.resolve()
คุณพร้อมสามารถวางเหตุการณ์ในคิวสัญญาอาชีพกับ
ECMAScript ให้โครงสร้างหลายภาษาให้การสนับสนุนรูปแบบ asynchrony เช่นyield
, ,async
await
แต่ปล่อยให้ไม่มีข้อผิดพลาด: ไม่มีรหัส JavaScript จะถูกขัดจังหวะโดยเหตุการณ์ภายนอก "การหยุดชะงัก" ว่าyield
และawait
ดูเหมือนจะให้เป็นเพียงการควบคุมวิธีการที่กำหนดไว้ล่วงหน้าของการกลับมาจากการเรียกฟังก์ชั่นและฟื้นฟูบริบทการปฏิบัติของตนในภายหลังอย่างใดอย่างหนึ่งตามรหัส JS (ในกรณีของyield
) หรือคิวเหตุการณ์ (ในกรณีของawait
)
เมื่อรหัส JavaScript เข้าถึง DOM API สิ่งนี้อาจทำให้ DOM API ทริกเกอร์การแจ้งเตือนแบบซิงโครนัสหนึ่งรายการขึ้นไป และหากรหัสของคุณมีตัวจัดการเหตุการณ์ที่ฟังอยู่มันจะถูกเรียก
สิ่งนี้อาจเกิดขึ้นพร้อมกันเป็น pre-emptive concurrency แต่ไม่ใช่: เมื่อตัวจัดการเหตุการณ์ของคุณส่งคืน DOM API จะกลับมาในที่สุดก็จะกลับมาและรหัส JavaScript เดิมจะดำเนินการต่อ
ในกรณีอื่น ๆ DOM API จะส่งเหตุการณ์ในคิวเหตุการณ์ที่เหมาะสมและ JavaScript จะรับมันเมื่อสแต็คการโทรว่างเปล่า
เป็นแบบซิงโครนัสในทุกกรณี
ตัวอย่างการบล็อกเธรดด้วยPromises
:
const test = () => new Promise((result, reject) => {
const time = new Date().getTime() + (3 * 1000);
console.info('Test start...');
while (new Date().getTime() < time) {
// Waiting...
}
console.info('Test finish...');
});
test()
.then(() => console.info('Then'))
.finally(() => console.info('Finally'));
console.info('Finish!');
ผลลัพธ์จะเป็น:
Test start...
Test finish...
Finish!