小磊

小磊 查看完整档案

北京编辑北方工业大学  |  计算机 编辑  |  填写所在公司/组织填写个人主网站
编辑

以一颗更加开放,更加多元,更加包容的心走进别人的世界

个人动态

小磊 关注了专栏 · 4月12日

前端开发那些事儿

前端知识:HTML、CSS、JS、React,nodejs、Chrome、数据结构与算法,计算机网络等精华知识分享交流。

关注 6687

小磊 关注了用户 · 4月12日

阿宝哥 @angular4

http://www.semlinker.com/
聚焦全栈,专注分享 Angular、TypeScript、Node.js/Java 、Spring 技术栈等全栈干货

欢迎各位小伙伴关注本人公众号全栈修仙之路

关注 2411

小磊 关注了标签 · 4月12日

node.js

图片描述
Node 是一个 Javascript 运行环境(runtime)。实际上它是对 Google V8 引擎(应用于 Google Chrome 浏览器)进行了封装。V8 引擎执行 Javascript 的速度非常快,性能非常好。Node 对一些特殊用例进行了优化,提供了替代的 API,使得 V8 在非浏览器环境下运行得更好。例如,在服务器环境中,处理二进制数据通常是必不可少的,但 Javascript 对此支持不足,因此,V8.Node 增加了 Buffer 类,方便并且高效地 处理二进制数据。因此,Node 不仅仅简单的使用了 V8,还对其进行了优化,使其在各环境下更加给力。

关注 82072

小磊 发布了文章 · 3月31日

Koa1技术分享

写在前面

  Koa使用了ES6规范的generator和异步编程是一个更轻量级Web开发的框架,Koa 的先天优势在于 generator。由于是我个人的分享交流,所以Node基础、ES6标准、Web开发基础以及Koa的"Hello World"程序都不在讨论,希望各位小伙伴提出意见和指导。
  PS:Koa 内核中没有捆绑任何中间件,但不用担心,Koa 拥有极其强悍的拓展性,正文所有中间件都可以在npm官网下载安装,但国内域名安装会有一些限制,提供一个国内镜像安装方法,速度非常快,在直接npm模块失败的时候非常好用,使用npm --registry=http://registry.npmjs.org install XXXXX –XX 命令安装,只需要在install后面加上要安装的中间件名称和相应的参数即可。

一、使用Koa搭建Web项目流程

1、Koa项目创建
  个人认为不管任何框架,Web项目搭建必需的几个方面,页面、中间件、路由、会话和存储、日志、静态文件指定,以及错误的处理。当然,网站开发不止这些东西,还有许多主题,比如实时通讯,搜索引擎架构,权限控制,邮件优先队列,日志记录分析,对Web开发还刚刚入门属于菜鸟级别,这里就不做深入的讨论了。了解Express框架的小伙伴一定知道Express的部署过程,不管是通过express-generator生成还是WebStorm等编译器直接创建,它的目录结构大概是这样的:

|——app.js
|——bin
|——node_modules
|——package.json
|——public
|——routes
|——views

  *app.js,是程序启动文件
  *bin,存放执行程序
  *node_modules,存放项目依赖库
  *package.json,是配置和一些相关信息
  *public,存放静态文件(css,js,img)
  *routes,存放路由文件
  *views,存放前台页面文件
  这些结构基本包含了上述提到的Web项目搭建的要素,但是目前类似express-generator的Koa部署工具Koa-generator(非官方)并不完善并且个人测试存在些许错误。其实Koa-generator也是仿造上述express-generator生成的目录,既然这样还不如手动创建目录来的爽快(generator-k是另一款生成器,用上去感觉还行),在根目录新建app.js作为程序的启动文件,创建三个文件夹分别命名public、routes和views,最后新建package.json文件存放你的项目的一些信息。完成这些创建之后,用npm命令安装Koa,这样的话一个基本的Koa框架就搭建好了,非常的的轻量级,它的目录结构如下:

    |——app.js
    |——node_modules
    |——public
    |    |——img
    |    |——css
    |    |——js
    |
    |——routes
    |    |——index.js
    |    |——user.Js
    |
    |——views
    |    |——_layout.html
    |    |——index.html
    |
    |——package.json
    Koa项目运行:node --harmony app.js
    必须加 --harmony ,这样才会支持 ES6 语法。

2、Koa日志
  日志是项目error调试和日常维护的基本手段,Koa有日志模块Koa-logger,npm install Koa-logger后使用app.use(logger());命令程序就会在控制台自动打印日志,当然如果你对Koa-logger的风格不满意或者想要看到更多得信息也可以自己编辑代码实现有自己风格的日志打印。
例如:

    auto map route -> [get]/authority/saveAddUser/
    auto map route -> [get]/authority/searchUserInfo/
    auto map route -> [get]/authority/updateUser/
    auto map route -> [get]/authority/deletedUser/
    auto map route -> [get]/authority/getSelectValues/
    auto map route -> [get]/authority/saveAuthority/

  最后呢,如果有需要,要把日志进行存储。
3、Koa的错误处理
  Koa 有 error 事件,当发生错误时,可以通过该事件,对错误进行统一的处理。

var Koa = require('koa');
var app = Koa();
app.on('error', function(err,ctx){
    console.log(err);
});   
app.listen(3000);

  上面这段代码在如果捕获到错误,页面会打印出 “Internal Server Error” (这是Koa对错误的默认处理)。这个错误我们在综合监控系统中也经常见到,那么我们显然无法根据这条日志得到什么信息

TypeError: Cannot read property 'split' of undefined
at Object.Home.index (d:\test\route\home.js:143:31)
at GeneratorFunctionPrototype.next (native)
at Object.dispatch (d:\test\node_modules\koa-router\lib\router.js:97:44)
at GeneratorFunctionPrototype.next (native)

  这些错误信息是怎么报出来的的呢,其实是Koa-onerror 中间件,它优化错误信息,根据这些错误信息就能更好的捕获到错误。
Koa-onerror使用方法:

    var onerror = require('Koa-onerror');
    onerror(app);

4、Koa静态文件指定
  Koa静态文件指定中间件Koa-static,npm install Koa-static之后就可以使用Koa-static负责托管 Koa 应用内的静态资源。映射了静态文件目录,引用的时候直接去该目录下寻找资源,会减少一些消耗。(不知道讲的准确不准确,只是个人的理解)指定public为静态文件目录的代码如下:

    var staticServer = require('koa-static');
    var path = require('path');
    app.use(staticServer(path.join(__dirname,'public')));

5、ejs模板的使用
  渲染页面需要一种模板,这里选择风格接近html的ejs模板。npm install Koa-ejs后就可以在Koa框架中使用ejs模版。

    var render = require('koa-ejs');
    render(app, {
        root: path.join(__dirname, 'views'),
        layout: '__layout',
        viewExt: 'html',
        cache: false,
        debug: true
    });
    app.use(function *(){
        yield this.render('index',{layout:false});
    });

6、Koa路由设置
  Koa个极简的web框架,简单到连路由模块都没有配备。自己手写路由是这样的:

    app.use(function *(){
        //我是首页
        if(this.path==='/'){
        }
    });

  使用更加强大的路由中间件,Koa中设置路由一般安装Koa-router,Koa-router支持五种方法

    router.get()
    router.post()
    router.put()
    router.del()
    router.patch()

  GET方法举例:

    var app = require('koa')();
    var Router = require('koa-router');
    var myRouter = new Router();
    myRouter.get('/', function *(next) {
      yield this.render('index',{layout:false});
    });
    app.use(myRouter.routes());
    app.listen(3000);

  Koa-router 拥有丰富的 api 细节,用好这些 api ,可以让页面代码更为优雅与可维护。
