前端知识库(lizh)
  • README
  • Bugs
    • 前端调试随笔
    • 浏览器常见问题概览
    • 浏览器兼容问题概览
    • HTML常见问题概览
    • CSS常见问题概览
    • JS常见问题概览
    • 移动端兼容性问题概览
    • 微信小程序开发
    • NodeJs常见问题概览
    • Mac常见问题概览
    • 微信开发遇坑指南
    • Npm包常见问题概览
    • 其他问题汇总
  • Css探索系列
    • CSS基础知识
    • CSS常见问答
    • CSS常见问答02
    • CSS应用示例
    • CSS应用示例02
    • 由Z Index引发的层叠上下文思考
    • 由浮动塌陷引发的块级格式上下文思考
    • CSS探索系列 Flex布局
    • CSS探索系列 Margin
    • CSS探索系列 Auto关键字
    • CSS探索系列 Gradient
    • CSS探索系列 Line Height
    • CSS探索系列 元素居中
    • CSS探索系列 动画
    • 为什么使用PostCSS处理CSS?
    • 重新认识伪类与伪元素
    • 自定义表单伪元素样式
    • 如何理解Css中的Display属性
    • 视口和软键盘对视口的影响
    • 关于Css
  • Frontend
    • 00 关于Web前端
    • 01 前端知识概览
    • 02 常用前端库概览
    • 基础 00 前端常见问题01
    • 基础 01 浏览器缓存
    • 基础 02 浏览器工作原理
    • 基础 03 谈谈前端跨源问题及解决方法
    • 进阶 01 Web性能优化
    • 进阶 02 搜索引擎优化(SEO)
    • 进阶 03 前端模块化编程
    • 进阶 04 规范代码:Linter、Prettier、EditorConfig
    • 进阶 11 前端自动化测试
    • 高级 01 前端安全
    • Vue2.X原理篇
    • Vue3初步了解及迁移指南
    • 重读Vue教程
    • React17.X原理篇
    • 你必须知道的React问题
    • 重读React教程
    • 聊一聊Cookie的一些问题
    • 如何理解HTTP响应的状态码
    • HTTP的历史演变及概述
    • Webpack4.X原理篇
    • Webpack基础入门篇
    • Webpack构建优化篇
    • TypeScript使用指南
    • 代码规范
      • 前端规范
      • HTML
      • CSS
      • JS
  • Html探索系列
    • HTML基础知识
    • HTML基础知识02
    • HTML常见问答
    • HTML经典实践用例
    • HTML元素的宽高及位置详解
    • Video元素的使用和常见问题总结
    • Html探索系列 Meta标签
    • DOCTYPE:文档类型与浏览器模式
    • DHTML(动态网页)简介
    • HTML标签详解
    • HTML布局的几种方式
    • HTML全局属性
    • 关于Html
  • Js探索系列
    • 基础知识
    • 常见问答
    • 应用示例
    • 趣味示例
    • 基础篇 05 AJAX
    • 基础篇 06 Window对象
    • 基础篇 07 Error、JSON、Math、Console对象
    • 基础篇 08 History、URL、Screen、Navigator、Location对象
    • 基础篇 09 文档对象模型(DOM)
    • 基础篇 10 Document对象
    • 基础篇 11 Element对象
    • 基础篇 12 Event对象
    • 基础篇 13 键盘、鼠标、触摸事件
    • 基础篇 15 CSS对象模型(CSSOM)
    • 进阶篇 01 Prototype对象和继承
    • 进阶篇 02 Promise对象
    • 进阶篇 07 迭代器(Iterator)
    • 进阶篇 08 Generator和Async函数
    • 进阶篇 09 JavaScript异步编程
    • Date对象和日期时间字符串格式
    • Canvas基础入门篇
    • Canvas进阶篇
    • SVG基础入门篇
    • 四种判断数据类型方法的优缺点
    • 深入理解JavaScript的浅拷贝和深拷贝
    • 谈谈JavaScript的作用域和上下文
    • 复制内容到剪贴板
    • 关于Javascript
  • NodeJs
    • 关于Node.Js
    • Node.Js:三种调试方法
    • Npm包管理器简介及一些机制
    • NPM:Package.Json详解(中文)
    • NPM:从零开始,开发一个软件包
    • NPM:常用命令
    • Node.Js:Fs(文件系统)
    • Node.Js:Global(全局变量)
    • Node.Js:HTTP
    • Node.Js:Module(模块)
    • Node.Js:Path(路径)
    • Node.Js:Readline(逐行读取)
  • Research
    • 极细边框(1px边框)实现方式
    • 如何监控前端异常?
    • H5页面跳转和刷新
    • Web主题切换和个性化定制方法总结
    • Vue SSR(服务端渲染)的简单实现
    • 基于Create React App打造代码规范化的React+Ts项目
    • H5可视化编辑
    • Web常用功能
    • Javascript加密混淆
    • Vue如何导入TypeScript
    • 移动端PDF预览
    • 纯CSS绘制箭头
    • 网站性能测量和优化方法
  • Tech
    • GOOGLE浏览器的搜索技巧
    • Curl的用法指南
    • Sublime3插件篇
    • Charles安装及使用
    • Nginx基础使用
    • 排序算法(Javascript)
    • 代码整洁之道(摘录笔记)
    • Java的24种设计模式与7大原则
    • 观察者和发布订阅模式
  • Tools
    • Git
      • Git基础教程
      • Git常见问题
    • Gitbook
      • Gitbook入门篇
      • Gitbook插件篇
      • Gitbook进阶篇
