ninohwang / blog-2022 Goto Github PK
View Code? Open in Web Editor NEWit`s blog
it`s blog
debouce(去抖),throttle (节流/降频) 的实现和应用场景是对 js 基础的常用考察点之一。
先简单概括下(已知的/实践过的)应用场景包括不限于:
window.resize 的事件响应处理
:root
根元素 fontSize
使用 js 动态计算;此时可以通过 throttle 处理让根元素的用于 rem 参考的字号大小的计算更加低频一些;输入框关联搜索结果,如果不想立即响应用户输入,搜索结果,可以使用 debounce / 或者 throttle 两个交互效果细微差别的搜索处理
页面滚动距离的记忆
总的来说,一大应用场景,就是对原本高频触发的事件降低其响应,通过 debouce / throttle “返回的目的函数的内部开关”,虽然目的函数本身也一直是高频调用的,但是内部开关过滤了大部分原本被包装函数的高频事件下的执行逻辑。
class FuncUtils {
static #innerDebounce(func, ms, ctx, isDebounceMode = true) {
let timer = 0;
return (...args) => {
if (timer) {
if (isDebounceMode) {
clearTimeout(timer);
} else {
return;
}
}
timer = setTimeout(() => {
func.apply(ctx, args);
// clearTimeout(timer); // 已无必要清空自执行过的计时器
timer = 0;
}, ms);
};
}
static debounce(func, ms, ctx = null) {
return FuncUtils.#innerDebounce(func, ms, ctx, true);
}
static throttle(func, ms, ctx = null) {
return FuncUtils.#innerDebounce(func, ms, ctx, false);
}
static throttleVer2(func, ms, ctx = null) {
let prevTime = 0;
return (...args) => {
const curTime = new Date();
if (prevTime - curTime < ms) {
return;
}
func.apply(ctx, args);
prevTime = curTime;
};
}
}
以函数工具库 lodash 为例,
Arguments
func (Function): The function to debounce.
[wait=0] (number): The number of milliseconds to delay.
[options={}] (Object): The options object.
[options.leading=false] (boolean): Specify invoking on the leading edge of the timeout.
[options.maxWait] (number): The maximum time func is allowed to be delayed before it's invoked.
[options.trailing=true] (boolean): Specify invoking on the trailing edge of the timeout.
TODO
写下这些主要还是为了提醒下自己:
步入社会多年(虽然半路转行前端,根基不牢),不能总还留着学生思维,具体是学生思维的哪方面特点呢?这里可能没法描述得太清晰准确——类似于常常觉得怎么才算学会了/掌握了呢,一定是能“闭卷考试”下,独立实现才算,提前看了“答案”就不能算了,就掺了水分了,类似于这样的思维吧。
过往自己实现过 useDebounce / useThrottle
,虽然当时也是跟着个入门视频课程来的。视频课的老师讲得深入浅出,不过当时的个人随堂笔记却做得很差,各笔记的整理工作也不到位,现在回头看已是“惨绝人寰的不堪入目”。
这段时间,打算再稍微复习下一些常用功能级 hook 的内部实现时,一开始还是迷信于当时的笔记,一番功夫翻找出来,费半天时间去猜测当时的一些写法,包括如——
当时手写代码实现的这一行,为啥是这样写,为啥需要 funcRef = useRef(null); funcRef.current = targetFunc
来把要处理的函数作一个 ref 固定指向呢;为啥需要把 ms/delay 作为 hook 依赖项;这里简单贴一个当时简单实现的代码片段:
function useDebounce(func, ms) {
const ref = useRef()
ref.current = func
return useMemo(
() => FuncUtils.debounce((...args) => ref.current.apply(null, args), ms),
// () => FuncUtils.debounce(ref.current, ms),
[ms]
)
}
但是这种“迷信”有用吗?或者说有多大意义呢,实际上,视频课的实现可能也就是告诉听课的一个实现理念,但是多半不是考虑全面、考虑到种种边缘场景的“最佳实现”,所以个人情况为前提来说,还迷信一份当时的初入门视频教授、以及初入门时的笔记记录,算是很呆槑了。
牵扯出来现下的一些感受:react hook 的使用虽然对逻辑解耦,便于封装,支持高度自定义一些基础功能向,或者业务向的 hook,但是有时依赖数组的确定、以及指向的“缓存”固定,感觉还是有不小的心智负担。至少现阶段对我来说是如此的。
若果需要对 react hook 有个较全面的认识和使用,自己尝试实现一遍一定有着不会被忽视的意义。
配合着直接去看下知名 react hook 第三方库比如 useThrottle useDebounce 的健全实现,读懂别人的实现方式,然后如此再”照虎画猫“的实现一下,说不出啥特别升华的话,就是如此时间成本上会省上不少。当然也有所谓弊端,一开始**就是用上了别人的模具,从模仿学习出发,不过见仁见智。
后续实现去抖、节流的 react-hook,先读懂如下别人的实现再说:
https://github.com/alibaba/hooks
https://github.com/streamich/react-use
所谓“牢*太盛防肠断,风物长宜放眼量”。当下能自洽就是好的。
其实再回头看,问题不难,修复 bug 的改动也不多。
但是认识到函数单一作用的重要性,以及不同处理逻辑还是应该往解耦方向的好。
终端设备(运行安卓系统)的产品程序运行时,当用户登录后进入 /detail 相关详情页面,登录状态下的详情界面本身,会根据 web 端后台管理系统设置的“无操作返回首页”的特定时长(该功能亦可关闭),当页面一直无操作到预定时长后,页面即会退出登录,返回首页;
如上的,很贴近实际的一个需求场景。
因为是安卓对 web 网页的套壳程序;所以前人(项目开发及维护前辈)的实现,比如管理系统中设置无操作返回首页的时长为 1min,采取了将 除去最后 10s 的计时交由前端计时来处理,比如利用 globalThis.setInterval 来处理。
这样或许也是为了直接用上前端开发下更加方便的 UI 渲染;而对于 10s 之外的倒计时,则交由安卓系统来计算;比如如果是 1min,会先由安卓代码调用设备本身系统层面,倒计时 50s(同时,这个倒计时也没有在 web 页面上渲染体现),同理如果是 2min,则由安卓代码调用设备本身系统层面倒计时 110s。
以上是初步设计;这中间可能会引发这样一种思考,因为是以内部套壳的 apk 形式安装于终端设备。那么计时器实现方案的选择有啥讲究吗?
接下来的例子都以管理系统设置“无操作登出”为 1min 时长为例:
该 bug 即是: 当界面进入登录状态下的详情页,只要用户一点击,计时器立马初始化为页面可见的 10s 倒计时,且用户再次点击,并不是恢复到期望的“不可见的”60s 计时,而是又重置为 10s。(稍后会由代码片段佐证这种情况)
(ps: 前人代码的一些改动,很容易将人带入误区;或者说强耦合的代码逻辑让人云里雾里)
这当中,一开始还是纠结于因为如“背景”一节下的描述,一个 1min 计时功能却分别交由了前端 js 代码,和安卓端代码分别各自维护“一部分”计时器。
同时又因为各自维护,所以这其中又涉及到前端与安卓端的一些数据“通信往来”。
所以先思考了下,如果把计时器都交由纯前端或者纯安卓端负责呢?
问题1: 为何不选择纯前端的计时?
纯前端计时,通常需要考虑计时的偏差,如果使用 setInterval, 因为浏览器 js 执行环境单线程本质,“倒计时越久”,则可能误差越大。
当然也有其他可选的减小误差的方式,包括不限于:
问题2: 为何不选择纯安卓端的计时?
不擅长安卓端代码,这里只能猜测——纯安卓端实现,也意味着调用系统底层来支持。
这样一是倒计时的准确性得到了保障,不用操心纯前端下可能出现的单线程运行过程中一些“很重”的代码执行,“阻塞”了后续计时代码的执行;
二是性能上或也有保障,相较于前端来负责长计时来说。
但前人将最后 10s 的计时还是交给了前端 setInterval api,而不是全盘由安卓接手,可能是考虑了程序内部是 web 页面,利用前端 Api 可能更方便渲染,这一角度。
同时最终 10s 的短计时,setInterval 带来的误差也在可接受范围内。
实际的交互细节与用户期望:
当用户在计时过程中(以 1min 为例),无论是不可见的安卓代码负责的 50s 计时,还是页面渲染出的由前端代码负责的最后 10s 计时,当用户重新点击屏幕时,我们都希望这个计时会重新重置到设置的原点也即 60s;
而不是反复的重置到 10s;
或者计时一旦开始就不能中断。
function autoLogoutByTimer() {
setCou(10); // -> cou = 10
globalThis.clearInterval(globalThis.decreaseCouTimer || 0);
if (!isLoginStatus) {
return;
}
globalThis.decreaseCouTimer = globalThis.setInterval(funcToDecreaseCou, 1000);
}
window.autoLogoutByTimer = autoLogoutByTimer;
// 有 cou 初始值为 10;funcToDecreaseCou 对其自减 1 操作;当 cou 自减到 1 则触发“退出登录 + 返回首页”动作;这里并未体现 cou 是全局状态,不过意思就是 cou 可以由页面组件取值并渲染
someFetchDataFunc() {
timerSize = await fetchData(); // -> 1
setTimerSize(timerSize);
window.bridge.removeRunnable("autoLogoutByTimer");
timerSize && window.bridge.addRunnable("autoLogoutByTimer", (timerSize * 60 - 10) * 1000); // 这里可见,排除最后 10s 的逻辑
}
// window.brige 指向安卓全局
不过原先这里的 removeRunnable
与 addRunnable
都只调用了一次;实际这里仅调用一次的处理也是有问题的,后文提及。
而在安卓端,为了响应触摸事件:(注意:以下为原错误写法示例)
@Override
public void onUserInteraction() {
// ... other logic
loadUrl("javascript:autoLogoutByTimer()");
super.onUserInteraction();
}
如上,实际 bridge#removeRunnable
与 addRunnable
类似扮演着 js 端 window.setTimeout 这样的延迟调用功能。只是这个延迟调用功能交由了安卓端负责。
此时 bug 产生的直接原因,即是随着用户触摸屏幕、onUserInteraction 的调用,最终调用 autoLogoutByTimer
而重新初始化 storeState#cou = 10,重置 setInterval 计时器,如此也就出现:每次点击屏幕,计时器总是立即恢复到 10s 然后立马开始计时。
一开始试着排查,很快就转换了视角,基于安卓端 addRunnable removeRunnable 功能函数的正确实现为前提(这一点也找安卓同事认真确认),那么就当做是 js 的 setTimeout 来看待,这样也可以忽略安卓端代码与前端代码“通信”带来的干扰。
(虽然所谓通信也就是—— js 端通过 window.bridge.[androidFuncName]
调用安卓端代码,安卓端通过 loadUrl("javascript:" + jsFuncName + "()");
调用前端代码)
同时==从代码逻辑耦合层面,这个时候问题也就显现了 —— 原 autoLogoutByTimer 调用情况: 即是 bridge#addRunable 的延迟回调,又是 bridge#onUserInteraction 的立即调用==,逻辑冲突且混乱。
如前文提及,bridge#removeRunnable addRunnable
,也就是 setTimeout 的同个逻辑,那么这里的 60s 计时,必然也是可以、也应该反复重置的才行(而这让我不能理解,这样==反复重置==的逻辑没有在原代码里有所体现,当然也有做到反复重置的,即是随着 onUserInteraction
调用而反复直接调用 autoLogoutByTimer
,如此“部分重置”的错误效果)。
所以新建拟调用函数如下:
function runAlpha() {
setCou(0); // cou -> 0 (重置为 0 以让视图不再渲染倒计时数值)
globalThis.clearInterval(globalThis.decreaseCouTimer || 0);
try {
window.bridge.removeRunnable("autoLogoutByTimer");
storeState.timerSize && window.bridge.addRunnable("autoLogoutByTimer", (storeState.timerSize * 60 - 10) * 1000);
}
}
window.runAlpha = runAlpah;
这个函数runAlpha
,实际就是期望“正确时机下”调用该函数时,内部的bridge#removeRunnable addRunnable
在反复执行,重置“整个计时器”(而非仅仅重置最后 10s 的由前端维护的倒计时),而不是如原本代码只在拿到请求接口返回的 timerSize 时调用 addRunnable 一次就没有后续了;
不过一开始我被这样带入误区,想着 onUserInteraction
下是不断调用 autoLogoutByTimer
,所以 runAlpha
的调用我也是想着直接加到 autoLogoutByTimer
内部;
也即改动 1:
function autoLogoutByTimer() {
if (cou > 0 && cou < 10) {
runAlpha();
return;
}
setCou(10); // -> cou = 10
globalThis.clearInterval(globalThis.decreaseCouTimer || 0);
if (!isLoginStatus) {
return;
}
globalThis.decreaseCouTimer = globalThis.setInterval(funcToDecreaseCou, 1000);
}
如上的改动,实际还是把“开关”添加到函数内部,让一个函数承担越来越多的功能角色。但是如此,页面就表现为:
当倒计时到 0 到 10 时,此时触摸屏幕,调用 runAlpha ,此时屏幕上的 10s 计时取消,同时添加下个 50s 之后执行 autoLogoutByTimer —— 如此,好像 bug 解决了?但是实际上:
当再次点击屏幕时,因为此时 cou 为 0, 所以直接跳过 if (cou > 0 && cou < 10)
执行逻辑,直接又开启了 10s 的计时。
所以产生新的 bug ,就是:当界面第一次出现 10s 倒计时的顶部弹窗时,用户点击屏幕,间隔性出现:10s 倒计时成功取消(重新进入安卓端 50s 倒计时)、10s 倒计时再次直接出现;
@Override
public void onUserInteraction() {
// ... other logic
loadUrl("javascript:runAlpha()");
super.onUserInteraction();
}
最大体悟:函数功能性、逻辑应适当解耦。
回头看,问题如果有思路,一下就能解决,对本问题的定性也会是个简单级别的问题。所以无奈也反感写的东西太过冗长,但还是算是个人思考过程的完整记录。
另外,这个问题算是代码里的历史遗留问题,前辈的几次改动中,有时索性就注解如下:
@Override
public void onUserInteraction() {
// ... other logic
// loadUrl("javascript:autoLogoutByTimer()");
super.onUserInteraction();
}
如此,倒计时一旦初始化(安卓端 + 前端),也就不能再响应用户触摸中断重置了;
另外,这个 bug 修复工作先前也交给过一个安卓开发同事处理,但是他并没有解决,所以一开始也让我对这个 bug 修复难度有了错误估计。
昨天 (2022-8-2),搞了个 blog-issues 界面,突然就觉得有很多可以(需要)总结并上传的(微)主题文章。
于是乎今天上午突然创建了多个主题 issue,嗯,有些浮躁了。
所以不如优先放在这里。
意义重大,算是自己的第一篇“博客”,虽然“随意”的写在了仓库 issues 界面。
重度拖延症下,终于写上了自己心心念念、代表着好兆头的爬楼梯题目(的题解分享)。
人生如逆旅,但愿我们都是不断向上攀爬着的~
https://leetcode.com/problems/climbing-stairs/
function climbStairs(n) {
// if (n === 1) {
// return 1;
// }
// if (n === 2) {
// return 2;
// }
if (n < 3) {
return n;
}
return climbStairs(n - 1) + climbStairs(n - 2);
}
说明:思路正确,但是因为递归深度,以及大量重复计算,导致 leetcode 测试用例不通过
function climbStairs(n, memo=[1,1,2]) {
if (memo[n]) {
return memo[n];
}
return memo[n] = climbStairs(n - 1, memo) + climbStairs(n - 2, memo);
}
说明:为解法1 的优化版本,引入缓存数组.
不过,一开始忽略了缓存数组的固定指向,错误写为如下:
function climbStairs_ERR(n) {
const helperArr = [1,1,2];
if (helperArr[n]) {
return helperArr[n];
}
return helperArr[n] = climbStairs(n - 1) + climbStairs(n - 2);
}
function climbStairs(n) {
const cache = [1,1];
for (let i = 2; i <= n; i++) {
cache[i] = cache[i - 1] + cache[i - 2];
}
return cache[n];
}
说明:常规循环 & 菲波那切数列特征 & 动态规划
说明2:如果一开始并不知道其就是考查菲波那切数列的特定索引的值,那么如何才想出其内部规律(即 cache[i] = cache[i - 1] + cache[i - 2] ),以如下图示说明
function climbStairs(n) {
let ansCou = 0;
(function climb(step){
if (step === n) {
ansCou++;
return;
}
if (step > n) {
return;
}
climb(step + 1);
climb(step + 2);
})(0)
return ansCou;
}
说明:如此写法,初看有些晦涩;但是细看就是利用递归解决问题的巧妙思路;
解法参考 Eloquent Javascript 某章节关于 可以选择多次 乘 3 或加 5 或是 乘 5 或加 3 得到特定数字,一共多少种解法。
虽然也因为递归深度,导致不通过;但是写法可以学习借鉴下!
另外,如此的解法,也适合于比如一开始并没有发现如 解法 1 到解法 3 的到 n 个台阶的走法等于 ( 到 n - 1 个台阶的走法与 n - 2个台阶的走法之和)这样的规律(也即菲波那切数列的特征);
也算是一种暴力破解了。
另外,对于解法 4 ,也可以很顺利的用到 当有 3 种走法,如一次可以迈 1 或 2 或 3 个台阶。
// ...
climb(step + 1);
climb(step + 2);
climb(step + 3);
stackbliz 体验非常不错的在线开发沙盒。
在提供的 User settings 配置文件入口,
目前自定义更改的:
"editor.fontSize": 14,
"editor.wordWrap": "off",
作为经典第一题,也可以很多不同的优化解法。
var twoSumX = function(nums, target) {
let ans = [];
for (let i = 0; i < nums.length; i++) {
for (let j = i + 1; j < nums.length; j++) {
if (nums[i] + nums[j] === target) {
return [i, j]
}
}
}
};
说明: 常规思路 O(n2) 时间复杂度
function twoSum(nums, target) {
const map = new Map();
for (let i = 0, len = nums.length; i < len; i++) {
const par = target - nums[i];
const mayIdx = map.get(par);
if (map.has(par) && mayIdx !== i) {
return [i, mayIdx];
}
map.set(nums[i], i);
}
}
说明: 初始化并管理好一个缓存结构
🚧或许再看看?
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.