ปรับปรุงความรู้ JavaScript ของคุณโดยการอ่านซอร์สโค้ด

เผยแพร่แล้ว: 2022-03-10
สรุปโดยย่อ ↬ เมื่อคุณยังอยู่ในช่วงเริ่มต้นของอาชีพการเขียนโปรแกรม การขุดซอร์สโค้ดของไลบรารีโอเพ่นซอร์สและเฟรมเวิร์กอาจเป็นความพยายามที่น่ากลัว ในบทความนี้ Carl Mungazi ได้แชร์วิธีที่เขาเอาชนะความกลัวและเริ่มใช้ซอร์สโค้ดเพื่อพัฒนาความรู้และทักษะของเขา เขายังใช้ Redux เพื่อสาธิตวิธีการทำลายห้องสมุด

คุณจำครั้งแรกที่คุณเจาะลึกลงไปในซอร์สโค้ดของไลบรารีหรือเฟรมเวิร์กที่คุณใช้บ่อยได้หรือไม่? สำหรับฉัน ช่วงเวลานั้นมาในช่วงงานแรกของฉันในฐานะนักพัฒนาส่วนหน้าเมื่อสามปีที่แล้ว

เราเพิ่งเสร็จสิ้นการเขียนกรอบงานเดิมภายในที่เราเคยใช้ในการสร้างหลักสูตรอีเลิร์นนิงใหม่ ในตอนเริ่มต้นของการเขียนใหม่ เราได้ใช้เวลาตรวจสอบโซลูชันต่างๆ มากมาย รวมถึง Mithril, Inferno, Angular, React, Aurelia, Vue และ Polymer เนื่องจากฉันเป็นมือใหม่อย่างมาก (ฉันเพิ่งเปลี่ยนจากการทำข่าวเป็นการพัฒนาเว็บ) ฉันจำได้ว่ารู้สึกกังวลใจกับความซับซ้อนของแต่ละเฟรมเวิร์กและไม่เข้าใจวิธีการทำงานของแต่ละเฟรมเวิร์ก

ความเข้าใจของฉันเพิ่มขึ้นเมื่อฉันเริ่มตรวจสอบกรอบการทำงานที่เราเลือกอย่าง Mithril อย่างละเอียดยิ่งขึ้น ตั้งแต่นั้นมา ความรู้ของฉันเกี่ยวกับ JavaScript และการเขียนโปรแกรมโดยทั่วไป - ได้รับความช่วยเหลืออย่างมากจากชั่วโมงที่ฉันได้ใช้เวลาในการขุดลึกลงไปในความกล้าของห้องสมุดที่ฉันใช้ทุกวันทั้งในที่ทำงานหรือในโครงการของฉันเอง ในโพสต์นี้ ฉันจะแบ่งปันวิธีการบางอย่างที่คุณสามารถนำไลบรารี่หรือกรอบงานที่คุณโปรดปรานไปใช้เป็นเครื่องมือทางการศึกษา

ซอร์สโค้ดสำหรับฟังก์ชันไฮเปอร์สคริปต์ของมิธริล
การแนะนำโค้ดการอ่านครั้งแรกของฉันคือการใช้ฟังก์ชันไฮเปอร์สคริปต์ของมิธริล (ตัวอย่างขนาดใหญ่)
เพิ่มเติมหลังกระโดด! อ่านต่อด้านล่าง↓

ประโยชน์ของการอ่านซอร์สโค้ด

ประโยชน์หลักๆ ประการหนึ่งของการอ่านซอร์สโค้ดคือจำนวนสิ่งที่คุณเรียนรู้ได้ เมื่อฉันดู codebase ของ Mithril ครั้งแรก ฉันมีความคิดที่คลุมเครือว่า DOM เสมือนคืออะไร เมื่อฉันทำเสร็จแล้ว ฉันก็พบว่า DOM เสมือนเป็นเทคนิคที่เกี่ยวข้องกับการสร้างทรีออบเจ็กต์ที่อธิบายว่าอินเทอร์เฟซผู้ใช้ของคุณควรมีลักษณะอย่างไร ต้นไม้นั้นจะถูกเปลี่ยนเป็นองค์ประกอบ DOM โดยใช้ DOM API เช่น document.createElement การอัปเดตทำได้โดยการสร้างทรีใหม่ที่อธิบายสถานะในอนาคตของอินเทอร์เฟซผู้ใช้ จากนั้นเปรียบเทียบกับออบเจกต์จากทรีเก่า

