React, Redux และ Immutable.js: ส่วนผสมสำหรับเว็บแอปพลิเคชันที่มีประสิทธิภาพ
เผยแพร่แล้ว: 2022-03-11React, Redux และ Immutable.js ปัจจุบันเป็นหนึ่งในไลบรารี JavaScript ที่ได้รับความนิยมมากที่สุด และกำลังกลายเป็นตัวเลือกแรกของนักพัฒนาอย่างรวดเร็วเมื่อต้องพัฒนา front-end ในโปรเจ็กต์ React และ Redux สองสามโปรเจ็กต์ที่ฉันทำงานอยู่ ฉันตระหนักว่านักพัฒนาจำนวนมากที่เริ่มต้นใช้งาน React ไม่เข้าใจ React อย่างถ่องแท้ และวิธีเขียนโค้ดที่มีประสิทธิภาพเพื่อใช้ศักยภาพสูงสุด
ในบทช่วยสอน Immutable.js นี้ เราจะสร้างแอปอย่างง่ายโดยใช้ React และ Redux และระบุการใช้ React ในทางที่ผิดที่พบบ่อยที่สุดและวิธีหลีกเลี่ยง
ปัญหาการอ้างอิงข้อมูล
React คือทั้งหมดที่เกี่ยวกับประสิทธิภาพ สร้างขึ้นจากพื้นฐานเพื่อให้มีประสิทธิภาพสูงสุด โดยแสดงเฉพาะส่วนที่น้อยที่สุดของ DOM ซ้ำเพื่อให้สอดคล้องกับการเปลี่ยนแปลงข้อมูลใหม่ แอป React ส่วนใหญ่ควรประกอบด้วยส่วนประกอบที่เรียบง่าย (หรือฟังก์ชันไร้สัญชาติ) ขนาดเล็ก ง่ายต่อการให้เหตุผลและส่วนใหญ่สามารถมีฟังก์ชัน shouldComponentUpdate ส่งคืนค่า false
shouldComponentUpdate(nextProps, nextState) { return false; }
ในแง่ของประสิทธิภาพ ฟังก์ชันวงจรชีวิตองค์ประกอบที่สำคัญที่สุดคือ shouldComponentUpdate และหากเป็นไปได้ ควรคืนค่า false เสมอ เพื่อให้แน่ใจว่าส่วนประกอบนี้จะไม่แสดงผลซ้ำ (ยกเว้นการเรนเดอร์เริ่มต้น) อย่างมีประสิทธิภาพทำให้แอป React รู้สึกเร็วมาก
เมื่อไม่เป็นเช่นนั้น เป้าหมายของเราคือการตรวจสอบความเท่าเทียมกันในราคาถูกของพร็อพ/สถานะเก่ากับอุปกรณ์/สถานะใหม่ และข้ามการแสดงผลซ้ำหากข้อมูลไม่เปลี่ยนแปลง
ย้อนกลับไปสักครู่แล้วทบทวนว่า JavaScript ดำเนินการตรวจสอบความเท่าเทียมกันสำหรับประเภทข้อมูลต่างๆ อย่างไร
การตรวจสอบความเท่าเทียมกันสำหรับประเภทข้อมูลดั้งเดิม เช่น boolean , string และ integer นั้นง่ายมาก เนื่องจากจะถูกเปรียบเทียบด้วยค่าจริงเสมอ:
1 === 1 'string' === 'string' true === true
ในทางกลับกัน การตรวจสอบความเท่าเทียมกันสำหรับประเภทที่ซับซ้อน เช่น อ อบเจ กต์ อาร์เรย์ และ ฟังก์ชัน นั้นแตกต่างกันโดยสิ้นเชิง วัตถุสองชิ้นจะเหมือนกันหากมีการอ้างอิงเหมือนกัน (ชี้ไปที่วัตถุเดียวกันในหน่วยความจำ)
const obj1 = { prop: 'someValue' }; const obj2 = { prop: 'someValue' }; console.log(obj1 === obj2); // false
แม้ว่า obj1 และ obj2 จะดูเหมือนเหมือนกัน แต่การอ้างอิงก็ต่างกัน เนื่องจากมีความแตกต่างกัน การเปรียบเทียบอย่างไร้เดียงสาภายในฟังก์ชัน shouldComponentUpdate จะทำให้คอมโพเนนต์ของเราแสดงผลใหม่โดยไม่จำเป็น
สิ่งสำคัญที่ควรทราบคือข้อมูลที่มาจากตัวลด Redux หากตั้งค่าไม่ถูกต้อง จะถูกเสิร์ฟพร้อมข้อมูลอ้างอิงที่แตกต่างกัน ซึ่งจะทำให้ส่วนประกอบแสดงผลใหม่ทุกครั้ง
นี่เป็นปัญหาหลักในการพยายามหลีกเลี่ยงการแสดงผลคอมโพเนนต์ซ้ำ
ข้อมูลอ้างอิง
ลองมาดูตัวอย่างที่เรามีวัตถุที่ซ้อนกันอย่างลึกล้ำ และเราต้องการเปรียบเทียบกับรุ่นก่อนหน้า เราสามารถวนซ้ำผ่านอุปกรณ์ประกอบฉากอ็อบเจ็กต์ที่ซ้อนกันและเปรียบเทียบแต่ละรายการได้ แต่แน่นอนว่ามันจะมีราคาแพงมากและไม่เป็นปัญหา
นั่นทำให้เรามีทางแก้ไขเพียงทางเดียว นั่นคือการตรวจสอบข้อมูลอ้างอิง แต่ปัญหาใหม่เกิดขึ้นอย่างรวดเร็ว:
- เก็บรักษาข้อมูลอ้างอิงไว้หากไม่มีการเปลี่ยนแปลง
- การเปลี่ยนการอ้างอิงหากมีการเปลี่ยนแปลงค่า prop ของอ็อบเจ็กต์/อาร์เรย์
นี่ไม่ใช่งานง่ายถ้าเราต้องการทำในลักษณะที่ดี สะอาด และเพิ่มประสิทธิภาพ Facebook ตระหนักถึงปัญหานี้เมื่อนานมาแล้วและเรียก Immutable.js เพื่อช่วยเหลือ
import { Map } from 'immutable'; // transform object into immutable map let obj1 = Map({ prop: 'someValue' }); const obj2 = obj1; console.log(obj1 === obj2); // true obj1 = obj1.set('prop', 'someValue'); // set same old value console.log(obj1 === obj2); // true | does not break reference because nothing has changed obj1 = obj1.set('prop', 'someNewValue'); // set new value console.log(obj1 === obj2); // false | breaks reference
ไม่มีฟังก์ชัน Immutable.js ใดที่ทำการเปลี่ยนแปลงโดยตรงกับข้อมูลที่กำหนด แต่ข้อมูลจะถูกโคลนภายใน กลายพันธุ์ และหากมีการเปลี่ยนแปลงใด ๆ การอ้างอิงใหม่จะถูกส่งกลับ มิฉะนั้นจะส่งกลับการอ้างอิงเริ่มต้น ต้องตั้งค่าการอ้างอิงใหม่อย่างชัดเจน เช่น obj1 = obj1.set(...);
.
ตัวอย่าง React, Redux และ Immutable.js
วิธีที่ดีที่สุดในการแสดงพลังของไลบรารีเหล่านี้คือการสร้างแอปอย่างง่าย และอะไรจะง่ายกว่าแอป todo?
เพื่อความกระชับ ในบทความนี้ เราจะอธิบายเฉพาะส่วนต่างๆ ของแอปที่มีความสำคัญต่อแนวคิดเหล่านี้เท่านั้น ซอร์สโค้ดทั้งหมดของโค้ดแอปมีอยู่ใน GitHub
เมื่อเริ่มต้นแอป คุณจะสังเกตเห็นว่าการเรียกไปยัง console.log นั้นอยู่ในพื้นที่สำคัญอย่างสะดวกเพื่อแสดงจำนวนการเรนเดอร์ DOM อีกครั้งอย่างชัดเจน ซึ่งถือว่าน้อยที่สุด
เช่นเดียวกับแอป todo อื่นๆ เราต้องการแสดงรายการสิ่งที่ต้องทำ เมื่อผู้ใช้คลิกที่รายการสิ่งที่ต้องทำ เราจะทำเครื่องหมายว่าเสร็จสิ้น นอกจากนี้ เราจำเป็นต้องมีฟิลด์อินพุตขนาดเล็กที่ด้านบนเพื่อเพิ่ม todos ใหม่และตัวกรอง 3 ตัวด้านล่าง ซึ่งจะทำให้ผู้ใช้สามารถสลับระหว่าง:
- ทั้งหมด
- สมบูรณ์
- คล่องแคล่ว
ลด Redux
ข้อมูลทั้งหมดในแอปพลิเคชัน Redux อยู่ในออบเจ็กต์ร้านค้าเดียว และเราสามารถดูที่ตัวลดขนาดเป็นเพียงวิธีที่สะดวกในการแบ่งร้านค้าออกเป็นชิ้นเล็ก ๆ ที่ง่ายต่อการให้เหตุผล เนื่องจากรีดิวเซอร์เป็นฟังก์ชันด้วย จึงสามารถแบ่งออกเป็นส่วนเล็กๆ ได้ด้วยเช่นกัน
ตัวลดของเราจะประกอบด้วย 2 ส่วนเล็ก ๆ :
- สิ่งที่ต้องทำ
- activeFilter
// reducers/todos.js import * as types from 'constants/ActionTypes'; // we can look at List/Map as immutable representation of JS Array/Object import { List, Map } from 'immutable'; import { combineReducers } from 'redux'; function todoList(state = List(), action) { // default state is empty List() switch (action.type) { case types.ADD_TODO: return state.push(Map({ // Every switch/case must always return either immutable id: action.id, // or primitive (like in activeFilter) state data text: action.text, // We let Immutable decide if data has changed or not isCompleted: false, })); // other cases... default: return state; } } function activeFilter(state = 'all', action) { switch (action.type) { case types.CHANGE_FILTER: return action.filter; // This is primitive data so there's no need to worry default: return state; } } // combineReducers combines reducers into a single object // it lets us create any number or combination of reducers to fit our case export default combineReducers({ activeFilter, todoList, });
การเชื่อมต่อกับ Redux
ตอนนี้เราได้ตั้งค่า Redux reducer ด้วยข้อมูล Immutable.js แล้ว มาเชื่อมต่อกับ React component เพื่อส่งข้อมูลเข้าไป
// components/App.js import { connect } from 'react-redux'; // ….component code const mapStateToProps = state => ({ activeFilter: state.todos.activeFilter, todoList: state.todos.todoList, }); export default connect(mapStateToProps)(App);
ในโลกที่สมบูรณ์แบบ การเชื่อมต่อควรทำบนส่วนประกอบเส้นทางระดับบนสุดเท่านั้น ดึงข้อมูลใน mapStateToProps และส่วนที่เหลือเป็น React พื้นฐานที่ส่งผ่านอุปกรณ์ประกอบฉากไปยังเด็ก สำหรับแอปพลิเคชันขนาดใหญ่ การติดตามการเชื่อมต่อทั้งหมดทำได้ยาก ดังนั้นเราจึงต้องการลดการเชื่อมต่อเหล่านั้นให้เหลือน้อยที่สุด
เป็นสิ่งสำคัญมากที่จะต้องทราบว่า state.todos เป็นอ็อบเจ็กต์ JavaScript ปกติที่ส่งคืนจากฟังก์ชัน Redux combineReducers (todos เป็นชื่อของตัวลดขนาด) แต่ state.todos.todoList เป็นรายการที่ไม่เปลี่ยนรูปแบบ และเป็นสิ่งสำคัญที่จะต้องอยู่ในรายการดังกล่าว จนกว่าจะผ่านการตรวจสอบ shouldComponentUpdate
หลีกเลี่ยงการแสดงส่วนประกอบซ้ำ
ก่อนที่เราจะเจาะลึกลงไป สิ่งสำคัญคือต้องทำความเข้าใจว่าข้อมูลประเภทใดที่ต้องให้บริการกับองค์ประกอบ:
- ดั้งเดิมทุกชนิด
- ออบเจ็กต์/อาร์เรย์ในรูปแบบที่ไม่เปลี่ยนรูปเท่านั้น
การมีข้อมูลประเภทนี้ทำให้เราสามารถเปรียบเทียบอุปกรณ์ประกอบฉากที่มากับส่วนประกอบ React ได้อย่างละเอียด
ตัวอย่างต่อไปแสดงวิธีแยกอุปกรณ์ประกอบฉากด้วยวิธีที่ง่ายที่สุด:
$ npm install react-pure-render
import shallowEqual from 'react-pure-render/shallowEqual'; shouldComponentUpdate(nextProps, nextState) { return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState); }
ฟังก์ชั่น shallowEqual จะตรวจสอบ props/state diff ลึกเพียง 1 ระดับเท่านั้น มันทำงานเร็วมากและทำงานร่วมกันอย่างสมบูรณ์แบบกับข้อมูลที่ไม่เปลี่ยนรูปของเรา จะต้องเขียนสิ่งนี้ shouldComponentUpdate ในทุกองค์ประกอบจะไม่สะดวกมาก แต่โชคดีที่มีวิธีแก้ปัญหาง่ายๆ

