JS异步与单线程、Date、Math

单线程和异步

  • 单线程一次只能做一件事情,js就是单线程的。
  • 异步为了让单线程不阻塞,就出现了异步
    • js是单线程的,但是遇到等待(网络请求/定时任务)的时候不能卡住,此时就需要异步。
    • 异步通常是基于回调函数的形式,当然还有其他的方法实现异步,比如Promise
  • JS是单线程语言,只能同时做一件事。
  • 浏览器和nodejs已支持JS启动进程,如Web Worker
  • JS和DOM渲染共用同一个线程,因为JS可修改DOM结构

异步与同步

  • 何时需要异步:
    • 同步异步的区别就是会不会阻塞后面程序的运行,那么我们需要后面程序不被阻塞的时候就要使用异步
    • 在可能发生等待的时候,等待过程中不能像alert一样阻塞程序运行,因此,所以的“等待的情况”都需要异步
  • 异步
    • 例子: setTimeout是异步,依次打印100 300 200,异步是不会阻塞后面程序的进行的。setTimeout是异步
      • 注意:就算setTimeout没有设置时间,但执行栈中还是先执行同步任务,setTimeout会被拿出去放在任务队列中,等同步任务执行完毕才执行他,这和他需不需要等待没关系。
      • 执行第一行,打印100
      • 执行setTimeout后,传入setTimeout的函数会被暂存到任务队列,不会立即执行(单线程的特点,不能同时干两件事)
      • 执行最后一行,打印300
      • 待所有程序执行完,处于空闲状态时,会立马看有没有在任务队列暂存起来的要执行。
      • 发现暂存起来的setTimeout中的函数无需等待时间,就立即来过来执行
  • 同步
    • 例子: alert是同步,打印100 200后需要等待用户点击确认,如不点击将一直卡在200,永远不会打印300。同步是会阻塞后面程序的进行。同步例子

前端使用异步的场景

  • 定时任务“setTimeout()与setInterval()”
  • 网络请求:ajax请求,动态<img>加载(即src赋值的过程是异步的)
  • 事件绑定:事件函数绑定以后并不会等触发事件函数才继续执行程序,程序照常执行,等需要触发的时候才会执行事件绑定函数。

ajax请求的例子

可参考ajax请求完整笔记
ajax请求代码示例
打印“start”、“end”,然后等./data1.json中的数据请求成功后执行函数-打印获取到的数据data1(关于$.get()可参考笔记

动态<img>加载的例子

动态`<img>`加载的例子
关于createElement()可以参考笔记“JS中修改/创建/移动/删除 HTML DOM元素”
onload事件就是callback的一种形式

执行顺序:
打印“start”=》创建一个图片元素img可参考笔记)=》给img的src赋值,src 属性值更新时浏览器会自动加载并显示出新图像。(可参考笔记) =》图片加载过程中打印“end” =》 等img加载完成才会执行onload绑定的函数,打印loaded可参考笔记

很明显src赋值的过程(图片动态加载)是异步的,不会阻塞end的打印


事件绑定的例子

事件绑定的例子
addEventListener()相关笔记
或者通过object.onclick=function(){SomeJavaScriptCode};绑定onclick事件也是一样的。


