Giter Club home page Giter Club logo

blog-2022's People

Contributors

ninohwang avatar

Watchers

 avatar

blog-2022's Issues

[WEAK NOTE] 关联远程仓库

git push -u origin [branch-main]

经常在推本地分支关联远程分支时,有提示: git push --set-upstream origin [branch-name];
不过注意 github 上帮助提示的关联命令:git push -u origin main;

不一样的,但仔细一看,感觉像是短命令的别名;
所以敲下 git push --help
跳转到帮助页面;
可见:
image
git 常用常新;

去抖和节流的基本实现、应用场景、react-hook 实现

最前

debouce(去抖),throttle (节流/降频) 的实现和应用场景是对 js 基础的常用考察点之一。

先简单概括下(已知的/实践过的)应用场景包括不限于:

  • window.resize 的事件响应处理

    • 比如 rem 移动端布局,对于 :root 根元素 fontSize 使用 js 动态计算;此时可以通过 throttle 处理让根元素的用于 rem 参考的字号大小的计算更加低频一些;
      如此也明显减少了浏览器回流重绘的次数;
  • 输入框关联搜索结果,如果不想立即响应用户输入,搜索结果,可以使用 debounce / 或者 throttle 两个交互效果细微差别的搜索处理

    • debounce-防抖处理下,当用户一直处于输入状态下,不会随时响应“过滤”出搜索结果;而是当用户停止输入一段时间(当然会很短)后,才开始过滤本地、或者请求远程,并给出过滤结果;
    • throttle-节流处理下,用户输入过程中,也一直处理用户输入文本并过滤本地、或者请求远程,只是频率是周期性的。
    • 不过观察了下如 google / bing 搜索,倒是一直高频响应。输一个字符就立马发出一个搜索请求;
  • 页面滚动距离的记忆

    • 单页面应用中,前端路由跳转到下个页面,处理完毕后再返回上个页面,对滚动距离的保存让页面“回到”刚才上个页面的浏览位置;
      而对 scrollY 的保存,可以防抖或者节流处理下来保存。

总的认识

总的来说,一大应用场景,就是对原本高频触发的事件降低其响应,通过 debouce / throttle “返回的目的函数的内部开关”,虽然目的函数本身也一直是高频调用的,但是内部开关过滤了大部分原本被包装函数的高频事件下的执行逻辑。

正文

手写,简单实现

在线demo

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;
    };
  }
}

工具库健全 api

以函数工具库 lodash 为例,

#debouce

#throttle

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.

另起别处

去抖、节流的 react-hook 自封装

去抖、节流的 react-hook 自封装

正文

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 修复 - 函数功能性解耦 & 避免逻辑混乱

最前

其实再回头看,问题不难,修复 bug 的改动也不多。

但是认识到函数单一作用的重要性,以及不同处理逻辑还是应该往解耦方向的好。

背景

终端设备(运行安卓系统)的产品程序运行时,当用户登录后进入 /detail 相关详情页面,登录状态下的详情界面本身,会根据 web 端后台管理系统设置的“无操作返回首页”的特定时长(该功能亦可关闭),当页面一直无操作到预定时长后,页面即会退出登录,返回首页;

如上的,很贴近实际的一个需求场景。

因为是安卓对 web 网页的套壳程序;所以前人(项目开发及维护前辈)的实现,比如管理系统中设置无操作返回首页的时长为 1min,采取了将 除去最后 10s 的计时交由前端计时来处理,比如利用 globalThis.setInterval 来处理。

这样或许也是为了直接用上前端开发下更加方便的 UI 渲染;而对于 10s 之外的倒计时,则交由安卓系统来计算;比如如果是 1min,会先由安卓代码调用设备本身系统层面,倒计时 50s(同时,这个倒计时也没有在 web 页面上渲染体现),同理如果是 2min,则由安卓代码调用设备本身系统层面倒计时 110s。

以上是初步设计;这中间可能会引发这样一种思考,因为是以内部套壳的 apk 形式安装于终端设备。那么计时器实现方案的选择有啥讲究吗?

出现 bug

接下来的例子都以管理系统设置“无操作登出”为 1min 时长为例:

该 bug 即是: 当界面进入登录状态下的详情页,只要用户一点击,计时器立马初始化为页面可见的 10s 倒计时,且用户再次点击,并不是恢复到期望的“不可见的”60s 计时,而是又重置为 10s。(稍后会由代码片段佐证这种情况)

