API luồng

Sử dụng các luồng, chúng tôi có thể nhận tài nguyên từ mạng hoặc từ các nguồn khác và xử lý nó ngay khi bit đầu tiên đến

Sử dụng các luồng, chúng ta có thể nhận một tài nguyên từ mạng hoặc từ các nguồn khác và xử lý nó ngay khi đến bit đầu tiên.

Thay vì đợi tài nguyên tải xuống hoàn toàn trước khi sử dụng, chúng ta có thể làm việc với nó ngay lập tức.

Luồng là gì

Ví dụ đầu tiên mà bạn nghĩ đến là tải video YouTube - bạn không cần phải tải hoàn toàn video trước khi có thể bắt đầu xem.

Hoặc phát trực tiếp, nơi bạn thậm chí không biết khi nào nội dung sẽ kết thúc.

Nội dung thậm chí không cần phải kết thúc. Nó có thể được tạo ra vô thời hạn.

API luồng

API Luồng cho phép chúng tôi làm việc với loại nội dung này.

Chúng tôi có 2 chế độ phát trực tuyến khác nhau: đọc từ một luồng và ghi vào một luồng.

Các luồng có thể đọc có sẵn trong tất cả các trình duyệt hiện đại ngoại trừ Internet Explorer.

Luồng ghi không khả dụng trên Firefox và Internet Explorer.

Như mọi khi, hãy kiểm tracaniuse.comđể có thông tin cập nhật nhất về vấn đề này.

Hãy bắt đầu với các luồng có thể đọc được

Luồng có thể đọc được

Chúng ta có 3 lớp đối tượng khi nói đến luồng có thể đọc được:

  • ReadableStream
  • ReadableStreamDefaultReader
  • ReadableStreamDefaultController

Chúng ta có thể sử dụng luồng bằng đối tượng ReadableStream.

Đây là ví dụ đầu tiên về một luồng có thể đọc được. API tìm nạp cho phép lấy tài nguyên từ mạng và cung cấp tài nguyên đó dưới dạng một luồng:

const stream = fetch('/resource')
  .then(response => response.body)

Cácbodythuộc tính của phản hồi tìm nạp là mộtReadableStreamcá thể đối tượng. Đây là luồng có thể đọc được của chúng tôi.

Độc giả

Kêu gọigetReader()trên mộtReadableStreamđối tượng trả về mộtReadableStreamDefaultReaderđối tượng, người đọc. Chúng ta có thể lấy nó theo cách này:

const reader = fetch('/resource').then(response => response.body.getReader())

Chúng tôi đọc dữ liệu theo từng phần, trong đó một phần là một byte hoặc một mảng đã nhập. Các đoạn tin nhắn được xếp vào hàng trong luồng và chúng tôi đọc chúng từng đoạn một.

Một luồng duy nhất có thể chứa nhiều loại khối khác nhau.

Khi chúng tôi có mộtReadableStreamDefaultReaderđối tượng mà chúng tôi có thể truy cập dữ liệu bằng cách sử dụngread()phương pháp.

Ngay sau khi một trình đọc được tạo, luồng sẽ bị khóa và không người đọc nào khác có thể nhận được các phần từ đó, cho đến khi chúng tôi gọireleaseLock()trên đó.

Bạn có thể phát trực tiếp để đạt được hiệu ứng này, hơn thế nữa về điều này sau này

Đọc dữ liệu từ một luồng có thể đọc được

Khi chúng tôi có mộtReadableStreamDefaultReadercá thể đối tượng chúng ta có thể đọc dữ liệu từ nó.

Đây là cách bạn có thể đọc đoạn đầu tiên của luồng nội dung HTML từ trang web flaviocopes.com, từng byte (vì lý do CORS, bạn có thể thực thi điều này trong cửa sổ DevTools được mở trên trang web đó).

fetch('https://flaviocopes.com/')
  .then(response => {
    response.body
      .getReader()
      .read()
      .then(({value, done}) => {
        console.log(value)
      })
  })

