Ghi nhớ là gì? Cách thức và thời điểm ghi nhớ trong JavaScript và React


Chào mọi người! Trong bài viết này, chúng ta sẽ nói về ghi nhớ, một kỹ thuật tối ưu hóa có thể giúp làm cho các quy trình tính toán nặng hiệu quả hơn.

Chúng ta sẽ bắt đầu bằng cách nói về ghi nhớ là gì và khi nào thì tốt nhất để thực hiện nó. Sau này chúng tôi sẽ đưa ra các ví dụ thực tế cho JavaScript và React.

Mục lục


Trong lập trình, ghi nhớ là một kỹ thuật tối ưu hóa làm cho các ứng dụng hiệu quả hơn và do đó nhanh hơn. Nó thực hiện điều này bằng cách lưu trữ kết quả tính toán trong bộ đệm và truy xuất cùng thông tin đó từ bộ đệm vào lần tiếp theo khi cần thay vì tính toán lại.

Nói một cách đơn giản hơn, nó bao gồm việc lưu trữ trong bộ đệm đầu ra của một hàm và làm cho hàm kiểm tra xem mỗi phép tính được yêu cầu có trong bộ đệm trước khi tính toán nó hay không.

Một bộ đệm chỉ đơn giản là một kho lưu trữ dữ liệu tạm thời chứa dữ liệu để các yêu cầu về dữ liệu đó trong tương lai có thể được phục vụ nhanh hơn.

Ghi nhớ là một thủ thuật đơn giản nhưng mạnh mẽ có thể giúp tăng tốc mã của chúng ta, đặc biệt là khi xử lý các hàm tính toán nặng và lặp đi lặp lại.

Khái niệm ghi nhớ trong JavaScript dựa trên hai khái niệm:

  • đóng cửa: Sự kết hợp của một hàm và môi trường từ vựng trong đó hàm đó được khai báo. Bạn có thể đọc thêm về chúng ở đây và ở đây.
  • Hàm bậc cao hơn: Các hàm hoạt động trên các hàm khác, bằng cách lấy chúng làm đối số hoặc bằng cách trả về chúng. Bạn có thể đọc thêm về chúng tại đây.

Để làm rõ vấn đề khó hiểu này, chúng ta sẽ sử dụng ví dụ kinh điển về dãy Fibonacci.

Các dãy Fibonacci là một tập hợp các số bắt đầu bằng một hoặc một số không, tiếp theo là một và tiến hành dựa trên quy tắc mỗi số (được gọi là số Fibonacci) bằng tổng của hai số trước đó.

Nó trông như thế này:

0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, …

Giả sử chúng ta cần viết một hàm trả về phần tử thứ n trong dãy Fibonacci. Biết rằng mỗi phần tử là tổng của hai phần tử trước đó, một giải pháp đệ quy có thể như sau:

const fib = n => {
  if (n <= 1) return 1
  return fib(n - 1) + fib(n - 2)
}

Nếu bạn không quen thuộc với đệ quy, thì đó đơn giản là khái niệm về một hàm gọi chính nó, với một số trường hợp cơ bản để tránh vòng lặp vô hạn (trong trường hợp của chúng tôi if (n <= 1)).

Nếu chúng ta gọi chức năng của mình như fib(5)đằng sau hậu trường, chức năng của chúng tôi sẽ thực thi như thế này:

Untitled-Diagram.drawio

Xem rằng chúng tôi đang thực hiện fib(0), fib(1), fib(2) and fib(3) nhiều lần. Chà, đó chính xác là kiểu ghi nhớ vấn đề giúp giải quyết.

Với tính năng ghi nhớ, không cần phải tính toán lại các giá trị giống nhau nhiều lần – chúng tôi chỉ lưu trữ từng phép tính và trả về cùng một giá trị khi được yêu cầu lại.

Thực hiện ghi nhớ, chức năng của chúng tôi sẽ trông như thế này:

const fib = (n, memo) => {
    memo = memo || {}

    if (memo[n]) return memo[n]

    if (n <= 1) return 1
    return memo[n] = fib(n-1, memo) + fib(n-2, memo)
}

Điều chúng tôi đang làm trước tiên là kiểm tra xem chúng tôi đã nhận được bản ghi nhớ đối tượng làm tham số. Nếu không, chúng tôi đặt nó thành một đối tượng trống:

memo = memo || {}

Sau đó, chúng tôi kiểm tra xem bản ghi nhớ có chứa giá trị mà chúng tôi nhận được dưới dạng thông số trong các khóa của nó hay không. Nếu có, chúng tôi trả lại. Đây là nơi phép màu xảy ra. Không cần thêm đệ quy khi chúng ta đã lưu trữ giá trị của mình trong bản ghi nhớ. =)

if (memo[n]) return memo[n]