可用作JS异步编程的方法

  • 回调函数,这是异步编程最基本的方法。上面提到的定时器、ajax请求、图片的动态加载、事件绑定 实际上都是回调函数的形式。
    • 例:假定有两个函数f1和f2,后者等待前者的执行结果,如果f1是一个很耗时的任务,可以考虑改写f1,把f2写成f1的回调函数。【此时f2是异步的,她耗费的时间并不影响同步任务的执行】
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      function f2() {
      console.log("f2");
      }
      console.log("start");
      function f1(callback) {
      console.log("f1");
      setTimeout(function () {
      callback();
      }, 1000);
      }
      f1(f2);
      console.log("end");
      // start f1 end f2
  • Promises对象,Promises 对象是CommonJS 工作组提出的一种规范,目的是为异步编程提供统一接口。
    • 简单说,它的思想是,每一个异步任务返回一个Promise对象,该对象有一个then方法,允许指定回调函数。回调函数(回调地狱)变成了链式写法,程序的流程可以看得很清楚,而且有一整套的配套方法,可以实现许多强大的功能。Promise的例子
  • async,async函数返回的是一个 Promise 对象,可以使用 then 方法添加回调函数,async 函数内部 return 语句返回的值,会成为 then 方法回调函数的参数。当函数执行的时候,一旦遇到await就会先执行await的相关异步操作,等到异步操作完成,再接着执行函数体内后面的语句(包括同步任务也先等着await执行)。
    • 比起Promise,await则不需要then,Promise与async对比的例子
  • 事件监听,任务的执行不取决于代码的顺序,而取决于某个事件是否发生。
    • 例:为f1绑定一个事件,当f1发生done事件,就执行f2。
  • 发布/订阅,与”事件监听”类似。
    • 我们假定,存在一个”信号中心”,某个任务执行完成,就向信号中心”发布”(publish)一个信号,其他任务可以向信号中心”订阅”(subscribe)这个信号,从而知道什么时候自己可以开始执行。这就叫做”发布/订阅模式”(publish-subscribe pattern),又称”观察者模式”(observer pattern)。
    • 这种方法的性质与”事件监听”类似,但是明显优于后者。因为我们可以通过查看”消息中心”,了解存在多少信号、每个信号有多少订阅者,从而监控程序的运行。

Promise

  • Promise的出现解决了回调地狱的问题
    • callback hell的例子层层嵌套的方式不符合我们的思维顺序
  • Promise的例子

事件循环(JS的执行机制)

可参考笔记从输入url到页面显示都经历了什么
注意:异步任务被挂起时会由浏览器的引擎去执行他们,而同步任务则是由JS的引擎去执行。


相关题目

同步和异步的区别,分别举例子

  • 异步是基于JS是单线程语言而出现的
  • 同步任务会阻塞代码执行,而异步不会
  • alert是同步,setTimeout是异步

手写用Promise加载一张图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 加载一张图片
function loadImg(src) {
const p = new Promise(
(resolve, reject) => {
const img = document.createElement('img')
img.onload = () => {
resolve(img)
}
img.onerror = () => {
const err = new Error(`图片加载失败 ${src}`)
reject(err)
}
img.src = src
}
)
return p
}

const url = 'https://img.mukewang.com/5a9fc8070001a82402060220-140-140.jpg'
loadImg(url).then(img => {
console.log(img.width)
return img
}).then(img => {
console.log(img.height)
}).catch(ex => console.error(ex))

Promise加载一张图片的结果

Promise加载2张图片(解决回调地域)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 加载2张图片,即使用Promise解决回调地域
function loadImg(src) {
const p = new Promise(
(resolve, reject) => {
const img = document.createElement('img')
img.onload = () => {
resolve(img)
}
img.onerror = () => {
const err = new Error(`图片加载失败 ${src}`)
reject(err)
}
img.src = src
}
)
return p
}

const url1 = 'https://img.mukewang.com/5a9fc8070001a82402060220-140-140.jpg'
const url2 = 'https://img3.mukewang.com/5a9fc8070001a82402060220-100-100.jpg'

loadImg(url1).then(img1 => {
console.log(img1.width)
return img1 // 普通对象
}).then(img1 => {
console.log(img1.height)
return loadImg(url2) // promise 实例
}).then(img2 => {
console.log(img2.width)
return img2
}).then(img2 => {
console.log(img2.height)
}).catch(ex => console.error(ex))

Promise加载2张图片的结果

关于setTimeout的笔试题

题目
连续打印1 3 5 2
一秒后打印4

1
2
3
4
5
6
7
8
9
10
console.log(1);
setTimeout(function () {
console.log(2)
}, 2000)
console.log(3);
setTimeout(function () {
console.log(4)
}, 1000)
console.log(5);
// 连续打印1 3 5 ,1秒后4 ,1秒后2

注意所有的同步任务在主线程上执行,形成一个执行栈。异步任务有了 运行结果 才会在任务队列中放置一个事件。脚本运行时先依次运行执行栈中的所有同步任务,然后会从任务队列里提取事件,选择需要首先执行的异步任务然后执行。

4先有结果,所以4先出现在任务队列了

补充一个微任务、宏任务的题目

