【JavaScript】同步和异步

同步

同步:只有当前API执行完成后,才能继续执行下一个API

console.log('before'); 
console.log('after');
//同步API从上到下依次执行,前面代码会阻塞后面代码的执行
//输出结构为:
//before
//after

异步

异步:当前API的执行不会阻塞后续代码的执行

console.log('before');
setTimeout(
   () => { console.log('last');
}, 2000);
console.log('after');
//异步则不会等待API执行完成后再向下执行代码
//输出为:
//before
//after
//last

** 为什么明明console.log(‘last’);在console.log(‘after’);之前却先输出了after?**
这是因为JavaScript的代码执行机制引起的

代码执行规则

在这里插入图片描述
代码运行时,JavaScript维护三个空间分别是 **执行栈 异步任务处理区 任务队列 **

根据代码的同步还是异步JavaScript会做出以下操作:

1.先执行,执行栈中的同步任务
2.当遇到异步任务,将其放入异步任务处理区中,而后继续执行下面的同步任务
3.当异步任务(如绑定的事件或计时器),在异步任务处理区中被触发JavaScript会按照触发顺序先后将其放入任务队列中等待被调用
4.一旦执行栈中的所有同步任务执行完毕,系统就会按次序读取任务队列中的异步任务,于是被读取的异步任务结束等待状态,进入执行栈,开始执行。

注意:执行栈中同步任务执行完后,将任务队列中的异步任务放入执行栈最下方开始执行

现在我们就知道了为啥会有同步代码和异步代码了

但是同步和异步还有一个非常巨大的区别

获取返回值

我们都知道同步API可以从返回值中拿到API执行的结果, 但是异步API却是不可以的

 // 同步
  function sum (n1, n2) { 
      return n1 + n2;
  } 
  const result = sum (10, 20);//30
// 异步
  function getMsg () { 
      setTimeout(function () { 
          return { msg: 'Hello Node.js' }
      }, 2000);
  }
  const msg = getMsg ();
  console.log(msg);//undefined

为什么会是undefined?
我们知道JavaScript的代码执行机制,会将异步代码放入任务队列中,而继续执行下面的同步代码,而造成undefined的原因正是异步函数还没有被触发,所以当调用getMsg ()函数时其中异步函数不阻塞线程,代码继续执行,getMsg ()函数默认return了一个undefined

所以这就造成了异步代码的不可控,我们根本不知道他什么时候执行,也就无法直接获取它的执行结果

那有办法解决么?
当然

回调函数获取异步API返回值

我们可以定义一个这样的函数:

function getData(callback) {
    callback(123);
}
getData(function(n) {
   console.log(n)//输出123
});

这就是回调函数

通过在函数形参中拿到的回调函数引用地址,可以调用回调函数并传递参数给回调函数的形参,这样回调函数就可以获得被调用函数中的信息

所以我们刚刚的代码可以写成这样

function getMsg (callback) {
	setTimeout(function () {
		callback({
			msg: 'hello node.js'
		})
	}, 2000)
}

getMsg(function (data) {
	console.log(data);
});

异步代码执行顺序

异步代码除了返回值还有一个执行顺序问题

console.log('代码开始执行');
setTimeout(() => {
    console.log('2秒后执行的代码');
}, 2000); 
setTimeout(() => {
    console.log('"0秒"后执行的代码');
}, 0);
console.log('代码结束执行');

上面的代码输出结果是什么呢?

我们知道JavaScript的代码执行机制,来分析一下:
很明显在执行栈中遇到异步代码,就会将其放到异步代码执行区,这时两个定时器都被放到异步代码执行区中了,然后在异步代码执行区中先触发的异步代码会先一步被放入任务队列中,等待调用

显然输出结果是先输出“0秒"后执行的代码”,然后再输出’2秒后执行的代码’

这样不可控制的输出顺序显然不是我们期望的,那么该如何解决呢?

回调函数控制异步代码执行顺序

