初步理解JavaScript 的运行机制

315 阅读5分钟

javascript是一种弱类型、动态的、解释型脚本语言,它的一大核心特点就是单线程,单线程就意味着所有的任务执行都需要排队。js高级中提到javascript是运行于单线程的环境中的,在页面的生命周期中,不同时间可能有其他代码在控制javascript的进程。在页面下载完成后的代码运行、事件处理程序、ajax回调函数都必须使用同样的线程来执行。实际上,浏览器负责排序将指派某段代码在某个时间点运行的优先级。

文章借鉴了几位大神的笔记:

jakearchibald.com/2015/tasks-… www.ruanyifeng.com/blog/2014/1… v.youku.com/v_show/id_X…

为了加强记忆,总结记录一下,如有错误烦请指出,立即改正。先来看几个概念:


  • 同步和异步

    同步就是调用之后一直等待,直到返回结果。

    异步则是调用之后,不能直接拿到结果,通过一系列的手段才最终拿到结果(调用之后,拿到结果中间的时间可以介入其他任务)

  • javascript进程

    页面载入时,首先执行的是一些页面生命周期后面要用到的一些简单的函数和变量的声明,也有可能包含一些初始数据的处理等同步任务。在执行完这些之后,javascript进程会处于空闲状态,等待执行下一段代码。当进程空闲时,下一段代码会被触发并立即执行。

  • 任务队列

    除了javascript执行进程之外,还有一个需要在进程下一次空闲时执行的代码队列。随着页面在生命周期中的不断推移,代码会按照执行顺序添加入队列。例如,点击某个按钮、接受到的某个ajax的回调、定时器等异步任务,它们对应的处理程序就会被添加到队列中。在javascript中,没有代码是被立即执行的,但一旦进程空闲则尽快执行。队列中所有的代码都要等到javascript进程空闲之后才能执行,不管它们是如何添加到队列中的。

    需要注意的是定时器对队列的工作方式,当特定时间过去后将代码插入,并不意味着会立即执行。比如,设置一个100ms的定时器不代表到了100ms代码就立即执行,它表示代码会在100ms后被加入到队列中等待执行。 setInterval仅当没有该定时器的任何其他代码实例时,才会被添加到队列中

  • Event Loop(事件循环)

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

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

    function mutiply(a, b) {
        return a * b
    }
    
    function square(n) {
        return mutiply(n, n)
    }
    
    function printSquare(n) {
        var squared = square(n)
        console.log(squared)
    }
    
    printSquare(4)
    

    假如main()为主进程,主进程运行到printSquare()时,先将printSquare入栈并开始执行,然后发现调用了square(n),square(n)中又调用了mutiply(n, n)。它们会被会依次压入栈中,如下图所示

    mutiply(n,n)
    square(n)
    printSquare(4)
    main()

    接下来会依次执行mutiply--square,如下图所示

    printSquare(4)
    main()

    然后会继续执行printSquare(4)的代码,并会将console.log(squared)压入栈中,如下图所示

    console.log(squared)
    printSquare(4)
    main()

    然后程序继续执行下去,完成所有进程。

    再来看一个例子

    console.log('hello')
    setTimeOut(function() {
        console.log('time') 
    }, 3000)
    console.log('world')
    

    很多人都知道输出的结果时 hellow、world、time,但是为什么? 首先看一下此时执行的过程

    console.log('hello')
    main()

    先将console.log('hello')执行完毕,然后执行定时器

    setTimeOut(cb, 3000)
    main()
    队列
    function() {console.log('time')}

    注意,这个时候定时器中的回调并没有被执行,栈中的代码会调用各种webapi,然后再在合适的时机将代码添加到任务队列中等待执行。继续向下执行

    console.log('world')
    main()

    这个时候会打印出world,进程空闲下来。这个时候会立即执行任务队列的中的任务,接着打印出time

  • console.log('script start');
    setTimeout(function() {
        console.log('setTimeout');
        Promise.resolve().then(function() {
            console.log('promise3');
        }).then(function() {
            console.log('promise4');
        });
    }, 0);
    Promise.resolve().then(function() {
        console.log('promise1');
    }).then(function() {
        console.log('promise2');
    });
    console.log('script end');
    

    思考一下上题的执行结果是什么?

    script start
    script end
    promise1
    promise2
    setTimeout
    promise3
    promise4
    

    为什么Promise的立即返回的异步任务执行会优先于setTimeout延时为0的任务执行?

    想理解其中原因,不得不提Macrotasks 和 Microtasks

  • Macrotask 和 Microtask

    Macrotask 和 Microtask都是属于异步任务中的一种,我们先看一下他们分别是哪些 API:

    • macrotasks: setTimeout, setInterval, setImmediate, I/O, UI rendering
    • microtasks: process.nextTick, Promises, Object.observe(废弃), MutationObserver

    原因是任务队列分为macrotasks和microtasks队列,在每一次事件循环 中,macrotask只会取一个执行,而 microtask 会一直提取,直到 microtasks队列清空。而事件循环每次只会入栈一个 macrotask,主线 程执行完该任务后又会先检查 microtasks队列并完成里面的所有任务 后再执行macrotask