HomeLập trìnhJavaScriptJavaScript — từ...

JavaScript — từ gọi lại đến không đồng bộ/đang chờ


JavaScript là đồng bộ. Điều này có nghĩa là nó sẽ thực thi khối mã của bạn theo thứ tự sau khi cẩu. Trước khi mã thực thi, varfunction khai báo được “nâng” lên đầu phạm vi của chúng.

Đây là một ví dụ về mã đồng bộ:

console.log('1')

console.log('2')

console.log('3')

Mã này sẽ ghi nhật ký “1 2 3″ một cách đáng tin cậy.

Các yêu cầu không đồng bộ sẽ đợi bộ đếm thời gian kết thúc hoặc yêu cầu phản hồi trong khi phần còn lại của mã tiếp tục thực thi. Sau đó, khi đến thời điểm thích hợp, một cuộc gọi lại sẽ đưa các yêu cầu không đồng bộ này vào hoạt động.

Đây là một ví dụ về mã không đồng bộ:

console.log('1')

setTimeout(function afterTwoSeconds() {
  console.log('2')
}, 2000)

console.log('3')

Điều này thực sự sẽ ghi “1 3 2”, vì “2” nằm trên setTimeout mà sẽ chỉ thực hiện, theo ví dụ này, sau hai giây. Ứng dụng của bạn không bị treo chờ hai giây là xong. Thay vào đó, nó tiếp tục thực thi phần còn lại của mã và khi hết thời gian chờ, nó sẽ quay lại afterTwoSeconds.

Bạn có thể hỏi “Tại sao điều này lại hữu ích?” hoặc “Làm cách nào để mã không đồng bộ của tôi trở thành đồng bộ?”. Hy vọng rằng tôi có thể cho bạn thấy câu trả lời.

“Vấn đề”

Giả sử mục tiêu của chúng tôi là tìm kiếm người dùng GitHub và lấy tất cả các kho lưu trữ của người dùng đó. Vấn đề là chúng tôi không biết tên chính xác của người dùng. Vì vậy, chúng tôi phải liệt kê tất cả người dùng có tên tương tự và kho lưu trữ tương ứng của họ.

Không cần quá cầu kỳ, đại loại như thế này

OHwdYj5jqLgcI0Sad-H3K0p0VUT14C0DmVV8
Rất nhiều phong cách, wow! Đây là “câu đố”

Trong các ví dụ này, mã yêu cầu sẽ sử dụng XHR (XMLHttpRequest). Bạn có thể thay thế nó bằng jQuery $.ajax hoặc cách tiếp cận bản địa gần đây hơn được gọi là fetch. Cả hai sẽ cung cấp cho bạn những lời hứa tiếp cận ra khỏi cổng.

Nó sẽ được thay đổi một chút tùy thuộc vào cách tiếp cận của bạn nhưng với tư cách là người mới bắt đầu:

// url argument can be something like 'https://api.github.com/users/daspinola/repos'

function request(url) {
  const xhr = new XMLHttpRequest();
  xhr.timeout = 2000;
  xhr.onreadystatechange = function(e) {
    if (xhr.readyState === 4) {
      if (xhr.status === 200) {
       // Code here for the server answer when successful
      } else {
       // Code here for the server answer when not successful
      }
    }
  }
  xhr.ontimeout = function () {
    // Well, it took to long do some code here to handle that
  }
  xhr.open('get', url, true)
  xhr.send();
}

Hãy nhớ rằng trong những ví dụ này, phần quan trọng không phải là kết quả cuối cùng của mã. Thay vào đó, mục tiêu của bạn là hiểu được sự khác biệt của các phương pháp tiếp cận và cách bạn có thể tận dụng chúng cho sự phát triển của mình.

Gọi lại

Bạn có thể lưu tham chiếu của một hàm trong một biến khi sử dụng JavaScript. Sau đó, bạn có thể sử dụng chúng làm đối số của hàm khác để thực thi sau này. Đây là “cuộc gọi lại” của chúng tôi.

Một ví dụ sẽ là:

// Execute the function "doThis" with another function as parameter, in this case "andThenThis". doThis will execute whatever code it has and when it finishes it should have "andThenThis" being executed.

doThis(andThenThis)

// Inside of "doThis" it's referenced as "callback" which is just a variable that is holding the reference to this function

function andThenThis() {
  console.log('and then this')
}

// You can name it whatever you want, "callback" is common approach

function doThis(callback) {
  console.log('this first')
  
  // the '()' is when you are telling your code to execute the function reference else it will just log the reference
  
  callback()
}

