Javascript基础-执行机制

try 里面放 return,finally 里的代码会不会执行,理解其内部机制

会执行,在方法返回给调用者前执行,因为如果存在finally代码块,try中的return语句
不会立马返回给调用者,而是记录下返回值待finally代码块执行完毕之后再返回;

1
2
3
4
5
6
7
8
9
10
11
12
// return 执行了但是没有立即返回,而是先执行了 finally
function kaimo() {
try {
return 0
} catch (err) {
console.log(err)
} finally {
console.log('a')
}
}

console.log(kaimo()) // a 0
1
2
3
4
5
6
7
8
9
10
11
12
// finally 中的 return 覆盖了 try 中的 return。
function kaimo() {
try {
return 0
} catch (err) {
console.log(err)
} finally {
return 1
}
}

console.log(kaimo()) // 1

注意
1、不管有木有出现异常,finally 块中代码都会执行;
2、当 trycatch 中有 return 时,finally 仍然会执行;
3、finally 是在 return 后面的表达式运算后执行的(此时并没有返回运算后的值,而是先把要返回的值保存起来,不管 finally 中的代码怎么样,返回的值都不会改变,任然是之前保存的值),所以函数返回值是在 finally 执行前确定的;
4、finally 中最好不要包含 return,否则程序会提前退出,返回值不是 trycatch 中保存的返回值。

参考资料: 重学前端学习笔记(二十)–try 里面放 return,finally 还会执行吗?

JavaScript 如何实现异步编程

1.回调函数
但会导致两个问题:
缺乏顺序性: 回调地狱导致的调试困难,和大脑的思维方式不符
缺乏可信任性: 控制反转导致的一系列信任问题
2.Promise
Promise 是基于 PromiseA+规范的实现,它很好的解决了控制反转导致的信任问题,将代码执行的主动权重新拿了回来。
3.生成器函数 Generator
使用 Generator,可以让我们用同步的方式来书写代码,解决了顺序性的问题,但是需要手动去控制 next(...),将回调成功返回的数据送回 JavaScript 主流程中。
4.Async/Await
Async/Await 结合了 PromiseGenerator,在 await 后面跟一个 Promise,它会自动等待 Promise 的决议值,解决了 Generator 需要手动控制 next(...)执行的问题,真正实现了用同步的方式书写异步代码。
5.发布订阅模式
JavaScript 中应用非常广泛,比如一些前端框架比如 React,Vue 等,都有使用这一设计模式,nodejs 使用的就更多了。
使用发布订阅模式的好处是事件集中管理,修改方便,缺点就是,代码可读性下降,事件容易冲突。

简单实现

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
//async_Event.js

//单对象写法 Event 就相当于事件中心
const Event = (function() {
//使用闭包的好处 : 把EventPool私有化,外界无法访问EventPool
const EventPool = new Map() //使用es6 map来存 event,callback 键值对
const isFunction = func => typeof func === 'function'

const on = (event, callback) => {
//注册事件
EventPool.get(event) || EventPool.set(event, [])
if (isFunction(callback)) {
EventPool.get(event).push(callback)
} else {
throw new Error('callback not is function')
}
}
const addEventListenr = (event, callback) => {
//on方法别名
on(event, callback)
}
const emit = (event, ...args) => {
//触发(发布)事件
//让事件的触发为一个异步的过程,即排在同步代码后执行
//也可以setTimeout(fn,0)
Promise.resolve().then(() => {
let funcs = EventPool.get(event)
if (funcs) {
funcs.forEach(f => f(...args))
} else {
throw new Error(`${event} not register`)
}
})
}
const send = (event, ...args) => {
//emit方法别名
emit(event, ...args)
}
const removeListener = event => {
//删除事件
Promise.resolve(() => {
//删除事件也为异步的过程
if (event) {
EventPool.delete(event)
} else {
throw new Error(`${event} not register`)
}
})
}

return {
on,
emit,
addEventListenr,
send
}
})()

简单使用

1
2
3
4
5
6
7
8
9
//注册事件,名为event
Event.on('event', data => {
      console.log(data)
    });
setTimeout(() => {
      Event.emit('event','hello wrold')
    },1000);

//1s后触发事件,输出hello world

宏任务和微任务分别有哪些

宏队列,macrotask,也叫 tasks。 一些异步任务的回调会依次进入 macro task queue,等待后续被调用,这些异步任务包括:

1
2
3
4
5
6
setTimeout
setInterval
setImmediate (Node独有)
requestAnimationFrame (浏览器独有)
I/O
UI rendering (浏览器独有)

微队列,microtask,也叫 jobs。 另一些异步任务的回调会依次进入 micro task queue,等待后续被调用,这些异步任务包括:

1
2
3
4
process.nextTick (Node独有)
Promise
Object.observe
MutationObserver

使用 Promise 实现串行

1
2
3
4
5
6
7
8
9
10
11
// 一个 promise 的 function
function delay(time) {
return new Promise((resolve, reject) => {
console.log(`wait ${time}s`)
setTimeout(() => {
console.log('execute')
resolve()
}, time * 1000)
})
}
const arr = [3, 4, 5]