แตกไฟล์ shouldComponentUpdate เป็นส่วนประกอบพิเศษแยกต่างหาก:
// components/PureComponent.js import React from 'react'; import shallowEqual from 'react-pure-render/shallowEqual'; export default class PureComponent extends React.Component { shouldComponentUpdate(nextProps, nextState) { return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState); } }
จากนั้นเพียงขยายส่วนประกอบใด ๆ ที่ต้องการตรรกะของส่วนประกอบควรได้รับการปรับปรุง:
// components/Todo.js export default class Todo extends PureComponent { // Component code }
นี่เป็นวิธีที่สะอาดและมีประสิทธิภาพมากในการหลีกเลี่ยงการแสดงส่วนประกอบซ้ำในกรณีส่วนใหญ่ และในภายหลังหากแอปมีความซับซ้อนมากขึ้นและต้องการโซลูชันที่กำหนดเองในทันใด ก็สามารถเปลี่ยนแปลงได้อย่างง่ายดาย
มีปัญหาเล็กน้อยเมื่อใช้ PureComponent ขณะส่งผ่านฟังก์ชันเป็นอุปกรณ์ประกอบฉาก ตั้งแต่ React ด้วย ES6 class จะไม่ผูก สิ่งนี้ กับฟังก์ชั่นโดยอัตโนมัติเราต้องทำด้วยตนเอง เราสามารถทำได้โดยทำอย่างใดอย่างหนึ่งต่อไปนี้:
- ใช้การเชื่อมโยงฟังก์ชันลูกศร ES6:
<Component onClick={() => this.handleClick()} />
- ใช้การ ผูก :
<Component onClick={this.handleClick.bind(this)} />
ทั้งสองวิธีจะทำให้ คอมโพเนนต์ แสดงผลใหม่เนื่องจากมีการส่งต่อการอ้างอิงที่แตกต่างกันไปยัง onClick ทุกครั้ง
เพื่อแก้ไขปัญหานี้ เราสามารถผูกฟังก์ชันล่วงหน้าในวิธี Constructor ได้ดังนี้:
constructor() { super(); this.handleClick = this.handleClick.bind(this); } // Then simply pass the function render() { return <Component onClick={this.handleClick} /> }
หากคุณพบว่าตัวเองผูกหลายฟังก์ชันไว้ล่วงหน้าเป็นส่วนใหญ่ เราสามารถส่งออกและนำฟังก์ชันตัวช่วยขนาดเล็กมาใช้ใหม่ได้:
// utils/bind-functions.js export default function bindFunctions(functions) { functions.forEach(f => this[f] = this[f].bind(this)); } // some component constructor() { super(); bindFunctions.call(this, ['handleClick']); // Second argument is array of function names }
หากไม่มีวิธีแก้ไขปัญหาใดๆ สำหรับคุณ คุณสามารถเขียนเงื่อนไข shouldComponentUpdate ด้วยตนเองได้ตลอดเวลา
การจัดการข้อมูลที่ไม่เปลี่ยนรูปภายในส่วนประกอบ
ด้วยการตั้งค่าข้อมูลที่ไม่เปลี่ยนรูปแบบในปัจจุบัน ทำให้ไม่มีการเรนเดอร์ซ้ำ และเราเหลือข้อมูลที่ไม่เปลี่ยนรูปภายในอุปกรณ์ประกอบฉากของส่วนประกอบ มีหลายวิธีในการใช้ข้อมูลที่ไม่เปลี่ยนรูปนี้ แต่ข้อผิดพลาดที่พบบ่อยที่สุดคือการแปลงข้อมูลเป็น JS ธรรมดาทันทีโดยใช้ฟังก์ชัน toJS ที่ ไม่เปลี่ยนรูป
การใช้ toJS เพื่อแปลงข้อมูลที่ไม่เปลี่ยนรูปอย่างลึกซึ้งเป็น JS ธรรมดาจะขัดต่อจุดประสงค์ทั้งหมดของการหลีกเลี่ยงการแสดงผลซ้ำ เนื่องจากเป็นไปตามที่คาดไว้ มันช้ามากและควรหลีกเลี่ยง แล้วเราจะจัดการกับข้อมูลที่ไม่เปลี่ยนรูปได้อย่างไร?
จำเป็นต้องใช้ตามที่เป็นอยู่ นั่นคือเหตุผลที่ Immutable API นำเสนอฟังก์ชันที่หลากหลาย แมป และ ใช้ งานบ่อยที่สุดในส่วนประกอบ React โครงสร้างข้อมูล todoList ที่มาจาก Redux Reducer คืออาร์เรย์ของออบเจ็กต์ในรูปแบบที่ไม่เปลี่ยนรูปแบบ โดยแต่ละอ็อบเจ็กต์จะเป็นตัวแทนของรายการสิ่งที่ต้องทำเดียว:
[{ id: 1, text: 'todo1', isCompleted: false, }, { id: 2, text: 'todo2', isCompleted: false, }]
Immutable.js API นั้นคล้ายกับ JavaScript ปกติมาก ดังนั้นเราจะใช้ todoList เช่นเดียวกับอาร์เรย์ของอ็อบเจ็กต์อื่นๆ ฟังก์ชั่นแผนที่พิสูจน์ได้ดีที่สุดในกรณีส่วนใหญ่
ภายในการเรียกกลับของแผนที่ เราได้รับ todo ซึ่งเป็นอ็อบเจ็กต์ที่ยังคงอยู่ในรูปแบบที่ไม่เปลี่ยนรูปแบบ และเราสามารถส่งผ่านมันได้อย่างปลอดภัยในองค์ประกอบ Todo
// components/TodoList.js render() { return ( // …. {todoList.map(todo => { return ( <Todo key={todo.get('id')} todo={todo}/> ); })} // …. ); }
หากคุณวางแผนที่จะดำเนินการวนซ้ำหลายครั้งบนข้อมูลที่ไม่เปลี่ยนรูป เช่น:
myMap.filter(somePred).sort(someComp)
… จากนั้น การแปลงเป็น Seq ก่อนโดยใช้ toSeq เป็นสิ่งสำคัญมาก และหลังจากการวนซ้ำแล้ว ให้เปลี่ยนกลับเป็นฟอร์มที่ต้องการ เช่น:
myMap.toSeq().filter(somePred).sort(someComp).toOrderedMap()
เนื่องจาก Immutable.js ไม่เคยเปลี่ยนแปลงข้อมูลที่ได้รับโดยตรง จึงจำเป็นต้องทำสำเนาอีกชุดหนึ่งเสมอ การทำซ้ำหลายครั้งเช่นนี้อาจมีราคาแพงมาก Seq เป็นลำดับของข้อมูลที่ไม่เปลี่ยนรูปอย่างเกียจคร้าน ซึ่งหมายความว่าจะดำเนินการน้อยที่สุดเท่าที่จะทำได้ในขณะที่ข้ามการสร้างสำเนาระดับกลาง Seq ถูกสร้างขึ้นเพื่อใช้ในลักษณะนี้
ภายในองค์ประกอบ Todo ใช้ get หรือ getIn เพื่อรับอุปกรณ์ประกอบฉาก
ง่ายพอใช่มั้ย?
สิ่งที่ฉันรู้ก็คือหลายครั้งที่มันอ่านไม่ได้เมื่อมี get()
จำนวนมาก และโดยเฉพาะอย่างยิ่ง getIn()
ดังนั้นฉันจึงตัดสินใจค้นหาจุดที่เหมาะสมระหว่างประสิทธิภาพและความสามารถในการอ่าน และหลังจากการทดลองง่ายๆ ฉันพบว่าฟังก์ชัน Immutable.js toObject และ toArray ทำงานได้ดีมาก
ฟังก์ชันเหล่านี้จะแปลงวัตถุ/อาร์เรย์ Immutable.js แบบตื้น (ลึก 1 ระดับ) เป็นวัตถุ/อาร์เรย์ JavaScript ธรรมดา หากเรามีข้อมูลฝังลึกอยู่ภายใน ข้อมูลเหล่านั้นจะยังคงอยู่ในรูปแบบที่ไม่เปลี่ยนรูปพร้อมที่จะส่งต่อไปยัง
มันช้ากว่า get()
เพียงแค่ระยะขอบเล็กน้อย แต่ดูสะอาดกว่ามาก:
// components/Todo.js render() { const { id, text, isCompleted } = this.props.todo.toObject(); // ….. }
มาดูกันเลยในการดำเนินการทั้งหมด
ในกรณีที่คุณยังไม่ได้โคลนโค้ดจาก GitHub ตอนนี้เป็นเวลาที่ดีที่จะทำ:
git clone https://github.com/rogic89/ToDo-react-redux-immutable.git cd ToDo-react-redux-immutable
การเริ่มต้นเซิร์ฟเวอร์นั้นง่ายเหมือนกัน (ตรวจสอบให้แน่ใจว่าได้ติดตั้ง Node.js และ NPM แล้ว) ดังนี้:
npm install npm start
ไปที่ http://localhost:3000 ในเว็บเบราว์เซอร์ของคุณ เมื่อเปิดคอนโซลนักพัฒนาซอฟต์แวร์ ดูบันทึกเมื่อคุณเพิ่มรายการสิ่งที่ต้องทำ ทำเครื่องหมายว่าเสร็จสิ้นแล้วและเปลี่ยนตัวกรอง:
- เพิ่ม 5 รายการสิ่งที่ต้องทำ
- เปลี่ยนตัวกรองจาก "ทั้งหมด" เป็น "ใช้งานอยู่" แล้วเปลี่ยนกลับเป็น "ทั้งหมด"
- ไม่มีสิ่งที่ต้องทำซ้ำ เพียงแค่กรองเปลี่ยน
- ทำเครื่องหมาย 2 รายการสิ่งที่ต้องทำว่าเสร็จสิ้น
- มีการแสดงโทโดสองรายการอีกครั้ง แต่ทีละรายการเท่านั้น
- เปลี่ยนตัวกรองจาก "ทั้งหมด" เป็น "ใช้งานอยู่" แล้วเปลี่ยนกลับเป็น "ทั้งหมด"
- มีเพียง 2 รายการสิ่งที่ต้องทำที่เสร็จสมบูรณ์เท่านั้นที่ถูกติดตั้ง/ยกเลิกการต่อเชื่อม
- แอคทีฟไม่ได้แสดงผลใหม่
- ลบรายการสิ่งที่ต้องทำเดียวจากตรงกลางรายการ
- เฉพาะรายการสิ่งที่ต้องทำที่นำออกเท่านั้นที่ได้รับผลกระทบ อื่นๆ จะไม่แสดงผลซ้ำ
สรุป
การผนึกกำลังของ React, Redux และ Immutable.js เมื่อใช้อย่างถูกต้อง จะนำเสนอวิธีแก้ปัญหาที่ยอดเยี่ยมสำหรับปัญหาด้านประสิทธิภาพมากมายที่มักพบในเว็บแอปพลิเคชันขนาดใหญ่
Immutable.js ช่วยให้เราตรวจจับการเปลี่ยนแปลงในออบเจ็กต์/อาร์เรย์ JavaScript โดยไม่ต้องอาศัยความไร้ประสิทธิภาพของการตรวจสอบความเท่าเทียมกันในเชิงลึก ซึ่งช่วยให้ React หลีกเลี่ยงการดำเนินการเรนเดอร์ซ้ำที่มีราคาแพงเมื่อไม่จำเป็น ซึ่งหมายความว่าประสิทธิภาพ Immutable.js มักจะดีในสถานการณ์ส่วนใหญ่
ฉันหวังว่าคุณจะชอบบทความนี้และพบว่ามีประโยชน์สำหรับการสร้าง React โซลูชันที่เป็นนวัตกรรมในโครงการในอนาคตของคุณ