編寫高質量可維護的代碼:異步優化

{"type":"doc","content":[{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"前言"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在現在前端開發中,異步操作的頻次已經越來越高了,特別對於數據接口請求和定時器的使用,使得我們不得不關注異步在業務中碰到的場景,以及對異步的優化。錯誤的異步處理可能會帶來很多問題,諸如頁面渲染、重複加載等問題。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面我們就先簡單的從 JavaScript 中有大致的哪幾種異步類型爲切入點,然後再列舉一些業務中我們會碰到的場景來逐個分析下,我們該如何解決。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"異步實現種類"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先關於異步實現的方式上大致有如下幾種:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"callback"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"callback 即回調函數。這傢伙出現很早很早了,他其實是處理異步的基本方法。並且回調的概念不單單出現在 JavaScript,你也會在 Java 或者 C# 等後端語言中也能找到他的影子。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"回調函數簡單的說其實就是給另外一個寄主函數作爲傳參的函數。在寄主函數執行完成或者執行到特定階段之後觸發調用回調函數並執行,然後把執行結果再返回給寄主函數的過程。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"比如我們熟悉的 setTimeout 或者 React 中的 setState 的第二個方法都是以回調函數方式去解決異步的實現。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"setTimeout(() => {\n   \/\/等待0.2s之後再做具體的業務操作\n   this.doSomething();\n}, 200);\nthis.setState({\n  count: res.count,\n}, () => {\n  \/\/在更新完count之後再做具體的業務操作\n  this.doSomething();\n});"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"Promise"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Promise 是個好東西,有了它之後我們可以對異步進行很多操作,並且可以把異步以鏈式的方式進行操作。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其實在 JQuery 中的 deferred 和它就有點像,都是採用回調函數的解決方案,都可以做鏈式調用,但是在 Promise 中增加了錯誤的 catch 方法可以更加方便的處理異常場景,並且它內置狀態(resolve, reject,pending),狀態只能由 pending 變爲另外兩種的其中一種,且改變後不可逆也不可再度修改。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"let promise = new Promise((resolve, reject) => { \n  reject(\"對不起,你不是我的菜\");\n});\npromise.then((data) => {\nconsole.log('第一次success' + data);\n  return '第一次success' + data\n},(error) => {\nconsole.log(error) }\n).then((data2) => {\n  console.log('第二次success' + data2);\n},(error2) => { \n  console.log(error2) }\n).catch((e) => {\n  console.log('抓到錯誤啦' + e);\n});"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"await\/async"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"await\/async 其實是 Promise 的一種升級版本,使用 await\/async 調用異步的時候是從上到下,順序執行,就像在寫同步代碼一樣,這更加的符合我們編寫代碼的習慣和思維邏輯,所以容易理解。整體代碼邏輯也會更加的清晰。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"async function asyncDemoFn() {\n  const data1 = await getData1();\n  const data2 = await getData2(data1);\n  const data3 =  await getData3(data2);\n  console.log(data3)\n}\nawait asyncDemoFn()"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"generator"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"generator 中文名叫構造器,是 ES6 中的一個新東西,我相信很多人在現實的代碼中很少能接觸到它,所以它相對而言對大家來說還是比較晦澀,但是這傢伙還是很強的,簡單來說它能控制異步調用,並且其實是一個狀態機。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"function* foo() {\n  for (let i = 1; i <= 3; i++) {\n    let x = yield `等我一下唄,i = ${i}`;\n    console.log(x);\n  }\n}\nsetTimeout(() => {\n  console.log('終於輪到我了');\n}, 1);\nvar a = foo();\nconsole.log(a); \/\/ foo {}\nvar b = a.next();\nconsole.log(b); \/\/ {value: \"等我一下唄,i = 1\", done: false}\nvar c = a.next();\nconsole.log(c); \/\/ {value: \"等我一下唄,i = 2\", done: false}\nvar d = a.next();\nconsole.log(d); \/\/ {value: \"等我一下唄,i = 3\", done: false}\nvar e = a.next();\nconsole.log(e); \/\/ {value: undefined, done: true}\n\/\/ 終於輪到我了"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面代碼的函數 foo 是一個協程,它的厲害的地方就是 yield 命令。它表示執行到此處,執行權將交給其他協程。也就是說,yield 命令是異步兩個階段的分界線。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"協程遇到 yield 命令就暫停,等到執行權返回,再從暫停的地方繼續往後執行。它的最大優點,就是代碼的寫法非常像同步操作,如果去除 yield 命令,簡直一模一樣。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"再來個有點貼近點場景方式來使用下 generator。比如現在在頁面中我們需要自動的執行 checkAuth 和 checkAddress 檢查,我們就用 generator 的方式去實現自動檢查上述兩異步檢查。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"const checkAuth = () => {\n return new Promise((resolve)=>{\n setTimeout(()=>{\n resolve('checkAuth1') \n },1000)\n })\n}\nconst checkAddress = () => {\n return new Promise((resolve)=>{\n setTimeout(()=>{\n resolve('checkAddress2')\n },2000)\n })\n}\nvar steps = [checkAuth,checkAddress]\nfunction* foo(checkList) {\n for (let i = 0; i < checkList.length; i++) {\n let x = yield checkList[i]();\n console.log(x);\n }\n}\nvar stepsGen = foo(steps)\nvar run = async (gen)=>{\n var isFinnish = false\n do{\n const {done,value} = gen.next()\n console.log('done:',done)\n console.log('value:',value)\n const result = await value\n console.log('result:',result)\n \n isFinnish = done\n }while(!isFinnish)\n console.log('isFinnish:',isFinnish)\n}\nrun(stepsGen)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"種類對比"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從時間維度從早到晚:callback,Promise,generator,await\/async"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"await\/async 是目前對於異步的終極形式"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"callback 讓我們有了基本的方式去處理異步情況,Promise 告別了 callback 的回調地獄並且增加 resolve,reject 和 catch 等方法讓我們能處理不同的情況,generator 增加了對於異步的可操作性,類似一個狀態機可暫時停住多個異步的執行,然後在合適的時候繼續執行剩餘的異步調用,await\/async 讓異步調用更加語義化,並且自動執行異步"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"異步業務中碰到的場景"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"回調地獄"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在使用回調函數的時候我們可能會有這樣的場景,B 需要在 A 的返回之後再繼續調用,所以在這樣有先後關係的時候就存在了一個叫回調地獄的問題了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"getData1().then((resData1) => {\n  getData2(resData1).then((resData2) => {\n    getData3(resData2).then((resData3)=>{\n      console.log('resData3:', resData3)\n    })\n  });\n});\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"碰到這樣的情況我們可以試着用 await\/async 方式去解這種有多個深層嵌套的問題。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"async function asyncDemoFn2() {\n  const resData1 = await getData1();\n  const resData2 = await getData2(resData1);\n  const resData3 =  await getData3(resData2);\n  console.log(resData3)\n}\nawait asyncDemoFn2()\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"異步循環"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在業務中我們最最經常碰到的就是其實還是存在多個異步調用的順序問題,大致上可以分爲如下幾種:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"並行執行"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在並行執行的時候,我們可以直接使用 Promise 的 all 方法"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"Promise.all([getData1(),getData2(),getData3()]).then(res={\nconsole.log('res:',res)\n})"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"順序執行"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在順序執行中,我們可以有如下的兩種方式去做"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"使用 async\/await 配合 for"}]}]}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"const sources = [getData1,getData2,getData3]\nasync function promiseQueue() {\n  console.log('開始');\n  for (let targetSource in sources) {\n    await targetSource();\n  }\n  console.log('完成');\n};\npromiseQueue()"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":2,"normalizeStart":2},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"使用 async\/await 配合 while"}]}]}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"\/\/ getData1,getData2,getData3 都爲 promise 對象\nconst sources = [getData1,getData2,getData3]\nasync function promiseQueue() {\n let index = 0\n console.log('開始');\n while(index >=0 && index < sources.length){\n await targetSource();\n index++\n }\n console.log('完成');\n};\npromiseQueue()"}]},{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":"3","normalizeStart":"3"},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"使用 async\/await 配合 reduce"}]}]}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"\/\/ getData1,getData2,getData3 都爲 promise 對象\nconst sources = [getData1,getData2,getData3]\nsources.reduce(async (previousValue, currentValue)=>{\n  await previousValue\n  return currentValue()\n},Promise.resolve())"}]},{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":"4","normalizeStart":"4"},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","text":"使用遞歸"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":5,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"const sources = [getData1,getData2,getData3]\nfunction promiseQueue(list , index = 0) {\n const len = list.length\n console.log('開始');\n if(index >= 0 && index < len){\n list[index]().then(()=>{\n promiseQueue(list, index+1) \n })\n }\n console.log('完成');\n}\npromiseQueue(sources)"}]},{"type":"paragraph","attrs":{"indent":0,"number":5,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"結尾"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"今天只是關於異步的普通使用場景的討論,並且做了些簡單的例子。其實關於異步的使用還有很多很多複雜的使用場景。更多的奇思妙想正等着你。"}]},{"type":"horizontalrule"},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"頭圖:Unsplash"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"作者:毅軒"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原文:https:\/\/mp.weixin.qq.com\/s\/s6fVoY31MqUXrW8RPka3pA"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原文:編寫高質量可維護的代碼:異步優化"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"來源:政採雲前端團隊 - 微信公衆號 [ID:Zoo-Team]"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"轉載:著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。"}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章