彻底搞懂js事件循环机制/js异步编程

一、知识储备

​ 在学习前端的时候看过很多帖子讲js的运行机制、js事件循环。说实话一看就忘,所以想自己整理一遍,以加深自己的理解。自己写过的东西即使是重复的,但至少在这个过程我也在思考和总结,也避免面试的时候被问到的时候支支吾吾说不出来,或者模糊的似懂非懂。

首先深入理解js的异步机制,必须要先理解几个概念。

Q1:什么是进程和线程?

A1:这是操作系统的基本概念,忘记的回去复习操作系统。

Q2:什么是同步和异步?

A2:同步是调用一旦开始,调用者必须等到调用方法返回后,才能继续后续的行为。调用这会等待主动结果。异步是当一个异步调用发出后,这个调用就立刻返回,调用者不会立即得到结果。而是通过某些通知来通知调用者,通过回调函数来处理这个调用。打个比喻,我是个很喜欢早上起来很喜欢喝一杯温开水的人,第一天我很笨,我点了烧开水的按钮之后我就一直在旁边等它烧开,在这等待期间我什么事情都没做,只是在等待我的水烧开,这就是同步。第一天晚上我躺下去睡觉的时候觉得我早上真的是愚蠢至极,这样等待完全是在浪费时间而且只能做一件事情。于是第二天,我在等烧开水的时候我去刷牙去了,等到水烧开了我才回来倒水,这就是异步。看,第二天做的事情比第一天多而且时间上利用率更大。

Q3:js为什么是单线程?

A3:js是浏览器的脚本语言,主要用于与用户进行交互以及操作DOM,这就决定了它只能是单线程的。假设js它有两个线程,一个节点说在某在DOM节点上添加内容,另一个线程说在直接删除这个DOM节点,你说浏览器要听哪一个线程呢?在HTML5有一个Worker线程,这是为了提高计算能力允许js创建多个线程,但是子线程完全受主线程控制的,而且它是不能操作DOM的。所以本质上还是单线程的原理,没有改变本质。

Q4:浏览器是多进程的?

A4:浏览器是多进程的,每次在浏览器打开一个Tab页面,就会产生一个进程。日常生活中我们不建议打开多个页面不关闭,会造成电脑越来越卡,很消耗CPU,像我们这种贫民窟女大学生还是爱护电脑一点,毕竟买不起新的。

二、js EventLoop事件循环机制

Q:什么是事件循环机制?

A:js引擎线程只会执行执行栈中的事件,执行栈中的代码执行完毕,就会读取事件队列中的事件并添加到执行栈中继续执行,这样Loop循环反复就是Event Loop。

2.1js的事件分为两种
  • 宏任务:整体代码script、setTimeout、setInterval、setImmediate()-Node、requestAnimationFrame()-浏览器
  • 微任务:Promise.then()、process.nextTick()–Node、catch、finally、Object.observe、MutationObserver
2.2事件执行顺序

先执行宏任务,然后执行微任务,这个是基础,任务可以有同步任务和异步任务,同步的进入主线程,异步的进入Event Table并注册函数,异步事件完成后,会将回调函数放入Event Queue中(宏任务和微任务是不同的Event Queue),同步任务执行完成后,会从Event Queue中读取事件放入主线程执行,回调函数中可能还会包含不同的任务,因此会循环执行上述操作。

执行 宏任务--微任务的Event Queue--宏任务的Event Queue
2.3宏任务

每一个宏任务都会从头执行到尾,不会执行其他。js引擎线程和GUI渲染线程是互斥关系。在一个宏任务完成之后在下一个宏任务完成之前,GUI渲染会对页面进行渲染。

宏任务--GUI渲染--宏任务
2.4微任务

ES6引入promise,微任务可以理解成当前宏任务执行后立即执行的任务。

宏任务--微任务--GUI渲染--宏任务

setTimeout不能精准执行的问题

setTimeOut并不是直接的把回掉函数放进异步队列中去,而是在定时器的时间到了之后,把回掉函数放到执行异步队列中去。需要满足两个条件:主进程必须是空闲的状态,如果不空闲,时间到了也不会执行;回调函数必须等到插入异步队列前面的异步函数执行完毕才会执行。

2.5执行栈执行顺序

(1)判断是否为同步,异步则进入异步进程,最终事件回调给事件触发线程的任务队列等待执行,同步继续执行

