js执行机制

浏览器常驻线程

浏览器常驻的线程:

  1. js引擎线程(解释执行js、用户输入、网络请求)
  2. GUI线程(绘制用户界面、与js主线程是互斥的)
  3. http网络请求线程(处理用户的get、post等请求,等返回结果后将回调函数推入任务队列)
  4. 定时触发器线程(setTimeout、setInterval等待时间结束后把执行函数推入任务队列中)
  5. 浏览器事件处理线程(将click、mouse等交互事件发生后将这些事件放入事件队列中)

大部分浏览器中都是具有这5个线程的,这些线程是通过UI主线程进行协调运作的。

js引擎线程和GUI线程是互斥的
js可以操作DOM元素,进而会影响到GUI的渲染结果,因此js引擎线程与GUI渲染线程是互斥的。也就是说方js引擎线程处于运行状态时,GUI渲染线程将处于冻结状态。

js单线程

JavaScript是基于单线程运行的,同时又是可以异步执行的,一般来说这种既是单线程又是异步的语言都是基于事件来驱动的,恰好浏览器就给JavaScript提供了这么一个环境。

js主线程

上图表达的原理

  1. 同步和异步任务分别进入不同的执行”场所“,同步的进入主线程(执行栈),异步的进入Event Table并注册函数
  2. 当指定的事情完成时,Event Table会将这个函数移入Event Queue(任务队列)
  3. 主线程内的任务执行完毕,会去Event Queue读取对应的函数,进入主线程执行

上述过程会不断重复,也就是常说的Event Loop(事件轮询)。事件轮询与宏任务和微任务密切相关。

定时器

setTimeout的等待时间结束后并不是直接执行的,而是先进入浏览器的一个任务队列。在同步队列结束后再依次调用任务队列中的任务。
setTimeout(function(){},0):如果定义为0ms,是指当js主线程中的执行栈为空时,再将此异步队列中的任务放入主线程执行。但是0ms实际上是达不到的,根据html标准,最低是4ms。况且如果js主线程的执行时间过长,也会依次等待主线程执行完毕,所以此函数的等待时间要超过0ms。
setInterval是每隔一段时间把任务放到Event Queue中,执行机制同setTimeout

宏任务&微任务

在一个事件循环中,异步事件返回结果后会被放到一个任务队列中。然而,根据这个异步事件的类型,这个事件实际上会被对应的宏任务队列或者微任务队列中去。并且在当前执行栈为空的时候,主线程会查看微任务队列是否有事件存在。如果不存在,那么再去宏任务队列中取出一个事件并把对应的回到加入当前执行栈;如果存在,则会依次执行队列中事件对应的回调,直到微任务队列为空,然后去宏任务队列中取出最前面的一个事件,把对应的回调加入当前执行栈…如此反复,进入循环。

宏任务

常见宏任务:I/O、setTimeout、setInterval、setImmediate、requestAnimationFrame

宏任务
微任务

常见微任务:process.nextTick()、MutationObserver、Promise.then/catch/finally

微任务

【任务1】在主线程上添加宏任务与微任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 在主线程上添加宏任务与微任务 
// 执行顺序:主线程 => 主线程上创建的微任务 => 主线程上创建的宏任务
console.log('-------start--------');
setTimeout(() => {
console.log('setTimeout');
// 将回调代码放入另一个宏任务队列
}, 0);

new Promise((resolve, reject) => {
for (let i = 0; i < 5; i++) {
console.log(i);
}
resolve()
}).then(() => {
console.log('Promise实例成功回调执行')
})

// 将回调代码放入微任务队列
console.log('-------end--------');

输出结果:

1
2
3
4
5
6
7
-------start--------
0
1
2
3
4
-------end--------

【任务2】在微任务中创建微任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 在微任务中创建微任务 
// 执行顺序:主线程 => 主线程上创建的微任务1 => 微任务1上创建的微任务2 => 主线程上创建的宏任务
setTimeout(() => console.log(4))
new Promise(resolve => {
resolve()
console.log(1)
}).then(() => {
console.log(3)
Promise.resolve().then(() => {
console.log('before timeout')
}).then(() => {
Promise.resolve().then(() => {
console.log('also before timeout')
})
})
})
console.log(2)

输出结果:

1
2
3
4
5
6
1
2
3
before timeout
also before timeout
4

【任务3】在宏任务中创建微任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 宏任务中创建微任务 // 执行顺序:主线程 => 主线程上的宏任务队列1 => 宏任务队列1中创建的微任务 // 宏任务队列 1 
setTimeout(() => {
// 宏任务队列 2.1
console.log('timer_1');
setTimeout(() => {
// 宏任务队列 3
console.log('timer_3')
}, 0)
new Promise(resolve => {
resolve()
console.log('new promise')
}).then(() => { // 微任务队列 1
console.log('promise then')
})
}, 0)
setTimeout(() => { // 宏任务队列 2.2
console.log('timer_2')
}, 0)
console.log('========== Sync queue ==========')