Sử dụng callback để giải quyết vấn đề của chúng tôi cho phép chúng tôi làm điều gì đó như thế này với request chức năng chúng tôi đã xác định trước đó:

function request(url, callback) {
  const xhr = new XMLHttpRequest();
  xhr.timeout = 2000;
  xhr.onreadystatechange = function(e) {
    if (xhr.readyState === 4) {
      if (xhr.status === 200) {
       callback(null, xhr.response)
      } else {
       callback(xhr.status, null)
      }
    }
  }
  xhr.ontimeout = function () {
   console.log('Timeout')
  }
  xhr.open('get', url, true)
  xhr.send();
}

Chức năng của chúng tôi cho yêu cầu bây giờ sẽ chấp nhận một callback để khi một request được thực hiện, nó sẽ được gọi trong trường hợp có lỗi và trong trường hợp thành công.

const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users`

request(userGet, function handleUsersList(error, users) {
  if (error) throw error
  const list = JSON.parse(users).items
  
  list.forEach(function(user) {
    request(user.repos_url, function handleReposList(err, repos) {
      if (err) throw err
      // Handle the repositories list here
    })
  })
})

Phá vỡ điều này xuống:

  • Chúng tôi yêu cầu lấy kho lưu trữ của người dùng
  • Sau khi yêu cầu hoàn tất, chúng tôi sử dụng gọi lại handleUsersList
  • Nếu không có lỗi thì chúng tôi phân tích phản hồi máy chủ của mình thành một đối tượng bằng cách sử dụng JSON.parse
  • Sau đó, chúng tôi lặp lại danh sách người dùng của mình vì nó có thể có nhiều hơn một
    Đối với mỗi người dùng, chúng tôi yêu cầu danh sách kho lưu trữ của họ.
    Chúng tôi sẽ sử dụng url được trả về cho mỗi người dùng trong phản hồi đầu tiên của chúng tôi
    Chúng tôi gọi repos_urllàm url cho các yêu cầu tiếp theo của chúng tôi hoặc từ phản hồi đầu tiên
  • Khi yêu cầu đã hoàn thành cuộc gọi lại, chúng tôi sẽ gọi
    Điều này sẽ xử lý lỗi của nó hoặc phản hồi với danh sách các kho lưu trữ cho người dùng đó
Đọc thêm  Các mô-đun JavaScript – Được giải thích bằng các ví dụ

Ghi chú: Gửi lỗi trước dưới dạng tham số là một phương pháp phổ biến, đặc biệt là khi sử dụng Node.js.

Một cách tiếp cận “đầy đủ” và dễ đọc hơn là xử lý lỗi. Chúng tôi sẽ giữ cuộc gọi lại tách biệt với việc thực hiện yêu cầu.

Một cái gì đó như thế này:

try {
  request(userGet, handleUsersList)
} catch (e) {
  console.error('Request boom! ', e)
}

function handleUsersList(error, users) {
  if (error) throw error
  const list = JSON.parse(users).items
  
  list.forEach(function(user) {
    request(user.repos_url, handleReposList)
  })
}

function handleReposList(err, repos) {
  if (err) throw err
  
  // Handle the repositories list here
  console.log('My very few repos', repos)
}

Điều này dẫn đến các vấn đề như các vấn đề về đua xe và xử lý lỗi. Cuộc đua xảy ra khi bạn không kiểm soát được người dùng nào bạn sẽ có trước. Chúng tôi đang yêu cầu thông tin cho tất cả chúng trong trường hợp có nhiều hơn một. Chúng tôi không tính đến một đơn đặt hàng. Ví dụ: người dùng 10 có thể đến trước và người dùng 2 đến sau cùng. Chúng tôi có một giải pháp khả thi sau này trong bài viết.

Vấn đề chính với các cuộc gọi lại là việc bảo trì và khả năng đọc có thể trở thành một vấn đề khó khăn. Nó sắp như vậy rồi và mã hầu như không làm gì cả. Điều này được gọi là địa ngục gọi lại có thể tránh được với cách tiếp cận tiếp theo của chúng tôi.

gnjFO34QsB-GSxf1kW-rES6NKbXikObOWHTG
Hình ảnh lấy từ đây. Gọi lại địa ngục tốt nhất của nó.

lời hứa

Hứa rằng bạn có thể làm cho mã của mình dễ đọc hơn. Một nhà phát triển mới có thể đến cơ sở mã và xem thứ tự thực thi rõ ràng đối với mã của bạn.

Để tạo một lời hứa, bạn có thể sử dụng:

const myPromise = new Promise(function(resolve, reject) {
  
  // code here
  
  if (codeIsFine) {
    resolve('fine')
  } else {
    reject('error')
  }
  
})

myPromise
  .then(function whenOk(response) {
    console.log(response)
    return response
  })
  .catch(function notOk(err) {
    console.error(err)
  })

Hãy để chúng tôi phân tách nó:

  • Một lời hứa được khởi tạo với một function cái đó có resolvereject các câu lệnh
  • Tạo mã không đồng bộ của bạn bên trong Promise chức năng
    resolve khi mọi thứ diễn ra như mong muốn
    Nếu không thì reject
  • Khi một resolve được tìm thấy .then phương thức sẽ thực thi cho điều đó Promise
    Khi một reject được tìm thấy .catch sẽ được kích hoạt

Những điều cần ghi nhớ:

  • resolvereject chỉ chấp nhận một tham số
    resolve(‘yey’, ‘works’) sẽ chỉ gửi ‘yey’ đến .then chức năng gọi lại
  • Nếu bạn xâu chuỗi nhiều .then
    thêm một return nếu bạn muốn tiếp theo .then giá trị không được undefined
  • Khi một reject bị bắt với .catch nếu bạn có một .then xích vào nó
    Nó vẫn sẽ thực hiện điều đó .then
    Bạn có thể nhìn thấy .then dưới dạng “luôn thực thi” và bạn có thể kiểm tra một ví dụ trong nhận xét này
  • Với một chuỗi trên .then nếu một lỗi xảy ra trên cái đầu tiên
    Nó sẽ bỏ qua tiếp theo .then cho đến khi nó tìm thấy một .catch
  • Một lời hứa có ba trạng thái
    chưa giải quyết
  • Khi chờ đợi một resolve hoặc reject xảy ra
    giải quyết
    vật bị loại bỏ
  • Một khi nó ở trong một resolved hoặc rejected tiểu bang
    Nó không thể thay đổi

Ghi chú: Bạn có thể tạo lời hứa mà không cần chức năng tại thời điểm khai báo. Cách mà tôi đang trình bày chỉ là một cách phổ biến để làm việc đó.

Đọc thêm  Gửi dữ liệu giữa máy chủ và máy khách trong Node.js và Javascript

“Lý thuyết, lý thuyết, lý thuyết…Tôi đang bối rối” bạn có thể nói.

Hãy sử dụng ví dụ về yêu cầu của chúng tôi với một lời hứa sẽ cố gắng làm sáng tỏ mọi thứ:

function request(url) {
  return new Promise(function (resolve, reject) {
    const xhr = new XMLHttpRequest();
    xhr.timeout = 2000;
    xhr.onreadystatechange = function(e) {
      if (xhr.readyState === 4) {
        if (xhr.status === 200) {
          resolve(xhr.response)
        } else {
          reject(xhr.status)
        }
      }
    }
    xhr.ontimeout = function () {
      reject('timeout')
    }
    xhr.open('get', url, true)
    xhr.send();
  })
}

Trong trường hợp này khi bạn thực hiện request nó sẽ trả về một cái gì đó như thế này:

NAgYZaSSRVgbc42aFAOEaTIYnA-2JweED-At
Một lời hứa đang chờ giải quyết hoặc bị từ chối
const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users`

