Showing Posts From

JavaScript

Vue 里强制组件重新渲染:改 key 才是正解

Vue 里"刷新组件"是个模糊需求。要重新 render 一次?重新执行 setup 拉数据?还是彻底把内部 state 归零?不同目标要用不同手段。 四种方式的差别手段 会销毁组件 会重新执行 setup 会重置 state改 :key ✅ ✅ ✅$forceUpdate() ❌ ❌ ❌重新赋值 ref/reactive ❌ ❌ 只改被赋值的字段v-if 切换 ✅ ✅ ✅场景一:改 :key(推荐) 想"完全刷新"一个组件(比如切换 tab 后要重新拉数据、重置表单): <template> <UserForm :key="refreshKey" /> <button @click="refresh">重新加载</button> </template><script setup> import { ref } from "vue";const refreshKey = ref(0); const refresh = () => refreshKey.value++; </script>refreshKey 一变,Vue 视这是新组件——旧的销毁、新的挂载、setup() 重跑、生命周期钩子从 onMounted 全走一遍。 场景二:$forceUpdate(几乎用不到) 只想强制重新 render,不动 state: import { getCurrentInstance } from "vue"; const instance = getCurrentInstance(); instance.proxy.$forceUpdate();适用场景其实很窄——一般是把不响应的对象塞进模板,或者用了第三方非响应库。只要还能改成响应式,就别用 $forceUpdate。 场景三:v-if 切换 粗暴但清晰: <template> <UserForm v-if="show" /> </template><script setup> const show = ref(true); async function reset() { show.value = false; await nextTick(); show.value = true; } </script>等价于改 key,用于"父组件不方便传 key"的场景。 数据变了但界面不更新 Vue3 大多数场景响应式自动生效,出问题基本是下面几种: 1. 解构丢了响应式 const state = reactive({ count: 1 }); const { count } = state; // count 是快照,不再响应改成 toRefs: const { count } = toRefs(state); count.value++; // 会触发更新2. 用了 shallowRef 但改的是深层 const data = shallowRef({ a: 1 }); data.value.a = 2; // 不会更新手动触发: import { triggerRef } from "vue"; triggerRef(data);或者直接换整个引用: data.value = { ...data.value, a: 2 };3. 用了 markRaw / Object.freeze 这些对象上禁用响应式,任你怎么改视图都不动。 4. Vue 2 的经典坑(Vue 3 无此问题)this.arr[index] = value 不触发 → 用 this.$set 或 splice(index, 1, value) this.obj.newKey = 1 不触发 → 用 this.$set(this.obj, "newKey", 1)nextTick:拿最新 DOM 修改数据后立刻读 DOM 大小、滚动位置: import { nextTick } from "vue";count.value++; await nextTick(); console.log(el.value.scrollHeight); // 这时才是新高度一句话总结 "重置组件"改 :key、"只重画"用 $forceUpdate(且大概率你不该用它)、"数据不更新"先查是不是解构丢了响应式。

JS 判断空值别只写 !value:0 和 false 会被误伤