输出结果:

1
2
3
4
5
6
========== Sync queue ==========
timer_1
new promise
promise then
timer_2
timer_3

【任务4】在微任务中创建宏任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 微任务队列中创建的宏任务 // 执行顺序:主线程 => 主线程上创建的微任务 => 主线程上创建的宏任务 => 微任务中创建的宏任务 // 宏任务1 
new Promise((resolve) => {
console.log('new Promise(macro task 1)');
resolve();
}).then(() => {
// 微任务1
console.log('micro task 1');
setTimeout(() => { // 宏任务3
console.log('macro task 3');
}, 0)
})
setTimeout(() => { // 宏任务2
console.log('macro task 2');
}, 1000)
console.log('========== Sync queue(macro task 1) ==========');

输出结果:

1
2
3
4
5
new Promise(macro task 1)
========== Sync queue(macro task 1) ==========
micro task 1
macro task 3
macro task 2

【任务5】综合

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
console.log('======== main task start ========');
new Promise(resolve => {
console.log('create micro task 1');
resolve();
}).then(() => {
console.log('micro task 1 callback');
setTimeout(() => {
console.log('macro task 3 callback');
}, 0);
})
console.log('create macro task 2');
setTimeout(() => {
console.log('macro task 2 callback');
new Promise(resolve => {
console.log('create micro task 3');
resolve();
}).then(() => {
console.log('micro task 3 callback');
})
console.log('create macro task 4');
setTimeout(() => {
console.log('macro task 4 callback');
}, 0);
}, 0);
new Promise(resolve => {
console.log('create micro task 2');
resolve();
}).then(() => {
console.log('micro task 2 callback');
})
console.log('======== main task end ========');

输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
======== main task start ========
create micro task 1
create macro task 2
create micro task 2
======== main task end ========
micro task 1 callback
micro task 2 callback
macro task 2 callback
create micro task 3
create macro task 4
micro task 3 callback
macro task 3 callback
macro task 4 callback

call、apply手写实现与应用

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
// 手写call 
Function.prototype.myCall = function() {
let ctx = arguments[0] || window
ctx.fn = this
let args = []
for (let i = 1; i < arguments.length; i++) {
args.push(`arguments[${i}]`)
}
let result = eval(`ctx.fn(${args.join(',')})`)
delete ctx.fn
return result
}

// 手写apply
Function.prototype.myApply = function(ctx, arr) {
ctx = ctx || window
ctx.fn = this
let result = null
if (!arr) {
result = ctx.fn()
} else {
let args = []
for (let i = 0; i < arr.length; i++) {
args.push(`arr[${i}]`)
}
result = eval(`ctx.fn(${args.join(',')})`)
}
delete ctx.fn
return result
}

// 开始测试
let value = 'window'
let obj = {
value: 'obj'
}

function show(name, age) {
console.log(this.value);
return {
name,
age
}
}
console.log(show.myCall(obj, 'ypp', 12));
console.log(show.myApply(obj, ['ypp', 12]));

输出结果:

1
2
3
4
obj
{ name: 'ypp', age: 12 }
obj
{ name: 'ypp', age: 12 }

函数柯里化

在数学和计算机科学中,柯里化是一种将使用多个参数的一个函数转化为一系列使用一个参数的函数的技术。
前端使用柯里化的用途主要是简化代码结构,提高系统的维护性,一个方法只有一个参数,强制了功能的单一性。很自然就做到了功能内聚,降低了耦合性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function add() {
let arr = Array.from(arguments)
return arr.reduce((pre, cur) => {
return pre + cur
}, 0)
}

function FixedCurry(fn) {
// 这里的arguments是[add, 1, 2]
let _args = Array.prototype.slice.call(arguments, 1)
//相当于[].call(arguments,1)
return function() {
// 这里的arguments是[2, 3]
let newArgs = _args.concat([].slice.call(arguments, 0))
//这里的 newArgs相当于完成了所有参数的拼接 [1,2,2,3]
return fn.apply(this, newArgs)
}
}
let newAdd = FixedCurry(add, 1)
console.log(newAdd(1, 2));
console.log(newAdd(3));
console.log(newAdd(4));

输出结果:

1
2
3
4
4
5
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
function add() {
let arr = Array.from(arguments)
return arr.reduce((pre, cur) => {
return pre + cur
}, 0)
}

