js中的单线程和异步编程的解决方案

一.进程和线程

1.进程
  • 是cpu资源分配的最小单位,是能拥有资源和独立运行的最小单位。
  • 拥有独立的地址空间。
2.线程
  • 是cpu调度的最小单位,安排CPU执行的最小单位。
  • 同一个进程下的所有线程,共享进程的地址空间。

(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)

cpu:“中央处理器(CPU,Central Processing Unit)是一块超大规模的集成电路,是一台计算机的运算核心和控制核心,它的功能主要是解释计算机指令以及处理计算机软件中的数据,计算机的性能在很大程度上由CPU的性能决定。”

3.关系
  • 一个进程至少有一个线程(主)
  • 一个进程中也可以同时运行多个线程, 我们会说程序是多线程运行的
  • 一个进程内的数据可以供其中的多个线程直接共享
  • 多个进程之间的数据是不能直接共享的
  • 程序是在某个进程中的某个线程执行的(即真正在处理机上运行的是线程)
  • 线程是在进程内部工作,而进程负责向外界输出

image.png

二.js中的单线程

1.JS为什么是单线程的

这与浏览器的用途有关,JS的主要用途是与用户互动和操作DOM。设想一段JS代码,分发到两个并行互不相关的线程上运行,一个线程在DOM上添加内容,另一个线程在删除DOM,那么会发生什么?以哪个为准?所以为了避免复杂性,JS从一开始就是单线程。

JS的语法单线程

一个语法的问题需要注意,就是JS的语法层面是单线程的,就是不能同时执行

console.log("A AT ", (new Date()).toLocaleTimeString())
alert('a')
console.log("B BT ", (new Date()).toLocaleTimeString())

js的单向数据流,只能自上而下执行,如果上一行解析时间很长,那么下面的代码就会被阻塞。对于用户而言,阻塞就意味着"卡死",这样就导致了很差的用户体验

2.JS的同步异步

由于Js的执行机制**(只要主线程空了,就会去读取任务队列)**导致的,js中所有任务可以分成两种

  • 同步任务:指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;

  • 异步任务:指的是,不进入主线程、而进入"任务队列"的任务,只有等主线程任务执行完毕,"任务队列"开始通知主线程,请求执行任务,该任务才会进入主线程执行。

同步代码

console.log("A")
console.log("B")
console.log("C")

代码的执行结果,是 A,B,C。这样,就是同步代码,执行顺序与编写顺序保持一致。

对比下面的异步代码:

console.log("A")
// setTimeout 就是异步代码
setTimeout(()=>{
  console.log("B")
}, 0)
console.log("C")

代码的执行结果,是 A,C,B。编写顺序上,我们先输出的B,但是执行结果却是先输出的C。本例中,setTimeout 函数就是异步执行,也就是当执行的到 setTimeout 时,内部的代码不会立即执行,而是将其放在异步执行队列中等待执行。同时 setTimeout 后边的代码就开始执行,也就是说输出C,没有等到输出B执行完,就开始执行了。

本例中,setTimeout 函数就是异步执行。

console.log("A AT ", (new Date()).toLocaleTimeString())

// setTimeout 异步,1000ms(1s)后执行输出B和时间
setTimeout(()=>{
  console.log("B ", (new Date()).toLocaleTimeString())
}, 1000)

// 大循环,循环很多次,为了保证循环时间大于1000ms
for (let i = 0, num=999999999; i <= num; ++ i) {
  if (0 == i) {
    console.log("Loop first Run at ", (new Date()).toLocaleTimeString())
  }
  if (num == i) {
    console.log("Loop last Run at ", (new Date()).toLocaleTimeString()) 
  }
}

结果:
A AT  21:57:58
Loop first Run at  21:57:58
Loop last Run at  21:58:01
B  21:58:01

上面的例子中,我们做了一个定时器,异步执行在1000ms后,之后执行一个for循环,很多次循环,执行时间大于了1000ms。从执行结果上看,当达到了1000ms时,并没有立即执行输出B,而是要等到for循环执行完毕后,才会执行已经到达时间的输出B。

异步执行还有一个问题,就是若某些操作需要依赖于异步操作的结果。那如何保证这些操作的执行时机呢? JS提供了多种语法方案供我们使用,例如:事件驱动,Promise,Generator,async,await等

