• 忘掉天地
  • 仿佛也想不起自己
bingliaolongBingliaolong  2025-04-13 17:47 Aet 隐藏边栏 |   抢沙发  2 
文章评分 1 次,平均分 5.0

回调

概述

  1. 为了演示回调、promise 和其他抽象概念的使用,我们将使用一些浏览器方法:具体地说,是加载脚本和执行简单的文档操作的方法
  2. JavaScript 主机(host)环境提供了许多函数,这些函数允许我们计划 异步 行为(action)—— 也就是在我们执行一段时间后才自行完成的行为
    1. 例如,setTimeout 函数就是一个这样的函数
  3. 让我们看一下函数 loadScript(src),该函数使用给定的 src 加载脚本:
    1. 它将一个新的、带有给定 src 的、动态创建的标签 <script src="…"> 插入到文档中
    2. 浏览器将自动开始加载它,并在加载完成后执行它

  1. 脚本是“异步”调用的,因为它从现在开始加载,但是在这个加载函数执行完成后才运行

  1. 脚本是“异步”调用的,因为它从现在开始加载,但是在这个加载函数执行完成后才运行
    1. 假设我们需要在新脚本加载后立即使用它。它声明了新函数,我们想运行它们
    2. 但如果我们在 loadScript(…) 调用后立即执行此操作,这将不会有效

  1. 自然情况下,浏览器可能没有时间加载脚本
    1. 到目前为止,loadScript 函数并没有提供跟踪加载完成的方法。脚本加载并最终运行,仅此而已
    2. 但我们希望了解脚本何时加载完成,以使用其中的新函数和变量
    3. 让我们添加一个 callback 函数作为 loadScript 的第二个参数,该函数应在脚本加载完成时执行:

onload 事件

  1. 它通常会在脚本加载和执行完成后执行一个函数
    1. 现在,如果我们想调用该脚本中的新函数,我们应该将其写在回调函数中:

  1. 这是我们的想法:第二个参数是一个函数(通常是匿名函数),该函数会在行为(action)完成时运行
    1. 这是一个带有真实脚本的可运行的示例:
    2. 这被称为“基于回调”的异步编程风格
    3. 异步执行某项功能的函数应该提供一个 callback 参数用于在相应事件完成时调用

在回调中回调

  1. 如何依次加载两个脚本:第一个,然后是第二个?
    1. 自然的解决方案是将第二个 loadScript 调用放入回调中,如下所示:
    2. 在外部 loadScript 执行完成时,回调就会发起内部的 loadScript

  1. 如果我们还想要一个脚本呢?
    1. 因此,每一个新行为(action)都在回调内部
    2. 这对于几个行为来说还好,但对于许多行为来说就不好了

处理 Error

  1. 上述示例中,我们并没有考虑出现 error 的情况
    1. 如果脚本加载失败怎么办?我们的回调应该能够对此作出反应
    2. 这是 loadScript 的改进版本,可以跟踪加载错误
    3. 加载成功时,它会调用 callback(null, script),否则调用 callback(error)

  1. 用法
    1. 我们在 loadScript 中所使用的方案其实很普遍。它被称为“Error 优先回调(error-first callback)”风格
    2. 约定是:
    3. callback 的第一个参数是为 error 而保留的。一旦出现 error,callback(err) 就会被调用
    4. 第二个参数(和下一个参数,如果需要的话)用于成功的结果。此时 callback(null, result1, result2…) 就会被调用
    5. 因此,单一的 callback 函数可以同时具有报告 error 和传递返回结果的作用

回调地狱

  1. 乍一看,它像是一种可行的异步编程方式
    1. 的确如此,对于一个或两个嵌套的调用看起来还不错
    2. 但对于一个接一个的多个异步行为,代码将会变成这样:
    3. 随着调用嵌套的增加,代码层次变得更深,维护难度也随之增加,尤其是我们使用的是可能包含了很多循环和条件语句的真实代码,而不是例子中的 ...

  1. 所以这种编码方式不是很好
    1. 我们可以通过使每个行为都成为一个独立的函数来尝试减轻这种问题,如下所示:

Promise

构造器语法

  1. Promise 对象的构造器(constructor)语法如下:
    1. 传递给 new Promise 的函数被称为 executor
    2. new Promise 被创建,executor 会自动运行。它包含最终应产出结果的生产者代码
    3. 它的参数 resolvereject 是由 JavaScript 自身提供的回调。我们的代码仅在 executor 的内部

  1. executor 获得了结果,无论是早还是晚都没关系,它应该调用以下回调之一:

    1. resolve(value) —— 如果任务成功完成并带有结果 value
    2. reject(error) —— 如果出现了 errorerror 即为 error 对象
  2. 总结:

    1. executor 会自动运行并尝试执行一项工作
    2. 尝试结束后,如果成功则调用 resolve,如果出现 error 则调用 reject
  3. new Promise 构造器返回的 promise 对象具有以下内部属性:

    1. state —— 最初是 "pending",然后在 resolve 被调用时变为 "fulfilled",或者在 reject 被调用时变为 "rejected"
    2. result —— 最初是 undefined,然后在 resolve(value) 被调用时变为 value,或者在 reject(error) 被调用时变为 error