由 GitBook 提供支持
在本页
  • 异步编程
  • 回调函数
  • Promise
  • Generators/yield
  • Async/await
  • 其他方法
  • 发布-订阅
  • 参考链接

这有帮助吗?

  1. Js探索系列

进阶篇 09 JavaScript异步编程

异步(Asynchronous,async)是与同步(Synchronous, sync)相对的概念。

在单线程编程中,程序的运行是同步的(同步不意味着所有步骤同时运行,而是指步骤在一个控制流序列中按顺序执行);而异步则是不保证同步的概念,也就是说,步骤的执行将不再与原有的序列有顺序关系。

所谓的同步就是在执行一个任务时,在该任务没有完成之前,其他任务是无法执行的,必须等该任务完成。换句话说,任务执行时,会阻塞后面的任务。

所谓的异步就是在执行一个任务时,为该任务指定一个回调函数来处理任务完成后需要做的事情,而不需要等该任务完成,再去执行其他任务。换句话说,任务发出后,不会阻塞后面的任务。

// 异步编程:定时器回调函数
setTimeout(() => {
    console.log(1)
}, 100)
console.log(2)
// 2
// 1

异步编程

计算机在设计上是异步的。

异步意味着事情可以独立于主程序流而发生,也就是说,异步就是从主线程发射一个子线程来完成任务。

异步编程技术使程序可以在执行一个可能长期运行的任务的同时,继续对其他事件做出反应,而不必等待任务完成,其常见形式是:提供的函数(回调函数)将在事件发生时被调用(而不是立即被调用)。