ฉันได้อ่านเกี่ยวกับเรื่องนี้ทั้งหมดในบทความและบทช่วยสอนต่างๆ และถึงแม้จะเป็นประโยชน์ แต่การได้สังเกตการทำงานในบริบทของแอปพลิเคชันที่เราจัดส่งให้นั้นก็เป็นเรื่องที่ให้ความกระจ่างมากสำหรับฉัน นอกจากนี้ยังสอนฉันว่าควรถามคำถามใดเมื่อเปรียบเทียบกรอบงานต่างๆ ตัวอย่างเช่น แทนที่จะดูดาว GitHub ตอนนี้ฉันรู้แล้วว่าต้องถามคำถามเช่น "วิธีที่แต่ละเฟรมเวิร์กดำเนินการอัปเดตส่งผลต่อประสิทธิภาพและประสบการณ์ของผู้ใช้อย่างไร"

ประโยชน์อีกประการหนึ่งคือการเพิ่มความซาบซึ้งและความเข้าใจในสถาปัตยกรรมแอปพลิเคชันที่ดีของคุณ แม้ว่าโครงการโอเพนซอร์สส่วนใหญ่มักใช้โครงสร้างเดียวกันกับที่เก็บข้อมูล แต่แต่ละโครงการก็มีความแตกต่าง โครงสร้างของ Mithril ค่อนข้างแบน และหากคุณคุ้นเคยกับ API ของมัน คุณสามารถคาดเดาโค้ดในโฟลเดอร์ต่างๆ เช่น render , router และ request ได้ ในทางกลับกัน โครงสร้างของ React สะท้อนถึงสถาปัตยกรรมใหม่ ผู้ดูแลได้แยกโมดูลที่รับผิดชอบสำหรับการอัปเดต UI ( react-reconciler ) ออกจากโมดูลที่รับผิดชอบสำหรับการแสดงผลองค์ประกอบ DOM ( react-dom )

ข้อดีอย่างหนึ่งของสิ่งนี้คือตอนนี้นักพัฒนาสามารถเขียนตัวแสดงภาพที่กำหนดเองได้ง่ายขึ้นโดยเชื่อมต่อกับแพ็คเกจ react-reconciler Parcel ซึ่งเป็นโมดูลบันเดิลที่ฉันเพิ่งศึกษามาไม่นานนี้ ยังมีโฟลเดอร์ packages อย่าง React ด้วย โมดูลคีย์มีชื่อว่า parcel-bundler และมีโค้ดที่รับผิดชอบในการสร้างบันเดิล การปั่นเซิร์ฟเวอร์โมดูลยอดนิยม และเครื่องมือบรรทัดคำสั่ง

ส่วนของข้อกำหนด JavaScript ซึ่งอธิบายวิธีการทำงานของ Object.prototype.toString
ไม่นานก่อนที่ซอร์สโค้ดที่คุณกำลังอ่านจะนำคุณไปสู่ข้อกำหนดจาวาสคริปต์ (ตัวอย่างขนาดใหญ่)

ข้อดีอีกประการหนึ่ง ซึ่งทำให้ฉันประหลาดใจก็คือ คุณจะรู้สึกสบายใจมากขึ้นในการอ่านข้อกำหนดจาวาสคริปต์อย่างเป็นทางการซึ่งกำหนดวิธีการทำงานของภาษา ครั้งแรกที่ฉันอ่านข้อมูลจำเพาะคือตอนที่ฉันกำลังตรวจสอบความแตกต่างระหว่างการ throw Error และข้อ throw new Error (การแจ้งเตือนสปอยเลอร์ - ไม่มี) ฉันตรวจสอบสิ่งนี้เพราะฉันสังเกตเห็นว่า Mithril ใช้ throw Error ในการใช้งานฟังก์ชั่น m ของมัน และฉันสงสัยว่าจะมีประโยชน์มากกว่า throw new Error หรือไม่ ตั้งแต่นั้นมา ฉันได้เรียนรู้ว่าตัวดำเนินการเชิงตรรกะ && และ || ไม่จำเป็นต้องส่งคืนบูลีน พบกฎที่ควบคุมวิธีที่ตัวดำเนินการความเท่าเทียมกัน == บังคับค่าและเหตุผล Object.prototype.toString.call({}) ส่งคืน '[object Object]'