(2)执行栈为空,询问队列中是否有事件回调

(3)任务队列中有事件回调则把它加入执行栈末尾

(4)任务队列中没有事件回调则不停发起询问

彻底搞懂js事件循环机制/js异步编程_第1张图片

完整执行顺序

彻底搞懂js事件循环机制/js异步编程_第2张图片

三、同步和异步

同步

  • 指在 主线程上排队执行的任务,只有前一个任务执行完毕,才能继续执行下一个任务。
  • 也就是调用一旦开始,必须这个调用 返回结果才能继续往后执行。程序的执行顺序和任务排列顺序是一致的。

异步

  • 异步任务是指不进入主线程,而进入 任务队列的任务,只有任务队列通知主线程,某个异步任务可以执行了,该任务才会进入主线程。
  • 每一个任务有一个或多个 回调函数。前一个任务结束后,不是执行后一个任务,而是执行回调函数,后一个任务则是不等前一个任务结束就执行。
  • 程序的执行顺序和任务的排列顺序是不一致的,异步的。
  • 我们常用的setTimeout和setInterval函数,Ajax都是异步操作。

四、经典前端笔试题—async/await、promise、setTimeout的执行顺序

在前面三点的知识点都能掌握的前提下,要理解这一道面试题并不是难事

async function async1() {
	console.log('async1 start');
	await async2();
	console.log('asnyc1 end');
}
async function async2() {
	console.log('async2');
}
console.log('script start');
setTimeout(() => {
	console.log('setTimeOut');
}, 0);
async1();
new Promise(function (reslove) {
	console.log('promise1');
	reslove();
}).then(function () {
	console.log('promise2');
})
console.log('script end');

解析

事件执行顺序是宏任务—微任务。执行顺序是宏任务—微任务Event Queue—宏任务Event Queue。

任务有同步任务和异步任务。同步进入主线程,异步先在Event Table注册函数,等待异步事件完成后,将它的回调函数放到Event Queue。宏任务和微任务的Event Queue不一样。同步任务完成后,在主进程是空闲的状态下,从Event Queue中读取事件放入主线程。

宏任务是整体代码script/setTimeout/seterval,微任务是Promise/.then。执行顺序。

new Promise是同步任务,放入主线程。.then()是异步任务,等promise状态结束的时候,放入异步队列。async关键词函数返回一个promise对象。await关键字在async关键字的函数内部,在外部会报错。await等待右侧表达式完成,await让出线程,阻塞asnyc关键字的函数的内部代码,先去执行async关键字函数的外部代码。等外部的执行完之后才会执行内部的。

以下这个图是在看解析的时候,觉得写得很清晰的图(来源于网路,不是原创!)
彻底搞懂js事件循环机制/js异步编程_第3张图片
① 先执行async 关键字函数的外部代码。前两个async1()/async2()是正常的函数声明,往下看执行console.log(‘script start’),输出script start;

②执行setTimeout,setTimeout是一个异步任务,放入宏任务的异步队列中。等时间到了,主线程空闲才会去调用。

③执行async1(),输出async1 start 继续向下执行。

④执行async2(),输出async2,并且返回一个promise对象,await让出线程,把返回的promise放入微任务异步队列,并且阻塞async内部代码的执行,即console.log(‘async1 end’),且async1()后面的代码要等待上面完成。

⑤执行new Promise,输出promise1,然后将resolve()放入微任务的异步队列。

⑥执行 console.log(‘script end’),输出script end。

⑦到这里同步代码全部执行完成,接着去异步任务队列获取任务,先微任务Event Queue再宏任务Event Queue

⑧执行resolve,这个resolve是async2返回的promise返回的,输出async1 end

⑨执行resolve,这个resolve是new Promise的,输出promise2

⑩微任务异步队列执行完,执行宏任务异步队列的setTimeout,输出setTimeOut

五、js异步编程的解决方案的发展和优缺点

1、回调函数(callback)

优点:解决了同步的缺点,只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行顺序

缺点:回调地狱、不能用try catch捕获错误,不能return

2、Promise

优点:解决回调地狱的问题

缺点:无法取消Promise,错误需要通过回调函数捕获

3、Generator/ yield

特点:可以控制函数的执行,可以配合 co 函数库使用

4、Async/await

优点:代码清晰,不用像 Promise 写一大堆 then 链,处理了回调地狱的问题