const myPromise = request(userGet)

console.log('will be pending when logged', myPromise)

myPromise
  .then(function handleUsersList(users) {
    console.log('when resolve is found it comes here with the response, in this case users ', users)
    
    const list = JSON.parse(users).items
    return Promise.all(list.map(function(user) {
      return request(user.repos_url)
    }))
  })
  .then(function handleReposList(repos) {
    console.log('All users repos in an array', repos)
  })
  .catch(function handleErrors(error) {
    console.log('when a reject is executed it will come here ignoring the then statement ', error)
  })

Đây là cách chúng tôi giải quyết vấn đề đua xe và một số vấn đề xử lý lỗi. Mã này vẫn còn một chút phức tạp. Nhưng đó là một cách để cho bạn thấy rằng cách tiếp cận này cũng có thể tạo ra các vấn đề về khả năng đọc.

Cách khắc phục nhanh là tách các cuộc gọi lại như sau:

const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users`

const userRequest = request(userGet)

// Just by reading this part out loud you have a good idea of what the code does
userRequest
  .then(handleUsersList)
  .then(repoRequest)
  .then(handleReposList)
  .catch(handleErrors)
  
function handleUsersList(users) {
  return JSON.parse(users).items
}

function repoRequest(users) {
  return Promise.all(users.map(function(user) {
    return request(user.repos_url)
  }))
}

function handleReposList(repos) {
  console.log('All users repos in an array', repos)
}

function handleErrors(error) {
  console.error('Something went wrong ', error)
}

Bằng cách nhìn vào những gì userRequest đang chờ đợi theo thứ tự với .then bạn có thể hiểu được những gì chúng tôi mong đợi ở khối mã này. Mọi thứ ít nhiều được ngăn cách bởi trách nhiệm.

Đây là “làm xước bề mặt” của những gì Hứa hẹn. Để có cái nhìn sâu sắc về cách chúng hoạt động, tôi không thể giới thiệu đủ bài viết này.

máy phát điện

Một cách tiếp cận khác là sử dụng máy phát điện. Đây là phần nâng cao hơn một chút, vì vậy nếu bạn mới bắt đầu, vui lòng chuyển sang chủ đề tiếp theo.

Một cách sử dụng cho trình tạo là chúng cho phép bạn có mã không đồng bộ trông giống như đồng bộ hóa.

Chúng được đại diện bởi một * trong một chức năng và trông giống như:

function* foo() {
  yield 1
  const args = yield 2
  console.log(args)
}
var fooIterator = foo()

console.log(fooIterator.next().value) // will log 1
console.log(fooIterator.next().value) // will log 2

fooIterator.next('aParam') // will log the console.log inside the generator 'aParam'

Thay vì trở lại với một returnmáy phát điện có một yield bản tường trình. Nó dừng thực thi chức năng cho đến khi một .next được thực hiện cho chức năng lặp lại đó. Nó tương tự như .then lời hứa chỉ thực hiện khi giải quyết trở lại.

Chức năng yêu cầu của chúng tôi sẽ trông như thế này:

function request(url) {
  return function(callback) {
    const xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function(e) {
      if (xhr.readyState === 4) {
        if (xhr.status === 200) {
          callback(null, xhr.response)
        } else {
          callback(xhr.status, null)
        }
      }
    }
    xhr.ontimeout = function () {
      console.log('timeout')
    }
    xhr.open('get', url, true)
    xhr.send()
  }
}

Chúng tôi muốn có url như một lý lẽ. Nhưng thay vì thực hiện yêu cầu ngoài cổng, chúng tôi chỉ muốn nó khi chúng tôi có một cuộc gọi lại để xử lý phản hồi.

Của chúng ta generator sẽ là một cái gì đó như:

function* list() {
  const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users`
 
  const users = yield request(userGet)
  
  yield
  
  for (let i = 0; i<=users.length; i++) {
    yield request(users[i].repos_url)
  }
}