function FixedCurry(fn) { // 这里的arguments是[add, 1, 2]
let _args = Array.prototype.slice.call(arguments, 1) //相当于[].call(arguments,1)
return function() { // 这里的arguments是[2, 3]
let newArgs = _args.concat([].slice.call(arguments, 0)) //这里的 newArgs相当于完成了所有参数的拼接 [1,2,2,3]
return fn.apply(this, newArgs)
}
}
// 要实现的效果
// let newAdd = Curry(add) // newAdd(1, 2, 3, 4) // newAdd(1)(2)(3)(4) // newAdd(1, 2)(3)(4) // ...
function Curry(fn, length) {
length = length || fn.length //fn.length代表fn函数需要传入的参数个数
return function() {
if (arguments.length < length) {
// combined=[fn,1,2,3,...]
let combined = [fn].concat([].slice.call(arguments, 0)) // 递归
return Curry(FixedCurry.apply(this, combined), length - arguments.length)
} else { // 传入的参数个数达到要求时,执行函数
return fn.apply(this, arguments)
}
}
}
let newAdd = Curry(add, 4)
console.log(newAdd(1, 2, 3, 4));
console.log(newAdd(1)(20)(3)(4));
console.log(newAdd(1, 22)(3)(4));

输出结果:

1
2
3
10
28
30

在ajax请求中应用函数curry化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function ajax(type, url, data) {
let xhr = new XMLHttpRequest()
xhr.open(type, url, true)
xhr.send(data)
}

const GET = 'get'
const POST = 'post'
ajax(POST, 'www.test1.com', "age=12")
ajax(POST, 'www.test2.com', "age=20")
ajax(POST, 'www.test23com', "age=23")

let ajaxCurry = curry(ajax)

let post = ajaxCurry(POST)
post('www.test1.com', 'age=12')

let postFromTest = post('www.test1.com')
postFromTest('age=12')
ajax curry

数据扁平化

数据扁平化是指将多维的数组压成一维的数组,不存在数组嵌套的问题。

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
// 1、方法一 
// 初始版本
function flatten(arr = []) {
let resArr = []
let len = arr.length
for (let i = 0; i < len; i++) {
if (Array.isArray(arr[i])) {
resArr = resArr.concat(flatten(arr[i]))
//递归
} else {
resArr.push(arr[i])
}
}
return resArr
}

// 挂载原型链上
Array.prototype.flatten = function() {
let resArr = []
this.forEach(item => {
Array.isArray(item) ? resArr = resArr.concat(item.flatten()) : resArr.push(item)
})
return resArr
}

// 2、方法二
// reduce实现(精简)
function flatten(arr = []) {
return arr.reduce((prev, cur) => {
return Array.isArray(cur) ? prev.concat(flatten(cur)) : prev.concat(cur)
}, [])
}
// 简化写法
const flatten = arr => arr.reduce((prev, cur) => {
return Array.isArray(cur) ? prev.concat(flatten(cur)) : prev.concat(cur)
}, [])

惰性函数

惰性函数是指在函数调用后,改变函数的实现,这样就不需要在每次调用此函数时都去根据条件选择具体的实现,有几个典型的应用场景:

  1. 用户vip会员判断(一次判断后固定优惠策略)
  2. 浏览器内核检测
  3. 事件绑定函数封装
  4. ……

示例代码如下:

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
// 场景1:永远打印第一次调用时的时间 
let printTime = (function() {
let t = null
return function() {
if (t)
return t
t = new Date().getTime()
return t
}
})()

// 以上版本有个问题,下次再调用时依次会走判断逻辑
let printTime = function() {
let t = new Date().getTime()
test = function() {
return t
}
return test
}

// 场景2:事件绑定方法邓庄
function addEvent(dom, type, handler) {
if (dom.addEventListener) {
dom.addEventListener(type, handler, false)
addEvent = function(dom, type, handler) {
dom.addEventListener(type, handler, false)
}
} else {
dom.attachEvent('on' + type, handler)
addEvent = function() {
dom.attachEvent('on' + type, handler)
}
}
}

函数组合

函数组合是指将多个函数的实现,糅合到一个函数(功能组合),示例如下:

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
// 古老版本,但是如果传入的是很多个函数呢? 
function comose(f, g) {
return function(x) {
return f(g(x))
}
}
// 依次传入很多个函数f1,f2,f3,f4...,要返回一个组合函数f1(f2,f3,f4(x))
function compose() {
let args = Array.prototype.slice.call(arguments) //变为数组
let idx = args.length - 1
return function(x) {
let result = args[idx](x)
while (idx--) {
result = args[idx](result)
}
return result
}
}
// 进一步优化
function compose() {
let args = Array.prototype.slice.call(arguments) //变为数组
return function(x) {
return args.reduceRight((prev, cur) => {
return cur(prev)
}, x)
}
}
// 进一步简化
const compose = (...args) => x => args.reduceRight((prev, cur) => {
return cur(prev)
}, x)