示例

  1. 下面是一个 promise 构造器和一个简单的 executor 函数,该 executor 函数具有包含时间(即 setTimeout)的“生产者代码”:
    1. executor 被自动且立即调用(通过 new Promise
    2. executor 接受两个参数:resolvereject
      这些函数由 JavaScript 引擎预先定义,因此我们不需要创建它们。我们只需要在准备好(译注:指的是 executor 准备好)时调用其中之一即可

  1. 下面则是一个 executorerror 拒绝 promise 的示例:

  1. 总而言之,executor 应该执行一项工作(通常是需要花费一些时间的事儿),然后调用 resolvereject 来改变对应的 promise 对象的状态

注意1

  1. 只有一个结果或一个 error
    1. executor 只能调用一个 resolve 或一个 reject。任何状态的更改都是最终的
    2. 所有其他的再对 resolvereject 的调用都会被忽略:

  1. Error 对象 reject
    1. 如果什么东西出了问题,executor 应该调用 reject
    2. 这可以使用任何类型的参数来完成(就像 resolve 一样)
    3. 但建议使用 Error 对象(或继承自 Error 的对象)。这样做的理由很快就会显而易见
  2. resolve/reject 可以立即进行
    1. 实际上,executor 通常是异步执行某些操作,并在一段时间后调用 resolve/reject
    2. 但这不是必须的。我们还可以立即调用 resolvereject,就像这样:
    3. 例如,当我们开始做一个任务,随后发现一切都已经完成并已被缓存时,可能就会发生这种情况

  1. stateresult 都是内部的
    1. Promise 对象的 stateresult 属性都是内部的
    2. 我们无法直接访问它们。但我们可以对它们使用 .then/.catch/.finally 方法

消费者:then,catch

  1. Promise 对象充当的是 executor(“生产者代码”或“歌手”)和消费函数(“粉丝”)之间的连接,后者将接收结果或 error
  2. 可以通过使用 .then.catch 方法注册消费函数
    1. .then 的第一个参数是一个函数,该函数将在 promise resolved 且接收到结果后执行
    2. .then 的第二个参数也是一个函数,该函数将在 promise rejected 且接收到 error 信息后执行

  1. reject 的情况下,运行第二个:

  1. 如果我们只对成功完成的情况感兴趣,那么我们可以只为 .then 提供一个函数参数:

catch

  1. 如果我们只对 error 感兴趣,那么我们可以使用 null 作为第一个参数:.then(null, errorHandlingFunction)
  2. 或者我们也可以使用 .catch(errorHandlingFunction),其实是一样的:

  1. .catch(f) 调用是 .then(null, f) 的完全的模拟,它只是一个简写形式

清理:finally

  1. 就像常规 try {...} catch {...} 中的 finally 子句一样,promise 中也有 finally
  2. 调用 .finally(f) 类似于 .then(f, f),因为当 promise settledf 就会执行:无论 promiseresolve 还是 reject
  3. finally 的功能是设置一个处理程序在前面的操作完成后,执行清理/终结
    1. 例如,停止加载指示器,关闭不再需要的连接等

  1. 请注意,finally(f) 并不完全是 then(f,f) 的别名
    1. 它们之间有重要的区别:
    2. finally 处理程序(handler)没有参数。在 finally 中,我们不知道 promise 是否成功。没关系,因为我们的任务通常是执行“常规”的完成程序(finalizing procedures)
    3. finally 处理程序将结果或 error “传递”给下一个合适的处理程序
    4. finally 处理程序也不应该返回任何内容。如果它返回了,返回的值会默认被忽略
      此规则的唯一例外是当 finally 处理程序抛出 error 时。此时这个 error(而不是任何之前的结果)会被转到下一个处理程序
  2. 正如我们所看到的,第一个 promise 返回的 value 通过 finally 被传递给了下一个 then
    1. 这非常方便,因为 finally 并不意味着处理一个 promise 的结果

  1. 下面是一个 promise 返回结果为 error 的示例,让我们看看它是如何通过 finally 被传递给 catch 的:

注意2

  1. 我们可以对 settledpromise 附加处理程序
    1. 如果 promisepending 状态,.then/catch/finally 处理程序(handler)将等待它的结果
    2. 有时候,当我们向一个 promise 添加处理程序时,它可能已经 settled
    3. 在这种情况下,这些处理程序会立即执行:

示例:loadScript

  1. 接下来,让我们看一下关于 promise 如何帮助我们编写异步代码的更多实际示例
  2. 这是基于回调函数的变体,记住它:

  1. promise 重写它
    1. 新函数 loadScript 将不需要回调。取而代之的是,它将创建并返回一个在加载完成时 resolvepromise 对象
    2. 外部代码可以使用 .then 向其添加处理程序(订阅函数):

promise区别与callback

  1. promise 允许我们按照自然顺序进行编码
  2. 而在调用 loadScript(script, callback) 时,我们必须有一个 callback 函数可供使用
    1. 换句话说,在调用 loadScript 之前,我们必须知道如何处理结果

Promise

概述

  1. 它的想法是通过 .then 处理程序(handler)链进行传递 result
    1. 初始 promise1 秒后 resolve (*)
    2. 然后 .then 处理程序被调用 (**),它又创建了一个新的 promise(以 2 作为值 resolve
    3. 下一个 then (***) 得到了前一个 then 的值,对该值进行处理(*2)并将其传递给下一个处理程序

  1. 这样之所以是可行的,是因为每个对 .then 的调用都会返回了一个新的 promise,因此我们可以在其之上调用下一个 .then
    1. 当处理程序返回一个值时,它将成为该 promiseresult,所以将使用它调用下一个 .then
  2. 新手常犯的一个经典错误:从技术上讲,我们也可以将多个 .then 添加到一个 promise 上。但这并不是 promise 链(chaining
    1. 如下
    2. 这里所做的只是一个 promise 的几个处理程序。它们不会相互传递 result;相反,它们之间彼此独立运行处理任务

返回 promise

  1. .then(handler) 中所使用的处理程序(handler)可以创建并返回一个 promise
    1. 在这种情况下,其他的处理程序将等待它 settled 后再获得其结果
    2. 例如:
    3. 这里第一个 .then 显示 1 并在 (*) 行返回 new Promise(…)
    4. 1 秒后它会进行 resolve,然后 resultresolve 的参数,在这里它是 result*2)被传递给第二个 .then 的处理程序
    5. 这个处理程序位于 (**) 行,它显示 2,并执行相同的行为

示例:loadScript

  1. 按顺序依次加载脚本:

  1. 可以用箭头函数来重写代码,让其变得简短一些:
    1. 在这儿,每个 loadScript 调用都返回一个 promise,并且在它 resolve 时下一个 .then 开始运行
    2. 然后,它启动下一个脚本的加载
    3. 所以,脚本是一个接一个地加载的

Thenables

  1. 确切地说,处理程序返回的不完全是一个 promise,而是返回的被称为 “thenable” 对象 —— 一个具有方法 .then 的任意对象。它会被当做一个 promise 来对待
  2. 这个想法是,第三方库可以实现自己的“promise 兼容(promise-compatible)”对象

更复杂的示例:fetch

  1. 在前端编程中,promise 通常被用于网络请求
    1. 执行这条语句,向 url 发出网络请求并返回一个 promise
    2. 当远程服务器返回 header(是在 全部响应加载完成前)时,该 promise 使用一个 response 对象来进行 resolve
    3. 为了读取完整的响应,我们应该调用 response.text() 方法:当全部文字内容从远程服务器下载完成后,它会返回一个 promise,该 promise 以刚刚下载完成的这个文本作为 result 进行 resolve

  1. fetch 返回的 response 对象还包含 response.json() 方法,该方法可以读取远程数据并将其解析为 JSON

  1. 现在,让我们用加载好的用户信息搞点事情
    1. 例如,我们可以再向 GitHub 发送一个请求,加载用户个人资料并显示头像:

  1. 请看 (*) 行:我们如何能在头像显示结束并被移除 之后 做点什么?
    1. 例如,我们想显示一个用于编辑该用户或者其他内容的表单。就目前而言,是做不到的
    2. 为了使链可扩展,我们需要返回一个在头像显示结束时进行 resolvepromise
    3. 就像这样:
    4. 也就是说,第 (*) 行的 .then 处理程序现在返回一个 new Promise,只有在 setTimeout 中的 resolve(githubUser) (**) 被调用后才会变为 settled
    5. 链中的下一个 .then 将一直等待这一时刻的到来

  1. 作为一个好的做法,异步行为应该始终返回一个 promise
    1. 这样就可以使得之后我们计划后续的行为成为可能
  2. 最后,我们可以将代码拆分为可重用的函数:

本文为原创文章,版权归所有,欢迎分享本文,转载请保留出处!

bingliaolong
Bingliaolong 关注:0    粉丝:0
Everything will be better.

发表评论

表情 格式 链接 私密 签到
扫一扫二维码分享