学习目标
完成本单元后,您将能够:
- 识别JavaScript中重要的异步功能。
- 使用setTimeout异步调用函数。
- 编写和调用回调函数。
- 编写和调用基于promise的函数。
- 描述Aura组件中的异步功能。
回想起我们最初引入JavaScript引擎时的方式。引擎有一个单线程,它可以正常工作,完成工作,然后填充新的工作以重新开始。
当然,至关重要的是不要阻塞线程。
让我们来看一个例子。
<html>
<script>
alert("Does JavaScript show first?");
</script>
<body>
<p>
Does HTML show first?
</p>
</body>
</html>
如果在浏览器中加载该HTML页面,则会发现先弹出警报,然后阻止HTML显示。这是因为该alert()
函数会中止JavaScript线程的执行,直到用户将其关闭为止。简而言之,当JavaScript阻止您的浏览器时,就永远不会带来良好的用户体验。
好消息是,除了一些alert()
上面的功能仍然存在的遗留特性外,JavaScript是一种异步语言。
异步JavaScript无处不在
为了开始异步之旅,让我们重新回顾事件和功能。以前我们看过这样的HTML和JavaScript。
<!-- HTML -->
<button id="clicker">
//JavaScript
let button = document.getElementById("clicker");
button.addEventListener("click", handleClick);
在此示例中,我们将handleClick
事件控件添加到按钮发出的click事件中。
在那里!我们已经编写了一些异步JavaScript。
事件触发时,所有发生的事情是将新消息添加到队列中。没有事件可以接管线程。触发的每个事件都必须进入队列并等待轮流运行。
一种说明方法是使用setTimeout
函数。在此 示例 invoking中setTimeout
,我们以毫秒为单位传递了事件处理程序和计时器。计时器到时,它将触发,将事件处理程序添加到队列中。
setTimeout(function(){
console.log("This comes first");
}, 0);
console.log("This comes second");
//output in console
// "This comes second"
// "This comes first"
在这里,我们将计时器设置为零。但这并不意味着“立即致电”。这仅表示“立即将其放入队列中”。但是,代码块本身的执行需要完成,从而清除了调用堆栈。只有这样,函数才能setTimeout
轮流使用。
另一个常见的错误是认为计时器是事件处理程序何时触发的准确预测器,但不一定。事件处理程序仍然必须等待轮到队列。通过测量时间,我们可以看到这个 在行动。
const timer = function(){
let start = Date.now();
setTimeout(function(){
let end = Date.now();
console.log( "Duration: " + (end - start) )
},1000);
};
timer();
// Console output when invoked several times:
// "Duration: 1007"
// "Duration: 1000"
// "Duration: 1002"
// "Duration: 1004"
时间设置为一秒,并且它接近于该时间。但是很明显,将函数添加到队列然后在每次调用时运行的速度有所不同。
现在,我们已经看到了一些异步调用的示例,我们可以看看一些常见的异步模式和构造。
回调模式
回调只是一个传递给另一个函数的函数,该函数会在将来的某个时刻调用它。
因此,实际上,我们已经看到了很多回调。
setTimeout(callback, timer)
Element.addEventListener(event, callback)
Array.map(function(item){...})
让我们将其应用于自行车 用例,以了解如何实现回调。当您换挡时,大多数情况下它都起作用。但是失败的可能性仍然很小。这是异步JavaScript的理想方案。让我们看一下回调的外观,该回调接收有关齿轮如何移动的数据,然后在完成时调用传入的函数。
Bike.prototype.changeGearAsync = function(shiftObject, callback){
let newIndex = shiftObject.currentIndex + shiftObject.changeBy;
if (newIndex < 0 || newIndex > shiftObject.maxIndex) {
callback(new Error("There is a problem"), null);
} else {
callback(null, newIndex);
}
};
该参数callback
实际上是一个函数。如果有错误,我们将调用它并为第一个参数设置要发送回的任何错误数据。成功后,我们将错误参数设为空并传回正确的数据。然后,我们可以看到如何调用新的换档功能。
Bike.prototype.changeGear = function(frontOrRear, changeBy) {
const shiftIndexName = frontOrRear + "GearIndex"
const that = this;
//contains state change for making the shift
let shiftObject = {
currentIndex: this[shiftIndexName],
maxIndex: this.transmission[frontOrRear + "GearTeeth"].length,
changeBy: changeBy
}
// invoke async function with anonymous callback
this.changeGearAsync(shiftObject, function(err, newIndex){
if (err) {
console.log("No Change");
} else {
that[shiftIndexName] = newIndex;
}
});
};
回调模式已被广泛接受并广泛使用,但是它也有一些缺点。首先,当多个回调链接在一起时,它们被嵌套在另一个中。这会造成不必要的复杂性,可读性问题,并且在阅读别人的代码时很难推理。此缺陷称为回调地狱。回调也没有隐式错误状态(像try/catch
这样)。由开发人员编写回调以明确查找带有if
条件的错误,这取决于开发人员。这些障碍导致了诺言的创造。
箭头功能
在前面的示例中,您可能已经注意到了这一行。
const that = this;
这是旧版JavaScript的遗物。我们仅介绍一种新的函数语法:箭头函数。回想一下调用函数时会发生什么。具体来说,它将绑定到新this
上下文。与匿名函数关闭范围内的其他变量不同,JavaScript this
实际上在实际上我们需要this
包含函数时重新绑定。
一个长期的解决方法是分配this
一个新变量(按照惯例通常称为self
or that
),然后将上下文引用保留在闭包中。
箭头函数通过不重新绑定来消除编码技巧的这一点this
。箭头函数语法如下所示:
(arg1, arg2) => {...function body...}
使用箭头功能,我们可以删除该that = this
位并将其调用更改changeGearsAsync
为以下内容。
// the anonymous function is now an arrow function
this.changeGearAsync(shiftObject, (err, newIndex)=>{
if (err) {
console.log("No Change");
} else {
// we reference this instead of that
this[shiftIndexName] = newIndex;
}
});
有前途的东西
开发为处理异步代码的库的承诺,使您可以更轻松地推断代码成功或失败的时间。它们还包含内置机制,可将一个呼叫链接到另一个呼叫。最终,竞争库在浏览器中被标准化为Promise
对象。让我们变身bike
一个多 的时间。
Bike.prototype.changeGearAsync = function(shiftObject){
return new Promise(
(resolve, reject) => {
let newIndex = shiftObject.currentIndex + shiftObject.changeBy;
if (newIndex < 0 || newIndex > shiftObject.maxIndex) {
reject("New Index is Invalid: " + newIndex);
} else {
resolve(newIndex);
}
}
);
};
首先,更新后的changeGearAsync
函数接收我们传递的数据,并返回一个新的Promise对象。我们传入一个参数:一个回调函数,它本身有两个函数传递给它,resolve
和reject
。
在实现promise时,您可以执行回调函数中所需的任何计算,请求等。完成后,如果一切顺利,您将resolve
使用要传递回的数据进行调用。如果遇到问题,您可以通过调用reject
任何相关错误作为参数来向函数调用者发出信号。
让我们看看我们现在如何使用它。
// invoke async function that returns a promise
this.changeGearAsync(shiftObject)
.then(
(newIndex) => {
this[shiftIndexName] = newIndex;
console.log(this.calculateGearRatio());
}
)
.catch(
(err) => {console.log("Error: " + err);}
);
现在,我们有了一些更容易推理的东西。如果changeGearAsync
可行,则将then
函数与传递给其参数的函数一起调用。如果没有,catch
则被调用。
如果回调函数本身返回的实例Promise
,那么事情就会变得令人兴奋。您可以简单地将这两个Promise函数链接在一起。举例来说,如果我们想改变 这两个前后齿轮。
Bike.prototype.changeBothGears = function(frontChange, rearChange) {
let shiftFront = {
currentIndex: this.frontGearIndex,
maxIndex: this.transmission.frontGearTeeth.length - 1,
changeBy: frontChange
};
let shiftRear = {
currentIndex: this.rearGearIndex,
maxIndex: this.transmission.rearGearTeeth.length - 1,
changeBy: rearChange
};
this.changeGearAsync(shiftFront)
.then(
(newIndex) => {
this.frontGearIndex = newIndex;
console.log(this.calculateGearRatio());
return this.changeGearAsync(shiftRear);
}
)
.then(
(newIndex) => {
this.rearGearIndex = newIndex;
console.log(this.calculateGearRatio());
}
)
.catch(
(err) => {console.log("Error: " + err);}
);
};
changeBothGears
上面的函数向我们显示了对的两个调用的链接changeGearsAsync
,每个调用都与对应于前齿轮或后齿轮的对象相关。第一次调用之后,我们在第一次调用结束时再次调用它then
。then
可以在上面加上另一个。从根本上讲,每当一个then
承诺返回时,都可以跟随另一个承诺,then
直到我们用尽了所有链式动作。
异步/等待
在签字之前,值得一提的是异步武器库中的一些新功能:async
和await
运算符。这些基于promise,使它们的使用方式与同步JavaScript非常相似。
闪电Web组件和异步JavaScript
利用Lightning Web Components,开发人员可以同时使用基于承诺的异步功能和异步/等待功能。现在唯一的建议是,为开发人员在Internet Explorer 11上为用户创建功能,async
/ await
在该浏览器中未实现。不过,请放心,您的代码可以使用。但是,当在IE11中使用async / await运行任何内容时,LWC会自动使用polyfill,以便语法正确运行。因此,如果经常使用,IE11的性能可能会有所下降。
与Salesforce互动
在Lightning Web Components中实现了使用异步JavaScript的若干功能。其中大多数围绕与服务器的交互。一个示例是可以在Lightning Web组件中强制调用Apex方法的方法。
考虑以下Apex类和方法:
public with sharing class ContactController {
@AuraEnabled(cacheable=true)
public static List<Contact> findContacts(String searchKey) {
if (String.isBlank(searchKey)) {
return new List<Contact>();
}
String key = '%' + searchKey + '%';
return [SELECT Id, Name, Title, Phone, Email, Picture__c FROM Contact WHERE Name LIKE :key AND Picture__c != null LIMIT 10];
}
...
Lightning Web Components使用基于Promise的API来展示此方法。您可以这样调用它:
import { LightningElement, track } from 'lwc';
import findContacts from '@salesforce/apex/ContactController.findContacts';
export default class ApexImperativeMethodWithParams extends LightningElement {
@track searchKey = '';
@track contacts;
@track error;
handleSearch() {
findContacts({ searchKey: this.searchKey })
.then(result => {
this.contacts = result;
this.error = undefined;
})
.catch(error => {
this.error = error;
this.contacts = undefined;
});
}
}
注意
这里有一些超出此模块范围的内容,最值得注意的是ES6中JavaScript模块的使用。要了解这些功能,这是考虑进入本教程“现代JavaScript开发”中的下一个模块的绝佳时机。但是在您这样做之前,请先了解一下此代码。
当我们称其import findContacts…
为标准模块语法时,该组件中将包含另一个模块的功能。我们在findContacts
这里将Apex方法展示为同名的JS函数。
当我们在handleSearch()
函数中调用它时,Apex方法的参数作为文字对象传递,然后我们看到了then和catch
函数的基于promise的语法。