0%

1
2
3
4
5
6
7
8
9
10
11
var flatten = function(arr) {
let res = [];
for(let i = 0;i<arr.length;i++){
if(Array.isArray(arr[i])){
res = res.concat(flatten(arr[i]))
}else{
res.push(arr[i])
}
}
return res;
}

Js执行流程:

编译阶段

变量提升:

是指在JavaScript代码执行过程中,JavaScript引擎把变量的声明部分和函数的声明部分提升到代码开头的“行为”。变量被提升后,会给变量设置默认值,这个默认值就是我们熟悉的undefined

执行部分的代码

经过编译后,生成两部分内容:执行上下文和可执行代码

执行上下文包括变量环境,词法环境,外部引用Outer(指向外部的执行上下文)和this,一般包括三种:全局执行上下文,函数执行上下文(调用函数,函数内代码被编译,创建函数上下文,函数执行结束,上下文销毁),eval(使用eval函数时,eval的代码会被编译,并创建执行上下文)

执行上下文会被js引擎压入调用栈中,执行完毕后,会把执行上下文弹出栈

执行阶段

Js引擎开始执行可执行代码,按照顺序一行行执行,当出现相同的变量和函数,会保存到执行上下文的变量环境中,一段代码如果定义了两个相同名字的函数,那么最终生效的是最后一个函数,而

作用域和作用域链以及词法作用域

作用域指在程序定义变量的区域,该位置决定了变量的生命周期,作用域就是变量与函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期

ES6之前只有全局作用域和函数作用域

  • 全局作用域中的对象在代码中的任何地方都能访问,其生命周期伴随着页面的生命周期。
  • 函数作用域就是在函数内部定义的变量或者函数,并且定义的变量或者函数只能在函数内部被访问。函数执行结束之后,函数内部定义的变量会被销毁。

ES6后多了一个块级作用域,let和const会创建块级作用域

变量提升造成的危害:

1.变量容易在不被察觉的情况在被覆盖

2.本应该被销毁的变量没被销毁:

1
2
3
4
5
6
function foo(){
for (var i = 0; i < 7; i++) {
}
console.log(i);
}
foo()

由于变量提升,变量i在创建执行上下文阶段被提升,当for循环结束,变量i没有被销毁

ES6解决变量提升带来的缺陷:

使用let和const支持块级作用域,块作用域中的变量会被放到执行上下文中的词法环境中,而不是变量环境,因此块级作用域中的变量不会出现变量提升的现象

词法作用域:

指作用域是由代码中函数声明的位置决定的,所以词法作用域是静态的作用域,通过它能够预测代码在执行过程中如何查找标识符

词法作用域由代码声明时的位置决定,所以整个词法作用域链顺序:foo函数作用域->bar函数作用域->main函数作用域->全局作用域

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function bar() {
var myName = " 极客世界 "
let test1 = 100
if (1) {
let myName = "Chrome 浏览器 "
console.log(test)
}
}
function foo() {
var myName = " 极客邦 "
let test = 2
{
let test = 3
bar()
}
}
var myName = " 极客时间 "
let myAge = 10
let test = 1
foo()

它的执行上下文栈如下:

先在执行上下文中的词法环境中查找->变量环境->外部作用域,最后在全局执行上下文的词法环境中找到test

从执行上下文角度看闭包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function foo() {
var myName = " 极客时间 "
let test1 = 1
const test2 = 2
var innerBar = {
getName:function(){
console.log(test1)
return myName
},
setName:function(newName){
myName = newName
}
}
return innerBar
}
var bar = foo()
bar.setName(" 极客邦 ")
bar.getName()
console.log(bar.getName())

根据词法作用域的规则,内部函数getName和setName总是可以访问到外部函数foo中的变量,左移当InnerBar对象返回给全局变量bar后,虽然foo函数已经执行结束,但是getName和setName函数依然可以使用foo函数中的变量myName和test1,当foo函数执行完成后,整个调用栈状态如下:

闭包定义:在JavaScript中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束,但是内部函数引用外部函数的变量依然保存在内存中,我们把这些变量的集合称为闭包。

闭包回收

如果引用闭包的函数是全局变量,那么闭包会一直存在直到页面关闭;但如果这个闭包以后不再使用的话,就会造成内存泄漏。

