<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[lingの知识库]]></title><description><![CDATA[web开发日记]]></description><link>https://lingwang.ink</link><image><url>https://lingwang.ink/innei.svg</url><title>lingの知识库</title><link>https://lingwang.ink</link></image><generator>Shiro (https://github.com/Innei/Shiro)</generator><lastBuildDate>Mon, 11 May 2026 10:52:05 GMT</lastBuildDate><atom:link href="https://lingwang.ink/feed" rel="self" type="application/rss+xml"/><pubDate>Mon, 11 May 2026 10:52:05 GMT</pubDate><language><![CDATA[zh-CN]]></language><item><title><![CDATA[lodash.difference]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://lingwang.ink/posts/compute/lodash.difference">https://lingwang.ink/posts/compute/lodash.difference</a></blockquote><div><blockquote><p>创建一个具有唯一array值的数组，每个值不包含在其他给定的数组中。（注：即创建一个新数组，这个数组中的值，为排除了给定数组中的值。结果值的顺序是由第一个数组中的顺序确定。</p></blockquote>
<p><strong>参数：</strong><br/>array (Array): 要检查的数组。<br/>[values] (...Array): 排除的值。<br/></p><pre class="language-javascript lang-javascript"><code class="language-javascript lang-javascript">function difference(...agrs){
    const array =  agrs[0]
    if(!Array.isArray(agrs[0])) return []
    const excludeSet = new Set(agrs.slice(1).flat(Infinity))
    let result = []
    
    for(const value of array){
        if(!excludeSet.has(value)){
            result.push(value)
        }
    }

    return result
}

console.log(difference([3, 2, 1], [1,[[[[2]]]]]))</code></pre></div><p style="text-align:right"><a href="https://lingwang.ink/posts/compute/lodash.difference#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://lingwang.ink/posts/compute/lodash.difference</link><guid isPermaLink="true">https://lingwang.ink/posts/compute/lodash.difference</guid><dc:creator><![CDATA[ling]]></dc:creator><pubDate>Wed, 18 Mar 2026 11:28:48 GMT</pubDate></item><item><title><![CDATA[lodash.concat]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://lingwang.ink/posts/compute/lodash.concat">https://lingwang.ink/posts/compute/lodash.concat</a></blockquote><div><blockquote><p>创建一个新数组，将array与任何数组 或 值连接在一起。</p></blockquote><pre class="language-javascript lang-javascript"><code class="language-javascript lang-javascript">function concat(...args) {
  const array = args[0];
  const rest = args.slice(1);

  let result = Array.isArray(array) ? [...array] : [array];
  for (const value of rest) {
    if (Array.isArray(value)) {
      result.push(...value);
    } else {
      result.push(value);
    }
  }
  return result
}

var array = [1];
var other = concat([1,2,3],[4,5,[6]]);
console.log(other);</code></pre></div><p style="text-align:right"><a href="https://lingwang.ink/posts/compute/lodash.concat#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://lingwang.ink/posts/compute/lodash.concat</link><guid isPermaLink="true">https://lingwang.ink/posts/compute/lodash.concat</guid><dc:creator><![CDATA[ling]]></dc:creator><pubDate>Wed, 18 Mar 2026 11:07:12 GMT</pubDate></item><item><title><![CDATA[lodash.compact]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://lingwang.ink/posts/compute/lodashcompact">https://lingwang.ink/posts/compute/lodashcompact</a></blockquote><div><blockquote><p>创建一个新数组，包含原数组中所有的非假值元素。例如false, null,0, &quot;&quot;, undefined, 和 NaN 都是被认为是“假值”</p></blockquote><pre class="language-javascript lang-javascript"><code class="language-javascript lang-javascript">function compact(array){
    if(!array || typeof array !== &#x27;object&#x27;) return []
    let result = []
    for(let i = 0; i &lt; array.length; i++){
        if(!(i in array)) continue;
        if(array[i]){
            result.push(array[i])
        }
    }
    return array
}