前端使用异步的场景有哪些

  • 定时任务:setTimeout,setInverval(可参考笔记“setTimeout()与setInterval()”
  • 网络请求:ajax请求,动态<img>加载
  • 事件绑定:事件函数绑定以后并不会等触发事件函数才继续执行程序,程序照常执行,等需要触发的时候才会执行事件绑定函数。

实现异步编程的方法

  • 回调函数
  • Promise
  • 事件绑定
  • 监听/订阅

Date

Date是日期的构造函数
Date各种API
可参考笔记Date对象

Math.random()

  • Math的其他方法可参考笔记Math对象
  • 在前端,Math.random()一般用于清除缓存

在项目中,有时改完js文件之后怎么也不能生效,只有清除浏览器缓存或者CTRL+F5强制刷新之后才能出现最新的版本,这样调试起来十分的不方便,此时可以在请求地址之后加上:

1
"?ran=" + Math.random(); //当然这里参数 ran可以任意取了

采用随机数的方式,使每一次的请求都是一个新请求,从而防止浏览器从缓存中读取旧版本
注意:因为Math.random() 只能在Javascript 下起作用,故只能通过Javascript的调用才可以

除了加入随机数还可以加入当前时间

1
"?time=" + new Date().getTime();

同样可以使每次请求的地址都是不一样的,从而防止浏览器使用缓存。


Date、Math相关题目

Date、Math、数组API、对象API 的相关题目。
数组API、对象API可以理解为数组、对象他们自带的那些方法

获取2017-06-10格式的日期

获取2017-06-10格式的日期

  • 1-18 函数 formatDate 接受一个Date对象,返回一个格式化后的日期
    • 2-4 为防止报错,如果没有传入参数,就新建一个Date对象作为参数dt
    • 5 getFullYear()getFullYear()获取年份获取年份
    • 6 getMonth()获取月份,由于获取到的是0-11,所以需要+1
    • 7 getDate()获取日期
    • 8-15 由于我们需要格式化的日期是两位数的,所以当month<10的时候我们需要在前面添加0。日期也是同理。
      • 这里涉及到之前讲的字符串拼接,字符串"0"和数字month拼接后相当于**强制类型转换**,month将成为字符串。
    • 17 通过-连接,将数字拼接字符串 得到 字符串
  • 19 新建一个Date对象dt
  • 20 将dt传入函数formatDate,得到格式化后的日期formatDate
  • 21 打印格式化后的日期formatDate

获取长度一致的字符串随机数

题目:获取随机数,要求是长度一致的字符串格式

注意:字符串也有slice(),和数组的slice()类似,用于截取字符串
获取长度一致的字符串格式的随机数
1 获取随机数random,小数部分长度不确定
2 随机数后加上10个0,保证截取的时候有10位(保证不报错)。数字拼接字符串后等于强制类型转换,random 变为字符串
3 使用字符串的slice()截取random的前10位(注意:10不在截取范围)


遍历对象和数组的通用forEach函数

题目:写一个能遍历对象和数组的通用forEach函数
(jquery中有现成的方法,数组本身就有forEach()
能遍历对象和数组的通用forEach函数

  • 1-14 定义forEach函数
    • 参数1:对象/数组。参数2:函数。
    • 注意:我们这里定义的forEach函数的index和item的顺序和数组自己的forEach()是反的
    • 3-7 判断传入的是对象/数组,如果是数组,就数组自己的forEach(),遍历数组后将index和item传入 参数2(fn函数),通过fn函数将index与item的位置调换一下后打印
    • 8-13 如果是对象,就通过for in遍历对象属性,并将 属性名称key 和 属性值obj[key]传给 参数2(fn函数),被调用后会被打印。
      • 注意:in运算符在 数组 中是判断有无 数组下标 的,不是数组元素。在 对象 中判断有无 属性。返回值是boolean。
  • 15 定义一个数组arr
  • 17-19 调用我们定义的forEach函数,传入参数1为数组arr,参数2为一个函数,该函数接收两个参数并打印他们。
    • 注意:这里的index和item的顺序和数组的forEach()是反的,主要是为了照顾后面对象的keyvalue
  • 21 定义一个对象obj
  • 22-24 让对象obj调用我们定义的forEach函数
    • key:属性名称。value:属性值。