如果引用闭包的函数是个局部变量,等函数销毁后,在下次 JavaScript 引擎执行垃圾回收时,判断闭包这块内容如果已经不再被使用了,那么 JavaScript 引擎的垃圾回收器就会回收这块内存。

所以在使用闭包的时候,你要尽量注意一个原则:如果该闭包会一直使用,那么它可以作为全局变量而存在;但如果使用频率不高,而且占用内存又比较大的话,那就尽量让它成为一个局部变量。

在执行上下文的视角讲this

作用域链和this是两套不同的系统,

this 是和执行上下文绑定的,也就是说每个执行上下文中都有一个 this。执行上下文主要分为三种——全局执行上下文、函数执行上下文和 eval 执行上下文,所以对应的 this 也只有这三种——全局执行上下文中的 this、函数中的 this 和 eval 中的 this。

全局执行上下文中的 this

全局执行上下文中的 this 是指向 window 对象的。这也是 this 和作用域链的唯一交点,作用域链的最底端包含了 window 对象,全局执行上下文中的 this 也是指向 window 对象

#函数执行上下文中的 this

1. 通过函数的 call 方法设置

2. 通过对象调用方法设置

3. 通过构造函数中设置

this 的设计缺陷以及应对方案

1. 嵌套函数中的 this 不会从外层函数中继承

我认为这是一个严重的设计错误,并影响了后来的很多开发者,让他们“前赴后继”迷失在该错误中。我们还是结合下面这样一段代码来分析下:

1
2
3
4
5
6
7
8
9
var myObj = {
name : " 极客时间 ",
showThis: function(){
console.log(this)
function bar(){console.log(this)}
bar()
}
}
myObj.showThis()

解决:

1.在外层函数中用一个变量self保存this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var myObj = {
name : " 极客时间 ",
showThis: function(){
console.log(this)
var self = this
function bar(){
self.name = " 极客邦 "
}
bar()
}
}
myObj.showThis()
console.log(myObj.name)
console.log(window.name)

2 内部函数使用箭头函数的形式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var myObj = {
name : " 极客时间 ",
showThis: function(){
console.log(this)
var bar = ()=>{
this.name = " 极客邦 "
console.log(this)
}
bar()
}
}
myObj.showThis()
console.log(myObj.name)
console.log(window.name)

箭头函数不会创建自身的执行上下文,因此箭头函数中的this取决于它的作用域链中的上一个执行上下文中的this

2. 普通函数中的 this 默认指向全局对象 window

上面我们已经介绍过了,在默认情况下调用一个函数,其执行上下文中的 this 是默认指向全局对象 window 的。

不过这个设计也是一种缺陷,因为在实际工作中,我们并不希望函数执行上下文中的 this 默认指向全局对象,因为这样会打破数据的边界,造成一些误操作。如果要让函数执行上下文中的 this 指向某个对象,最好的方式是通过 call 方法来显示调用。

这个问题可以通过设置 JavaScript 的“严格模式”来解决。在严格模式下,默认执行一个函数,其函数的执行上下文中的 this 值是 undefined,这就解决上面的问题了

Generator和协程

生成器函数的具体使用方式:

在生成器函数内部执行一段代码,如果遇到 yield 关键字,那么 JavaScript 引擎将返回关键字后面的内容给外部,并暂停该函数的执行。
外部函数可以通过 next 方法恢复函数的执行

Generator返回的是一个协程,协程是一种比线程更轻量级的存在,你可以把协程看出是跑在线程上的任务,一个线程上可以存在多个协程,但是在线程上同时只能执行一个协程,比如当前执行的是A协程,要启动B协程,那么协程就需要把主线程的控制权交给B协程。如果从A协程启动B协程,把A协程称为B协程的父协程

一个进程拥有多个线程,一个线程也可以拥有多个协程,协程不是由操作系统内核管理,而完全是由程序控制(也就是用户态执行),好处就是性能得到了提升,不会像线程切换那样消耗资源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function* genDemo() {
console.log(" 开始执行第一段 ")
yield 'generator 2'

console.log(" 开始执行第二段 ")
yield 'generator 2'

console.log(" 开始执行第三段 ")
yield 'generator 2'

console.log(" 执行结束 ")
return 'generator 2'
}

