hybrid-native开发或者reat-native开发时,不可避免涉及到js脚本编写。对于大前端来说,对于es6/es7的语法熟悉是必备素质。这里介绍一个前端常用的工具库lodash。
遇到的问题
今天在优化大搜索页面时碰到一个问题,feedback返回用户反馈搜索页面卡顿。先定位到原先的代码:是在 < TextInput > 的onchange回调中,请求suggest接口返回模糊搜索结果。但是在IOS设备上会有一个问题:键盘输入拼音时系统会把未输入完的结果录到input组建中,导致onchange回调多次调用,suggest接口频繁请求,hint内容不断刷新,造成页面卡顿的同时,也增加了接口的负担。
优化点
经过分析,我认为用户使用模糊搜索具备 幂等性 .即在一段时间内用户输入的key word应返回相同的suggest。不应频繁调用接口。
基于以上思想,很自然想到setTimeout()函数,用户输入停止后设置一个延时再请求网络,但是直接在回调里setTimeout这个做法十分hardcode。于是逛了一下万能的gayHub,发现了一个不错的解决方案:使用debounce函数去除抖动。
lodash介入
这里的debounce函数属于鼎鼎大名的lodash库(https://github.com/lodash/lodash)
debounce函数的官方文档
debounce 与 throttle
debounce(防抖):当调用函数n秒后,才会执行该动作,若在这n秒内又调用该函数则将取消前一次并重新计算执行时间,举个简单的例子,我们要根据用户输入做suggest,每当用户按下键盘的时候都可以取消前一次,并且只关心最后一次输入的时间就行了。
throttle(节流):将一个函数的调用频率限制在一定阈值内,例如 1s 内一个函数不能被调用两次。
这里拿出debounce的源码解析一下: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
126function debounce(func, wait, options) {
let lastArgs,
lastThis,
maxWait,
result,
timerId,
lastCallTime
// 参数初始化
let lastInvokeTime = 0 // func 上一次执行的时间
let leading = false
let maxing = false
let trailing = true
// 基本的类型判断和处理
if (typeof func != 'function') {
throw new TypeError('Expected a function')
}
wait = +wait || 0
if (isObject(options)) {
// 对配置的一些初始化
}
function invokeFunc(time) {
const args = lastArgs
const thisArg = lastThis
lastArgs = lastThis = undefined
lastInvokeTime = time
result = func.apply(thisArg, args)
return result
}
function leadingEdge(time) {
// Reset any `maxWait` timer.
lastInvokeTime = time
// 为 trailing edge 触发函数调用设定定时器
timerId = setTimeout(timerExpired, wait)
// leading = true 执行函数
return leading ? invokeFunc(time) : result
}
function remainingWait(time) {
const timeSinceLastCall = time - lastCallTime // 距离上次debounced函数被调用的时间
const timeSinceLastInvoke = time - lastInvokeTime // 距离上次函数被执行的时间
const timeWaiting = wait - timeSinceLastCall // 用 wait 减去 timeSinceLastCall 计算出下一次trailing的位置
// 两种情况
// 有maxing:比较出下一次maxing和下一次trailing的最小值,作为下一次函数要执行的时间
// 无maxing:在下一次trailing时执行 timerExpired
return maxing
? Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
: timeWaiting
}
// 根据时间判断 func 能否被执行
function shouldInvoke(time) {
const timeSinceLastCall = time - lastCallTime
const timeSinceLastInvoke = time - lastInvokeTime
// 几种满足条件的情况
return (lastCallTime === undefined //首次
|| (timeSinceLastCall >= wait) // 距离上次被调用已经超过 wait
|| (timeSinceLastCall < 0) //系统时间倒退
|| (maxing && timeSinceLastInvoke >= maxWait)) //超过最大等待时间
}
function timerExpired() {
const time = Date.now()
// 在 trailing edge 且时间符合条件时,调用 trailingEdge函数,否则重启定时器
if (shouldInvoke(time)) {
return trailingEdge(time)
}
// 重启定时器,保证下一次时延的末尾触发
timerId = setTimeout(timerExpired, remainingWait(time))
}
function trailingEdge(time) {
timerId = undefined
// 有lastArgs才执行,意味着只有 func 已经被 debounced 过一次以后才会在 trailing edge 执行
if (trailing && lastArgs) {
return invokeFunc(time)
}
// 每次 trailingEdge 都会清除 lastArgs 和 lastThis,目的是避免最后一次函数被执行了两次
// 举个例子:最后一次函数执行的时候,可能恰巧是前一次的 trailing edge,函数被调用,而这个函数又需要在自己时延的 trailing edge 触发,导致触发多次
lastArgs = lastThis = undefined
return result
}
function cancel() {}
function flush() {}
function pending() {}
function debounced(...args) {
const time = Date.now()
const isInvoking = shouldInvoke(time) //是否满足时间条件
lastArgs = args
lastThis = this
lastCallTime = time //函数被调用的时间
if (isInvoking) {
if (timerId === undefined) { // 无timerId的情况有两种:1.首次调用 2.trailingEdge执行过函数
return leadingEdge(lastCallTime)
}
if (maxing) {
// Handle invocations in a tight loop.
timerId = setTimeout(timerExpired, wait)
return invokeFunc(lastCallTime)
}
}
// 负责一种case:trailing 为 true 的情况下,在前一个 wait 的 trailingEdge 已经执行了函数;
// 而这次函数被调用时 shouldInvoke 不满足条件,因此要设置定时器,在本次的 trailingEdge 保证函数被执行
if (timerId === undefined) {
timerId = setTimeout(timerExpired, wait)
}
return result
}
debounced.cancel = cancel
debounced.flush = flush
debounced.pending = pending
return debounced
}
首次进入函数时因为 lastCallTime === undefined 并且 timerId === undefined,所以会执行 leadingEdge,如果此时 leading 为 true 的话,就会执行 func。同时,这里会设置一个定时器,在等待 wait(s) 后会执行 timerExpired,timerExpired 的主要作用就是触发 trailing。
如果在还未到 wait 的时候就再次调用了函数的话,会更新 lastCallTime,并且因为此时 isInvoking 不满足条件,所以这次什么也不会执行。
时间到达 wait 时,就会执行我们一开始设定的定时器timerExpired,此时因为time-lastCallTime < wait,所以不会执行 trailingEdge。
这时又会新增一个定时器,下一次执行的时间是 remainingWait,这里会根据是否有 maxwait 来作区分:
如果没有 maxwait,定时器的时间是 wait - timeSinceLastCall,保证下一次 trailing 的执行。
如果有 maxing,会比较出下一次 maxing 和下一次 trailing 的最小值,作为下一次函数要执行的时间。
最后,如果不再有函数调用,就会在定时器结束时执行 trailingEdge。
lodash其他常用函数:
遍历对象类型:
_.forEach(obj, (value, key) => { console.log(value) })
遍历和过滤的快捷方式:
从一组对象中摘取出某个属性的值:1
2
3
4
5let arr = [{ n: 1 }, { n: 2 }]
// ES6
arr.map((obj) => obj.n)
// Lodash
_.map(arr, 'n')
当对象类型的嵌套层级很多时,Lodash 的快捷方式就更实用了:1
2
3
4
5
6
7
8let arr = [
{ a: [ { n: 1 } ]},
{ b: [ { n: 1 } ]}
]
// ES6
arr.map((obj) => obj.a[0].n) // TypeError: 属性 'a' 在 arr[1] 中未定义
// Lodash
_.map(arr, 'a[0].n') // => [1, undefined]
可以看到,Lodash 的快捷方式还对 null 值做了容错处理。此外还有过滤快捷方式,以下是从 Lodash 官方文档中摘取的示例代码:1
2
3
4
5
6
7
8
9
10let users = [
{ 'user': 'barney', 'age': 36, 'active': true },
{ 'user': 'fred', 'age': 40, 'active': false }
];
// ES6
users.filter((o) => o.active)
// Lodash
_.filter(users, 'active')
_.filter(users, ['active', true])
_.filter(users, {'active': true, 'age': 36})
链式调用和惰性求值:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18et lines = `
an apple orange the grape
banana an apple melon
an orange banana apple
`.split('\n')
_.chain(lines)
.flatMap(line => line.split(/\s+/))
.filter(word => word.length > 3)
.groupBy(_.identity)
.mapValues(_.size)
.forEach((count, word) => { console.log(word, count) })
// apple 3
// orange 2
// grape 1
// banana 2
// melon 1
解构赋值和箭头函数:
ES6 引入了解构赋值、箭头函数等新的语言特性,可以用来替换 Lodash: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// Lodash
_.head([1, 2, 3]) // => 1
_.tail([1, 2, 3]) // => [2, 3]
// ES6 解构赋值(destructuring syntax)
const [head, ...tail] = [1, 2, 3]
// Lodash
let say = _.rest((who, fruits) => who + ' likes ' + fruits.join(','))
say('Jerry', 'apple', 'grape')
// ES6 spread syntax
say = (who, ...fruits) => who + ' likes ' + fruits.join(',')
say('Mary', 'banana', 'orange')
// Lodash
_.constant(1)() // => 1
_.identity(2) // => 2
// ES6
(x => (() => x))(1)() // => 1
(x => x)(2) // => 2
// 偏应用(Partial application)
let add = (a, b) => a + b
// Lodash
let add1 = _.partial(add, 1)
// ES6
add1 = b => add(1, b)
// 柯里化(Curry)
// Lodash
let curriedAdd = _.curry(add)
let add1 = curriedAdd(1)
// ES6
curriedAdd = a => b => a + b
add1 = curriedAdd(1)
一些参考信息:
10 Lodash Features You Can Replace with ES6
Does ES6 Mean The End Of Underscore / Lodash?