<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          JavaScript 異步編程指南 — 事件與回調函數 Callback

          共 7387字,需瀏覽 15分鐘

           ·

          2021-05-30 13:34

          這是一個系列文章,你可以關注公眾號「五月君」訂閱話題《JavaScript 異步編程指南》獲取最新信息。

          JavaScript 異步編程中回調是最常用和最基礎的實現(xiàn)模式。回調就是函數,一般我們也會稱它為 Callback,相信這對于 JavaScript 開發(fā)者不會陌生,而函數在 JavaScript 中屬于一等公民,可以將函數傳遞給方法作為實參調用。

          這種編程模式對于習慣同步思維的人來說很難理解,一般我們的大腦對事物的理解是同步的、線性的,在異步編程中它是一種相反的模式,你會看到代碼的編寫順序與實際執(zhí)行順序并不是我們預期的,因為它們的編寫與實際執(zhí)行順序也許沒有什么直接的關系,特別是在處理一些復雜的業(yè)務場景時,掌握不好異步編程,通常也會寫出糟糕的代碼。

          在筆者組建的技術交流群中,有時候大家提問一些問題,當看到一大堆 Callback 嵌套的代碼時,感覺就很糟糕,頓時很難讓人在有耐心去看它,這種模式它不會給予我們很友好的閱讀體驗,有時看到了我會說你先把代碼書寫邏輯整理下,也許問題就出在這里!

          談回調也少不了一個概念 “事件”,在使用 JavaScript 操作 DOM、網絡請求或在 Node.js 中更多的是一種事件驅動的模型,由事件觸發(fā)執(zhí)行我們的回調。

          定時器

          例如,我們?yōu)?定時器 API 其傳入一個函數,讓其在將來某個時間之后執(zhí)行。我們可以通過 setTimeout 或 setInterval 實現(xiàn),前一個 setTimeout 是僅執(zhí)行一次,后一個 setInterval 是間隔指定時間后重復執(zhí)行。

          這兩個 API 在瀏覽器、Node.js 環(huán)境中使用都是一樣的。

          function fn({
           // do something...
          }
          setTimeout(fn, 1000);
          setInterval(fn, 1000);

          網絡事件

          發(fā)起一個請求從另一端獲取數據,這也是異步中很常見的一個操作,在客戶端早期我們可以使用 XMLHttpRequest發(fā)起 HTTP 請求并異步處理服務器返回的響應。

          const httpRequest = new XMLHttpRequest();
          httpRequest.open('GET''http://openapi.xxx.com/api');
          httpRequest.send();
          httpRequest.onreadystatechange = function({
           if (httpRequest.readyState === XMLHttpRequest.DONE) {
                if (httpRequest.status === 200) {
                  alert(httpRequest.responseText);
                } else {
                  alert('There was a problem with the request.');
                }
              }
          };

          現(xiàn)在瀏覽器端有了一個新的 API fetch() 取代了復雜且名字容易誤導人的 XMLHttpRequest,因為這個雖然名字帶了 XML 但和 XML 沒關系,fetch() API 完全基于 Promise 可以方便的讓你編寫代碼從網絡獲取數據,簡單看一下:

          fetch('http://example.com/movies.json')
           .then(function(response{
              return response.json();
            })
            .then(function(myJson{
              console.log(myJson);
            });

          Node.js 中也定義了一些網絡相關的 API,Node.js 提供的 HTTP/HTTPS 模塊可以幫助我們在 Node.js 客戶端向服務端請求數據

          const http = require('http');
          function sendRequest({
            const req = http.request({
              method'GET',
              host'127.0.0.1',
              port3010,
              path'/api'
            }, res => {
              let data = '';
              res.on('data', chunk => data += chunk.toString());
              res.on('end', () => {
                console.log('response body: ', data);
              });
            });
            req.on('error'console.error);
            req.end();
          }
          sendRequest();

          這種方式來寫還是有點繁瑣的,在實際的業(yè)務開發(fā)中我們使用一些功能完備的 HTTP 請求模塊,例如 node-fetch、nodejs/undici、axios 等,這些工具都是可以基于 Promise 的形式。

          Node.js 做為一個服務端啟動,我們還可以使用 HTTP 模塊,如下方式啟動一個 Server:

          const http = require('http');
          http.createServer((req, res) => {
            req.on('data', chunk => {
            // TODO
           });
            req.on('end', () => res.end('ok!'))
            req.on('error', () => ...)
          }).listen(3010);

          客戶端 DOM 事件與回調

          客戶端下的 JavaScript 我們可以獲取指定的 DOM 元素,為特定類型的事件注冊回調函數,當用戶移動鼠標或移動觸摸板、按下鍵盤時,瀏覽器會生成相應的事件并調用我們事先注冊的回調函數,這些都是由事件驅動的。

          下例,通過 addEventListener() 函數為事件注冊回調函數。相對來說 DOM 事件在互相依賴、多級依賴嵌套的場景較少些,但是在 Node.js 里面你可能會遇到很多。

          <button id="btn"> 點我哦 </button>
          <script>
            const btn = document.getElementById('btn');

            /
          / 單擊時觸發(fā)
            btn.addEventListener('click', event => console.log('click!'));

            /
          / 鼠標移入觸發(fā)
            btn.addEventListener('mouseover', event => console.log('mouseover!'));

            /
          / 鼠標移出觸發(fā)
            btn.addEventListener('mouseout', event => console.log('mouseout!'));
          </
          script>

          Node.js 中的事件與回調

          Node.js 作為 JavaScript 的服務端運行時,大部分的 API 都是異步的,大家可能也聽過 Node.js 比較擅長 I/O 密集型任務,這與它的單線程、基于事件驅動模型、異步 I/O是有關系的,它無需像多線程程序那樣為每一個請求創(chuàng)建額外的線程、省掉了線程創(chuàng)建、銷毀、上下文切換等開銷。

          它通過主循環(huán)加事件觸發(fā)的方式執(zhí)行程序,事件循環(huán)會不停地處理網絡/文件 IO 事件,每一次的事件循環(huán)就是檢查,檢查是否有待處理的事件,如果有就取出事件及關聯(lián)的回調函數,如果有傳入 JavaScript 回調函數,傳遞到業(yè)務邏輯層執(zhí)行,也許回調函數里還會在發(fā)起一次新的 I/O 請求,整個程序不斷的通過事件循環(huán)調度執(zhí)行。

          也許你聽過這樣一句話:“它的優(yōu)秀之處并非原創(chuàng),它的原創(chuàng)之處并不優(yōu)秀。” 異步 I/O 并非 Node.js 原創(chuàng),但 Node.js 卻是第一個成功的平臺,Node.js 2009 年出現(xiàn)之前,JavaScript 在服務端近乎空白。例如,文件 API 在 Node.js 中默認就是異步的,也就是它的標準庫 I/O 本身給你提供的就是非阻塞的,它沒有任何的歷史包袱。

          談到異步 I/O 必然少不了異步編程,早期我們的很多程序中都充斥著 Callback 風格的代碼,包括 Node.js 提供的 API 大多數也是,大家都遵循一個默認的規(guī)則 “錯誤優(yōu)先的回調函數”。

          例如,下面 API 第一個參數為 err 如果有錯誤就是一個 Error 對象,否則就為 null,這也是一種默認的約定。

          fs.readFile(filename, (err, file) => {
           // TODO
          })

          現(xiàn)在 Node.js 的一些系統(tǒng)模塊已經為我們提供了一些工具可以方便的將 callback 轉換為 Promise 的工具,或者文件模塊我們可以通過 fs.promises 直接引入基于 Promise 版本的 API,這些編程方法我們會在后續(xù)章節(jié) Promise 篇幅里講。

          一個糟糕的回調地獄例子

          當我們在 Node.js 中有時需要處理一些復雜的業(yè)務場景,有些需要多級依賴,如果以 callback 形式很容易造成函數嵌套過深,例如下面示例很容易寫出回調地獄、冗余的代碼,這也是早期 Node.js 被人詬病比較多的地方。包括現(xiàn)在前段在群里仍然還有看到有些提問題的,寫出類似于下面嵌套的代碼,確實要改下了。

          fs.readdir('/path/xxxx', (err, files) => {
            if (err) {
              // TODO...
            }
            files.forEach((filename, index) => {
              fs.lstat(filename, (err, stats) => {
                if (err) {
                  // TODO...
                }
                if (stats.isFile()) {
                  fs.readFile(filename, (err, file) => {
                    // TODO
                  })
                }
              })
            })
          });

          異步編程 Callback 的形式一個難點是上面說的容易出現(xiàn)回調地獄的例子,另外一方面是異常的處理很麻煩,在一些同步的代碼中我們可以像下面示例這樣使用 try/catch 捕獲錯誤。

          try {
           doSomething(...);
          catch(err) {
           // TODO
          }

          這種方式在一些異步方法面前顯得無能為力,上面我們寫的回調嵌套的示例,如果我們對 fs.readFile() 做 try/catch 捕獲,當我們調用 fs.readFile 并為其注冊回調函數這個步驟對應異步 I/O 中是提交請求,而 callback 函數會被存放起來,等到下一個事件循環(huán)到來 callback 才會被取出執(zhí)行,這個時間是將來的某個時間點,而 try/catch 是同步的,捕獲不到這個錯誤的。

          下面因為我對一個 null 對象做了非法操作,這時程序會給我們報一個 TypeError: Cannot read property 'a' of null 錯誤,在 Java 中可以稱它為空指針異常

          類似于這樣的一個錯誤如果沒有被捕獲到,在單進程的應用程序中必然會導致進程退出,無關語言

          try {
           fs.readFile(filename, (err, file) => {
             const obj = null
              obj.a;
              // TODO
            })
          catch () {
           // TODO
          }

          有時候也會聽大家說為什么我的 Node.js 程序老是崩潰?也有人說 Node.js 弱爆了(這個我曾經聽過一個架構師這樣說過...)如果程序這樣寫,就算你用的 Java 照樣崩潰。

          在延伸一點,Node.js 的 Process 對象為我們提供了兩個事件可以用來捕獲程序中出現(xiàn)的未捕獲異常,方便程序優(yōu)雅退出,這是筆者之前寫的一篇文章,可以看看如何處理 Node.js 中出現(xiàn)的未捕獲異常?

          process.on('uncaughtException', fn);
          process.on('unhandledRejection', fn);

          總結

          異步編程中 Callback 是比較早的模式,也是異步編程的基礎,但是隨著業(yè)務的發(fā)展、復雜度的上升,基于 Callback 的模式已經不能滿足我們的需求了,就像我們的大腦對事物的思考,需要一種同步的、順序的方式表達異步編程思想。

          “辦法總比困難多”,解決問題的方案還是很多的,目前的 JavaScript 中已有一些更高級、強大的異步編程模式,在本系列中會逐步的講解。



          ● 字節(jié)跳動最愛考的前端面試題:CSS 基礎

          ● 字節(jié)跳動最愛考的前端面試題:JavaScript 基礎

          ● 字節(jié)跳動最愛考的前端面試題:計算機網絡基礎



          ·END·

          圖雀社區(qū)

          匯聚精彩的免費實戰(zhàn)教程



          關注公眾號回復 z 拉學習交流群


          喜歡本文,點個“在看”告訴我

          瀏覽 87
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  羽月希产后再次复出电影 | 91福利在线视频 | 国产毛片一区二区三区 | 操比一区二区三区 | 国产有码视频 |