面试官:请实现一个通用函数把 callback 转成 promise
1. 前言
大家好,我是若川。最近組織了源碼共讀活動,感興趣的可以加我微信 ruochuan12 參與,或者在公眾號:若川視野,回復"源碼"參與,每周大家一起學習200行左右的源碼,共同進步。已進行三個月了,很多小伙伴表示收獲頗豐。
想學源碼,極力推薦之前我寫的《學習源碼整體架構系列》 包含jQuery、underscore、lodash、vuex、sentry、axios、redux、koa、vue-devtools、vuex4、koa-compose、vue-next-release、vue-this、create-vue、玩具vite等10余篇源碼文章。
本文倉庫 remote-git-tags-analysis,求個star^_^[1]
我們經常會在本地git倉庫切換tags,或者git倉庫切換tags。那么我們是否想過如果獲取tags呢。本文就是學習 remote-git-tags 這個22行代碼的源碼庫。源碼不多,但非常值得我們學習。
閱讀本文,你將學到:
1.?Node?加載采用什么模塊 2.?獲取?git?倉庫所有?tags?的原理 3.?學會調試看源碼 4.?學會面試高頻考點?promisify?的原理和實現 5.?等等剛開始先不急著看上千行、上萬行的源碼。源碼長度越長越不容易堅持下來。看源碼講究循序漸進。比如先從自己會用上的百來行的開始看。
我之前在知乎上回答過類似問題。
一年內的前端看不懂前端框架源碼怎么辦?
簡而言之,看源碼
循序漸進 借助調試 理清主線 查閱資料 總結記錄2. 使用
import?remoteGitTags?from?'remote-git-tags';console.log(await?remoteGitTags('https://github.com/lxchuan12/blog.git')); //=>?Map?{'3.0.5'?=>?'6020cc35c027e4300d70ef43a3873c8f15d1eeb2',?…}3. 源碼
Get tags from a remote Git repo
這個庫的作用是:從遠程倉庫獲取所有標簽。
原理:通過執行 git ls-remote --tags repoUrl (倉庫路徑)獲取 tags
應用場景:可以看有哪些包依賴的這個包。npm 包描述信息[2]
其中一個比較熟悉的是npm-check-updates[3]
npm-check-updates 將您的 package.json 依賴項升級到最新版本,忽略指定的版本。
還有場景可能是 github 中獲取所有 tags 信息,切換 tags 或者選定 tags 發布版本等,比如微信小程序版本。
看源碼前先看 package.json 文件。
3.1 package.json
//?package.json {//?指定?Node?以什么模塊加載,缺省時默認是?commonjs"type":?"module","exports":?"./index.js",//?指定?nodejs?的版本"engines":?{"node":?"^12.20.0?||?^14.13.1?||?>=16.0.0"},"scripts":?{"test":?"xo?&&?ava"} }眾所周知,Node 之前一直是 CommonJS 模塊機制。Node 13 添加了對標準 ES6 模塊的支持。
告訴 Node 它要加載的是什么模塊的最簡單的方式,就是將信息編碼到不同的擴展名中。如果是 .mjs 結尾的文件,則 Node 始終會將它作為 ES6 模塊來加載。如果是 .cjs 結尾的文件,則 Node 始終會將它作為 CommonJS 模塊來加載。
對于以 .js 結尾的文件,默認是 CommonJS 模塊。如果同級目錄及所有目錄有 package.json 文件,且 type 屬性為module 則使用 ES6 模塊。type 值為 commonjs 或者為空或者沒有 package.json 文件,都是默認 commonjs 模塊加載。
關于 Node 模塊加載方式,在《JavaScript權威指南第7版》16.1.4 Node 模塊 小節,有更加詳細的講述。此書第16章都是講述Node,感興趣的讀者可以進行查閱。
3.2 調試源碼
#?推薦克隆我的項目,保證與文章同步,同時測試文件齊全 git?clone?https://github.com/lxchuan12/remote-git-tags-analysis.git #?npm?i?-g?yarn cd?remote-git-tags?&&?yarn #?VSCode?直接打開當前項目 #?code?.#?或者克隆官方項目 git?clone?https://github.com/sindresorhus/remote-git-tags.git #?npm?i?-g?yarn cd?remote-git-tags?&&?yarn #?VSCode?直接打開當前項目 #?code?.用最新的VSCode 打開項目,找到 package.json 的 scripts 屬性中的 test 命令。鼠標停留在test命令上,會出現 運行命令 和 調試命令 的選項,選擇 調試命令 即可。
調試如圖所示:
調試如圖所示VSCode 調試 Node.js 說明如下圖所示:
VSCode 調試 Node.js 說明更多調試詳細信息,可以查看這篇文章新手向:前端程序員必學基本技能——調試JS代碼。
跟著調試,我們來看主文件。
3.3 主文件僅有22行源碼
//?index.js import?{promisify}?from?'node:util'; import?childProcess?from?'node:child_process';const?execFile?=?promisify(childProcess.execFile);export?default?async?function?remoteGitTags(repoUrl)?{const?{stdout}?=?await?execFile('git',?['ls-remote',?'--tags',?repoUrl]);const?tags?=?new?Map();for?(const?line?of?stdout.trim().split('\n'))?{const?[hash,?tagReference]?=?line.split('\t');//?Strip?off?the?indicator?of?dereferenced?tags?so?we?can?override?the//?previous?entry?which?points?at?the?tag?hash?and?not?the?commit?hash//?`refs/tags/v9.6.0^{}`?→?`v9.6.0`const?tagName?=?tagReference.replace(/^refs\/tags\//,?'').replace(/\^{}$/,?'');tags.set(tagName,?hash);}return?tags; }源碼其實一眼看下來就很容易懂。
3.4 git ls-remote --tags
支持遠程倉庫鏈接。
git ls-remote 文檔[4]
如下圖所示:
ls-remote獲取所有tags git ls-remote --tags https://github.com/vuejs/vue-next.git
把所有 tags 和對應的 hash值 存在 Map 對象中。
3.5 node:util
Node 文檔[5]
Core modules can also be identified using the node: prefix, in which case it bypasses the require cache. For instance, require('node:http') will always return the built in HTTP module, even if there is require.cache entry by that name.
也就是說引用 node 原生庫可以加 node: 前綴,比如 import util from 'node:util'
看到這,其實原理就明白了。畢竟只有22行代碼。接著講述 promisify。
4. promisify
源碼中有一段:
const?execFile?=?promisify(childProcess.execFile);promisify 可能有的讀者不是很了解。
接下來重點講述下這個函數的實現。
promisify函數是把 callback 形式轉成 promise 形式。
我們知道 Node.js 天生異步,錯誤回調的形式書寫代碼?;卣{函數的第一個參數是錯誤信息。也就是錯誤優先。
我們換個簡單的場景來看。
4.1 簡單實現
假設我們有個用JS加載圖片的需求。我們從 這個網站[6] 找來圖片。
examples const?imageSrc?=?'https://www.themealdb.com/images/ingredients/Lime.png';function?loadImage(src,?callback)?{const?image?=?document.createElement('img');image.src?=?src;image.alt?=?'公眾號若川視野專用圖?';image.style?=?'width:?200px;height:?200px';image.onload?=?()?=>?callback(null,?image);image.onerror?=?()?=>?callback(new?Error('加載失敗'));document.body.append(image); }我們很容易寫出上面的代碼,也很容易寫出回調函數的代碼。需求搞定。
loadImage(imageSrc,?function(err,?content){if(err){console.log(err);return;}console.log(content); });但是回調函數有回調地獄等問題,我們接著用 promise 來優化下。
4.2 promise 初步優化
我們也很容易寫出如下代碼實現。
const?loadImagePromise?=?function(src){return?new?Promise(function(resolve,?reject){loadImage(src,?function?(err,?image)?{if(err){reject(err);return;}resolve(image);});}); }; loadImagePromise(imageSrc).then(res?=>?{console.log(res); }) .catch(err?=>?{console.log(err); });但這個不通用。我們需要封裝一個比較通用的 promisify 函數。
4.3 通用 promisify 函數
function?promisify(original){function?fn(...args){return?new?Promise((resolve,?reject)?=>?{args.push((err,?...values)?=>?{if(err){return?reject(err);}resolve(values);});//?original.apply(this,?args);Reflect.apply(original,?this,?args);});}return?fn; }const?loadImagePromise?=?promisify(loadImage); async?function?load(){try{const?res?=?await?loadImagePromise(imageSrc);console.log(res);}catch(err){console.log(err);} } load();需求搞定。這時就比較通用了。
這些例子在我的倉庫存放在 examples 文件夾中??梢钥寺∠聛?#xff0c;npx http-server .跑服務,運行試試。
examples跑失敗的結果可以把 imageSrc 改成不存在的圖片即可。
promisify 可以說是面試高頻考點。很多面試官喜歡考此題。
接著我們來看 Node.js 源碼中 promisify 的實現。
4.4 Node utils promisify 源碼
github1s node utils 源碼[7]
源碼就暫時不做過多解釋,可以查閱文檔。結合前面的例子,其實也容易理解。
utils promisify 文檔[8]
const?kCustomPromisifiedSymbol?=?SymbolFor('nodejs.util.promisify.custom'); const?kCustomPromisifyArgsSymbol?=?Symbol('customPromisifyArgs');let?validateFunction;function?promisify(original)?{//?Lazy-load?to?avoid?a?circular?dependency.if?(validateFunction?===?undefined)({?validateFunction?}?=?require('internal/validators'));validateFunction(original,?'original');if?(original[kCustomPromisifiedSymbol])?{const?fn?=?original[kCustomPromisifiedSymbol];validateFunction(fn,?'util.promisify.custom');return?ObjectDefineProperty(fn,?kCustomPromisifiedSymbol,?{value:?fn,?enumerable:?false,?writable:?false,?configurable:?true});}//?Names?to?create?an?object?from?in?case?the?callback?receives?multiple//?arguments,?e.g.?['bytesRead',?'buffer']?for?fs.read.const?argumentNames?=?original[kCustomPromisifyArgsSymbol];function?fn(...args)?{return?new?Promise((resolve,?reject)?=>?{ArrayPrototypePush(args,?(err,?...values)?=>?{if?(err)?{return?reject(err);}if?(argumentNames?!==?undefined?&&?values.length?>?1)?{const?obj?=?{};for?(let?i?=?0;?i?<?argumentNames.length;?i++)obj[argumentNames[i]]?=?values[i];resolve(obj);}?else?{resolve(values[0]);}});ReflectApply(original,?this,?args);});}ObjectSetPrototypeOf(fn,?ObjectGetPrototypeOf(original));ObjectDefineProperty(fn,?kCustomPromisifiedSymbol,?{value:?fn,?enumerable:?false,?writable:?false,?configurable:?true});return?ObjectDefineProperties(fn,ObjectGetOwnPropertyDescriptors(original)); }promisify.custom?=?kCustomPromisifiedSymbol;5. ES6+ 等知識
文中涉及到了Map、for of、正則、解構賦值。
還有涉及封裝的 ReflectApply、ObjectSetPrototypeOf、ObjectDefineProperty、ObjectGetOwnPropertyDescriptors 等函數都是基礎知識。
這些知識可以查看esma規范[9],或者阮一峰老師的《ES6 入門教程》[10] 等書籍。
6. 總結
一句話簡述 remote-git-tags 原理:使用Node.js的子進程 child_process 模塊的execFile方法執行 git ls-remote --tags repoUrl 獲取所有 tags 和 tags 對應 hash 值 存放在 Map 對象中。
文中講述了我們可以循序漸進,借助調試、理清主線、查閱資料、總結記錄的流程看源碼。
通過 remote-git-tags 這個22行代碼的倉庫,學會了 Node 加載采用什么模塊,知道了原來 git ls-remote --tags支持遠程倉庫,學到了面試高頻考點 promisify 函數原理和源碼實現,鞏固了一些 ES6+ 等基礎知識。
建議讀者克隆我的倉庫[11]動手實踐調試源碼學習。
后續也可以看看 es6-promisify[12] 這個庫的實現。
最后可以持續關注我@若川。歡迎加我微信 ruochuan12 交流,參與 源碼共讀 活動,大家一起學習源碼,共同進步。
參考資料
[1]
本文倉庫 remote-git-tags-analysis,求個star^_^: https://github.com/lxchuan12/remote-git-tags-analysis.git
[2]npm 包描述信息: https://npm.im/remote-git-tags
[3]npm-check-updates: https://www.npmjs.com/package/npm-check-updates
[4]git ls-remote 文檔: https://git-scm.com/docs/git-ls-remote
[5]Node 文檔: https://nodejs.org/dist/latest-v16.x/docs/api/modules.html
[6]這個網站: https://www.themealdb.com/api.php
[7]github1s node utils 源碼: https://github1s.com/nodejs/node/blob/master/lib/internal/util.js#L343
[8]utils promisify 文檔: http://nodejs.cn/api/util/util_promisify_original.html
[9]esma規范: https://yanhaijing.com/es5/
[10]《ES6 入門教程》: https://es6.ruanyifeng.com/
[11]我的倉庫: https://github.com/lxchuan12/remote-git-tags-analysis.git
[12]es6-promisify: https://github.com/mikehall314/es6-promisify
最近組建了一個江西人的前端交流群,如果你是江西人可以加我微信?ruochuan12?私信 江西 拉你進群。
推薦閱讀
1個月,200+人,一起讀了4周源碼
我歷時3年才寫了10余篇源碼文章,但收獲了100w+閱讀
老姚淺談:怎么學JavaScript?
我在阿里招前端,該怎么幫你(可進面試群)
·················?若川簡介?·················
你好,我是若川,畢業于江西高校。現在是一名前端開發“工程師”。寫有《學習源碼整體架構系列》10余篇,在知乎、掘金收獲超百萬閱讀。
從2014年起,每年都會寫一篇年度總結,已經寫了7篇,點擊查看年度總結。
同時,最近組織了源碼共讀活動,幫助1000+前端人學會看源碼。公眾號愿景:幫助5年內前端人走向前列。
識別上方二維碼加我微信、拉你進源碼共讀群
今日話題
略。歡迎分享、收藏、點贊、在看我的公眾號文章~
總結
以上是生活随笔為你收集整理的面试官:请实现一个通用函数把 callback 转成 promise的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: [html] 精确获取页面元素位置的方
- 下一篇: 前端学习(2991):vue+eleme