理解JavaScript概念系列--异步任务

栏目: JavaScript · 发布时间: 5年前

内容简介:最近权利的游戏第八季已经开播两集了,权游迷们看完第二集的时候不知道有没有这样一种体会,想象一下如果你是剧中的一位人物,在与异鬼大军大战前夜你会想什么或者你会做些什么事?不得不说导演把剧中人物在大战前夜他们的心理活动以及表现描述的恰到好处,每一帧画面都值得细细体味。回到写文章上来,如何在一篇文章中把想要总结的知识点深入浅出的罗列出来是我一直在思考的问题,欢迎同学们给出宝贵意见。所谓
  1. 什么是JavaScript异步?
  2. 为什么要实现JavaScript异步?
  3. 怎么实现JavaScript异步?
  4. JavaScript异步原理是什么?

最近权利的游戏第八季已经开播两集了,权游迷们看完第二集的时候不知道有没有这样一种体会,想象一下如果你是剧中的一位人物,在与异鬼大军大战前夜你会想什么或者你会做些什么事?不得不说导演把剧中人物在大战前夜他们的心理活动以及表现描述的恰到好处,每一帧画面都值得细细体味。

回到写文章上来,如何在一篇文章中把想要总结的知识点深入浅出的罗列出来是我一直在思考的问题,欢迎同学们给出宝贵意见。

JavaScript异步

所谓 "异步" ,简单说就是一个任务不是连续完成的,可以理解成该任务被人为分成两段,先执行第一段,然后转而执行其他任务,等做好了准备,再回过头执行第二段。

Javascript语言的执行环境是 "单线程" (single thread)。所谓“单线程”执行也可以理解为JavaScript代码从上到下顺序解释(编译)执行。在程序执行过程中遇到需要控制在未来某个时间点(比如 setTimeout , callback 等)才能执行的程序则将其放到一边(消息队列),继续执行其下面的程序。那些程序被控制在未来某个时间点执行就形成了JavaScript的 异步执行机制

function add(a, b) {
    return a + b;
}
function sub(c, d) {
    return c - d;
}
function multiply(e, f) {
    return e * f;
}

console.log(add(1, 2));
setTimeout(function() {
    console.log(sub(2, 1));
}, 1000);
console.log(multiply(1, 2));
// 3 console.log(add(1, 2))
// 2 console.log(multiply(1, 2))
// 1 console.log(sub(2, 1))
复制代码

如果没有这种异步执行机制,当遇到一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。常见的浏览器无响应(假死),往往就是因为某一段Javascript代码长时间运行(比如死循环),导致整个页面卡在这个地方,其他任务无法执行。

JavaScript 单线程与任务

1. 运行在浏览器中的JavaScript程序是单线程执行的

  • Js程序始终在一个线程上执行,这个线程就是Js引擎线程。
  • 每个浏览器只有一个Js引擎线程。
  • 单线程,即Js引擎在同一时刻只能执行一个任务,其他任务要执行,需要排队。
  • Js引擎线程和UI线程互斥,因为Js操作DOM导致Js执行时影响页面的渲染。
  • HTML5提测Web Worker标准,运行Js脚本创建多个线程,但是子线程完全受主线程的控制且不能操作DOM元素,所以并没有改变Js单线程的本质。

后续会有文章介绍Web Worker

2. 浏览器是多线程的

  • Js引擎线程:执行JavaScript程序
  • UI渲染线程:渲染页面
  • 浏览器事件触发线程:控制交互,相应用户触发事件
  • 网络请求线程:处理网络请求,ajax是委托给浏览器新开的一个http线程
  • EventLoop处理线程:处理轮询消息队列

后续会有文章介绍浏览器运行机制

JavaScript任务实际上是程序中的一个个代码块(执行单元)或者一行代码,由于JavaScript引擎的单线程执行机制,程序中凡是在JavaScript引擎中顺序执行的代码则为 同步任务 ,在未来某个时间点执行的代码则为 异步任务

同步任务有哪些?异步任务有哪些?

除了异步任务以外的都是同步任务(有点废话),同步任务太常见了,比如 add(a, b) 数学运算, document.getElementById('main').style.fontSize = '12px dom运算, quickSort(arr) 数组快速 排序 等。然而JavaScript中提供实现异步任务的代码方式相对来说是有限的,主要是下文中的几种。