缺点:await 将异步代码改造成同步代码,如果多个异步操作没有依赖性而使用 await 会导致性能上的降低。

js异步编程进化史

callback -> promise -> generator -> async + await

六、异步编程实现方法

1、回调函数callback
ajax(url, () => {
    // 处理逻辑
})

如果多个请求存在依赖性,就会面临回调地狱,各个部分之间高度耦合,使程序结构混乱,难以追踪(多个函数嵌套,套娃)每个任务只能返回一个回调函数。不能通过try catch捕获错误,不能return。

ajax(url, () => {
    // 处理逻辑
    ajax(url1, () => {
        // 处理逻辑
        ajax(url2, () => {
            // 处理逻辑
        })
    })
})
(1)定时器setTimeout

优点:去耦合、利于实现模块化。可以绑定多个事件,每个事件可以有多个回调函数

缺点:整个程序变成事件驱动型,运行流程不清晰。

//当f1发生done事件,就执行f2
f1.on('done', f2);
//改写
function f1() {
  setTimeout(function () {
    // ...
    f1.trigger('done');
  }, 1000);
}

解析:setTimeout()第一个参数是一个函数,第二个参数是以毫秒为单位的时间间隔,setTimeout()是注册回调函数的函数,也可以表示在什么异步条件下调用回调函数,setTimeout()方法只会调用一次回调函数。

(2)事件监听
//给目标 DOM 绑定一个监听函数,用addEventListener
document.getElementById('#test').addEventListener('click', (e) => {
  console.log('click')
}, false);