4.异步编程的解决方案
4.1 回调函数
  • 把一个函数当参数,传递给另一个函数

回调函数是异步操作最基本的方法

ajax(url, () => {
    // 处理逻辑
})

但是回调函数有一个致命的弱点,就是容易写出回调地狱(Callback hell)。假设多个请求存在依赖性,你可能就会写出如下代码

ajax(url, () => {
    // 处理逻辑
    ajax(url1, () => {
        // 处理逻辑
        ajax(url2, () => {
            // 处理逻辑
        })
    })
})

优点:简单、容易理解和实现

缺点:不利于代码的阅读和维护,各个部分之间高度耦合,使得程序结构混乱、流程难以追踪(尤其是多个回调函数嵌套的情况)

4.2 事件监听
  • 异步任务的执行不取决于代码的顺序,而取决于某个事件是否发生

下面是两个函数f1和f2,编程的意图是f2必须等到f1执行完成,才能执行。首先,为f1绑定一个事件(这里采用的jQuery的写法)

f1.on('done', f2);
function f1() {
  setTimeout(function () {
    // ...
    f1.trigger('done');
  }, 1000);
}

上面代码中,f1.trigger(‘done’)表示,执行完成后,立即触发done事件,从而开始执行f2。

优点:比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以"去耦合",有利于实现模块化。

缺点:整个程序都要变成事件驱动型,运行流程会变得很不清晰。阅读代码的时候,很难看出主流程。

4.3 发布订阅
  • 多个线程互相协作,完成异步任务。

我们假定,存在一个"信号中心",某个任务执行完成,就向信号中心"发布"(publish)一个信号,其他任务可以向信号中心"订阅"(subscribe)这个信号,从而知道什么时候自己可以开始执行。这就叫做"发布/订阅模式"(publish-subscribe pattern)

首先,f2向信号中心jQuery订阅done信号。

jQuery.subscribe('done', f2);

然后,f1进行如下改写

function f1() {
  setTimeout(function () {
    // ...
    jQuery.publish('done');
  }, 1000);
}

上面代码中,jQuery.publish(‘done’)的意思是,f1执行完成后,向信号中心jQuery发布done信号,从而引发f2的执行。

f2完成执行后,可以取消订阅(unsubscribe)

jQuery.unsubscribe('done', f2);

这种方法的性质与“事件监听”类似,但是明显优于后者。因为可以通过查看“消息中心”,了解存在多少信号、每个信号有多少订阅者,从而监控程序的运行。

4.4 Promises对象
  • 是es6新增的一种新的写法,用来解决“回调地狱”的问题

两个特点:

​ - 对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:pending(进行中)、resolve(fulfilled已成功)和rejected(已失败)

​ - 一旦状态设定,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就凝固了.

image.png

new Promise((resolve,reject) => {
  setTimeout(() => {
    resolve('hello')
  }, 2000)
}).then(res => {
  console.log(res)
})

new Promise((resolve,reject)=>{
  var a = 1;
  if(a == 1){
    reject('错误')
  }
}).catch(res => {
  console.log(res)
})

ajax(url)
  .then(res => {
      console.log(res)
      return ajax(url1)
  }).then(res => {
      console.log(res)
      return ajax(url2)
  }).then(res => console.log(res))

优点:让回调函数变成了规范的链式写法,程序流程可以看的很清楚。他有一整套接口,可以实现许多强大的功能,比如同时执行多个异步操作,等到他们的状态都改变以后,在执行一个回调函数。

缺点:编写的难度比传统写法高,而且阅读代码也不是一眼可以看懂。你只会看到一堆then,必须自己在then的回调函数里面理清逻辑。

4.5 生成器Generators

Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同,Generator 最大的特点就是可以控制函数的执行。

特征:

(1)function 关键字和函数之间有一个星号(*),且内部使用yield(返回数据)表达式,定义不同的内部状态

(2)调用Generator函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象

function *gen() {
    for(let i = 0; i < 9999; i++){console.log(yield i)};
    yield 10;
    yield 20;
    return 30;
}


const it = gen();