1.reduce

1
2
3
arr.reduce((s, v) => {
return s.then(() => delay(v))
}, Promise.resolve())

2.async + 循环 + await

1
2
3
4
5
;(async function() {
for (const v of arr) {
await delay(v)
}
})()

3.普通循环

1
2
3
4
let p = Promise.resolve()
for (const i of arr) {
p = p.then(() => delay(i))
}

4.递归

1
2
3
4
5
function dispatch(i, p = Promise.resolve()) {
if (!arr[i]) return Promise.resolve()
return p.then(() => dispatch(i + 1, delay(arr[i])))
}
dispatch(0)

EventLoop 是什么

event loop 是一个执行模型,在不同的地方有不同的实现。浏览器和 NodeJS 基于不同的技术实现了各自的 Event Loop

浏览器的 Event Loop

这张图将浏览器的 Event Loop 完整的描述了出来,我来讲执行一个 JavaScript 代码的具体流程:

1
2
3
4
5
6
7
8
9
10
1.执行全局Script同步代码,这些同步代码有一些是同步语句,有一些是异步语句(比如setTimeout等);
2.全局Script代码执行完毕后,调用栈Stack会清空;
3.从微队列microtask queue中取出位于队首的回调任务,放入调用栈Stack中执行,执行完后microtask queue长度减1;
4.继续取出位于队首的任务,放入调用栈Stack中执行,以此类推,直到直到把microtask queue中的所有任务都执行完毕。注意,如果在执行microtask的过程中,又产生了microtask,那么会加入到队列的末尾,也会在这个周期被调用执行;
5.microtask queue中的所有任务都执行完毕,此时microtask queue为空队列,调用栈Stack也为空;
6.取出宏队列macrotask queue中位于队首的任务,放入Stack中执行;
7.执行完毕后,调用栈Stack为空;
8.重复第3-7个步骤;
9.重复第3-7个步骤;
10.......

这里归纳 3 个重点: 1.宏队列macrotask一次只从队列中取一个任务执行,执行完后就去执行微任务队列中的任务; 2.微任务队列中所有的任务都会被依次取出来执行,知道microtask queue为空; 3.图中没有画UI rendering的节点,因为这个是由浏览器自行判断决定的,但是只要执行UI rendering,它的节点是在执行完所有的microtask之后,下一个macrotask之前,紧跟着执行UI render

测试一下你是否掌握了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
console.log(1)

setTimeout(() => {
console.log(2)
Promise.resolve().then(() => {
console.log(3)
})
})

new Promise((resolve, reject) => {
console.log(4)
resolve(5)
}).then(data => {
console.log(data)
})

setTimeout(() => {
console.log(6)
})

console.log(7)

这里结果会是什么呢?运用上面了解到的知识,先自己做一下试试看。

1
2
3
4
5
6
7
8
// 正确答案
1
4
7
5
2
3
6

参考资料: 带你彻底弄懂 Event Loop

如何解决页面加载海量数据而页面不卡顿

1.分治思想,在一定的时间内多次加载数据,直至渲染完成,使用 window.requestAnimationFramedocument.createDocumentFragment() 实现

2.局部显示,毕竟用户能看到的就一屏内容,监听用户的滚动行为,改变显示元素,可使 DOM 结构最简单化

题目:页面上有个空的无序列表节点 ul ,其 id 为 list-with-big-data ,现需要往列表插入 10w 个 li ,每个列表项的文本内容可自行定义,且要求当每个 li 被单击时,通过 alert 显示列表项内的文本内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>页面加载海量数据</title>
</head>

<body>
<ul id="list-with-big-data">
100000 数据
</ul>
<script>
// 此处添加你的代码逻辑
</script>
</body>
</html>

解决方案

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
;(function() {
const ulContainer = document.getElementById('list-with-big-data')

// 防御性编程
if (!ulContainer) {
return
}

const total = 100000 // 插入数据的总数
const batchSize = 4 // 每次批量插入的节点个数,个数越多,界面越卡顿
const batchCount = total / batchSize // 批处理的次数
let batchDone = 0 // 已完成的批处理个数

function appendItems() {
// 使用 DocumentFragment 减少 DOM 操作次数,对已有元素不进行回流
const fragment = document.createDocumentFragment()

for (let i = 0; i < batchSize; i++) {
const liItem = document.createElement('li')
liItem.innerText = batchDone * batchSize + i + 1
fragment.appendChild(liItem)
}

// 每次批处理只修改 1 次 DOM
ulContainer.appendChild(fragment)
batchDone++
doAppendBatch()
}

function doAppendBatch() {
if (batchDone < batchCount) {
// 在重绘之前,分批插入新节点
window.requestAnimationFrame(appendItems)
}
}

// kickoff
doAppendBatch()

// 使用 事件委托 ,利用 JavaScript 的事件机制,实现对海量元素的监听,有效减少事件注册的数量
ulContainer.addEventListener('click', function(e) {
const target = e.target

if (target.tagName === 'LI') {
alert(target.innerText)
}
})
})()