以下情景纯属虚构,如有雷同纯属巧合
初相见
这两天小刘被安排到一个项目(被老板pua中),遇到了这样一个问题:在用户登录的时候手抖点了两次,请求了两侧资源设置了两次token,但是真正完成的有效功能只有一次。
1 | let login = () => { |
第一次
本着大国工匠的精神,老板亲自催促小刘优化这个问题
于是小刘把它做成了下面这样
1 | let delay = 500 |
不错,很棒,每次点击如果和上一次相差不到0.5秒,那么取消上一次的登录请求,并做一些提示。
但是,过了两天,小刘又遇到了同样的问题,用户的登出方法以及其他的每个按钮好像都需要类似的功能
要知道,小刘是个连骑电车都嫌拿着钥匙麻烦的人
他可不愿意每次都定义好几个delay
1 | let delay = 500 |
闭包
突然,小刘灵光一闪,既然每份延迟都使用独立的delay和timeout,那我是不是可以用闭包记住上下文的特性,把这种类似的方法单独抽象出来?
1 | export function debounce(func, delay) { |
现在,小刘的代码成了这样子
1 | let login = () => { |
这样,就不会有那么多delay1.delay2.delay3.delay4……delayn了
小刘兴冲冲把他的代码提交了上去
再修改 —— 原型
另一位同事小文兴冲冲地把更新的代码拉了下来,看到了小刘的代码
然后他浑身一颤,想到了项目发展到最后可能出现这样的情况
1 | login = debounce(login, 500) |
每个方法执行防抖都要重新赋一个值,每个赋值都需要传入一个可能完全一样的delay
不仅如此,fn1被debounce处理之前的函数也丢失了!
这意味着我们如果想要使用未debounce的函数,就必须要重新定义一个,而且名字还不能一样。
事实上,小文不仅仅是个喝水都懒得张嘴的懒货,而且也是一个无可救药的 ”起名困难户“,
他绝对不能让这种事情发生
所以他想到,
能不能让每个方法都具有一个debounce的方法属性,调用debounce则执行
of course, it can!
1 | export function debounce(delay) { |
小文首先把debounce工具类函数改成了上面这个样子
然后又添加了这样一句话
1 | Function.prototype.debounce = debounce |
于是
1 | Function.prototype.debounce = debounce |
这样就可以在不影响原始方法的情况下使用它的debounce拉,而且还不需要传delay值!
1 | if (!delay) delay = 500; //如果delay不存在(null、undefined、0...) 就默认为500 |
需要注意的是,将 debounce
函数添加到 Function.prototype
上并不是一个好的实践,因为它会改变所有函数的行为,可能导致其他依赖于 Function.prototype
的代码出现问题。
此处仅仅是一个示范,如果没有严谨的文档和规约不建议在项目中使用
特殊的环境
不久,小文就被优化了,他的代码流到了一位更年轻,技术更好的小刀身上,
小刀是一位资深的java工程师,非常喜欢用类、对象表示一切
不久,他就发现了这样一个问题:
1 | // eslint-disable-next-line no-extend-native |
当使用user里的sayHi调用debounce时,会报错!
1 | Uncaught TypeError: Cannot convert undefined or null to object |
小文刀当即力断,立刻反应出来了问题在哪,然后他把debounce改建成了如下这样
1 | export function debounce(delay) { |
不过可惜,他对javascript this机制的了解还是不是很深
深谙javascript八股之道的朋友可能已经发现了,这段代码依旧会报错!
以下是原因
- 在调用user.sayHi时才会计算this的值
- 调用过程为: (user.sayHi).debounce()()
,在user.sayHi时,this的值并没有成为”user“,而且sayHi也不是一个词法环境,所以在调用debounce,形成闭包并返回函数后,给env赋值的this时undefined
所以 ….
当然,前面说过,小刀技术水平还不错,于是他很自然地想到了,把需要成为this的环境传进去,当然,也需要兼容不需要this的函数
1 | export function debounce(delay, env) { |
然而,沉溺在修改代码的忙碌中,小刀并没有意识到可以这么做….
1 | <button onClick={user.sayHi.bind(user).debounce(500)}> |
bind会返回一个绑定了user的函数
apply则是返回绑定了user的函数执行的结构
结尾
说实话,在写这篇文章之前,我只准确地判断了了原型及之前的结果,对于user.sayHi.debouce的运行结果,我理所当然认为也是可以那么运行的。
除去对this的分析之外,还有一点react的小小的收获
1 | let user = { |
在这段代码中调用onClick,是会报错的,因为此处的user.sayHi是一个”函数名“,React会把user.sayHi赋值给onClick,然后再执行onClick()
但是,仔细一想,其实这种逻辑我早就应该知道,如下
1 | let user = { |
onClick的赋值正是第一种,
解决方法,外部包裹一个箭头函数或者function匿名函数即可,但是要注意的是这种方法对于要调用debounce的onClick却行不通,
1 | <button onClick={()=>{ |
因为赋值给onClick的是箭头函数,而不是debounce,所以,每次调用onClick的时候都会重新创建一个debounce
顺便说一句,本来是想用ts写这篇文章的,但是在this赋值的时候会报一堆错,于是改用js
想起来前些日子看到了一篇文章 ”get out typescript“ 好像题目如下,抨击了ts丢失了js最显著的优点:”灵活性“
现在看来,确实如此