console.log(it.next()); // { value: 10, done: false }
console.log(it.next()); // { value: 20, done: false }
console.log(it.next()); // { value: 30, done: true }
console.log(it.next()); // { value: undefined, done: true }
console.log(10)
console.log(it.next()); // { value: undefined, done: true }

换言之,next 方法的作用是分阶段执行 Generator 函数。每次调用 next 方法,会返回一个对象,表示当前阶段的信息( value 属性和 done 属性)。value 属性是 yield 语句后面表达式的值,表示当前阶段的值;done 属性是一个布尔值,表示 Generator 函数是否执行完毕,即是否还有下一个阶段(done为false 继续执行)

普通函数的执行会形成一个调用栈,入栈和出栈是一口气完成的。而Generator必须得手动调用next()才能往下执行,相当于把执行的控制权从引擎交给了开发者。

所以Generator解决的是流程控制的问题。

它可以在执行过程暂时中断,先执行别的程序,但是它的执行上下文并没有销毁,仍然可以在需要的时候切换回来,继续往下执行。

最重要的优势在于,它看起来是同步的语法,但是却可以异步执行。

总结:从上例中我们看出虽然Generator将异步操作表示得很简洁,但是手动迭代Generator 函数很麻烦,实现逻辑有点绕,而实际开发一般会配合 co 库去使用,co库可以简化代码的写法 npm install co

4.6 async/await
  • Generator 函数的语法糖。

ES2017 标准引入了 async 函数,使得异步操作变得更加方便。

var asyncReadFile = async function (){
  var f1 = await  readFile('./a.txt');
  var f2 = await  readFile('./b.txt');
  console.log(f1.toString());
  console.log(f2.toString());
};

该语法的目的就是定义一个异步执行的函数,内部实现是对Promise的封装

async函数的优点

(1)内置执行器:Generator 函数的执行必须靠执行器,所以才有了 co 函数库,而 async 函数自带执行器。也就是说,async 函数的执行,与普通函数一模一样,只要一行。

(2) 语义化更好:async 和 await,比起星号和 yield,语义更清楚了。async 是“异步”的简写,而 await 可以认为是 async wait 的简写。所以应该很好理解 async 用于申明一个 function 是异步的,而 await 用于等待一个异步方法执行完成。

(3) Promise使用then函数进行链式调用,一直点点点,是一种从左向右的横向写法;async/await从上到下,顺序执行,就像写同步代码一样,更符合代码编写习惯, 是对promise的进一步优化,更强大且结构也更加清晰。

5.EventLoop 事件循环

1.所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
2.主线程之外,还存在一个任务队列(event queue)。只要异步任务有了运行结果,就在任务队列之中放置一个事件。
3.一旦执行栈中的所有同步任务执行完毕,系统就会读取任务队列(event queue),看看里面有哪些异步任务可以执行,进入执行栈,开始执行。
主线程不断重复上面的第三步。(event loop)

任务队列里面分为***宏任务***和***微任务***,执行栈在执行完同步任务后,查看执行栈是否为空,如果执行栈为空,就会去任务队列,遇到Task(宏任务),先执行宏任务,将宏任务放入宏任务的event queue, 然后在执行微任务,将微任务放入微任务event queue

注意:往外拿的时候,先从微任务里拿这个回调函数,按照先入先出的规则全部执行完微任务(microTask)后,然后再从宏任务的queue上拿宏任务的回调函数**,**如此循环

宏任务队列可以有多个,微任务队列只有一个

遇到微任务,放在当前任务列的最底端

遇到宏任务,放在下一个新增任务列的最顶端,宏任务按照秒数先后执行·

微任务 promise async await

宏任务 setTimeout setInterval

     console.log("a");
      setTimeout(function(){
          console.log("b")
      },0);
     console.log("c");
      new Promise(function(resolve,reject){
          console.log("d")
          resolve();
      }).then(function(){
          console.log("e");
      })//a,c,d,e,b
			

			setTimeout(()=>{
        console.log('t1');
        Promise.resolve().then(()=>{
          console.log('p1');
        })
      },10)
			setTimeout(()=>{
        console.log('t2')
        Promise.resolve().then(()=>{
          console.log('p2')
        })
      },0)
image-20210401170523993 image-20210401171146003
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值