从node的promisify中学到将callback转化为promise

780 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1天,点击查看活动详情

1. 引言

最近学习热情不够之前高涨,趁着官方的六月更文活动,积极学习,努力产出,正向刺激自己不断提升自我,加油!!!ヾ(◍°∇°◍)ノ゙

2. 准备工作

2.1 拉取源码

并不是源码阅读的第一步,但本地阅读源码体验确实会比在github上直接看体验要好些,当然只是个人感受。这里我使用的是若川大佬提供的代码仓库github

3. 关于Node

Node13后添加对标准ES6模块的支持。可以使用不同的文件后缀名来告知Node加载的是什么模块,如.mjs表示作为ES6模块来加载,.cjs表示作为CommonJS模块来加载;此外对于.js结尾的文件,默认情况下是CommonJS模块,若同级目录及所有项目目录存在package.json 文件,可在package.json文件中配置type属性为module表示使用ES6模块。

 // package.json
 {
   // 指定 Node 以什么类型模块加载,缺省为 commonjs
     "type": "module"
 }

4. 源码解析

项目就一个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;
 }

remoteGitTags

  1. 首先看remoteGitTags函数所完成的工作,函数接受一个参数repoUrl标识仓库的url,支持远程仓库;await execFile('git', ['ls-remote', '--tags', repoUrl]);,获取到shell命令git ls-remote --tags repoUrl的执行结果,暨获取到指定远程仓库的tags列表。执行下git ls-remote --tags https://github.com/vuejs/core.git,可以看到返回结果如下(截取部分):

     d8c1536ead56429f21233bf1fe984ceb3e273fe9        refs/tags/v3.0.0
     7d2ae08277b448fd1ce2ef9ba18f854d0dcd27d4        refs/tags/v3.0.0-alpha.0
     1bb1271b5e78cc4b446ce363de73e68db74a6c89        refs/tags/v3.0.0-alpha.1
     de81faf00ab22c68ae2a0df14c9e28547f35f544        refs/tags/v3.0.0-alpha.10
     7402951d945b4e49474661594992a95f878de3f0        refs/tags/v3.0.0-alpha.11
     1d9f8fc979a1c62e381a89eb26d8659a2c527f09        refs/tags/v3.0.0-alpha.12
     11654a6e50839519b67efc571b696abd13a4d180        refs/tags/v3.0.0-alpha.13
    
  2. 之后是遍历版本和Hash放到一个Map对象中,并作为一个返回值返回

    • for (const line of stdout.trim().split('\n'))遍历取出每一行的数据
    • const [hash, tagReference] = line.split('\t');取出hash值和tagReference
    • const tagName = tagReference.replace(/^refs/tags//, '').replace(/^{}$/, '');使用 两次替换,最终获得版本

5. promisify

可以注意到在代码的一开始有一个全局定义

 const execFile = promisify(childProcess.execFile);

其中promisify函数将callback转化为promise,也是全文的重点。

3.1 回调函数

图片加载

 const imageSrc = 'https://www.themealdb.com/images/ingredients/Lime.png'; // 定义默认图片
 ​
 function loadImage(src, callback) {
     const image = document.createElement('img');
     image.src = src; // 设置src属性
     image.style = 'width: 200px;height: 200px'; // 设置样式属性
     image.onload = () => callback(null, image);
     image.onerror = () => callback(new Error('加载出错')); 
     document.body.append(image); // 将DOM元素添加到body中
 }
  1. 定义图片加载函数,接收两个参数,一个是图片地址,一个是图片回调函数
  2. document.createElement('img') 创建新的DOM元素
  3. callback(null, image) 设置onload事件时调用callback函数,onload<img>元素事件,当图片加载完成时触发
  4. callback(new Error('加载出错')) 设置onerror事件时调用callback函数,onerror也是<img>中的事件,当图片加载出错时触发

回调函数事件处理

 loadImage(imageSrc, function (err, image) {
     if (err) { // 出错时,打印错误信息
         console.log(err);
         return;
     }
     console.log(image); // 成功时打印<img>元素
 });

调试时使用了若川大佬提供的代码库,所以选择examples/index-1.html文件,然后直接在VSCode中运行,简单步骤如下

  1. 选择需要调试运行的html文件,选择运行与调试:

    屏幕截图(25).png

  2. 选择运行的浏览器环境:

    屏幕截图(27).png

  3. VSCode会自动打开所选择的浏览器执行

    屏幕截图(28).png

  4. VSCode中设置的断点也会在浏览器中效

    屏幕截图(29).png

3.2 promise

可以浅试一下将上面的回调方式转化为promise的方式

 const loadImagePromise = function (src) {
   return new Promise(function (resolve, reject) {
     loadImage(src, function (err, image) {
       if (err) {
         reject(err); // 使用reject返回错误,可被.catch方法捕获到
       }
       resolve(image); // 使用resolve返回结果,可被.then方法获取到
     });
   });
 };
 loadImagePromise(imageSrc)
   .then((data) => {
     console.log(data);
   })
   .catch((err) => {
     console.error(err);
   });

对于每一个回调函数都可以使用这种方式转化为Promise,但是出于逻辑复用的考虑,一般会将上面转化修改为更加通用的方式,promisify函数就是这样的一个通用方法。

我们可以先尝试修改上述代码为通用的方式,以便于理解promisify原理

 const promisify = function (func) {
   return function (...args) {
     return new Promise(function (resolve, reject) {
       func.call(this, args, function (err, ...values) {
         if (err) {
           reject(err);
         }
         resolve(values);
       });
     });
   };
 };

基本原理其实并不难,promisify本质上就是一个返回值为函数的函数,并且返回的函数将回调方式转化为Promise方式,但这里存在一点小问题,就是如果使用fun.call()的话虽然在这个例子中能正常运行,但当需要传递多个参数时会失效。

看了下若川大佬的实现

 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;
 }

后知后觉我可以使用apply呀,不就可以传参数数组了嘛,优化下之前的代码👇

 const promisify = function (func) {
   return function (...args) {
     return new Promise(function (resolve, reject) {
       args.push(function (err, ...values) {
         if (err) {
           reject(err);
         }
         resolve(values);
       });
       func.apply(this, args);
     });
   };
 };

顺便还学到了一个新的对象ReflectReflect对象是一个内置对象(不可以被构造出,不需要使用new创建),提供了一系列拦截JavaScript操作的方法,可以简单理解为以比较非常规的手段操作对象,这里只简单说下代码中出现的Reflect.apply()方法,其余更多函数可参考MDN

Reflect.apply

静态方法,通过指定参数列表发起对目标函数的调用,和apply方法类似,但是该方法接受三个参数

  1. function 需要调用的目标函数(比apply多的一个参数)
  2. this 函数调用时绑定的this对象
  3. args 函数调用时传入的实参列表,类数组对象

6. 总结

通过本次源码阅读也获得了以下几点收获

  1. 了解到node的模块加载机制
  2. 学到一个新的git命令git ls-remote --tags repoUrl
  3. 学到了如何将回调函数异步调用转化为Promise方式,以及如何进一步封装为更具有普适性的promisify函数
  4. 巩固了call()apply()两个方法的异同
  5. 学到一个新的调用函数的方法Reflect.apply

继续加油呀👍!!!

7. 参考文章

《从22行有趣的源码库中,我学到了 callback promisify 化的 Node.js 源码实现》

\