13

异步与回调/函数的作用域链

异步操作概述--阮一峰的javascript教学

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是单线程模式的,那么他是怎么实现异步操作的?
答:

  1. js里面的任务分为同步任务异步任务
  2. 同步任务进入主线程一个一个得执行。异步任务进入任务队列,等同步任务执行完了之后,只有触发了某个条件,才把任务队列里面的任务放到主线程执行(比如ajax得到返回的数据,就开始执行回调函数,setTimeOut的时间到了,就执行回调函数)
  3. 异步任务的写法通常是回调函数。一旦异步任务重新进入主线程,就会执行对应的回调函数。
  4. 同步任务执行完之后引擎就一遍一遍得检查。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();//起床

结果:
PTiLzd.png

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就是回调函数

区分同步和异步

PTQojK.png
就是因为有了setTimeout才算异步

所以我们来看看ajax.如果$.ajax()是同步的,即我们发送请求,然后等待服务器发回的响应来到之后在继续执行下面的代码,那么有什么后果:

假设我们想直接拿到请求的结果,那么我们有下面的代码:
PTQX4A.png
意思就是不管请求相应多久,都等着,直到响应接收到,然后返回给这个创建的变量response.如果从发送请求到拿到相应用了2s,那么代码就停在这里了2s.

所以$.ajax()是异步的,我们拿到的只是一个承诺(Promise),我承诺会执行,并承诺会在拿到结果后执行什么什么什么
如下:
PTlkNj.png
所以就可以使用promise.then(success,error)承诺成功之后执行success函数,承诺失败后执行error函数.
这个success,error就是callBack(回调函数),这个Promise(承诺)就是异步任务
promise就是知道没法得到结果,那我就要你一个承诺,要承诺好拿到结果后要做什么事.
所以$.ajax()返回的结果是一个承诺,不是结果,因为结果还没有到来

使用回调函数

使用回调要用这样的形式

fn(参数1,参数2,()=>{
    回调函数(xxx,xxx,()=>{})
})

不要用

fn(参数1,参数2,回调函数(xxx,xxx))

因为这个参数里传入的回调函数(xxx,xxx)并不是函数本身,而是运行完毕之后的返回值.

下面带我是我的一个小作品里的一部分代码,一直在嵌套回调函数.
PHiIoV.png

会动的简历--完整代码地址
会动的简历--预览地址

函数的作用域链

先看面试题
题目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

解密

  1. 函数在执行的过程中,先从自己内部找变量
  2. 如果找不到,再从创建当前函数所在的作用域去找, 以此往上
  3. 注意找的是变量的当前的状态

风彻
1.5k 声望142 粉丝

引用和评论

0 条评论