JavaScript 创建异步任务

1、JavaScript原生事件处理函数

由Js事件触发的函数本身就是异步任务。

JavaScript原生事件有哪些?

DOM元素上可以触发的有 onclickonmouseoveronmouseoutonmousedownonmouseupondblclickonkeydownonkeypressonkeyup 等,窗口 onresize ,滚动条 onscroll 等,资源加载 onload 等,网络请求 onreadystatechange ···

// DOM0事件模型
var btn = document.getElementById("myBtn");
btn.onclick = function() {
    alert(this.id);
}
// DOM2事件模型
btn.addEventListener('click', function() {
    alert('hello '+this.id);
}, true)
复制代码

2、定时器

setImmediate() , setTimeout()setInterval() 三种JavaScript中设置定时执行某些程序的函数属于异步任务。

setImmediate(fn) 是将事件插入到事件队列尾部,主线程和事件队列的函数执行完成之后立即执行 setImmediate 指定的回调函数,和 setTimeout(fn,0) 的效果差不多,但是当他们同时在同一个事件循环中时,执行顺序是 不确定 的。

// note: setImmediate() 是node环境下的函数
setImmediate(function() {
    console.log('run setImmediate');
});
setTimeout(function() {
    alert('run setTimeout');
}, 0);

// run setImmediate -> run setTimeout
// 也有可能是
// run setTimeout -> run setImmediate
复制代码

异步任务的执行顺会在后面文章《理解JavaScript概念系列--Event Loop》中详细介绍

3、MessageChannel

后续补充相关内容

4、Promise

Promise对象用于表示一个异步操作的最终状态(完成或失败),以及该异步操作的结果值。

下面看一下 Promise 的两个例子,一个是无条件触发异步函数,另一个是简单模拟实现axios中的 get 方法。

// 无条件触发异步结果函数resolve和reject
console.log('before promise run');
var promise = new Promise(function(resolve, reject) {
    console.log('promise is running');
    resolve('promise is resolved');
});
promise.then(result => console.log(result));
console.log('I am a line of reference');
// before promise run
// promise is running
// I am a line of reference
// promise is resolved

// 在一定条件下触发异步结果函数resolve和reject
var axios = {};
axios.get = function(url) {
    return new Promise(function(resolve, reject) {
        var xhr = new XMLHttpRequest();
        xhr.open('GET', url, true);
        xhr.send();
        // 异步事件
        xhr.onreadystatechange = function() {
            if(xhr.readyState == 4) {
                if(xhr.status == 200) {
                    try {
                        var response = JSON.parse(xhr.responseText);
                        resolve(response);
                    }catch(e) {
                        reject(e);
                    }
                }else {
                    reject(new Error(xhr.statusText));
                }
            }
        }
    });
}

axios.get('/userInfo').then(res => res.data)
复制代码

Promise 是JavaScript中提供的原生解决异步编程的一种方案,一般程序中最终异步阶段的调用是需要 外部事件 或者 定时器 等额外异步单元来触发,如果无条件触发异步结果函数,结果响应函数也要等JS引擎主线程所有同步任务执行结束后再执行。

5、Generator

生成器对象是由一个 generator function 返回的,并且它符合可迭代协议和迭代器协议。

function* gen() {
    yield 1;
    yield 2;
    yield 3;
}
let g = gen();
console.log(g); // Generator {  }
console.log(g.next()); // Object { value: 1, done: false }
console.log(g.next()); // Object { value: 2, done: false }
console.log(g.next()); // Object { value: 3, done: false }
console.log(g.next()); // Object { value: undefined, done: true }
复制代码

上面代码中,调用 Generator 函数,会返回一个内部指针(即遍历器) g 。这是 Generator 函数不同于普通函数的另一个地方,即执行它不会返回结果,返回的是指针对象。调用指针 gnext 方法,会移动内部指针(即执行异步任务的第一段),指向第一个遇到的yield语句,上例是执行到 yield 3 为止。

换言之, next 方法的作用是分阶段执行 Generator 函数。每次调用 next 方法,会返回一个对象,表示当前阶段的信息( value 属性和 done 属性)。value属性是 yield 语句后面表达式的值,表示当前阶段的值; done 属性是一个布尔值,表示 Generator 函数是否执行完毕,即是否还有下一个阶段。