console.log('main 0')
let gen = genDemo()
console.log(gen.next().value)
console.log('main 1')
console.log(gen.next().value)
console.log('main 2')
console.log(gen.next().value)
console.log('main 3')
console.log(gen.next().value)
console.log('main 4')

协程四点规则:

  • 调用生成器函数genDemo创建一个写成gen,创建后,gen协程并没有立即执行
  • 要让gen协程执行,需要通过调用gen.next
  • 当协程正在执行时,可以通过yield关键字来暂停gen协程的执行,并返回主信息给父协程
  • 如果协程在执行期间,遇到return关键字,那么js引擎会结束当前协程,并将return后面的内容返回给父协程

父协程有自己的调用栈,gen 协程时也有自己的调用栈,当 gen 协程通过 yield 把控制权交给父协程时,V8 是如何切换到父协程的调用栈?当父协程通过 gen.next 恢复 gen 协程时,又是如何切换 gen 协程的调用栈?

要搞清楚上面的问题,你需要关注以下两点内容。

第一点:gen 协程和父协程是在主线程上交互执行的,并不是并发执行的,它们之前的切换是通过 yield 和 gen.next 来配合完成的。

第二点:当在 gen 协程中调用了 yield 方法时,JavaScript 引擎会保存 gen 协程当前的调用栈信息,并恢复父协程的调用栈信息。同样,当在父协程中执行 gen.next 时,JavaScript 引擎会保存父协程的调用栈信息,并恢复 gen 协程的调用栈信息。

使用Promise和generator:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//foo 函数
function* foo() {
let response1 = yield fetch('https://www.geekbang.org')
console.log('response1')
console.log(response1)
let response2 = yield fetch('https://www.geekbang.org/test')
console.log('response2')
console.log(response2)
}

// 执行 foo 函数的代码
let gen = foo()
function getGenPromise(gen) {
return gen.next().value
}
getGenPromise(gen).then((response) => {
console.log('response1')
console.log(response)
return getGenPromise(gen)
}).then((response) => {
console.log('response2')
console.log(response)
})

在foo函数里实现了用同步方式实现异步操作,foo函数外部代码:

  • let gen=foo()创建gen协程
  • 父协程中通过执行gen.next把主线程控制权交给gen协程
  • gen协程获取到主线程控制权,就调用fetch函数创建一个Promise对象reponse1,然后通过yield暂停gen协程的执行,将response1返回给父协程
  • 父协程恢复执行后,调用reponse1.then方法等待结果
  • 等通过fetch发起的请求完成后,会调用then中回调函数,then中的回调函数拿到结果后,通过调用gen.next放弃主线程控制权

把执行生成器的代码封装成一个函数,并把这个执行生成器代码的函数称为执行器

1
2
3
4
5
6
7
8
9
function* foo() {
let response1 = yield fetch('https://www.geekbang.org')
console.log('response1')
console.log(response1)
let response2 = yield fetch('https://www.geekbang.org/test')
console.log('response2')
console.log(response2)
}
co(foo());

async/await

async是一个通过异步执行隐式返回Promise作为结果的函数

调用async的foo函数返回一个Promise对象,

1
2
3
4
5
6
7
8
9
async function foo() {
console.log(1)
let a = await 100
console.log(a)
console.log(2)
}
console.log(0)
foo()
console.log(3)

foo函数被async标记,当进入该函数时,js引擎会保存当前调用栈信息,当执行到await(100),会默认创建一个Promise对象。

1
2
3
let promise_ = new Promise((resolve,reject){
resolve(100)
})

在这个promise__对象创建过程中,executor函数调用resolve函数,js引擎会将该任务提交给微任务,然后js引擎会暂停当前协程执行,将主线程的控制权交给父协程执行,同时将promise__对象返回给父协程,主线程的控制权已经交给父协程,这时候父协程要做的事就是调用promise_.then监控 promise状态的改变。继续执行父协程的流程,执行console.log(3),父协程执行结束后,在结束之前,会进入微任务检查点,执行微任务队列,微任务队列有resolve(100),触发promise_.then的回调函数

1
2
3
promise_.then((value)=>{
//回调函数被激活后,将主线程控制权交给foo协程,并将value值传给协程
})