接收query参数

    http://localhost:3000/?a=1(条件)
    index.js
    var router = require('koa-router')();
          router
          .get('/',function *(next){
          console.log(this.query);
          yield this.render('index',{layout:false});
      })
          .get('/home',function *(ctx,next){
          ctx.render('home');
      });
      //ctx为Koa2.0中支持
      ... ...
      module.exports = router;
      控制台打印:
      <-- GET /?a=1
      { a: '1' }
      { a: '1' }
      接收params参数 
      http://localhost:3000/users/123(参数)
      router.get('/user/:id', function *(next) {
        console.log(this.params.id);
      });

  param() 用于封装参数处理中间件,当访问 /detail/:id 路由时,会先执行 param() 定义的 generator function 逻辑。函数的第一个是路由参数的值,next 是中间件流程关键标识变量。
yield next;
  表示执行下一个中间件。

      app.param('id',function *(id,next){
          this.id = Number(id);
          if ( typeof this.id != 'number') return this.status = 404;
          yield next;
      }).get('/detail/:id', function *(next) {
          //我是详情页面
          var id = this.id; //123
          this.body = id;
      });

7、Koa中间件
  Koa的中间件很像Express的中间件,也是对HTTP请求进行处理的函数,但是必须是一个Generator函数即 function *(){} 语法,不然会报错。可以这么说,Nodejs的Web程序中任何请求和响应都是中间件在操作。

      app
      .use(logger())               //日志中间件
      .use(serve(__dirname + '/public'))        //静态文件指定中间件
      .use(router.routes())          //路由中间件
      .use(router.allowedMethods());             //路由中间件

  app.use 加载用于处理http请求的middleware(中间件),当一个请求来的时候,会依次被这些 middlewares处理。执行的顺序是你定义的顺序。中间件的执行顺序规则是类似“栈”的结构,所有需要执行的中间件都被一个一个放入“栈”中,当没有遇到next()的时候,“栈”里边的这些中间件被逆序执行。

      app.use(function *(next){
        this; // is the Context
        this.request; // is a Koa Request
        this.response; // is a Koa Response
      });

说明:
  •this是上下文
  •*代表es6里的generator
  http模型里的请求和响应
  •this.request
  •this.response
  app.use() 究竟发生了什么不可思议的化学反应呢?
其实 app.use() 就干了一件事,就是将中间件放入一个数组,真正执行逻辑的是:app.listen(3000);
Koa 的 listen() 除了指定了 http 服务的端口号外,还会启动 http server,等价于:

     var http = require('http');
      http.createServer(app.callback()).listen(3000);

  后面这种繁琐的形式有什么用呢?
  一个典型的场景是启动 https 服务,默认 app.listen(); 是启动 http 服务,启动 https 服务就需要:

      var https = require('https');
      https.createServer(app.callback()).listen(3000);

二、异步编程

1、异步流程控制
  异步编程对 JavaScript 语言太重要。JavaScript 只有一根线程,如果没有异步编程,根本没法用,非卡死不可。
  以前,异步编程的方法,大概有下面四种。
  回调函数
  事件监听
  发布/订阅
  Promise 对象
  JavaScript 语言对异步编程的实现,就是回调函数。所谓回调函数,就是把任务的第二段单独写在一个函数里面,等到重新执行这个任务的时候,就直接调用这个函数。它的英语名字 callback,直译过来就是"重新调用"。
读取文件进行处理,是这样写的。

    fs.readFile('/etc/passwd', function (err, data) {
        if (err) throw err;
        console.log(data);
      });

  上面代码中,readFile 函数的第二个参数,就是回调函数,也就是任务的第二段。等到操作系统返回了 /etc/passwd 这个文件以后,回调函数才会执行。回调函数本身并没有问题,它的问题出现在多个回调函数嵌套。假定读取A文件之后,再读取B文件,代码如下。

      fs.readFile(fileA, function (err, data) {
        fs.readFile(fileB, function (err, data) {
          // ...
        });
      });

  不难想象,如果依次读取多个文件,就会出现多重嵌套。代码不是纵向发展,而是横向发展,很快就会乱成一团,无法管理。这种情况就称为"回调函数噩梦"(callback hell)。Promise就是为了解决这个问题而提出的。它不是新的语法功能,而是一种新的写法,允许将回调函数的横向加载,改成纵向加载。采用Promise,连续读取多个文件,写法如下。

      var readFile = require('fs-readfile-promise');
      readFile(fileA)
      .then(function(data){
        console.log(data.toString());
      })
      .then(function(){
        return readFile(fileB);
      })
      .then(function(data){
        console.log(data.toString());
      })
      .catch(function(err) {
        console.log(err);
      });

  上面代码中,我使用了 fs-readfile-promise 模块,它的作用就是返回一个 Promise 版本的 readFile 函数。Promise 提供 then 方法加载回调函数,catch方法捕捉执行过程中抛出的错误。可以看到,Promise 的写法只是回调函数的改进,使用then方法以后,异步任务的两段执行看得更清楚了,除此以外,并无新意。
  Promise 的最大问题是代码冗余,原来的任务被Promise 包装了一下,不管什么操作,一眼看去都是一堆 then,原来的语义变得很不清楚。
  那么,有没有更好的写法呢?
  ECMAScript 6 (简称 ES6 )作为下一代 JavaScript 语言,将 JavaScript 异步编程带入了一个全新的阶段。异步编程的语法目标,就是怎样让它更像同步编程。
  Koa 的先天优势在于 generator。
  generator指的是

      function* xxx(){
      }

  是es6里的写法。

      var r = 3;  
      function* infinite_ap(a) {
          for( var i = 0; i < 3 ; i++) {
              a = a + r ;
              yield a;
          }
      }
      var sum = infinite_ap(5);
      console.log(sum.next()); // returns { value : 8, done : false }
      console.log(sum.next()); // returns { value : 11, done: false }
      console.log(sum.next()); // returns { value : 14, done: false }
      console.log(sum.next()); //return { value: undefined, done: true }

  yield语句就是暂停标志,next方法遇到yield,就会暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回对象的value属性的值。当下一次调用next方法时,再继续往下执行,直到遇到下一个yield语句。如果没有再遇到新的yield语句,就一直运行到函数结束,将return语句后面的表达式的值,作为value属性的值,如果该函数没有return语句,则value属性的值为undefined。当第一次调用 sum.next() 时 返回的a变量值是5 + 3,同理第二次调用 sum.next() ,a变量值是8 +3,知道循环执行结束,返回done:true标识。大家有没有发现个问题,Koa 中 generator 的用法与上述 demo 演示的用法有非常大得差异,那是因为 Koa 中的 generator 使用了 co 进行了封装。
2、co的使用
  Ps:(这里只是简单介绍,后续可以作为一个专题来讲)
  co 函数库是著名程序员 TJ Holowaychuk 于2013年6月发布的一个小工具,用于 Generator 函数的自动执行。
  比如,有一个 Generator 函数,用于依次读取两个文件。

      var gen = function* (){
        var f1 = yield readFile('/etc/fstab');
        var f2 = yield readFile('/etc/shells');
        console.log(f1.toString());
        console.log(f2.toString());
      };

  co 函数库可以让你不用编写 Generator 函数的执行器。

      var co = require('co');
      co(gen);

  上面代码中,Generator 函数只要传入 co 函数,就会自动执行。
  co 函数返回一个 Promise 对象,因此可以用 then 方法添加回调函数。

      co(gen).then(function (){
        console.log('Generator 函数执行完成');
      })

  上面代码中,等到 Generator 函数执行结束,就会输出一行提示。
  为什么 co 可以自动执行 Generator 函数?
  前面文章说过,Generator 函数就是一个异步操作的容器。它的自动执行需要一种机制,当异步操作有了结果,能够自动交回执行权。
  两种方法可以做到这一点。
  (1)回调函数。将异步操作包装成 Thunk 函数,在回调函数里面交回执行权。
  (2)Promise 对象。将异步操作包装成 Promise 对象,用 then 方法交回执行权。
  co 函数库其实就是将两种自动执行器(Thunk 函数和 Promise 对象),包装成一个库。使用 co 的前提条件是,Generator 函数的 yield 命令后面,只能是 Thunk 函数或 Promise 对象。
  参考:http://www.ruanyifeng.com/blog/2015/05/co.html
3、Koa 中间件机制实现原理
使用 Koa 的同学一定会有如下疑问:

  1. Koa 的中间件机制是如何实现?
  2. 为什么中间件必须是 generator function?
  3. next 实参指向是什么?为什么可以通过 yield next 可以执行下一个中间件?
  4. 为什么中间件从上到下执行完后,可以从下到上执行 yield next 后的逻辑?

  通过实现简单的 Koa 框架(剥离除中间件外所有的逻辑)来解答上述问题,这个框架的名字叫 SimpleKoa:

      var co = require('co');
      function SimpleKoa(){
          this.middlewares = [];
      }
      SimpleKoa.prototype = {
          //注入个中间件
          use: function(gf){
              this.middlewares.push(gf);
          },
          //执行中间件
          listen: function(){
              this._run();
          },
          _run: function(){
              var ctx = this;
              var middlewares = ctx.middlewares;
              return co(function *(){
                  var prev = null;
                  var i = middlewares.length;
                  //从最后一个中间件到第一个中间件的顺序开始遍历
                  while (i--) {
                   //实际Koa的ctx应该指向server的上下文,这里做了简化
                  //prev 将前面一个中间件传递给当前中间件
                      prev = middlewares[i].call(ctx, prev);
                  }
                //执行第一个中间件
                  yield prev;
              })();
          }
      };

  写个 demo 印证下中间件执行顺序:

      var app = new SimpleKoa();
      app.use(function *(next){
          this.body = '1';
          yield next;
          this.body += '5';
          console.log(this.body);
      });
      app.use(function *(next){
          this.body += '2';
          yield next;
          this.body += '4';
      });
      app.use(function *(next){
          this.body += '3';
          });
      app.listen();

  执行后控制台输出:123456,对照 Koa 中间件执行顺序,完全一致!寥寥几行代码,我们就实现了 Koa 的中间件机制!这就是 co 的魔力。

三、Koa中涉及但本次没有讲的问题

1、Koa中的cookie和session(后续详细讲解)
  web应用程序都离不开cookie和session的使用,是因为Http是一种无状态性的协议。保存用户状态信息的一种方法或手段,Session 与 Cookie 的作用都是为了保持访问用户与后端服务器的交互状态。
2、Koa中nosql(后续技术分享会详细讲解)
  mongodb是一个基于文档的非关系型数据库,所有数据是从磁盘上进行读写的,其优势在于查询功能比较强大,能存储海量数据。
  redis是内存型数据库,数据保存在内存中,通过tcp直接存取,优势是速度快,并发高,缺点是数据类型有限,查询功能不强,一般用作缓存。它由C语言实现的,与 NodeJS工作原理近似,同样以单线程异步的方式工作,先读写内存再异步同步到磁盘,读写速度上比MongoDB有巨大的提升,当并发达到一定程度时,即可考虑使用Redis来缓存数据和持久化Session。

      var mongoose = require('mongoose');
      // 引入 mongoose 模块
      mongoose.connect('mongodb://localhost/blog');
      // 然后连接对应的数据库:mongodb://localhost/test
      // 其中,前面那个 mongodb 是 protocol scheme 的名称;localhost 是 mongod 所在的地址;
      // 端口号省略则默认连接 27017;blog是数据库的名称
      // mongodb 中不需要建立数据库,当你需要连接的数据库不存在时,会自动创建一个出来。
      module.exports = mongoose;
      // 导出 mongoose 模块
      var mongoose = require('../modules/db');
      // 引入 mongoose 模块
      var User = mongoose.model('User',{
          name: {type: String, match: /^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/},
          password: String
      });
      //创建了一个名为 User 的 model
      var user1 = new User({name:'12345@qqqqqq.com'});
      user1.password = 'a5201314';  
      user1.save(function(err){
         if(err){
             console.log("save error");
         }
      });
查看原文

赞 0 收藏 0 评论 0

小磊 发布了文章 · 3月31日

Node交互式命令行工具开发——自动化文档工具

  nodejs开发命令行工具,流程相对简单,但一套完整的命令行程序开发流程下来,还是需要下点功夫,网上资料大多零散,这篇教程意在整合一下完整的开发流程。
  npm上命令行开发相关包很多,例如minimistoptimistnoptcommander.jsyargs等等,使用方法和效果类似。其中用得比较多的是TJ大神的commanderyargs,本文以commander为基础讲述,可以参考这篇教程,yargs教程可以参考阮大神的或者这一篇
  另外,一个完整的命令行工具开发,还需要了解processshelljspathlinebyline等模块,这些都是node基础模块或一些简单模块,非常简单,就不多说了,另外如果你不想用回调函数处理异步还需要了解一下PromiseGenerator函数。这是教程:i5ting大神的《深入浅出js(Node.js)异步流程控制》和阮大神的异步编程教程以及promise小人书,另外想尝试ES7 stage3阶段的async/await异步解决方案,可参考这篇教程async/await解决方案需要babel转码,这是教程。本人喜欢async/await(哪个node开发者不喜欢呢?)但不喜欢倒腾,况且async/await本身就是Promise的语法糖,所以没选择使用,据江湖消息,nodejs将在今年晚些时候(10月份?)支持async/await,很是期待。
  以下是文章末尾实例用到的一些依赖。

"dependencies": {
    "bluebird": "^3.4.1",
    "co": "^4.6.0",
    "colors": "^1.1.2",
    "commander": "^2.9.0",
    "dox": "^0.9.0",
    "handlebars": "^4.0.5",
    "linebyline": "^1.3.0",
    "mkdirp": "^0.5.1"
  }

  其中bluebird用于Promise化,TJ大神的co用于执行Generator函数,handlebars是一种模板,linebyline用于分行读取文件,colors用于美化输出,mkdirp用于创建目录,另外教程中的示例是一款工具,可以自动化生成数据库和API接口的markdown文档,并通过修改git hooks,使项目的每次commit都会自动更新文档,借助了TJ大神的dox模块。
  <span style="color:rgb(0, 136, 204)">所有推荐教程/教材,仅供参考,自行甄选阅读。</span>

安装Node

  各操作系统下安装见Nodejs官网,安装完成之后用node -v或者which node等命令测试安装是否成功。which在命令行开发中是一个非常有用的命令,使用which命令确保你的系统中不存在名字相同的命令行工具,例如which commandName,例如which testdev命令返回空白那么说明testdev命令名称还没有被使用。

初始化

  1. 新建一个.js文件,即是你的命令要执行的主程序入口文件,例如testdev.js。在文件第一行加入#!/usr/bin/env node指明系统在运行这个文件的时候使用node作为解释器,等价于node testdev.js命令。
  2. 初始化package.json文件,使用npm init命令根据提示信息创建,也可以是使用npm init -y使用默认设置创建。创建完成之后需要修改package.json文件内容加入"bin": {"testdev": "./testdev.js"}这条信息用于告诉npm你的命令(testdev)要执行的脚本文件的路径和名字,这里我们指定testdev命令的执行文件为当前目录下的testdev.js文件。
  3. 为了方便测试在testdev.js文件中加入代码console.log('hello world');,这里只是用于测试环境是否搭建成功,更加复杂的程序逻辑和过程需要按照实际情况进行编写

测试

  使用npm link命令,可以在本地安装刚刚创建的包,然后就可以用testdev来运行命令了,如果正常的话在控制台会打印出hello world

commander

  TJ的commander非常简洁,README.md已经把使用方法写的非常清晰。下面是例子中的代码:

const program = require('commander'),
  co = require('co');

const appInfo = require('./../package.json'),
  asyncFunc = require('./../common/asyncfunc.js');

program.allowUnknownOption();
program.version(appInfo.version);

program
  .command('init')
  .description('初始化当前目录doc.json文件')
  .action(() => co(asyncFunc.initAction));

program
  .command('show')
  .description('显示配置文件状态')
  .action(() => co(asyncFunc.showAction));

program
  .command('run')
  .description('启动程序')
  .action(() => co(asyncFunc.runAction));

program
  .command('modifyhook')
  .description('修改项目下的hook文件')
  .action(() => co(asyncFunc.modifyhookAction));

program
  .command('*')
  .action((env) => {
    console.error('不存在命令 "%s"', env);
  });

program.on('--help', () => {
  console.log('  Examples:');
  console.log('');
  console.log('    $ createDOC --help');
  console.log('    $ createDOC -h');
  console.log('    $ createDOC show');
  console.log('');
});

program.parse(process.argv);

  定义了四个命令和个性化帮助说明。

交互式命令行process

  commander只是实现了命令行参数与回复一对一的固定功能,也就是一个命令必然对应一个回复,那如何实现人机交互式的命令行呢,类似npm init或者eslint --init这样的与用户交互,交互之后根据用户的不同需求反馈不同的结果呢。这里就需要node内置的process模块。
  这是我实现的一个init命令功能代码:

exports.initAction = function* () {
  try {
    var docPath = yield exists(process.cwd() + '/doc.json');
    if (docPath) {
      func.initRepl(config.coverInit, arr => {
        co(newDoc(arr));
      })
    } else {
      func.initRepl(config.newInit, arr => {
        co(newDoc(arr));
      })
    }
  } catch (err) {
    console.warn(err);
  }

  首先检查doc.json文件是否存在,如果存在执行覆盖交互,如果不存在执行生成交互,try...catch捕获错误。
  交互内容配置如下:

    newInit:
    [
        {
            title:'initConfirm',
            description:'初始化createDOC,生成doc.json.确认?(y/n)  ',
            defaults: 'y'
        },
        {
            title:'defaultConfirm',
            description:'是否使用默认配置.(y/n)  ',
            defaults: 'y'
        },
        {
            title:'showConfig',
            description:'是否显示doc.json当前配置?(y/n)  ',
            defaults: 'y'
        }
    ],
    coverInit:[
        {
            title:'modifyConfirm',
            description:'doc.json已存在,初始化将覆盖文件.确认?(y/n)  ',
            defaults: 'y'
        },
        {
            title:'defaultConfirm',
            description:'是否使用默认配置.(y/n)  ',
            defaults: 'y'
        },
        {
            title:'showConfig',
            description:'是否显示doc.json当前配置?(y/n)  ',
            defaults: 'y'
        }
    ],

  人机交互部分代码也就是initRepl函数内容如下:

//初始化命令,人机交互控制
exports.initRepl = function (init, func) {
  var i = 1;
  var inputArr = [];
  var len = init.length;
  process.stdout.write(init[0].description);
  process.stdin.resume();
  process.stdin.setEncoding('utf-8');
  process.stdin.on('data', (chunk) => {
    chunk = chunk.replace(/[\s\n]/, '');
    if (chunk !== 'y' && chunk !== 'Y' && chunk !== 'n' && chunk !== 'N') {
      console.log(config.colors.red('您输入的命令是: ' + chunk));
      console.warn(config.colors.red('请输入正确指令:y/n'));
      process.exit();
    }
    if (
      (init[i - 1].title === 'modifyConfirm' || init[i - 1].title === 'initConfirm') &&
      (chunk === 'n' || chunk === 'N')
    ) {
      process.exit();
    }
    var inputJson = {
      title: init[i - 1].title,
      value: chunk,
    };
    inputArr.push(inputJson);
    if ((len--) > 1) {
      process.stdout.write(init[i++].description)
    } else {
      process.stdin.pause();
      func(inputArr);
    }
  });
}

  人机交互才用向用户提问根据用户不同输入产生不同结果的形式进行,顺序读取提问列表并记录用户输入结果,如果用户输入n/N则终止交互,用户输入非法字符(除y/Y/n/N以外)提示输入命令错误。

文档自动化

  文档自动化,其中数据库文档自动化,才用依赖sequelize的方法手写(根据需求不同自行编写逻辑),API文档才用TJ的dox也很简单。由于此处代码与命令行功能相关度不大,请读者自行去示例地址查看代码。

示例地址

github地址
npm地址

查看原文

赞 12 收藏 5 评论 1

小磊 发布了文章 · 3月31日

深入剖析let/const在for循环中的作用原理

快速排序

'use strict';

const quickSort = function (array, low, high) {
  if (low >= high) {
    return;
  }
  let i = low;
  let j = high;
  const tmp = array[i];
  while (i < j) {
    while (i < j && array[j] >= tmp) {
      j--;
    }
    if (i < j) {
      array[i++] = array[j];
    }
    while (i < j && array[i] <= tmp) {
      i++;
    }
    if (i < j) {
      array[j--] = array[i];
    }
  }
  array[i] = tmp;
  quickSort(array, low, i - 1);
  quickSort(array, i + 1, high);
}

const arr = [1, 23, 7, 123, 45, 78, 10];
console.log(arr);
quickSort(arr, 0, arr.length - 1);
console.log(arr);

归并排序

'use strict';

// 两个有序数组合并
const memeryArray = function (arr1, arr2) {
  const c = [];
  let i = 0;
  let j = 0;
  let k = 0;
  while (i < arr1.length && j < arr2.length) {
    if (arr1[i] < arr2[j]) c[k++] = arr1[i++];
    else c[k++] = arr2[j++];
  }
  while (i < arr1.length) c[k++] = arr1[i++];
  while (j < arr2.length) c[k++] = arr2[j++];

  return c;
}

// 将同一数组,两段有序序列合并
const memery = function (arr, first, mid, last) {
  const temp = [];
  let i = first;
  const j = mid;
  let m = mid + 1;
  const n = last;
  let k = 0;
  while (i <= j && m <= n) {
    if (arr[i] < arr [m]) temp[k++] = arr[i++];
    else temp[k++] = arr[m++];
  }
  while (i <= j) temp[k++] = arr[i++];
  while (m <= n) temp[k++] = arr[m++];
  for (i = 0; i < k; i++) arr[first + i] = temp[i];
}

const mergeSort = function (arr, first, last) {
  if (!Array.isArray(arr)) throw new Error('argument is not a array');
  if (first < last) {
    const mid = parseInt((first + last) / 2, 10);
    mergeSort(arr, first, mid);
    mergeSort(arr, mid + 1, last);
    memery(arr, first, mid, last);
  }
};

const arr1 = [1, 7, 13, 20, 6, 9, 10, 10, 11, 26, 29];
console.log(arr1);
mergeSort(arr1, 0, arr1.length - 1);
console.log(arr1);
查看原文

赞 1 收藏 0 评论 0

小磊 发布了文章 · 3月31日

JS面试题 - 台阶走法和字符串压缩、快速排序和归并排序

这篇文章继续分享一些公司常问面试题,以供参考。

台阶走法问题

'use strict';

// 递归实现
const recursionCount = function (n) {
  if (n <= 0) return 0;
  if (n === 1) return 1;
  if (n === 2) return 2;

  return count(n - 1) + count(n - 2);
};

// 非递归 用数组实现
const unrecursionCount1 = function (n) {
  if (n <= 0) return 0;
  if (n === 1) return 1;
  if (n === 2) return 2;
  const steps = [1, 2];
  for (let i = 2; i < n; i++) {
    steps[i] = a[i - 1] + a[i - 2];
  }

  return steps[n - 1];
};

// 非递归 用迭代实现
const unrecursionCount2 = function (n) {
  let i = 2,
    a = 1,
    b = 2,
    sum = 0;
  if (n < 1) {
    return -1;
  }
  for (; i < n; i++) {
    sum = a + b;
    a = b;
    b = sum;
  }

  return sum;
};

// 打印路径
const printSteps = function (n, preStr) {
  if (typeof n !== 'number') throw new Error('not a number');
  const str = preStr ? preStr : '';
  if (n < 0) {
    console.log('can\'t print Steps, n < 0');

    return;
  }
  if (n === 0) {
    console.log(str);

    return;
  }
  if (n === 1) {
    console.log(`${str} 1`);

    return;
  }
  for (let i = 1; i <= 2; i++) {
    printSteps(n - i, `${str} ${i}`);
  }

  return;
}

printSteps(3, 'steps: ')

字符串压缩,比如abbbc压缩为ab3c。

分析:如果字符是数字需要处理,比如aa2222b压缩为a224b,解压的时候就不知道是224个a,还是2个a,4个2或者其他。
解决办法,如果是数字,在前面加一个特殊字符标识,同时这个特殊字符也要单独处理
(前面加特殊字符是最精简的方法,只需要一个特殊字符,另外对于比较散乱的字符串,也就是单个字符很多的情况,会有很多1,所以如果字符数量是1则不压缩。)

'use strict';

const small = function (arr) {
  let strList;
  if (Array.isArray(arr)) strList = arr;
  else if (typeof arr === 'string') strList = arr.split('');
  else throw new Error('....');

  let arrStr = '';
  let count = 1;
  for (let i = 0; i < strList.length; i++) {
    if (strList[i + 1] === strList[i]) {
      count++;
    } else {
      if (/\d/.test(strList[i]) || /'/.test(strList[i])) arrStr += `'${strList[i]}`;
      else arrStr += `${strList[i]}`;
      if (count !== 1) arrStr += `${count}`;
      count = 1;
    }
  }

  return arrStr;
};

const str = 'abgjldfff11111111111f4ous\'\'\'';
console.log(small(str));

快速排序

'use strict';

const quickSort = function (array, low, high) {
  if (low >= high) {
    return;
  }
  let i = low;
  let j = high;
  const tmp = array[i];
  while (i < j) {
    while (i < j && array[j] >= tmp) {
      j--;
    }
    if (i < j) {
      array[i++] = array[j];
    }
    while (i < j && array[i] <= tmp) {
      i++;
    }
    if (i < j) {
      array[j--] = array[i];
    }
  }
  array[i] = tmp;
  quickSort(array, low, i - 1);
  quickSort(array, i + 1, high);
}

const arr = [1, 23, 7, 123, 45, 78, 10];
console.log(arr);
quickSort(arr, 0, arr.length - 1);
console.log(arr);

归并排序

'use strict';

// 两个有序数组合并
const memeryArray = function (arr1, arr2) {
  const c = [];
  let i = 0;
  let j = 0;
  let k = 0;
  while (i < arr1.length && j < arr2.length) {
    if (arr1[i] < arr2[j]) c[k++] = arr1[i++];
    else c[k++] = arr2[j++];
  }
  while (i < arr1.length) c[k++] = arr1[i++];
  while (j < arr2.length) c[k++] = arr2[j++];

  return c;
}

// 将同一数组,两段有序序列合并
const memery = function (arr, first, mid, last) {
  const temp = [];
  let i = first;
  const j = mid;
  let m = mid + 1;
  const n = last;
  let k = 0;
  while (i <= j && m <= n) {
    if (arr[i] < arr [m]) temp[k++] = arr[i++];
    else temp[k++] = arr[m++];
  }
  while (i <= j) temp[k++] = arr[i++];
  while (m <= n) temp[k++] = arr[m++];
  for (i = 0; i < k; i++) arr[first + i] = temp[i];
}

const mergeSort = function (arr, first, last) {
  if (!Array.isArray(arr)) throw new Error('argument is not a array');
  if (first < last) {
    const mid = parseInt((first + last) / 2, 10);
    mergeSort(arr, first, mid);
    mergeSort(arr, mid + 1, last);
    memery(arr, first, mid, last);
  }
};

const arr1 = [1, 7, 13, 20, 6, 9, 10, 10, 11, 26, 29];
console.log(arr1);
mergeSort(arr1, 0, arr1.length - 1);
console.log(arr1);
查看原文

赞 0 收藏 0 评论 0

小磊 发布了文章 · 3月31日

javascript创建4位不重复邀请码思路

起因

关于随机不重复字符串,如果没有长度限制,那么最简单的方法当前时间戳 + 固定位数随机字符串的形式完全可以满足(仍有概率重复,但几乎可以忽略),至于长度,有很多办法解决,但最终也无法做到很短。

最近公司有个小需求,把邀请码降低到四位,四位不重复邀请码的实现方式就不那么随意了,直观的想法是通过10个数字(0-9) + 26个小写字母(a-z)排列组合实现,即所谓的排列可重复问题。

这类问题公式如下,每次n种选择,选择r次的排列共有:n的r次幂。这很好理解,一次有n种选择,第二次有n∗n种选择,……,第r次有nr种选择。

对应我们的需求,也就是可以有36的4次幂 = 1679616种组合,完全可以满足一个小型项目的需求。如果项目比较大,完全可以通过加入其他项(比如特殊字符、大写字母等等)到可择列表实现,最终还可以提升邀请码位数进一步扩展。

思路比较直观简单,但也比较实用,欢迎大家学习交流。

实现

思路:

1、首先要确定随机数的依据,采用递增数字(可以对应数据库id)作为基础,创建随机串。
2、打乱10个数字 + 26个字母的组合,这样随机数看起来会舒服一些。
3、把基数转换成36进制数字,转换后不足4位则补充0位。
4、把四位数字的每一位作为下标对应打乱后10个数字 + 26个字母的组合的列表。
5、组合成4位唯一随机字符串。

下面是代码:

exports.random = (number) => {
  // 打乱的10个数字 + 26个字母
  const arr = ["m","0","j","f","8","o","z","w","5","t","p","a","1","d","s","h","v","x","9","b","r","y","2","e","7","4","3","q","6","n","u","l","c","g","i","k"];
  // 把number由十进制数转换成36进制数
  const transNumber = binaryConversion(+number, 10, 36);
  // 排除大于4位的情况
  const len = transNumber.toString().length;
  if (len > 4) {
    console.log('数字过大');
    return;
  }
  // 转换后的数字不足4位则补充0位,并按字符转换成数组
  const list = prefixInteger(transNumber, 4).toString().split('');
  const len4Arr = [];
  for (const num of list) {
    // 判断当前字符是数字还是字母
    const type = checkStrType(num);
    // 如果是数字不处理,如果是字母则转换成对应的数组(10 - 35)
    const index = type === 'string' ? stringTonum(num) + 9 : num;
    len4Arr.push(arr[+index]);
  }

  // 返回结果
  return len4Arr.join('');
}

补充

缺点:基数数字低于36无法创建,比如起始数字大于36。

欢迎大家提出高效思路,不足之处请指出。

其他代码如下:


function binaryConversion(num, m ,n) {
  return parseInt(String(num), m).toString(n);
}
function prefixInteger(num, length) {
 return (Array(length).join('0') + num).slice(-length);
}
function stringTonum(a) {
  const str = a.toLowerCase().split('');
  const al = str.length;
  const getCharNumber = charx => charx.charCodeAt() - 96;
  let numout = 0;
  let charnum = 0;
  for (let i = 0; i < al; i++) {
      charnum = getCharNumber(str[i]);
      numout += charnum * Math.pow(26, al - i - 1);
  };

  return numout;
}

function checkStrType(str) {
  //验证是否是英文
  var pattern = new RegExp("[A-Za-z]+");
  if (pattern.test(str)) return 'string';
  //验证是否是数字
  var patternNumber = new RegExp("[0-9]+");
  if (patternNumber.test(str)) return 'number';

  return '';
}
查看原文

赞 1 收藏 1 评论 0

小磊 关注了用户 · 3月23日

码哥字节 @magebyte

公众号-码哥字节

一个有品位的博主,不跟风不扯淡,助力程序员成长。

一线大厂互联网工作经验,左手微服务、右手中间件、前脚高并发、后脚分布式。

关注 6342

小磊 关注了用户 · 3月23日

码农小胖哥 @10000000

技术公众号:Felordcn 欢迎关注
微信圈子:程序员交流圈 欢迎投稿交流
个人独立博客: https://felord.cn

关注 5320

小磊 关注了标签 · 3月23日

docker

an open source project to pack, ship and run any application as a lightweight container ! By Lock !

关注 44337

小磊 关注了标签 · 3月23日

关注 92307

小磊 关注了标签 · 3月23日

vue.js

Reactive Components for Modern Web Interfaces.

Vue.js 是一个用于创建 web 交互界面的。其特点是

  • 简洁 HTML 模板 + JSON 数据,再创建一个 Vue 实例,就这么简单。
  • 数据驱动 自动追踪依赖的模板表达式和计算属性。
  • 组件化 用解耦、可复用的组件来构造界面。
  • 轻量 ~24kb min+gzip,无依赖。
  • 快速 精确有效的异步批量 DOM 更新。
  • 模块友好 通过 NPM 或 Bower 安装,无缝融入你的工作流。

官网:https://vuejs.org
GitHub:https://github.com/vuejs/vue

关注 137809

小磊 关注了标签 · 3月23日

css

层叠样式表(英语:Cascading Style Sheets,简写CSS),又称串样式列表,由W3C定义和维护的标准,一种用来为结构化文档(如HTML文档或XML应用)添加样式(字体、间距和颜色等)的计算机语言。

关注 97405

小磊 关注了标签 · 3月23日

react.js

React (sometimes styled React.js or ReactJS) is an open-source JavaScript library for creating user interfaces that aims to address challenges encountered in developing single-page applications. It is maintained by Facebook, Instagram and a community of individual developers and corporations.

关注 69777

小磊 关注了标签 · 3月23日

typescript

TypeScript 是微软开发的 JavaScript 的超集,TypeScript兼容JavaScript,可以载入JavaScript代码然后运行。TypeScript与JavaScript相比进步的地方。包括:加入注释,让编译器理解所支持的对象和函数,编译器会移除注释,不会增加开销;增加一个完整的类结构,使之更新是传统的面向对象语言。

关注 35522

小磊 关注了标签 · 3月23日

前端

Web前端开发是从网页制作演变而来的,名称上有很明显的时代特征。在互联网的演化进程中,网页制作是Web 1.0时代的产物,那时网站的主要内容都是静态的,用户使用网站的行为也以浏览为主。2005年以后,互联网进入Web 2.0时代,各种类似桌面软件的Web应用大量涌现,网站的前端由此发生了翻天覆地的变化。网页不再只是承载单一的文字和图片,各种富媒体让网页的内容更加生动,网页上软件化的交互形式为用户提供了更好的使用体验,这些都是基于前端技术实现的。

Web前端优化
  1. 尽量减少HTTP请求 (Make Fewer HTTP Requests)
  2. 减少 DNS 查找 (Reduce DNS Lookups)
  3. 避免重定向 (Avoid Redirects)
  4. 使得 Ajax 可缓存 (Make Ajax Cacheable)
  5. 延迟载入组件 (Post-load Components)
  6. 预载入组件 (Preload Components)
  7. 减少 DOM 元素数量 (Reduce the Number of DOM Elements)
  8. 切分组件到多个域 (Split Components Across Domains)
  9. 最小化 iframe 的数量 (Minimize the Number of iframes)
  10. 杜绝 http 404 错误 (No 404s)

关注 194794

小磊 关注了标签 · 3月23日

javascript

JavaScript 是一门弱类型的动态脚本语言,支持多种编程范式,包括面向对象和函数式编程,被广泛用于 Web 开发。

一般来说,完整的JavaScript包括以下几个部分:

  • ECMAScript,描述了该语言的语法和基本对象
  • 文档对象模型(DOM),描述处理网页内容的方法和接口
  • 浏览器对象模型(BOM),描述与浏览器进行交互的方法和接口

它的基本特点如下:

  • 是一种解释性脚本语言(代码不进行预编译)。
  • 主要用来向HTML页面添加交互行为。
  • 可以直接嵌入HTML页面,但写成单独的js文件有利于结构和行为的分离。

JavaScript常用来完成以下任务:

  • 嵌入动态文本于HTML页面
  • 对浏览器事件作出响应
  • 读写HTML元素
  • 在数据被提交到服务器之前验证数据
  • 检测访客的浏览器信息

《 Javascript 优点在整个语言中占多大比例?

关注 176703

小磊 发布了文章 · 3月23日

Promise知识汇总和面试情况

写在前面

Javascript异步编程先后经历了四个阶段,分别是Callback阶段,Promise阶段,Generator阶段和Async/Await阶段。Callback很快就被发现存在回调地狱和控制权问题,Promise就是在这个时间出现,用以解决这些问题,Promise并非一个新事务,而是按照一个规范实现的类,这个规范有很多,如 Promise/APromise/BPromise/D 以及 Promise/A 的升级版 Promise/A+,最终 ES6 中采用了 Promise/A+ 规范。后来出现的Generator函数以及Async函数也是以Promise为基础的进一步封装,可见Promise在异步编程中的重要性。

关于Promise的资料已经很多,但每个人理解都不一样,不同的思路也会有不一样的收获。这篇文章会着重写一下Promise的实现以及笔者在日常使用过程中的一些心得体会。

实现Promise

规范解读

Promise/A+规范主要分为术语、要求和注意事项三个部分,我们重点看一下第二部分也就是要求部分,以笔者的理解大概说明一下,具体细节参照完整版Promise/A+标准。

1、Promise有三种状态pendingfulfilledrejected。(为了一致性,此文章称fulfilled状态为resolved状态)

  • 状态转换只能是pendingresolved或者pendingrejected
  • 状态一旦转换完成,不能再次转换。

2、Promise拥有一个then方法,用以处理resolvedrejected状态下的值。

  • then方法接收两个参数onFulfilledonRejected,这两个参数变量类型是函数,如果不是函数将会被忽略,并且这两个参数都是可选的。
  • then方法必须返回一个新的promise,记作promise2,这也就保证了then方法可以在同一个promise上多次调用。(ps:规范只要求返回promise,并没有明确要求返回一个新的promise,这里为了跟ES6实现保持一致,我们也返回一个新promise
  • onResolved/onRejected有返回值则把返回值定义为x,并执行[[Resolve]](promise2, x);
  • onResolved/onRejected运行出错,则把promise2设置为rejected状态;
  • onResolved/onRejected不是函数,则需要把promise1的状态传递下去。

3、不同的promise实现可以的交互。

  • 规范中称这一步操作为promise解决过程,函数标示为[[Resolve]](promise, x),promise为要返回的新promise对象,xonResolved/onRejected的返回值。如果xthen方法且看上去像一个promise,我们就把x当成一个promise的对象,即thenable对象,这种情况下尝试让promise接收x的状态。如果x不是thenable对象,就用x的值来执行 promise
  • [[Resolve]](promise, x)函数具体运行规则:

    • 如果 promisex 指向同一对象,以 TypeError 为据因拒绝执行 promise;
    • 如果 xPromise ,则使 promise 接受 x 的状态;
    • 如果 x 为对象或者函数,取x.then的值,如果取值时出现错误,则让promise进入rejected状态,如果then不是函数,说明x不是thenable对象,直接以x的值resolve,如果then存在并且为函数,则把x作为then函数的作用域this调用,then方法接收两个参数,resolvePromiserejectPromise,如果resolvePromise被执行,则以resolvePromise的参数value作为x继续调用[[Resolve]](promise, value),直到x不是对象或者函数,如果rejectPromise被执行则让promise进入rejected状态;
    • 如果 x 不是对象或者函数,直接就用x的值来执行promise

代码实现

规范解读第1条,代码实现:

class Promise {
  // 定义Promise状态,初始值为pending
  status = 'pending';
  // 状态转换时携带的值,因为在then方法中需要处理Promise成功或失败时的值,所以需要一个全局变量存储这个值
  data = '';

  // Promise构造函数,传入参数为一个可执行的函数
  constructor(executor) {
    // resolve函数负责把状态转换为resolved
    function resolve(value) {
      this.status = 'resolved';
      this.data = value;
    }
    // reject函数负责把状态转换为rejected
    function reject(reason) {
      this.status = 'rejected';
      this.data = reason;
    }

    // 直接执行executor函数,参数为处理函数resolve, reject。因为executor执行过程有可能会出错,错误情况需要执行reject
    try {
      executor(resolve, reject);
    } catch(e) {
      reject(e)
    }
  }
}

第1条就是实现完毕了,相对简单,配合代码注释很容易理解。

规范解读第2条,代码实现:

  /**
    * 拥有一个then方法
    * then方法提供:状态为resolved时的回调函数onResolved,状态为rejected时的回调函数onRejected
    * 返回一个新的Promise
  */
  then(onResolved, onRejected) {
    // 设置then的默认参数,默认参数实现Promise的值的穿透
    onResolved = typeof onResolved === 'function' ? onResolved : function(v) { return e };
    onRejected = typeof onRejected === 'function' ? onRejected : function(e) { throw e };
    
    let promise2;
    
    promise2 =  new Promise((resolve, reject) => {
      // 如果状态为resolved,则执行onResolved
      if (this.status === 'resolved') {
        try {
          // onResolved/onRejected有返回值则把返回值定义为x
          const x = onResolved(this.data);
          // 执行[[Resolve]](promise2, x)
          resolvePromise(promise2, x, resolve, reject);
        } catch (e) {
          reject(e);
        }
      }
      // 如果状态为rejected,则执行onRejected
      if (this.status === 'rejected') {
        try {
          const x = onRejected(this.data);
          resolvePromise(promise2, x, resolve, reject);
        } catch (e) {
          reject(e);
        }
      }
    });
    
    return promise2;
  }

现在我们就按照规范解读第2条,实现了上述代码,上述代码很明显是有问题的,问题如下

  1. resolvePromise未定义;
  2. then方法执行的时候,promise可能仍然处于pending状态,因为executor中可能存在异步操作(实际情况大部分为异步操作),这样就导致onResolved/onRejected失去了执行时机;
  3. onResolved/onRejected这两个函数需要异步调用(官方Promise实现的回调函数总是异步调用的)。

解决办法:

  1. 根据规范解读第3条,定义并实现resolvePromise函数;
  2. then方法执行时如果promise仍然处于pending状态,则把处理函数进行储存,等resolve/reject函数真正执行的的时候再调用。
  3. promise.then属于微任务,这里我们为了方便,用宏任务setTiemout来代替实现异步,具体细节特别推荐这篇文章

好了,有了解决办法,我们就把代码进一步完善:

class Promise {
  // 定义Promise状态变量,初始值为pending
  status = 'pending';
  // 因为在then方法中需要处理Promise成功或失败时的值,所以需要一个全局变量存储这个值
  data = '';
  // Promise resolve时的回调函数集
  onResolvedCallback = [];
  // Promise reject时的回调函数集
  onRejectedCallback = [];

  // Promise构造函数,传入参数为一个可执行的函数
  constructor(executor) {
    // resolve函数负责把状态转换为resolved
    function resolve(value) {
      this.status = 'resolved';
      this.data = value;
      for (const func of this.onResolvedCallback) {
        func(this.data);
      }
    }
    // reject函数负责把状态转换为rejected
    function reject(reason) {
      this.status = 'rejected';
      this.data = reason;
      for (const func of this.onRejectedCallback) {
        func(this.data);
      }
    }

    // 直接执行executor函数,参数为处理函数resolve, reject。因为executor执行过程有可能会出错,错误情况需要执行reject
    try {
      executor(resolve, reject);
    } catch(e) {
      reject(e)
    }
  }
  /**
    * 拥有一个then方法
    * then方法提供:状态为resolved时的回调函数onResolved,状态为rejected时的回调函数onRejected
    * 返回一个新的Promise
  */
  then(onResolved, onRejected) {

    // 设置then的默认参数,默认参数实现Promise的值的穿透
    onResolved = typeof onResolved === 'function' ? onResolved : function(v) { return e };
    onRejected = typeof onRejected === 'function' ? onRejected : function(e) { throw e };

    let promise2;

    promise2 =  new Promise((resolve, reject) => {
      // 如果状态为resolved,则执行onResolved
      if (this.status === 'resolved') {
        setTimeout(() => {
          try {
            // onResolved/onRejected有返回值则把返回值定义为x
            const x = onResolved(this.data);
            // 执行[[Resolve]](promise2, x)
            this.resolvePromise(promise2, x, resolve, reject);
          } catch (e) {
            reject(e);
          }
        }, 0);
      }
      // 如果状态为rejected,则执行onRejected
      if (this.status === 'rejected') {
        setTimeout(() => {
          try {
            const x = onRejected(this.data);
            this.resolvePromise(promise2, x, resolve, reject);
          } catch (e) {
            reject(e);
          }
        }, 0);
      }
      // 如果状态为pending,则把处理函数进行存储
      if (this.status = 'pending') {
        this.onResolvedCallback.push(() => {
          setTimeout(() => {
            try {
              const x = onResolved(this.data);
              this.resolvePromise(promise2, x, resolve, reject);
            } catch (e) {
              reject(e);
            }
          }, 0);
        });

        this.onRejectedCallback.push(() => {
          setTimeout(() => {
            try {
              const x = onRejected(this.data);
              this.resolvePromise(promise2, x, resolve, reject);
            } catch (e) {
              reject(e);
            }
          }, 0);
        });
      }

    });

    return promise2;
  }

  // [[Resolve]](promise2, x)函数
  resolvePromise(promise2, x, resolve, reject) {
    
  }
  
}

至此,规范中关于then的部分就全部实现完毕了。代码添加了详细的注释,参考注释不难理解。

规范解读第3条,代码实现:

// [[Resolve]](promise2, x)函数
  resolvePromise(promise2, x, resolve, reject) {
    let called = false;

    if (promise2 === x) {
      return reject(new TypeError('Chaining cycle detected for promise!'))
    }
    
    // 如果x仍然为Promise的情况
    if (x instanceof Promise) {
      // 如果x的状态还没有确定,那么它是有可能被一个thenable决定最终状态和值,所以需要继续调用resolvePromise
      if (x.status === 'pending') {
        x.then(function(value) {
          resolvePromise(promise2, value, resolve, reject)
        }, reject)
      } else { 
        // 如果x状态已经确定了,直接取它的状态
        x.then(resolve, reject)
      }
      return
    }
  
    if (x !== null && (Object.prototype.toString(x) === '[object Object]' || Object.prototype.toString(x) === '[object Function]')) {
      try {
        // 因为x.then有可能是一个getter,这种情况下多次读取就有可能产生副作用,所以通过变量called进行控制
        const then = x.then 
        // then是函数,那就说明x是thenable,继续执行resolvePromise函数,直到x为普通值
        if (typeof then === 'function') { 
          then.call(x, (y) => { 
            if (called) return;
            called = true;
            this.resolvePromise(promise2, y, resolve, reject);
          }, (r) => {
            if (called) return;
            called = true;
            reject(r);
          })
        } else { // 如果then不是函数,那就说明x不是thenable,直接resolve x
          if (called) return ;
          called = true;
          resolve(x);
        }
      } catch (e) {
        if (called) return;
        called = true;
        reject(e);
      }
    } else {
      resolve(x);
    }
  }

这一步骤非常简单,只要按照规范转换成代码即可。

最后,完整的Promise按照规范就实现完毕了,是的,规范里并没有规定catchPromise.resolvePromise.rejectPromise.all等方法,接下来,我们就看一看Promise的这些常用方法。

Promise其他方法实现

1、catch方法

catch方法是对then方法的封装,只用于接收reject(reason)中的错误信息。因为在then方法中onRejected参数是可不传的,不传的情况下,错误信息会依次往后传递,直到有onRejected函数接收为止,因此在写promise链式调用的时候,then方法不传onRejected函数,只需要在最末尾加一个catch()就可以了,这样在该链条中的promise发生的错误都会被最后的catch捕获到。

  catch(onRejected) {
    return this.then(null, onRejected);
  }
2、done方法

catchpromise链式调用的末尾调用,用于捕获链条中的错误信息,但是catch方法内部也可能出现错误,所以有些promise实现中增加了一个方法donedone相当于提供了一个不会出错的catch方法,并且不再返回一个promise,一般用来结束一个promise链。

  done() {
    this.catch(reason => {
      console.log('done', reason);
      throw reason;
    });
  }
3、finally方法

finally方法用于无论是resolve还是rejectfinally的参数函数都会被执行。

  finally(fn) {
    return this.then(value => {
      fn();
      return value;
    }, reason => {
      fn();
      throw reason;
    });
  };
4、Promise.all方法

Promise.all方法接收一个promise数组,返回一个新promise2,并发执行数组中的全部promise,所有promise状态都为resolved时,promise2状态为resolved并返回全部promise结果,结果顺序和promise数组顺序一致。如果有一个promiserejected状态,则整个promise2进入rejected状态。

  static all(promiseList) {
    return new Promise((resolve, reject) => {
      const result = [];
      let i = 0;
      for (const p of promiseList) {
        p.then(value => {
          result[i] = value;
          if (result.length === promiseList.length) {
            resolve(result);
          }
        }, reject);
        i++;
      }
    });
  }
5、Promise.race方法

Promise.race方法接收一个promise数组, 返回一个新promise2,顺序执行数组中的promise,有一个promise状态确定,promise2状态即确定,并且同这个promise的状态一致。

  static race(promiseList) {
    return new Promise((resolve, reject) => {
      for (const p of promiseList) {
        p.then((value) => {
          resolve(value);   
        }, reject);
      }
    });
  }
6、Promise.resolve方法/Promise.reject

Promise.resolve用来生成一个rejected完成态的promisePromise.reject用来生成一个rejected失败态的promise

  static resolve(value) {
    let promise;

    promise = new Promise((resolve, reject) => {
      this.resolvePromise(promise, value, resolve, reject);
    });
  
    return promise;
  }
  
  static reject(reason) {
    return new Promise((resolve, reject) => {
      reject(reason);
    });
  }

常用的方法基本就这些,Promise还有很多扩展方法,这里就不一一展示,基本上都是对then方法的进一步封装,只要你的then方法没有问题,其他方法就都可以依赖then方法实现。

Promise面试相关

面试相关问题,笔者只说一下我司这几年的情况,并不能代表全部情况,参考即可。
Promise是我司前端开发职位,nodejs开发职位,全栈开发职位,必问的一个知识点,主要问题会分布在Promise介绍、基础使用方法以及深层次的理解三个方面,问题一般在3-5个,根据面试者回答情况会适当增减。

1、简单介绍下Promise。

Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。有了Promise对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise对象提供统一的接口,使得控制异步操作更加容易。
(当然了也可以简单介绍promise状态,有什么方法,callback存在什么问题等等,这个问题是比较开放的)

  • 提问概率:99%
  • 评分标准:人性化判断即可,此问题一般作为引入问题。
  • 加分项:熟练说出Promise具体解决了那些问题,存在什么缺点,应用方向等等。
2、实现一个简单的,支持异步链式调用的Promise类。

这个答案不是固定的,可以参考最简实现 Promise,支持异步链式调用

  • 提问概率:50%(手撸代码题,因为这类题目比较耗费时间,一场面试并不会出现很多,所以出现频率不是很高,但却是必备知识)
  • 加分项:基本功能实现的基础上有onResolved/onRejected函数异步调用,错误捕获合理等亮点。
3、Promise.then在Event Loop中的执行顺序。(可以直接问,也可以出具体题目让面试者回答打印顺序)

JS中分为两种任务类型:macrotaskmicrotask,其中macrotask包含:主代码块,setTimeoutsetIntervalsetImmediate等(setImmediate规定:在下一次Event Loop(宏任务)时触发);microtask包含:Promiseprocess.nextTick等(在node环境下,process.nextTick的优先级高于Promise
Event Loop中执行一个macrotask任务(栈中没有就从事件队列中获取)执行过程中如果遇到microtask任务,就将它添加到微任务的任务队列中,macrotask任务执行完毕后,立即执行当前微任务队列中的所有microtask任务(依次执行),然后开始下一个macrotask任务(从事件队列中获取)
浏览器运行机制可参考这篇文章

  • 提问概率:75%(可以理解为4次面试中3次会问到,顺便可以考察面试者对JS运行机制的理解)
  • 加分项:扩展讲述浏览器运行机制。
4、阐述Promise的一些静态方法。

Promise.deferredPromise.allPromise.racePromise.resolvePromise.reject

  • 提问概率:25%(相对基础的问题,一般在其他问题回答不是很理想的情况下提问,或者为了引出下一个题目而提问)
  • 加分项:越多越好
5、Promise存在哪些缺点。

1、无法取消Promise,一旦新建它就会立即执行,无法中途取消。
2、如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。
3、吞掉错误或异常,错误只能顺序处理,即便在Promise链最后添加catch方法,依然可能存在无法捕捉的错误(catch内部可能会出现错误)
4、阅读代码不是一眼可以看懂,你只会看到一堆then,必须自己在then的回调函数里面理清逻辑。

  • 提问概率:25%(此问题作为提高题目,出现概率不高)
  • 加分项:越多越合理越好(网上有很多说法,不一一佐证)
    (此题目,欢迎大家补充答案)
6、使用Promise进行顺序(sequence)处理。

1、使用async函数配合await或者使用generator函数配合yield
2、使用promise.then通过for循环或者Array.prototype.reduce实现。

function sequenceTasks(tasks) {
    function recordValue(results, value) {
        results.push(value);
        return results;
    }
    var pushValue = recordValue.bind(null, []);
    return tasks.reduce(function (promise, task) {
        return promise.then(() => task).then(pushValue);
    }, Promise.resolve());
}
  • 提问概率:90%(我司提问概率极高的题目,即能考察面试者对promise的理解程度,又能考察编程逻辑,最后还有bindreduce等方法的运用)
  • 评分标准:说出任意解决方法即可,其中只能说出async函数和generator函数的可以得到20%的分数,可以用promise.then配合for循环解决的可以得到60%的分数,配合Array.prototype.reduce实现的可以得到最后的20%分数。
7、如何停止一个Promise链?

在要停止的promise链位置添加一个方法,返回一个永远不执行resolve或者rejectPromise,那么这个promise永远处于pending状态,所以永远也不会向下执行thencatch了。这样我们就停止了一个promise链。

    Promise.cancel = Promise.stop = function() {
      return new Promise(function(){})
    }
  • 提问概率:50%(此问题主要考察面试者罗辑思维)
    (此题目,欢迎大家补充答案)
8、Promise链上返回的最后一个Promise出错了怎么办?

catchpromise链式调用的末尾调用,用于捕获链条中的错误信息,但是catch方法内部也可能出现错误,所以有些promise实现中增加了一个方法donedone相当于提供了一个不会出错的catch方法,并且不再返回一个promise,一般用来结束一个promise链。

  done() {
    this.catch(reason => {
      console.log('done', reason);
      throw reason;
    });
  }
  • 提问概率:90%(同样作为出题率极高的一个题目,充分考察面试者对promise的理解程度)
  • 加分项:给出具体的done()方法代码实现
9、Promise存在哪些使用技巧或者最佳实践?

1、链式promise要返回一个promise,而不只是构造一个promise
2、合理的使用Promise.allPromise.race等方法。
3、在写promise链式调用的时候,then方法不传onRejected函数,只需要在最末尾加一个catch()就可以了,这样在该链条中的promise发生的错误都会被最后的catch捕获到。如果catch()代码有出现错误的可能,需要在链式调用的末尾增加done()函数。

  • 提问概率:10%(出题概率极低的一个题目)
  • 加分项:越多越好

(此题目,欢迎大家补充答案)

至此,我司关于Promise的一些面试题目就列举完毕了,有些题目的答案是开放的,欢迎大家一起补充完善。总结起来,Promise作为js面试必问部分还是相对容易掌握并通过的。

总结

Promise作为所有js开发者的必备技能,其实现思路值得所有人学习,通过这篇文章,希望小伙伴们在以后编码过程中能更加熟练、更加明白的使用Promise。

参考链接:

http://liubin.org/promises-book
https://github.com/xieranmaya/blog/issues/3
https://segmentfault.com/a/1190000016550260

查看原文

赞 22 收藏 16 评论 0

小磊 关注了专栏 · 3月12日

有道技术团队

网易有道是中国领先的智能学习公司,致力于提供100%以用户为导向的学习产品和服务。 旗下有网易有道词典、有道精品课、有道云笔记、有道翻译官等多款深受用户喜爱的产品。

关注 5553