欢迎阅读专门探索 JavaScript 及其构建组件的系列文章的第四章。 在识别和描述核心元素的过程中,我们还分享了关于构建 SessionStack 时需要遵循的一些经验法则,一个 JavaScript 应用必须是强大且高性能的,才能保持竞争力。
你有没有错过前三章? 你可以在这里找到它们:
Google 的 V8 引擎里面的 5 个关于如何编写优化代码的技巧
这一次,我们将通过回顾如何克服在单线程环境中编程的缺点以及构建令人惊叹的 JavaScript UI 来扩展我们的第一篇文章。按惯例,在文章的最后我们将会分享 5 个关于如何用 async / await 编写更简洁代码的技巧。
为什么说单线程是一种限制?
在我们开始的第一篇文章中,我们思考了在调用堆栈(Call Stack)中进行函数调用时需要处理耗费大量时间的程序时会发生什么情况。
想象一下,例如,一个在浏览器中运行的复杂图像转换算法。
虽然调用堆栈具有执行的功能,但此时浏览器不能做任何事情 —— 它被停止下来。这意味着浏览器无法渲染,它不能运行任何代码,它卡住了。那么问题来了 - 你的应用用户界面不再高效和令人满意。
你的应用程序卡住了。
在某些情况下,这可能不是很关键的问题。但是,这是一个更严重的问题。一旦你的浏览器开始处理调用堆栈中的太多任务,它可能会停止响应很长一段时间。在这一点上,许多浏览器会通过抛出错误来处理上述问题,显示并询问是否应该终止页面:
这是很难看的,它完全毁了你的用户体验:
构建JavaScript程序模块
您可能正在将您的JavaScript应用程序写入一个单独.js文件,但是肯定的是您的程序由几个模块组成,其中只有一个将会立即执行,其余的将在稍后执行。 最常见的模块单位是函数。
大多数JavaScript新手开发者似乎都有这样的理解,即以后不一定要求立即发生。 换句话说,根据定义,现在无法完成的任务将以异步的形式完成,这意味着当您想到使用异步来处理时,将不会遇到上述浏览器停止的行为。
我们来看看下面的例子:
// ajax(..) is some arbitrary Ajax function given by a library
var response = ajax('https://example.com/api');
console.log(response);
// `response` won't have the response
您可能知道标准的Ajax请求并不是同步完成的,这意味着在执行代码的时候,ajax(..)函数还没有任何返回值来分配给用于返回的变量。
一种简单的“等待”异步函数返回结果的方式是使用callback的函数:
ajax('https://example.com/api', function(response) {
console.log(response); // `response` is now available
});
需要说明一下:实际上,您可以创建同步的Ajax请求。 但永远不要这样做。 如果您发出同步的Ajax请求,则JavaScript应用的UI界面将被阻止渲染 - 用户将无法点击,输入数据,导航或滚动。 这将阻止任何用户与浏览器交互。 这是一个可怕的做法。
// This is assuming that you're using jQuery
jQuery.ajax({
url: 'https://api.example.com/endpoint',
success: function(response) {
// This is your callback.
},
async: false // And this is a terrible idea
});
这是它的样子,但请不要这样做 - 不要毁掉你的网站:我们以一个Ajax请求为例。 你可以编写任何代码模块并异步执行。
这可以通过使用setTimeout(回调(callback),毫秒(milliseconds))函数来完成。 setTimeout函数的作用是设置一个在稍后发生的事件(一个超时)。 让我们来看看:
function first() {
console.log('first');
}
function second() {
console.log('second');
}
function third() {
console.log('third');
}
first();
setTimeout(second, 1000); // Invoke `second` after 1000ms
third();
控制台中的输出如下所示:
first
third
second
分析事件循环
我们从一个奇怪的说法开始——尽管允许执行异步JavaScript代码(如我们刚才讨论的setTimeout函数),但直到ES6出现,实际上JavaScript本身从来没有任何明确的异步概念。 JavaScript引擎从来都只是执行单个程序模块而不做更多别的事情。
有关JavaScript引擎如何工作的详细信息(特别是Google的V8),请查看我们之前关于该主题的文章。
那么,谁来告诉JS引擎去执行你编写的一大段程序?实际上,JS引擎并不是孤立运行,它运行在一个宿主环境中,对于大多数开发人员来说,宿主环境就是一个典型的Web浏览器或Node.js。实际上,如今,JavaScript被嵌入到从机器人到灯泡的各种设备中。每个设备都代表一个包含JS引擎的不同类型的宿主环境。
所有环境中的共同点是一个称为事件循环的内置机制,它随着时间的推移处理程序中多个模块的执行顺序,并每次调用JS引擎。
这意味着JS引擎只是任何JS代码的一个按需执行环境。并调度事件的周围环境(JS代码执行)。
所以,例如,当你的JavaScript程序发出一个Ajax请求来从服务器获取一些数据时,你在一个函数(“回调函数”)中写好了“响应”代码,JS引擎将会告诉宿主环境:
“嘿,我现在暂停执行,但是每当你完成这个网络请求,并且你有一些数据,请调用这个函数并返回给我。
然后浏览器开始监听来自网络的响应,当响应返回给你的时候,宿主环境会将回调函数插入到事件循环中来安排回调函数的执行顺序。
我们来看下面的图表:
您可以在我们以前的文章中阅读更多关于内存堆和调用栈的信息。
这些Web API是什么? 从本质上讲,它们是你无法访问的线程,你仅仅只可以调用它们。 它们是浏览器并行启动的一部分。如果你是一个Node.js开发者,那么这些就相当于是C ++ API。
那么事件循环究竟是什么?
Event Loop有一个简单的工作机制——就是去监视Call Stack和Callback Queue。 如果调用栈为空,它将从队列中取出第一个事件,并将其推送到调用栈,从而更有效率的运行。
这种迭代在事件循环中被称为一“刻度(tick)”。 每个事件只是一个函数回调。
console.log('Hi');
setTimeout(function cb1() {
console.log('cb1');
}, 5000);
console.log('Bye');
现在执行一下这段代码,看发生了什么:
1、状态是清晰的。浏览器控制台没有输出,调用堆栈是空的。
2、console.log('Hi') 被添加到调用堆栈。
3、执行 console.log('Hi').
4、console.log('Hi') 从调用堆栈中删除。
5、函数 setTimeout(function cb1(){...}) 添加到调用堆栈
6、执行函数 setTimeout(function cb1(){...}) 。浏览器用 Web API 创建一个定时器,定时器开始倒计时。
7、函数 setTimeout(function cb1(){...}) 执行完成并从调用堆栈中删除。
8、console.log('Bye') 添加到调用堆栈。
9、函数 console.log('Bye') 被执行。
10、console.log('Bye') 从调用堆栈中删除。
11、在至少1500毫秒之后,定时器结束并且定时器将回调函数 cb1 放入回调函数队列里面。
12、事件循环从回调队列里面取出 cb1 并将其放入调用堆栈。
13、cb1 被执行并且 console.log('cb1') 被放入调用堆栈。
14、函数 console.log('cb1') 被执行。
15、console.log('cb1') 被从调用堆栈中删除。
16、cb1 被从调用堆栈中删除。
快速回顾:
有趣的是,ES6指定了事件循环应该如何工作,这意味着在技术上事件循环被规定在JS引擎的职责范围之内,不再扮演一个宿主环境的角色。 这个变化的一个主要原因是在ES6中引入了Promises,因为后者需要直接、细致地控制事件循环队列上的调度操作(我们将在后面更详细地讨论它们)。
setTimeout(...)函数如何工作
请注意,setTimeout(...)函数不会自动将您的回调函数放在事件循环队列中。它设置了一个计时器。当定时器到期时,环境将你的回调放到事件循环中,以便将来的某时拿来执行。看看这段代码:
setTimeout(myCallback, 1000);
这并不意味着myCallback将在1000 ms内执行,而是在1000 ms内将myCallback添加到队列中。但是,队列中可能还有其他事件已经被添加了 - 您的回调事件将不得不等待执行。
市面上有很多关于开始使用JavaScript中的异步代码的文章和教程里会建议您使用setTimeout(callback,0)。那么,现在你知道事件循环是怎么做的以及setTimeout是如何工作的:调用setTimeout设置0作为第二个参数会延迟到调用栈被清除为止才会被执行callback事件。
看看下面的代码:
console.log('Hi');
setTimeout(function() {
console.log('callback');
}, 0);
console.log('Bye');
尽管等待时间设置为0 ms,但浏览器控制台中的结果如下所示:
Hi
Bye
callback
ES6中的Jobs是什么?
在ES6中引入了一个名为“Job Queue”的新概念。它是Event Loop队列之上的一个图层。在处理Promises的异步行为时,你最有可能碰到它(我们也会谈论它们)。
现在我们将简单介绍一下这个概念,以便当我们和Promises讨论异步行为的时候,你就会明白这些行为是如何被调度和处理的。
想象一下:Job Queue是一个连接到Event Loop队列中每个瞬时末尾的队列。在事件循环的瞬时期间某些异步操作不会将一个全新的事件添加到事件循环队列,而是将一个项目(又名单元作业(Job))添加到当前瞬时单元作业(Job)队列的末尾。
这意味着您可以添加其他功能以便稍后执行,您可以放心,它将在执行任何其他操作之前立即执行。
单元作业(Job)还可以使更多单元(Jobs)添加到同一队列的末尾。从理论上讲,一个Job“循环”(一个不断增加的Job)可能无限地循环,从而导致需要的资源进入下一个事件循环节点。从概念上讲,这和在你的代码中只是表示长时间运行或者无限循环(比如while(true)..)类似。
Jobs有点像setTimeout(callback,0)“hack”,但实现的方式是它们引入了一个更加明确和有保证的排序:稍后就会介绍。
回调
如您所知,回调是迄今为止在JavaScript程序中展现异步和管理异步的最常见方式。 事实上,回调是JavaScript语言中最基本的异步模式。 无数的JS程序,甚至是非常精密和复杂的程序,都基本被写在了回调之上,而不是在其他异步实现上。
除了回调没有缺点。 许多开发人员正在试图找到更好的异步模式。 但是,如果你不了解底层实际情况,就不可能有效地使用抽象方法。
在下面的章节中,我们将深入探讨这些抽象概念,以说明为什么更精妙的异步模式(将在后续的帖子中讨论)是必要的甚至是推荐的。
嵌套回调
看下面的代码:
listen('click', function (e){
setTimeout(function(){
ajax('https://api.example.com/endpoint', function (text){
if (text == "hello") {
doSomething();
}
else if (text == "world") {
doSomethingElse();
}
});
}, 500);
});
我们有一个嵌套在一起的三个函数回调链,每个代表一个异步系列中的一个步骤。
这种代码通常被称为“回调地狱”。 但是“回调地狱”实际上与嵌套/缩进几乎没有任何关系。 这是一个更深层次的问题。
首先,我们正在等待“click”事件,然后等待定时器启动,然后等待Ajax响应返回,此时可能会再次重复。
乍一看,这个代码可能似乎将其异步映射到如下的连续步骤:
listen('click', function (e) {
// ..
});
那么:
setTimeout(function(){
// ..
}, 500);
然后:
ajax('https://api.example.com/endpoint', function (text){
// ..
});
最后:
if (text == "hello") {
doSomething();
}
else if (text == "world") {
doSomethingElse();
}
那么,这种表达异步代码顺序的方式似乎更加自然,不是吗? 一定有这样的方式吧?
Promises
看看下面的代码:
var x = 1;
var y = 2;
console.log(x + y);
这非常明显:它将x和y的值相加并打印到控制台。但是,如果x或y的值缺失而且还有待确定,该怎么办?比方说,我们需要从服务器中检索x和y的值,然后才能在表达式中使用它们。假设我们有一个函数loadX和loadY,它们分别从服务器载入x和y的值。然后,想象一下,我们有一个求和函数,一旦加载了它们,就将x和y的值相加。
它可能看起来像这样(是不是相当丑陋):
function sum(getX, getY, callback) {
var x, y;
getX(function(result) {
x = result;
if (y !== undefined) {
callback(x + y);
}
});
getY(function(result) {
y = result;
if (x !== undefined) {
callback(x + y);
}
});
}
// A sync or async function that retrieves the value of `x`
function fetchX() {
// ..
}
// A sync or async function that retrieves the value of `y`
function fetchY() {
// ..
}
sum(fetchX, fetchY, function(result) {
console.log(result);
});
这里有一些非常重要的东西 - 在这个代码片段中,我们将x和y作为待定值,并且我们展示了一个求和操作sum(...)(从外部)不关心是x还是y还是待定值。
当然,这种粗糙的基于回调的方法还有很多不足之处。这只是迈向了解推导待定值的好处而迈出的第一步,而不用担心它们何时可用的。
Promise Value
让我们简单地看看我们如何用Promises来表示x + y的例子:
function sum(xPromise, yPromise) {
// `Promise.all([ .. ])` takes an array of promises,
// and returns a new promise that waits on them
// all to finish
return Promise.all([xPromise, yPromise])
// when that promise is resolved, let's take the
// received `X` and `Y` values and add them together.
.then(function(values){
// `values` is an array of the messages from the
// previously resolved promises
return values[0] + values[1];
} );
}
// `fetchX()` and `fetchY()` return promises for
// their respective values, which may be ready
// *now* or *later*.
sum(fetchX(), fetchY())
// we get a promise back for the sum of those
// two numbers.
// now we chain-call `then(...)` to wait for the
// resolution of that returned promise.
.then(function(sum){
console.log(sum);
});
在代码中Promises有两层。
fetchX()和fetchY()被直接调用,返回值(promises!)传递给了sum(...).promises潜在的值可能现在准备好了或者延时,但是每一个promise的行为都是严格相同的。我们以独立于时间的方式分析x和y值。它们是future values、时期。
promise的第二层是sum(...)创造(通过Promise.all([...]))和返回的,然后等待通过调用then(...).当sum(...)的操作完成,我们总的future value准备好了并且能够打印输出。我们在sum(...)中隐藏了x和y的future value等待逻辑。
注意:在sum(...)里,Promise.all([...])创建了一个promise(它等待promiseX和promiseY解决)。链式调用.then(...)创建另一个promise,立即返回value[0]+value[1]的结果(加法结果)。因此,then(...)在最后调用了sum(...)——在代码最后——实际上执行的是第二个promise的返回值,而不是被Promise.all([...])创建的第一个。另外,虽然我们有将第二个then(...)结束,他也创建了另外一个promise,取决于我们是观察/使用它。这Promise链的内容将在本章后面部分进行更详细地解释。
随着Promises, then(...)实际调用了两个方法,第一个用于实现(如前面所示),第二个为拒绝:
sum(fetchX(), fetchY())
.then(
// fullfillment handler
function(sum) {
console.log( sum );
},
// rejection handler
function(err) {
console.error( err ); // bummer!
}
);
如果当我们在获取x或y的时候出错,或者在相加过程中不知何故失败了,promise的sum(...)返回将会被拒绝,并且第二个回调异常处理器将通过then(...)接收promise的拒绝值。
因为Promises封装时态——等待实现或拒绝的潜在的价值——从外界,Promise自身是时间独立的,因次Promises可以以可预测的方式组成(组合),而不考虑时间和结果。
而且,一旦Promise得到解决,它就会永远保持下去。它将在某一时刻变成一个immutable value——然后可以根据需要观察多次。
链式 Promise 是十分有用的:
function delay(time) {
return new Promise(function(resolve, reject){
setTimeout(resolve, time);
});
}
delay(1000)
.then(function(){
console.log("after 1000ms");
return delay(2000);
})
.then(function(){
console.log("after another 2000ms");
})
.then(function(){
console.log("step 4 (next Job)");
return delay(5000);
})
// ...
调用 delay(2000) 会创建一个 Promise ,这个请求会在 2000ms 内实现。之后我们会返回第一个 then(...) 的实现的调用,这又会引起第二个 then(...) 的 Promise ,这个请求同样延迟 2000ms 。
注意:因为 Promise 一旦执行完成就是在外部不可变的。知道它不能被意外或恶意修改之后,我们现在就可以放心地把这个值传递给任何地方。 关于多方观察 “Promise” 的解决方案,尤其如此。 一方不可能影响另一方遵守 Promise 解决方案的能力。 不变性可能听起来像是一个学术话题,但它实际上是 Promise 设计的最基本和最重要的方面之一,这不应该被忽略。
用还是不用 Promise ?
Promise 的一个重要的特性是可以确定是某个变量是否是一个 Promise ,换句话说就是那个变量的行为是否类似 Promise ?
我们知道 Promises 通过语法 new Promise(...) 构造,而且你可能觉得 p instanceof Promise 是一个有效的检测方法,但实际上这种方式并不是非常有效。
主要原因是你可能接收到来自其他浏览器页面(例如 iframe )的 Promise 变量,而且这个变量可能有自己的 Promise 类型,当和当前窗口或者 frame 的 Promise 类型不一样的时候,上边的检测方法就可能无法检测出该变量是一个 Promise 实例。
此外,一个库或者框架可能会实现自己的 Promise 并且不使用 ES6 原生的 Promise 。事实上,你也可能在早期不支持 Promise 的浏览器中通过库来使用 Promise 。
吞吐异常
如果在构造一个 Promise 对象,或者监控系数的任一情况下,抛出了一个 JavaScript 异常错误,例如抛出一个 TypeError 或者 ReferenceError ,那么异常即会被捕获,并且它将迫使问题中的 Promise 对象拒绝访问。
举个例子:
var p = new Promise(function(resolve, reject){
foo.bar(); // `foo` is not defined, so error!
resolve(374); // never gets here :(
});
p.then(
function fulfilled(){
// never gets here :(
},
function rejected(err){
// `err` will be a `TypeError` exception object
// from the `foo.bar()` line.
}
);
如果 Promise 对象已经执行了 fulfilled() 方法( fulfilled 与方法 fulfilled() 同名),那么在监控过程中(在 then() 方法注册回调内)抛出了一个 JS 异常时又会发生什么?即使它不会丢失,但是你可能会发现它们的处理方式有点令人吃惊。除非你挖的更深一点:
var p = new Promise( function(resolve,reject){
resolve(374);
});
p.then(function fulfilled(message){
foo.bar();
console.log(message); // never reached
},
function rejected(err){
// never reached
}
);
这串代码看起来来自 foo.bar() 的异常确实被吞噬了。但是,其实并没有。相反,更深层次的、监听不到的东西出错了。p.then() 方法调用自身来返回了另一个 promise 对象,并且这个 promise 对象因抛出 TypeError 异常而拒绝访问。
处理未抛出的异常
有很多人们觉得更好的其他方法。
通常的建议是 Promises 应该有一个 done(...) 方法,这本质上标记了 Promise 链上的“已做”,done() 没有创建和返回 Promise,因此,传递到 done(..) 的回调显然不会被链接,并将问题提交给一个不存在的链式 Promise 。
在未捕获的错误条件下,它会被处理:done() 内部的任何异常,都会将拒绝处理作为全局未捕获的错误抛出(在开发人员的控制台上,基本上是这样的):
var p = Promise.resolve(374);
p.then(function fulfilled(msg){
// numbers don't have string functions,
// so will throw an error
console.log(msg.toLowerCase());
})
.done(null, function() {
// If an exception is caused here, it will be thrown globally
});
view raw
在ES8中发生着什么?异步/等待
JavaScript ES8提出了异步/等待,使得和Promises一起完成的任务更加容易了。我们将简短地整理下异步/等待所提供的可能性以及如何利用它们去写异步代码。
接下来,我们一起来看看异步/等待是如何工作的。
使用异步函数声明定义了一个异步函数。那么该函数返回一个AsyncFunction对象。这个AsyncFunction对象代表了执行包含在函数内部代码的异步函数。当一个函数被调用时,它返回一个Promise。当异步函数返回一个值时,它不是一个Promise,Promise是会自动被创建,并和函数返回值一起被解决。当异步函数出现异常,Promise将会和生成的异常值一起被拒收。
异步函数可以包含一个等待表达式,它可以暂停函数的执行并等待上一个Promise的解决,然后恢复异步函数的执行并返回被解决的值。
你可以把JavaScript中的Promise看作成java中的Future或C #中的Task。
异步/等待的作用就是简化使用Promises的运转状态。
下面来看一个实例:
// Just a standard JavaScript function
function getNumber1() {
return Promise.resolve('374');
}
// This function does the same as getNumber1
async function getNumber2() {
return 374;
}
同样地,抛出异常的函数相当于返回被拒绝的Promises的函数:
function f1() {
return Promise.reject('Some error');
}
async function f2() {
throw 'Some error';
}
等待关键字只能用于异步函数并且允许同时等待Promise。如果我们在一个异步函数以外使用Promises,我们还必须使用回调:
async function loadData() {
// `rp` is a request-promise function.
var promise1 = rp('https://api.example.com/endpoint1');
var promise2 = rp('https://api.example.com/endpoint2');
// Currently, both requests are fired, concurrently and
// now we'll have to wait for them to finish
var response1 = await promise1;
var response2 = await promise2;
return response1 + ' ' + response2;
}
// Since, we're not in an `async function` anymore
// we have to use `then`.
loadData().then(() => console.log('Done'));
你也可以通过一个“异步函数表达式”来定义异步功能。异步函数表达式和异步函数声明非常相似,两者有着几乎相同的语法。异步函数表达式和异步函数声明之间的主要区别在于函数名,在异步函数表达式中创建匿名函数时函数名是可以省略的。异步函数表达式可以被当做一个 IIFE(立即调用函数表达式)来使用,即一被定义就可运行。
就像这个例子一样:
var loadData = async function() {
// `rp` is a request-promise function.
var promise1 = rp('https://api.example.com/endpoint1');
var promise2 = rp('https://api.example.com/endpoint2');
// Currently, both requests are fired, concurrently and
// now we'll have to wait for them to finish
var response1 = await promise1;
var response2 = await promise2;
return response1 + ' ' + response2;
}
更重要的是,异步/等待被所有主流浏览器支持:
最后,其实重要的事情不是盲目去选择“最新”的方式来编写异步代码。理解异步 JavaScript 的内部本质,了解它为什么这么重要以及深度理解你所选择的方法的内涵是极为必要的。就像编程中的其他方面一样,每种方法都有它各自的优点和缺点。
5个小技巧编写高度可维护,健壮的异步代码
1.清理代码:使用async/await可以让你编写更少的代码。每次使用async/await让你跳过一些不必要的步骤:编写。然后,创建一个匿名函数来处理响应,命名该回调的响应。
例如:
// `rp` is a request-promise function.
rp(‘https://api.example.com/endpoint1').then(function(data) {
// …
});
与:
// `rp` is a request-promise function.
var response = await rp(‘https://api.example.com/endpoint1');
2.错误处理:Async/await可以使用相同的代码结构——众所周知的try/catch语句处理同步和异步错误。让我们看看Promises的样子:
function loadData() {
try { // Catches synchronous errors.
getJSON().then(function(response) {
var parsed = JSON.parse(response);
console.log(parsed);
}).catch(function(e) { // Catches asynchronous errors
console.log(e);
});
} catch(e) {
console.log(e);
}
}
与:
async function loadData() {
try {
var data = JSON.parse(await getJSON());
console.log(data);
} catch(e) {
console.log(e);
}
}
3.条件:用async/await编写条件代码更直截了当:
function loadData() {
return getJSON()
.then(function(response) {
if (response.needsAnotherRequest) {
return makeAnotherRequest(response)
.then(function(anotherResponse) {
console.log(anotherResponse)
return anotherResponse
})
} else {
console.log(response)
return response
}
})
}
view raw
与:
async function loadData() {
var response = await getJSON();
if (response.needsAnotherRequest) {
var anotherResponse = await makeAnotherRequest(response);
console.log(anotherResponse)
return anotherResponse
} else {
console.log(response);
return response;
}
}
4.堆栈框架:与async/await不同,从promise链返回的错误堆栈不知道发生错误的位置。看看下面的内容:
function loadData() {
return callAPromise()
.then(callback1)
.then(callback2)
.then(callback3)
.then(() => {
throw new Error("boom");
})
}
loadData()
.catch(function(e) {
console.log(err);
// Error: boom at callAPromise.then.then.then.then (index.js:8:13)
});
与:
async function loadData() {
await callAPromise1()
await callAPromise2()
await callAPromise3()
await callAPromise4()
await callAPromise5()
throw new Error("boom");
}
loadData()
.catch(function(e) {
console.log(err);
// output
// Error: boom at loadData (index.js:7:9)
});
5.调试:如果你使用过promise,你知道调试它们是一场噩梦。例如,如果在.then块中设置断点并使用“stop-over”之类的调试快捷方式,则调试器将不会移动到以下位置,因为它只通过同步代码“steps”。
通过async/await,您可以完全按照正常的同步函数一步步地等待调用。
编写异步JavaScript代码不仅对于应用程序本身而且对于编写js库也很重要。
例如,SessionStack库会记录您的Web应用程序/网站中的所有内容:所有DOM更改,用户交互,JavaScript异常,堆栈跟踪,网络请求失败以及调试消息。
而这一切都必须在您的生产环境中发生,而不会影响任何用户体验。我们需要大量优化我们的代码,并尽可能使其异步,以便我们可以增加事件循环中可以处理的事件的数量。
而不只是js库!在SessionStack中重现用户会话时,我们必须在出现问题时渲染用户浏览器中发生的所有事情,并且必须重构整个状态,以便在会话时间线中来回跳转。为了使这成为可能,我们正在大量使用JavaScript提供的异步机制来实现。
这里有一个免费的计划,你可以从这里开始。
资源:
https://github.com/getify/You-Dont-Know-JS/blob/master/async%20%26%20performance/ch2.md
https://github.com/getify/You-Dont-Know-JS/blob/master/async%20%26%20performance/ch3.md