异步编程的问题:

1.代码逻辑不连续

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 执行状态
function onResolve(response){console.log(response) }
function onReject(error){console.log(error) }

let xhr = new XMLHttpRequest()
xhr.ontimeout = function(e) { onReject(e)}
xhr.onerror = function(e) { onReject(e) }
xhr.onreadystatechange = function () { onResolve(xhr.response) }

// 设置请求类型,请求 URL,是否同步信息
let URL = 'https://time.geekbang.com'
xhr.open('Get', URL, true);

// 设置参数
xhr.timeout = 3000 // 设置 xhr 请求的超时时间
xhr.responseType = "text" // 设置响应返回的数据格式
xhr.setRequestHeader("X_TEST","time.geekbang")

// 发出请求
xhr.send();

上述代码包含五个回调,导致代码逻辑不连贯,不线性,这就是异步回调影响我们的编程方式。

2.回调地狱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
XFetch(makeRequest('https://time.geekbang.org/?category'),
function resolve(response) {
console.log(response)
XFetch(makeRequest('https://time.geekbang.org/column'),
function resolve(response) {
console.log(response)
XFetch(makeRequest('https://time.geekbang.org')
function resolve(response) {
console.log(response)
}, function reject(e) {
console.log(e)
})
}, function reject(e) {
console.log(e)
})
}, function reject(e) {
console.log(e)
})
  1. 嵌套调用,下面的任务依赖上个任务的请求结果,并在上个任务的回调函数内部执行新的业务逻辑,这样嵌套层次多了以后,代码可读性变差

  2. 任务不确定性,执行每个任务有两种可能的结果(成功或者失败),所以体现在代码中就需要对每个任务的执行结果做两次判断,这种对每个任务都要进行一次额外的错误处理方式,明显增加了代码的混乱程度。

解决两个问题:

  1. 消灭嵌套调用
  2. 合并多个任务的错误处理

Promise如何消灭嵌套调用和多次错误处理

产生嵌套函数的主要原因就是在发起任务请求时会带上回调函数,这样当任务处理结束后,下个任务就只能在回调函数中处理

1.Promise实现回调函数延时绑定。在代码上体现就是先创建Promise对象x1,通过Promise的构造函数executor来执行业务逻辑,创建好Promise对象x1后,再使用x1.then设置回调函数:

1
2
3
4
5
6
7
8
9
10
11
12
// 创建 Promise 对象 x1,并在 executor 函数中执行业务逻辑
function executor(resolve, reject){
resolve(100)
}
let x1 = new Promise(executor)


//x1 延迟绑定回调函数 onResolve
function onResolve(value){
console.log(value)
}
x1.then(onResolve)

2.将回调函数onResolve的返回值穿透到最外层,因为我们会根据onResolve函数的传入值来决定创建什么类型的Promise任务,创建好的Promise对象需要返回到最外层,这样就可以摆脱嵌套循环。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 创建 Promise 对象 x1,并在 executor 函数中执行业务逻辑
function executor(resolve, reject){
resolve(100)
}
let x1 = new Promise(executor)


//x1 延迟绑定回调函数 onResolve
function onResolve(value){
console.log(value)
let x2 = new Promise((resolve,reject) =>{
resolve(value+1)
})
return x2;//内部返回值穿透到最外层
}
x1.then(onResolve)

处理异常:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function executor(resolve, reject) {
let rand = Math.random();
console.log(1)
console.log(rand)
if (rand > 0.5)
resolve()
else
reject()
}
var p0 = new Promise(executor);

var p1 = p0.then((value) => {
console.log("succeed-1")
return new Promise(executor)
})

var p3 = p1.then((value) => {
console.log("succeed-2")
return new Promise(executor)
})

var p4 = p3.then((value) => {
console.log("succeed-3")
return new Promise(executor)
})

p4.catch((error) => {
console.log("error")
})
console.log(2)

这段代码四个Promise对象,无论哪个对象抛出异常,都可以通过最后一个对象p4.catch捕获异常,通过这种方式可以将所有Promise对象的错误合并到一个函数来处理,这样就解决了每个任务需要单独处理异常的问题。Promise对象的错误具有“冒泡”性质,会一直向后传递,直到被onReject函数处理或catch语句捕获为止。具备这样的“冒泡”特性后,就不需要在每个Promise对象中单独捕获异常。

