ES6 事件迴圈機制

Inlight發表於2018-06-25
執行上下文(Execution Context)

JavaScript中的執行環境大概包括三種情況:

  • 全域性環境:JavaScript程式碼執行起來會首先進入該環境
  • 函式環境:當函式被呼叫執行時,會進入當前函式中執行程式碼
  • eval:存在安全問題(因為它可以執行傳給它的任何字串,所以永遠不要傳入字串或者來歷不明和不受信任源的引數)不建議使用,可忽略

每次當控制器轉到可執行程式碼的時候,就會進入一個執行上下文。執行上下文可以理解為當前程式碼的執行環境,它會形成一個作用域。

函式呼叫棧(call stack)

因此在一個JavaScript程式中,必定會產生多個執行上下文,JavaScript引擎會以函式呼叫棧的方式來處理它們。棧底永遠都是全域性上下文,而棧頂就是當前正在執行的上下文。

var color = 'blue';

function changeColor() {
    var anotherColor = 'red';

    function swapColors() {
        var tempColor = anotherColor;
        anotherColor = color;
        color = tempColor;
    }

    swapColors();
}
changeColor();
複製程式碼

注意:函式中,遇到 return 能直接終止可執行程式碼的執行,因此會直接將當前上下文彈出棧。

全域性上下文的生命週期,與程式的生命週期一致,只要程式執行不結束,比如關掉瀏覽器視窗,全域性上下文就會一直存在。其他所有的上下文環境,都能直接訪問全域性上下文的屬性。

解了這個過程之後,我們就可以對執行上下文做一些總結:

  • 單執行緒
  • 同步執行,只有棧頂的上下文處於執行中,其他上下文需要等待
  • 全域性上下文只有唯一的一個,它在瀏覽器關閉時出棧
  • 函式的執行上下文的個數沒有限制
  • 每次某個函式被呼叫,就會有個新的執行上下文為其建立,即使是呼叫的自身函式
執行上下文生命週期

一個執行上下文的生命週期可以分為兩個階段:

  • 建立階段:在這個階段中,執行上下文會分別建立變數物件,建立作用域鏈,以及確定this的指向。

    1. 建立變數物件:
      1. 建立arguments物件:檢查當前上下文中的引數,建立該物件下的屬性與屬性值。
      2. 檢查當前上下文的函式宣告:在變數物件中以函式名建立一個屬性,屬性值為指向該函式所在記憶體地址的引用,如果函式名的屬性已經存在,那麼該屬性將會被新的引用所覆蓋。
      3. 檢查當前上下文中的變數宣告:每找到一個變數宣告,在變數物件中以變數名建立一個屬性,屬性值為undefined,如果該變數名的屬性已經存在,為了防止同名的函式被修改為undefined,則會直接跳過,原屬性值不會被修改。
    2. 建立作用域鏈:作用域鏈,是由當前環境與上層環境的一系列變數物件組成,它保證了當前執行環境對符合訪問許可權的變數和函式的有序訪問
    3. 確定this的指向:this的指向,是在函式被呼叫的時候確定的,在函式執行過程中,this一旦被確定,就不可更改了。如果呼叫者函式,被某一個物件所擁有,那麼該函式在呼叫時,內部的this指向該物件。如果函式獨立呼叫,那麼該函式內部的this,則指向undefined。
  • 程式碼執行階段:建立完成之後,就會開始執行程式碼,這個時候,會完成變數賦值,函式引用,以及執行其他程式碼。

建立變數物件:

例子1:

function test() {
    console.log(a);
    console.log(foo());

    var a = 1;
    function foo() {
        return 2;
    }
}
test();
複製程式碼

等價於

function test() {
    function foo() {
        return 2;
    }
    var a;
    console.log(a);
    console.log(foo());
    a = 1;
}

test();
複製程式碼

例子2:

function test() {
    console.log(foo);
    console.log(bar);

    var foo = 'Hello';
    console.log(foo);
    var bar = function () {
        return 'world';
    }

    function foo() {
        return 'hello';
    }
}

test();
複製程式碼

等價於

function test() {
    function foo() {
        return 'hello';
    }
    var bar;

    console.log(foo);
    console.log(bar);
    foo = 'Hello';
    console.log(foo);
    var bar = function () {
        return 'world';
    }
}

test();
複製程式碼

未進入執行階段之前,變數物件中的屬性都不能訪問。但是進入執行階段之後,變數物件轉變為了活動物件,裡面的屬性都能被訪問了,然後開始進行執行階段的操作。 變數物件和活動物件其實都是同一個物件,只是處於執行上下文的不同生命週期。不過只有處於函式呼叫棧棧頂的執行上下文中的變數物件,才會變成活動物件。 我們可以用建立變數物件來理解變數提升。

建立作用域鏈: 作用域鏈,是由當前環境與上層環境的一系列變數物件組成,它保證了當前執行環境對符合訪問許可權的變數和函式的有序訪問。

var a = 20;

function test() {
    var b = a + 10;

    function innerTest() {
        var c = 10;
        return b + c;
    }

    return innerTest();
}

test();
複製程式碼

