视频1 视频21 视频41 视频61 视频文章1 视频文章21 视频文章41 视频文章61 推荐1 推荐3 推荐5 推荐7 推荐9 推荐11 推荐13 推荐15 推荐17 推荐19 推荐21 推荐23 推荐25 推荐27 推荐29 推荐31 推荐33 推荐35 推荐37 推荐39 推荐41 推荐43 推荐45 推荐47 推荐49 关键词1 关键词101 关键词201 关键词301 关键词401 关键词501 关键词601 关键词701 关键词801 关键词901 关键词1001 关键词1101 关键词1201 关键词1301 关键词1401 关键词1501 关键词1601 关键词1701 关键词1801 关键词1901 视频扩展1 视频扩展6 视频扩展11 视频扩展16 文章1 文章201 文章401 文章601 文章801 文章1001 资讯1 资讯501 资讯1001 资讯1501 标签1 标签501 标签1001 关键词1 关键词501 关键词1001 关键词1501 专题2001
浏览器UI多线程及对JavaScript单线程底层运行机制的理解
2020-11-27 20:24:34 责编:小采
文档

早在我刚刚学习JavaScript的时候,我就被“灌输”了这样的思想

JavaScript是单线程的
可是在我不断的学习过程中
学到了定时器、ajax的异步加载
一度让我对这句话产生怀疑
既然JavaScript是单线程的,为什么它还存在异步加载?
后来我知道了浏览器中不仅仅有js一个线程,它与其他线程共同组成了 —– 浏览器UI多线程

早就想写一篇这样的文章,只是感觉理解的还不够,怕我写的对大家产生误导
看完网上大家写的各种博文,更是一脸懵逼
不过最后还是鼓起勇气决定来谈一下╮(╯_╰)╭
那么现在抛开JavaScript线程的问题不谈,我们先来看看什么是浏览器UI线程

浏览器UI多线程

先来上一张图

一般情况下浏览器至少有三个常驻线程

  • GUI渲染线程(渲染页面)

  • JS引擎线程(处理脚本)

  • 事件触发线程(控制交互)

  • 包括浏览器有时候会开辟新的线程比如用完就扔的Http请求线程(Ajax)等等其他线程
    它们共同构成了浏览器的UI线程
    这些线程在UI线程的控制下井然有序的工作着
    关于这个常驻线程网上观点不一致,各个浏览器实现可能也不一样,这里就不深考究了

    虽然我把js引擎线程放在了右下角,但是它是浏览器的主线程
    而且它和GUI渲染线程是水火不容的,不能够同时工作
    道理很简单,因为它们都要操作DOM,如果js线程想要某个DOM的样式,渲染引擎必须停止工作(霸道总裁让你站那儿别动)

    js单线程

    为什么JavaScript是单线程
    单线程就是同一时间只能干一件事
    那么JavaScript多线程不好吗,那效率多高啊
    不好
    js设计出来就是为了与用户交互,处理DOM
    如果JavaScript多线程了,那就必须处理多线程同步的问题(依稀记得曾经被C++线程同步支配的恐怖)
    假如js是多线程,同一时间一个线程想要修改DOM,另一个线程想要删除DOM
    问题就变得复杂许多,浏览器不知道听谁的,如果引入“锁”的机制,那就麻烦死了(那我就不学前端了( ̄_, ̄ ))
    所以我们这样一个脚本语言根本没有必要搞得那么复杂,所以JavaScript诞生起就是单线程执行

    虽然H5提出了Web Worker,但是它不能够操作DOM,还是要委托个大哥js主线程解决
    这个Web Worker我还没有了解太多,这里就不多说了
    这些子线程完全受主线程大哥控制的,所以没有改变JavaScript单线程的本质

    执行栈

    我们先来看看什么是执行栈
    栈是先进后出(FILO)的数据结构
    执行栈中存放正在执行的任务,每一个任务叫做“帧”
    举个例子

    function foo(c){
     var a = 1;
     bar(200);
    }function bar(d){
     var b = 2;
    }
    foo(100);

    我们来看看执行栈发生了怎样的变化

  • 最开始,代码没有执行的时候,执行栈为空栈

  • foo函数执行时,创建了一帧,这帧中包含了形参、局部变量(预编译过程),然后把这一帧压入栈中

  • 然后执行foo函数内代码,执行bar函数

  • 创建新帧,同样有形参、局部变量,压入栈中

  • bar函数执行完毕,弹出栈

  • foo函数执行完毕,弹出栈

  • 执行栈为空

  • 执行栈其实相当于js主线程

    任务队列

    队列是先入先出(FIFO)的数据结构
    js线程中还存在着一个任务队列
    任务队列包含了一系列待处理的任务
    单线程就意味着所有任务需要一个接一个的执行,如果一个任务执行的时间太长,那后面的任务就不得不等着
    就好比护士阿姨给排队的小朋友打针,如果最前面的小朋友一直滚针,那就一直扎,后面的小朋友就得等着(这比喻好像不恰当)
    可是如果最前面的小朋友晕针昏倒了
    那么护士阿姨不可能坐那里了等到他醒来,一定是先给后面的小朋友扎针
    也就是相当于把那位小朋友“挂起”(异步)

    所以,任务可以分为两种

  • 同步任务

  • 异步任务

  • 同步任务就是正在主线程执行栈中执行的任务(在屋子内打针的小朋友)
    而异步任务是在任务队列等候处理的任务(在屋子外等候打针的小朋友)
    一旦执行栈中没有任务了,它就会从执行队列中获取任务执行

    事件与回调

    任务队列是一个事件的队列,IO设备(输入/输出设备)每完成一项任务,就会在任务队列中添加事件处理
    用户触发了事件,也同样会将回调添加到任务队列中去
    主线程执行异步任务,便是执行回调函数(事件处理函数)
    只要执行栈一空,排在执行队列前面的会被优先读取执行,
    不过主线程会检查时间,某些事件需要到了规定时间才能进入主线程处理(定时器事件)

    事件循环

    主线程从执行队列不断地获取任务,这个过程是循环不断地,叫做“Event Loop”事件循环
    同步任务总是会在异步任务之前执行
    只有当前的脚本执行完,才能够去拿任务队列中的任务执行
    前面也说到了,任务队列中的事件可以是定时器事件
    定时器分为两种 setTimeout() 和 setInterval()
    前者是定时执行一次,后者定时重复执行
    第一个参数为执行的回调函数,第二个参数是间隔时间(ms)
    来看这样一个例子

    setTimeout(function(){
     console.log('timer');
    },1000);console.log(1);console.log(2);console.log(3);

    这个没什么问题,浏览器打印的是 1 2 3 timer
    但是这样呢

    setTimeout(function(){
     console.log('timer');
    },0);//0延时console.log(1);
    console.log(2);
    console.log(3);

    浏览器打印依然打印的是 1 2 3 timer
    也许有同学知道,旧版浏览器,setTimeout定时至少是10ms(即便你设置了0ms),
    H5新规范是定时至少4ms(我读书少不知道为什么),改变DOM也是至少16ms
    也许这是因为这个原因
    那么我再改动一下代码

    setTimeout(function(){
     console.log('timer');
    },0);var a = +new Date();for(var i = 0; i < 1e5; i++){
     console.log(1);
    }var b = +new Date();
    console.log(b - a);

    这回够刺激了吧,输出10w次,我浏览器都假死了(心疼我chrome)
    不仅如此,我还打印了循环所用时间
    来看看控制台

    输出了10w个1,用了将近7s
    timer依然是最后打印的
    这就证明了我前面说的话: 同步任务总是会在异步任务之前执行
    只有我执行栈空了,才会去你任务队列中取任务执行

    实例

    最后我举一个例子加深一下理解

    demo.onclick = function(){
     console.log('click');
    }function foo(a){
     var b = 1;
     bar(200);
    }function bar(c){
     var d = 2;
     click//伪代码 此时触发了click事件(这里我假装程序运行到这里手动点击了demo)
     setTimeout(function(){
     console.log('timer');
     }, 0);
    }
    foo(100);

    怕大家蒙我就不写Ajax了
    Ajax如果处理结束后(通过Http请求线程),也会将回调函数放在任务队列中
    还有一点click那一行伪代码我最开始是想用demo.click()模拟触发事件
    后来在测试过程中,发现它好像跟真实触发事件不太一样
    它应该是不通过触发事件线程,而是存在于执行栈中,就相当于单纯地执行click回调函数
    不过这只是我自己的想法有待考证,不过这不是重点,重点是我们理解这个过程,请大家不要吐槽我╰( ̄▽ ̄)╭

    下面看看执行这段代码时发生了什么(主要说栈和队列的问题,不会赘述预编译过程)

  • 主线程开始执行,产生了栈、堆、队列

  • demo节点绑定了事件click,交给事件触发线程异步监听

  • 执行foo函数(之前同样有预编译过程),创建了帧包括foo函数的形参、局部变量压入执行栈中

  • foo函数内执行bar函数,创建帧包括bar函数的形参、局部变量压入执行栈中

  • 触发了click事件,事件触发线程将回调事件处理函数放到js线程的任务队列中

  • 触发了定时器事件,事件触发线程立即(4ms)将回调处理函数放到js线程的任务队列中

  • bar函数执行完毕,弹出栈

  • foo函数执行完毕,弹出栈

  • 此时执行栈为空

  • 执行栈向任务队列中获取一个任务:click回调函数,输出‘click’

  • 执行栈项任务队列中获取一个任务:定时器回调函数,输出‘timer’

  • 执行结束

  • 这里从任务队列里不断取任务的过程就是Event Loop

    有一些我的理解,如果发现不对或者有疑问的地方,请联系我
    相信大家看了这个例子应该对js底层运行机制有了一个大概的了解

    下载本文
    显示全文
    专题