Promise与微任务

由于Promise采用回调函数延迟绑定技术,所以在执行resolve函数时,回调函数还没有绑定,那么只能推迟回调函数的执行

1
2
3
4
5
6
7
8
9
10
11
12
function Promise(executor){
var onResolve_=null
var onReject_=null
//模拟实现resolve和then
this.then=function(onResolve,onReject){
onResolve_ = onResolve
}
function resolve(value){
onResolve_(value)
}
executor(resolve,null)
}

执行这段代码:

1
2
3
4
5
6
7
8
function executor(resolve,reject){
resolve(100)
}
let demo = new Promise(executor)
function onResolve(value){
console.log(value)
}
demo.then(onResolve)

代码报错是由于Promise的延迟绑定导致的,在调用onResolve_时,Promise.then还没执行,所以会报onResolve_ is not a function错误

因此,改造Promise的resolve方法,让resolve延迟调用onResolve_

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Promise(executor){
var onResolve_=null
var onReject_=null
//模拟实现resolve和then
this.then=function(onResolve,onReject){
onResolve_ = onResolve
}
function resolve(value){
setTimeout(()=>{
onResolve_(value)
},0)
}
executor(resolve,null)
}

用定时器推迟onResolve执行,用定时器效率低,因此用微任务

参考链接:

https://blog.poetries.top/browser-working-principle/guide/part4/lesson19.html#promise-%E6%B6%88%E7%81%AD%E5%B5%8C%E5%A5%97%E8%B0%83%E7%94%A8%E5%92%8C%E5%A4%9A%E6%AC%A1%E9%94%99%E8%AF%AF%E5%A4%84%E7%90%86

为什么有nextTick?

因为vue采用的异步更新策略,当检测到数据发生变化时不会立即更新DOM,而是开启一个任务队列,并缓存在同一个事件循环中发送的所有变更,当直接操作DOM改变数据时,DOM不会立刻更新,会等到异步队列清空,也就是下一个事件循环开始时执行更新时才会进行必要的DOM更新,这种做法带来的好处就是可以将多次数据更新合并成一次,减少操作DOM的次数,如果不采用这种方法,假设数据改变100次就要去更新100次DOM,而频繁的DOM更新是很耗性能的;

nextTick作用

nextTick 接收一个回调函数作为参数,并将这个回调函数延迟到DOM更新后才执行
使用场景:想要操作 基于最新数据生成的DOM 时,就将这个操作放在 nextTick 的回调中

nextTick实现原理

将传入的回调函数包装成异步任务,异步任务又分为微任务和宏任务,为了尽快执行选择微任务,nextTick 提供了四种异步方法 Promise.then、MutationObserver、setImmediate、setTimeout(fn,0)

源码解读:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'

// 上面三行与核心代码关系不大,了解即可
// noop 表示一个无操作空函数,用作函数默认值,防止传入 undefined 导致报错
// handleError 错误处理函数
// isIE, isIOS, isNative 环境判断函数,
// isNative 判断某个属性或方法是否原生支持,如果不支持或通过第三方实现支持都会返回 false


export let isUsingMicroTask = false // 标记 nextTick 最终是否以微任务执行

const callbacks = [] // 存放调用 nextTick 时传入的回调函数
let pending = false // 标记是否已经向任务队列中添加了一个任务,如果已经添加了就不能再添加了
// 当向任务队列中添加了任务时,将 pending 置为 true,当任务被执行时将 pending 置为 false
//