確定this的指向: this的指向,是在函式被呼叫的時候確定的,在函式執行過程中,this一旦被確定,就不可更改了。如果呼叫者函式,被某一個物件所擁有,那麼該函式在呼叫時,內部的this指向該物件。如果函式獨立呼叫,那麼該函式內部的this,則指向undefined。

// demo01
var a = 20;
function fn() {
    console.log(this.a);
}
fn();
複製程式碼
// demo02
var a = 20;
function fn() {
    function foo() {
        console.log(this.a);
    }
    foo();
}
fn();
複製程式碼
// demo03
var a = 20;
var obj = {
    a: 10,
    c: this.a + 20,
    fn: function () {
        return this.a;
    }
}

console.log(obj.c);
console.log(obj.fn());
複製程式碼

使用call,apply顯示指定this

function fn() {
    console.log(this.a);
}
var obj = {
    a: 20
}

fn.call(obj);
複製程式碼

call與applay後面的引數,都是向將要執行的函式傳遞引數。其中call以一個一個的形式傳遞,apply以陣列的形式傳遞。這是他們唯一的不同。

function fn(num1, num2) {
    console.log(this.a + num1 + num2);
}
var obj = {
    a: 20
}

fn.call(obj, 100, 10); 
fn.apply(obj, [20, 10]); 
複製程式碼
事件迴圈機制

JS 引擎建立在單執行緒事件迴圈的概念上。單執行緒( Single-threaded )意味著同一時刻只能執行一段程式碼,與 Swift、 Java 或 C++ 這種允許同時執行多段不同程式碼的多執行緒語言形成了反差。

JavaScript程式碼的執行過程中,除了依靠函式呼叫棧來搞定函式的執行順序外,還依靠任務佇列(task queue)來搞定另外一些程式碼的執行。

  • 一個執行緒中,事件迴圈是唯一的,但是任務佇列可以擁有多個。
  • 任務佇列又分為macro-task(巨集任務)與micro-task(微任務),在最新標準中,它們被分別稱為task與jobs。
  • macro-task大概包括:script(整體程式碼), setTimeout, setInterval, setImmediate, I/O, UI rendering。
  • micro-task大概包括: process.nextTick, Promise, Object.observe(已廢棄), MutationObserver(html5新特性)
  • setTimeout/Promise等我們稱之為任務源。而進入任務佇列的是他們指定的具體執行任務。
  • 來自不同任務源的任務會進入到不同的任務佇列。
  • 事件迴圈的順序,決定了JavaScript程式碼的執行順序。它從script(整體程式碼)開始第一次迴圈。之後全域性上下文進入函式呼叫棧。直到呼叫棧清空(只剩全域性),然後執行所有的micro-task。當所有可執行的micro-task執行完畢之後。迴圈再次從macro-task開始,找到其中一個任務佇列執行完畢,然後再執行所有的micro-task,這樣一直迴圈下去。
  • 其中每一個任務的執行,無論是macro-task還是micro-task,都是藉助函式呼叫棧來完成。

例子1:

setTimeout(function() {
    console.log('timeout1');
})

new Promise(function(resolve) {
    console.log('promise1');
    for(var i = 0; i < 1000; i++) {
        i == 99 && resolve();
    }
    console.log('promise2');
}).then(function() {
    console.log('then1');
})

console.log('global1');
複製程式碼

例子2:

// demo02
console.log('glob1');

setTimeout(function() {
    console.log('timeout1');
    process.nextTick(function() {
        console.log('timeout1_nextTick');
    })
    new Promise(function(resolve) {
        console.log('timeout1_promise');
        resolve();
    }).then(function() {
        console.log('timeout1_then')
    })
})

setImmediate(function() {
    console.log('immediate1');
    process.nextTick(function() {
        console.log('immediate1_nextTick');
    })
    new Promise(function(resolve) {
        console.log('immediate1_promise');
        resolve();
    }).then(function() {
        console.log('immediate1_then')
    })
})

process.nextTick(function() {
    console.log('glob1_nextTick');
})
new Promise(function(resolve) {
    console.log('glob1_promise');
    resolve();
}).then(function() {
    console.log('glob1_then')
})

setTimeout(function() {
    console.log('timeout2');
    process.nextTick(function() {
        console.log('timeout2_nextTick');
    })
    new Promise(function(resolve) {
        console.log('timeout2_promise');
        resolve();
    }).then(function() {
        console.log('timeout2_then')
    })
})

process.nextTick(function() {
    console.log('glob2_nextTick');
})
new Promise(function(resolve) {
    console.log('glob2_promise');
    resolve();
}).then(function() {
    console.log('glob2_then')
})

setImmediate(function() {
    console.log('immediate2');
    process.nextTick(function() {
        console.log('immediate2_nextTick');
    })
    new Promise(function(resolve) {
        console.log('immediate2_promise');
        resolve();
    }).then(function() {
        console.log('immediate2_then')
    })
})
複製程式碼

80% 應聘者都不及格的 JS 面試題

Excuse me?這個前端面試在搞事!

yangbo5207.github.io/wutongluo/

相關文章