Uint8Array

Nếu bạn mở từng nhóm mục mảng, bạn sẽ nhận được các mục đơn lẻ. Đó là những byte, được lưu trữ trong mộtUint8Array:

bytes stored in Uint8Array

Bạn có thể chuyển đổi các byte đó thành các ký tự bằng cách sử dụngAPI mã hóa:

const decoder = new TextDecoder('utf-8')
fetch('https://flaviocopes.com/')
  .then(response => {
    response.body
      .getReader()
      .read()
      .then(({value, done}) => {
        console.log(decoder.decode(value))
      })
  })

sẽ in ra các ký tự được tải trong trang:

Printed characters

Phiên bản mã mới này tải từng đoạn của luồng và in ra:

(async () => {
  const fetchedResource = await fetch('https://flaviocopes.com/')
  const reader = await fetchedResource.body.getReader()

let charsReceived = 0 let result = ‘’

reader.read().then(function processText({ done, value }) { if (done) { console.log(‘Stream finished. Content received:’) console.log(result) return }

<span style="color:#a6e22e">console</span>.<span style="color:#a6e22e">log</span>(<span style="color:#e6db74">`Received </span><span style="color:#e6db74">${</span><span style="color:#a6e22e">result</span>.<span style="color:#a6e22e">length</span><span style="color:#e6db74">}</span><span style="color:#e6db74"> chars so far!`</span>)

<span style="color:#a6e22e">result</span> <span style="color:#f92672">+=</span> <span style="color:#a6e22e">value</span>

<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">reader</span>.<span style="color:#a6e22e">read</span>().<span style="color:#a6e22e">then</span>(<span style="color:#a6e22e">processText</span>)

}) })()

Tôi gói cái này trong mộtasynchàm được gọi ngay lập tức để sử dụngawait.

Hàm processText () chúng ta tạo nhận một đối tượng có 2 thuộc tính.

  • donetrue nếu luồng kết thúc và chúng tôi nhận được tất cả dữ liệu
  • valuegiá trị của đoạn hiện tại nhận được

Chúng tôi tạo hàm đệ quy này để xử lý toàn bộ luồng.

Tạo luồng

Cảnh báo: không được hỗ trợ trong Edge và Internet Explorer

Chúng ta vừa xem cách sử dụng một luồng có thể đọc được tạo bởi API Tìm nạp, đây là một cách tuyệt vời để bắt đầu làm việc với các luồng, vì trường hợp sử dụng là thực tế.

Bây giờ chúng ta hãy xem cách tạo một luồng có thể đọc được, để chúng ta có thể cấp quyền truy cập vào một tài nguyên bằng mã của chúng ta.

Chúng tôi đã sử dụng mộtReadableStreamđối tượng trước đó. Bây giờ chúng ta hãy tạo một thương hiệu mới bằng cách sử dụngnewtừ khóa:

const stream = new ReadableStream()

Luồng này bây giờ không hữu ích cho lắm. Đó là một luồng trống và nếu bất kỳ ai muốn đọc từ đó, sẽ không có dữ liệu.

Chúng ta có thể xác định cách thức hoạt động của luồng bằng cách truyền một đối tượng trong quá trình khởi tạo. Đối tượng này có thể xác định các thuộc tính đó:

  • startmột hàm được gọi khi luồng có thể đọc được tạo. Tại đây, bạn kết nối với nguồn dữ liệu và thực hiện các tác vụ quản trị.
  • pullmột hàm được gọi nhiều lần để lấy dữ liệu, trong khi không đạt đến vạch nước cao của hàng đợi nội bộ
  • cancelmột hàm được gọi khi luồng bị hủy, ví dụ: khicancel()phương thức được gọi ở đầu nhận

Đây là một ví dụ về cấu trúc đối tượng:

const stream = new ReadableStream({
  start(controller) {

}, pull(controller) {

}, cancel(reason) {

} })