Nó sẽ:

  • Chờ cho đến khi đầu tiên request được chuẩn bị
  • Trả lại một function tài liệu tham khảo mong đợi một callback lần đầu tiên request
    Của chúng ta request chức năng chấp nhận một url
    và trả về một function mà mong đợi một callback
  • mong đợi một users sẽ được gửi trong lần tiếp theo .next
  • Lặp đi lặp lại users
  • đợi một .next cho mỗi users
  • Trả lại chức năng gọi lại tương ứng của họ

Vì vậy, việc thực hiện điều này sẽ là:

try {
  const iterator = list()
  iterator.next().value(function handleUsersList(err, users) {
    if (err) throw err
    const list = JSON.parse(users).items
    
    // send the list of users for the iterator
    iterator.next(list)
    
    list.forEach(function(user) {
      iterator.next().value(function userRepos(error, repos) {
        if (error) throw repos
        
        // Handle each individual user repo here
        console.log(user, JSON.parse(repos))
      })
    })
  })  
} catch (e) {
  console.error(e)
}

Chúng ta có thể tách các chức năng gọi lại như chúng ta đã làm trước đây. Bây giờ bạn đã có thỏa thuận, một điểm đáng chú ý là giờ đây chúng tôi có thể xử lý từng danh sách kho lưu trữ người dùng riêng lẻ.

Đọc thêm  Hãy làm sáng tỏ từ khóa 'mới' của JavaScript

Tôi có nhiều suy nghĩ khác nhau về máy phát điện. Một mặt, tôi có thể nắm bắt được những gì được mong đợi ở mã bằng cách xem trình tạo.

Nhưng quá trình thực thi của nó kết thúc bằng những vấn đề tương tự như địa ngục gọi lại.

Giống như async/await, nên sử dụng trình biên dịch. Điều này là do nó không được hỗ trợ trong các phiên bản trình duyệt cũ hơn.

Ngoài ra nó không phải là phổ biến trong kinh nghiệm của tôi. Vì vậy, nó có thể gây nhầm lẫn trong các cơ sở mã được duy trì bởi các nhà phát triển khác nhau.