Nếu chúng tôi chưa có giá trị trong bản ghi nhớ, chúng tôi gọi một lần nữa, nhưng bây giờ đi qua bản ghi nhớ làm tham số, vì vậy các hàm chúng ta đang gọi sẽ chia sẻ cùng các giá trị được ghi nhớ mà chúng ta có trong hàm “gốc”. Lưu ý rằng chúng tôi thêm kết quả cuối cùng vào bộ đệm trước khi trả lại.

return memo[n] = fib(n-1, memo) + fib(n-2, memo)

Và thế là xong! Với hai dòng mã, chúng tôi đã triển khai tính năng ghi nhớ và cải thiện đáng kể hiệu suất của chức năng của chúng tôi!

Đọc thêm  Cách sử dụng Bộ sưu tập JavaScript – Bản đồ và Tập hợp

Trong React, chúng ta có thể tối ưu hóa ứng dụng của mình bằng cách tránh kết xuất lại thành phần không cần thiết bằng cách ghi nhớ.

Như tôi đã đề cập trong bài viết khác này về việc quản lý trạng thái trong React, các thành phần kết xuất lại vì hai điều: thay đổi trạng thái hoặc một thay đổi đạo cụ. Đây chính xác là thông tin mà chúng tôi có thể “lưu trữ” để tránh kết xuất lại không cần thiết.

Nhưng trước khi bắt đầu viết mã, hãy giới thiệu một số khái niệm quan trọng.

linh kiện tinh khiết

React hỗ trợ các thành phần lớp hoặc chức năng. Thành phần chức năng là một hàm JavaScript đơn giản trả về JSX và thành phần lớp là một lớp JavaScript mở rộng React.Component và trả về JSX bên trong phương thức kết xuất.

Và một thành phần tinh khiết sau đó là gì? Chà, dựa trên khái niệm về độ tinh khiết trong các mô hình lập trình chức năng, một hàm được cho là thuần túy nếu:

  • Giá trị trả về của nó chỉ được xác định bởi các giá trị đầu vào của nó
  • Giá trị trả về của nó luôn giống nhau cho cùng một giá trị đầu vào

Theo cách tương tự, một thành phần React được coi là thuần túy nếu nó hiển thị cùng một đầu ra cho cùng một trạng thái và đạo cụ.

Một thành phần chức năng thuần túy có thể trông như thế này:

// Pure component
export default function PureComponent({name, lastName}) {
  return (
    <div>My name is {name} {lastName}</div>
  )
}

Hãy xem rằng chúng ta chuyển hai props và thành phần hiển thị hai props đó. Nếu các đạo cụ giống nhau thì kết xuất sẽ luôn giống nhau.

Mặt khác, chẳng hạn, giả sử chúng tôi thêm một số ngẫu nhiên vào mỗi chỗ dựa trước khi kết xuất. Sau đó, đầu ra có thể khác ngay cả khi các đạo cụ vẫn giữ nguyên, vì vậy đây sẽ là một thành phần không trong sạch.

// Impure component
export default function ImpurePureComponent({name, lastName}) {
  return (
    <div>My "impure" name is {name + Math.random()} {lastName + Math.random()}</div>
  )
}

Các ví dụ tương tự với các thành phần lớp sẽ là:

// Pure component
class PureComponent extends React.Component {
    render() {
      return (
        <div>My "name is {this.props.name} {this.props.lastName}</div>
      )
    }
  }

export default PureComponent
// Impure component
class ImpurePureComponent extends React.Component {
    render() {
      return (
        <div>My "impure" name is {this.props.name + Math.random()} {this.props.lastName + Math.random()}</div>
      )
    }
  }

export default ImpurePureComponent

Lớp PureComponent

lớp thành phần thuần túyđể thực hiện ghi nhớ React cung cấp PureComponent lớp cơ sở.

Các thành phần lớp mở rộng React.PureComponent class có một số cải tiến về hiệu suất và tối ưu hóa kết xuất. Điều này là do React thực hiện shouldComponentUpdate() phương pháp cho họ với một so sánh nông cạn giữa props và state.

Đọc thêm  Ưu đãi và Hạn chế JavaScript

Hãy xem nó trong một ví dụ. Ở đây chúng ta có một thành phần lớp là một bộ đếm, với các nút để thay đổi bộ đếm cộng hoặc trừ các số. Chúng tôi cũng có một thành phần con mà chúng tôi đang chuyển một tên chống đỡ là một chuỗi.

import React from "react"
import Child from "./child"

class Counter extends React.Component {
    constructor(props) {
      super(props)
      this.state = { count: 0 }
    }

    handleIncrement = () => { this.setState(prevState => {
        return { count: prevState.count - 1 };
      })
    }

    handleDecrement = () => { this.setState(prevState => {
        return { count: prevState.count + 1 };
      })
    }