start()pull()lấy một đối tượng điều khiển, là một thể hiện củaReadableStreamDefaultControllerđối tượng, cho phép bạn kiểm soát trạng thái luồng và hàng đợi nội bộ.

Để thêm dữ liệu vào luồng, chúng tôi gọicontroller.enqueue()chuyển biến lưu giữ dữ liệu của chúng tôi:

const stream = new ReadableStream({
  start(controller) {
    controller.enqueue('Hello')
  }
})

Khi chúng tôi sẵn sàng đóng luồng, chúng tôi gọicontroller.close().

cancel()được mộtreasonđó là một chuỗi được cung cấp choReadableStream.cancel()gọi phương thức khi luồng bị hủy.

Chúng ta cũng có thể chuyển một đối tượng thứ hai tùy chọn xác định chiến lược xếp hàng. Nó chứa 2 thuộc tính:

  • highWaterMarktổng số phần có thể được lưu trữ trong hàng đợi nội bộ. Chúng tôi đã đề cập đến điều này khi nói vềpull()trước
  • size, một phương pháp mà bạn có thể sử dụng để thay đổi kích thước phân đoạn, được biểu thị bằng byte

    {
    highWaterMark,
    size()
    }
    

chúng hữu ích chủ yếu để kiểm soát áp lực lên luồng, đặc biệt là trong bối cảnhdây chuyền ống, một cái gì đó vẫn đang thử nghiệm trong API Web.

Khi màhighWaterMarkgiá trị của một luồng đã đạt được,áp suất ngượctín hiệu được gửi đến các luồng trước đó trong đường ống để báo cho chúng biết làm chậm áp suất dữ liệu.

Chúng tôi có 2 đối tượng tích hợp xác định chiến lược xếp hàng:

  • ByteLengthQueuingStrategychờ cho đến khi kích thước tích lũy tính bằng byte của các khối vượt qua mốc nước cao được chỉ định
  • CountQueuingStrategysẽ đợi cho đến khi số lượng khối tích lũy vượt qua mốc nước cao được chỉ định

Ví dụ đặt dấu nước cao 32 byte:

new ByteLengthQueuingStrategy({ highWaterMark: 32 * 1024 }

Ví dụ đặt mốc nước cao 1 đoạn:

new CountQueuingStrategy({ highWaterMark: 1 })

Tôi đề cập đến điều này để nói với bạn rằng bạncó thểkiểm soát lượng dữ liệu chảy vào một luồng và giao tiếp với các tác nhân khác, nhưng chúng tôi sẽ không đi sâu vào chi tiết hơn vì mọi thứ trở nên phức tạp khá nhanh.

Phát trực tiếp

Trước đây, tôi đã đề cập rằng ngay sau khi chúng tôi bắt đầu đọc một luồng, luồng đó sẽ bị khóa và những người đọc khác không thể truy cập cho đến khi chúng tôi gọireleaseLock()trên đó.

Tuy nhiên, chúng tôi có thể sao chép luồng bằng cách sử dụngtee()phương thức trên chính luồng:

const stream = //...
const tees = stream.tee()

teeshiện là một mảng có chứa 2 luồng mới, bạn có thể sử dụng để đọc từ cách sử dụngtees[0]tees[1].

Luồng có thể ghi được

Chúng ta có 3 lớp đối tượng khi nói đến luồng có thể ghi:

  • WritableStream
  • WritableStreamDefaultReader
  • WritableStreamDefaultController

Chúng ta có thể tạo các luồng mà sau này chúng ta có thể sử dụng, sử dụng đối tượng WordsStream.

Đây là cách chúng tôi tạo một luồng có thể ghi mới:

const stream = new WritableStream()

Chúng ta phải truyền một đối tượng để trở nên hữu ích. Đối tượng này sẽ có các triển khai các phương thức tùy chọn sau:

  • start()được gọi khi đối tượng được khởi tạo
  • write()được gọi khi một đoạn đã sẵn sàng được ghi vào phần chìm (cấu trúc cơ bản giữ dữ liệu luồng trước khi được ghi)
  • close()được gọi khi chúng tôi viết xong các đoạn
  • abort()được gọi khi chúng tôi muốn báo hiệu một lỗi

Đây là một bộ xương:

const stream = new WritableStream({
  start(controller) {

}, write(chunk, controller) {

}, close(controller) {

}, abort(reason) {

} })

start(),close()write()vượt qua bộ điều khiển, mộtWritableStreamDefaultControllercá thể đối tượng.

Đối vớiReadableStream(), chúng ta có thể chuyển đối tượng thứ hai tớinew WritableStream()thiết lập chiến lược xếp hàng.

Ví dụ: hãy tạo một luồng đã cho một chuỗi được lưu trữ trong bộ nhớ, tạo một luồng mà người tiêu dùng có thể kết nối.

Chúng tôi bắt đầu bằng cách xác định một bộ giải mã mà chúng tôi sẽ sử dụng để chuyển đổi các byte chúng tôi nhận được thành các ký tự bằng cách sử dụngAPI mã hóa TextDecoder()constructor:

const decoder = new TextDecoder("utf-8")

Chúng ta có thể khởi tạo WordsStream triển khaiclose()phương thức này sẽ in ra bảng điều khiển khi nhận được đầy đủ thông báo và mã máy khách gọi nó:

const writableStream = new WritableStream({
  write(chunk) {
    //...
  },
  close() {
    console.log(`The message is ${result}`)
  }
})

Chúng tôi bắt đầuwrite()triển khai bằng cách khởi tạo bộ đệm ArrayBuffer và bằng cách thêm đoạn này vào. Sau đó, chúng tôi tiến hành giải mã đoạn này, là một byte, thành một ký tự bằng cách sử dụng phương thức decoder.decode () của API mã hóa. Sau đó, chúng tôi thêm giá trị này vàoresultchuỗi mà chúng tôi khai báo bên ngoài đối tượng này:

let result

const writableStream = new WritableStream({ write(chunk) { const buffer = new ArrayBuffer(2) const view = new Uint16Array(buffer) view[0] = chunk const decoded = decoder.decode(view, { stream: true }) result += decoded }, close() { //… } })

Đối tượng WordsStream hiện đã được khởi tạo.

Bây giờ chúng ta đi và triển khai mã khách hàng sẽ sử dụng luồng này.

Đầu tiên chúng tôi nhận đượcWritableStreamDefaultWriterđối tượng từwritableStreamvật:

const writer = writableStream.getWriter()

Tiếp theo, chúng tôi xác định một tin nhắn sẽ được gửi đi:

const message = 'Hello!'

Sau đó, chúng tôi khởi tạo bộ mã hóa đểmã hóacác ký tự mà chúng tôi muốn gửi đến luồng:

const encoder = new TextEncoder()
const encoded = encoder.encode(message, { stream: true })

Tại thời điểm này, chuỗi đã được mã hóa trong một mảng byte. Bây giờ, chúng tôi sử dụngforEachlặp trên mảng này để gửi từng byte đến luồng. Trước mỗi cuộc gọi đếnwrite()phương pháp của trình viết luồng, chúng tôi kiểm trareadythuộc tính trả về một lời hứa, vì vậy chúng tôi chỉ viết khi người viết luồng đã sẵn sàng:

encoded.forEach(chunk => {
  writer.ready.then(() => {
    return writer.write(chunk)
  })
})

Điều duy nhất chúng tôi nhớ bây giờ là đóng cửa nhà văn.forEachlà một vòng lặp đồng bộ, có nghĩa là chúng ta chỉ đạt đến điểm này sau khi mỗi mục đã được viết xong.

Chúng tôi vẫn kiểm trareadythuộc tính, sau đó chúng tôi gọi phương thức close ():

writer.ready.then(() => {
  writer.close()
})

Tải xuống miễn phí của tôiSổ tay dành cho Người mới bắt đầu JavaScript


Các hướng dẫn khác về trình duyệt: