【译】关于 Promise 的 9 个提示

提升Promise技巧
本文提供九条实用建议帮助读者更好地理解和运用JavaScript中的Promise,包括在.then中返回Promise、利用Promise.all并正确处理错误。

正如同事所说的那样,Promise 在工作中表现优异。

这篇文章会给你一些如何改善与 Promise 之间关系的建议。

1. 你可以在 .then 里面 return 一个 Promise

让我来说明这最重要的一点

是的!你可以在 .then 里面 return 一个 Promise

而且,return 的这个 Promise 将在下一个 .then 中自动解析。

.then(r => {
    return serverStatusPromise(r); // 返回 { statusCode: 200 } 的 Promise
})
.then(resp => {
    console.log(resp.statusCode); // 200; 注意自动解析的 promise
})
复制代码

2. 每次执行 .then 的时候都会自动创建一个新的 Promise

如果熟悉 javascript 的链式风格,那么你应该会感到很熟悉。但是对于一个初学者来说,可能就不会了。

在 Promise 中不论你使用 .then 或者 .catch 都会创建一个新的 Promise。这个 Promise 是刚刚链式调用的 Promise 和 刚刚加上的 .then / .catch 的组合。

让我们来看一个 ?:

var statusProm = fetchServerStatus();

var promA = statusProm.then(r => (r.statusCode === 200 ? "good" : "bad"));

var promB = promA.then(r => (r === "good" ? "ALL OK" : "NOTOK"));

var promC = statusProm.then(r => fetchThisAnotherThing());
复制代码

上面 Promise 的关系可以在流程图中清晰的描述出来:

需要特别注意的是 promApromBpromC 全部都是不同的但是有关联的 Promise。

我喜欢把 .then 想像成一个大型管道,当上游节点出现问题时,水就会停止流向下游。例如,如果 promB 失败,下游节点不会受到影响,但是如果 statusProm 失败,那么下游的所有节点都将受到影响,即 rejected

3. 对调用者来说,Promiseresolved/rejected 状态是唯一的

我认为这个是让 Promise 好好运行的最重要的事情之一。简单来说,如果在你的应用中 Promise 在很多不同的模块之间共享,那么当 Promise 返回 resolved/rejected 状态时,所有的调用者都会收到通知。

这也意味着没有人可以改变你的 Promise,所以可以放心的把它传递出去。

function yourFunc() {
  const yourAwesomeProm = makeMeProm();

  yourEvilUncle(yourAwesomeProm); // 无论 Promise 受到了怎样的影响,它最终都会成功执行

  return yourAwesomeProm.then(r => importantProcessing(r));
}

function yourEvilUncle(prom) {
  return prom.then(r => Promise.reject("destroy!!")); // 可能遭受的影响
}
复制代码

通过上面的例子可以看出,Promise 的设计使得自身很难被改变。正如我上面所说的:"保持冷静,并将 Promise 传递下去"。

4. Promise 构造函数不是解决方案

我看到很多开发者喜欢用构造函数的风格,他们认为这就是 Promise 的方式。但这却是一个谎言,实际的原因是构造函数 API 和之前回调函数的 API 相似,而且这样的习惯很难改变。

如果你发现自己正在到处使用 Promise 构造函数,那你的做法是错的!

要真正的向前迈进一步并且摆脱回调,你需要小心谨慎并且最小程度地使用 Promise 构造函数。

让我们看一下使用 Promise 构造函数 的具体情况:

return new Promise((res, rej) => {
  fs.readFile("/etc/passwd", function(err, data) {
    if (err) return rej(err);
    return res(data);
  });
});
复制代码

Promise 构造函数 应该只在你想要把回调转换成 Promise 时使用。 一旦你掌握了这种创建 Promise 的优雅方式,它将会变的非常有吸引力。

让我们看一下冗余的 Promise 构造函数

☠️错误的

return new Promise((res, rej) => {
    var fetchPromise = fetchSomeData(.....);
    fetchPromise
        .then(data => {
            res(data); // 错误!!!
        })
        .catch(err => rej(err))
})
复制代码

?正确的

return fetchSomeData(...); // 正确的!
复制代码

Promise 构造函数 封装 Promise 是多余的,并且违背了 Promise 本身的目的

?高级技巧

如果你是一个 nodejs 开发者,我建议你可以看一看 util.promisify。这个方法可以帮助你把 node 风格的回调转换为 Promise。

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

const readFileAsync = promisify(fs.readFile);

readFileAsync('myfile.txt', 'utf-8')
  .then(r => console.log(r))
  .catch(e => console.error(e));
复制代码

5. 使用 Promise.resolve

Javascript 提供了 Promise.resolve 方法,像下面的例子这样简洁:

var similarProm = new Promise(res => res(5));
// ^^ 等价于
var prom = Promise.resolve(5);
复制代码

它有多种使用情况,我最喜欢的一种是可以把普通的(异步的)js 对象转化成 Promise。

// 将同步函数转换为异步函数
function foo() {
  return Promise.resolve(5);
}
复制代码

当不确定它是一个 Promise 还是一个普通的值的时候,你也可以做一个安全的封装。

function goodProm(maybePromise) {
  return Promise.resolve(maybePromise);
}

goodProm(5).then(console.log); // 5

var sixPromise = fetchMeNumber(6);

goodProm(sixPromise).then(console.log); // 6

goodProm(Promise.resolve(Promise.resolve(5))).then(console.log); // 5, 注意,它会自动解析所有的 Promise!
复制代码

6.使用 Promise.reject

Javascript 也提供了 Promise.reject 方法。像下面的例子这样简洁:

var rejProm = new Promise((res, reject) => reject(5));

rejProm.catch(e => console.log(e)) // 5
复制代码

我最喜欢的用法是提前使用 Promise.reject 来拒绝。

function foo(myVal) {
    if (!mVal) {
        return Promise.reject(new Error('myVal is required'))
    }
    return new Promise((res, rej) => {
        // 从你的大回调到 Promise 的转换!
    })
}
复制代码

简单来说,使用 Promise.reject 可以拒绝任何你想要拒绝的 Promise。

在下面的例子中,我在 .then 里面使用:

.then(val => {
  if (val != 5) {
    return Promise.reject('Not Good');
  }
})
.catch(e => console.log(e)) // 这样是不好的
复制代码

注意:你可以像 Promise.resolve 一样在 Promise.reject 中传递任何值。你经常在失败的 Promise 中发现 Error 的原因是因为它主要就是用来抛出一个异步错误的。

7. 使用 Promise.all

Javascript 提供了 Promise.all 方法。像 ... 这样的简洁,好吧,我想不出来例子了?。

在伪算法中,Promise.all 可以被概括为:

接收一个 Promise 数组

    然后同时运行他们

    然后等到他们全部运行完成

    然后 return 一个新的 Promise 数组

    他们其中有一个失败或者 reject,都可以被捕获。
复制代码

下面的例子展示了所有的 Promise 完成的情况:

var prom1 = Promise.resolve(5);
var prom2 = fetchServerStatus(); // 返回 {statusCode: 200} 的 Promise

Proimise.all([prom1, prom2])
.then([val1, val2] => { // 注意,这里被解析成一个数组
    console.log(val1); // 5
    console.log(val2.statusCode); // 200
})
复制代码

下面的例子展示了当他们其中一个失败的情况:

var prom1 = Promise.reject(5);
var prom2 = fetchServerStatus(); // 返回 {statusCode: 200} 的 Promise

Proimise.all([prom1, prom2])
.then([val1, val2] => {
    console.log(val1); 
    console.log(val2.statusCode); 
})
.catch(e =>  console.log(e)) // 5, 直接跳转到 .catch
复制代码

注意:Promise.all 是很聪明的!如果其中一个 Promise 失败了,它不会等到所有的 Promise 完成,而是立即中止!

8. 不要害怕 reject,也不要在每个 .then 后面加冗余的 .catch

我们是不是会经常担心错误会在它们之间的某处被吞噬?

为了克服这个恐惧,这里有一个简单的小提示:

让 reject 来处理上游函数的问题。

在理想的情况下,reject 方法应该是应用的根源,所有的 reject 都会向下传递。

不要害怕像下面这样写

return fetchSomeData(...);
复制代码

现在如果你想要处理函数中 reject 的情况,请决定是解决问题还是继续 reject。

? 解决 reject

解决 reject 是很简单的,在 .catch 不论你返回什么内容,都将被假定为已解决的。然而,如果你在 .catch 中返回 Promise.reject,那么这个 Promise 将会是失败的。

.then(() => 5.length) // <-- 这里会报错
.catch(e => {
        return 5;  // <-- 重新使方法正常运行
})
.then(r => {
    console.log(r); // 5
})
.catch(e => {
    console.error(e); // 这个方法永远不会被调用 :)
})
复制代码

?拒绝一个 reject