เทคนิคการอ่านซอร์สโค้ด

มีหลายวิธีในการเข้าถึงซอร์สโค้ด ฉันพบวิธีที่ง่ายที่สุดในการเริ่มต้นคือการเลือกวิธีการจากไลบรารีที่คุณเลือกและบันทึกสิ่งที่เกิดขึ้นเมื่อคุณเรียกใช้ อย่าจัดทำเอกสารทุกขั้นตอนแต่พยายามระบุขั้นตอนและโครงสร้างโดยรวม

ฉันเพิ่งทำสิ่งนี้ด้วย ReactDOM.render และเรียนรู้มากมายเกี่ยวกับ React Fiber และเหตุผลบางประการเบื้องหลังการใช้งาน โชคดีที่ React เป็นเฟรมเวิร์กที่ได้รับความนิยม ฉันพบบทความมากมายที่เขียนโดยนักพัฒนารายอื่นในประเด็นเดียวกัน ซึ่งทำให้กระบวนการนี้เร็วขึ้น

การดำน้ำลึกนี้ยังแนะนำให้ฉันรู้จักกับแนวคิดของการตั้งเวลาแบบร่วมมือ วิธี window.requestIdleCallback และตัวอย่างในโลกแห่งความเป็นจริงของรายการที่เชื่อมโยง (React จัดการการอัปเดตโดยใส่ไว้ในคิวซึ่งเป็นรายการลิงก์ของการอัปเดตตามลำดับความสำคัญ) เมื่อทำเช่นนี้ ขอแนะนำให้สร้างแอปพลิเคชันพื้นฐานโดยใช้ไลบรารี สิ่งนี้ทำให้การดีบักง่ายขึ้นเพราะคุณไม่ต้องจัดการกับการติดตามสแต็กที่เกิดจากไลบรารีอื่น

หากฉันไม่ได้ทำการตรวจสอบเชิงลึก ฉันจะเปิดโฟลเดอร์ /node_modules ในโครงการที่ฉันกำลังทำงานอยู่ หรือฉันจะไปที่ที่เก็บ GitHub สิ่งนี้มักจะเกิดขึ้นเมื่อฉันพบจุดบกพร่องหรือคุณสมบัติที่น่าสนใจ เมื่ออ่านโค้ดบน GitHub ตรวจสอบให้แน่ใจว่าคุณกำลังอ่านจากเวอร์ชันล่าสุด คุณสามารถดูโค้ดจากการคอมมิตด้วยแท็กเวอร์ชันล่าสุดได้โดยคลิกปุ่มที่ใช้เปลี่ยนสาขาและเลือก "แท็ก" ไลบรารีและเฟรมเวิร์กกำลังอยู่ระหว่างการเปลี่ยนแปลงตลอดไป ดังนั้นคุณจึงไม่ต้องการเรียนรู้เกี่ยวกับบางสิ่งที่อาจจะหายไปในเวอร์ชันถัดไป

อีกวิธีหนึ่งที่เกี่ยวข้องน้อยกว่าในการอ่านซอร์สโค้ดคือสิ่งที่ฉันชอบเรียกว่าวิธี 'การมองคร่าวๆ' เมื่อฉันเริ่มอ่านโค้ดตั้งแต่เนิ่นๆ ฉันติดตั้ง express.js เปิดโฟลเดอร์ /node_modules และดำเนินการอ้างอิง หาก README ไม่ได้ให้คำอธิบายที่น่าพอใจแก่ฉัน ฉันจะอ่านที่มา การทำเช่นนี้ทำให้ฉันค้นพบสิ่งที่น่าสนใจเหล่านี้:

  • Express ขึ้นอยู่กับสองโมดูลซึ่งทั้งคู่รวมอ็อบเจ็กต์ แต่ทำในลักษณะที่แตกต่างกันมาก merge-descriptors จะเพิ่มคุณสมบัติที่พบโดยตรงบนวัตถุต้นทางเท่านั้น และมันยังรวมคุณสมบัติที่ไม่สามารถระบุได้ ในขณะที่ utils-merge จะวนซ้ำเฉพาะคุณสมบัติที่นับได้ของวัตถุเช่นเดียวกับที่พบในห่วงโซ่ต้นแบบ merge-descriptors ใช้ Object.getOwnPropertyNames() และ Object.getOwnPropertyDescriptor() ในขณะที่ utils-merge ใช้สำหรับ for..in
  • โมดูล setprototypeof มีวิธีการตั้งค่าแบบข้ามแพลตฟอร์มสำหรับการสร้างต้นแบบของอ็อบเจกต์ที่สร้างอินสแตนซ์
  • escape-html เป็นโมดูล 78 บรรทัดสำหรับการหลีกเลี่ยงสตริงของเนื้อหา เพื่อให้สามารถสอดแทรกในเนื้อหา HTML ได้

