标签: promise

  • 在 Code.fun 做 Code Review(三):聊聊 Promise 的错误处理、如何真正学到技术

    在 Code.fun 做 Code Review(三):聊聊 Promise 的错误处理、如何真正学到技术

    嗯,不知不知觉这个系列写到第三篇,这一篇会改变一下写法,从一次 Code Review 出发,讲解几个技术点,然后分享一下技术学习的经验,以及 Code Review 的用途。希望对大家有帮助。

    顺便推广下前两篇:

    0. 起因

    某天,我给一位同事做 code review,看到下面这段代码:

    const err = new Error('错误信息');
    
    return Promise.reject(err);

    于是我就回复:error 最好直接 throw

    然后他回复我:如果改成 throw 的话,这个 catch 好像是捕获不到 throw 的。

    这就很诡异了,这不符合 Promise 的设计;而 Promise 不是新生事物,它有很大的测试集可以保证行为符合预期,我觉得我们想遇到问题都很困难。于是我就跟这位同事连线,帮他分析问题。

    1. 真正的问题

    连线之后,我发现他的真正问题并不是按照我的要求修改代码之后遇到的。由于基础不太牢靠,他先写了一段实验代码,想要验证我的要求,结果这段代码行为出现异常:

    他希望能够通过 .catch(err => console.log(err)) 捕获并记录错误,但是从控制台的输出来看,错误却是 Uncaught(未捕获)。那么问题出在哪儿呢?

    作为曾经讲解过 Promise 的我当然是一眼就看出来问题所在,但是这位同学却琢磨不透。于是我就把问题发到研发群里,果然又问倒好几位同学。

    现在我请问各位读者老爷,你们知道么?或者换个方向,如果想正常使用 .catch() 捕获到错误,应该怎么修改呢?

    2. 问题解答

    Promise

    这里我们必须回到 Promise 的规范,才能了解上面截图的问题所在(关于详细规范,请参阅 MDN Promise,这里只摘我们需要的部分):

    1. Promise 主要用来改进编写异步函数的体验。
    2. 通过 new Promise(function (resolve, reject) {}) 会创建一个 Promise 实例,其中的参数应该是一个函数(通常是异步函数),接受两个参数:resolvereject。当异步操作成功之后,应调用 resolve(result) 并传递结果;当异步操作失败,应调用 reject(reason) 并传递错误信息。
    3. Promise 有三个状态:pendingfulfilledrejected,起始状态是 pending,变更后,就固定下来,不会再次变更。
    4. 如果异步函数本身抛出错误,Promise 也会进入 rejected 状态。
    5. fulfilled 的 Promise 实例会转入 .then() 处理;rejected 会转入 .catch() 处理。

    我们再回头看上面的截图,这里的问题在于,错误是在 setTimeout(异步函数)的回调函数里抛出的,抛出时当前 Promise 所在的执行栈已经结束了,回调函数是全新开启的执行栈,所以 Promise 无法捕获到它里面的异常;而它也没有主动调用 reject(err) 传出错误,所以就变成了 Uncaught(未捕获)。

    函数执行栈

    既然说到执行栈,我们就顺便补充一下执行栈的知识吧。形如以下代码:

    function a() {
      b();
    }
    function b() {
      c();
    }
    function c() {
      d();
    }
    function d() {
      // do something
    }
    a();

    当函数 a 执行的时候,运行时会开启一个新的执行栈,并且把 a 入栈接下来发现 a 调用了 b,就又会把 b 入栈;然后发现 c……运行时会重复这个过程。当一个函数执行完毕,比如 d,运行时就会把它移出栈,然后继续执行 c 的剩下部分。

    对于 JavaScript 而言,错误是可以冒泡的。即在 d 抛出的异常,如果没有被捕获,它就会一直上浮,直到回到全局。所以我们可以在栈内的任何一个环节捕获它;但如果跨栈,那就无法捕获。

    Event Loop

    异步函数的回调函数会由 Event Loop 开启新栈执行,与所以异步函数自身处于不同的执行栈,所以错误不会被异步函数捕获,自然也不会改变 Promise 的状态。

    解决问题

    所以,要正确捕获错误的话,就需要想办法捕获异步函数的回调函数的错误,即:

    function yy() {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          // 方案1
          reject(new Error('错误信息'));
          // 方案2,实际中很少这么写,这里只是用来演示。但要注意,捕获必须放在回调栈,才能捕获到发生的错误。
          try {
            throw new Error('错误信息');
          } catch (e) {
            reject(e);
          }
        }, 1000);
    
        // 实际场景中,更多是这样的,err 作为参数传给回调函数
        fs.writeFile(path, content, 'utf8', (err) => {
          if (err) {
            return reject(err);
          }
          resolve();
        });
      });
    }

    3. 如何学习技术

    对于上述问题,我们只要掌握 Promise 规范、函数执行栈、Event Loop,就很容易判断。但是如果这三个概念有一个不太清楚,就容易犯迷糊。所以,很多时候,要避免出问题、要保证软件正常工作,我们必须清楚了解每个技术的规范、定义。

    比如 Promise、原型链、闭包等等,它们都不是自然界的产物;而是在长期软件开发实践中,由开发者总结设计出来,用于解决特定问题的发明。他们都有严格的定义、功用、优缺点,等等。我们日常开发,应该把这些东西当成知识储备起来,针对需求做排列组合,给出解决方案。

    有些同学相反,他们会记下来一些技术的用法,然后反推这些技术的特性。如果对一个技术不熟悉,他们会尝试做类似实际场景的实验,然后再想办法搬到实际场景中。这样做,覆盖常见场景可能没问题,遇到陌生的领域就容易踩坑。

    所以,我要强调,对于新技术,大家不熟悉又要尽快用在实际开发中,临时做点实验记住些经验用法当然可以。但不应满足于此,要找时间把技术规范补起来,把完整的设计至少读个几遍。一方面纠正自己的错误实践,另一方面,也可以扩大你对这个技术的应用面。

    4. Code Review 的作用

    这里也不得不提 Code Review 的一个重要作用:

    传承知识。Code Reviewe 是非常好的查缺补漏机会,可以针对性补强开发者的知识盲区,纠正不良习惯。

    通过 Code Review,我发现了一位同事在 Promise 和函数执行栈方面存在知识盲区,然后我借机帮他补齐了这方面的知识。接下来,我把这个问题分享到技术研讨群,还有几位同学也不是很清楚,也趁机补齐了。于是,我厂再出现这个问题的概率,就降低了。换言之,我厂技术群体的下限,就拔高了。

    思否上有同学问:什么样的技术 leader 是称职的? 我的答案第一条就是:

    给团队的技术兜底。通过工具、规范、流程,保证无论开发水平如何,都能尽快提交符合要求、满足规范、质量过硬的代码。

    具体一点:要重视每一次 Code Review,找到问题,解决问题,补全大家的知识点,提升团队下限。

    5. 总结

    上次的 Code Review 分享获得了意料之外的欢迎,希望这次同样能帮助到大家——我已经把上面的问题加入我的面试题库了,哈哈。

    我现在在 code.fun 工作,我们的产品会把设计稿自动转换成代码,期望大大提升前端的开发效率,如果你对这个产品感兴趣,欢迎联系我,或直接试用。我们公司提供远程工作岗位,有兴趣的同学可以联系我;朋友的公司 API7 也在招聘前端,有兴趣可以找我内推。

    如果诸位读者老爷对软件质量管理、软件开发效率、Code Review 有什么想问的、想说的,敬请在评论区与我互动。

    6. 扩展阅读

  • babel@6 升级到 babel@7,兼容性代码膨胀的原因

    babel@6 升级到 babel@7,兼容性代码膨胀的原因

    最近尝试把厂里项目的依赖从 babel@6 升级到 babel@7,发现打包之后体积大了很多。于是打开 webpack-bundle-analyzer,果然大部分代码都是 corejs 引入的,项目本身的逻辑只占少部分。

    从报告来看,虽然目标浏览器的版本均高于 Promise 的启动版本(比如 Chrome 32),但 es.promise 仍然会被打包进来。于是以 es.promise 为突破口开始分析,找到答案:因为 JavaScript 引擎 V8 直至 v6.6 版本时,在 Promise 实现方面都存在严重 bug,所以 babel@7 保守地选择 Chrome 66/67 作为临界点。

    想来其它体积多半也是这么加上来的,就不再一个一个排查了。 ​​​

    这就很难处理。不升级吧,新特性兼容不了;升级吧,包体积变大,公司上层又未必同意。

    下一步试试 esbuild 吧,或者回头手动打包一套 polyfill。目前设想的方案是:

    1. 用 babel 之类的工具提取出所有特性
    2. 根据 caniuse 生成必须的特性列表
    3. 像上面说的 Promise,因为 bug 所以必须兼容,我们就不考虑了,可以反过来加一条 eslint 规则规避
    4. 最终生成新的 polyfill 打包进来
    (更多…)
  • 记一个 `try…catch` 异步函数的坑

    记一个 `try…catch` 异步函数的坑

    前几天遇到一个问题:想捕获异步函数的错误,但是捕获不到。我的代码大概是这样:

    try {
      (async () => {
        // 一大堆异步函数
      })();
    } catch (e) {
      if (e.message.startsWith('my error:') {
        // 我的错误,提示一下即可
      } else {
        // 某种不知名错误,继续抛出
        throw new Error(e.message);
      }
    }

    不知道读者是否发现问题,我当时是左看右看没看出来。后来只好写最小用例缩小问题范围,终于找到问题所在:异步函数前面缺少 await。于是想通了,上面的代码其实被引擎解释成:

    1. try 一个函数
    2. 这个函数的返回值是一个 Promise
    3. Promise 是正常对象,所以 try 成功
    4. 至于 Promise 里的函数是否失败,不关 try 的事,所以自然捕获不到

    所以正确的写法是:

    // 外面先套一层,不然不能用 await
    (async () => {
      try {
        // 再加上 `await`
        await (async () => {
          // 异步函数体
        })();
      } catch (e) {
        // ....
      }
    })();

    这个 await 至关重要。这是异步函数的一大特性,即 try...catch 可以捕获整个异步操作前后所有栈的异常,而不仅仅是当前函数的异常。同时我们也要注意,异步函数的调用可能并不在当前栈,也无法被 try...catch 在当前栈捕获到错误。

  • 解决 Firefox 下的 race 问题

    解决 Firefox 下的 race 问题

    我厂有几个产品,需要从后端获取大量的信息,为了让用户能够近乎实时的看到这些信息,大部分数据都是通过 WebSocket 发给前端。这些产品在 Chrome 下表现正常,但是在 Firefox 下经常把数据格式搞乱,最终渲染失败。

    因为 Firefox DevTools 没法解析 WebSocket 数据,而且市场占有率比较低,所以我一直没有解决这个问题。前几天终于把最小可复现实例搞出来,正准备研究,结果同事已经修好了。

    预览版的 Firefox 终于可以在 DevTools 里查看 WebSocket 每一帧的数据,所以她尝试看了一下,发现从解析二进制数据的角度来看,Firefox 应该没问题。于是又回到代码,发现了一个可能产生 race 的点:

    if (data instanceof Blob) {
      data = await new Response(data).arrayBuffer();
    }

    因为服务器返回的数据是二进制,所以我需要进行一次转换,把它变成 ArrayBuffer,然后再通过 TextDecoder 转换成文本,然后处理。Response.arrayBuffer 返回的是 Promise,所以我就很自然的用 await,并且在 Chrome 上运行良好。

    但是在 Firefox 里,某些帧会后发先完成转换,a b c 变成 a c b,于是数据格式错乱,无法正常解析。我怀疑 Chrome 并没有真的把这一步保留到用户,而是同时存了两份数据,这样转换的时候直接给出数据就好,所以是微任务,不走 Event loop,不会产生 race。而 Firefox 则是实时转换,所以是宏任务,所以出问题。

    我尝试去翻了一下源码,无奈平时没看过,所以没能找到证据。如果有哪位同学刚好知道,可以在评论里告诉我。

  • Node.js 里使用 Promise 的小技巧

    Node.js 里使用 Promise 的小技巧

    Node.js 8 的时候,引入了 util.promisify() 方法,可以把 node-like 的回调函数改造成返回 Promise 实例的方法,我当时还写了篇博文《Node.js 8 中的 util.promisify》小记。

    所以我现在写 Node.js 基本都是这种风格:

    const fs = require('fs');
    const {promisify} = require('util');
    
    const readFile = promisify(fs.readFile);
    const writeFile = promisify(fs.writeFile);

    刚才在推上看到两位大佬聊起这个话题,发现可以这么搞:

    去查了一下 Node.js 的文档,发现这是 v10 新增的 API,升级之后即可使用。

    我比较喜欢这么做:

    const {promises: {readFile, writeFile}} = require('fs');
    
    (async () => {
      let content = await readFile('1.txt', 'utf8');
      content = doSthToContent(content);
      await writeFile('2.txt', content, 'utf8');
      console.log('ok');
    })();
  • Promise 改造 child_process.exec

    Promise 改造 child_process.exec

    child_process 是 Node.js 的一个内建模块,用于分裂出(spawn)一个子进程,执行一些特定操作。.exec() 是它的方法,接受一个参数,即要执行的 shell 命令,然后通过回调返回结果。.exec().spawn() 的不同之处在于,前者重在返回结果,后者则重在返回内容。所以当你需要执行一个命令,你并不关心执行过程中发生了什么,只要看到结果就好,那么就用 .exec();反之,假如执行过程中产生的信息对你特别有价值,你并不是特别在意结果,就应该用 .spawn()

    另外,我之前在《Node.js 8 中的 util.promisify》中介绍过,Node.js 8 引入了一个新函数,位于 util 模块,叫做 promisify(),用于将回调风格的 Node.js 函数改造成 Promise 规范的函数。

    OK,背景知识介绍结束。近期开发中,我需要执行一个命令,并且取得它的 stdoutstderrexit code,使用 promisify() 之后发现没有 exit code,于是只好重新写了一下,代码如下:

    import {exec as BaseExec} from 'util';
    
    function exec(command, options) {
      return new Promise((resolve, reject) => {
        let result = {};
        const cp = baseExec(command, options, (err, stdout, stderr) => {
          if (err) {
            err.stdout = stdout;
            err.stderr = stderr;
            reject(err);
            return;
          }
    
          result.stdout = stdout;
          result.stderr = stderr;
          if ('code' in result) {
            resolve(result);
          }
        });
    
        cp.on('exit', (code, signal) => {
          result.code = code;
          result.signal = signal;
          if ('stdout' in result) {
            resolve(result);
          }
        });
      });
    }
    

    希望对大家有用。

    新键盘到了,FC660C,静电容,试用一下,效果还不错。略硬,段落感不强,声音不大。

  • JavaScript 异步开发全攻略

    JavaScript 异步开发全攻略

    之前在 GitChat 做过一次分享:《JavaScript 异步开发全攻略》。在我看来,原始内容可能不够完美,但通过后来的维护,可以把它打磨得越来越好。昨天在 SF 上回答了一个 Vuex 的 action 里使用 Promise 的问题,然后就想去补充一下这方面的内容。结果发现 GitChat 竟然不支持编辑文章,只能把内容发给运营人工修改。

    索性把内容放到 Gitbook 上好了,反正 GitChat SEO 也不做,文章也自然移动到第二页了。试了试,没有被墙,很好。于是简单整理了一下,上传。

    欢迎阅读,欢迎分享,因为我会不时更新新内容,请关注 star:

    JavaScript 异步开发全攻略

  • 第一场 GitChat 总结

    第一场 GitChat 总结

    开始之前,先做广告吧。

    GitChat 分享 《JavaScript 异步开发全攻略》

    为解决异步函数的回调陷阱,开发社区不断摸索,终于折腾出 Promise/A+。它不增加新的语法,可以适配几乎所有浏览器;以队列的形式组织代码,易读好改;捕获异常方案也基本可用。这套方案在迭代中逐步完善,最终被吸收进 ES2015。不仅如此,ES2017 中还增加了 Await/Async,可以用顺序的方式书写异步代码,甚至可以正常抛出捕获错误,维护同一个栈。可以说彻底解决了异步回调的问题。 现在大部分浏览器和 Node.js 都已原生支持 Promise,很多类库也开始返回 Promise 对象,更有各种降级适配策略。Node.js 7+ 则实装了 Await/Async。如果您现在还不会使用,那么我建议您尽快学习一下。

    下次直播分享 前端面试攻略:JavaScript 排序与搜索

    从事前端开发的同学很多从页面仔入门,比如说我,自学比例很大,有些时候会无意中忽视一些基础,比如算法、数据结构。这些欠缺在某些时候就会显得很致命,比如说面试,或者处理大量数据的场景。所以希望这样的一场分享能够帮助大家夯实原本不太扎实的基础,将来的开发之路更加顺畅。

    目前早鸟票发送中,7月13日前门票5折,19日前75折,开播当日恢复全价。

    (更多…)

  • 【修正】Promise N种用法-异步回调的问题-findLargest 解析

    【修正】Promise N种用法-异步回调的问题-findLargest 解析

    做慕课视频的时候,仔细琢磨了一下,发现之前讲的还是有问题,所以重新录了一遍。

    (更多…)

  • Node.js 8 中的 util.promisify

    Node.js 8 中的 util.promisify

    Node.js 8 于上个月月底正式发布,带来了很多新特性。其中比较值得注意的,便有 util.promisify() 这个方法。

    如果你已经很熟悉 Promise,请继续往下看。如果你还不熟悉 Promise,可以先跳过去看下下章:Promise 介绍

    util.promisify()

    虽然 Promise 已经普及,但是 Node.js 里仍然有大量依赖回调的异步函数,如果我们把每个函数都封装一遍,那真是齁麻烦齁麻烦的,比齁还麻烦。

    所以 Node.js 8 就提供了 util.promisify() 这个方法,方便我们把原来的异步回调方法改成支持 Promise 的方法,接下来,想继续 .then().then().then() 搞队列,还是 await 就看实际需要了。

    我们看下范例,让读取目录文件状态的 fs.stat 支持 Promise:

    const util = require('util');
    const fs = require('fs');
    
    const stat = util.promisify(fs.stat);
    stat('.')
      .then((stats) => {
        // Do something with `stats`
      })
      .catch((error) => {
        // Handle the error.
      });
    

    怎么样,很简单吧?按照文档的说法,只要符合 Node.js 的回调风格,所有函数都可以这样转换。也就是说,只要满足下面两个条件,无论是不是原生方法,都可以:

    1. 最后一个参数是回调函数
    2. 回调函数的参数为 (err, result),前面是可能的错误,后面是正常的结果

    结合 Await/Async 使用

    同样是上面的例子,如果想要结合 Await/Async,可以这样使用:

    const util = require('util');
    const fs = require('fs');
    
    const stat = util.promisify(fs.stat);
    async function readStats(dir) {
      try {
        let stats = await stat(dir);
        // Do something with `stats`
      } catch (err) { // Handle the error.
        console.log(err);
      }
    }
    readStats('.');
    

    自定义 Promise 化处理函数

    那如果现有的使用回调的函数不符合这个风格,还能用 util.promisify() 么?答案也是肯定的。我们只要给函数增加一个属性 util.promisify.custom,指定一个函数作为 Promise 化处理函数,即可。请看下面的代码:

    const util = require('util');
    
    // 这就是要处理的使用回调的函数
    function doSomething(foo, callback) { 
      // ...
    }
    
    // 给它增加一个方法,用来在 Promise 化时调用
    doSomething[util.promisify.custom] = function(foo) { 
      // 自定义生成 Promise 的逻辑
      return getPromiseSomehow(); 
    };
    
    const promisified = util.promisify(doSomething);
    console.log(promisified === doSomething[util.promisify.custom]);
    // prints 'true'
    

    如此一来,任何时候我们对目标函数 doSomething 进行 Promise 化处理,都会得到之前定义的函数。运行它,就会按照我们设计的特定逻辑返回 Promise 实例。

    我们就可以升级以前所有的异步回调函数了。

    (更多…)