Bạn có thể tìm thấy thông tin chi tiết tuyệt vời về cách thức hoạt động của máy phát điện trong bài viết này. Và đây là một nguồn tài nguyên tuyệt vời khác.

Không đồng bộ/Đang chờ

Phương pháp này có vẻ giống như sự kết hợp của các trình tạo với lời hứa. Bạn chỉ cần nói cho mã của mình biết các chức năng sẽ là gì async. Và phần nào của mã sẽ phải await vì điều đó promise kêt thuc.

sumTwentyAfterTwoSeconds(10)
  .then(result => console.log('after 2 seconds', result))
  
async function sumTwentyAfterTwoSeconds(value) {
  const remainder = afterTwoSeconds(20)
  return value + await remainder
}

function afterTwoSeconds(value) {
  return new Promise(resolve => {
    setTimeout(() => { resolve(value) }, 2000);
  });
}

Trong kịch bản này:

  • Chúng ta có sumTwentyAfterTwoSeconds như là một chức năng không đồng bộ
  • Chúng tôi bảo mã của chúng tôi đợi resolve hoặc reject cho chức năng lời hứa của chúng tôi afterTwoSeconds
  • Nó sẽ chỉ kết thúc trong .then khi mà await hoạt động kết thúc
    Trong trường hợp này chỉ có một

Áp dụng điều này cho chúng tôi request chúng tôi để nó như một promise như đã thấy trước đó:

function request(url) {
  return new Promise(function(resolve, reject) {
    const xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function(e) {
      if (xhr.readyState === 4) {
        if (xhr.status === 200) {
          resolve(xhr.response)
        } else {
          reject(xhr.status)
        }
      }
    }
    xhr.ontimeout = function () {
      reject('timeout')
    }
    xhr.open('get', url, true)
    xhr.send()
  })
}

chúng tôi tạo ra của chúng tôi async chức năng với sự chờ đợi cần thiết như vậy:

async function list() {
  const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users`
  
  const users = await request(userGet)
  const usersList = JSON.parse(users).items
  
  usersList.forEach(async function (user) {
    const repos = await request(user.repos_url)
    
    handleRepoList(user, repos)
  })
}

function handleRepoList(user, repos) {
  const userRepos = JSON.parse(repos)
  
  // Handle each individual user repo here
  
  console.log(user, userRepos)
}

Vì vậy, bây giờ chúng tôi có một async list chức năng sẽ xử lý các yêu cầu. Một async khác là cần thiết trong forEach để chúng tôi có danh sách repos để mỗi người dùng thao tác.

Chúng tôi gọi nó là:

list()
  .catch(e => console.error(e))

Cách tiếp cận này và cách tiếp cận hứa hẹn là mục yêu thích của tôi vì mã dễ đọc và dễ thay đổi. Bạn có thể đọc sâu hơn về async/await tại đây.

Một nhược điểm của việc sử dụng async/await là nó không được hỗ trợ ở mặt trước bởi các trình duyệt cũ hơn hoặc ở mặt sau. Bạn phải sử dụng Nút 8.

Bạn có thể sử dụng trình biên dịch như babel để giải quyết vấn đề đó.

“Dung dịch”

Bạn có thể thấy mã kết thúc hoàn thành mục tiêu ban đầu của chúng tôi bằng cách sử dụng async/await trong đoạn mã này.

Một điều tốt nên làm là tự mình thử theo các hình thức khác nhau được đề cập trong bài viết này.

Phần kết luận

Tùy thuộc vào tình huống mà bạn có thể thấy mình đang sử dụng:

Tùy thuộc vào bạn những gì phù hợp với mục đích của bạn. Và điều gì cho phép bạn duy trì mã sao cho người khác và bản thân bạn trong tương lai có thể hiểu được.

Ghi chú: Bất kỳ cách tiếp cận nào trở nên ít dài dòng hơn một chút khi sử dụng các lựa chọn thay thế cho các yêu cầu như $.ajaxfetch.

Hãy cho tôi biết bạn sẽ làm những gì khác nhau và những cách khác nhau mà bạn tìm thấy để làm cho mỗi cách tiếp cận dễ đọc hơn.

Đây là Điều 11 trên 30. Đây là một phần của dự án xuất bản một bài báo ít nhất một lần một tuần, từ những suy nghĩ vu vơ đến hướng dẫn. Để lại nhận xét, theo dõi tôi trên Diogo Spínola và sau đó quay lại dự án tuyệt vời của bạn!



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