异步与回调/函数的作用域链
异步与回调/函数的作用域链
JavaScript 只在一个线程上运行,JavaScript 同时只能执行一个任务,其他任务都必须在后面排队等待。
这种模式的好处是实现起来比较简单,执行环境相对单纯;坏处是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。
JavaScript 语言本身并不慢,慢的是读写外部数据,比如等待 Ajax 请求返回结果。这个时候,如果对方服务器迟迟没有响应,或者网络不通畅,就会导致脚本的长时间停滞。
异步与回调
同步任务与异步任务
程序里面所有的任务,可以分成两类:同步任务(synchronous)和异步任务(asynchronous)。
同步任务是那些没有被引擎挂起、在主线程上排队执行的任务。只有前一个任务执行完毕,才能执行后一个任务。
异步任务是那些被引擎放在一边,不进入主线程、而进入任务队列的任务。只有引擎认为某个异步任务可以执行了(比如 Ajax 操作从服务器得到了结果),该任务(采用回调函数的形式)才会进入主线程执行。排在异步任务后面的代码,不用等待异步任务结束会马上运行,也就是说,异步任务不具有”堵塞“效应。
举例来说,Ajax 操作可以当作同步任务处理,也可以当作异步任务处理,由开发者决定。如果是同步任务,主线程就等着 Ajax 操作返回结果,再往下执行;如果是异步任务,主线程在发出 Ajax 请求以后,就直接往下执行,等到 Ajax 操作有了结果,主线程再执行对应的回调函数。
任务队列和事件循环
JavaScript 运行时,除了一个正在运行的主线程,引擎还提供一个任务队列(task queue),里面是各种需要当前程序处理的异步任务。(实际上,根据异步任务的类型,存在多个任务队列。为了方便理解,这里假设只存在一个队列。)
首先,主线程会去执行所有的同步任务。等到同步任务全部执行完,就会去看任务队列里面的异步任务。如果满足条件,那么异步任务就重新进入主线程开始执行,这时它就变成同步任务了。等到执行完,下一个异步任务再进入主线程开始执行。一旦任务队列清空,程序就结束执行。
异步任务的写法通常是回调函数。一旦异步任务重新进入主线程,就会执行对应的回调函数。如果一个异步任务没有回调函数,就不会进入任务队列,也就是说,不会重新进入主线程,因为没有用回调函数指定下一步的操作。
JavaScript 引擎怎么知道异步任务有没有结果,能不能进入主线程呢?答案就是引擎在不停地检查,一遍又一遍,只要同步任务执行完了,引擎就会去检查那些挂起来的异步任务,是不是可以进入主线程了。这种循环检查的机制,就叫做事件循环(Event Loop)。维基百科的定义是:“事件循环是一个程序结构,用于等待和发送消息和事件(a programming construct that waits for and dispatches events or messages in a program)”。
问关于异步、主线程、事件循环(Event Loop)的时候:按照下面回答:
面试官问问题:js是单线程模式的,那么他是怎么实现异步操作的?
答:
- js里面的任务分为同步任务和异步任务
- 同步任务进入主线程一个一个得执行。异步任务进入任务队列,等同步任务执行完了之后,只有触发了某个条件,才把任务队列里面的任务放到主线程执行(比如ajax得到返回的数据,就开始执行回调函数,setTimeOut的时间到了,就执行回调函数)
- 异步任务的写法通常是回调函数。一旦异步任务重新进入主线程,就会执行对应的回调函数。
- 同步任务执行完之后,引擎就一遍一遍得检查。JavaScript 引擎怎么知道异步任务有没有结果,能不能进入主线程呢?答案就是引擎在不停地检查,一遍又一遍,只要同步任务执行完了,引擎就会去检查那些挂起来的异步任务,是不是可以进入主线程了。这种循环检查的机制,就叫做事件循环(Event Loop)。事件循环机制一遍一遍得检查,符合条件,就把异步任务放到主线程里面去执行(比如ajax返回的数据到了,setTimeOut里面的时间到了)
异步操作的方法:
1.回调函数
2.事件监听(触发条件,执行回调函数)
3.ES6:Promise
定时器
它们向任务队列添加定时任务。时间到就添加,然后事件循环就会扫到,扫到了就执行里面的回调函数。
异步操作
异步操作的模式--回调函数
有这样一个问题:
我想先定个闹钟,三秒钟后闹钟就会响.这时候我再起床.
如果代码这样写:
function setClock(){ console.log('1定一个闹钟,三秒钟之后响'); setTimeout(()=>{ console.log('2三秒到了,闹钟响了!'); },3000) } function getUp(){ console.log('3闹钟已经响了,该起床了') } setClock();//定闹钟 getUp();//起床
结果:
getUp();//起床
这个函数不会等到三秒后执行,而是会在setClock()
执行后立即执行.
异步就是不等结果,直接进行下一步.setClock();//定闹钟
执行完了之后直接进行下一步getUp();//起床
setClock();//定闹钟
就是异步代码,不等待setClock()
执行完就执行getUp()
,setClock()
就是异步任务
解决方法是使用回调函数:
回调是拿到异步结果的一种方式
(其实回调也可以拿同步结果)
举一个例子:
- 同步:我让黄牛去买票,我站着等他买好票再给我,然后再去做别的.
- 异步:我让黄牛去买票(告诉黄牛买到票就call我一下),然后我继续去做别的事
这里:我让黄牛去买票,然后我继续去做别的事
就是异步,括号里的(告诉黄牛买到票就call我一下)
就是回调
callBack英文有回电话的意思.就是打电话回去告诉异步结果已经得到了,可以继续依照这个结果来做下面的事了.callBack就是这个意思
代码执行完在执行下面的代码就是同步,代码没有执行完就去执行下面的代码就是异步
使用回调函数
function setClock(callBack){ console.log('1定一个闹钟,三秒钟之后响'); setTimeout(()=>{ console.log('2三秒到了,闹钟响了!'); callBack(); },3000) } function getUp(){ console.log('3闹钟已经响了,该起床了') } setClock(getUp);
将getUp
作为参数传入setClock
函数,等三秒后在执行函数.getUp
就是回调函数
区分同步和异步
就是因为有了setTimeout
才算异步
所以我们来看看ajax.如果$.ajax()是同步的,即我们发送请求,然后等待服务器发回的响应来到之后在继续执行下面的代码,那么有什么后果:
假设我们想直接拿到请求的结果,那么我们有下面的代码:
意思就是不管请求相应多久,都等着,直到响应接收到,然后返回给这个创建的变量response
.如果从发送请求到拿到相应用了2s,那么代码就停在这里了2s.
所以$.ajax()
是异步的,我们拿到的只是一个承诺(Promise),我承诺会执行,并承诺会在拿到结果后执行什么什么什么
如下:
所以就可以使用promise.then(success,error)
承诺成功之后执行success
函数,承诺失败后执行error
函数.
这个success
,error
就是callBack
(回调函数),这个Promise
(承诺)就是异步任务
promise就是知道没法得到结果,那我就要你一个承诺,要承诺好拿到结果后要做什么事.
所以$.ajax()
返回的结果是一个承诺,不是结果,因为结果还没有到来
使用回调函数
使用回调要用这样的形式
fn(参数1,参数2,()=>{ 回调函数(xxx,xxx,()=>{}) })
不要用
fn(参数1,参数2,回调函数(xxx,xxx))
因为这个参数里传入的回调函数(xxx,xxx)
并不是函数本身,而是运行完毕之后的返回值.
下面带我是我的一个小作品里的一部分代码,一直在嵌套回调函数.
函数的作用域链
先看面试题
题目1
var a = 1 function fn1(){ function fn2(){ console.log(a) } function fn3(){ var a = 4 fn2() } var a = 2 return fn3 } var fn = fn1() fn() //2
题目2
var a = 1 function fn1(){ function fn3(){ var a = 4 fn2() } var a = 2 return fn3 } function fn2(){ console.log(a) } var fn = fn1() fn() //1
题目3
var a = 1 function fn1(){ function fn3(){ function fn2(){ console.log(a) } var a fn2() a = 4 } var a = 2 return fn3 } var fn = fn1() fn() //undefined
解密
- 函数在执行的过程中,先从自己内部找变量
- 如果找不到,再从创建当前函数所在的作用域去找, 以此往上
- 注意找的是变量的当前的状态