JavaScript ให้ความยืดหยุ่นเป็นพิเศษเมื่อต้องจัดการกับฟังก์ชันต่างๆ พวกเขาสามารถส่งผ่านไปใช้เป็นวัตถุได้และตอนนี้เราจะดูวิธี โอน สายระหว่างพวกเขาและ ตกแต่ง พวกเขา
สมมติว่าเรามีฟังก์ชัน slow(x)
ซึ่งใช้ CPU มาก แต่ผลลัพธ์กลับมีเสถียรภาพ กล่าวอีกนัยหนึ่ง สำหรับ x
เดียวกัน จะส่งกลับผลลัพธ์เดียวกันเสมอ
หากมีการเรียกใช้ฟังก์ชันบ่อยครั้ง เราอาจต้องการแคช (จำ) ผลลัพธ์เพื่อหลีกเลี่ยงการใช้เวลาเพิ่มเติมในการคำนวณใหม่
แต่แทนที่จะเพิ่มฟังก์ชันนั้นลงใน slow()
เราจะสร้างฟังก์ชัน wrapper ที่เพิ่มแคช ดังที่เราจะได้เห็นว่าการทำเช่นนี้มีประโยชน์มากมาย
นี่คือรหัสและคำอธิบายดังต่อไปนี้:
ฟังก์ชั่นช้า (x) { // ที่นี่อาจมีงานที่ต้องใช้ CPU มาก alert(`เรียกด้วย ${x}`); กลับ x; - ฟังก์ชั่น cachingDecorator (func) { ให้แคช = แผนที่ใหม่ (); ฟังก์ชันส่งคืน (x) { if (cache.has(x)) { // หากมีคีย์ดังกล่าวอยู่ในแคช กลับ cache.get(x); //อ่านผลลัพธ์จากมัน - ให้ผลลัพธ์ = func(x); // หรือเรียก func cache.set(x ผลลัพธ์); // และแคช (จำ) ผลลัพธ์ ส่งคืนผลลัพธ์; - - ช้า = แคชมัณฑนากร (ช้า); แจ้งเตือน( ช้า(1) ); // ช้า (1) ถูกแคชและส่งคืนผลลัพธ์ alert( "อีกครั้ง:" + ช้า(1) ); // ผลลัพธ์ช้า (1) ส่งคืนจากแคช แจ้งเตือน( ช้า(2) ); // ช้า (2) ถูกแคชและส่งคืนผลลัพธ์ alert( "อีกครั้ง:" + ช้า(2) ); // ผลลัพธ์ช้า (2) ส่งคืนจากแคช
ในโค้ดด้านบน cachingDecorator
คือ มัณฑนากร : ฟังก์ชันพิเศษที่รับฟังก์ชันอื่นและปรับเปลี่ยนพฤติกรรมของมัน
แนวคิดก็คือเราสามารถเรียก cachingDecorator
สำหรับฟังก์ชันใดก็ได้ และมันจะส่งคืน wrapper แคช เยี่ยมมาก เพราะเรามีฟังก์ชันมากมายที่สามารถใช้ฟีเจอร์ดังกล่าวได้ และสิ่งที่เราต้องทำคือใช้ cachingDecorator
กับฟังก์ชันเหล่านั้น
โดยการแยกแคชออกจากโค้ดฟังก์ชันหลัก เรายังทำให้โค้ดหลักง่ายขึ้นอีกด้วย
ผลลัพธ์ของ cachingDecorator(func)
คือ “wrapper”: function(x)
ที่ “ตัด” การเรียกของ func(x)
เข้าสู่ลอจิกแคช:
จากโค้ดภายนอก ฟังก์ชัน slow
ที่ห่อไว้ยังคงทำเหมือนเดิม มันเพิ่งได้รับการเพิ่มลักษณะแคชให้กับพฤติกรรมของมัน
โดยสรุป มีข้อดีหลายประการของการใช้ cachingDecorator
แยกต่างหาก แทนที่จะเปลี่ยนโค้ดของ slow
เอง:
cachingDecorator
สามารถนำมาใช้ซ้ำได้ เราสามารถนำไปใช้กับฟังก์ชันอื่นได้
ตรรกะการแคชแยกจากกัน มันไม่ได้เพิ่มความซับซ้อนของ slow
เอง (ถ้ามี)
เราสามารถรวมผู้ตกแต่งหลายรายเข้าด้วยกันได้หากจำเป็น (ผู้ตกแต่งรายอื่นจะตามมา)
มัณฑนากรแคชที่กล่าวถึงข้างต้นไม่เหมาะกับการทำงานกับวิธีอ็อบเจ็กต์
ตัวอย่างเช่น ในโค้ดด้านล่าง worker.slow()
หยุดทำงานหลังการตกแต่ง:
// เราจะทำการแคช worker.slow ให้คนงาน = { วิธีการบางอย่าง() { กลับ 1; - ช้า(x) { // งาน CPU หนักน่ากลัวที่นี่ alert("ถูกเรียกด้วย " + x); กลับ x * this.someMethod(); - - - // รหัสเดียวกับเมื่อก่อน ฟังก์ชั่น cachingDecorator (func) { ให้แคช = แผนที่ใหม่ (); ฟังก์ชันส่งคืน (x) { ถ้า (cache.has(x)) { กลับ cache.get(x); - ให้ผลลัพธ์ = func(x); - cache.set(x ผลลัพธ์); ส่งคืนผลลัพธ์; - - การแจ้งเตือน( worker.slow(1) ); // วิธีดั้งเดิมใช้งานได้ worker.slow = cachingDecorator (คนงานช้า); // ตอนนี้ทำให้มันเป็นแคช การแจ้งเตือน( worker.slow(2) ); //อุ๊ย! ข้อผิดพลาด: ไม่สามารถอ่านคุณสมบัติ 'someMethod' ของไม่ได้กำหนด
เกิดข้อผิดพลาดในบรรทัด (*)
ที่พยายามเข้าถึง this.someMethod
และล้มเหลว คุณเห็นไหมว่าทำไม?
เหตุผลก็คือ wrapper เรียกฟังก์ชันดั้งเดิมเป็น func(x)
ในบรรทัด (**)
และเมื่อถูกเรียกเช่นนั้น ฟังก์ชันจะได้รับ this = undefined
เราจะสังเกตเห็นอาการที่คล้ายกันหากเราพยายามเรียกใช้:
ให้ func = worker.slow; ฟังก์ชั่น(2);
ดังนั้น wrapper ผ่านการเรียกไปยังวิธีการดั้งเดิม แต่ไม่มีบริบท this
จึงมีข้อผิดพลาด
มาแก้ไขกัน
มีเมธอดฟังก์ชันพิเศษในตัว func.call(context, …args) ที่อนุญาตให้เรียกใช้ฟังก์ชันโดยตั้งค่า this
อย่างชัดเจน
ไวยากรณ์คือ:
func.call (บริบท, arg1, arg2, ... )
มันรัน func
โดยระบุอาร์กิวเมนต์แรกเป็น this
และอาร์กิวเมนต์ถัดไปเป็นอาร์กิวเมนต์
พูดง่ายๆ ก็คือ การโทรทั้งสองนี้ทำเกือบจะเหมือนกัน:
ฟังก์ชั่น(1, 2, 3); func.call (obj, 1, 2, 3)
พวกเขาทั้งสองเรียก func
ด้วยข้อโต้แย้ง 1
, 2
และ 3
ข้อแตกต่างเพียงอย่างเดียวคือ func.call
ยังตั้งค่า this
เป็น obj
ตามตัวอย่าง ในโค้ดด้านล่างเราเรียกว่า sayHi
ในบริบทของอ็อบเจ็กต์ที่แตกต่างกัน: sayHi.call(user)
รัน sayHi
โดยระบุ this=user
และบรรทัดถัดไปจะตั้งค่า this=admin
:
ฟังก์ชั่น sayHi() { การแจ้งเตือน (this.name); - ให้ผู้ใช้ = { ชื่อ: "จอห์น" }; ให้ผู้ดูแลระบบ = { ชื่อ: "ผู้ดูแลระบบ" }; // ใช้การเรียกเพื่อส่งผ่านวัตถุต่าง ๆ เป็น "นี่" sayHi.call( ผู้ใช้ ); // จอห์น sayHi.call( ผู้ดูแลระบบ ); //แอดมิน
และในที่นี้เราใช้ call
to call say
กับบริบทและวลีที่กำหนด:
ฟังก์ชั่นพูด (วลี) { การแจ้งเตือน (this.name + ': ' + วลี); - ให้ผู้ใช้ = { ชื่อ: "จอห์น" }; // ผู้ใช้กลายเป็นสิ่งนี้ และ "Hello" กลายเป็นอาร์กิวเมนต์แรก say.call( ผู้ใช้ "สวัสดี" ); // จอห์น: สวัสดี
ในกรณีของเรา เราสามารถใช้ call
ใน wrapper เพื่อส่งบริบทไปยังฟังก์ชันดั้งเดิม:
ให้คนงาน = { วิธีการบางอย่าง() { กลับ 1; - ช้า(x) { alert("ถูกเรียกด้วย " + x); กลับ x * this.someMethod(); - - - ฟังก์ชั่น cachingDecorator (func) { ให้แคช = แผนที่ใหม่ (); ฟังก์ชันส่งคืน (x) { ถ้า (cache.has(x)) { กลับ cache.get(x); - ให้ผลลัพธ์ = func.call (นี่, x); // "นี่" ผ่านอย่างถูกต้องแล้ว cache.set(x ผลลัพธ์); ส่งคืนผลลัพธ์; - - worker.slow = cachingDecorator (คนงานช้า); // ตอนนี้ทำให้มันเป็นแคช การแจ้งเตือน( worker.slow(2) ); //ทำงาน การแจ้งเตือน( worker.slow(2) ); // ใช้งานได้ไม่เรียกต้นฉบับ (แคช)
ตอนนี้ทุกอย่างเรียบร้อยดี
เพื่อให้ทุกอย่างชัดเจน เรามาดูกันอย่างลึกซึ้งยิ่งขึ้นว่า this
ส่งต่อได้อย่างไร:
หลังจากที่ decoration worker.slow
กลายเป็น function (x) { ... }
ดังนั้นเมื่อ worker.slow(2)
ถูกดำเนินการ wrapper จะได้รับ 2
เป็นอาร์กิวเมนต์และ this=worker
(เป็นวัตถุก่อนจุด)
ภายใน wrapper สมมติว่าผลลัพธ์ยังไม่ได้ถูกแคช func.call(this, x)
ส่งค่าปัจจุบัน this
( =worker
) และอาร์กิวเมนต์ปัจจุบัน ( =2
) ไปยังวิธีการดั้งเดิม
ตอนนี้เรามาทำให้ cachingDecorator
เป็นสากลมากยิ่งขึ้น จนถึงตอนนี้มันใช้งานได้กับฟังก์ชันอาร์กิวเมนต์เดี่ยวเท่านั้น
ตอนนี้จะแคชเมธอด multi-argument worker.slow
ได้อย่างไร
ให้คนงาน = { ช้า (นาที, สูงสุด) { กลับขั้นต่ำ + สูงสุด; // ถือว่า CPU-hogger น่ากลัว - - // ควรจำการเรียกอาร์กิวเมนต์เดียวกัน worker.slow = cachingDecorator (คนงานช้า);
ก่อนหน้านี้ สำหรับอาร์กิวเมนต์เดียว x
เราสามารถทำได้เพียงแค่ cache.set(x, result)
เพื่อบันทึกผลลัพธ์และ cache.get(x)
เพื่อดึงข้อมูลออกมา แต่ตอนนี้เราต้องจำผลลัพธ์ของ การรวมกันของอาร์กิวเมนต์ (min,max)
Map
ดั้งเดิมจะใช้ค่าเดียวเป็นคีย์เท่านั้น
มีวิธีแก้ไขมากมายที่เป็นไปได้:
ใช้โครงสร้างข้อมูลคล้ายแผนที่ใหม่ (หรือใช้บุคคลที่สาม) ซึ่งมีความหลากหลายมากขึ้นและอนุญาตให้ใช้หลายคีย์ได้
ใช้แผนที่แบบซ้อน: cache.set(min)
จะเป็น Map
ที่เก็บคู่ (max, result)
ดังนั้นเราจึงได้ result
เป็น cache.get(min).get(max)
รวมสองค่าเป็นหนึ่งเดียว ในกรณีเฉพาะของเรา เราสามารถใช้สตริง "min,max"
เป็นคีย์ Map
ได้ เพื่อความยืดหยุ่น เราสามารถจัดเตรียม ฟังก์ชันแฮช สำหรับมัณฑนากรที่รู้วิธีสร้างค่าหนึ่งค่าจากหลายๆ ค่า
สำหรับการใช้งานจริงหลายๆ รายการ ตัวแปรที่ 3 ก็เพียงพอแล้ว ดังนั้นเราจะยึดตามนั้น
นอกจากนี้เรายังต้องส่งผ่านไม่เพียงแค่ x
เท่านั้น แต่ต้องผ่านข้อโต้แย้งทั้งหมดใน func.call
ลองจำไว้ว่าใน function()
เราสามารถรับอาร์เรย์หลอกของอาร์กิวเมนต์เป็น arguments
ได้ ดังนั้น func.call(this, x)
ควรถูกแทนที่ด้วย func.call(this, ...arguments)
นี่คือ cachingDecorator
ที่ทรงพลังกว่า:
ให้คนงาน = { ช้า (นาที, สูงสุด) { alert(`เรียกด้วย ${min},${max}`); กลับขั้นต่ำ + สูงสุด; - - ฟังก์ชั่น cachingDecorator (func, แฮช) { ให้แคช = แผนที่ใหม่ (); ฟังก์ชันส่งคืน () { ให้คีย์ = แฮช (อาร์กิวเมนต์); - ถ้า (cache.has (คีย์)) { กลับ cache.get (คีย์); - ให้ result = func.call(นี่, ...อาร์กิวเมนต์); - cache.set (คีย์, ผลลัพธ์); ส่งคืนผลลัพธ์; - - ฟังก์ชั่นแฮช (args) { กลับหาเรื่อง [0] + ',' + หาเรื่อง [1]; - worker.slow = cachingDecorator (คนงานช้า, แฮช); การแจ้งเตือน( worker.slow(3, 5) ); //ทำงาน alert( "อีกครั้ง" + worker.slow(3, 5) ); // เหมือนกัน (แคช)
ตอนนี้มันใช้งานได้กับอาร์กิวเมนต์จำนวนเท่าใดก็ได้ (แม้ว่าฟังก์ชันแฮชจะต้องได้รับการปรับเพื่อให้สามารถอาร์กิวเมนต์จำนวนเท่าใดก็ได้ก็ตาม วิธีที่น่าสนใจในการจัดการนี้จะกล่าวถึงด้านล่าง)
มีการเปลี่ยนแปลงสองประการ:
ในบรรทัด (*)
จะเรียก hash
เพื่อสร้างคีย์เดียวจาก arguments
ที่นี่เราใช้ฟังก์ชัน "เข้าร่วม" ง่ายๆ ที่เปลี่ยนอาร์กิวเมนต์ (3, 5)
ให้เป็นคีย์ "3,5"
. กรณีที่ซับซ้อนมากขึ้นอาจต้องใช้ฟังก์ชันแฮชอื่นๆ
จากนั้น (**)
ใช้ func.call(this, ...arguments)
เพื่อส่งผ่านทั้งบริบทและอาร์กิวเมนต์ทั้งหมดที่ wrapper ได้รับ (ไม่ใช่แค่อันแรก) ไปยังฟังก์ชันดั้งเดิม
แทนที่จะใช้ func.call(this, ...arguments)
เราสามารถใช้ func.apply(this, arguments)
ได้
ไวยากรณ์ของเมธอดในตัว func.apply คือ:
func.apply (บริบท, args)
มันรันการตั้งค่า func
this=context
และใช้ args
วัตถุที่มีลักษณะคล้ายอาร์เรย์เป็นรายการอาร์กิวเมนต์
ความแตกต่างทางไวยากรณ์เพียงอย่างเดียวระหว่าง call
และ apply
คือ call
ต้องการรายการอาร์กิวเมนต์ ในขณะที่ apply
จะนำอ็อบเจ็กต์ที่มีลักษณะคล้ายอาร์เรย์ไปด้วย
ดังนั้นการโทรทั้งสองนี้จึงเกือบจะเทียบเท่ากัน:
func.call(บริบท, ...args); func.apply (บริบท, args);
พวกเขาดำเนินการเรียก func
เดียวกันกับบริบทและข้อโต้แย้งที่กำหนด
มีความแตกต่างเพียงเล็กน้อยเกี่ยวกับ args
:
ไวยากรณ์การแพร่กระจาย ...
อนุญาตให้ส่ง args
ที่สามารถทำซ้ำได้ เป็นรายการที่จะ call
apply
ยอมรับเฉพาะ args
ที่มีลักษณะคล้ายอาร์เรย์ เท่านั้น
…และสำหรับอ็อบเจ็กต์ที่ทั้งทำซ้ำได้และเหมือนอาเรย์ เช่น อาเรย์จริง เราสามารถใช้อ็อบเจ็กต์ใดก็ได้ แต่ apply
อาจจะเร็วกว่า เนื่องจากเอ็นจิ้น JavaScript ส่วนใหญ่ปรับให้เหมาะสมภายในได้ดีกว่า
การส่งผ่านข้อโต้แย้งทั้งหมดพร้อมกับบริบทไปยังฟังก์ชันอื่นเรียกว่า การโอนสาย
นั่นเป็นรูปแบบที่ง่ายที่สุด:
ให้ wrapper = function() { กลับ func.apply (นี่คือข้อโต้แย้ง); -
เมื่อโค้ดภายนอกเรียก wrapper
ดังกล่าว มันจะแยกไม่ออกจากการเรียกฟังก์ชันเดิม func
ตอนนี้เรามาทำการปรับปรุงเล็กน้อยอีกประการหนึ่งในฟังก์ชันการแฮช:
ฟังก์ชั่นแฮช (args) { กลับหาเรื่อง [0] + ',' + หาเรื่อง [1]; -
ณ ตอนนี้มันใช้ได้กับสองอาร์กิวเมนต์เท่านั้น จะดีกว่าถ้าสามารถติด args
ได้จำนวนเท่าใดก็ได้
วิธีแก้ปัญหาตามธรรมชาติคือใช้วิธี arr.join:
ฟังก์ชั่นแฮช (args) { กลับ args.join(); -
…น่าเสียดายที่มันใช้งานไม่ได้ เนื่องจากเรากำลังเรียก hash(arguments)
และอ็อบเจ็กต์ arguments
มีทั้งแบบวนซ้ำและแบบอาร์เรย์ แต่ไม่ใช่อาร์เรย์จริง
ดังนั้นการเรียก join
จะล้มเหลว ดังที่เราเห็นด้านล่าง:
ฟังก์ชั่นแฮช() { การแจ้งเตือน( arguments.join() ); // ข้อผิดพลาด: arguments.join ไม่ใช่ฟังก์ชัน - แฮช (1, 2);
ยังมีวิธีง่ายๆ ในการใช้ array join:
ฟังก์ชั่นแฮช() { alert( [].join.call(อาร์กิวเมนต์) ); // 1,2 - แฮช (1, 2);
เคล็ดลับนี้เรียกว่า การยืมวิธีการ
เราใช้ (ยืม) วิธีการเข้าร่วมจากอาร์เรย์ปกติ ( [].join
) และใช้ [].join.call
เพื่อเรียกใช้ในบริบทของ arguments
ทำไมมันถึงได้ผล?
นั่นเป็นเพราะว่าอัลกอริธึมภายในของวิธีการเนทิฟ arr.join(glue)
นั้นง่ายมาก
นำมาจากข้อกำหนดเกือบ "ตามสภาพ":
ให้ glue
เป็นอาร์กิวเมนต์แรก หรือถ้าไม่มีอาร์กิวเมนต์ ให้ใช้ลูกน้ำ ","
.
ให้ result
เป็นสตริงว่าง
ผนวก this[0]
เข้ากับ result
ต่อ glue
และ this[1]
.
ต่อ glue
และ this[2]
.
…ทำเช่นนี้จนสินค้า this.length
ติดกาวแล้ว
กลับ result
.
ในทางเทคนิคแล้วต้องใช้ this
และรวม this[0]
, this[1]
…ฯลฯ เข้าด้วยกัน มีเจตนาเขียนในลักษณะที่อนุญาตให้มีอาร์เรย์เช่น this
(ไม่ใช่เรื่องบังเอิญ มีหลายวิธีปฏิบัติตามแนวทางนี้) นั่นเป็นเหตุผลว่าทำไมมันถึงใช้งานได้กับ this=arguments
ด้วย
โดยทั่วไปแล้วจะปลอดภัยที่จะแทนที่ฟังก์ชันหรือวิธีการด้วยฟังก์ชันที่ตกแต่งแล้ว ยกเว้นสิ่งเล็กๆ น้อยๆ เพียงอย่างเดียว หากฟังก์ชันดั้งเดิมมีคุณสมบัติเช่น func.calledCount
หรืออะไรก็ตาม ฟังก์ชันที่ตกแต่งแล้วจะไม่จัดเตรียมไว้ เพราะนั่นคือกระดาษห่อ ดังนั้นเราต้องระมัดระวังหากมีใครใช้มัน
เช่นในตัวอย่างข้างต้น หากฟังก์ชัน slow
มีคุณสมบัติใดๆ อยู่ ดังนั้น cachingDecorator(slow)
จะเป็น wrapper ที่ไม่มีคุณสมบัติเหล่านั้น
มัณฑนากรบางคนอาจจัดเตรียมคุณสมบัติของตนเองไว้ เช่น มัณฑนากรอาจนับจำนวนครั้งที่เรียกใช้ฟังก์ชันและใช้เวลาเท่าใด และเปิดเผยข้อมูลนี้ผ่านคุณสมบัติของ wrapper
มีวิธีสร้างมัณฑนากรที่สามารถเข้าถึงคุณสมบัติของฟังก์ชันได้ แต่ต้องใช้ออบเจ็กต์ Proxy
พิเศษเพื่อรวมฟังก์ชัน เราจะพูดถึงเรื่องนี้ในบทความ Proxy and Reflect
มัณฑนากร เป็นเสื้อคลุมรอบฟังก์ชันที่เปลี่ยนแปลงพฤติกรรมของมัน งานหลักยังคงดำเนินการโดยหน้าที่
มัณฑนากรอาจมองว่าเป็น “คุณลักษณะ” หรือ “ลักษณะ” ที่สามารถเพิ่มเข้าไปในฟังก์ชันได้ เราจะเพิ่มอันเดียวหรือเพิ่มหลายอันก็ได้ และทั้งหมดนี้โดยไม่ต้องเปลี่ยนรหัส!
เพื่อใช้งาน cachingDecorator
เราได้ศึกษาวิธีการ:
func.call(context, arg1, arg2…) – เรียกใช้ func
โดยมีบริบทและอาร์กิวเมนต์ที่กำหนด
func.apply(context, args) – เรียก func
ที่ส่งผ่าน context
เช่น this
และ args
ที่มีลักษณะคล้ายอาร์เรย์เข้าไปในรายการอาร์กิวเมนต์
การโอนสาย ทั่วไปมักจะทำได้โดยใช้ apply
:
ให้ wrapper = function() { กลับ original.apply (นี่คือข้อโต้แย้ง); -
นอกจากนี้เรายังเห็นตัวอย่างของ วิธีการยืม เมื่อเราใช้วิธีจากวัตถุและ call
มันในบริบทของวัตถุอื่น เป็นเรื่องปกติที่จะใช้วิธีการแบบอาร์เรย์และนำไปใช้กับ arguments
อีกทางเลือกหนึ่งคือใช้วัตถุพารามิเตอร์ที่เหลือที่เป็นอาร์เรย์จริง
มีนักตกแต่งมากมายในป่า ตรวจสอบว่าคุณได้รับพวกมันมาดีแค่ไหนโดยการแก้ปัญหาในบทนี้
ความสำคัญ: 5
สร้าง spy(func)
ที่ควรส่งคืน wrapper ที่บันทึกการเรียกทั้งหมดให้ทำงานในคุณสมบัติ calls
ทุกการโทรจะถูกบันทึกเป็นอาร์เรย์ของอาร์กิวเมนต์
ตัวอย่างเช่น:
ฟังก์ชั่นงาน (a, b) { การแจ้งเตือน (a + b); // งานเป็นฟังก์ชันหรือวิธีการที่กำหนดเอง - งาน = สายลับ (งาน); งาน(1, 2); // 3 งาน(4, 5); // 9 สำหรับ (ให้ args ของ work.calls) { alert( 'โทร:' + args.join() ); // "โทร:1,2", "โทร:4,5" -
ป.ล. บางครั้งมัณฑนากรนั้นมีประโยชน์สำหรับการทดสอบหน่วย รูปแบบขั้นสูงคือ sinon.spy
ในไลบรารี Sinon.JS
เปิดแซนด์บ็อกซ์พร้อมการทดสอบ
wrapper ที่ส่งคืนโดย spy(f)
ควรเก็บข้อโต้แย้งทั้งหมดแล้วใช้ f.apply
เพื่อโอนสาย
ฟังก์ชั่นสายลับ (func) { ตัวตัดฟังก์ชัน (...args) { // ใช้ ...args แทนอาร์กิวเมนต์เพื่อจัดเก็บอาร์เรย์ "ของจริง" ใน wrapper.calls wrapper.calls.push(args); กลับ func.apply (นี่, args); - wrapper.calls = []; กระดาษห่อกลับ; -
เปิดโซลูชันพร้อมการทดสอบในแซนด์บ็อกซ์
ความสำคัญ: 5
สร้าง delay(f, ms)
ที่หน่วงเวลาการเรียก f
แต่ละครั้งเป็น ms
วินาที
ตัวอย่างเช่น:
ฟังก์ชัน ฉ(x) { การแจ้งเตือน(x); - // สร้าง wrapper ให้ f1,000 = ความล่าช้า (f, 1,000); ให้ f1500 = ความล่าช้า (f, 1500); f1000("ทดสอบ"); // แสดง "ทดสอบ" หลังจาก 1,000 มิลลิวินาที f1500("ทดสอบ"); // แสดง "ทดสอบ" หลังจาก 1500ms
กล่าวอีกนัยหนึ่ง delay(f, ms)
ส่งคืนตัวแปร “ล่าช้าโดย ms
” ของ f
ในโค้ดด้านบน f
เป็นฟังก์ชันของอาร์กิวเมนต์เดียว แต่โซลูชันของคุณควรผ่านอาร์กิวเมนต์ทั้งหมดและบริบท this
เปิดแซนด์บ็อกซ์พร้อมการทดสอบ
วิธีแก้ปัญหา:
ฟังก์ชั่นล่าช้า (f, ms) { ฟังก์ชันส่งคืน () { setTimeout(() => f.apply (นี่, อาร์กิวเมนต์), ms); - - ให้ f1,000 = ล่าช้า (แจ้งเตือน, 1,000); f1000("ทดสอบ"); // แสดง "ทดสอบ" หลังจาก 1,000 มิลลิวินาที
โปรดทราบว่าฟังก์ชันลูกศรถูกนำมาใช้ที่นี่อย่างไร ดังที่เราทราบ ฟังก์ชั่นลูกศรไม่มี this
และ arguments
ของตัวเอง ดังนั้น f.apply(this, arguments)
จึงรับ this
และ arguments
จาก wrapper
ถ้าเราผ่านฟังก์ชันปกติ setTimeout
จะเรียกมันโดยไม่มีอาร์กิวเมนต์และ this=window
(สมมติว่าเราอยู่ในเบราว์เซอร์)
เรายังคงส่งค่า this
ไปได้โดยใช้ตัวแปรตัวกลาง แต่นั่นจะยุ่งยากกว่านิดหน่อย:
ฟังก์ชั่นล่าช้า (f, ms) { ฟังก์ชันส่งคืน (...args) { ให้ saveThis = นี้; // เก็บสิ่งนี้ไว้ในตัวแปรระดับกลาง setTimeout (ฟังก์ชัน () { f.apply(savedThis, args); //ใช้ที่นี่ }, มิลลิวินาที); - -
เปิดโซลูชันพร้อมการทดสอบในแซนด์บ็อกซ์
ความสำคัญ: 5
ผลลัพธ์ของเครื่องมือตกแต่ง debounce(f, ms)
คือ wrapper ที่ระงับการเรียก f
จนกว่าจะไม่มีการใช้งาน ms
มิลลิวินาที (ไม่มีการเรียก “ระยะเวลาคูลดาวน์”) จากนั้นเรียกใช้ f
หนึ่งครั้งพร้อมกับอาร์กิวเมนต์ล่าสุด
กล่าวอีกนัยหนึ่ง debounce
ก็เหมือนกับเลขานุการที่รับ "โทรศัพท์" และรอจนกว่าจะเงียบไป ms
วินาที จากนั้นจึงโอนข้อมูลการโทรล่าสุดไปที่ “เจ้านาย” (เรียกค่า f
จริง)
ตัวอย่างเช่น เรามีฟังก์ชัน f
และแทนที่ด้วย f = debounce(f, 1000)
จากนั้นหากฟังก์ชัน wrap ถูกเรียกที่ 0ms, 200ms และ 500ms แล้วไม่มีการเรียก f
จริงจะถูกเรียกเพียงครั้งเดียวที่ 1500ms นั่นคือ: หลังจากช่วงเวลาคูลดาวน์ 1,000 มิลลิวินาทีจากการโทรครั้งล่าสุด
…และมันจะรับอาร์กิวเมนต์ของการโทรครั้งสุดท้าย ส่วนสายอื่น ๆ จะถูกละเว้น
นี่คือโค้ดสำหรับมัน (ใช้ตัวตกแต่ง debounce จากไลบรารี Lodash):
ให้ f = _.debounce(alert, 1,000); ฉ("ก"); setTimeout( () => f("b"), 200); setTimeout( () => f("c"), 500); // ฟังก์ชั่น debounced จะรอ 1,000 มิลลิวินาทีหลังจากการโทรครั้งสุดท้าย จากนั้นจึงรัน: alert("c")
ตอนนี้เป็นตัวอย่างในทางปฏิบัติ สมมติว่าผู้ใช้พิมพ์อะไรบางอย่าง และเราต้องการส่งคำขอไปยังเซิร์ฟเวอร์เมื่ออินพุตเสร็จสิ้น
ไม่มีประโยชน์ที่จะส่งคำขอสำหรับอักขระทุกตัวที่พิมพ์ แต่เราอยากจะรอแล้วจึงประมวลผลผลลัพธ์ทั้งหมดแทน
ในเว็บเบราว์เซอร์ เราสามารถตั้งค่าตัวจัดการเหตุการณ์ ซึ่งเป็นฟังก์ชันที่เรียกใช้ทุกครั้งที่มีการเปลี่ยนแปลงช่องป้อนข้อมูล โดยปกติแล้ว ตัวจัดการเหตุการณ์จะถูกเรียกบ่อยมากสำหรับคีย์ที่พิมพ์ทุกอัน แต่ถ้าเรา debounce
มัน 1,000 มิลลิวินาที มันจะถูกเรียกเพียงครั้งเดียว หลังจาก 1,000 มิลลิวินาทีหลังจากอินพุตล่าสุด
ในตัวอย่างสดนี้ ตัวจัดการจะใส่ผลลัพธ์ลงในช่องด้านล่าง ลองทำดู:
ดู? อินพุตที่สองเรียกฟังก์ชัน debounced ดังนั้นเนื้อหาจะถูกประมวลผลหลังจาก 1,000 มิลลิวินาทีจากอินพุตสุดท้าย
ดังนั้น debounce
เป็นวิธีที่ดีในการประมวลผลลำดับเหตุการณ์: ไม่ว่าจะเป็นลำดับการกดปุ่ม การเคลื่อนไหวของเมาส์ หรืออย่างอื่น
มันจะรอตามเวลาที่กำหนดหลังจากการโทรครั้งสุดท้าย จากนั้นจึงเรียกใช้ฟังก์ชันที่สามารถประมวลผลผลลัพธ์ได้
ภารกิจคือการใช้มัณฑนากร debounce
คำแนะนำ: นั่นเป็นเพียงไม่กี่บรรทัดหากคุณลองคิดดู :)
เปิดแซนด์บ็อกซ์พร้อมการทดสอบ
ฟังก์ชั่น debounce (func, ms) { ปล่อยให้หมดเวลา; ฟังก์ชันส่งคืน () { clearTimeout (หมดเวลา); หมดเวลา = setTimeout(() => func.apply (นี่, อาร์กิวเมนต์), ms); - -
การเรียกร้องให้ debounce
ส่งคืน wrapper เมื่อเรียก จะกำหนดเวลาการเรียกใช้ฟังก์ชันเดิมหลังจาก ms
ที่กำหนด และยกเลิกการหมดเวลาดังกล่าวก่อนหน้านี้
เปิดโซลูชันพร้อมการทดสอบในแซนด์บ็อกซ์
ความสำคัญ: 5
สร้าง throttle(f, ms)
- ที่ส่งคืน wrapper
เมื่อมันถูกเรียกหลายครั้ง มันจะผ่านการเรียกไปที่ f
สูงสุดหนึ่งครั้งต่อ ms
วินาที
เมื่อเปรียบเทียบกับมัณฑนากร debounce พฤติกรรมจะแตกต่างอย่างสิ้นเชิง:
debounce
รันฟังก์ชันหนึ่งครั้งหลังจากช่วง "คูลดาวน์" เหมาะสำหรับการประมวลผลผลลัพธ์สุดท้าย
throttle
วิ่งไม่บ่อยกว่าเวลา ms
กำหนด เหมาะสำหรับการอัปเดตเป็นประจำซึ่งไม่ควรบ่อยนัก
กล่าวอีกนัยหนึ่ง throttle
เป็นเหมือนเลขานุการที่รับสาย แต่รบกวนเจ้านาย (เรียกจริงว่า f
) ไม่บ่อยกว่าหนึ่งครั้งต่อ ms
วินาที
มาตรวจสอบแอปพลิเคชันในชีวิตจริงเพื่อทำความเข้าใจข้อกำหนดนั้นให้ดียิ่งขึ้นและดูว่ามาจากไหน
ตัวอย่างเช่น เราต้องการติดตามการเคลื่อนไหวของเมาส์
ในเบราว์เซอร์ เราสามารถตั้งค่าฟังก์ชันให้ทำงานทุกครั้งที่เมาส์เคลื่อนที่ และรับตำแหน่งตัวชี้ขณะเคลื่อนที่ ในระหว่างการใช้งานเมาส์ ฟังก์ชันนี้มักจะทำงานบ่อยมาก อาจเป็นประมาณ 100 ครั้งต่อวินาที (ทุกๆ 10 มิลลิวินาที) เราต้องการอัปเดตข้อมูลบางอย่างบนหน้าเว็บเมื่อตัวชี้เคลื่อนที่
…แต่การอัพเดตฟังก์ชั่ update()
นั้นหนักเกินกว่าจะทำได้กับทุกการเคลื่อนไหวระดับไมโคร นอกจากนี้ยังไม่สมเหตุสมผลที่จะอัปเดตบ่อยกว่าหนึ่งครั้งต่อ 100 มิลลิวินาที
ดังนั้นเราจะรวมมันไว้ในมัณฑนากร: ใช้ throttle(update, 100)
เป็นฟังก์ชันในการทำงานในการเลื่อนเมาส์แต่ละครั้ง แทนที่จะเป็น update()
ดั้งเดิม มัณฑนากรจะถูกเรียกบ่อยครั้ง แต่ส่งต่อสายเพื่อ update()
สูงสุดหนึ่งครั้งต่อ 100 มิลลิวินาที
สายตาจะมีลักษณะดังนี้:
สำหรับการเคลื่อนไหวเมาส์ครั้งแรก รูปแบบที่ตกแต่งแล้วจะส่งผ่านการโทรเพื่อ update
ทันที สิ่งสำคัญคือผู้ใช้จะเห็นปฏิกิริยาของเราต่อการเคลื่อนไหวทันที
จากนั้นเมื่อเมาส์เคลื่อนที่ต่อไป จนถึง 100ms
ก็ไม่มีอะไรเกิดขึ้น รูปแบบการตกแต่งไม่สนใจการโทร
เมื่อสิ้นสุด 100ms
- update
อีกครั้งหนึ่งเกิดขึ้นกับพิกัดล่าสุด
ในที่สุดเมาส์ก็หยุดอยู่ที่ไหนสักแห่ง ตัวแปรที่ตกแต่งแล้วรอจนกระทั่ง 100ms
หมดอายุ จากนั้นจึงรัน update
ด้วยพิกัดล่าสุด สิ่งสำคัญคือต้องประมวลผลพิกัดสุดท้ายของเมาส์
ตัวอย่างรหัส:
ฟังก์ชัน ฉ(ก) { console.log(ก); - // f1000 ผ่านการเรียกไปยัง f สูงสุดหนึ่งครั้งต่อ 1,000 ms ให้ f1,000 = เค้น (f, 1,000); f1000(1); // แสดง 1 f1,000(2); // (กำลังควบคุมปริมาณ 1,000ms ยังไม่ออก) f1000(3); // (กำลังควบคุมปริมาณ 1,000ms ยังไม่ออก) // เมื่อหมดเวลา 1,000 ms... // ...เอาต์พุต 3 ค่ากลาง 2 ถูกละเว้น
PS อาร์กิวเมนต์และบริบท this
ส่งไปยัง f1000
ควรส่งผ่านไปยังต้นฉบับ f
เปิดแซนด์บ็อกซ์พร้อมการทดสอบ
ฟังก์ชั่นคันเร่ง (func, ms) { ให้ isThrottled = เท็จ ที่บันทึกไว้ Args, บันทึกนี้; ตัวห่อฟังก์ชัน () { ถ้า (ถูกควบคุม) { // (2) saveArgs = อาร์กิวเมนต์; saveThis=สิ่งนี้; กลับ; - ถูกควบคุมปริมาณ = จริง; func.apply (นี่คือข้อโต้แย้ง); // (1) setTimeout (ฟังก์ชัน () { isThrottled = เท็จ; // (3) ถ้า (savedArgs) { wrapper.apply (savedThis, saveArgs); saveArgs = saveThis = null; - }, มิลลิวินาที); - กระดาษห่อกลับ; -
การเรียก throttle(func, ms)
ส่งคืน wrapper
ในระหว่างการเรียกครั้งแรก wrapper
จะรัน func
และตั้งค่าสถานะคูลดาวน์ ( isThrottled = true
)
ในสถานะนี้การโทรทั้งหมดจะถูกจดจำไว้ใน savedArgs/savedThis
โปรดทราบว่าทั้งบริบทและข้อโต้แย้งมีความสำคัญเท่าเทียมกันและควรจดจำไว้ เราต้องการมันพร้อมกันเพื่อสร้างการโทรซ้ำ
หลังจากผ่านไป ms
มิลลิวินาที ระบบจะทริกเกอร์ setTimeout
สถานะคูลดาวน์จะถูกลบออก ( isThrottled = false
) และหากเราละเว้นการโทร wrapper
จะถูกดำเนินการด้วยอาร์กิวเมนต์และบริบทที่จดจำล่าสุด
ขั้นตอนที่ 3 ไม่ได้รัน func
แต่ wrapper
เนื่องจากเราไม่เพียงแต่จำเป็นต้องดำเนินการ func
เท่านั้น แต่ยังเข้าสู่สถานะคูลดาวน์อีกครั้งและตั้งค่าการหมดเวลาเพื่อรีเซ็ต
เปิดโซลูชันพร้อมการทดสอบในแซนด์บ็อกซ์