function getData(callback) {
    setTimeout(() => {
        callback(123);
    }, 0);
}

console.log('代码开始执行');
getData(function(n) {
    console.log('callback函数被调用了')
    console.log(n)
    console.log('callback函数结束了')
});
console.log('代码结束执行');
//代码开始执行
//代码结束执行
//callback函数被调用了
//123
//callback函数结束了

我们看到在回调函数中代码是依次进行的

回调函数的作用就是,将异步代码写在一个块级作用域中,当其中的异步函数执行后,才会调用这个回调函数,这时异步函数已经执行完成,所以在回调函数中代码就可以如同步代码一样依次执行

所以问题就解决了,只需要在回调函数中调用其他的异步API就可以保证异步代码的顺序执行

function getData(callback) {
    setTimeout(function() {
        console.log('2s')
        setTimeout(function() {
            console.log('0s')
            callback(123);
        }, 0)
    }, 2000)


}

console.log('代码开始执行');
getData(function(n) {
    console.log(n)
});
console.log('代码结束执行');
//代码开始执行
//代码结束执行
//2s
//0s
//123

但这样做会有一个后果,那就是回调地狱!!

回调地狱

需求:依次读取A文件、B文件、C文件

const fs = require('fs');

fs.readFile('./A.txt', 'utf8', (err, result1) => {
	console.log(result1)
	fs.readFile('./B.txt', 'utf8', (err, result2) => {
		console.log(result2)
		fs.readFile('./C.txt', 'utf8', (err, result3) => {
			console.log(result3)
		})
	})
});

将依赖当前异步API的执行结果的代码,写到所依赖的异步API的回调函数中,逐层嵌套,这样虽然可以有效解决异步API代码依赖问题,但这样写出来的代码十分复杂,不可维护。

这样写不但繁琐难以维护,而且写起来十分的麻烦,所以promise模块应运而生

promise

为了解决回调地狱问题,在ES6中提供了Promise

Promise实际上就是在原本的异步API上面包裹一层函数,其中Promise参数函数的resolve , reject两个参数,实际上和普通的回调函数一样,都接受一个回调函数作为实参,而在运行时返回一个实参给调用他的then或catch两个回调函数,这样就会获得异步API中的执行结果

而这个包裹的一层函数就是promise对象

let promise = new Promise((resolve, reject) => {
    setTimeout(() => {
        if (true) {
            resolve({name: '张三'})
        }else {
            reject('失败了') 
        } 
    }, 2000);
});
promise.then(result => console.log(result); // {name: '张三'})
       .catch(error => console.log(error); // 失败了)

而且promise还可以链式调用
Promise 的链式调用是通过 .then() 和 .catch() 方法实现的,其中 .catch() 等价于 .then(null, onRejected),因此我们在接下来的内容当中只需研究 .then() 方法的性质即可。

.then() 方法除了用于注册监听函数之外,本身也会创建并返回一个 Promise 对象,这个 Promise 对象用于表征回调函数的执行情况。
当回调函数执行成功时(内部的resolve函数调用) Promise 状态将变更为 ‘fulfilled’,执行过程抛出异常 (内部的rejected函数调用)Promise 状态则变成 ‘rejected’

透传

在链式调用过程当中,假如某个环节的 Promise 不存在相应状态的监听回调函数,那么这个 Promise 的状态将会往下透传
即,Promise中回调函数执行,却没有相应的then()或catch()处理,所以会将上一个 Promise 的状态持续透传了下去。

Promise 值传递

.then() 方法会将回调函数的执行结果(then中回调函数的return)记录下来,并作为下一个 onFulfilled 回调的参数将其传递下去
由于回调函数可以返回任何结果,甚至返回一个 Promise 对象也是可行的。
回调函数所抛出的错误将作为下一个 onRejected 的参数传递下去

猜你喜欢

转载自blog.csdn.net/qq_43915356/article/details/105349358