拒绝一个 reject 是简单的。不需要做任何事情。 就像我刚刚说的,让它成为其他函数的问题。通常情况下,父函数有比当前函数处理 reject 更好的方法。

需要记住的重要的一点是,一旦你写了 catch 方法,就意味着你正在处理这个错误。这个和同步 try/catch的工作方式相似。

如果你确实想要拦截一个 reject:(我强烈建议不要这样做!)

.then(() => 5.length) // <-- 这里会报错
.catch(e => {
  errorLogger(e); // 做一些错误处理
  return Promise.reject(e); // 拒绝它,是的,你可以这么做!
})
.then(r => {
    console.log(r); // 这个 .then (或者任何后面的 .then) 将永远不会被调用,因为我们在上面使用了 reject :)
})
.catch(e => {
    console.error(e); //<-- 它变成了这个 catch 方法的问题
})
复制代码

.then(x,y) 和 then(x).catch(x) 之间的分界线

.then 接收的第二个回调函数参数也可以用来处理错误。它和 then(x).catch(x) 看起来很像,但是他们处理错误的区别在于他们自身捕获的错误。

我会用下面的例子来说明这一点:

.then(function() {
   return Promise.reject(new Error('something wrong happened'));
}).catch(function(e) {
   console.error(e); // something wrong happened
});

.then(function() {
   return Promise.reject(new Error('something wrong happened'));
}, function(e) { // 这个回调处理来自当前 `.then` 方法之前的错误
    console.error(e); // 没有错误被打印出来
});
复制代码

当你想要处理的是来自上游 Promise 而不是刚刚在 .then 里面加上去的错误的时候, .then(x,y) 变的很方便。

提示: 99.9% 的情况使用简单的 then(x).catch(x) 更好。

9. 避免 .then 回调地狱

这个提示是相对简单的,尽量避免 .then 里包含 .then 或者 .catch。相信我,这比你想象的更容易避免。

☠️错误的

request(opts)
.catch(err => {
  if (err.statusCode === 400) {
    return request(opts)
           .then(r => r.text())
           .catch(err2 => console.error(err2))
  }
})
复制代码

?正确的

request(opts)
.catch(err => {
  if (err.statusCode === 400) {
    return request(opts);
  }
})
.then(r => r.text())
.catch(err => console.erro(err));
复制代码

有些时候我们在 .then 里面需要很多变量,那就别无选择了,只能再创建一个 .then 方法链。

.then(myVal => {
    const promA = foo(myVal);
    const promB = anotherPromMake(myVal);
    return promA
          .then(valA => {
              return promB.then(valB => hungryFunc(valA, valB)); // 很丑陋!
          })
})
复制代码

我推荐使用 ES6 的解构方法混合着 Promise.all 方法就可以解决这个问题。

.then(myVal => {
    const promA = foo(myVal);
    const promB = anotherPromMake(myVal);
    return Promise.all([prom, anotherProm])
})
.then(([valA, valB]) => {   // 很好的使用 ES6 解构
    console.log(valA, valB) // 所有解析后的值
    return hungryFunc(valA, valB)
})
复制代码

注意:如果你的 node/浏览器/老板/意识允许,还可以使用 async/await 方法来解决这个问题。

我真心希望这篇文章对你理解 Promise 有所帮助。

请查看我之前的博客文章。

如果你 ❤️ 这篇文章,请分享这篇文章来传播它。

在 Twitter 上联系我 @kushan2020


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

