promisify源码解读及nodejs断点调试

996 阅读4分钟

欢迎大家参与若川大佬组织的源码共读活动: # 每周一起学习200行源码共读活动

本章节是看源码时的一些思考和总结。

nodejs调试

对于开发来说,熟悉调试工具的使用无疑是可以达到事半功倍的效果的。所以工欲善其事必先利其器,首先我们得学习怎么去调试nodejs程序。具体的使用方法大家可以看若川的文章#前端程序员必学基本技能——调试JS代码

说说调试时左边的工具栏的作用吧

image.png

变量

与标题意思一致,展示当前调用栈中的作用域和可访问的局部变量等信息。大致可分为:

  • Local:本地作用域
  • Block: 块级作用域
  • Closure: 闭包作用域
  • Module: 模块作用域
  • Global: 全局作用域 以及作用域中的变量,如图所示

image.png

作用域是根据名称查找变量的一套规则,当程序在一个环境中执行时,会创建变量对象的一个作用域链,作用域链的用途就是保证对执行环境有权访问的所有变量和函数的有序访问。我们从下面代码来分析作用域。

import {promisify} from 'node:util';
import childProcess from 'node:child_process';
const execFile = promisify(childProcess.execFile);
function test() {
	const text = '测试闭包';
	return function () {
		return text;
	};
}
const k = test()();
console.log(k);
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');
		const tagName = tagReference.replace(/^refs\/tags\//, '').replace(/\^{}$/, '');
		tags.set(tagName, hash);
	}
	return tags;
}

我们从以上代码分析remoteGitTags函数,可以分为四个部分:

  • 第一层:当前模块与remoteGitTags所在同级的作用域
  • 第二层:进入remoteGitTags中,内部所在的作用域
  • 第三层:for循环的()中的作用域
  • 第四层:for循环的{}中的作用域

Global作用域

首先刚创建函数时,调用函数时会先复制外层作用域

Local

local作用域查看当前函数内部的直接变量,也就是 stdouttagsrepoUrl

Block

块级作用域,remoteGitTags中有两个块级作用域也就是for循环中的(){}包裹的部分,所以变量分为两个部分。

image.png

Module

模块作用域,暂时不知道如何理解,有大佬明白的可以在评论告知一下。

Closure

闭包作用域,查看当前闭包作用域中的变量,如图所示,text在闭包环境中。

image.png

Global

node环境的全局作用域,与浏览器的window一样理解即可,查看全局方法

image.png

调用堆栈

可以查看当前程序运行的堆栈调用情况,可以边执行调试比如单步跳过,边查看堆栈变化情况,可以发现变量展示栏也只是展示的在当前堆栈环境中的可访问变量。并且当想要查看之前的代码变量时就可以通过切换调用堆栈定位到想要的那个变量所在的代码行,就能在变量一栏查看历史变量了。

动画1.gif

断点调试

除了常规的打断点以外,还可以添加日志断点。可以通过{}传入表达式,与vue中的{{ }}理解一样,比如图中的console.log({text})

image.png

Promisify

首先介绍一下作用:promisify函数是把 callback 形式转成 promise 形式。

贴一下简易封装的源码,分为3个函数:

  • loadImage:创建图片并回调成功或失败
  • promisify:封装的promisify,将回调形式转为promise形式
  • load:执行函数
        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);
        }

        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();

其中静态方法 Reflect.apply()  通过指定的参数列表发起对目标(target)函数的调用。参照MDN。在这里的作用就是执行original并将this也就是window以及参数args传入,作用跟original.apply(this, args)是一样的。

那么,这里传入的args是什么呢?我们使用上面介绍的调试方法,可以很方便的在调试工具的变量里面查看当前的内容,如图所示。 1638366718(1).jpg

通过push的形式,往args里面添加了一个函数。此时的args也就包含了loadImage的两个参数,srccallback,所以之前push的函数其实就是回调函数。

(err, ...values) => {
    if(err){
        return reject(err);
    }
    resolve(values);
}

并且回调函数中有resolvereject,无论如何它们会返回一个新的promise。所以如果程序正常运行,会将它们作为成功和失败的回调并执行,如图所示

image.png

我们的promisify就因此输出了promise形式的回调函数了。也就可以通过async await同步使用了