// 声明 nextTick 函数,接收一个回调函数和一个执行上下文作为参数
// 回调的 this 自动绑定到调用它的实例上
export function nextTick(cb?: Function, ctx?: Object) {
let _resolve
// 将传入的回调函数存放到数组中,后面会遍历执行其中的回调
callbacks.push(() => {
if (cb) { // 对传入的回调进行 try catch 错误捕获
try {
cb.call(ctx)
} catch (e) { // 进行统一的错误处理
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})

// 如果当前没有在 pending 的回调,
// 就执行 timeFunc 函数选择当前环境优先支持的异步方法
if (!pending) {
pending = true
timerFunc()
}

// 如果没有传入回调,并且当前环境支持 promise,就返回一个 promise
// 在返回的这个 promise.then 中 DOM 已经更新好了,
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}


// 判断当前环境优先支持的异步方法,优先选择微任务
// 优先级:Promise---> MutationObserver---> setImmediate---> setTimeout
// setTimeout 可能产生一个 4ms 的延迟,而 setImmediate 会在主线程执行完后立刻执行
// setImmediate 在 IE10 和 node 中支持

// 当在同一轮事件循环中多次调用 nextTick 时 ,timerFunc 只会执行一次

let timerFunc
// 判断当前环境是否原生支持 promise
if (typeof Promise !== 'undefined' && isNative(Promise)) { // 支持 promise
const p = Promise.resolve()
timerFunc = () => {
// 用 promise.then 把 flushCallbacks 函数包裹成一个异步微任务
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
// 标记当前 nextTick 使用的微任务
isUsingMicroTask = true


// 如果不支持 promise,就判断是否支持 MutationObserver
// 不是IE环境,并且原生支持 MutationObserver,那也是一个微任务
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
let counter = 1
// new 一个 MutationObserver 类
const observer = new MutationObserver(flushCallbacks)
// 创建一个文本节点
const textNode = document.createTextNode(String(counter))
// 监听这个文本节点,当数据发生变化就执行 flushCallbacks
observer.observe(textNode, { characterData: true })
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter) // 数据更新
}
isUsingMicroTask = true // 标记当前 nextTick 使用的微任务


// 判断当前环境是否原生支持 setImmediate
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
timerFunc = () => { setImmediate(flushCallbacks) }
} else {

// 以上三种都不支持就选择 setTimeout
timerFunc = () => { setTimeout(flushCallbacks, 0) }
}


// 如果多次调用 nextTick,会依次执行上面的方法,将 nextTick 的回调放在 callbacks 数组中
// 最后通过 flushCallbacks 函数遍历 callbacks 数组的拷贝并执行其中的回调
function flushCallbacks() {
pending = false
const copies = callbacks.slice(0) // 拷贝一份 callbacks
callbacks.length = 0 // 清空 callbacks
for (let i = 0; i < copies.length; i++) { // 遍历执行传入的回调
copies[i]()
}
}

// 为什么要拷贝一份 callbacks

// 用 callbacks.slice(0) 将 callbacks 拷贝出来一份,
// 是因为考虑到在 nextTick 回调中可能还会调用 nextTick 的情况,
// 如果在 nextTick 回调中又调用了一次 nextTick,则又会向 callbacks 中添加回调,
// 而 nextTick 回调中的 nextTick 应该放在下一轮执行,
// 否则就可能出现一直循环的情况,
// 所以需要将 callbacks 复制一份出来然后清空,再遍历备份列表执行回调



Fiber起源

React15之前,Reconciler采用递归创建虚拟DOM,递归过程不能中断,如果组件树的层级很深,递归会占用线程很多时间,造成卡顿

React16将递归的无法中断的更新重构为异步的可中断更新,由于曾经用于递归的虚拟DOM数据结构已经无法满足需要,全新的Fiber诞生

Fiber含义

1.作为架构,React15的Reconciler采用递归的方式执行,数据保存在递归调用栈中,所以被称为stack Reconciler,React 16 的Reconciler基于Fiber节点实现,被称为Fiber Reconciler

2.作为静态数据结构来说,每个Fiber节点对应于一个React element**,保存了该组件的类型**(函数组件/类组件/原生组件…),对应的DOM结点信息

3.对于动态的工作单元来说,每个Fiber结点保存了本次更新中该组件改变的状态,要执行的工作(需要被删除/被插入页面/被更新)

Fiber结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
function FiberNode (
tag: WorkTag,
pendingProps:mixed,
key: null|stirng,
mode: TypeofMode,
){
//作为静态数据结构的属性
this.tag = tag;
this.key=key;
this.elementType=null;
this.type=null;
this.stateNode=null;
//用于连接其他Fiber结点形成Fiber树
this.return=null;
this.child=null;
this.sibling=null;
this.index=0;
this.ref=null;
//作为动态的工作单元的属性
this.pendingProps=pendingProps;
this.memoizedProps=null;
this.updateQueue=null;
this.memoizedState=null;
this.dependencies=null;
this.mode=mode;
this.effectTag = NoEffect;
this.nextEffect = null;

this.firstEffect = null;
this.lastEffect = null;

// 调度优先级相关
this.lanes = NoLanes;
this.childLanes = NoLanes;

// 指向该fiber在另一次更新时对应的fiber
this.alternate = null;

}

作为架构来说

每个Fiber节点有个对应的React element,多个Fiber节点如何连接成树?

1
2
3
4
5
6
//指向父级Fiber结点
this.return=null;
//指向子Fiber节点
this.child=null;
//指向右边第一个兄弟Fiber结点
this.sibling=null;

作为静态的数据结构

保存了相关组件的信息

1
2
3
4
5
6
7
8
9
10
// Fiber对应组件的类型 Function/Class/Host...
this.tag = tag;
// key属性
this.key = key;
// 大部分情况同type,某些情况不同,比如FunctionComponent使用React.memo包裹
this.elementType = null;
// 对于 FunctionComponent,指函数本身,对于ClassComponent,指class,对于HostComponent,指DOM节点tagName
this.type = null;
// Fiber对应的真实DOM节点
this.stateNode = null;

作为动态的工作单元

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 保存本次更新造成的状态改变相关信息
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null;
this.memoizedState = null;
this.dependencies = null;

this.mode = mode;

// 保存本次更新会造成的DOM操作
this.effectTag = NoEffect;
this.nextEffect = null;

this.firstEffect = null;
this.lastEffect = null;

保存优先级调度的相关信息

1
2
3
// 调度优先级相关
this.lanes = NoLanes;
this.childLanes = NoLanes;

Fiber架构的工作原理:

双缓存:

在内存中构建并直接替换的技术叫做双缓存

React使用双缓存来完成Fiber树的构建和替换——对应着DOM树的创建和更新

双缓存Fiber树

React中最多同时存在两棵Fiber树,当前屏幕显示内容对应的Fiber树称为current Fiber树,正在内存中构建的Fiber树称为workInProgress Fiber树,current Fiber树中的Fiber节点称为current fiber,workInProgress Fiber树中的Fiber节点被称为workInProgress fiber,它们通过alternate属性连接

1
2
currentFiber.alternate === workInProgressFiber;
workInProgressFiber.alternate === currentFiber;

React应用的根节点通过使用current指针在不同的Fiber树的rootFiber间切换来完成current Fiber树之间的切换

即当workInProgress Fiber树构建完成交给Renderer渲染在页面后,应用根结点的current指针指向workInProgress Fiber树,此时workInProgress Fiber树就变为current Fiber树

总结:

Reconciler工作的阶段称为render阶段,因为该阶段会调用组件的render方法

Renderer工作的阶段称为commit阶段,commit阶段会把renderer阶段提交的信息渲染到页面

render与commit阶段统称为work,即React在工作中,相对应的,如果任务正在Scheduler内调度,不属于work.

loader是导出为一个函数的node模块,该函数在loader转换资源时调用,给定函数将调用loader API,并通过this上下文访问

最简单的loader源码:

1
2
3
4
5
module.exports = function(source){
//source为compiler传递给Loader的一个文件的原内容
//该函数需要返回处理后的内容
return source
}

获得Loader的options:

1
2
3
4
5
6
const loaderUtils = require("loader-utils")
module.exports = function(source) {
//获取到用户给当前loader传入的options
const options = loaderUtils.getOptions(this)
return source
}

返回其他结果

有些场景下还需要返回除了内容外的东西,比如source Map,以方便调试源码

1
2
3
4
5
6
7
8
module.exports = function(source){
//通过this.callback告诉webpack返回的结果
this.callback(null,source,sourceMaps)
//当使用this.callback返回内容时,该loader必须返回undefined
//以让webpack知道该loader返回的结果在thi.callback中,而不是在return中
return;

}

this.callback是webpack给loader注入的api,以方便loader和webpack之间的通信,this.callback详细用法:

1
2
3
4
5
6
7
8
9
10
11
this.callback(
// 当无法转换原内容时,给 Webpack 返回一个 Error
err: Error | null,
// 原内容转换后的内容
content: string | Buffer,
// 用于把转换后的内容得出原内容的 Source Map,方便调试
sourceMap?: SourceMap,
// 如果本次转换为原内容生成了 AST 语法树,可以把这个 AST 返回,
// 以方便之后需要 AST 的 Loader 复用该 AST,以避免重复生成 AST,提升性能
abstractSyntaxTree?: AST
);

同步和异步

loader有同步和异步,同步loader的转换流程都是同步的,转换完成后再返回结果,但在有些场景下转换是异步的,例如一些网络请求

1
2
3
4
5
6
7
8
module.exports = function(source) {
// 告诉 Webpack 本次转换是异步的,Loader 会在 callback 中回调结果
var callback = this.async();
someAsyncOperation(source, function(err, result, sourceMaps, ast) {
// 通过 callback 返回异步执行后的结果
callback(err, result, sourceMaps, ast);
});
};

本地测试自定义loader:

1.在rule对象使用path.resolve指定一个本地文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const path = require('path')
module.exports = {
module:{
rules:[
{
test:/\.js$/,
use:[
{
loader: path.resolve('path/to/loader.js'),
options:{

}
}
]
}
]
}
}

匹配多个loader可以使用resolveLoader.modules配置,webpack将会从这些目录中搜索这些loaders,例如你的项目中有一个/loaders本地目录:

webpack.config.js:

1
2
3
4
5
module.exports = {
resolveLoader:{
modules: ['node_modules',path.resolve(__dirname,'loaders')]
}
}

webpack插件由以下组成:

一个javaScript命名函数或者JavaScript类

由插件函数的prototype上定义的一个apply方法

指定一个绑定到webpack自身的事件钩子

处理webpack内部实例的特定数据

功能完成后调用webpack提供的回调

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 一个 JavaScript 类
class MyExampleWebpackPlugin {
// 在插件函数的 prototype 上定义一个 `apply` 方法,以 compiler 为参数。
apply(compiler) {
// 指定一个挂载到 webpack 自身的事件钩子。
compiler.hooks.emit.tapAsync(
'MyExampleWebpackPlugin',
(compilation, callback) => {
console.log('这是一个示例插件!');
console.log(
'这里表示了资源的单次构建的 `compilation` 对象:',
compilation
);

// 用 webpack 提供的插件 API 处理构建过程
compilation.addModule(/* ... */);

callback();
}
);
}
}

webpack通过plugin机制让其更加灵活,以适应各种应用场景,在webpack运行的生命周期中会广播出许多事件,plugin可以监听这些事件,在合适的时机通过webpack提供的api改变输出结果

Compiler和Compilation

插件开发中最重要的两个资源就是compiler和compilation对象,理解它们的角色是扩展webpack引擎重要的第一步

compiler对象代表了完整的webpack环境配置,这个对象在启动webpack时被一次性实例化,可以简单理解为webpack实例,并配置好所有可操作的设置,包括options,loader和plugin。当在webpack环境中启用一个插件时,插件将受到compiler对象的引用,可以使用它来访问webpack的主环境

compilation对象代表一次资源版本构建,当运行webpack开发环境中间件时,每当检测一个文件变化,就会创建一个新的compilation,从而生成一组新的编译资源,一个compilation对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖大的状态信息。compilation对象也提供了很多关键时机的回调,以供插件做自定义处理时选择使用,通过compilation也能读取到compiler对象

compiler和compilation区别在于:compiler代表了整个webpack从启动到关闭的生命周期,而compilation只是代表了一次新的编译

基本插件架构

插件是由具有apply方法的prototype对象所实例化出来的,这个apply方法在安装插件时,会被webpack compiler调用一次,apply方法可以接受一个webpack compiler对象的引用,从而可以在回调函数中访问到compiler对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class HelloWorldPlugin {
apply(compiler) {
compiler.hooks.done.tap(
'Hello World Plugin',
(
stats /* 绑定 done 钩子后,stats 会作为参数传入。 */
) => {
console.log('Hello World!');
}
);
}
}

module.exports = HelloWorldPlugin;

安装插件:

1
2
3
4
5
6
7
// webpack.config.js
var HelloWorldPlugin = require('hello-world');

module.exports = {
// ... 这里是其他配置 ...
plugins: [new HelloWorldPlugin({ options: true })],
};