美文网首页前端攻城狮程序员JavaScript
JavaScript 异步编程学习笔记

JavaScript 异步编程学习笔记

作者: 15d843cd48a8 | 来源:发表于2016-03-09 21:05 被阅读319次

    这篇文章是我看完《JavaScript异步编程》之后结合书中内容和现在掌握的知识记录下来的。

    首先要认识JavaScript是单线程语言,可以利用事件模型处理异步触发任务。如果只有两三个可能的事件,单线程语言编写的面向事件的代码要比多线程代码简单得多。但如果有很多事件,同时要求数据的状态能够从一个事件传递到下一个事件,那么就会像下面这样:

    step1(function(result1){
        step2(function(result2){
            step3(function(result3){
                //...
            })
        })
    })
    

    这被称为『回调地狱』,很明显是不能让人忍受的。

    JavaScript 运行机制 和 Event Loop

    想要让 JavaScript 中的某段代码将来再运行,可以将它放在回调中。回调就是就是一个普通函数,运行回调时,我们称已触发某事件。

    setTimeout

    对 setTimeout 的描述通常是:

    给定一个回调及n毫秒的延迟,setTimeout 就会在 n 毫秒后运行该回调。

    那么看一个例子:

    for(var i=1;i<=3;i++){
        setTimeout(function(){
            console.log(console.log(i))
        },0)
    }
    

    返回的是3个4,因为:

    1. i 变量的作用域在 setTimeout 的回调函数内
    2. 循环结束时,i等于4
    3. JavaScript事件处理器在线程空闲之前不会运行

    第3条很重要也很难理解,但只要我们了解 线程的阻塞 就明白了。

    思考下面的代码:

    var start = new Date()
    setTimeout(function(){
      var end = new Date()
    
      console.log(end - start)
    },500)
    
    while(new Date - start<1000){
    
    }
    

    因为 while 循环会持续一秒,线程繁忙,结果是大于等于 1000 的数字,可能会稍有不同,这是因为 setTimeout 很不精准。不过,这个数字至少是1000,因为setTimeout回调在while循环结束之前不可能被触发。

    之所以会这样,全是因为 任务队列 的存在。

    任务队列 task queue

    因为JavaScript是单线程的,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时长,后一个任务就不得不一直等着。

    很多时候我们可以先挂起等待中的任务,先运行排在后面的任务,等之前的任务有了结果,再把挂起的任务继续执行。

    所以,任务分成了两种:

    • 同步任务:在主线程中排队依次执行的任务
    • 异步任务:不进入主线程,进入任务队列的任务。只有任务队列通知主线程某异步任务可以执行,该任务才会进入主线程执行。

    异步任务通常可以分为两大类:I/O 函数(AJAX、readFile等)和计时函数(setTimeout、setInterval)

    其实所谓的『回调函数』,就是那些被主线程挂起来的代码。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。

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

    主线程和任务队列示意图:

    主线程和任务队列示意图

    只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制。这个过程会不断重复。

    // 点击按钮 5s 后才会生效
    var start = new Date()
    
    btn.onclick = function(){
      console.log('click')
    }
    
    while(new Date - start<5000){
    
    }
    

    以上面代码为例,用户点击 btn 时,会有一个 click 事件排入队列。但是,该单击事件处理器要等到当前所有正在运行的代码都结束后(可能还要等其他此前已排队的事件也以次结束)才会执行。

    事件循环 Event Loop

    主线程从『任务队列』中读取事件,这个过程是循环不断的,所以这个运行机制又称为 事件循环 (Event Loop)

    Event Loop

    上图中,主线程运行的时候,产生堆(heap)和栈(stack),栈中的代码调用各种外部API,它们在"任务队列"中加入各种事件(click,load,done)。只要栈中的代码执行完毕,主线程就会去读取"任务队列",依次执行那些事件所对应的回调函数。

    参考文章


    事件模型 - 发布 / 订阅模式

    上面介绍了 JavaScript 的运行机制,现在说说 JavaScript 的事件在实际中的处理。JavaScript 中的事件模型是典型的 发布/订阅模式。

    jQuery 的 on / trigger 就是一个具体实现的例子。

    如果一个事件的回调里有很多任务要完成,比如登录网站成功后要更新header模块的头像、导航模块的头像、刷新消息列表:

    login.succ(function(data){
        header.setAvatar(data.avatar)
        nav.setAvatar(data.avatar)
        message.refresh()
    })
    

    这样模块之间耦合严重,header模块不能再更改 setAvatar 方法。header模块要重构的话,势必也会影响这里。最好是这样:与用户登录相关的业务模块订阅登录成功的事件,登录模块只要发布订阅成功的信息。这样,登录成功后,它们进行各自的业务处理,两不相干。

    $.ajax('/login',function(data){ //登录成功
        login.trigger('loginSucc',data) //发布登录成功消息
    })
    
    var header = (function(){
        login.listen('loginSucc',function(data){
            header.setAvatar(data.avatar)
        })
        return {
            setAvatar: function(data){
                //...       
            }
        }
    })()
    

    这样,就算要在登录成功后增加新功能,登录模块也不用改,只要新加模块就行:

    var xxx = (function(){
        login.listen('loginSucc',function(data){
            xxx.abc(data)
        })
        return {
            abc: function(data){
                //...
            }
        }
    })()
    

    参考文章


    Promise

    Promise 是一个管理事务的对象。

    Promise有三个状态,Pending(进行中)、Resolved(已完成)
    、Rejected(已失败)。只有异步操作的结果才决定当前是哪一种状态。

    状态一旦改变,就不会再变,任何时候可以得到这个结果。只有两个结果:Pending -> ResolvedPending -> Rejected

    具体可以看 Promise

    异步 API

    这几个是JavaScript异步编程的API


    异步脚本加载

    defer - 脚本延迟运行

    <script>defer 属性,相当于告诉浏览器:马上加载这个脚本,但是,等到文档就绪且此前具有 defer 属性的脚本都结束运行之后再运行它。

    简单来说,加了 defer 之后 <script> 放在 <head><body> 是没有区别的。

    async - 脚本的并行化

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

    这两个脚本会以任意次序运行,而且会立即运行,不论文档是否就绪。

    如果同时使用 deferasyncasync 会覆盖掉 defer

    <html>
        <head>
            <script src="a.js"></script>
            <script defer src="b.js"></script>
        </head>
        <body>
            <!-- content -->
            <script async defer src="c.js"></script>
            <script async defer src="d.js"></script>
        </body>
    </html>
    

    在大多数浏览器中,a.js 结束运行时,DOM开始渲染。在渲染的同时,加载 b.js。渲染结束时,运行 b.jsc.jsd.js。其中,c.jsd.js 会无序运行。

    相关文章

      网友评论

        本文标题:JavaScript 异步编程学习笔记

        本文链接:https://www.haomeiwen.com/subject/qzctlttx.html