แม้ว่าการค้นพบนี้จะไม่มีประโยชน์ในทันที แต่การมีความเข้าใจทั่วไปเกี่ยวกับการอ้างอิงที่ใช้โดยไลบรารีหรือกรอบงานของคุณนั้นมีประโยชน์

เมื่อพูดถึงการดีบักโค้ดส่วนหน้า เครื่องมือดีบั๊กของเบราว์เซอร์คือเพื่อนที่ดีที่สุดของคุณ เหนือสิ่งอื่นใด สิ่งเหล่านี้อนุญาตให้คุณหยุดโปรแกรมเมื่อใดก็ได้ และตรวจสอบสถานะของโปรแกรม ข้ามการดำเนินการของฟังก์ชัน หรือก้าวเข้าหรือออกจากโปรแกรม บางครั้งสิ่งนี้จะไม่สามารถทำได้ในทันทีเนื่องจากโค้ดถูกย่อให้เล็กสุด ฉันมักจะ unminify และคัดลอกโค้ด unminified ลงในไฟล์ที่เกี่ยวข้องในโฟลเดอร์ /node_modules

ซอร์สโค้ดสำหรับฟังก์ชัน ReactDOM.render
เข้าใกล้การดีบักเหมือนกับที่คุณทำกับแอปพลิเคชันอื่นๆ สร้างสมมติฐานแล้วทดสอบ (ตัวอย่างขนาดใหญ่)

กรณีศึกษา: ฟังก์ชันการเชื่อมต่อของ Redux

React-Redux เป็นไลบรารี่ที่ใช้จัดการสถานะของแอปพลิเคชัน React เมื่อต้องจัดการกับไลบรารียอดนิยมเช่นนี้ ฉันเริ่มต้นด้วยการค้นหาบทความที่เขียนเกี่ยวกับการนำไปใช้ ในการทำเช่นนั้นสำหรับกรณีศึกษานี้ ฉันมาเจอบทความนี้ นี่เป็นข้อดีอีกอย่างของการอ่านซอร์สโค้ด ขั้นตอนการวิจัยมักจะนำคุณไปสู่บทความที่ให้ข้อมูลเช่นนี้ ซึ่งจะช่วยปรับปรุงความคิดและความเข้าใจของคุณเองเท่านั้น

connect เป็นฟังก์ชัน React-Redux ซึ่งเชื่อมต่อส่วนประกอบ React กับที่เก็บ Redux ของแอปพลิเคชัน ยังไง? ตามเอกสาร มันทำสิ่งต่อไปนี้:

“...ส่งคืนคลาสส่วนประกอบใหม่ที่เชื่อมต่อซึ่งรวมส่วนประกอบที่คุณส่งผ่านเข้ามา”

หลังจากอ่านแล้ว ฉันจะถามคำถามต่อไปนี้:

  • ฉันรู้รูปแบบหรือแนวความคิดใด ๆ ที่ฟังก์ชันรับอินพุตแล้วส่งคืนอินพุตเดียวกันนั้นพร้อมฟังก์ชันเพิ่มเติมหรือไม่
  • ถ้าฉันรู้รูปแบบดังกล่าว ฉันจะใช้สิ่งนี้ตามคำอธิบายที่ให้ไว้ในเอกสารได้อย่างไร