解析:通过id给 test 的一个元素绑定点击事件,任务的执行时机推迟到当点击这个动作。addEventListener`注册了回调函数,这个方法的第一个参数是一个字符串,指定要注册的事件类型,如果用户点击了指定的元素,浏览器就会调用回调函数,并给他传入一个对象,其中包含着事件的详细信息。异步任务的执行不取决于代码的顺序,而取决于某个事件是否发生

(3)网络请求

网络请求是一种典型的异步操作

const SERVER_URL = "/server";
let xhr = new XMLHttpRequest();
// 创建 Http 请求
xhr.open("GET", SERVER_URL, true);
// 设置状态监听函数
xhr.onreadystatechange = function() {
  if (this.readyState !== 4) return;
  // 当请求成功时
  if (this.status === 200) {
    handle(this.response);
  } else {
    console.error(this.statusText);
  }
};
// 设置请求失败时的监听函数
xhr.onerror = function() {
  console.error(this.statusText);
};
// 发送 Http 请求
xhr.send(null);

(4)Node的回调与事件

nodejs服务端在js环境底层就是异步的,定义了很多回调和事件的API

//读取文件的API是异步的,读取文件内容之后调用一个回调函数
const fs = require('fs');
let options = {}

//  读取配置文件,调用回调函数
fs.readFile('config.json', 'utf8', (err, data) => {
    if(err) {
      throw err;
    }else{
    	Object.assign(options, JSON.parse(data))
    }
		startProgram(options)
});

解析:fs.readFile()方法以接收两个参数的回调作为最后一个参数。它会异步读取指定文件,如果读取成功就会将第二个参数传递给回调的第二个参数,如果发生错误,就会将错误传递给回调的第一个参数。

2、Promise
(1)Promise的概念

Promise是一个对象,表示异步操作的结果。Promise可以让多层嵌套回调以一种更线性的链式形式表达出来。

(2)Promise的三种状态
  • Pending----Promise对象实例创建时候的初始状态
  • Fulfilled----可以理解为成功的状态
  • Rejected----可以理解为失败的状态

彻底搞懂js事件循环机制/js异步编程_第4张图片

(3)Promise实例有两个过程
  • pending -> fulfilled : Resolved(已完成)
  • pending -> rejected:Rejected(已拒绝)
(4)Promise特点
  • 一旦状态变为 resolved 后,就不能 再次改变为Fulfilled
  • 如果不设置回调函数,Promise内部抛出的错误,不会反映到外部
  • Promise 处理的问题都是“一次性”的,因为一个 Promise 实例只能 resolve 或 reject 一次
(5)Promise方法
  • then()

then方法接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为resolved时调用,第二个回调函数是Promise对象的状态变为rejected时调用。其中第二个参数可以省略。then方法返回的是一个新的Promise实例。因此可以采用链式写法。

promise.then(function(value) {
  // success
}, function(error) {
  // failure
});
  • catch()

Promise对象的catch方法指向reject的回调函数。catch方法还有一个作用,就是在执行resolve回调函数时,如果出现错误,抛出异常,不会停止运行,而是进入catch方法中。

p.then((data) => {
     console.log('resolved',data);
},(err) => {
     console.log('rejected',err);
}); 
  • all()

调用all方法时的结果成功的时候是回调函数的参数是一个数组,这个数组按顺序保存着每一个promise对象resolve执行时的值

let promise1 = new Promise((resolve,reject)=>{
	setTimeout(()=>{
       resolve(1);
	},2000)
});
let promise2 = new Promise((resolve,reject)=>{
	setTimeout(()=>{
       resolve(2);
	},1000)
});
let promise3 = new Promise((resolve,reject)=>{
	setTimeout(()=>{
       resolve(3);
	},3000)
});

Promise.all([promise1,promise2,promise3]).then(res=>{
    console.log(res);  
})

彻底搞懂js事件循环机制/js异步编程_第5张图片

  • race()

race方法和all一样,接受的参数是一个每项都是promise的数组,但与all不同的是,当最先执行完的事件执行完之后,就直接返回该promise对象的值。当需要执行一个任务,超过多长时间就不做了,就可以用这个方法来解决。

let promise1 = new Promise((resolve,reject) => {
	setTimeout(() =>  {
       reject(1);
	},2000)
});
let promise2 = new Promise((resolve,reject) => {
	setTimeout(() => {
       resolve(2);
	},1000)
});
let promise3 = new Promise((resolve,reject) => {
	setTimeout(() => {
       resolve(3);
	},3000)
});
Promise.race([promise1,promise2,promise3]).then(res => {
	console.log(res); 
},rej => {
    console.log(rej)});

彻底搞懂js事件循环机制/js异步编程_第6张图片

  • finally()

finally方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的

promise.then(result => {···})
			 .catch(error => {···})
       .finally(() => {···});
(6) promise链式调用
  • 每次调用返回的都是一个新的Promise实例(这就是then可用链式调用的原因)
  • 如果then中返回的是一个结果的话会把这个结果传递下一次then中的成功回调
  • 如果then中出现异常,会走下一个then的失败回调
  • 在 then中使用了return,那么 return 的值会被Promise.resolve() 包装
  • then中可以不传递参数,如果不传递会透到下一个then中

链式操作:

new Promise(resolve => {
    resolve(1);
})
    .then(result => console.log(result)) //1
    .then(result => {
        console.log(result);              //undefined
        return 2;
    })
    .then(result => {
        console.log(result);             //2
        throw new Error("err");
    })
    .then((result) =>{
        console.log(result);            
    }, (err)=>{
        console.log(err);                //Error: err
        return 3;
    })
    .then((result) => {
        console.log(result);            //3
    })

(7)promise异常处理

Promise.prototype.catch,用于指定发生错误时的回调函数,返回一个新的promise对象。Promise对象后面要跟catch方法,这样可以处理Promise内部发生的错误。catch方法返回的还是一个Promise对象,因此后面还可以接着调用then方法。Promise对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。

new Promise(() => {
    throw new Error('err1');
})
    .then(() => {console.log(1);})
    .then(() => {console.log(2);})
    .catch((err) => {
        console.log(err); //Err: err1
        throw  new Error('err2');
    })
    .catch((err) => {console.log(err);})//Err: err2

异常处理还有一个好处就是很好解决地狱回调问题,但是无法取消promise,错误要通过回调函数捕获。

//地狱回调
ajax(url, () => {
    // 处理逻辑
    ajax(url1, () => {
        // 处理逻辑
        ajax(url2, () => {
            // 处理逻辑
        })
    })
})
//promise解决地狱回调
ajax(url)
  .then(res => {
      console.log(res)
      return ajax(url1)
  }).then(res => {
      console.log(res)
      return ajax(url2)
  }).then(res => console.log(res))
3、Generator/yield
(1)什么是Generator

Generator生成器函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同,严格意义上说,Generator不是函数,是一个带星函数。

(2)有什么特点/注意事项

最大的特点就是可以控制函数的执行,通过yield关键字控制函数的暂时或next控制函数的执行

每次返回的是yield后的表达式结果,yield表达式本身没有返回值,或者说总是返回undefined

Generator函数可以理解为一个封装机,封装很多内部状态

(3)Generator
    function* test(x) {
        let y = 2 * (yield(x + 1))
        let z = yield(y / 3)
        return (x + y + z)
    }
    let it = test(5)
    console.log(it.next())
    console.log(it.next(12))
    console.log(it.next(13))

彻底搞懂js事件循环机制/js异步编程_第7张图片

代码分析

①Generator不是普通的函数,甚至都不是函数,返回的是一个迭代器

②执行第7行next时,传入的参数停留到第2行yield,返回5+1 = 6

③执行第8行next时,传入的12参数会被当成上一个表达式(第2行)的返回值,如果不传入参数,返回的是undefined,此时第2行y=2*12=24,第3行 z = 24/3=8

④执行第9行next时,传入的13参数被当成上一个表达式(第3行)的返回值,如果不传入参数,返回的是undefined,此时第4行retrun x=5,z=13,y=24,return42

(4)依赖联系紧密,Generator函数绕逻辑

依赖紧密,代码就会冗长且不容易看通逻辑

//3个本地文件 1.txt 2.txt 3.txt
let fs = require('fs')
function read(file) {
  return new Promise(function(resolve, reject) {
    fs.readFile(file, 'utf8', function(err, data) {
      if (err) reject(err)
      resolve(data)
    })
  })
}
function* r() {
  let r1 = yield read('./1.txt')
  let r2 = yield read(r1)
  let r3 = yield read(r2)
  console.log(r1)
  console.log(r2)
  console.log(r3)
}
let it = r()
let { value, done } = it.next()
value.then(function(data) { // value是个promise
  console.log(data) //data=>2.txt
  let { value, done } = it.next(data)
  value.then(function(data) {
    console.log(data) //data=>3.txt
    let { value, done } = it.next(data)
    value.then(function(data) {
      console.log(data) //data=>结束
    })
  })
})

解决方案:配合co库(一个nodejs和浏览器打造的基于生成器的流程控制工具)

function* r() {
  let r1 = yield read('./1.txt')
  let r2 = yield read(r1)
  let r3 = yield read(r2)
  console.log(r1)
  console.log(r2)
  console.log(r3)
}
let co = require('co')
co(r()).then(function(data) {
  console.log(data)
})
// 2.txt=>3.txt=>结束=>undefined
//解决地狱回调
function *fetch() {
    yield ajax(url, () => {})
    yield ajax(url1, () => {})
    yield ajax(url2, () => {})
}
let it = fetch()
let result1 = it.next()
let result2 = it.next()
let result3 = it.next()
4、Async/Await并发请求
(1)什么是async/await

可以理解为Generator+co,可以完成这两个工作。是ES7新增了两个关键字: async和await。是 Generator 的语法糖,它能实现的效果都能用then链来实现,它是为优化then链而开发出来的。

(2)async/await有什么特点

①基于Promise实现的,它不能用于普通的回调函数

②Promise一样,是非阻塞的

③异步代码看起来像同步代码

(3)如何理解

async是“异步”的简写,await则为等待。async 用来声明异步函数,这个关键字可以用在函数声明、函数表达式、箭头函数和方法上,异步主要是一些不会马上完成的任务,需要控制它的暂停和执行。await关键字就是暂停异步代码的执行,等待Promise解决。

(4)await

await究竟在等待什么?await 是在等待一个 async 函数完成,一个表达式的值 Promise 对象或其它值,是一个返回值。

①等promise对象:await会阻塞后面的代码,等promise对象resolve,得到的值作为表达式的值

②等的不是promise对象:await等待什么就返回什么

(5)Async/Await并发请求
   let fs = require('fs')
   function read(file) {
     return new Promise(function(resolve, reject) {
       fs.readFile(file, 'utf8', function(err, data) {
         if (err) reject(err)
         resolve(data)
       })
     })
   }
   function readAll() {
     read1()
     read2()//这个函数同步执行
   }
   async function read1() {
     let r = await read('1.txt','utf8')
     console.log(r)
   }
   async function read2() {
     let r = await read('2.txt','utf8')
     console.log(r)
   }
   readAll() // 2.txt 3.txt
(6)使用方法
//async 关键字声明一个异步函数
async function httpRequest() {
}
//加上await关键字 会转化为一个返回值或者抛出一个异常
async function httpRequest() {
  let res1 = await httpPromise(url1)
  console.log(res1)
}
(7)返回什么
async function testAsy(){
   return 'hello world';
}
let result = testAsy(); 
console.log(result)

彻底搞懂js事件循环机制/js异步编程_第8张图片

代码解析

async 函数返回的是 Promise 对象。如果异步函数使用return关键字返回了值(如果没有return则会返回undefined),这个值则会被 Promise.resolve() 包装成 Promise 对象。异步函数始终返回Promise对象。

function getSomething() {
    return "something";
}
async function testAsync() {
    return Promise.resolve("hello async");
}
async function test() {
    const v1 = await getSomething();
    const v2 = await testAsync();
    console.log(v1, v2);
}
test();

彻底搞懂js事件循环机制/js异步编程_第9张图片

(8)async/await异常处理
const exe = (flag) => () => new Promise((resolve, reject) => {
    console.log(flag);
    setTimeout(() => {
        flag ? resolve("yes") : reject("no");
    }, 1000);
});
const run = async () => {
	try {
		await exe(false)();
		await exe(true)();
	} catch (e) {
		console.log(e);
	}
}
run();

彻底搞懂js事件循环机制/js异步编程_第10张图片

代码解析

异步run()方法,await后面需要跟promise对象,因此通过额外的一个方法调用()把原来的exe方法内部的Thunk包装拆掉,即执行 exe(false)() 或 exe(true)() 返回的就是 Promise 对象。在 try 块之后,使用 catch 来捕捉。

(9)async/await异步编程终极解决方案

①处理 then 的调用链,简介清晰写出代码,看起来像同步代码,同时优雅解决回调地狱问题

②内置执行器。Generator函数执行必须要执行器(co函数库因此而出现),async函数自带执行器。

③广泛的实用性。co 函数库约定,yield 命令后面只能是 Thunk 函数或 Promise 对象,而 async 函数的 await 命令后面,可以跟 Promise 对象和原始类型的值。

④更好的语义。

七、异步编程的比较

(1)promise和 async await 区别

①Promise的出现解决了传统callback函数导致的“地域回调”问题,形成回调链,在复杂的开发环境中语法会显得不美观。async /await代码看起来会简洁些,使得异步代码看起来像同步代码。

②async/await与Promise一样,是非阻塞的

③async/await是基于Promise实现的,理解为进阶的Promise,它不能用于普通的回调函数

(2)defer和async区别

执行的时间的区别。执行时间,defer会在文档解析完之后执行,并且多个defer会按照顺序执行,而async则是在js加载好之后就会执行,并且多个async,哪个加载好就执行哪个。

①在没有defer或者async的情况下:会立即执行脚本,所以通常建议把script放在body最后

<script src="script.js"></script>

②async:有async的话,加载和渲染后续文档元素的过程将和 script.js 的加载与执行并行进行(异步)。
但是多个js文件的加载顺序不会按照书写顺序进行。

<script async src="script.js"></script>

③derer:有derer的话,加载后续文档元素的过程将和 script.js 的加载并行进行(异步),但是 script.js 的执行要在所有元素解析完成之后,DOMContentLoaded 事件触发之前完成,并且多个defer会按照顺序进行加载。

<script defer src="script.js"></script>

promise和 async await 区别

①Promise的出现解决了传统callback函数导致的“地域回调”问题,形成回调链,在复杂的开发环境中语法会显得不美观。async /await代码看起来会简洁些,使得异步代码看起来像同步代码。

②async/await与Promise一样,是非阻塞的

③async/await是基于Promise实现的,理解为进阶的Promise,它不能用于普通的回调函数

(2)defer和async区别

执行的时间的区别。执行时间,defer会在文档解析完之后执行,并且多个defer会按照顺序执行,而async则是在js加载好之后就会执行,并且多个async,哪个加载好就执行哪个。

①在没有defer或者async的情况下:会立即执行脚本,所以通常建议把script放在body最后

<script src="script.js"></script>

②async:有async的话,加载和渲染后续文档元素的过程将和 script.js 的加载与执行并行进行(异步)。
但是多个js文件的加载顺序不会按照书写顺序进行。

<script async src="script.js"></script>

③derer:有derer的话,加载后续文档元素的过程将和 script.js 的加载并行进行(异步),但是 script.js 的执行要在所有元素解析完成之后,DOMContentLoaded 事件触发之前完成,并且多个defer会按照顺序进行加载。

<script defer src="script.js"></script>

你可能感兴趣的:(前端js深入理解,javascript,前端)