console.log(compact([0, 1, false, 2, &#x27;&#x27;, 3]))</code></pre></div><p style="text-align:right"><a href="https://lingwang.ink/posts/compute/lodashcompact#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://lingwang.ink/posts/compute/lodashcompact</link><guid isPermaLink="true">https://lingwang.ink/posts/compute/lodashcompact</guid><dc:creator><![CDATA[ling]]></dc:creator><pubDate>Wed, 18 Mar 2026 10:48:54 GMT</pubDate></item><item><title><![CDATA[lodash.chunk]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://lingwang.ink/posts/compute/lodashchunk">https://lingwang.ink/posts/compute/lodashchunk</a></blockquote><div><blockquote><p> 将数组（array）拆分成多个 size 长度的区块，并将这些区块组成一个新数组。 如果array 无法被分割成全部等长的区块，那么最后剩余的元素将组成一个区块。</p></blockquote><pre class="language-javascript lang-javascript"><code class="language-javascript lang-javascript">function chunk(array, size = 1){
    if(typeof array !== &#x27;object&#x27; || typeof size !== &#x27;number&#x27;){
        return []
    }
    if(!array || !array.length || size &lt; 1) {
        return []
    }
    let result = [];
    let lastSlice = 0;
    for(let i = 0;i &lt; array.length; i++){
        if(i % size === 0 &amp;&amp; i !== 0){
            result.push(array.slice(lastSlice,i))
            lastSlice = i;
        }
    }
    if(lastSlice &lt; array.length){
        result.push(array.slice(lastSlice,array.length))
    }

    return result;
}

chunk([&#x27;a&#x27;, &#x27;b&#x27;, &#x27;c&#x27;, &#x27;d&#x27;], 2);
    // =&gt; [[&#x27;a&#x27;, &#x27;b&#x27;], [&#x27;c&#x27;, &#x27;d&#x27;]]
    
chunk([&#x27;a&#x27;, &#x27;b&#x27;, &#x27;c&#x27;, &#x27;d&#x27;], 3);
    // =&gt; [[&#x27;a&#x27;, &#x27;b&#x27;, &#x27;c&#x27;], [&#x27;d&#x27;]]</code></pre><p>s</p></div><p style="text-align:right"><a href="https://lingwang.ink/posts/compute/lodashchunk#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://lingwang.ink/posts/compute/lodashchunk</link><guid isPermaLink="true">https://lingwang.ink/posts/compute/lodashchunk</guid><dc:creator><![CDATA[ling]]></dc:creator><pubDate>Wed, 18 Mar 2026 10:32:05 GMT</pubDate></item><item><title><![CDATA[mm 2026笔试-前端开发]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://lingwang.ink/posts/exam/mm">https://lingwang.ink/posts/exam/mm</a></blockquote><div><h2 id="css-">CSS 相关</h2><h3 id="-1">题目 1：容器宽度自适应与限制</h3><p><strong>题干：</strong> 设置一个容器，使其宽度占屏幕宽度的 80%，但最大宽度不超过 500px，以下哪个 CSS 写法是正确的？</p><p><strong>选项：</strong></p><ul><li>A. <code>width: 80%;</code></li><li>B. <code>width: 80vw;</code></li><li>C. <code>width: 80%; max-width: 500px;</code></li><li>D. <code>width: calc(80% - 500px);</code></li></ul><p><strong>答案：</strong> C ✅</p><p><strong>解析：</strong></p><table><thead><tr><th>选项</th><th>说明</th></tr></thead><tbody><tr><td>A</td><td>仅设置 80% 宽度，无最大宽度限制，不符合 &quot;最大不超过 500px&quot; 的要求</td></tr><tr><td>B</td><td>80vw 是视口宽度的 80%，同样无最大宽度限制</td></tr><tr><td>C</td><td><code>width: 80%</code> 控制基础宽度，<code>max-width: 500px</code> 限制最大宽度，完全符合需求</td></tr><tr><td>D</td><td><code>calc(80% - 500px)</code> 计算逻辑错误，无法实现目标效果</td></tr></tbody></table><hr/><h3 id="-2css-grid-">题目 2：CSS Grid 响应式列表布局</h3><p><strong>题干：</strong> 实现一个书籍列表布局，要求每行最多显示 4 本书，当屏幕宽度变窄时，列数自动减少为 2 本，以下哪个 CSS 写法最符合需求？</p><p><strong>选项：</strong></p><ul><li>A. <code>grid-template-columns: repeat(auto-fill, minmax(200px, 1fr))</code></li><li>B. <code>grid-template-columns: repeat(4, 1fr)</code></li><li>C. <code>grid-template-columns: repeat(auto-fit, minmax(200px, 1fr))</code></li><li>D. <code>grid-template-columns: 1fr 1fr 1fr 1fr</code></li></ul><p><strong>答案：</strong> C ✅</p><p><strong>解析：</strong></p><table><thead><tr><th>选项</th><th>说明</th></tr></thead><tbody><tr><td>A</td><td>auto-fill 会创建尽可能多的列，但不会折叠空列，窄屏可能出现空白列</td></tr><tr><td>B/D</td><td>固定 4 列，无法自适应调整列数</td></tr><tr><td>C</td><td>auto-fit 自动适配列数并折叠空列，minmax(200px, 1fr) 控制列宽范围（最小 200px，最大均分空间），符合响应式需求</td></tr></tbody></table><hr/><h3 id="-3css-">题目 3：CSS 样式优先级</h3><p><strong>题干：</strong> 以下 HTML 元素最终显示的文字颜色是什么？</p><pre class="language-html lang-html"><code class="language-html lang-html">&lt;div id=&quot;one&quot; class=&quot;two&quot; style=&quot;color:red&quot;&gt;我是什么颜色&lt;/div&gt;</code></pre><p><strong>选项：</strong></p><ul><li>A. yellow</li><li>B. green</li><li>C. pink</li><li>D. red</li></ul><p><strong>答案：</strong> 红色 ✅</p><p><strong>解析：</strong> CSS 样式优先级从高到低：行内样式（style=&quot;&quot;）&gt; ID 选择器（#one）&gt; 类选择器（.two）&gt; 标签选择器（div）。行内样式 <code>color:red</code> 优先级最高，因此文字为红色。</p><hr/><h3 id="-4">题目 4：大屏数据可视化布局重构</h3><p><strong>题干：</strong> 重构大屏数据可视化页面，要求左侧导航可折叠、右侧图表区域保持 16:9 宽高比、多端自适应（PC / 平板 / 大屏），且解决折叠时图表抖动、高分屏下元素溢出的问题，以下哪种方案最优？</p><p><strong>选项：</strong></p><ul><li>A. 使用 Flexbox 嵌套布局，通过 calc() 动态计算尺寸，用 transition 实现折叠动画</li><li>B. 组合使用 Grid 布局定义宏观结构，Flexbox 处理局部对齐，通过 CSS 自定义属性维护比例系统，应用容器查询和视窗单位进行响应式适配</li><li>C. 采用 CSS Grid 定义布局模板，结合 aspect-ratio 属性控制图表区域，使用 CSS 容器查询处理多端适配</li><li>D. 使用 JavaScript 监听 resize 事件动态计算尺寸，通过 transform 缩放整个画布，利用 will-change 优化动画性能</li></ul><p><strong>答案：</strong> B ✅</p><p><strong>解析：</strong></p><table><thead><tr><th>选项</th><th>说明</th></tr></thead><tbody><tr><td>A</td><td>Flexbox 适合一维布局，难以统一管理比例系统，无法解决复杂适配问题</td></tr><tr><td>B</td><td>Grid 管宏观布局、Flexbox 管局部对齐、CSS 自定义属性统一比例、容器查询 / 视窗单位适配多端，是最优组合</td></tr><tr><td>C</td><td>仅 Grid + aspect-ratio 缺乏局部对齐和比例统一管理</td></tr><tr><td>D</td><td>JS 动态计算增加性能开销，transform 缩放易导致图表模糊</td></tr></tbody></table><hr/><h2 id="vue-">Vue 相关</h2><h3 id="-1vue-watch-">题目 1：Vue watch 配置项</h3><p><strong>题干：</strong> 关于 Vue 中 watch 的 immediate 和 deep 属性，以下说法错误的是？</p><p><strong>选项：</strong></p><ul><li>A. immediate 属性表示在 watch 首次绑定数据时，是否执行 handler 方法</li><li>B. immediate 值为 false 时，表示在 watch 声明时就立即执行 handler 方法</li><li>C. immediate 值为 true 时，表示在数据发生变化时才执行 handler 方法</li><li>D. deep 属性表示进行深度监听，目的是为了发现对象内部值的变化，复杂类型的数据时使用</li></ul><p><strong>答案：</strong> B、C ✅（错误选项）</p><p><strong>解析：</strong></p><table><thead><tr><th>选项</th><th>说明</th></tr></thead><tbody><tr><td>A</td><td>正确，immediate: true 初始化时立即执行 handler</td></tr><tr><td>B</td><td>错误，immediate: false 是默认值，仅数据变化时执行 handler</td></tr><tr><td>C</td><td>错误，immediate: true 初始化时执行一次，数据变化时也执行</td></tr><tr><td>D</td><td>正确，deep: true 用于监听对象 / 数组内部属性变化</td></tr></tbody></table><hr/><h3 id="-2vue-">题目 2：Vue 动态样式绑定（表单按钮）</h3><p><strong>题干：</strong> 表单验证场景中，当所有字段验证通过（formValid 为 true）时，提交按钮背景色为绿色，否则为灰色，以下哪个写法正确？</p><p><strong>选项：</strong></p><ul><li>A. <code>v-bind:style=&quot;{ backgroundColor: formValid ? &#x27;green&#x27; : &#x27;gray&#x27; }&quot;</code></li><li>B. <code>v-if=&quot;formValid&quot; style=&quot;background-color: green&quot;</code></li><li>C. <code>v-model=&quot;formValid&quot; style=&quot;background-color: green&quot;</code></li><li>D. <code>v-for=&quot;formValid&quot; style=&quot;background-color: green&quot;</code></li></ul><p><strong>答案：</strong> A ✅</p><p><strong>解析：</strong></p><table><thead><tr><th>选项</th><th>说明</th></tr></thead><tbody><tr><td>A</td><td>动态绑定 style，通过三元表达式根据 formValid 切换背景色，符合需求</td></tr><tr><td>B</td><td>v-if 控制元素渲染，而非样式，formValid 为 false 时按钮会被移除</td></tr><tr><td>C</td><td>v-model 用于表单双向绑定，无法控制样式</td></tr><tr><td>D</td><td>v-for 用于循环渲染，与样式无关</td></tr></tbody></table><hr/><h3 id="-3vue-3-ssr">题目 3：Vue 3 服务端渲染（SSR）性能优化</h3><p><strong>题干：</strong> 高流量促销活动场景下，Vue 3 SSR 页面出现服务器过载、首屏渲染延迟的问题，以下哪种优化策略最优？</p><p><strong>选项：</strong></p><ul><li>A. 流式渲染（renderToStream）+ Transfer-Encoding: chunked</li><li>B. 异步组件加载 + 客户端 hydration 重试机制</li><li>C. renderToString + 服务器端内存缓存</li><li>D. 静态站点生成（SSG）+ 托管到 CDN</li></ul><p><strong>答案：</strong> C ✅</p><p><strong>解析：</strong></p><table><thead><tr><th>选项</th><th>说明</th></tr></thead><tbody><tr><td>A</td><td>流式渲染优化传输过程，未解决服务器过载问题</td></tr><tr><td>B</td><td>客户端优化，对服务器性能无帮助</td></tr><tr><td>C</td><td>缓存完整 HTML 响应，避免重复渲染，大幅降低服务器 CPU / 内存消耗，适配高流量场景</td></tr><tr><td>D</td><td>SSG 适合静态内容，促销页面多为动态数据（价格 / 库存），灵活性不足</td></tr></tbody></table><hr/><h3 id="-4vue-mousemove">题目 4：Vue 高频事件优化（mousemove）</h3><p><strong>题干：</strong> Vue 组件中绑定高频 mousemove 事件导致 UI 卡顿，以下哪种方案能最小化性能开销并保证状态同步？</p><p><strong>选项：</strong></p><ul><li>A. 通过 provide/inject 定义事件总线，子组件触发自定义事件</li><li>B. 在组件模板中使用 @mousemove.passive 修饰符绑定处理函数，结合防抖逻辑</li><li>C. 为每个目标元素添加独立的 @mousemove 监听器，并在处理函数内调用 event.stopPropagation()</li><li>D. 在 setup() 中使用 window.addEventListener 绑定事件，在 onUnmounted 中移除监听器，并利用 requestAnimationFrame 节流</li></ul><p><strong>答案：</strong> D ✅</p><p><strong>解析：</strong></p><table><thead><tr><th>选项</th><th>说明</th></tr></thead><tbody><tr><td>A</td><td>事件总线仅解决通信问题，无性能优化作用</td></tr><tr><td>B</td><td>passive 适配滚动事件，对 mousemove 无效，且未结合 requestAnimationFrame 优化渲染时机</td></tr><tr><td>C</td><td>多监听器增加内存占用，stopPropagation 仅阻止冒泡，无性能优化作用</td></tr><tr><td>D</td><td>卸载时移除监听器避免内存泄漏，requestAnimationFrame 节流控制触发频率，减少重绘开销</td></tr></tbody></table><hr/><h2 id="typescript-">TypeScript 相关</h2><h3 id="-1typescript-">题目 1：TypeScript 类型推断规则</h3><p><strong>题干：</strong> 以下哪个不是 TypeScript 的类型推断规则？</p><p><strong>选项：</strong></p><ul><li>A. 如果初始化时没有类型和赋值，推断为 any 类型</li><li>B. 如果初始化时有赋值但没有类型，推断为该值的类型</li><li>C. 如果是在大多数分支中推断出的类型，就使用那个类型</li><li>D. 如果初始化时有类型但没有赋值，推断为 unknown 类型</li></ul><p><strong>答案：</strong> C ✅（错误选项）</p><p><strong>解析：</strong></p><ul><li>A/B/D：均为 TypeScript 核心类型推断规则</li><li>C：TypeScript 会根据所有分支计算联合类型，而非 &quot;大多数分支&quot; 的类型，因此该说法错误</li></ul><hr/><h2 id="javascript--dom-">JavaScript 基础与 DOM 操作</h2><h3 id="-1fetch-api--json-">题目 1：fetch API 获取 JSON 数据</h3><p><strong>题干：</strong> 使用 fetch API 从 <a href="https://api.example.com/users">https://api.example.com/users</a> 获取用户数据，并打印 JSON 格式的结果到控制台，以下哪个写法正确？</p><p><strong>选项：</strong></p><ul><li>A. <code>fetch(&#x27;https://api.example.com/users&#x27;).then(data =&gt; console.log(data))</code></li><li>B. <code>fetch(&#x27;https://api.example.com/users&#x27;).then(response =&gt; response.json()).then(data =&gt; console.log(data))</code></li><li>C. <code>fetch(&#x27;https://api.example.com/users&#x27;).then(response =&gt; console.log(response))</code></li><li>D. <code>console.log(fetch(&#x27;https://api.example.com/users&#x27;))</code></li></ul><p><strong>答案：</strong> B ✅</p><p><strong>解析：</strong></p><table><thead><tr><th>选项</th><th>说明</th></tr></thead><tbody><tr><td>A</td><td>fetch 返回 Response 对象，未解析为 JSON，打印的是响应对象而非数据</td></tr><tr><td>B</td><td>response.json() 将响应体解析为 JSON，第二个 then 接收并打印数据，符合需求</td></tr><tr><td>C</td><td>打印 Response 对象，非 JSON 数据</td></tr><tr><td>D</td><td>打印 Promise 对象，无法获取实际数据</td></tr></tbody></table><hr/><h3 id="-2">题目 2：为对象添加独立方法（音乐播放器）</h3><p><strong>题干：</strong> 音乐播放器项目中，每首歌曲对象共享原型方法 play，需为某一首歌曲单独添加 pause 方法（不影响其他歌曲），以下哪种方式正确？</p><p><strong>选项：</strong></p><ul><li>A. 修改原型链中的原型对象</li><li>B. 直接为该歌曲对象添加 pause 方法</li><li>C. 使用 Object.setPrototypeOf() 方法</li><li>D. 使用 class 关键字并继承歌曲类</li></ul><p><strong>答案：</strong> B ✅</p><p><strong>解析：</strong></p><table><thead><tr><th>选项</th><th>说明</th></tr></thead><tbody><tr><td>A</td><td>修改原型会让所有歌曲对象继承 pause 方法，不符合 &quot;仅某一首&quot; 的需求</td></tr><tr><td>B</td><td>直接给单个对象添加方法，仅作用于该对象，无副作用</td></tr><tr><td>C</td><td>setPrototypeOf 修改原型，影响其他对象</td></tr><tr><td>D</td><td>继承类需实例化新对象，无法给已有对象添加方法</td></tr></tbody></table><hr/><h3 id="-3-dom-">题目 3：高频 DOM 更新性能优化</h3><p><strong>题干：</strong> 实时监控系统中，每秒需更新 5000+ DOM 元素，导致频繁样式计算和布局抖动，以下哪种方案最优？</p><p><strong>选项：</strong></p><ul><li>A. 使用 document.createElement 逐个创建元素后直接追加到容器，通过 will-change: transform 强制提升图层</li><li>B. 将容器设为 display: none 后通过 innerHTML 全量替换，操作完成再恢复显示</li><li>C. 创建离屏 Canvas 进行数据绘制，通过位图快照替换 DOM 内容，同时维护隐藏的 DOM 副本</li><li>D. 采用文档碎片（document.createDocumentFragment）批量构建节点，使用 requestPostAnimationFrame 控制插入时机</li></ul><p><strong>答案：</strong> D ✅</p><p><strong>解析：</strong></p><table><thead><tr><th>选项</th><th>说明</th></tr></thead><tbody><tr><td>A</td><td>逐个追加触发多次重排，性能差</td></tr><tr><td>B</td><td>display: none 恢复时一次性大规模重排，仍会卡顿，且丢失元素状态</td></tr><tr><td>C</td><td>Canvas 不适合交互场景，隐藏 DOM 副本增加内存占用</td></tr><tr><td>D</td><td>文档碎片批量构建节点（一次重排），requestPostAnimationFrame 控制渲染时机，避免布局抖动</td></tr></tbody></table><hr/><h3 id="-4">题目 4：新闻网站异步加载更多</h3><p><strong>题干：</strong> 开发新闻网站时，需实现 &quot;点击加载更多&quot; 功能，要求不刷新页面加载新内容，以下哪种方案最合适？</p><p><strong>选项：</strong></p><ul><li>A. 预加载所有新闻并设置为 display: none，点击按钮切换为 display: block</li><li>B. 使用分页技术和静态页面，点击按钮导航到新页面</li><li>C. 使用 JavaScript 和 Fetch API 监听按钮点击，异步请求下一批新闻并添加到当前列表</li><li>D. 使用 <code>&lt;iframe&gt;</code> 加载新的新闻页面</li></ul><p><strong>答案：</strong> C ✅</p><p><strong>解析：</strong></p><table><thead><tr><th>选项</th><th>说明</th></tr></thead><tbody><tr><td>A</td><td>预加载所有新闻导致初始加载慢，浪费带宽</td></tr><tr><td>B</td><td>导航到新页面触发全页刷新，不符合需求</td></tr><tr><td>C</td><td>AJAX 异步请求 + 追加内容，实现无刷新加载，是标准方案</td></tr><tr><td>D</td><td><code>&lt;iframe&gt;</code> 体验差，不利于 SEO 和性能优化</td></tr></tbody></table><hr/><h2 id="">性能优化相关</h2><h3 id="-1">题目 1：高并发实时金融数据处理</h3><p><strong>题干：</strong> 金融监控系统需处理高并发实时数据流，保证数据实时性、渲染性能和代码可维护性，以下哪些方案正确？</p><p><strong>选项：</strong></p><ul><li>A. 使用 WebSocket 进行数据推送，结合 Redux 实现状态管理，并使用 memoization 技术优化渲染性能</li><li>B. 使用 AJAX 定时轮询数据，结合 Vue 的响应式机制，确保每次数据更新都直接触发 UI 更新</li><li>C. 使用 Service Worker 进行数据缓存，结合自定义事件进行数据更新，并在每次更新时重绘整个 UI</li><li>D. 使用 GraphQL 订阅进行数据推送，结合高阶函数封装数据处理逻辑，同时利用虚拟 DOM 减少重绘次数</li></ul><p><strong>答案：</strong> A、D ✅</p><p><strong>解析：</strong></p><table><thead><tr><th>选项</th><th>说明</th></tr></thead><tbody><tr><td>A</td><td>WebSocket 实时推送 + Redux 状态管理 + 记忆化优化渲染，符合需求</td></tr><tr><td>B</td><td>AJAX 轮询延迟高、服务器压力大，每次更新触发 UI 导致频繁重绘</td></tr><tr><td>C</td><td>Service Worker 适合离线缓存，全量重绘性能差</td></tr><tr><td>D</td><td>GraphQL 订阅按需推送 + 高阶函数封装逻辑 + 虚拟 DOM 减少重绘，符合需求</td></tr></tbody></table><hr/><h2 id="">数据处理相关</h2><h3 id="-1">题目 1：学生成绩统计</h3><p><strong>题干：</strong> 给定学生成绩数组，计算 classOne 为 true 的学生的语文 + 数学总分。</p><p><strong>输入：</strong></p><pre class="language-javascript lang-javascript"><code class="language-javascript lang-javascript">const studentList = [
  { id: 5, name: &quot;Alex&quot;, chineseScore: 98, mathScore: 56, classOne: true },
  { id: 82, name: &quot;Bob&quot;, chineseScore: 73, mathScore: 99, classOne: true },
  { id: 22, name: &quot;Cindy&quot;, chineseScore: 20, mathScore: 59, classOne: false }
];</code></pre><p><strong>要求：</strong> 使用 JavaScript 内置方法实现。</p><p><strong>答案：</strong></p><pre class="language-javascript lang-javascript"><code class="language-javascript lang-javascript">// 筛选 + 累加
const totalScore = studentList
  .filter(student =&gt; student.classOne) // 筛选一班学生
  .reduce((sum, student) =&gt; sum + student.chineseScore + student.mathScore, 0); // 累加总分

console.log(totalScore); // 输出：326</code></pre><p><strong>解析：</strong></p><ul><li><code>filter</code>：过滤出 <code>classOne: true</code> 的学生</li><li><code>reduce</code>：遍历筛选结果，累加语文和数学成绩，初始值为 0</li></ul><hr/><h3 id="-2">题目 2：购物车商品归类</h3><p><strong>题干：</strong> 给定购物车数组，将同名商品归类到同一数组中（以商品名称为键）。</p><p><strong>输入：</strong></p><pre class="language-javascript lang-javascript"><code class="language-javascript lang-javascript">const shoppingCart = [
  {name: &#x27;Apple&#x27;, price: 1.99, quantity: 3},
  {name: &#x27;Apple&#x27;, price: 1.99, quantity: 3},
  {name: &#x27;Xiomi&#x27;, price: 2.99, quantity: 2},
  {name: &#x27;Samsung&#x27;, price: 3.99, quantity: 1},
  {name: &#x27;Tesla&#x27;, price: 3.99, quantity: 1},
  {name: &#x27;Tesla&#x27;, price: 4.99, quantity: 4},
  {name: &#x27;Nokia&#x27;, price: 4.99, quantity: 4}
];</code></pre><p><strong>要求：</strong> 使用 reduce 实现归类。</p><p><strong>答案：</strong></p><pre class="language-javascript lang-javascript"><code class="language-javascript lang-javascript">const groupedCart = shoppingCart.reduce((result, item) =&gt; {
  // 若当前商品名称未在结果中，初始化空数组
  if (!result[item.name]) {
    result[item.name] = [];
  }
  // 将当前商品添加到对应数组
  result[item.name].push(item);
  return result;
}, {}); // 初始值为空对象

console.log(groupedCart);</code></pre><p><strong>输出：</strong></p><pre class="language-javascript lang-javascript"><code class="language-javascript lang-javascript">{
  Apple: [ {name: &#x27;Apple&#x27;, price: 1.99, quantity: 3}, {name: &#x27;Apple&#x27;, price: 1.99, quantity: 3} ],
  Xiomi: [ {name: &#x27;Xiomi&#x27;, price: 2.99, quantity: 2} ],
  Samsung: [ {name: &#x27;Samsung&#x27;, price: 3.99, quantity: 1} ],
  Tesla: [ {name: &#x27;Tesla&#x27;, price: 3.99, quantity: 1}, {name: &#x27;Tesla&#x27;, price: 4.99, quantity: 4} ],
  Nokia: [ {name: &#x27;Nokia&#x27;, price: 4.99, quantity: 4} ]
}</code></pre><p><strong>解析：</strong> reduce 遍历数组，以商品名称为键构建对象，将同名商品收集到对应数组中，实现归类。</p></div><p style="text-align:right"><a href="https://lingwang.ink/posts/exam/mm#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://lingwang.ink/posts/exam/mm</link><guid isPermaLink="true">https://lingwang.ink/posts/exam/mm</guid><dc:creator><![CDATA[ling]]></dc:creator><pubDate>Wed, 18 Mar 2026 06:15:27 GMT</pubDate></item><item><title><![CDATA[内存泄漏]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://lingwang.ink/posts/js/memoryleak">https://lingwang.ink/posts/js/memoryleak</a></blockquote><div><p>内存泄漏是指应用程序中的内存不再被使用但仍然被占用，导致内存消耗逐渐增加，最终可能导致应用程序性能下降或崩溃。内存泄漏通常是由于开发者编写的代码未正确释放不再需要的对象或数据而导致的。</p><h2 id="">内存泄漏的案例</h2><h3 id="1-">1. 意外的全局变量</h3><pre class="language-javascript lang-javascript"><code class="language-javascript lang-javascript">function someFunction() {
    // 这个变量会变成全局变量，并可能导致内存泄漏
    myobject = { /* ... */ };
}</code></pre><p><strong>问题</strong>：未使用 <code>var</code>、<code>let</code> 或 <code>const</code> 声明的变量会自动成为全局变量。</p><h3 id="2-">2. 闭包</h3><p>闭包可能会无意中持有对不再需要的变量或对象的引用，从而阻止它们被垃圾回收。</p><pre class="language-javascript lang-javascript"><code class="language-javascript lang-javascript">function createClosure() {
    const data = [/* 大量数据 */];
    return function() {
        // 闭包仍然持有对 &#x27;data&#x27; 的引用，即使它不再需要
        console.log(data);
    };
}

const closureFunction = createClosure();
// 当 &#x27;closureFunction&#x27; 不再需要时，它仍然保留着 &#x27;data&#x27; 的引用，导致内存泄漏。</code></pre><h3 id="3-">3. 事件监听器</h3><p>忘记移除事件监听器可能会导致内存泄漏，因为与监听器相关联的对象将无法被垃圾回收。</p><pre class="language-javascript lang-javascript"><code class="language-javascript lang-javascript">function createListener() {
    const element = document.getElementById(&#x27;myElement&#x27;);

    element.addEventListener(&#x27;click&#x27;, () =&gt; {
        // 事件处理逻辑
    });
}

createListener();
// 即使 &#x27;myElement&#x27; 从 DOM 中移除，该元素及其事件监听器仍然在内存中。</code></pre><h3 id="4-">4. 循环引用</h3><p>对象之间的循环引用会阻止它们被垃圾回收。</p><pre class="language-javascript lang-javascript"><code class="language-javascript lang-javascript">function createCircularReferences() {
    const obj1 = {};
    const obj2 = {};

    obj1.ref = obj2;
    obj2.ref = obj1;
}

createCircularReferences();
// 由于循环引用，&#x27;obj1&#x27; 和 &#x27;obj2&#x27; 都将保留在内存中。</code></pre><h3 id="5-settimeout--setinterval">5. setTimeout / setInterval</h3><p>使用 setTimeout 或 setInterval 时，如果没有正确清理，可能会导致内存泄漏，特别是当回调函数持有对大型对象的引用时。</p><pre class="language-javascript lang-javascript"><code class="language-javascript lang-javascript">function doSomethingRepeatedly() {
    const data = [/* 大量数据 */];

    setInterval(() =&gt; {
        // 闭包持有对 &#x27;data&#x27; 的引用，即使它不再需要
        console.log(data);
    }, 1000);
}

doSomethingRepeatedly();
// &#x27;doSomethingRepeatedly&#x27; 不再使用时，定时器仍然在运行，导致内存泄漏。</code></pre><h2 id="">如何避免内存泄漏</h2><ol start="1"><li><strong>使用严格模式</strong> (<code>&#x27;use strict&#x27;</code>) - 防止意外创建全局变量</li><li><strong>及时移除事件监听器</strong> <ul><li>使用 <code>removeEventListener</code> 或一次性监听器</li></ul></li><li><strong>清理定时器</strong> <ul><li>在组件卸载时调用 <code>clearInterval</code> 或 <code>clearTimeout</code></li></ul></li><li><strong>避免循环引用</strong> <ul><li>使用 <code>WeakMap</code> 或 <code>WeakSet</code> 让垃圾回收器自动清理</li></ul></li><li><strong>使用闭包时注意</strong> <ul><li>确保不再需要时释放闭包引用</li></ul></li><li><strong>使用工具检测</strong> <ul><li>Chrome DevTools Memory 面板、leak suspects 等</li></ul></li></ol></div><p style="text-align:right"><a href="https://lingwang.ink/posts/js/memoryleak#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://lingwang.ink/posts/js/memoryleak</link><guid isPermaLink="true">https://lingwang.ink/posts/js/memoryleak</guid><dc:creator><![CDATA[ling]]></dc:creator><pubDate>Tue, 17 Mar 2026 07:39:09 GMT</pubDate></item><item><title><![CDATA[节流]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://lingwang.ink/posts/compute/throtte">https://lingwang.ink/posts/compute/throtte</a></blockquote><div><blockquote><p>让函数在指定时间内，最多只能执行一次；如果频繁触发，也只会按固定频率执行。</p></blockquote><pre class="language-javascript lang-javascript"><code class="language-javascript lang-javascript">/**
 * @param {Function} func - 需要节流的目标函数
 * @param {number} wait - 节流时间间隔（毫秒），默认300ms
 * @returns {Function} - 包装后的节流函数
 */
function throttle(func, wait = 300) {
  // 保存上一次执行函数的时间戳
  let lastExecuteTime = 0;

  // 返回包装后的节流函数
  return function (...args) {
    // 保存当前上下文（比如DOM事件中的this）
    const context = this;
    // 获取当前时间戳
    const now = Date.now();

    // 核心逻辑：只有当前时间 - 上一次执行时间 &gt;= 节流间隔，才执行函数
    if (now - lastExecuteTime &gt;= wait) {
      // 执行原函数，传递上下文和参数
      func.apply(context, args);
      // 更新上一次执行时间为当前时间
      lastExecuteTime = now;
    }
    // 未达到间隔：直接忽略，不做任何处理（无最后一次执行）
  };
}</code></pre><pre class="language-javascript lang-javascript"><code class="language-javascript lang-javascript">import { useCallback, useRef } from &#x27;react&#x27;;

/**
 * React 节流 Hook
 * @param {Function} func - 需要节流的目标函数
 * @param {number} wait - 节流时间间隔（毫秒），默认300ms
 * @returns {[Function, Function]} - [包装后的节流函数, 重置节流的方法]
 */
function useThrottle(func, wait = 300) {
  // 用 useRef 保存上一次执行时间（跨渲染周期保存，避免重置）
  const lastExecuteTimeRef = useRef(0);
  // 用 useRef 保存最新的函数引用（解决闭包陷阱）
  const funcRef = useRef(func);

  // 每次渲染更新函数引用，保证拿到最新的func
  funcRef.current = func;

  // 用 useCallback 缓存节流函数，避免组件重渲染时重新创建
  const throttledFunc = useCallback((...args) =&gt; {
    // 保存当前上下文
    const context = this;
    // 获取当前时间戳
    const now = Date.now();

    // 核心节流逻辑：达到间隔才执行
    if (now - lastExecuteTimeRef.current &gt;= wait) {
      // 执行最新的业务函数
      funcRef.current.apply(context, args);
      // 更新上一次执行时间
      lastExecuteTimeRef.current = now;
    }
  }, [wait]);

  // 重置节流状态（可选：比如需要手动重置执行时间）
  const resetThrottle = useCallback(() =&gt; {
    lastExecuteTimeRef.current = 0;
  }, []);

  // 返回节流函数和重置方法
  return [throttledFunc, resetThrottle];
}

export default useThrottle;</code></pre><p>.</p></div><p style="text-align:right"><a href="https://lingwang.ink/posts/compute/throtte#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://lingwang.ink/posts/compute/throtte</link><guid isPermaLink="true">https://lingwang.ink/posts/compute/throtte</guid><dc:creator><![CDATA[ling]]></dc:creator><pubDate>Mon, 16 Mar 2026 15:01:55 GMT</pubDate></item><item><title><![CDATA[防抖]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://lingwang.ink/posts/compute/debounce">https://lingwang.ink/posts/compute/debounce</a></blockquote><div><blockquote><p>让一个函数在触发后，等待指定时间再执行；如果在等待期间函数再次被触发，则重新计时</p></blockquote>
<h2 id="">标准实现</h2><pre class="language-javascript lang-javascript"><code class="language-javascript lang-javascript">/**
 * 防抖函数
 * @param {Function} func - 需要防抖的目标函数
 * @param {number} wait - 等待时间（毫秒），默认300ms
 * @returns {Function} - 包装后的防抖函数
 */
function debounce(func, wait = 300) {
  // 保存定时器ID，用于清除计时
  let timeoutId = null;
  // 返回包装后的防抖函数
  return function (...args) {
    // 保存当前上下文（比如DOM事件中的this）
    const context = this;
    // 核心逻辑：每次触发时，先清除之前的定时器，重新计时
    if (timeoutId) clearTimeout(timeoutId);
    // 新建定时器，等待指定时间后执行目标函数
    timeoutId = setTimeout(() =&gt; {
      // 执行原函数，传递上下文和参数，避免this丢失、参数丢失
      func.apply(context, args);
      // 执行后清空定时器ID（非必需，但更规范）
      timeoutId = null;
    }, wait);
  };</code></pre><h2 id="react-hook">react hook封装</h2><pre class="language-javascript lang-javascript"><code class="language-javascript lang-javascript">import { useCallback, useRef } from &#x27;react&#x27;;

/**
 * React 防抖 Hook
 * @param {Function} func - 需要防抖的目标函数
 * @param {number} wait - 等待时间（毫秒），默认300ms
 * @returns {Function} - 包装后的防抖函数
 */
function useDebounce(func, wait = 300) {
  // 用 useRef 保存定时器ID（避免每次渲染重置）
  // 其次是我们在settimeout中用了这个变量，为了避免闭包陷阱，还是加上ref
  const timeoutIdRef = useRef(null);
  // 用 useRef 保存最新的函数引用（避免依赖变化导致防抖失效）
  // 其次是我们在settimeout和usecallback中用了这个变量，为了避免闭包陷阱，还是加上ref
  const funcRef = useRef(func);

  // 每次渲染更新函数引用，保证拿到最新的func
  funcRef.current = func;

  // 用 useCallback 缓存防抖函数，避免组件重渲染时重新创建
  const debouncedFunc = useCallback((...args) =&gt; {
    // 保存当前上下文（React 中通常是组件实例/事件对象）
    const context = this;

    // 清除之前的定时器，重新计时
    if (timeoutIdRef.current) {
      clearTimeout(timeoutIdRef.current);
    }

    // 新建定时器
    timeoutIdRef.current = setTimeout(() =&gt; {
      // 执行最新的函数引用
      funcRef.current.apply(context, args);
      // 执行后清空定时器ID
      timeoutIdRef.current = null;
    }, wait);
  }, [wait]);

  // 清理函数：组件卸载/防抖函数更新时，清除未执行的定时器
  const cancelDebounce = useCallback(() =&gt; {
    if (timeoutIdRef.current) {
      clearTimeout(timeoutIdRef.current);
      timeoutIdRef.current = null;
    }
  }, []);

  // 返回防抖函数和取消方法
  return [debouncedFunc, cancelDebounce];
}

export default useDebounce;</code></pre></div><p style="text-align:right"><a href="https://lingwang.ink/posts/compute/debounce#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://lingwang.ink/posts/compute/debounce</link><guid isPermaLink="true">https://lingwang.ink/posts/compute/debounce</guid><dc:creator><![CDATA[ling]]></dc:creator><pubDate>Mon, 16 Mar 2026 13:33:46 GMT</pubDate></item><item><title><![CDATA[2026晓多科技一面---转正实习]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://lingwang.ink/posts/exam/xiaoduo">https://lingwang.ink/posts/exam/xiaoduo</a></blockquote><div><h4 id="1-">1. 自我介绍</h4><h4 id="2-">2. 项目中遇到的难点和解决办法</h4><h4 id="3-react--useeffect">3. react 中的 useEffect经典闭包场景</h4><pre class="language-javascript lang-javascript"><code class="language-javascript lang-javascript">import { useState, useEffect } from &#x27;react&#x27;;

function Counter() {
  const [count, setCount] = useState(0);

  // 点击按钮触发的函数
  const handleClick = () =&gt; {
    setCount(1); // 先把count设为1
    
    // 3秒后尝试修改count
    setTimeout(() =&gt; {
      console.log(&#x27;定时器里的count值：&#x27;, count); // 关键：输出的是多少？
      setCount(count + 1); // 期望count变成2，实际会变成多少？
    }, 3000);
  };

  return (
    &lt;div&gt;
      &lt;p&gt;当前计数：{count}&lt;/p&gt;
      &lt;button onClick={handleClick}&gt;点击修改计数&lt;/button&gt;
    &lt;/div&gt;
  );
}

export default Counter;</code></pre><p>问 counter点击之后有什么变化</p>
<h4 id="4-">4. 同步代码、微任务、宏任务的执行顺序</h4><pre class="language-javascript lang-javascript"><code class="language-javascript lang-javascript">async function async1() {
  console.log(&#x27;async1 start&#x27;)
  await async2()
  console.log(&#x27;async1 end&#x27;)
}

async function async2() {
  console.log(&#x27;async2&#x27;)
}

console.log(&#x27;script start&#x27;)

setTimeout(() =&gt; {
  console.log(&#x27;setTimeout&#x27;)
}, 0)

async1()

new Promise(resolve =&gt; {
  console.log(&#x27;Promise1&#x27;)
  resolve()
}).then(() =&gt; {
  console.log(&#x27;Promise2&#x27;)
})

console.log(&#x27;script end&#x27;)</code></pre><p>回头看很简单，之前没做过这类题目，没做出来！</p><h4 id="5-">5. 项目中遇到性能优化问题没，是什么解决的，有什么常用方法？</h4><h4 id="6-react-usecallbackmemousememo">6. react中的 usecallback、memo、useMemo</h4><h4 id="7--fetch-sse-eventsource">7. 为什么项目中使用 fetch sse流而不是 EventSource?</h4><h4 id="8-sse">8. 在sse流的流式输出时候，你是如何解决输出抖动问题的？</h4><h4 id="9-ai-chat">9. ai-chat项目中有没有用到虚拟列表，它和普通列表相比，难点在哪？</h4><h4 id="10-">10. 如何实现在流式中，视口自动下拉？</h4><h4 id="11-httpslingwanginkpostsprojectscrollup">11. 如果在流式中，视口自动下拉，但是用户想上拉视口怎么解决？。此问题有完全总结答案<a href="https://lingwang.ink/posts/project/scrollup">https://lingwang.ink/posts/project/scrollup</a></h4><h4 id="12--">12. 讲一下原型链和作用域链 （作用域链没说出来！）</h4><h4 id="13-">13. 讲一下闭包</h4><h4 id="14--lodashflattenobject-">14. 手撕算法 lodash库中的flattenObject方法 （没解出来）</h4><pre class="language-javascript lang-javascript"><code class="language-javascript lang-javascript">输入：
const obj = {
  a: {
    b: {
      c: {
        d: &quot;value&quot;
      },
      e: &quot;e-value&quot;
    },
    f: &quot;f-value&quot;
  },
  g: &quot;g-value&quot;
};

要求输出：
{
  &quot;a.b.c.d&quot;: &quot;value&quot;,
  &quot;a.b.e&quot;: &quot;e-value&quot;,
  &quot;a.f&quot;: &quot;f-value&quot;,
  &quot;g&quot;: &quot;g-value&quot;
}


/**
 * 扁平化嵌套对象，将嵌套路径转换为点连接的键
 * @param {Object} target - 要扁平化的目标对象
 * @param {string} [prefix=&#x27;&#x27;] - 递归时的路径前缀
 * @param {Object} [result={}] - 存储结果的对象
 * @returns {Object} 扁平化后的对象
 */
function flattenObject(target, prefix = &#x27;&#x27;, result = {}) {
  // 遍历对象的每个键
  for (const key in target) {
    // 确保是对象自身的属性（排除原型链属性）
    if (target.hasOwnProperty(key)) {
      // 拼接当前路径：前缀 + 点（如果有前缀） + 当前键
      const currentKey = prefix ? `${prefix}.${key}` : key;
      const value = target[key];
      
      // 判断当前值是否是对象且不是 null（null 的 typeof 也是 object）
      if (typeof value === &#x27;object&#x27; &amp;&amp; value !== null) {
        // 递归处理嵌套对象
        flattenObject(value, currentKey, result);
      } else {
        // 非对象类型，直接存入结果
        result[currentKey] = value;
      }
    }
  }
  return result;
}

// 执行扁平化并打印结果
const flattenedObj = flattenObject(obj);
console.log(JSON.stringify(flattenedObj, null, 2));</code></pre><h4 id="15--hook">15. 因为 算法没做出来，面试官换了个简单的防抖hook，因为太紧张，防抖写错了，严重拉低预期</h4><h3 id="">总结</h3><p>本次面试一共持续 1 小时 3 分钟，面试官给出建议：“项目经验比较丰富，但是基础薄弱”，预期挂。自我评估：最近忙于项目和ai-coding,代码写的变少了，导致算法题和简单的utils工具方法都无从下手，头重脚轻。</p></div><p style="text-align:right"><a href="https://lingwang.ink/posts/exam/xiaoduo#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://lingwang.ink/posts/exam/xiaoduo</link><guid isPermaLink="true">https://lingwang.ink/posts/exam/xiaoduo</guid><dc:creator><![CDATA[ling]]></dc:creator><pubDate>Mon, 16 Mar 2026 13:07:36 GMT</pubDate></item><item><title><![CDATA[linums 聊天栏用户上拉脱敏方法]]></title><description><![CDATA[<link rel="preload" as="image" href="https://lingwang.ink/api/v2/objects/icon/02jngfw36fnhareidx.png"/><div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://lingwang.ink/posts/project/scrollup">https://lingwang.ink/posts/project/scrollup</a></blockquote><div><p>这个功能叫&quot;滚动锚点脱敏&quot;（Scroll AnchorDe-sensitization），目的是用户查看历史消息时，不被新消息顶走。
<img src="https://lingwang.ink/api/v2/objects/icon/02jngfw36fnhareidx.png" alt="dwadaw" height="912" width="764"/></p><p><strong>ScollHeight</strong>:是一个元素内容高度的度量，包括由于溢出导致的视图中不可见内容。<br/><strong>ScollTop</strong>: 是当前视口所滑动到的顶部位置高度。<br/><strong>ClientHeight</strong>：是当前视口高度。<br/></p><p>具体策略是计算用户当前视口底部( ScrollTop + ClientHeight) 与 消息列表元素内容高度之间的距离是否大于一定的阈值，本项目中设置为 100，具体计算公式为 distanceFromBottom = ScrollHeight - ( ScrollTop + ClientHeight)。</p><p><strong><em>Effect 实现：</em></strong></p><pre class="language-typescript lang-typescript"><code class="language-typescript lang-typescript">
  // ========== 监听用户滚动 ==========
  /**
   * 监听用户滚动行为
   *
   * 当用户滚动到距离底部超过 100px 时，进入&quot;脱敏模式&quot;
   * 脱敏模式下，新消息不会自动滚动到底部
   * 这让用户可以安心查看历史消息而不被新消息顶走
   */
  useEffect(() =&gt; {
    const container = scrollContainerRef.current
    if (!container) return

    const handleScroll = () =&gt; {
      const { scrollTop, scrollHeight, clientHeight } = container
      const distanceFromBottom = scrollHeight - scrollTop - clientHeight
      setUserScrolledUp(distanceFromBottom &gt; 100)
    }

    container.addEventListener(&#x27;scroll&#x27;, handleScroll, { passive: true })
    return () =&gt; container.removeEventListener(&#x27;scroll&#x27;, handleScroll)
  }, [])</code></pre></div><p style="text-align:right"><a href="https://lingwang.ink/posts/project/scrollup#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://lingwang.ink/posts/project/scrollup</link><guid isPermaLink="true">https://lingwang.ink/posts/project/scrollup</guid><dc:creator><![CDATA[ling]]></dc:creator><pubDate>Sat, 14 Mar 2026 06:50:34 GMT</pubDate></item></channel></rss>