// 测试
const f = compose(fn1, fn2, fn3)
console.log(f('dferfes'));

纯函数

纯函数:对于相同的输入,永远的到相同的输出,而且没有任何可观察的副作用,也不依赖于外部环境的状态。
纯函数非常容易进行单元测试,因为不需要考虑上下文环境,只需要考虑输入和输出。
纯函数能更好地管理状态,使得可预测性增强,降低了代码管理难度,但是前端基本上都是在和副作用打交道,所有函数都是纯函数这种愿望不可强求~

函数记忆

函数记忆是针对需要重复计算和获取值的场景下设置的,类似于动态规划的思想,将每一个状态的前一个状态记录下来,这样可以加速运算~

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
let count = 0,
cache = []

function fn(n) {
if (cache[n]) {
return cache[n]
} else { //
if (n === 0 || n === 1) {
cache[0] = 1
cache[1] = 1
return 1
} //
cache[n] = n * fn(n - 1)
return cache[n]
}
}
// 优化,缓存应该是私有的
function memorize(fn) {
let cache = {} //对象的查找速度比数组更快
return function() { // 保证key唯一
let key = arguments.length + Array.prototype.join.call(arguments)
if (cache[key]) {
return cache[key]
} else {
cache[key] = fn.apply(arguments)
return cache[key]
}
}
}

防抖和节流

防抖

防抖:在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。

  1. search搜索联想,用户在不断输入值时,用防抖来节约请求资源。
  2. window触发resize的时候,不断的调整浏览器窗口大小会不断的触发这个事件,用防抖来让其只触发一次
    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
    // 防抖:非立即执行版
    function debounce1(func, wait) {
    let timeout;
    return function() {
    let context = this;
    let args = arguments;
    if (timeout) clearTimeout(timeout);
    timeout = setTimeout(() => {
    func.apply(context, args)
    }, wait);
    }
    } // 防抖:立即执行版
    function debounce2(func, wait) {
    let timeout;
    return function() {
    let context = this;
    let args = arguments;
    if (timeout) clearTimeout(timeout);
    let callNow = !timeout;
    timeout = setTimeout(() => {
    timeout = null;
    }, wait)
    if (callNow) func.apply(context, args)
    }
    }
    // 防抖:双剑合璧版
    /**
    *
    * @desc 函数防抖
    * @param func 函数
    * @param wait 延迟执行毫秒数
    * @param immediate 立即执行 true 表立即执行,false 表非立即执行
    */
    function debounce3(func, wait, immediate) {
    let timeout;
    return function() {
    let context = this;
    let args = arguments;
    if (timeout) clearTimeout(timeout);
    if (immediate) {
    var callNow = !timeout;
    timeout = setTimeout(() => {
    timeout = null;
    }, wait)
    if (callNow) func.apply(context, args)
    } else {
    timeout = setTimeout(function() {
    func.apply(context, args)
    }, wait);
    }
    }
    }

节流

节流:规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。

  1. 鼠标不断点击触发,mousedown(单位时间内只触发一次)
  2. 监听滚动事件,比如是否滑到底部自动加载更多,用throttle来判断
    页面频繁点击
    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
    // 节流:时间戳版 
    function throttle(func, wait) {
    let previous = 0;
    return function() {
    let now = Date.now();
    let context = this;
    let args = arguments;
    if (now - previous > wait) {
    func.apply(context, args);
    previous = now;
    }
    }
    }
    // 节流:定时器版
    function throttle(func, wait) {
    let timeout;
    return function() {
    let context = this;
    let args = arguments;
    if (!timeout) {
    timeout = setTimeout(() => {
    timeout = null;
    func.apply(context, args)
    }, wait)
    }
    }
    }
    // 节流:双剑合璧版
    /**
    * @desc 函数节流
    * @param func 函数
    * @param wait 延迟执行毫秒数
    * @param type 1 表时间戳版,2 表定时器版
    */
    function throttle(func, wait, type) {
    if (type === 1) {
    let previous = 0;
    } else if (type === 2) {
    let timeout;
    }
    return function() {
    let context = this;
    let args = arguments;
    if (type === 1) {
    let now = Date.now();
    if (now - previous > wait) {
    func.apply(context, args);
    previous = now;
    }
    } else if (type === 2) {
    if (!timeout) {
    timeout = setTimeout(() => {
    timeout = null;
    func.apply(context, args)
    }, wait)
    }
    }
    }
    }