โดยปกติ ขั้นตอนต่อไปคือการสร้างแอปตัวอย่างพื้นฐานที่ใช้การ connect อย่างไรก็ตาม ในโอกาสนี้ ฉันเลือกใช้แอป React ใหม่ที่เรากำลังสร้างที่ Limejump เพราะฉันต้องการเข้าใจการ connect ภายในบริบทของแอปพลิเคชัน ซึ่งในที่สุดจะเข้าสู่สภาพแวดล้อมการผลิต

องค์ประกอบที่ฉันมุ่งเน้นมีลักษณะดังนี้:

 class MarketContainer extends Component { // code omitted for brevity } const mapDispatchToProps = dispatch => { return { updateSummary: (summary, start, today) => dispatch(updateSummary(summary, start, today)) } } export default connect(null, mapDispatchToProps)(MarketContainer);

เป็นส่วนประกอบคอนเทนเนอร์ที่ห่อหุ้มส่วนประกอบเชื่อมต่อที่มีขนาดเล็กลงสี่ชิ้น สิ่งแรกที่คุณเจอในไฟล์ที่เอ็กซ์พอร์ตวิธีการ connect คือความคิดเห็นนี้: connect เป็น Facade เหนือ connectAdvanced โดยไม่ต้องไปไกล เรามีช่วงเวลาการเรียนรู้ครั้งแรกของเรา: โอกาสในการสังเกตรูปแบบการออกแบบซุ้มในการดำเนินการ ในตอนท้ายของไฟล์เราจะเห็นว่าการ connect ส่งออกการเรียกใช้ฟังก์ชันที่เรียกว่า createConnect พารามิเตอร์ของมันคือค่าดีฟอลต์จำนวนหนึ่งซึ่งถูกทำลายล้างดังนี้:

 export function createConnect({ connectHOC = connectAdvanced, mapStateToPropsFactories = defaultMapStateToPropsFactories, mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories, mergePropsFactories = defaultMergePropsFactories, selectorFactory = defaultSelectorFactory } = {})

อีกครั้ง เราเจอช่วงเวลาแห่งการเรียนรู้อื่น: การ ส่งออกฟังก์ชันที่เรียกใช้ และ การทำลายโครงสร้างอาร์กิวเมนต์ของฟังก์ชันเริ่มต้น ส่วนการทำลายล้างเป็นช่วงเวลาแห่งการเรียนรู้เพราะมีการเขียนโค้ดดังนี้:

 export function createConnect({ connectHOC = connectAdvanced, mapStateToPropsFactories = defaultMapStateToPropsFactories, mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories, mergePropsFactories = defaultMergePropsFactories, selectorFactory = defaultSelectorFactory })

มันจะส่งผลให้เกิดข้อผิดพลาด Uncaught TypeError: Cannot destructure property 'connectHOC' of 'undefined' or 'null'. เนื่องจากฟังก์ชันไม่มีอาร์กิวเมนต์เริ่มต้นที่จะถอยกลับ

หมายเหตุ : สำหรับข้อมูลเพิ่มเติม คุณสามารถอ่านบทความของ David Walsh ช่วงเวลาแห่งการเรียนรู้บางช่วงอาจดูไร้สาระ ทั้งนี้ขึ้นอยู่กับความรู้ภาษาของคุณ ดังนั้นคุณควรจดจ่ออยู่กับสิ่งที่คุณไม่เคยเห็นมาก่อนหรือจำเป็นต้องเรียนรู้เพิ่มเติม

createConnect เองไม่ได้ทำอะไรในเนื้อหาฟังก์ชัน มันส่งคืนฟังก์ชั่นที่เรียกว่า connect อันที่ฉันใช้ที่นี่:

 export default connect(null, mapDispatchToProps)(MarketContainer)

ต้องใช้อาร์กิวเมนต์สี่ตัว เป็นทางเลือกทั้งหมด และอาร์กิวเมนต์สามตัวแรกต้องผ่านฟังก์ชัน match ซึ่งช่วยกำหนดพฤติกรรมตามอาร์กิวเมนต์ที่มีอยู่และประเภทค่าของอาร์กิวเมนต์ ตอนนี้ เนื่องจากอาร์กิวเมนต์ที่สองที่มีให้เพื่อ match เป็นหนึ่งในสามฟังก์ชันที่นำเข้ามา connect ฉันต้องตัดสินใจว่าจะติดตามเธรดใด

มีช่วงเวลาแห่งการเรียนรู้ด้วยฟังก์ชันพร็อกซีที่ใช้ในการห่ออาร์กิวเมนต์แรกเพื่อ connect หากอาร์กิวเมนต์เหล่านี้เป็นฟังก์ชัน ยูทิลิตี isPlainObject ที่ใช้ตรวจสอบวัตถุธรรมดาหรือโมดูล warning ซึ่งแสดงวิธีตั้งค่าดีบักเกอร์ให้ทำลายข้อยกเว้นทั้งหมด หลังจากฟังก์ชันจับคู่ เรามาที่ connectHOC ซึ่งเป็นฟังก์ชันที่ใช้ส่วนประกอบ React ของเราและเชื่อมต่อกับ Redux เป็นการเรียกใช้ฟังก์ชันอื่นที่ส่งคืน wrapWithConnect ซึ่งเป็นฟังก์ชันที่จัดการการเชื่อมต่อส่วนประกอบกับร้านค้า

เมื่อดูการใช้งานของ connectHOC ฉันสามารถชื่นชมได้ว่าทำไมต้อง connect เพื่อซ่อนรายละเอียดการใช้งาน เป็นหัวใจสำคัญของ React-Redux และมีตรรกะที่ไม่จำเป็นต้องเปิดเผยผ่านการ connect แม้ว่าฉันจะจบการดำน้ำลึกที่นี่ หากฉันทำต่อ นี่ก็เป็นเวลาที่เหมาะสมที่จะปรึกษากับเอกสารอ้างอิงที่ฉันพบก่อนหน้านี้ เนื่องจากมีคำอธิบายโดยละเอียดอย่างเหลือเชื่อของฐานรหัส

สรุป

การอ่านซอร์สโค้ดนั้นยากในตอนแรก แต่ทุกอย่างจะง่ายขึ้นเมื่อเวลาผ่านไป เป้าหมายไม่ใช่เพื่อเข้าใจทุกอย่าง แต่เพื่อออกไปด้วยมุมมองที่แตกต่างและความรู้ใหม่ กุญแจสำคัญคือการไตร่ตรองเกี่ยวกับกระบวนการทั้งหมดและอยากรู้อยากเห็นทุกอย่างอย่างเข้มข้น

ตัวอย่างเช่น ฉันพบว่าฟังก์ชัน isPlainObject น่าสนใจเพราะใช้ฟังก์ชันนี้ if (typeof obj !== 'object' || obj === null) return false เพื่อให้แน่ใจว่าอาร์กิวเมนต์ที่กำหนดเป็นอ็อบเจกต์ธรรมดา เมื่อฉันอ่านการใช้งานครั้งแรก ฉันสงสัยว่าเหตุใดจึงไม่ใช้ Object.prototype.toString.call(opts) !== '[object Object]' ซึ่งใช้โค้ดน้อยกว่าและแยกความแตกต่างระหว่างอ็อบเจ็กต์และประเภทย่อยของอ็อบเจ็กต์ เช่น Date วัตถุ. อย่างไรก็ตาม การอ่านบรรทัดถัดไปเผยให้เห็นว่าในเหตุการณ์ที่ไม่น่าเป็นไปได้อย่างยิ่งที่นักพัฒนาที่ใช้การ connect ส่งคืนอ็อบเจ็กต์ Date ตัวอย่างเช่น Object.getPrototypeOf(obj) === null check จะจัดการสิ่งนี้

ความน่าสนใจอีกเล็กน้อยใน isPlainObject คือรหัสนี้:

 while (Object.getPrototypeOf(baseProto) !== null) { baseProto = Object.getPrototypeOf(baseProto) }

การค้นหาโดย Google บางอย่างทำให้ฉันมาที่เธรด StackOverflow และปัญหา Redux ที่อธิบายว่าโค้ดนั้นจัดการกับกรณีต่างๆ อย่างไร เช่น การตรวจสอบกับวัตถุที่มาจาก iFrame

ลิงค์ที่เป็นประโยชน์ในการอ่านซอร์สโค้ด

  • “วิธีการย้อนกลับกรอบงานวิศวกร” Max Koretskyi, Medium
  • “วิธีการอ่านโค้ด” Aria Stewart, GitHub