前端最常见的"低级 bug"之一——接口返回: { "status": 0, "count": 0 }代码里: if (!data.status) { // 走这里,本意是"没返回 status" }结果 status 明明是 0(业务成功),这段错误逻辑也被触发。!value 把 falsy 全当空,误伤面太大: !0 // true !false // true !NaN // true !"" // true !null // true !undefined// true精确的"空"判断 如果空的定义是 null | undefined | "",那写: function isEmpty(value) { return value == null || value === ""; }关键是 value == null——用宽松等号,同时匹配 null 和 undefined(这是 ESLint 少数允许 == 的地方)。 isEmpty(null) // true isEmpty(undefined) // true isEmpty("") // trueisEmpty(0) // false isEmpty(false) // false isEmpty([]) // false想把 " " 也算空 用户输入常见"全是空格"的字符串: function isEmpty(value) { return value == null || (typeof value === "string" && value.trim() === ""); }isEmpty(" ") // true数组 / 对象也想算空 深一点的版本: function isEmpty(value) { if (value == null) return true; if (typeof value === "string") return value.trim() === ""; if (Array.isArray(value)) return value.length === 0; if (typeof value === "object") return Object.keys(value).length === 0; return false; }isEmpty([]) // true isEmpty({}) // true isEmpty(0) // false isEmpty(false) // falselodash 的 _.isEmpty 差不多是这个思路。 ?? vs ||:也是同一个坑 新版 JS 的空值合并: const port = config.port ?? 3000; // 只有 null/undefined 才取默认 const port = config.port || 3000; // 0 也会被换掉,多半是 bug配置里 port: 0 是合法的(让系统随机分配端口),用 || 会把它替换成 3000。这类场景统一用 ??。 表格速查表达式 什么算"空"!value falsy:0, false, NaN, "", null, undefinedvalue == null null, undefinedvalue === "" || value == null null, undefined, ""lodash _.isEmpty(value) null, undefined, "", [], {}, Map(), Set()一句话总结 !value 判空是懒省事,业务代码里最好写 value == null || value === "",避免把 0 和 false 一起干掉。

SyntaxError: Unexpected identifier '目':文本被贴进 JS 文件

看到这个报错,几乎不用想: 题 目 AI辅助的全方位科研管理与创作平台 ^ SyntaxError: Unexpected identifier '目' at wrapSafe (node:internal/modules/cjs/loader:1637:18)Node 把这一行当代码解析了,而中文字符不是合法 JS 标识符,直接崩。 真正原因 八成是你想读的文件内容被复制粘贴到 JS 源码里了。你的 .js 文件现在长这样: const fs = require('fs') rd = fs.readFileSync("1.txt") console.log(rd.length)题 目 AI辅助的全方位科研管理与创作平台 // ← 这一行是裸文本裸文本既不是字符串(没引号)也不是注释(没 // 或 /* */),Node 只能当代码解析,然后崩在第一个中文字符上。 正确姿势 JS 文件里只留代码,文本内容留在 1.txt 里: // index.js const fs = require("fs");const text = fs.readFileSync("1.txt", "utf8"); console.log(text.length);# 1.txt 题 目 AI辅助的全方位科研管理与创作平台顺手补:readFileSync 不传 encoding 的坑 fs.readFileSync("1.txt") 返回的是 Buffer,不是字符串: const rd = fs.readFileSync("1.txt"); console.log(rd.length); // 字节数,中文会算错想拿字符长度就要传编码: const text = fs.readFileSync("1.txt", "utf8"); console.log(text.length); // 字符数 console.log(Buffer.byteLength(text, "utf8"));// 字节数一个中文字符在 UTF-8 里是 3 字节,字符长度和字节长度会差好几倍——涉及"长度"就要先想清楚要哪种。 触类旁通:类似的 SyntaxError 同样是"文件混了不该混的东西",报错方向不同:报错 通常原因Unexpected identifier '目' 中文文本被贴进 .jsUnexpected token '<' HTML 页面被当 JS 加载(一般是路径错)Unexpected token 'export' ESM 语法进了 CJS 文件(改 .mjs 或加 "type": "module")Unexpected token '?' / ?? / &&= 老 Node 不认新语法,升级 NodeInvalid or unexpected token 指向  文件带了 UTF-8 BOM,重存为无 BOM一句话总结 看到 Unexpected identifier '中文'——先打开源码文件,把混进去的文本挪出去。readFileSync 记得传 "utf8"。

为什么 forEach 里的 await 没用:JS 异步循环踩坑记

前几天写脚本,想在数组遍历里挨个 await 一下: const arr = [1, 2, 3];arr.forEach(async (item) => { await sleep(1000); console.log(item); });console.log('结束');预期看到 1 2 3 结束,结果输出是: 结束 1 2 3forEach 完全没等异步回调结束,console.log('结束') 就先跑了。 为什么 forEach 不等待 看 Array.prototype.forEach 的内部实现,本质就是: for (let i = 0; i < arr.length; i++) { callback(arr[i]); }它 不关心 callback 返回的是不是 Promise,也不收集 Promise,更不会 await。所以哪怕你写: await arr.forEach(async x => await fn(x));等价于: await undefined因为 forEach 的返回值就是 undefined。 正确写法 需要顺序 await:用 for...of 一个执行完再执行下一个,最稳、最好读: for (const item of arr) { await doSomething(item); }适合请求限流、数据库写入、有依赖关系的自动化脚本。 需要并发 await:用 Promise.all + map 全部同时开跑,最快: await Promise.all( arr.map(async (item) => { await doSomething(item); }) );注意别把上游接口打爆——需要限流的话可以配合 p-limit 这类库。 记忆表场景 写法顺序 await for...of并发 await Promise.all + map不需要等待 forEach 也无所谓一条经验 只要方法里出现 await,就别用 forEach。 想清楚这一步之后,很多"数据处理完了但结果不对"的 bug 就会消失。

document.cookie 设置有效期:max-age vs expires 与删除的坑

document.cookie 是一个字符串 API,"设置有效期"其实就是在 cookie 字符串里追加 expires 或 max-age 属性。 规则很简单:不写有效期 = 会话 cookie(浏览器关掉就没)、写了 = 持久化 cookie。 方式一:expires(老写法) 传一个 UTC 时间字符串: const d = new Date(); d.setTime(d.getTime() + 7 * 24 * 60 * 60 * 1000); // 7 天document.cookie = `token=abc; expires=${d.toUTCString()}; path=/`;注意必须是 toUTCString(),用 toString() 或 toISOString() 会挂。 方式二:max-age(现代推荐) 直接指定"多少秒后过期": document.cookie = `token=abc; max-age=${7 * 24 * 60 * 60}; path=/`;单位是秒。max-age 更直观,也不用管时区。老到 IE 那一档才要考虑 expires。 删除 cookie 本质就是"立刻过期": document.cookie = "token=; max-age=0; path=/"; // 或 document.cookie = "token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/";三个删不掉的坑 1. path 不一致 创建时用 path=/,删除也必须带上 path=/。不带就等于删的是当前路径下的 cookie,很可能和实际存放的 cookie 不是同一个。 2. domain 不一致 .example.com 和 sub.example.com 是两条不同的 cookie。删除时必须匹配写入时的 domain。 3. HttpOnly cookie 前端改不了 服务端设置了 HttpOnly 的 cookie,document.cookie 完全读不到、也删不掉。只能后端接口去清。 封装模板 前端项目里放一份省心: export function setCookie(name, value, days = 7, options = {}) { const d = new Date(); d.setTime(d.getTime() + days * 864e5); // 864e5 = 24*60*60*1000 const parts = [ `${name}=${encodeURIComponent(value)}`, `expires=${d.toUTCString()}`, `path=${options.path || "/"}`, ]; if (options.domain) parts.push(`domain=${options.domain}`); if (options.secure) parts.push("secure"); if (options.sameSite) parts.push(`samesite=${options.sameSite}`); document.cookie = parts.join("; "); }export function getCookie(name) { return document.cookie .split("; ") .find(row => row.startsWith(name + "=")) ?.split("=")[1]; }export function deleteCookie(name, options = {}) { setCookie(name, "", -1, options); // 复用同样的 path/domain }上生产别忘的属性 正经站点几乎必配的三个属性:Secure:只走 HTTPS SameSite=Lax(登录态用 Strict 更安全) HttpOnly(防 XSS 盗 token,只能后端 Set-Cookie 时写)前端能设的就 Secure 和 SameSite,HttpOnly 必须走服务端。 一句话总结 现代写法用 max-age、删除记得带 path=/、跨域跨子域记得对齐 domain。真安全靠 HttpOnly + Secure + SameSite,那部分归后端管。

在浏览器里 hook setInterval:原理和五种写法

在浏览器里"hook" 某个内置函数,本质就是——把原函数偷出来,用你自己的函数替换它。setInterval 是最常见的 hook 目标:调试疯狂轮询、监控页面定时器、干掉反调试轮询。 一句话原理 const _origin = window.setInterval;window.setInterval = function (fn, delay, ...args) { // 你的自定义逻辑 return _origin(fn, delay, ...args); // 回到原函数 };_origin 是原函数引用,window.setInterval 是你替换后的新函数。之后所有 setInterval(...) 调用都会先经过你。 场景一:监控所有调用 (function () { const _origin = window.setInterval; window.setInterval = function (fn, delay, ...args) { console.log("setInterval:", { delay, fn: fn.toString().slice(0, 80) }); return _origin(fn, delay, ...args); }; })();用途:找出页面里到底谁在挂定时器。 场景二:按条件拦截 只关心 1 秒轮询: window.setInterval = function (fn, delay, ...args) { if (delay === 1000) { console.trace("1s interval:", fn); } return _origin(fn, delay, ...args); };console.trace 直接打出调用栈,能追到是哪段代码挂的。 场景三:阻止高频轮询 页面每 50ms 疯狂检查某个东西(典型反调试或广告轮询),CPU 直接吃满: window.setInterval = function (fn, delay, ...args) { if (delay < 500) { console.log("阻止高频 interval:", delay); return null; // 直接返回 null,不真的开定时器 } return _origin(fn, delay, ...args); };场景四:记录所有 ID 方便统一清 (function () { const _origin = window.setInterval; const ids = new Set(); window.setInterval = function (fn, delay, ...args) { const id = _origin(fn, delay, ...args); ids.add(id); return id; }; window.__clearAll = () => { ids.forEach(clearInterval); ids.clear(); console.log("全部清除"); }; })();在 devtools 里手动 __clearAll() 就能一键把当前页面所有定时器停了。 场景五:油猴脚本里 hook 必须 @run-at document-start——否则页面脚本先跑,等你 hook 上去已经晚了: // ==UserScript== // @name hook setInterval // @match https://example.com/* // @run-at document-start // @grant none // ==/UserScript==(function () { const _origin = window.setInterval; window.setInterval = function (fn, delay, ...args) { console.log("intercepted:", delay); return _origin.call(window, fn, delay, ...args); }; })();注意用 _origin.call(window, ...)——某些浏览器的 setInterval 严格要求 this === window,不 bind 会抛 Illegal invocation。 常见反 hook 手法 页面可能这样绕过你: // 提前保存原始引用 const _real = window.setInterval.bind(window); _real(fn, 100);页面代码保存的是你的 hook 前的原函数,你就拦不到 或者 iframe 里跑,window 隔离 或者用 postMessage + Worker,压根不走主线程 setInterval对策:@run-at document-start 越早越好 同时 hook setTimeout / requestAnimationFrame——很多站点会混着用 iframe 里的定时器要另外注入用 Proxy 更隐蔽 上面的写法暴露痕迹(window.setInterval.toString() 会变),要更隐蔽: window.setInterval = new Proxy(window.setInterval, { apply(target, thisArg, args) { const [fn, delay] = args; console.log("intercepted:", delay); return Reflect.apply(target, thisArg, args); }, });Proxy 版本的 .toString() 还是原始的,页面用 toString().includes("[native code]") 检测更难发现。 一句话总结 hook = 存原函数引用 + 替换 window 属性。油猴里必须 run-at document-start,进阶用 Proxy 更隐蔽。同一套模板套 setTimeout / fetch / XMLHttpRequest 也照样能用。

ethers v6 里 provider / signer 怎么拿,以及手写一个 signer

前端 dApp 连 MetaMask 用 ethers.js,v5 和 v6 API 变了。写代码总卡在"provider 怎么初始化"这一步。 ethers v6 的最小骨架 <script src="https://cdn.jsdelivr.net/npm/ethers@6.7.1/dist/ethers.min.js"></script> <script> let provider, signer;if (window.ethereum) { provider = new ethers.BrowserProvider(window.ethereum); } else { alert("请先安装 MetaMask"); }document.getElementById("connect").addEventListener("click", async () => { await provider.send("eth_requestAccounts", []); signer = await provider.getSigner(); const address = await signer.getAddress(); const balance = await provider.getBalance(address); console.log(address, ethers.formatEther(balance), "ETH"); }); </script>要点:v6 用 ethers.BrowserProvider(v5 是 ethers.providers.Web3Provider) getSigner() 现在返回 Promise,要 await ethers.formatEther(v6)替代了 ethers.utils.formatEther(v5)v5 vs v6 对照场景 v5 v6浏览器 Provider new ethers.providers.Web3Provider(eth) new ethers.BrowserProvider(eth)RPC Provider ethers.providers.JsonRpcProvider(url) ethers.JsonRpcProvider(url)formatEther ethers.utils.formatEther(v) ethers.formatEther(v)parseEther ethers.utils.parseEther(s) ethers.parseEther(s)BigNumber ethers.BigNumber.from(x) 原生 BigInt手写一个 signer(不依赖 ethers) 有些教学场景或轻量项目不想引入整个 ethers.js,直接靠 window.ethereum 也能拼出一个 signer: async function myGetSigner() { const accounts = await window.ethereum.request({ method: "eth_requestAccounts", }); const address = accounts[0]; return { address, async getAddress() { return address; }, async signMessage(message) { const hex = typeof message === "string" ? toHex(message) : message; return window.ethereum.request({ method: "personal_sign", params: [hex, address], }); }, async sendTransaction(tx) { const params = { from: address, to: tx.to, value: tx.value ? "0x" + BigInt(tx.value).toString(16) : "0x0", gas: tx.gas ? "0x" + BigInt(tx.gas).toString(16) : undefined, data: tx.data || "0x", }; return window.ethereum.request({ method: "eth_sendTransaction", params: [params], }); }, }; }// 浏览器没有 Buffer,自己写一个字符串转 hex function toHex(str) { let hex = ""; for (const ch of str) { hex += ch.charCodeAt(0).toString(16).padStart(2, "0"); } return "0x" + hex; }常见踩坑 Buffer is not defined 从 Node 教程抄的 Buffer.from(...).toString('hex') 在浏览器不能用。用上面的 toHex 或 TextEncoder: const hex = "0x" + Array.from(new TextEncoder().encode(str)) .map(b => b.toString(16).padStart(2, "0")).join("");BigInt 序列化 v6 起金额和 nonce 都是原生 BigInt,直接 JSON.stringify 会崩: TypeError: Do not know how to serialize a BigInt要么手动转字符串,要么全局 patch: BigInt.prototype.toJSON = function () { return this.toString(); };window.ethereum 是数组 装了多个钱包(MetaMask + Coinbase + OKX)后,window.ethereum 可能变成聚合对象,isMetaMask 判断失效。EIP-6963 出来后的做法: window.addEventListener("eip6963:announceProvider", (event) => { console.log(event.detail.info.name, event.detail.provider); }); window.dispatchEvent(new Event("eip6963:requestProvider"));一句话总结 ethers v6:new BrowserProvider(window.ethereum) + await provider.getSigner();不想引 ethers 时手写 signer 也不难,注意浏览器里没 Buffer 就行。