(ps: 前人代码的一些改动,很容易将人带入误区;或者说强耦合的代码逻辑让人云里雾里)

过程中

一些思考

这当中,一开始还是纠结于因为如“背景”一节下的描述,一个 1min 计时功能却分别交由了前端 js 代码,和安卓端代码分别各自维护“一部分”计时器。

同时又因为各自维护,所以这其中又涉及到前端与安卓端的一些数据“通信往来”。

所以先思考了下,如果把计时器都交由纯前端或者纯安卓端负责呢?

  • 问题1: 为何不选择纯前端的计时?
    纯前端计时,通常需要考虑计时的偏差,如果使用 setInterval, 因为浏览器 js 执行环境单线程本质,“倒计时越久”,则可能误差越大。
    当然也有其他可选的减小误差的方式,包括不限于:

    • setTimeout(func, delay) ,每次动态修正变量 delay 的值,以减小长时间倒计时产生的误差。
    • Web Worker ,依赖另开进程;(🎈暂未用过 may_link)
    • ... (🎈其他实践方案)
    • (以及可选项)在倒计时最开始时,作额外的精确对秒(当然这个多用于时间戳的精确显示);
  • 问题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 指向安卓全局

不过原先这里的 removeRunnableaddRunnable 都只调用了一次;实际这里仅调用一次的处理也是有问题的,后文提及。

而在安卓端,为了响应触摸事件:(注意:以下为原错误写法示例)

@Override
public void onUserInteraction() {
    // ... other logic
    loadUrl("javascript:autoLogoutByTimer()");
    super.onUserInteraction();
}

如上,实际 bridge#removeRunnableaddRunnable 类似扮演着 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:

  • 改动 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 倒计时再次直接出现;

正确改动

  • 改动 2
@Override
public void onUserInteraction() {
    // ... other logic
    loadUrl("javascript:runAlpha()");
    super.onUserInteraction();
}

最后

最大体悟:函数功能性、逻辑应适当解耦。

回头看,问题如果有思路,一下就能解决,对本问题的定性也会是个简单级别的问题。所以无奈也反感写的东西太过冗长,但还是算是个人思考过程的完整记录。

另外,这个问题算是代码里的历史遗留问题,前辈的几次改动中,有时索性就注解如下:

@Override
public void onUserInteraction() {
    // ... other logic
    // loadUrl("javascript:autoLogoutByTimer()");
    super.onUserInteraction();
}

如此,倒计时一旦初始化(安卓端 + 前端),也就不能再响应用户触摸中断重置了;

另外,这个 bug 修复工作先前也交给过一个安卓开发同事处理,但是他并没有解决,所以一开始也让我对这个 bug 修复难度有了错误估计。

🐬Todo List

Motivation

昨天 (2022-8-2),搞了个 blog-issues 界面,突然就觉得有很多可以(需要)总结并上传的(微)主题文章。

于是乎今天上午突然创建了多个主题 issue,嗯,有些浮躁了。

所以不如优先放在这里。

Todo List

  • 使用 express 搭建一个简单的后台服务
  • 使用 nestjs 搭建一个简单的后台服务
  • shell 脚本的(用到过那些)简单记录下
  • 跨域 CORS / JSONP / Proxy 代理;以及 CORS 细节;以及 Cookie 携带细节
  • 链表、多叉树的刷题准备,序列化与反序列化的实现;
  • redux 源码;状态管理模型的认识、分类及总结
  • hooks 之于 HOC 高阶组件的区别
  • react-router v6 使用

题目-爬楼梯

🎉写在最前

意义重大,算是自己的第一篇“博客”,虽然“随意”的写在了仓库 issues 界面。
重度拖延症下,终于写上了自己心心念念、代表着好兆头的爬楼梯题目(的题解分享)。
人生如逆旅,但愿我们都是不断向上攀爬着的~

题目

https://leetcode.com/problems/climbing-stairs/

解法1

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 测试用例不通过

解法2

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);
}

解法3

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] ),以如下图示说明

image

解法4

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);

在线沙盒,无压力速起 demo

最前

stackbliz 体验非常不错的在线开发沙盒。

更改一些默认配置

在提供的 User settings 配置文件入口,
目前自定义更改的:

"editor.fontSize": 14,
"editor.wordWrap": "off",

🚧 Two Sum

题目

作为经典第一题,也可以很多不同的优化解法。

解法1

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) 时间复杂度

解法2

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);
    }
}

说明: 初始化并管理好一个缓存结构

解法x

🚧或许再看看?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.