#关键词#
- Command
- 事件循环
- import-local
#Lerna Command执行过程#
1、上章代码示例看到yargs注册命令如下:
// index.js
cli()
.command(listCmd)
// listCommand.js
"use strict";
const filterable = require("@lerna/filter-options");
const listable = require("@lerna/listable");
exports.command = "list";
exports.aliases = ["ls", "la", "ll"];
exports.describe = "List local packages";
exports.builder = yargs => {
listable.options(yargs);
return filterable(yargs);
};
exports.handler = function handler(argv) {
return require(".")(argv);
};
2、其中command是命令全称,aliases是别名,describe是描述,builder是命令前置的相关options配置,最关键的是handler是命令的执行过程,深入解析可以看到:
module.exports = factory;
function factory(argv) {
return new ListCommand(argv);
}
class ListCommand extends Command {
get requiresGit() {
return false;
}
initialize() {
let chain = Promise.resolve();
chain = chain.then(() => getFilteredPackages(this.packageGraph, this.execOpts, this.options));
chain = chain.then(filteredPackages => {
this.result = listable.format(filteredPackages, this.options);
});
return chain;
}
execute() {
// piping to `wc -l` should not yield 1 when no packages matched
if (this.result.text.length) {
output(this.result.text);
}
this.logger.success(
"found",
"%d %s",
this.result.count,
this.result.count === 1 ? "package" : "packages"
);
}
}
handler文件里exports出来的是一个factory工厂,返回的是ListCommand的实例化对象,ListCommand类里没有constructor,因为继承了Command类,所以会执行到Command类的constructor里解读如下显示:
class Command {
constructor(_argv) {
log.pause();
log.heading = "lerna";
const argv = cloneDeep(_argv);
log.silly("argv", argv);
// "FooCommand" => "foo"
this.name = this.constructor.name.replace(/Command$/, "").toLowerCase();
// ......省略部分代码......
// launch the command
let runner = new Promise((resolve, reject) => {
// run everything inside a Promise chain
let chain = Promise.resolve();
chain = chain.then(() => {
this.project = new Project(argv.cwd);
});
chain = chain.then(() => this.configureEnvironment());
chain = chain.then(() => this.configureOptions());
chain = chain.then(() => this.configureProperties());
chain = chain.then(() => this.configureLogging());
chain = chain.then(() => this.runValidations());
chain = chain.then(() => this.runPreparations());
chain = chain.then(() => this.runCommand());
chain.then(
result => {
warnIfHanging();
resolve(result);
},
err => {
if (err.pkg) {
// Cleanly log specific package error details
logPackageError(err, this.options.stream);
} else if (err.name !== "ValidationError") {
// npmlog does some funny stuff to the stack by default,
// so pass it directly to avoid duplication.
log.error("", cleanStack(err, this.constructor.name));
}
// ValidationError does not trigger a log dump, nor do external package errors
if (err.name !== "ValidationError" && !err.pkg) {
writeLogFile(this.project.rootPath);
}
warnIfHanging();
// error code is handled by cli.fail()
reject(err);
}
);
});
// passed via yargs context in tests, never actual CLI
/* istanbul ignore else */
if (argv.onResolved || argv.onRejected) {
runner = runner.then(argv.onResolved, argv.onRejected);
// when nested, never resolve inner with outer callbacks
delete argv.onResolved; // eslint-disable-line no-param-reassign
delete argv.onRejected; // eslint-disable-line no-param-reassign
}
// ......省略部分代码(常规逻辑)......
}
// ......省略部分代码......
runCommand() {
return Promise.resolve()
.then(() => this.initialize())
.then(proceed => {
if (proceed !== false) {
return this.execute();
}
// early exits set their own exitCode (if non-zero)
});
}
initialize() {
throw new ValidationError(this.name, "initialize() needs to be implemented.");
}
execute() {
throw new ValidationError(this.name, "execute() needs to be implemented.");
}
}
module.exports = Command;
其中runner跟我们命令真正执行过程息息相关,它定义了一个Promise对象,这个方法会立即执行,执行完以后进入内部定义了一个chain,也是一个Promise对象,这个Promise对象then里面的内容不会被马上执行,它会被依次加入到我们的微任务执行栈当中,由于在同一个Promise对象后面不停的调用then,微任务队列会形成一个排队,依次执行,这是为了在整个常规逻辑执行完之后,再开始执行这些代码逻辑,并且可以通过异步的方式来进行代码执行。
前面微任务里的操作主要做了些脚手架执行过程中的初始化环节,初始化环境变量,配置options、属性,验证工作,验证准备等,以及最核心的runCommand方法,runCommand里面呢又分为2部:initialize初始化和execute,如果子类ListCommand未实现这两个方法会报错,强制我们去实现这两个方法,最终命令的业务逻辑在这两个方法内进行实现。
#javascript事件循环#
- 事件循环又称为event loop
- task主要分为宏任务和微任务
- javascript脚本执行过程中(如下代码示例):整个脚本指令首先会被加入到宏任务队列中进行依次执行,先打印出start,执行到setTimeout时会往我们宏任务队列里加入一个function,不会立即执行,会等到微任务队列执行完后进行执行,然后执行到new Promise对象,这Promise对象里面的function会进行立即执行,它会定义一个chain,chain里面会定义一个Promise对象,Promise对象调then()会立即执行,并且会往微任务队列里依次插入function,此时微任务队列里有console.log('chain1');console.log('chain2');console.log('chain3')三个方法,最后执行end,所以整个输出是start、end、然后执行微任务里的chain1、chain2、chain3,最后清空宏任务队列里的setTimeout,直到任务执行完毕。
handler:() => {
console.log('start');
setTimeout(() => {
console.log('settimeout');
})
new Promise(() => {
let chain = Promise.resolve();
chain().then(() => console.log('chain1'));
chain().then(() => console.log('chain2'));
chain().then(() => console.log('chain3'));
})
console.log('end')
}
#import-local 执行流程深度解析#
#!/usr/bin/env node
"use strict";
/* eslint-disable import/no-dynamic-require, global-require */
const importLocal = require("import-local");
if (importLocal(__filename)) {
require("npmlog").info("cli", "using local version of lerna");
} else {
require(".")(process.argv.slice(2));
}
import-local库:当我们项目当中本地存在一个脚手架命令,同时全局在node当中也存在一个脚手架命令的时候,它优先选用我们node_modules当中的版本。
__filename:which lerna对应的代码文件链接到的地址,把filename传入到import库当中,如果返回的是true,它会优先使用我们本地的版本,会帮我们把本地版本加载进来,并打印一条log。
node执行过程中会在我们上下文注入一些变量包括module、require、exports、__filename、__dirname等
// import-local.js
'use strict';
const path = require('path');
const resolveCwd = require('resolve-cwd');
const pkgDir = require('pkg-dir');
module.exports = filename => {
const globalDir = pkgDir.sync(path.dirname(filename));
const relativePath = path.relative(globalDir, filename);
const pkg = require(path.join(globalDir, 'package.json'));
const localFile = resolveCwd.silent(path.join(pkg.name, relativePath));
// Use `path.relative()` to detect local package installation,
// because __filename's case is inconsistent on Windows
// Can use `===` when targeting Node.js 8
// See https://github.com/nodejs/node/issues/6624
return localFile && path.relative(localFile, filename) !== '' ? require(localFile) : null;
};