React 核心理解 react16之前更新 React Fiber是16版本之后的一种更新机制,使用链表取代了树,是一种fiber数据结构,其有三个指针,分别指向了父节点、子节点、兄弟节点,当中断的时候会记录下当前的节点,然后继续更新,而15版本中的DOM stack不能有中断操作,它把组件渲染的工作分片,到时会主动让出渲染主线程;提炼fiber的关键词,大概给出如下几点: fiber是一种数据结构。 fiber使用父子关系以及next的妙用,以链表形式模拟了传统调用栈。 fiber是一种调度让出机制,只在有剩余时间的情况下运行。 fiber实现了增量渲染,在浏览器允许的情况下一点点拼凑出最终渲染效果。 fiber实现了并发,为任务赋予不同优先级,保证了一有时间总是做最高优先级的事,而不是先来先占位死板的去执行。 fiber有协调与提交两个阶段,协调包含了fiber创建与diff更新,此过程可暂停。而提交必须同步执行,保证渲染不卡顿。 react17更新 1、新的JSX转换,不需要手动引入react React 16: babel-loader会预编JSX为 React.createElement(...) React 17: React 17中的 JSX 转换不会将 JSX 转换为 React.createElement,而是自动从 React 的 package 中引入react并调用。 另外此次升级不会改变 JSX 语法,旧的 JSX 转换也将继续工作。 2、事件代理更改 在React 17中,将不再在后台的文档级别附加事件处理程序,不在document对象上绑定事件,改为绑定于每个react应用的rootNode节点,因为各个应用的rootNode肯定不同,所以这样可以使多个版本的react应用同时安全的存在于页面中,不会因为事件绑定系统起冲突。react应用之间也可以安全的进行嵌套。 3、事件池(event pooling)的改变 React 17去除了事件池(event pooling),不在需要e.persist(),现在可以直接在异步事件中(回掉或timeout等)拿到事件对象,操作更加直观,不会令人迷惑。e.persist()仍然可用,但是不会有任何效果。 4、异步执行 React 17将副作用清理函数(useEffect)改为异步执行,即在浏览器渲染完毕后执行。 5、forwardRef 和 memo组件的行为 React 17中forwardRef 和 memo组件的行为会与常规函数组件和class组件保持一致。它们在返回undefined时会报错。 react18更新 并发模式 v18的新特性是使用现代浏览器的特性构建的,彻底放弃对 IE 的支持。 v17 和 v18 的区别就是:从同步不可中断更新变成了异步可中断更新,v17可以通过一些试验性的API开启并发模式,而v18则全面开启并发模式。 并发模式可帮助应用保持响应,并根据用户的设备性能和网速进行适当的调整,该模式通过使渲染可中断来修复阻塞渲染限制。在 Concurrent 模式中,React 可以同时更新多个状态。 这里参考下文区分几个概念: 并发模式是实现并发更新的基本前提 v18 中,以是否使用并发特性作为是否开启并发更新的依据。 并发特性指开启并发模式后才能使用的特性,比如:useDeferredValue/useTransition 更新 render API v18 使用 ReactDOM.createRoot() 创建一个新的根元素进行渲染,使用该 API,会自动启用并发模式。如果你升级到v18,但没有使用ReactDOM.createRoot()代替ReactDOM.render()时,控制台会打印错误日志要提醒你使用React,该警告也意味此项变更没有造成breaking change,而可以并存,当然尽量是不建议。 自动批处理 批处理是指 React 将多个状态更新,聚合到一次 render 中执行,以提升性能 在v17的批处理只会在事件处理函数中实现,而在Promise链、异步代码、原生事件处理函数中失效。而v18则所有的更新都会自动进行批处理。 Suspense 支持 SSR SSR 一次页面渲染的流程: 服务器获取页面所需数据 将组件渲染成 HTML 形式作为响应返回 客户端加载资源 (hydrate)执行 JS,并生成页面最终内容 上述流程是串行执行的,v18前的 SSR 有一个问题就是它不允许组件"等待数据",必须收集好所有的数据,才能开始向客户端发送HTML。如果其中有一步比较慢,都会影响整体的渲染速度。 v18 中使用并发渲染特性扩展了Suspense的功能,使其支持流式 SSR,将 React 组件分解成更小的块,允许服务端一点一点返回页面,尽早发送 HTML和选择性的 hydrate, 从而可以使SSR更快的加载页面: startTransition Transitions 是 React 18 引入的一个全新的并发特性。它允许你将标记更新作为一个 transitions(过渡),这会告诉 React 它们可以被中断执行,并避免回到已经可见内容的 Suspense 降级方案。本质上是用于一些不是很急迫的更新上,用来进行并发控制 在v18之前,所有的更新任务都被视为急迫的任务,而Concurrent Mode 模式能将渲染中断,可以让高优先级的任务先更新渲染。 React 的状态更新可以分为两类: 紧急更新:比如点击按钮、搜索框打字是需要立即响应的行为,如果没有立即响应给用户的体验就是感觉卡顿延迟 过渡/非紧急更新:将 UI 从一个视图过渡到另一个视图。一些延迟可以接受的更新操作,不需要立即响应 startTransition API 允许将更新标记为非紧急事件处理,被startTransition包裹的会延迟更新的state,期间可能被其他紧急渲染所抢占。因为 React 会在高优先级更新渲染完成之后,才会渲染低优先级任务的更新 React 无法自动识别哪些更新是优先级更高的。比如用户的键盘输入操作后,setInputValue会立即更新用户的输入到界面上,是紧急更新。而setSearchQuery是根据用户输入,查询相应的内容,是非紧急的。 React无法自动识别,所以它提供了 startTransition让我们手动指定哪些更新是紧急的,哪些是非紧急的,从而让我们改善用户交互体验。 useTransition 当有过渡任务(非紧急更新)时,我们可能需要告诉用户什么时候当前处于 pending(过渡) 状态,因此v18提供了一个带有isPending标志的 Hook useTransition来跟踪 transition 状态,用于过渡期。 useTransition 执行返回一个数组。数组有两个状态值: isPending: 指处于过渡状态,正在加载中 startTransition: 通过回调函数将状态更新包装起来告诉 React这是一个过渡任务,是一个低优先级的更新 直观感觉这有点像 setTimeout,而防抖节流其实本质也是setTimeout,区别是防抖节流是控制了执行频率,让渲染次数减少了,而 v18的 transition 则没有减少渲染的次数。 useDeferredValue useDeferredValue 和 useTransition 一样,都是标记了一次非紧急更新。useTransition是处理一段逻辑,而useDeferredValue是产生一个新状态,它是延时状态,这个新的状态则叫 DeferredValue。所以使用useDeferredValue可以推迟状态的渲染 useDeferredValue 接受一个值,并返回该值的新副本,该副本将推迟到紧急更新之后。如果当前渲染是一个紧急更新的结果,比如用户输入,React 将返回之前的值,然后在紧急渲染完成后渲染新的值。 JavaScript 运行代码 复制代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 function Typeahead() { const query = useSearchQuery(&#39;&#39;); const deferredQuery = useDeferredValue(query); // Memoizing 告诉 React 仅当 deferredQuery 改变, // 而不是 query 改变的时候才重新渲染 const suggestions = useMemo(() => <SearchSuggestions query={deferredQuery} />, [deferredQuery] ); return ( <> <SearchInput query={query} /> <Suspense fallback="Loading results..."> {suggestions} </Suspense> </> ); } 这样一看,useDeferredValue直观就是延迟显示状态,那用防抖节流有什么区别呢? 如果使用防抖节流,比如延迟300ms显示则意味着所有用户都要延时,在渲染内容较少、用户CPU性能较好的情况下也是会延迟300ms,而且你要根据实际情况来调整延迟的合适值;但是useDeferredValue是否延迟取决于计算机的性能。 useId useId支持同一个组件在客户端和服务端生成相同的唯一的 ID,避免 hydration 的不匹配,原理就是每个 id 代表该组件在组件树中的层级结构: JavaScript 运行代码 复制代码 1 2 3 4 5 6 7 8 9 function Checkbox() { const id = useId() return ( <> <label htmlFor={id}>Do you like React?</label> <input id={id} type="checkbox" name="react" /> </> ) } React.memo 和性能优化。当某个组件状态更新时,它的所有子组件树将会重新渲染。 React.memo 和记忆化数据 React.memo 和 React.useMemo 优化性能 React.memo 和 React.useCallback 优化性能 React useEffect cleanup。在这段代码中,示例演示 cleanup 的时机 React 中可以以数组的 index 作为 key 吗?。在这段代码中,使用 index 作为 key,其中夹杂了 input,引发 bug React 中以数组的 index 作为 key。在这段代码中,使用 index 作为 key,其中夹杂了随机数,引发了 bug React 兄弟组件通信。兄弟组件在 React 中如何通信 React 中合成事件。React 中事件为合成事件,你可以通过 e.nativeEvent 获取到原生事件,观察 e.nativeEvent.currentTarget 你将会发现 React 将所有事件都绑定在了 #app(React 应用挂载的根组件) React 中 input.onChange 的原生事件是什么?。观察 e.nativeEvent.type 可知 React hooks 如何实现一个计数器 Counter React FiberNode 数据结构。贯彻 element._owner 可知 FiberNode 数据结构 React 点击按钮时自增三次。此时需使用回调函数,否则会报错 React 不可变数据的必要性。 React 不可变数据的必要性之函数组件。当在 React hooks 中 setState 两次为相同数据时,不会重新渲染 React 状态批量更新之事件处理。事件处理中的状态会批量更新,减少渲染次数 React 状态批量更新之异步请求。异步请求中的状态不会批量更新,将会造成多次渲染 React18 状态批量更新。在 React 18 中所有状态将会批量更新 React capture value 整理成markerdown代码风格写出来 我copy
最新发布
08-23
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值