// generator函数异步任务封装
var fetch = require('node-fetch');
function* gen() {
    var url = 'https: //api.xxx.users';
    var result = yield fetch(url);
    console.log(result);
}

var g = gen(); // g为遍历器对象
var result = g.next(); // fetch(url)返回一个promise对象

result.value.then(
    data => data.json;
).then(
    data => g.next(data); // 本次next,跳出当前遍历器
)
复制代码

上面代码中,首先执行 Generator 函数,获取遍历器对象,然后使用next方法(第二行),执行异步任务的第一阶段。由于 Fetch 模块返回的是一个 Promise 对象,因此要用 then 方法调用下一个 next 方法。

可以看到,虽然 Generator 函数将异步操作表示得很简洁,但是流程管理却不方便(即何时执行第一阶段、何时执行第二阶段)。

Generator 创建的异步任务,必须通过触发 next 才能执行完成异步阶段的任务,要不然将一直不返回异步结果。

6、async函数

async 函数是 Generator 函数的一个语法糖。

const fs = require('fs');

const readFile = function(fileName) {
    return new Promise(function(resolve, reject) {
        fs.readFile(fileName, function(error, data) {
            if(error) {
                return reject(error);
            }
            resolve(data);
        });
    });
}
// Generator函数读取文件过程
const gen = function* () {
    const f1 = yield readFile('../etc/text');
    const f2 = yield readFile('../etc/xml');
    console.log(f1.toString());
    console.log(f2.toString());
}

// 将上面函数gen改写成async函数
const asyncReadFile = async function() {
    const f1 = await readFile('../etc/text');
    const f1 = await readFile('../etc/xml');
    console.log(f1.toString());
    console.log(f2.toString());
}
复制代码

async 函数就是将 Generator 函数的星号(*)替换成 async ,将 yield 替换成 awaitasync 函数对 Generator 函数的改进,体现在以下四点。

  1. 内置执行器
  2. 更好的语义
  3. 更广的适用性
  4. 返回值是Promise

更加详细的介绍请阅读阮一峰老师的总结《async 函数》。下面看一下 async 函数的 核心—返回 Promise 对象

async function fn() {
    return 'hello async'; // 等同于 return await 'hello async'
}
var p = fn();
console.log(p); // Promise { <state>: "fulfilled", <value>: "hello async" }
p.then(value => console.log(value)); // hello async
复制代码

正常情况下, await 命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。

process.nextTick(node环境)

Node.js 服务端环境也是单线程的,除了系统 I/O 之外,在它的时间轮询过程中同一时间(点)只会处理一个事件。在 I/O 型应用中,给每一个输入输出定义回调函数,他们会自动添加到时间轮询的处理队列中。当 I/O 操作完成后,这些回调函数会被触发并执行。 process.nextTick 的意思就是定义一个(异步)动作,并且这个动作在下一个时间轮询的时间点上执行。

function f() {
    console.log('foo')
}

process.nextTick(f);

setTimeout(function() {
    console.log('bar');
}, 0);

process.nextTick(f);
console.log('I am the first, although at the end of the code');

// I am the first, although at the end of the code
// foo
// foo
// bar
复制代码

从上面程序的执行过程可以看出, process.nextTick 创建的是一个异步任务,但是它优先于 setTimeout 执行。

现在再回顾一下文章开头那几个问题,你心中有答案了吗?


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

暗网

暗网

杰米·巴特利特 / 刘丹丹 / 北京时代华文书局 / 2018-7 / 59.00

全面深入揭秘“黑暗版淘宝”暗网的幕后世界和操纵者 现实中所有的罪恶,在暗网中,都是明码标价的商品。 暗杀、色情、恋童癖、比特币犯罪、毒品交易…… TED演讲、谷歌特邀专家、英国智库网络专家杰米•巴特利特代表作! 1、 被大家戏称为“黑暗版淘宝”的暗网究竟是什么?微信猎奇 文不能告诉你的真相都在这里了! 2、 因章莹颖一案、Facebook信息泄露危机而被国人所知的暗网......一起来看看 《暗网》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换