面试官:请实现一个通用函数把 callback 转成 promise
1. 前言
大家好,我是若川。最近組織了源碼共讀活動,感興趣的可以加我微信 ruochuan12 參與,或者在公眾號:若川視野,回復(fù)"源碼"參與,每周大家一起學(xué)習(xí)200行左右的源碼,共同進(jìn)步。已進(jìn)行三個月了,很多小伙伴表示收獲頗豐。
想學(xué)源碼,極力推薦之前我寫的《學(xué)習(xí)源碼整體架構(gòu)系列》 包含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]
我們經(jīng)常會在本地git倉庫切換tags,或者git倉庫切換tags。那么我們是否想過如果獲取tags呢。本文就是學(xué)習(xí) remote-git-tags 這個22行代碼的源碼庫。源碼不多,但非常值得我們學(xué)習(xí)。
閱讀本文,你將學(xué)到:
1.?Node?加載采用什么模塊 2.?獲取?git?倉庫所有?tags?的原理 3.?學(xué)會調(diào)試看源碼 4.?學(xué)會面試高頻考點(diǎn)?promisify?的原理和實(shí)現(xiàn) 5.?等等剛開始先不急著看上千行、上萬行的源碼。源碼長度越長越不容易堅(jiān)持下來。看源碼講究循序漸進(jìn)。比如先從自己會用上的百來行的開始看。
我之前在知乎上回答過類似問題。
一年內(nèi)的前端看不懂前端框架源碼怎么辦?
簡而言之,看源碼
循序漸進(jìn) 借助調(diào)試 理清主線 查閱資料 總結(jié)記錄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
這個庫的作用是:從遠(yuǎn)程倉庫獲取所有標(biāo)簽。
原理:通過執(zhí)行 git ls-remote --tags repoUrl (倉庫路徑)獲取 tags
應(yīng)用場景:可以看有哪些包依賴的這個包。npm 包描述信息[2]
其中一個比較熟悉的是npm-check-updates[3]
npm-check-updates 將您的 package.json 依賴項(xiàng)升級到最新版本,忽略指定的版本。
還有場景可能是 github 中獲取所有 tags 信息,切換 tags 或者選定 tags 發(fā)布版本等,比如微信小程序版本。
看源碼前先看 package.json 文件。
3.1 package.json
//?package.json {//?指定?Node?以什么模塊加載,缺省時默認(rèn)是?commonjs"type":?"module","exports":?"./index.js",//?指定?nodejs?的版本"engines":?{"node":?"^12.20.0?||?^14.13.1?||?>=16.0.0"},"scripts":?{"test":?"xo?&&?ava"} }眾所周知,Node 之前一直是 CommonJS 模塊機(jī)制。Node 13 添加了對標(biāo)準(zhǔn) ES6 模塊的支持。
告訴 Node 它要加載的是什么模塊的最簡單的方式,就是將信息編碼到不同的擴(kuò)展名中。如果是 .mjs 結(jié)尾的文件,則 Node 始終會將它作為 ES6 模塊來加載。如果是 .cjs 結(jié)尾的文件,則 Node 始終會將它作為 CommonJS 模塊來加載。
對于以 .js 結(jié)尾的文件,默認(rèn)是 CommonJS 模塊。如果同級目錄及所有目錄有 package.json 文件,且 type 屬性為module 則使用 ES6 模塊。type 值為 commonjs 或者為空或者沒有 package.json 文件,都是默認(rèn) commonjs 模塊加載。
關(guān)于 Node 模塊加載方式,在《JavaScript權(quán)威指南第7版》16.1.4 Node 模塊 小節(jié),有更加詳細(xì)的講述。此書第16章都是講述Node,感興趣的讀者可以進(jìn)行查閱。
3.2 調(diào)試源碼
#?推薦克隆我的項(xiàng)目,保證與文章同步,同時測試文件齊全 git?clone?https://github.com/lxchuan12/remote-git-tags-analysis.git #?npm?i?-g?yarn cd?remote-git-tags?&&?yarn #?VSCode?直接打開當(dāng)前項(xiàng)目 #?code?.#?或者克隆官方項(xiàng)目 git?clone?https://github.com/sindresorhus/remote-git-tags.git #?npm?i?-g?yarn cd?remote-git-tags?&&?yarn #?VSCode?直接打開當(dāng)前項(xiàng)目 #?code?.用最新的VSCode 打開項(xiàng)目,找到 package.json 的 scripts 屬性中的 test 命令。鼠標(biāo)停留在test命令上,會出現(xiàn) 運(yùn)行命令 和 調(diào)試命令 的選項(xiàng),選擇 調(diào)試命令 即可。
調(diào)試如圖所示:
調(diào)試如圖所示VSCode 調(diào)試 Node.js 說明如下圖所示:
VSCode 調(diào)試 Node.js 說明更多調(diào)試詳細(xì)信息,可以查看這篇文章新手向:前端程序員必學(xué)基本技能——調(diào)試JS代碼。
跟著調(diào)試,我們來看主文件。
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; }源碼其實(shí)一眼看下來就很容易懂。
3.4 git ls-remote --tags
支持遠(yuǎn)程倉庫鏈接。
git ls-remote 文檔[4]
如下圖所示:
ls-remote獲取所有tags git ls-remote --tags https://github.com/vuejs/vue-next.git
把所有 tags 和對應(yīng)的 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'
看到這,其實(shí)原理就明白了。畢竟只有22行代碼。接著講述 promisify。
4. promisify
源碼中有一段:
const?execFile?=?promisify(childProcess.execFile);promisify 可能有的讀者不是很了解。
接下來重點(diǎn)講述下這個函數(shù)的實(shí)現(xiàn)。
promisify函數(shù)是把 callback 形式轉(zhuǎn)成 promise 形式。
我們知道 Node.js 天生異步,錯誤回調(diào)的形式書寫代碼。回調(diào)函數(shù)的第一個參數(shù)是錯誤信息。也就是錯誤優(yōu)先。
我們換個簡單的場景來看。
4.1 簡單實(shí)現(xiàn)
假設(shè)我們有個用JS加載圖片的需求。我們從 這個網(wǎng)站[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); }我們很容易寫出上面的代碼,也很容易寫出回調(diào)函數(shù)的代碼。需求搞定。
loadImage(imageSrc,?function(err,?content){if(err){console.log(err);return;}console.log(content); });但是回調(diào)函數(shù)有回調(diào)地獄等問題,我們接著用 promise 來優(yōu)化下。
4.2 promise 初步優(yōu)化
我們也很容易寫出如下代碼實(shí)現(xiàn)。
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 函數(shù)。
4.3 通用 promisify 函數(shù)
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 文件夾中。可以克隆下來,npx http-server .跑服務(wù),運(yùn)行試試。
examples跑失敗的結(jié)果可以把 imageSrc 改成不存在的圖片即可。
promisify 可以說是面試高頻考點(diǎn)。很多面試官喜歡考此題。
接著我們來看 Node.js 源碼中 promisify 的實(shí)現(xiàn)。
4.4 Node utils promisify 源碼
github1s node utils 源碼[7]
源碼就暫時不做過多解釋,可以查閱文檔。結(jié)合前面的例子,其實(shí)也容易理解。
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、正則、解構(gòu)賦值。
還有涉及封裝的 ReflectApply、ObjectSetPrototypeOf、ObjectDefineProperty、ObjectGetOwnPropertyDescriptors 等函數(shù)都是基礎(chǔ)知識。
這些知識可以查看esma規(guī)范[9],或者阮一峰老師的《ES6 入門教程》[10] 等書籍。
6. 總結(jié)
一句話簡述 remote-git-tags 原理:使用Node.js的子進(jìn)程 child_process 模塊的execFile方法執(zhí)行 git ls-remote --tags repoUrl 獲取所有 tags 和 tags 對應(yīng) hash 值 存放在 Map 對象中。
文中講述了我們可以循序漸進(jìn),借助調(diào)試、理清主線、查閱資料、總結(jié)記錄的流程看源碼。
通過 remote-git-tags 這個22行代碼的倉庫,學(xué)會了 Node 加載采用什么模塊,知道了原來 git ls-remote --tags支持遠(yuǎn)程倉庫,學(xué)到了面試高頻考點(diǎn) promisify 函數(shù)原理和源碼實(shí)現(xiàn),鞏固了一些 ES6+ 等基礎(chǔ)知識。
建議讀者克隆我的倉庫[11]動手實(shí)踐調(diào)試源碼學(xué)習(xí)。
后續(xù)也可以看看 es6-promisify[12] 這個庫的實(shí)現(xiàn)。
最后可以持續(xù)關(guān)注我@若川。歡迎加我微信 ruochuan12 交流,參與 源碼共讀 活動,大家一起學(xué)習(xí)源碼,共同進(jìn)步。
參考資料
[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]這個網(wǎng)站: 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規(guī)范: 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?私信 江西 拉你進(jìn)群。
推薦閱讀
1個月,200+人,一起讀了4周源碼
我歷時3年才寫了10余篇源碼文章,但收獲了100w+閱讀
老姚淺談:怎么學(xué)JavaScript?
我在阿里招前端,該怎么幫你(可進(jìn)面試群)
·················?若川簡介?·················
你好,我是若川,畢業(yè)于江西高校。現(xiàn)在是一名前端開發(fā)“工程師”。寫有《學(xué)習(xí)源碼整體架構(gòu)系列》10余篇,在知乎、掘金收獲超百萬閱讀。
從2014年起,每年都會寫一篇年度總結(jié),已經(jīng)寫了7篇,點(diǎn)擊查看年度總結(jié)。
同時,最近組織了源碼共讀活動,幫助1000+前端人學(xué)會看源碼。公眾號愿景:幫助5年內(nèi)前端人走向前列。
識別上方二維碼加我微信、拉你進(jìn)源碼共讀群
今日話題
略。歡迎分享、收藏、點(diǎn)贊、在看我的公眾號文章~
總結(jié)
以上是生活随笔為你收集整理的面试官:请实现一个通用函数把 callback 转成 promise的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: [html] 精确获取页面元素位置的方
- 下一篇: 听说你对 ES6 class 类还不是很