整合營銷服務(wù)商

          電腦端+手機端+微信端=數(shù)據(jù)同步管理

          免費咨詢熱線:

          16.HTML 表單提交

          現(xiàn)代web開發(fā)中,表單是用戶與網(wǎng)站互動的重要方式之一。HTML5為表單提交提供了強大的功能和豐富的輸入類型,讓收集和驗證用戶輸入數(shù)據(jù)變得更加容易和安全。本文將詳細介紹HTML5表單的各個方面,包括基本結(jié)構(gòu)、輸入類型、驗證方法和提交過程。

          基本結(jié)構(gòu)

          HTML表單由<form>標簽定義,它可以包含輸入字段、標簽、按鈕等元素。一個基本的表單結(jié)構(gòu)如下所示:

          <form action="/submit_form" method="post">
            <label for="name">姓名:</label>
            <input type="text" id="name" name="name" required>
            
            <label for="email">電子郵箱:</label>
            <input type="email" id="email" name="email" required>
            
            <input type="submit" value="提交">
          </form>
          

          在這個例子中,表單有兩個輸入字段:姓名和電子郵箱。每個輸入字段都有一個<label>標簽,這不僅有助于用戶理解輸入的內(nèi)容,也有助于屏幕閱讀器等輔助技術(shù)。<form>標簽的action屬性定義了數(shù)據(jù)提交到服務(wù)器的URL,method屬性定義了提交數(shù)據(jù)的HTTP方法(通常是post或get)。

          輸入類型

          HTML5提供了多種輸入類型,以支持不同的數(shù)據(jù)格式和設(shè)備。

          文本輸入

          <!-- 單行文本 -->
          <input type="text" name="username" placeholder="請輸入用戶名" required>
          
          <!-- 密碼 -->
          <input type="password" name="password" required minlength="8">
          
          <!-- 郵箱 -->
          <input type="email" name="email" required placeholder="example@domain.com">
          
          <!-- 搜索框 -->
          <input type="search" name="search" placeholder="搜索...">
          

          數(shù)值輸入

          <!-- 數(shù)值 -->
          <input type="number" name="age" min="18" max="100" step="1" required>
          
          <!-- 滑動條 -->
          <input type="range" name="volume" min="0" max="100" step="1">
          
          <!-- 電話號碼 -->
          <input type="tel" name="phone" pattern="^\+?\d{0,13}" placeholder="+8613800000000">
          

          日期和時間輸入

          <!-- 日期 -->
          <input type="date" name="birthdate" required>
          
          <!-- 時間 -->
          <input type="time" name="appointmenttime">
          
          <!-- 日期和時間 -->
          <input type="datetime-local" name="appointmentdatetime">
          

          選擇輸入

          <!-- 復選框 -->
          <label><input type="checkbox" name="interest" value="coding"> 編程</label>
          <label><input type="checkbox" name="interest" value="music"> 音樂</label>
          
          <!-- 單選按鈕 -->
          <label><input type="radio" name="gender" value="male" required> 男性</label>
          <label><input type="radio" name="gender" value="female"> 女性</label>
          
          <!-- 下拉選擇 -->
          <select name="country" required>
            <option value="china">中國</option>
            <option value="usa">美國</option>
          </select>
          

          特殊輸入

          <!-- 顏色選擇器 -->
          <input type="color" name="favcolor" value="#ff0000">
          
          <!-- 文件上傳 -->
          <input type="file" name="resume" accept=".pdf,.docx" multiple>
          

          驗證方法

          HTML5表單提供了內(nèi)置的驗證功能,可以在數(shù)據(jù)提交到服務(wù)器之前進行檢查。

          必填字段

          <input type="text" name="username" required>
          

          正則表達式

          <input type="text" name="zipcode" pattern="\d{5}(-\d{4})?" title="請輸入5位數(shù)的郵政編碼">
          

          數(shù)值范圍

          <input type="number" name="age" min="18" max="99">
          

          長度限制

          <input type="text" name="username" minlength="4" maxlength="8">
          

          表單提交

          當用戶填寫完表單并點擊提交按鈕時,瀏覽器會自動檢查所有輸入字段的有效性。如果所有字段都滿足要求,表單數(shù)據(jù)將被發(fā)送到服務(wù)器。否則,瀏覽器會顯示錯誤信息,并阻止表單提交。

          <input type="submit" value="提交">
          

          可以使用JavaScript來自定義驗證或處理提交事件:

          document.querySelector('form').addEventListener('submit', function(event) {
            // 檢查表單數(shù)據(jù)
            if (!this.checkValidity()) {
              event.preventDefault(); // 阻止表單提交
              // 自定義錯誤處理
            }
            // 可以在這里添加額外的邏輯,比如發(fā)送數(shù)據(jù)到服務(wù)器的Ajax請求
          });
          

          完整例子

          <!DOCTYPE html>
          <html lang="en">
          <head>
          <meta charset="UTF-8">
          <title>表單提交并顯示JSON</title>
          </head>
          <body>
          
          <!-- 表單定義 -->
          <form id="myForm">
            <label for="name">姓名:</label>
            <input type="text" id="name" name="name">
            <br>
          
            <label for="email">電子郵件:</label>
            <input type="email" id="email" name="email">
            <br>
          
            <input type="button" value="提交" onclick="submitForm()">
          </form>
          
          <script>
          // JavaScript函數(shù),處理表單提交
          function submitForm() {
            // 獲取表單元素
            var form = document.getElementById('myForm');
            
            // 創(chuàng)建一個FormData對象
            var formData = new FormData(form);
            
            // 創(chuàng)建一個空對象來存儲表單數(shù)據(jù)
            var formObject = {};
            
            // 將FormData轉(zhuǎn)換為普通對象
            formData.forEach(function(value, key){
              formObject[key] = value;
            });
            
            // 將對象轉(zhuǎn)換為JSON字符串
            var jsonString = JSON.stringify(formObject);
            
            // 彈出包含JSON字符串的對話框
            alert(jsonString);
            
            // 阻止表單的默認提交行為
            return false;
          }
          </script>
          
          </body>
          </html>
          

          在這個例子中:

          1. 我們定義了一個包含姓名和電子郵件輸入字段的表單,以及一個按鈕,當點擊按鈕時會調(diào)用submitForm函數(shù)。
          2. 在submitForm函數(shù)中,我們首先獲取表單元素并創(chuàng)建一個FormData對象。然后,我們遍歷FormData對象,將表單數(shù)據(jù)復制到一個普通的JavaScript對象formObject中。
          3. 接著,我們使用JSON.stringify方法將formObject轉(zhuǎn)換成JSON字符串。
          4. 最后,我們使用alert函數(shù)彈出一個包含JSON字符串的對話框。

          注意,這個例子中我們使用了type="button"而不是type="submit",因為我們不希望表單有默認的提交行為。我們的JavaScript函數(shù)submitForm會處理所有的邏輯,并且通過返回false來阻止默認的表單提交。如果你想要使用type="submit",你需要在<form>標簽上添加一個onsubmit="return submitForm()"屬性來代替按鈕上的onclick事件。

          結(jié)論

          HTML5的表單功能為開發(fā)者提供了強大的工具,以便創(chuàng)建功能豐富、用戶友好且安全的網(wǎng)站。通過使用HTML5的輸入類型和驗證方法,可以確保用戶輸入的數(shù)據(jù)是有效的,同時提高用戶體驗。隨著技術(shù)的不斷進步,HTML5表單和相關(guān)API將繼續(xù)發(fā)展,為前端工程師提供更多的可能性。

          兩個菜單按鈕系列選項實例("File" 和 "Edit"):

          <menutype="toolbar"><li><menulabel="File"><buttontype="button"onclick="file_new()">New...</button><buttontype="button"onclick="file_open()">Open...</button><buttontype="button"onclick="file_save()">Save</button></menu></li><li><menulabel="Edit"><buttontype="button"onclick="edit_cut()">Cut</button><buttontype="button"onclick="edit_copy()">Copy</button><buttontype="button"onclick="edit_paste()">Paste</button></menu></li></menu>


          瀏覽器支持

          目前主流瀏覽器并不支持 <menu> 標簽。


          標簽定義及使用說明

          <menu> 標簽定義了一個命令列表或菜單。

          <menu> 標簽通常用于文本菜單,工具條和命令列表選項。


          提示和注釋

          提示: 使用 CSS 來定義菜單列表樣式。


          HTML 4.01 與 HTML5之間的差異

          HTML 4.01的 <menu> 元素已廢棄。

          HTML5 中 <menu> 元素已被重新定義。


          屬性

          New:HTML5 新屬性。

          屬性描述
          labelNewtext描述菜單項的標記。
          typeNewcontexttoolbarlist描述顯示菜單類型. 默認為 "list"。

          全局屬性

          <menu> 標簽支持全局屬性,查看完整屬性表 HTML全局屬性。


          事件屬性

          <menu> 標簽支持所有 HTML事件屬性。

          如您還有不明白的可以在下面與我留言或是與我探討QQ群308855039,我們一起飛!

          . JS的運行機制

          介紹

          眾所周知JavaScript是一門單線程的語言,所以在JavaScript的世界中默認的情況下,同一個時間節(jié)點只能做一件事情,這樣的設(shè)定就造成了JavaScript這門語言的一些局限性,比如在我們的頁面中加載一些遠程數(shù)據(jù)時,如果按照單線程同步的方式運行,一旦有HTTP請求向服務(wù)器發(fā)送,就會出現(xiàn)等待數(shù)據(jù)返回之前網(wǎng)頁假死的效果出現(xiàn)。因為JavaScript在同一個時間只能做一件事,這就導致了頁面渲染和事件的執(zhí)行,在這個過程中無法進行。顯然在實際的開發(fā)中我們并沒有遇見過這種情況。

          關(guān)于同步和異步

          基于以上的描述,我們知道在JavaScript的世界中,應該存在一種解決方案,來處理單線程造成的詬病。這就是同步【阻塞】和異步【非阻塞】執(zhí)行模式的出現(xiàn)。

          同步(阻塞)

          同步的意思是JavaScript會嚴格按照單線程(從上到下、從左到右的方式)執(zhí)行代碼邏輯,進行代碼的解釋和運行,所以在運行代碼時,不會出現(xiàn)先運行4、5行的代碼,再回頭運行1、3行的代碼這種情況。比如下列操作。

          
          var a = 1
          var b = 2
          var c = a + b
          //這個例子總c一定是3不會出現(xiàn)先執(zhí)行第三行然后在執(zhí)行第二行和第一行的情況
          console.log(c)

          接下來通過下列的案例升級一下代碼的運行場景:

          var a = 1
          var b = 2
          var d1 = new Date().getTime()
          var d2 = new Date().getTime()
          while(d2-d1<2000){
            d2 = new Date().getTime()
          }
          //這段代碼在輸出結(jié)果之前網(wǎng)頁會進入一個類似假死的狀態(tài)
          console.log(a+b)

          當我們按照順序執(zhí)行上面代碼時,我們的代碼在解釋執(zhí)行到第4行時,還是正常的速度執(zhí)行,但是在下一行就會進入一個持續(xù)的循環(huán)中。d2d1在行級間的時間差僅僅是毫秒內(nèi)的差別,所以在執(zhí)行到while循環(huán)的時候d2-d1的值一定比2000小,那么這個循環(huán)會執(zhí)行到什么時候呢?由于每次循環(huán)時,d2都會獲取一次當前的時間發(fā)生變化,直到d2-d1==2000等情況,這時也就是正好過了2秒的時間,我們的程序才能跳出循環(huán),進而再輸出a+b的結(jié)果。那么這段程序的實際執(zhí)行時間至少是2秒以上。這就導致了程序阻塞的出現(xiàn),這也是為什么將同步的代碼運行機制叫做阻塞式運行的原因。

          阻塞式運行的代碼,在遇到消耗時間的代碼片段時,之后的代碼都必須等待耗時的代碼運行完畢,才能得到執(zhí)行資源,這就是單線程同步的特點。

          異步(非阻塞):

          在上面的闡述中,我們明白了單線程同步模型中的問題所在,接下來引入單線程異步模型的介紹。異步的意思就是和同步對立,所以異步模式的代碼是不會按照默認順序執(zhí)行的。JavaScript執(zhí)行引擎在工作時,仍然是按照從上到下從左到右的方式解釋和運行代碼。在解釋時,如果遇到異步模式的代碼,引擎會將當前的任務(wù)“掛起”并略過。也就是先不執(zhí)行這段代碼,繼續(xù)向下運行非異步模式的代碼,那么什么時候來執(zhí)行異步代碼呢?直到同步代碼全部執(zhí)行完畢后,程序會將之前“掛起”的異步代碼按照“特定的順序”來進行執(zhí)行,所以異步代碼并不會【阻塞】同步代碼的運行,并且異步代碼并不是代表進入新的線程同時執(zhí)行,而是等待同步代碼執(zhí)行完畢再進行工作。我們閱讀下面的代碼分析:

          var a = 1
          var b = 2
          setTimeout(function(){
            console.log('輸出了一些內(nèi)容')
          },2000)
          //這段代碼會直接輸出3并且等待2秒左右的時間在輸出function內(nèi)部的內(nèi)容
          console.log(a+b)

          這段代碼的setTimeout定時任務(wù)規(guī)定了2秒之后執(zhí)行一些內(nèi)容,在運行當前程序執(zhí)行到setTimeout時,并不會直接執(zhí)行內(nèi)部的回調(diào)函數(shù),而是會先將內(nèi)部的函數(shù)在另外一個位置(具體是什么位置下面會介紹)保存起來,然后繼續(xù)執(zhí)行下面的console.log進行輸出,輸出之后代碼執(zhí)行完畢,然后等待大概2秒左右,之前保存的函數(shù)再執(zhí)行。

          非阻塞式運行的代碼,程序運行到該代碼片段時,執(zhí)行引擎會將程序保存到一個暫存區(qū),等待所有同步代碼全部執(zhí)行完畢后,非阻塞式的代碼會按照特定的執(zhí)行順序,分步執(zhí)行。這就是單線程異步的特點。

          通俗的講:

          通俗的講,同步和異步的關(guān)系是這樣的:

          【同步的例子】:比如我們在核酸檢測站,進行核酸檢測這個流程就是同步的。每個人必須按照來的時間,先后進行排隊,而核酸檢測人員會按照排隊人的順序嚴格的進行逐一檢測,在第一個人沒有檢測完成前,第二個人就得無條件等待,這個就是一個阻塞流程。如果排隊過程中第一個人在檢測時出了問題,如棉簽斷了需要換棉簽,這樣更換時間就會追加到這個人身上,直到他順利的檢測完畢,第二個人才能輪到。如果在檢測中間棉簽沒有了,或者是錄入信息的系統(tǒng)崩潰了,整個隊列就進入無條件掛起狀態(tài)所有人都做不了了。這就是結(jié)合生活中的同步案例。

          【異步的例子】:還是結(jié)合生活中,當我們進餐館吃飯時,這個場景就屬于一個完美的異步流程場景。每一桌來的客人會按照他們來的順序進行點單,假設(shè)只有一個服務(wù)員的情況,點單必須按照先后順序,但是服務(wù)員不需要等第一桌客人點好的菜出鍋上菜,就可以直接去收集第二桌第三桌客人的需求。這樣可能在十分鐘之內(nèi),服務(wù)員就將所有桌的客人點菜的菜單統(tǒng)計出來,并且發(fā)送給了后廚。之后的菜也不會按照點餐顧客的課桌順序,因為后廚收集到菜單之后可能有1,2,3桌的客人都點了鍋包肉,那么他可能會先一次出三份鍋包肉,這樣鍋包肉在上菜的時候1,2,3桌的客人都能得到,并且其他的菜也會亂序的逐一上菜,這個過程就是異步的。如果按照同步的模式點餐,默認在飯店點菜就會出現(xiàn)飯店在第一桌客人上滿菜之前第二桌之后的客人就只能等待連單都不能點的狀態(tài)。

          總結(jié):

          JavaScript的運行順序就是完全單線程的異步模型:同步在前,異步在后。所有的異步任務(wù)都要等待當前的同步任務(wù)執(zhí)行完畢之后才能執(zhí)行。請看下面的案例:

          var a = 1
          var b = 2
          var d1 = new Date().getTime()
          var d2 = new Date().getTime()
          setTimeout(function(){
            console.log('我是一個異步任務(wù)')
          },1000)
          while(d2-d1<2000){
            d2 = new Date().getTime()
          }
          //這段代碼在輸出3之前會進入假死狀態(tài),'我是一個異步任務(wù)'一定會在3之后輸出
          console.log(a+b)

          觀察上面的程序我們實際運行之后就會感受到單線程異步模型的執(zhí)行順序了,并且這里我們會發(fā)現(xiàn)setTimeout設(shè)置的時間是1000毫秒但是在while的阻塞2000毫秒的循環(huán)之后并沒有等待1秒而是直接輸出了我是一個異步任務(wù),這是因為setTimout的時間計算是從setTimeout()這個函數(shù)執(zhí)行時開始計算的。

          JS的線程組成

          上面我們通過幾個簡單的例子大概了解了一下JS的運行順序,那么為什么是這個順序,這個順序的執(zhí)行原理是什么樣的,我們應該如何更好更深的探究真相呢?這里需要介紹一下瀏覽器中一個Tab頁面的實際線程組成。

          在了解線程組成前要了解一點,雖然瀏覽器是單線程執(zhí)行JavaScript代碼的,但是瀏覽器實際是以多個線程協(xié)助操作來實現(xiàn)單線程異步模型的,具體線程組成如下:

          1. GUI渲染線程
          2. JavaScript引擎線程
          3. 事件觸發(fā)線程
          4. 定時器觸發(fā)線程
          5. http請求線程
          6. 其他線程

          按照真實的瀏覽器線程組成分析,我們會發(fā)現(xiàn)實際上運行JavaScript的線程其實并不是一個,但是為什么說JavaScript是一門單線程的語言呢?因為這些線程中實際參與代碼執(zhí)行的線程并不是所有線程,比如GUI渲染線程為什么單獨存在,這個是防止我們在html網(wǎng)頁渲染一半的時候突然執(zhí)行了一段阻塞式的JS代碼而導致網(wǎng)頁卡在一半停住這種效果。JavaScript代碼運行的過程中實際執(zhí)行程序時,同時只存在一個活動線程,這里實現(xiàn)同步異步就是靠多線程切換的形式來進行實現(xiàn)的

          所以我們通常分析時,將上面的細分線程歸納為下列兩條線程:

          1. 【主線程】:這個線程用來執(zhí)行頁面的渲染,JavaScript代碼的運行,事件的觸發(fā)等等
          2. 【工作線程】:這個線程是在幕后工作的,用來處理異步任務(wù)的執(zhí)行來實現(xiàn)非阻塞的運行模式

          2. JavaScript的運行模型

          上圖是JavaScript運行時的一個工作流程和內(nèi)存劃分的簡要描述,我們根據(jù)圖中可以得知主線程就是我們JavaScript執(zhí)行代碼的線程,主線程代碼在運行時,會按照同步和異步代碼將其分成兩個去處,如果是同步代碼執(zhí)行,就會直接將該任務(wù)放在一個叫做“函數(shù)執(zhí)行棧”的空間進行執(zhí)行,執(zhí)行棧是典型的【棧結(jié)構(gòu)】(先進后出),程序在運行的時候會將同步代碼按順序入棧,將異步代碼放到【工作線程】中暫時掛起,【工作線程】中保存的是定時任務(wù)函數(shù)、JS的交互事件、JS的網(wǎng)絡(luò)請求等耗時操作。

          當【主線程】將代碼塊篩選完畢后,進入執(zhí)行棧的函數(shù)會按照從外到內(nèi)的順序依次運行,運行中涉及到的對象數(shù)據(jù)是在堆內(nèi)存中進行保存和管理的。當執(zhí)行棧內(nèi)的任務(wù)全部執(zhí)行完畢后,執(zhí)行棧就會清空。執(zhí)行棧清空后,“事件循環(huán)”就會工作,“事件循環(huán)”會檢測【任務(wù)隊列】中是否有要執(zhí)行的任務(wù),那么這個任務(wù)隊列的任務(wù)來源就是工作線程,程序運行期間,工作線程會把到期的定時任務(wù)、返回數(shù)據(jù)的http任務(wù)等【異步任務(wù)】按照先后順序插入到【任務(wù)隊列】中,等執(zhí)行棧清空后,事件循環(huán)會訪問任務(wù)隊列,將任務(wù)隊列中存在的任務(wù),按順序(先進先出)放在執(zhí)行棧中繼續(xù)執(zhí)行,直到任務(wù)隊列清空。

          從代碼片段開始分析

          function task1(){
              console.log('第一個任務(wù)')
          }
          function task2(){
              console.log('第二個任務(wù)')
          }
          function task3(){
              console.log('第三個任務(wù)')
          }
          function task4(){
              console.log('第四個任務(wù)')
          }
          task1()
          setTimeout(task2,1000)
          setTimeout(task3,500)
          task4()
          

          剛才的文字閱讀可能在大腦中很難形成一個帶動畫的圖形界面來幫助我們分析JavaScript的實際運行思路,接下來我們將這段代碼肢解之后詳細的研究一下。

          按照字面分析:

          按照字面分析,我們創(chuàng)建了四個函數(shù)代表4個任務(wù),函數(shù)本身都是同步代碼。在執(zhí)行的時候會按照1,2,3,4進行解析,解析過程中我們發(fā)現(xiàn)任務(wù)2和任務(wù)3被setTimeout進行了定時托管,這樣就只能先運行任務(wù)1和任務(wù)4了。當任務(wù)1和任務(wù)4運行完畢之后500毫秒后運行任務(wù)3,1000毫米后運行任務(wù)2。

          那么他們在實際運行時又是經(jīng)歷了怎么樣的流程來運行的呢?大概的流程我們以圖解的形式分析一下。

          圖解分析:

          如上圖,在上述代碼剛開始運行的時候我們的主線程即將工作,按照順序從上到下進行解釋執(zhí)行,此時執(zhí)行棧、工作線程、任務(wù)隊列都是空的,事件循環(huán)也沒有工作。接下來我們分析下一個階段程序做了什么事情。

          結(jié)合上圖可以看出程序在主線程執(zhí)行之后就將任務(wù)1、4和任務(wù)2、3分別放進了兩個方向,任務(wù)1和任務(wù)4都是立即執(zhí)行任務(wù)所以會按照1->4的順序進棧出棧(這里由于任務(wù)1和4是平行任務(wù)所以會先執(zhí)行任務(wù)1的進出棧再執(zhí)行任務(wù)4的進出棧),而任務(wù)2和任務(wù)3由于是異步任務(wù)就會進入工作線程掛起并開始計時,并不影響主線程運行,此時的任務(wù)隊列還是空置的。

          我們發(fā)現(xiàn)同步任務(wù)的執(zhí)行速度是飛快的,這樣一下執(zhí)行棧已經(jīng)空了,而任務(wù)2和任務(wù)3還沒有到時間,這樣我們的事件循環(huán)就會開始工作等待任務(wù)隊列中的任務(wù)進入,接下來就是執(zhí)行異步任務(wù)的時候了。

          我們發(fā)現(xiàn)任務(wù)隊列并不是一下子就會將任務(wù)2和任務(wù)三一起放進去,而是哪個計時器到時間了哪個放進去,這樣我們的事件循環(huán)就會發(fā)現(xiàn)隊列中的任務(wù),并且將任務(wù)拿到執(zhí)行棧中進行消費,此時會輸出任務(wù)3的內(nèi)容。

          到這就是最后一次執(zhí)行,當執(zhí)行完畢后工作線程中沒有計時任務(wù),任務(wù)隊列的任務(wù)清空程序到此執(zhí)行完畢。

          總結(jié)

          我們通過圖解之后腦子里就會更清晰的能搞懂異步任務(wù)的執(zhí)行方式了,這里采用最簡單的任務(wù)模型進行描繪復雜的任務(wù)在內(nèi)存中的分配和走向是非常復雜的,我們有了這次的經(jīng)驗之后就可以通過觀察代碼在大腦中先模擬一次執(zhí)行,這樣可以更清晰的理解JS的運行機制。

          關(guān)于執(zhí)行棧

          執(zhí)行棧是一個棧的數(shù)據(jù)結(jié)構(gòu),當我們運行單層函數(shù)時,執(zhí)行棧執(zhí)行的函數(shù)進棧后,會出棧銷毀然后下一個進棧下一個出棧,當有函數(shù)嵌套調(diào)用的時候棧中就會堆積棧幀,比如我們查看下面的例子:

          function task1(){
            console.log('task1執(zhí)行')
            task2()
            console.log('task2執(zhí)行完畢')
          }
          function task2(){
            console.log('task2執(zhí)行')
            task3()
            console.log('task3執(zhí)行完畢')
          }
          function task3(){
            console.log('task3執(zhí)行')
          }
          task1()
          console.log('task1執(zhí)行完畢')
          

          我們根據(jù)字面閱讀就能很簡單的分析出輸出的結(jié)果會是

          /*
          task1執(zhí)行
          task2執(zhí)行
          task3執(zhí)行
          task3執(zhí)行完畢
          task2執(zhí)行完畢
          task1執(zhí)行完畢
          */

          那么這種嵌套函數(shù)在執(zhí)行棧中的操作流程是什么樣的呢?

          第一次執(zhí)行的時候調(diào)用task1函數(shù)執(zhí)行到console.log的時候先進行輸出,接下來會遇到task2函數(shù)的調(diào)用會出現(xiàn)下面的情況:

          執(zhí)行到此時檢測到task2中還有調(diào)用task3的函數(shù),那么就會繼續(xù)進入task3中執(zhí)行,如下圖:

          在執(zhí)行完task3中的輸出之后task3內(nèi)部沒有其他代碼,那么task3函數(shù)就算執(zhí)行完畢那么就會發(fā)生出棧工作。

          此時我們會發(fā)現(xiàn)task3出棧之后程序運行又會回到task2的函數(shù)中繼續(xù)他的執(zhí)行。接下來會發(fā)生相同的事情。

          再之后就剩下task1自己了,他在task2銷毀之后輸出task2執(zhí)行完畢后他也會隨著出棧而銷毀。

          task1執(zhí)行完畢之后它隨著銷毀最后一行輸出,就會進入執(zhí)行棧執(zhí)行并銷毀,銷毀之后執(zhí)行棧和主線程清空。這個過程就會出現(xiàn)123321的這個順序,而且我們在打印輸出時,也能通過打印的順序來理解入棧和出棧的順序和流程。

          關(guān)于遞歸

          關(guān)于上面的執(zhí)行棧執(zhí)行邏輯清楚后,我們就順便學習一下遞歸函數(shù),遞歸函數(shù)是項目開發(fā)時經(jīng)常涉及到的場景。我們經(jīng)常會在未知深度的樹形結(jié)構(gòu),或其他合適的場景中使用遞歸。那么遞歸在面試中也會經(jīng)常被問到風險問題,如果了解了執(zhí)行棧的執(zhí)行邏輯后,遞歸函數(shù)就可以看成是在一個函數(shù)中嵌套n層執(zhí)行,那么在執(zhí)行過程中會觸發(fā)大量的棧幀堆積,如果處理的數(shù)據(jù)過大,會導致執(zhí)行棧的高度不夠放置新的棧幀,而造成棧溢出的錯誤。所以我們在做海量數(shù)據(jù)遞歸的時候一定要注意這個問題。

          關(guān)于執(zhí)行棧的深度:

          執(zhí)行棧的深度根據(jù)不同的瀏覽器和JS引擎有著不同的區(qū)別,我們這里就Chrome瀏覽器為例子來嘗試一下遞歸的溢出:

          var i = 0;
          function task(){
            let index = i++
            console.log(`遞歸了${index}次`)
            task()
            console.log(`第${index}次遞歸結(jié)束`)
          }
          
          task()

          我們發(fā)現(xiàn)在遞歸了11378次之后會提示超過棧深度的錯誤,也就是我們無法在Chrome或者其他瀏覽器做太深層的遞歸操作。

          如何跨越遞歸限制

          發(fā)現(xiàn)問題后,我們再考慮如何能通過技術(shù)手段跨越遞歸的限制。可以將代碼做如下更改,這樣就不會出現(xiàn)遞歸問題了。

          var i = 0;
          function task(){
            let index = i++
            console.log(`遞歸了${index}次`)
            setTimeout(function(){
              task()
            })
            console.log(`第${index}次遞歸結(jié)束`)
          }
          task()

          我們發(fā)現(xiàn)只是做了一個小小的改造,這樣就不會出現(xiàn)溢出的錯誤了。這是為什么呢?

          在了解原因之前我們先看控制臺的輸出,結(jié)合控制臺輸出我們發(fā)現(xiàn)確實超過了界限也沒有報錯。

          圖解原因:

          這個是因為我們這里使用了異步任務(wù)去調(diào)用遞歸中的函數(shù),那么這個函數(shù)在執(zhí)行的時候就不只使用棧進行執(zhí)行了。

          先看沒有異步流程時候的執(zhí)行圖例:


          再看有了異步任務(wù)的遞歸:

          有了異步任務(wù)之后我們的遞歸就不會疊加棧幀了,因為放入工作線程之后該函數(shù)就結(jié)束了,可以出棧銷毀,那么在執(zhí)行棧中就永遠都是只有一個任務(wù)在運行,這樣就防止了棧幀的無限疊加,從而解決了無限遞歸的問題,不過異步遞歸的過程是無法保證運行速度的,在實際的工作場景中,如果考慮性能問題,還需要使用 while 循環(huán)等解決方案,來保證運行效率的問題,在實際工作場景中,盡量避免遞歸循環(huán),因為遞歸循環(huán)就算控制在有限棧幀的疊加,其性能也遠遠不及指針循環(huán)。


          3.宏任務(wù)和微任務(wù)

          在明確了事件循環(huán)模型以及JavaScript的執(zhí)行流程后,我們認識了一個叫做任務(wù)隊列的容器,他的數(shù)據(jù)結(jié)構(gòu)式隊列的結(jié)構(gòu)。所有除同步任務(wù)外的代碼都會在工作線程中,按照他到達的時間節(jié)點有序的進入任務(wù)隊列,而且任務(wù)隊列中的異步任務(wù)又分為【宏任務(wù)】和【微任務(wù)】。

          舉個例子:

          在了解【宏任務(wù)】和【微任務(wù)】前,還是哪生活中的實際場景舉個例子:

          比如: 在去銀行辦理業(yè)務(wù)時,每個人都需要在進入銀行時找到取票機進行取票,這個操作會把來辦理業(yè)務(wù)的人按照取票的順序排成一個有序的隊列。假設(shè)銀行只開通了一個辦事窗口,窗口的工作人員會按照排隊的順序進行叫號,到達號碼的人就可以前往窗口辦理業(yè)務(wù),在第一個人辦理業(yè)務(wù)的過程中,第二個以后的人都需要進行等待。

          這個場景與JavaScript的異步任務(wù)隊列執(zhí)行場景是一模一樣的,如果把每個辦業(yè)務(wù)的人當作JavaScript中的每一個異步的任務(wù),那么取號就相當于將異步任務(wù)放入任務(wù)隊列。銀行的窗口就相當于【函數(shù)執(zhí)行棧】,在叫號時代表將當前隊列的第一個任務(wù)放入【函數(shù)執(zhí)行棧】運行。這時可能每個人在窗口辦理的業(yè)務(wù)內(nèi)容各不相同,比如第一個人僅僅進行開卡的操作,這樣銀行工作人員就會為其執(zhí)行開卡流程,這就相當于執(zhí)行異步任務(wù)內(nèi)部的代碼。

          如果第一個人的銀行卡開通完畢,銀行的工作人員不會立即叫第二個人過來,而是會詢問第一個人,“您是否需要為剛才開通的卡辦理一些增值業(yè)務(wù),比如做個活期儲蓄。”,這時相當于在原始開卡的業(yè)務(wù)流程中臨時追加了一個新的任務(wù),按照JavaScript的執(zhí)行順序,這個人的新任務(wù)應該回到取票機拿取一張新的號碼,并且在隊尾重新排隊,這樣工作的話辦事效率就會急劇下降。所以銀行實際的做法是在叫下一個人辦理業(yè)務(wù)前,如果前面的人臨時有新的業(yè)務(wù)要辦理,工作人員會繼續(xù)為其辦理業(yè)務(wù),直到這個人的所有事情都辦理完畢。

          從取卡到辦理追加業(yè)務(wù)完成的這個過程,就是微任務(wù)的實際體現(xiàn)。在JavaScript運行環(huán)境中,包括主線程代碼在內(nèi),可以理解為所有的任務(wù)內(nèi)部都存在一個微任務(wù)隊列,在每下一個宏任務(wù)執(zhí)行前,事件循環(huán)系統(tǒng)都會先檢測當前的代碼塊中是否包含已經(jīng)注冊的微任務(wù),并將隊列中的微任務(wù)優(yōu)先執(zhí)行完畢,進而執(zhí)行下一個宏任務(wù)。所以實際的任務(wù)隊列的結(jié)構(gòu)是這樣的,如圖:

          宏任務(wù)與微任務(wù)的介紹

          由上述內(nèi)容得知JavaScript中存在兩種異步任務(wù),一種是宏任務(wù)一種是微任務(wù),他們的特點如下:

          宏任務(wù)

          宏任務(wù)是JavaScript中最原始的異步任務(wù),包括setTimeoutsetIntervalAJAX等,在代碼執(zhí)行環(huán)境中按照同步代碼的順序,逐個進入工作線程掛起,再按照異步任務(wù)到達的時間節(jié)點,逐個進入異步任務(wù)隊列,最終按照隊列中的順序進入函數(shù)執(zhí)行棧進行執(zhí)行。

          微任務(wù)

          微任務(wù)是隨著ECMA標準升級提出的新的異步任務(wù),微任務(wù)在異步任務(wù)隊列的基礎(chǔ)上增加了【微任務(wù)】的概念,每一個宏任務(wù)執(zhí)行前,程序會先檢測其中是否有當次事件循環(huán)未執(zhí)行的微任務(wù),優(yōu)先清空本次的微任務(wù)后,再執(zhí)行下一個宏任務(wù),每一個宏任務(wù)內(nèi)部可注冊當次任務(wù)的微任務(wù)隊列,再下一個宏任務(wù)執(zhí)行前運行,微任務(wù)也是按照進入隊列的順序執(zhí)行的。

          總結(jié)

          JavaScript的運行環(huán)境中,代碼的執(zhí)行流程是這樣的:

          1. 默認的同步代碼按照順序從上到下,從左到右運行,運行過程中注冊本次的微任務(wù)和后續(xù)的宏任務(wù):
          2. 執(zhí)行本次同步代碼中注冊的微任務(wù),并向任務(wù)隊列注冊微任務(wù)中包含的宏任務(wù)和微任務(wù)
          3. 將下一個宏任務(wù)開始前的所有微任務(wù)執(zhí)行完畢
          4. 執(zhí)行最先進入隊列的宏任務(wù),并注冊當次的微任務(wù)和后續(xù)的宏任務(wù),宏任務(wù)會按照當前任務(wù)隊列的隊尾繼續(xù)向下排列

          常見的宏任務(wù)和微任務(wù)劃分

          宏任務(wù)

          #

          瀏覽器

          Node

          I/O

          ?

          ?

          setTimeout

          ?

          ?

          setInterval

          ?

          ?

          setImmediate

          ?

          ?

          requestAnimationFrame

          ?

          ?

          有些地方會列出來UI Rendering,說這個也是宏任務(wù),可是在讀了HTML規(guī)范文檔以后,發(fā)現(xiàn)這很顯然是和微任務(wù)平行的一個操作步驟 requestAnimationFrame姑且也算是宏任務(wù)吧,requestAnimationFrame在MDN的定義為,下次頁面重繪前所執(zhí)行的操作,而重繪也是作為宏任務(wù)的一個步驟來存在的,且該步驟晚于微任務(wù)的執(zhí)行

          微任務(wù)

          #

          瀏覽器

          Node

          process.nextTick

          ?

          ?

          MutationObserver

          ?

          ?

          Promise.then catch finally

          ?

          ?

          經(jīng)典筆試題

          代碼輸出順序問題1

          setTimeout(function() {console.log('timer1')}, 0)
           
          requestAnimationFrame(function(){
              console.log('UI update')
          })
           
          setTimeout(function() {console.log('timer2')}, 0)
           
          new Promise(function executor(resolve) {
              console.log('promise 1')
              resolve()
              console.log('promise 2')
          }).then(function() {
              console.log('promise then')
          })
           
          console.log('end')

          解析:

          本案例輸出的結(jié)果為:猜對我就告訴你,先思考,猜對之后結(jié)合運行結(jié)果分析。

          按照同步先行,異步靠后的原則,閱讀代碼時,先分析同步代碼和異步代碼,Promise對象雖然是微任務(wù),但是new Promise時的回調(diào)函數(shù)是同步執(zhí)行的,所以優(yōu)先輸出promise 1 和 promise 2。

          resolve執(zhí)行時Promise對象的狀態(tài)變更為已完成,所以then函數(shù)的回調(diào)被注冊到微任務(wù)事件中,此時并不執(zhí)行,所以接下來應該輸出end

          同步代碼執(zhí)行結(jié)束后,觀察異步代碼的宏任務(wù)和微任務(wù),在本次的同步代碼塊中注冊的微任務(wù)會優(yōu)先執(zhí)行,參考上文中描述的列表,Promise為微任務(wù),setTimeoutrequestAnimationFrame為宏任務(wù),所以Promise的異步任務(wù)會在下一個宏任務(wù)執(zhí)行前執(zhí)行,所以promise then是第四個輸出的結(jié)果。

          接下來參考setTimeoutrequestAnimationFrame兩個宏任務(wù),這里的運行結(jié)果是多種情況。如果三個宏任務(wù)都為setTimeout的話會按照代碼編寫的順序執(zhí)行宏任務(wù),而中間包含了一個requestAnimationFrame ,這里就要學習一下他們的執(zhí)行時機了。setTimeout是在程序運行到setTimeout時立即注冊一個宏任務(wù),所以兩個setTimeout的順序一定是固定的timer1timer2會按照順序輸出。而requestAnimationFrame是請求下一次重繪事件,所以他的執(zhí)行頻率要參考瀏覽器的刷新率。

          參考如下代碼:

          let i = 0;
          let d = new Date().getTime()
          let d1 = new Date().getTime()
          function loop(){
            d1 = new Date().getTime()
            i++
            //當間隔時間超過1秒時執(zhí)行
            if((d1-d)>=1000){
              d = d1
              console.log(i)
              i = 0
              console.log('經(jīng)過了1秒')
            }
            requestAnimationFrame(loop)
          }
          loop()

          該代碼在瀏覽器運行時,控制臺會每間隔1秒進行一次輸出,輸出的i就是loop函數(shù)執(zhí)行的次數(shù),如下圖:

          這個輸出意味著requestAnimationFrame函數(shù)的執(zhí)行頻率是每秒鐘60次左右,他是按照瀏覽器的刷新率來進行執(zhí)行的,也就是當屏幕刷新一次時該函數(shù)就會觸發(fā)一次,相當于運行間隔是16毫秒左右。

          繼續(xù)參考下列代碼:

          let i = 0;
          let d = new Date().getTime()
          let d1 = new Date().getTime()
          
          function loop(){
            d1 = new Date().getTime()
            i++
            if((d1-d)>=1000){
              d = d1
              console.log(i)
              i = 0
              console.log('經(jīng)過了1秒')
            }
            setTimeout(loop,0)
          }
          loop()

          該代碼結(jié)構(gòu)與上面的案例類似,循環(huán)是采用setTimeout進行控制的,所以參考運行結(jié)果,如圖:


          根據(jù)運行結(jié)果得知,setTimeout(fn,0)的執(zhí)行頻率是每秒執(zhí)行200次左右,所以他的間隔是5毫秒左右。

          由于這兩個異步的宏任務(wù)出發(fā)時機和執(zhí)行頻率不同,會導致三個宏任務(wù)的觸發(fā)結(jié)果不同,如果我們打開網(wǎng)頁時,恰好趕上5毫秒內(nèi)執(zhí)行了網(wǎng)頁的重繪事件,requestAnimationFrame在工作線程中就會到達觸發(fā)時機優(yōu)先進入任務(wù)隊列,所以此時會輸出:UI update->timer1->timer2

          而當打開網(wǎng)頁時上一次的重繪剛結(jié)束,下一次重繪的觸發(fā)是16毫秒后,此時setTimeout注冊的兩個任務(wù)在工作線程中就會優(yōu)先到達觸發(fā)時機,這時輸出的結(jié)果是:timer1->timer2->UI update

          所以此案例的運行結(jié)果如下2圖所示:


          代碼輸出順序問題2

          document.addEventListener('click', function(){
              Promise.resolve().then(()=> console.log(1));
              console.log(2);
          })
           
          document.addEventListener('click', function(){
              Promise.resolve().then(()=> console.log(3));
              console.log(4);
          })

          解析:仍然是猜對了告訴你哈~,先運行一下試試吧。

          這個案例代碼簡單易懂,但是很容易引起錯誤答案的出現(xiàn)。由于該事件是直接綁定在document上的,所以點擊網(wǎng)頁就會觸發(fā)該事件,在代碼運行時相當于按照順序注冊了兩個點擊事件,兩個點擊事件會被放在工作線程中實時監(jiān)聽觸發(fā)時機,當元素被點擊時,兩個事件會按照先后的注冊順序放入異步任務(wù)隊列中進行執(zhí)行,所以事件1和事件2會按照代碼編寫的順序觸發(fā)。

          這里就會導致有人分析出錯誤答案:2,4,1,3。

          為什么不是2,4,1,3呢?由于事件執(zhí)行時并不會阻斷JS默認代碼的運行,所以事件任務(wù)也是異步任務(wù),并且是宏任務(wù),所以兩個事件相當于按順序執(zhí)行的兩個宏任務(wù)。

          這樣就會分出兩個運行環(huán)境,第一個事件執(zhí)行時,console.log(2);是第一個宏任務(wù)中的同步代碼,所以他會立即執(zhí)行,而Promise.resolve().then(()=> console.log(1));屬于微任務(wù),他會在下一個宏任務(wù)觸發(fā)前執(zhí)行,所以這里輸出2后會直接輸出1.

          而下一個事件的內(nèi)容是相同道理,所以輸出順序為:2,1,4,3。

          總結(jié)

          關(guān)于事件循環(huán)模型今天就介紹到這里,在NodeJS中的事件循環(huán)模型和瀏覽器中是不一樣的,本文是以瀏覽器的事件循環(huán)模型為基礎(chǔ)進行介紹,事件循環(huán)系統(tǒng)在JavaScript異步編程中占據(jù)的比重是非常大的,在工作中可使用場景也是眾多的,掌握了事件循環(huán)模型就相當于,異步編程的能力上升了一個新的高度。


          主站蜘蛛池模板: 日韩一区二区三区在线观看| 亚洲美女一区二区三区| 亚洲欧洲一区二区三区| 性色AV 一区二区三区| 亚洲国产成人精品无码一区二区 | 无码8090精品久久一区| 中文字幕在线一区二区在线 | av无码精品一区二区三区四区 | 三级韩国一区久久二区综合| 亚洲欧美日韩一区二区三区在线| 亚洲一区二区三区影院| 久久99精品免费一区二区| 亚洲区精品久久一区二区三区| 日韩亚洲一区二区三区| 国产午夜毛片一区二区三区| 国产麻豆剧果冻传媒一区| 在线|一区二区三区四区| 国产乱码精品一区二区三区中| 日韩精品无码中文字幕一区二区| 狠狠色婷婷久久一区二区三区| 国产精品一区二区三区免费| 日本精品一区二区三区在线视频 | 在线精品国产一区二区三区| 视频一区二区精品的福利| 精品国产一区二区三区www| 无码人妻精品一区二区三18禁| 国产AV午夜精品一区二区三| 国产99精品一区二区三区免费 | 精品深夜AV无码一区二区老年| 3d动漫精品啪啪一区二区中文 | 激情久久av一区av二区av三区| 日韩av片无码一区二区不卡电影| 亚洲国产系列一区二区三区 | 色偷偷av一区二区三区| 亚洲一区中文字幕在线电影网| 一区二区无码免费视频网站 | 91视频一区二区| 亚洲一区二区三区写真| 在线精品日韩一区二区三区| 国产a久久精品一区二区三区| 国产无码一区二区在线|