    render() {
      console.log("Parent render")

      return (
        <div className="App">

          <button onClick={this.handleIncrement}>Increment</button>
          <button onClick={this.handleDecrement}>Decrement</button>

          <h2>{this.state.count}</h2>

          <Child name={"Skinny Jack"} />
        </div>
      )
    }
  }

  export default Counter

Thành phần con là một thành phần tinh khiết điều đó chỉ hiển thị chỗ dựa nhận được.

import React from "react"

class Child extends React.Component {
    render() {
      console.log("Skinny Jack")
      return (
          <h2>{this.props.name}</h2>
      )
    }
  }

export default Child

Lưu ý rằng chúng tôi đã thêm console.logs vào cả hai thành phần để chúng tôi nhận được thông báo bảng điều khiển mỗi khi chúng hiển thị. Và nói về điều đó, hãy đoán xem điều gì sẽ xảy ra khi chúng ta nhấn nút tăng hoặc giảm? Bảng điều khiển của chúng tôi sẽ trông như thế này:

2022-04-24_21-59

Thành phần con đang kết xuất lại ngay cả khi nó luôn nhận được cùng một chỗ dựa.

Để thực hiện ghi nhớ và tối ưu hóa tình huống này, chúng ta cần mở rộng React.PureComponent lớp trong thành phần con của chúng tôi, như thế này:

import React from "react"

class Child extends React.PureComponent {
    render() {
      console.log("Skinny Jack")
      return (
          <h2>{this.props.name}</h2>
      )
    }
  }

export default Child

Sau đó, nếu chúng ta nhấn nút tăng hoặc giảm, bảng điều khiển của chúng ta sẽ trông như thế này:

2022-04-24_22-00

Chỉ kết xuất ban đầu của thành phần con và không kết xuất lại không cần thiết khi chỗ dựa không thay đổi. Miếng bánh. 😉

Với điều này, chúng tôi đã đề cập đến các thành phần lớp, nhưng trong các thành phần chức năng, chúng tôi không thể mở rộng React.PureComponent lớp. Thay vào đó, React cung cấp một HOC và hai hook để xử lý việc ghi nhớ.

Ghi nhớ Thành phần bậc cao hơn

Nếu chúng ta chuyển đổi ví dụ trước thành các thành phần chức năng, chúng ta sẽ nhận được như sau:

import { useState } from 'react'
import Child from "./child"

export default function Counter() {

    const [count, setCount] = useState(0)

    const handleIncrement = () => setCount(count+1)
    const handleDecrement = () => setCount(count-1)

    return (
        <div className="App">
            {console.log('parent')}
            <button onClick={() => handleIncrement()}>Increment</button>
            <button onClick={() => handleDecrement()}>Decrement</button>

            <h2>{count}</h2>

            <Child name={"Skinny Jack"} />
        </div>                    
    )
}
import React from 'react'

export default function Child({name}) {
console.log("Skinny Jack")
  return (
    <div>{name}</div>
  )
}

Điều này sẽ gây ra vấn đề tương tự như trước đây, đó là thành phần Con được hiển thị lại một cách không cần thiết. Để giải quyết nó, chúng ta có thể bọc thành phần con của mình trong memo thành phần bậc cao hơn, như sau:

import React from 'react'

export default React.memo(function Child({name}) {
console.log("Skinny Jack")
  return (
    <div>{name}</div>
  )
})

Một thành phần bậc cao hơn hoặc HOC tương tự như hàm bậc cao hơn trong javascript. Các hàm bậc cao hơn là các hàm lấy các hàm khác làm đối số HOẶC trả về các hàm khác. Các HOC phản ứng lấy một thành phần làm chỗ dựa và điều khiển nó đến một điểm nào đó mà không thực sự thay đổi chính thành phần đó. Bạn có thể nghĩ về điều này giống như các thành phần bao bọc.

Trong trường hợp này, memo làm một công việc tương tự như PureComponenttránh kết xuất lại không cần thiết các thành phần mà nó bao bọc.

Khi nào nên sử dụng useCallback Hook

Một điều quan trọng cần đề cập là bản ghi nhớ không hoạt động nếu chỗ dựa được truyền cho thành phần là một chức năng. Hãy cấu trúc lại ví dụ của chúng ta để thấy điều này:

import { useState } from 'react'
import Child from "./child"

export default function Counter() {

    const [count, setCount] = useState(0)

    const handleIncrement = () => setCount(count+1)
    const handleDecrement = () => setCount(count-1)

    return (
        <div className="App">
            {console.log('parent')}
            <button onClick={() => handleIncrement()}>Increment</button>
            <button onClick={() => handleDecrement()}>Decrement</button>

            <h2>{count}</h2>

            <Child name={console.log('Really Skinny Jack')} />
        </div>                    
    )
}
import React from 'react'