通常,编程语言(C、Java、C#、PHP、Go、Ruby、Swift、Python)是同步的,有些会在语言或库中提供管理异步性的方法,比如,使用线程(衍生新的进程)来处理异步操作。

JavaScript 语言是同步的,并且是单线程(一次只能完成一个任务;如果有多个任务,则必须排队)的。 这意味着无法通过创建线程的方法来处理异步操作。

而浏览器提供的许多功能可能需要很长的时间来完成,因此需要异步完成:

  • 定时器函数:setTimeout、setInterval。

  • 网络请求:异步 Ajax 或者 fetch 发起的 HTTP 请求。

  • DOM 事件机制中的监听器:onClick、onMouseOver、onChange、onSubmit 等的监听函数。

  • 使用 getUserMedia() 访问用户的摄像头和麦克风。

  • 使用 showOpenFilePicker() 请求用户选择文件以供访问。

为了解决这个问题,JavaScript 指供了一些异步编程的方案来处理异步任务:回调函数、Promise、Generators/yield、Async/await。

回调函数

回调函数是 JavaScript 异步编程的最基本、最原始的方式,例如事件回调、setTimeout/setInterval、ajax等,但是使用回调函数存在一个非常棘手的问题,那就是回调地狱,一开始写没什么,等过一段时间后,不管自己看还是别人看,都会觉得这代码写的真恶心。

回调函数其实是一个简单的函数,会作为值被传给另一个函数,并且仅在事件发生时才被执行。 之所以这样做,是因为 JavaScript 具有顶级的函数,这些函数可以被分配给变量并传给其他函数(称为高阶函数)。

从历史发展的脉络来看,早些年为了实现 JavaScript 的异步编程,一般都采用回调函数的方式。

// window 对象的 load 事件监听器
window.addEventListener('load', () => {
    console.log('页面准备就绪时才会运行')
})

// 定时器的回调函数
setTimeout(() => {
    console.log('1秒之后运行')
}, 1000)

// XHR请求的回调函数
const xhr = new XMLHttpRequest()
xhr.onreadystatechange = () => {
    if (xhr.readyState === 4) {
        if (xhr.status === 200) {
            console.log(xhr.responseText)
            return
        }
        console.error('出错')
    }
}
xhr.open('GET', 'http://example.com')
xhr.send()

使用回调函数存在一个很常见的问题:当嵌套层级过多时,会形成 “回调地狱” 或 “厄运金字塔”,代码会变得非常复杂,难以阅读和调试:

const fs = require('fs')
fs.readFile('./1.txt', 'utf-8', (err, r1) => {
    console.log(r1)
    fs.readFile(r1, 'utf-8', (err, r2) => {
        console.log(r2)
        fs.readFile(r2, 'utf-8', function(err, r3) {
            console.log(r3)
            fs.readFile('./1.txt', 'utf-8', function(err, r3) {
                console.log('全部完成!')
            });
        });
    });
});

Promise

Promise 是异步编程的一种解决方案,比传统的解决方案 “回调函数和事件” 更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了 Promise 对象,在一定程度上解决了回调地狱的问题。但是存在的问题也很明显,过多使用 then 链式调用,其实并没有从根本上解决回调地狱的问题,只是换了一种写法,可读性虽然有所提升,但是依旧很难维护。

Promise 本意是承诺,在程序中的意思就是承诺过一段时间后会给一个结果。 什么时候会用到过一段时间?答案是异步操作,异步是指可能比较长时间才有结果的才做,例如网络请求、读取本地文件等。

从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。

当 Promise 被调用后,它会以处理中状态开始。 这意味着调用的函数会继续执行其他任务,而 Promise 仍处于处理中直到解决为止,从而为调用的函数提供所请求的任何数据。被创建的 Promise 最终会以被解决状态或被拒绝状态结束,并在完成时调用相应的回调函数(then 或 catch)。

Promise 的三种状态:

  • pending:处理中,Promise 对象实例创建时候的初始状态。

  • resolved:成功状态。

  • rejected:失败状态。

const fs = require('fs')
function read(file) {
    return new Promise((resolve, reject) => {
        fs.readFile(file, 'utf8', (err, data) => {
            if (err) {
                reject(err)
            }
            console.log(data)
            resolve(data)
        })
    })
}

read('./1.txt').then(r1 => {
    return read(r1);
}).then(r2 => {
    return read(r2);
}).then(r3 => {
    return read(r3);
}).catch(err => {
    console.log(err);
});
// 或者
read('./1.txt')
    .then(read)
    .then(read)
    .then(read)
    .catch(err => {
    console.log(err);
});

Promise 对 “回调地狱” 进行了改进,可读性的确有一定的提升,可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数,但是 Promise 也存在一些问题,即便是使用 Promise 的链式调用,如果操作过多,其实并没有从根本上解决回调地狱的问题,只是换了一种写法,可读性虽然有所提升,但是依旧很难维护。

另外,Promise 还提供了一个 all 方法,用于并行执行多个 Promise:

// all 方法:只有所有promise的状态都成功,all的状态才会变成成功。
Promise.all([read('./1.txt'), read('./2.txt'), read('./3.txt')]).then(data => {
    console.log(data);
}).catch(err => {
    console.log(err)
});

Generators/yield

Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同,Generator 最大的特点就是可以控制函数的执行。Generator 函数可以看作是异步任务的容器,需要暂停的地方,都用 yield 语法来标注:Generator 函数返回的是一个迭代器对象,函数内遇到 yield 关键字会暂停,然后,通过调用迭代器的 next() 方法可重新启动,每次返回的是 yield 后的表达式结果。

yield 表达式本身没有返回值,或者说总是返回 undefined。next() 方法可以带一个参数,该参数就会被当作上一个 yield 表达式的返回值。

function* generatorFunc(x) {
    const y = 2 * (yield (x + 1))
    const z = yield (y / 3)
    return (x + y + z)
}
const it = generatorFunc(5)
console.log(it.next())   // {value: 6, done: false}
console.log(it.next(12)) // {value: 8, done: false}
console.log(it.next(13)) // {value: 42, done: true}

代码执行分析:

  • Generator 函数调用和普通函数不同,它会返回一个迭代器;

  • 当执行第一次 next 时,传参会被忽略,并且函数暂停在 yield (x + 1) 处,所以返回 5 + 1 = 6;

  • 当执行第二次 next 时,传入的参数 12 就会被当作上一个 yield 表达式的返回值,此时 let y = 2 * 12,所以第二个 yield 等于 2 * 12 / 3 = 8。注意: 如果不传参,yield 永远返回 undefined。

  • 当执行第三次 next 时,传入的参数 13 就会被当作上一个 yield 表达式的返回值,所以 z = 13, x = 5, y = 24,相加等于 42。

注意: yield 后面的表达式是同步的,也就是说,如果后面的表达式是异步操作,next() 方法启动时,不会等待当前停留的 yield 的异步操作执行结束。

const fs = require('fs')
function read(file) {
    return new Promise((resolve, reject) => {
        fs.readFile(file, 'utf8', (err, data) => {
            if (err) {
                reject(err)
            }
            console.log(data)
            resolve(data)
        })
    })
}

function * generatorRead() {
    const r1 = yield read('./1.txt')
    const r2 = yield read(r1)
    const r3 = yield read(r2)
}
const it = generatorRead();
it.next().value.then((r1) => {
    it.next(r1).value.then((r2) => {
        it.next(r2).value.then(() => {
            console.log('全部完成!')
        })
    })
})

以上是 Generator 函数异步编程的代码,跟回调函数的 “回调地狱” 很像。在实际开发中,通常配合 co 库使用,可以让代码变得简洁易读。

co 是一个为 Node.js 和浏览器打造的基于生成器的流程控制工具,借助于 Promise,可以使用更加优雅的方式编写非阻塞代码。

const co = require('co')
co(generatorRead()).then(() => {
    console.log('全部完成!')
})

Async/await

async/await 是 ES2017 提出的一种异步解决方案,它相当于 Generator + 执行器的语法糖,就目前来说,是最佳的异步解决方案,真正实现了异步代码,同步表示。

其最大的优点是代码清晰,让异步逻辑的代码看起来像同步一样。

const fs = require('fs')
function read(file) {
    return new Promise((resolve, reject) => {
        fs.readFile(file, 'utf8', (err, data) => {
            if (err) {
                reject(err)
            }
            console.log(data)
            resolve(data)
        })
    })
}

async function promiseRead() {
    const r1 = await read('./1.txt')
    const r2 = await read(r1)
    const r3 = await read(r2)
}
promiseRead();

其他方法

事件监听

通过事件机制,实现代码的解耦。JavaScript 处理 DOM 交互就是采用的事件机制。

发布-订阅

发布订阅基于事件监听,发布者和订阅者通过一个事件中心进行通信,并且实现了多个事件解耦。

参考链接

上一页进阶篇 08 Generator和Async函数下一页Date对象和日期时间字符串格式

最后更新于1年前

这有帮助吗?

MDN- 异步 JavaScript 简介
Node.js - JavaScript 异步编程与回调
runoob.com - JavaScript 异步编程
稀土掘金 - JS 异步编程都有哪些方案?