export default React.memo(function Child({name}) {
console.log("Skinny Jack")
  return (
    <>
        {name()}
        <div>Really Skinny Jack</div>
    </>
  )
})

Bây giờ chỗ dựa của chúng ta là một hàm luôn ghi lại cùng một chuỗi và bảng điều khiển của chúng ta sẽ trông như thế này:

Đọc thêm  Các phương thức cửa sổ JavaScript được giải thích bằng các ví dụ
2022-04-24_22-04

Điều này là do trong thực tế, một chức năng mới đang được tạo trên mỗi lần kết xuất lại thành phần chính. Vì vậy, nếu một chức năng mới đang được tạo, điều đó có nghĩa là chúng ta có một chỗ dựa mới và điều đó có nghĩa là thành phần con của chúng ta cũng sẽ kết xuất lại.

Để giải quyết vấn đề này, phản ứng cung cấp sử dụngCallback cái móc. Chúng ta có thể thực hiện nó theo cách sau:

import { useState, useCallback } from 'react'
import Child from "./child"

export default function Counter() {

    const [count, setCount] = useState(0)

    const handleIncrement = () => setCount(count+1)
    const handleDecrement = () => setCount(count-1)

    return (
        <div className="App">
            {console.log('parent')}
            <button onClick={() => handleIncrement()}>Increment</button>
            <button onClick={() => handleDecrement()}>Decrement</button>

            <h2>{count}</h2>

             <Child name={ useCallback(() => {console.log('Really Skinny Jack')}, [])  } />
        </div>                    
    )
}

Và điều đó giải quyết vấn đề kết xuất lại con không cần thiết.

Những gì useCallback làm là giữ giá trị của hàm mặc dù thành phần cha mẹ kết xuất lại, do đó, chỗ dựa con sẽ giữ nguyên miễn là giá trị hàm vẫn giữ nguyên.

Để sử dụng nó, chúng ta chỉ cần quấn hook useCallback xung quanh hàm mà chúng ta đang khai báo. Trong mảng có trong hook, chúng ta có thể khai báo các biến sẽ kích hoạt sự thay đổi của giá trị hàm khi biến đó cũng thay đổi (giống hệt như cách useEffect hoạt động).

const testingTheTest = useCallback(() => { 
    console.log("Tested");
  }, [a, b, c]);

Khi nào nên sử dụng useMemo Hook

sử dụng bản ghi nhớ là một hook rất giống với useCallback, nhưng thay vì lưu vào bộ đệm một chức năng, useMemo sẽ lưu vào bộ đệm giá trị trả về của hàm.

Trong ví dụ này, useMemo sẽ lưu trữ số 2.

const num = 1
const answer = useMemo(() => num + 1, [num])

Trong khi useCallback sẽ lưu trữ () => num + 1.

const num = 1
const answer = useMemo(() => num + 1, [num])

Bạn có thể sử dụng useMemo theo cách rất giống với bản ghi nhớ HOC. Sự khác biệt là useMemo là một hook với một loạt các thành phần phụ thuộc và memo là một HOC chấp nhận làm tham số cho một hàm tùy chọn sử dụng các đạo cụ để cập nhật thành phần theo điều kiện.

Ngoài ra, useMemo lưu trữ một giá trị được trả về giữa các lần hiển thị, trong khi bản ghi nhớ lưu trữ toàn bộ thành phần phản ứng giữa các lần hiển thị.

Khi nào cần ghi nhớ

Ghi nhớ trong React là một công cụ tốt cần có trong thắt lưng của chúng ta, nhưng nó không phải là thứ bạn nên sử dụng ở mọi nơi. Những công cụ này rất hữu ích để xử lý các chức năng hoặc tác vụ yêu cầu tính toán nặng.

Chúng tôi phải lưu ý rằng trong nền tảng, cả ba giải pháp này cũng bổ sung thêm chi phí cho mã của chúng tôi. Vì vậy, nếu kết xuất lại là do các tác vụ không nặng về mặt tính toán, có thể tốt hơn là giải quyết theo cách khác hoặc để yên.

Tôi giới thiệu bài viết này của Kent C. Dodds để biết thêm thông tin về chủ đề này.

Vậy đó, mọi người! Như mọi khi, tôi hy vọng bạn thích bài viết này và học được điều gì đó mới. Nếu muốn, bạn cũng có thể theo dõi tôi trên LinkedIn hoặc Twitter.

Chúc mừng và hẹn gặp lại bạn trong phần tiếp theo! =D

tạm biệt-1





Zik.vn – Biên dịch & Biên soạn Lại

spot_img

Create a website from scratch

Just drag and drop elements in a page to get started with Newspaper Theme.

Buy Now ⟶

Bài viết liên quang

DMCA.com Protection Status