面试分享:二本靠7轮面试成功拿下大厂P6

前言

时隔两年再次更新了,本文主要给大家带来一些我面试的经历和经验,希望对正在求职大厂前端岗位的同学有所帮助。我先大致说下面试之前的个人情况:2021年毕业去了一家500人左右的小公司,react和vue项目经验都有,两年半后拿礼包去了一家中厂,后来在这家公司待了一年左右最近面试京东拿到了P6的offer,差不多四年的工作经验终于拿到了大厂的入场券。

我写的篇幅可能比较长,这里讲一下为什么会有七轮面试,刚开始社区找人内推的北京团队,后来三面老板面因为工作地点的原因挂掉了(本人毕业后一直在上海工作),由于前面技术轮评价比较好,又找了上海的团队内推重新面了四轮才成功拿下。

好了,接下来就大概讲讲我的多轮京东面试

北京1轮

React 代码实现:在页面上实时显示窗口宽度的变化

import { useState, useEffect, useCallback } from 'react';

// 防抖函数
function debounce(func, wait) {
  let timeout;
  return function executedFunction(...args) {
    const later = () => {
      clearTimeout(timeout);
      func(...args);
    };
    clearTimeout(timeout);
    timeout = setTimeout(later, wait);
  };
}

// 自定义 Hook
function useWindowWidth(delay = 300) {
  const [width, setWidth] = useState(window.innerWidth);

  const handleResize = useCallback(() => {
    setWidth(window.innerWidth);
  }, []);

  useEffect(() => {
    // 创建防抖版本的 resize 处理函数
    const debouncedHandleResize = debounce(handleResize, delay);
    
    // 添加事件监听
    window.addEventListener('resize', debouncedHandleResize);
    
    // 立即执行一次以获取初始宽度
    handleResize();
    
    // 清理函数
    return () => {
      window.removeEventListener('resize', debouncedHandleResize);
    };
  }, [handleResize, delay]);

  return width;
}

// 使用示例
function WindowWidthDisplay() {
  const windowWidth = useWindowWidth();

  return (
    <div>
      <h2>窗口宽度实时显示</h2>
      <p>当前窗口宽度: {windowWidth}px</p>
    </div>
  );
}

export default WindowWidthDisplay;

 

react为什么不能在if里写useEffect

React hooks的调用顺序必须稳定,React 在内部通过调用顺序来区分和管理不同的 Hooks。例如:

function Component() {
  const [count, setCount] = useState(0); // Hook 1
  useEffect(() => {});                   // Hook 2
  const [name, setName] = useState(""); // Hook 3
}

 

React 会按顺序将这些 Hook 存储在一个链表中(类似 [Hook1, Hook2, Hook3] )。如果组件的多次渲染中 Hook 的调用顺序不一致(比如因为条件语句跳过某个 Hook),React 就无法正确匹配状态,导致 bug

useEffect的各种机制

useEffect 的工作流程依赖于 React 的渲染周期:

  1. 依赖项比对

    React 会对比当前渲染和上一次渲染的依赖项数组(deps),如果不同,会重新执行 useEffect 的回调。

  2. 清理与执行

    • 组件挂载时执行 useEffect
    • 依赖项变化时,先执行上一次的清理函数(如果存在),再执行新的 useEffect
    • 组件卸载时执行清理函数。

如果 Hook 的调用顺序不稳定,React 无法正确追踪哪些 useEffect 需要清理或更新

useRef和useState底层区别

useRef 和 useState 都是 React Hooks,但它们的用途和底层实现有显著不同:

特性useStateuseRef
存储的值存储状态,触发重新渲染存储可变值,不触发重新渲染
返回值[state, setState](状态 + 更新函数){current: value }(可变 ref 对象)
是否触发渲染✅ 调用 setState 会触发重新渲染❌ 修改 ref.current 不会触发渲染
底层存储位置在 Fiber 节点的memoizedState 链表里在 Fiber 节点的ref 属性上
试用场景需要 UI 响应的数据(如表单、计数)存储 DOM 引用、缓存变量、避免重复计算

底层实现对比

  • useState

    • React 在组件 Fiber 节点上维护一个 Hooks 链表,每个 useState 对应链表中的一个节点。
    • 调用 setState 会标记组件需要更新,触发 re-render
    • 状态变化后,React 会重新执行组件函数,并返回最新的 state
  • useRef

    • useRef 返回一个普通 JavaScript 对象 { current: initialValue }
    • 这个对象在组件的整个生命周期中保持不变(即使组件重新渲染)。
    • 修改 ref.current 不会触发 React 的更新机制,因为 React 不追踪 ref 的变化

useEffect能监听useRef的值发生改变吗?

默认情况下,useEffect 无法监听 useRef 的变化

原因分析

  1. useRef 的修改不会触发重新渲染

    useEffect 的依赖项机制依赖于 组件渲染,而 ref.current 的变化不会导致重新渲染,因此 useEffect 不会自动检测到变化。

  2. useRef 返回的对象始终是同一个引用

    const ref = useRef(0);
    // 即使修改 ref.current,ref 本身仍然是同一个对象
    ref.current = 1;// React 不会感知到这个变化
     

    由于 ref 对象在组件的整个生命周期中引用不变useEffect 的依赖项比对(Object.is)会认为它没有变化。

如何让 useEffect 监听 useRef 的变化?

虽然 useRef 本身不会触发 useEffect,但可以通过 额外状态 或 自定义 Hook 间接监听

function useWatchableRef(initialValue) {
  const ref = useRef(initialValue);
  const [version, setVersion] = useState(0);

  const setRef = (value) => {
    ref.current = value;
    setVersion(v => v + 1); // 更新版本号触发 useEffect
  };

  return [ref, setRef, version];
}

function Component() {
  const [ref, setRef, refVersion] = useWatchableRef(0);

  useEffect(() => {
    console.log("ref changed:", ref.current);
  }, [refVersion]); // 监听 version 变化
}

 

为什么 React 不直接支持监听 useRef

  1. 设计初衷不同

    • useRef 主要用于 存储可变值而不触发渲染(如 DOM 引用、定时器 ID)。
    • useState 才是用于 管理状态并触发 UI 更新 的。
  2. 性能优化

    • 如果 useRef 的每次修改都触发 useEffect,可能会导致不必要的副作用执行(比如频繁的 DOM 操作)。
  3. 避免滥用

    • 如果业务逻辑需要响应式更新,应该优先使用 useState 或 useReducer,而不是强行监听 useRef

手写算法 标签树的深度

const htmlStr = `
    <div>
        <div>
            <span>123</span>
            <a>222</a>
            <div>
                <button>333</button>
                <br/>
            </div>
        </div>
    </div>
`;

 

答案

function getHtmlTreeDepth(htmlStr) {
    let depth = 0;
    let maxDepth = 0;
    const stack = [];
    const tagRegex = /<\/?([a-z][a-z0-9]*)[^>]*>/gi;
    
    htmlStr.replace(tagRegex, (tag) => {
        if (tag.startsWith('</')) {
            // 闭合标签
            depth--;
            stack.pop();
        } else if (!tag.endsWith('/>')) {
            // 非自闭合的开放标签
            depth++;
            maxDepth = Math.max(maxDepth, depth);
            stack.push(tag.match(/<([a-z]+)/i)[1]);
        }
        // 自闭合标签(如 <br/>)不改变深度
        return tag;
    });

    return maxDepth;
}

const depth = getHtmlTreeDepth(htmlStr);
console.log(depth); // 输出: 4

 

北京2轮

ssr、ssg、rsc原理及优缺点

1. SSR(Server-Side Rendering,服务端渲染)

原理

  • 用户请求页面时,服务器实时执行 JavaScript,生成完整 HTML 并返回给浏览器。
  • 浏览器接收到 HTML 后直接显示,然后再进行 hydration(注水) 使其可交互。

优点

✅ 首屏加载快:用户直接看到渲染好的内容,无需等待 JS 执行。

✅ SEO 友好:搜索引擎爬虫能直接获取完整 HTML。

✅ 低端设备兼容:不依赖客户端 JS 能力,适合性能较差的设备。

缺点

❌ 服务器压力大:每次请求都需要实时渲染,高并发时可能拖慢服务器。

❌ TTFB(首字节时间)延迟:用户需等待服务器生成 HTML。

❌ 交互延迟:必须等 hydration 完成后才能交互。

适用场景

  • 需要 SEO 的内容型网站(如新闻、博客)。
  • 对首屏速度要求高的应用。

2. SSG(Static Site Generation,静态站点生成)

原理

  • 在 构建阶段 提前生成所有页面的静态 HTML,直接托管到 CDN。
  • 用户访问时,CDN 直接返回预渲染的 HTML,无需服务器计算。

优点

✅ 极致性能:CDN 直接返回 HTML,速度极快。

✅ 零服务器压力:无需运行时渲染,节省计算资源。

✅ 高安全性:没有动态服务器逻辑,减少攻击面。

缺点

❌ 不适合动态内容:数据更新必须重新构建。

❌ 规模化成本高:页面数量多时,构建时间会很长。

❌ 无法个性化:所有用户看到相同的内容(除非结合客户端 JS)。

适用场景

  • 内容不经常变的网站(如文档、营销页)。
  • 需要极致性能的场景。

3. RSC(React Server Components,React 服务端组件)

原理

  • 按组件粒度 区分哪些在服务端渲染,哪些在客户端渲染。
  • 服务端组件直接访问数据库/API,返回 JSON 数据,客户端组合渲染。

优点

✅ 减少客户端 JS 体积:部分逻辑留在服务端,前端代码更小。

✅ 自动代码拆分:按需加载组件,优化性能。

✅ 直接访问后端:服务端组件可直接调用 DB/API,无需额外接口。

缺点

❌ 复杂度高:需要处理服务端/客户端组件的通信问题。

❌ 兼容性要求:必须使用 React 18+ 和特定框架(如 Next.js)。

❌ 调试困难:跨环境调试较麻烦。

适用场景

  • 复杂应用,需要优化包大小和加载速度。
  • 需要服务端直接获取数据的场景(如仪表盘、管理后台)。

node ssr什么原因引起的堵塞,怎么解决

  1. CPU 密集型任务阻塞事件循环
  • 复杂组件渲染(如大型列表、深度递归组件)
  • 繁重的模板计算(如Pug/EJS模板处理大数据)
  • 加密/解密操作(如JWT验证)
  1. 同步I/O操作阻塞
  • 同步文件读写(fs.readFileSync
  • 同步数据库查询(某些ORM的同步方法)
  • 阻塞式网络请求(未正确使用Promise)
  1. 内存泄漏
  • 全局变量累积(如缓存未清理)
  • 闭包引用未释放
  • 事件监听器未移除
  1. 资源竞争
  • 数据库连接池耗尽
  • 文件描述符耗尽
  • 子进程管理不当

解决方案

1.架构层优化

// 使用React的renderToNodeStream
import { renderToNodeStream } from 'react-dom/server';

app.get('/', (req, res) => {
  res.write('<!DOCTYPE html>');
  const stream = renderToNodeStream(<App />);
  stream.pipe(res, { end: false });
  stream.on('end', () => res.end());
});

 

2.渲染性能优化

// 使用React的renderToNodeStream
2import { renderToNodeStream } from 'react-dom/server';
3
4app.get('/', (req, res) => {
5  res.write('<!DOCTYPE html>');
6  const stream = renderToNodeStream(<App />);
7  stream.pipe(res, { end: false });
8  stream.on('end', () => res.end());
9});

 

3.异步I/O和非阻塞操作

// 使用Promise.all优化并行请求
async function fetchData() {
  const [user, posts] = await Promise.all([
    UserModel.findById(userId),
    PostModel.findByUser(userId)
  ]);
  return { user, posts };
}

 

数据库/API 调用优化:

  • 使用 Data Cache(如 Redis)减少重复查询。
  • 对慢接口设置 超时 或 降级处理

站点seo怎么做的

1.语义化 HTML

  • 使用 <h1> ~ <h6> 合理嵌套标题
  • 使用 <section><article><nav> 等语义化标签
  • 图片必须加 alt 属性

2.Meta 标签优化

<head>
  <title>网站标题 - 重要关键词</title>
  <meta name="description" content="网站描述,包含核心关键词" />
  <meta name="keywords" content="关键词1, 关键词2" />
  <!-- 避免被转码 -->
  <meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />
  <!-- 移动端适配 -->
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <!-- 禁止搜索引擎转码(如百度) -->
  <meta http-equiv="Cache-Control" content="no-transform" />
  <meta http-equiv="Cache-Control" content="no-siteapp" />
</head>

 

3.前端框架(React/Vue)SEO优化

  • Next.js(React) / Nuxt.js(Vue) 默认支持 SSR
  • 静态生成(SSG)
  • React Helmet(Next.js 内置 Head 组件)

4.Sitemap(站点地图)

作用

  • 告诉搜索引擎网站有哪些页面可被抓取
  • 标注页面的更新频率(lastmod)和优先级(priority
  • 尤其适合 大型网站 或 动态内容网站(如电商、新闻站)

手动编写 Sitemap

<!-- public/sitemap.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="<http://www.sitemaps.org/schemas/sitemap/0.9>">
  <url>
    <loc><https://example.com/></loc>
    <lastmod>2023-10-01</lastmod>
    <changefreq>daily</changefreq>
    <priority>1.0</priority>
  </url>
</urlset>

 

5.Robots.txt(爬虫协议)

作用

  • 控制搜索引擎哪些页面可以抓取
  • 禁止抓取敏感页面(如后台、临时页)

标准配置

txt

# public/robots.txt
User-agent: *          # 对所有爬虫生效
Disallow: /admin/      # 禁止抓取后台
Disallow: /tmp/        # 禁止抓取临时文件
Allow: /public/        # 允许抓取 public 目录
Crawl-delay: 2         # 抓取延迟(秒),防止服务器压力

# 声明 Sitemap
Sitemap: <https://example.com/sitemap.xml>

北京3轮

平时怎么学习?遇到比较难的问题怎么解决?讲一下工作中遇到的问题

上海1轮

手写算法 版本号列表排序

给定一个版本号列表从小到大排序 如[1.0.1, 0.1, 1.3.26, 1.0.3.29, 2.1.3, 1.0.9.7.25]

function sortVersions(versions) {
  return versions.sort((a, b) => {
    const partsA = a.split('.').map(Number);
    const partsB = b.split('.').map(Number);
    
    const maxLength = Math.max(partsA.length, partsB.length);
    
    for (let i = 0; i < maxLength; i++) {
      const numA = partsA[i] || 0;
      const numB = partsB[i] || 0;
      
      if (numA !== numB) {
        return numA - numB;
      }
    }
    
    return 0;
  });
}

// 测试
const versions = ["1.0.1", "0.1", "1.3.26", "1.0.3.29", "2.1.3", "1.0.9.7.25"];
console.log(sortVersions(versions));
// 输出: ["0.1", "1.0.1", "1.0.3.29", "1.0.9.7.25", "1.3.26", "2.1.3"]

 

聊聊ssr和segment ssr架构以及rsi相关的

1.传统 SSR(服务端渲染)

运行时渲染:用户请求时,服务器动态生成完整 HTML,工作流程:

用户请求
->NodeJs服务器
->执行React/vue组件渲染
->返回完整HTML+客户端js
->浏览器Hydration交互

 

✅ 优点: 首屏速度快、SEO友好、低端设备兼容

❌缺点:高服务器负载、 TTFB(首字节时间)较长、 TTFB(首字节时间)较长

2.Segment SSR(分段服务端渲染)

  • 流式分块渲染:将页面拆分为多个可独立渲染的模块(Segments)
  • 渐进式传输:边渲染边传输,而非等待整个页面完成
// 1. 定义可分段组件
import { Suspense } from 'react';

function Page() {
  return (
    <div>
      <Header />
      <Suspense fallback={<Spinner />}>
        <ProductList />  // 异步加载的模块
      </Suspense>
      <Footer />
    </div>
  );
}

// 2. 服务端流式响应
import { renderToPipeableStream } from 'react-dom/server';

app.get('/', (req, res) => {
  const stream = renderToPipeableStream(<Page />);
  stream.pipe(res);
});

 

✅ 优势

  • 关键内容优先渲染(Critical CSS/JS),非关键模块延迟加载
  • 服务器CPU时间分段使用,减少内存峰值压力

3.RSI(Remote Server Includes)

边缘包含技术:将页面拆分为多个独立服务渲染的片段,在CDN边缘组合

与传统SSR对比

- 传统SSR: [DB → 完整渲染 → HTML]
+ RSI:    [DB → 片段A渲染] 
           [API → 片段B渲染] → CDN组合
// CDN边缘逻辑示例(伪代码)
async function handleRequest(request) {
  const [header, body] = await Promise.all([
    fetch('<https://service-a/render-header>'),
    fetch('<https://service-b/render-body>')
  ]);
  
  return new Response(`
    ${await header.text()}
    ${await body.text()}
  `);
}

 

✅ 优势

  • 减少回源延迟,单个片段失败不影响整体
  • 静态部分预生成,动态部分按需渲染

微前端底层怎么实现的,css隔离、通信怎么做

1. 应用隔离机制

proxy沙箱:

  • 每个子应用运行在独立的proxy上下文
  • 卸载时清除所有副作用
// 基于Proxy的沙箱实现
class Sandbox {
  constructor() {
    const fakeWindow = {};
    this.proxy = new Proxy(fakeWindow, {
      get(target, key) {
        return target[key] || window[key];
      },
      set(target, key, value) {
        target[key] = value;
        return true;
      }
    });
  }
}

 

CSS隔离:

Shadow DOM天然隔离样式

const shadow = element.attachShadow({ mode: 'open' });
2shadow.innerHTML = `<style>h1 { color: red; }</style>`;

 

Scoped CSS通过前缀隔离(如**qiankunscoped-css**)

通信机制:

Custom Events

// 子应用发送事件
window.dispatchEvent(new CustomEvent('micro-event', {
  detail: { type: 'login' }
}));

// 主应用监听
window.addEventListener('micro-event', handler);

 

聊下ai相关的llm、mcp的 对日常工作作用

这里具体自己翻文档了解使用和简单原理,具体可以看看mcp文档

上海2轮

编辑器的多人协作冲突原理

OT (Operational Transformation) 算法

将编辑操作转换为原子操作(如**insert(pos, text) delete(pos, length)**),通过转换函数解决操作冲突

// 操作转换函数示例
function transform(op1, op2) {
  // 如果op2在op1之前发生,调整op1的位置
  if (op1.type === 'insert' && op2.type === 'insert') {
    if (op1.pos <= op2.pos) {
      return { ...op1 }; // op1不需要修改
    } else {
      return { ...op1, pos: op1.pos + op2.text.length };
    }
  }
  // 其他情况处理...
}

 

CRDT (Conflict-Free Replicated Data Type)

将数据结构设计保证最终一致性,每个编辑操作天生可合并

// 使用唯一ID标识每个字符
const doc = {
  "id1": { char: 'H', id: 'id1', pos: 0.1 },
  "id2": { char: 'i', id: 'id2', pos: 0.2 }
};
// 插入新字符时分配介于两者之间的position
insertAfter('id1', '!') → pos = (0.1 + 0.2)/2 = 0.15

 

webpack的loader和plugin原理

Loader 的作用

  • 转换文件内容:将非 JS 文件(如 .css.vue)转换成 Webpack 可识别的模块。
  • 链式调用:多个 Loader 可以串联执行(如 sass-loader → css-loader → style-loader

Loader的底层实现

Loader 本质是一个函数,接收源文件内容,返回转换后的内容:

// 一个简单的 Loader 示例
    module.exports = function(source) {
      // 1. 处理 source(如编译、转换)
      const transformed = source.replace('red', 'blue');
      
      // 2. 返回处理后的内容(可以是 JS/CSS/其他)
      return transformed;
      
      // 可选:返回多个结果(如 source + sourceMap)
      // this.callback(null, transformed, sourceMap);
    };

 

Plugin 的作用

  • 扩展构建流程:在 Webpack 编译的不同阶段注入自定义逻辑。
  • 访问 Webpack 内部:可以修改模块、优化 chunks、生成 assets 等。

Plugin的底层原理

Plugin 是一个,必须实现 apply 方法:

 class MyPlugin {
      apply(compiler) {
        // 1. 注册钩子
        compiler.hooks.emit.tap('MyPlugin', (compilation) => {
          // 2. 操作 compilation(如修改生成的资源)
          compilation.assets['new-file.txt'] = {
            source: () => 'Hello Webpack!',
            size: () => 13,
          };
        });
      }
    }

    module.exports = MyPlugin;

 

关键点

  • compiler 代表整个 Webpack 环境。
  • compilation 包含当前构建的所有信息(模块、chunks、assets)。
  • 可以通过 Tapable 的 tapAsync/tapPromise 处理异步逻辑。

webpack的底层打包原理

1.依赖图构建

从入口文件出发,递归分析所有依赖,形成完整的模块关系图

根据 webpack.config.js 中的 entry 配置(如 ./src/index.js),创建初始模块 遍历 AST 找到所有 import/require 语句,收集依赖路径

递归处理子模块,对每个依赖路径解析为绝对路径

最终生成一个 Map 结构的依赖图:

const dependencyGraph = {
      './src/index.js': {
        id: './src/index.js',
        dependencies: ['./src/utils.js'],
        source: '...'
      },
      './src/utils.js': {
        id: './src/utils.js',
        dependencies: ['lodash'],
        source: '...'
      },
      'lodash': { ... }
    };

 

2. Chunk 生成

  • Entry Chunk: 每个 entry 配置生成一个初始 Chunk
  • Async Chunk: 动态导入(import() )自动生成新 Chunk
  • SplitChunks 优化: 根据 optimization.splitChunks 配置拆分公共模块,如将 lodash 等第三方库提取到 vendors Chunk

3.模块转译

  • Loader 链式处理: 按配置顺序调用 Loader 处理模块内容
  • 生成运行时兼容代码: 将 ESM 的 import 转为 Webpack 的 __webpack_require__

4.模板渲染 生成 Runtime 代码包含模块加载、缓存等逻辑

 // 运行时伪代码
    function __webpack_require__(moduleId) {
      if (cache[moduleId]) return cache[moduleId].exports;
      const module = { exports: {} };
      modules[moduleId](module, module.exports, __webpack_require__);
      return module.exports;
    }

 

拼接 Chunk 内容

 (function(modules) {
      // Runtime 代码...
    })({
      './src/index.js': function(module, exports, __webpack_require__) {
        // 模块代码
      },
      './src/utils.js': function(...) { ... }
    });

 

生成 Asset 文件

根据 output.filename 规则命名文件(如 [name].[contenthash].js),如果配置了 devtool 还会生成SourceMap文件

5.优化阶段

Tree Shaking 基于 ES Module 的静态分析,标记未使用的导出

 // 源码:
    export function a() {}
    export function b() {}  // 未被导入

    // 打包后:
    /* unused harmony export b */

 

Scope Hoisting合并模块作用域,减少闭包

// 优化前:
    (function(module) {
      module.exports = 1;
    });

    // 优化后:
    module.exports = 1;  // 直接内联

 

代码压缩 使用 TerserPlugin 混淆和压缩 JS

讲下esm、cmd、umd区别

1. ESM

ES6 引入的模块化方案,现代浏览器和 Node.js 原生支持,依赖关系在编译时确定

运行环境:

  • 浏览器: <script type="module">
  • Node.js: .mjs 文件或 package.json 设置 "type": "module"

✅优点:静态分析支持 Tree Shaking、浏览器原生支持,无需打包工具(但生产环境仍需打包优化)、支持循环引用(模块只被加载一次,存在缓存中)

❌缺点:旧浏览器不支持(需通过 Webpack/Babel 转译)、Node.js 中与 CommonJS 混用需注意兼容性

2.CMD

NodeJS默认规范,主要用于服务端,依赖关系在运行时确定,模块同步加载会阻塞执行

运行环境:

  • Node.js 原生支持
  • 浏览器需通过 Webpack/Browserify 打包

✅优点:Node.js 生态默认支持、简单易用

❌缺点:同步加载不适合浏览器环境(会导致性能问题)、不支持 Tree Shaking

3.UMD

同时兼容 ESM、CMD 和浏览器全局变量,根据环境自动选择导出方式

 (function (root, factory) {
      if (typeof define === 'function' && define.amd) {
        // AMD 规范(如 RequireJS)
        define(['jquery'], factory);
      } else if (typeof exports === 'object') {
        // CommonJS 规范
        module.exports = factory(require('jquery'));
      } else {
        // 浏览器全局变量
        root.MyLib = factory(root.jQuery);
      }
    }(this, function ($) {
      // 模块逻辑
      return { name: 'UMD' };
    }));

 

运行环境

  • 浏览器:直接引入 <script> 或通过 AMD 加载器
  • Node.js:直接 require

✅优点:跨环境兼容(浏览器/Node.js/AMD)、适合库开发(如 jQuery、Lodash)

❌缺点:代码冗余(包含多种兼容逻辑)、无法利用 ESM 的静态优化

上海3轮

聊项目深度、聊解决问题思路

上海4轮

这一轮面试官是Taro团队的,也是技术深度和面试难度最大的一轮

react18以前批处理是啥样的? react18+的批处理怎么做的,从底层讲讲

1.React 18 之前的批处理

仅合并合成事件:在 React 事件处理函数(如 onClick)中的多个 setState 会被批量处理,React 使用事务机制延迟更新(异步批处理)到事件结束

function App() {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);

  const handleClick = () => {
    setCount(c => c + 1);  // 不会立即触发渲染
    setFlag(f => !f);      // 不会立即触发渲染
    // React 会将这两个更新合并为一次渲染
  };

  return <button onClick={handleClick}>Click</button>;
}

 

局限性: setTimeout/Promise/原生事件 中的 setState 会立即触发更新

setTimeout(() => {
  setCount(c => c + 1); // 立即渲染
  setFlag(f => !f);     // 再次渲染
}, 0);

 

2.React 18+ 的自动批处理

全场景批处理:合并所有上下文中的更新(包括事件、定时器、Promise、原生事件等),使用新的并发调度器(Scheduler)管理更新优先级

// 以下所有场景都会批量处理!
function App() {
  const handleClick = () => {
    setCount(c => c + 1);
    setFlag(f => !f); 
    // 合并为一次渲染(即使放在 setTimeout 中)
  };

  const fetchData = async () => {
    const res = await fetch('/api');
    setData(res);
    setLoading(false); // 异步操作后仍会批量处理
  };

  return <button onClick={handleClick}>Click</button>;
}

 

底层实现:

1.更新队列:所有更新被放入共享队列,通过 lane(车道)模型标记优先级

function dispatchSetState(fiber, queue, action) {
  const update = {
    lane: requestUpdateLane(fiber), // 分配优先级
    action,
    next: null
  };
  enqueueUpdate(fiber, queue, update); // 入队
  scheduleUpdateOnFiber(fiber); // 调度更新
}

 

2.调度合并

function scheduleUpdateOnFiber(root, fiber, lane) {
  markRootUpdated(root, lane); // 标记待处理更新
  ensureRootIsScheduled(root); // 统一调度
}

 

  1. 车道模型(Lane)

export const SyncLane = 0b0001; // 同步优先级
export const InputContinuousLane = 0b0010; // 连续输入(如滚动)
export const DefaultLane = 0b0100; // 默认优先级

 

useLayoutEffect和useEffect啥区别,从底层讲讲

1.核心区别总结

特性useLayoutEffectuseEffect
执行时机DOM 更新后、浏览器绘制前(同步)浏览器绘制后(异步)
堵塞渲染✅ 会阻塞浏览器绘制❌ 不阻塞
适用场景需要同步计算 DOM 布局的场景数据获取、事件订阅等副作用
源码阶段commitLayoutEffects 阶段执行commitBeforeMutationEffects 阶段调度

2.底层执行流程

React 的渲染分为 Render 阶段 和 Commit 阶段,两者在 Commit 阶段的不同时机执行 Commit阶段

  • BeforeMutation:调度 useEffect 的销毁函数(如果依赖项变化)
  • Mutation:实际更新 DOM(React 17+ 使用 Fiber 直接操作)。
  • Layout:执行 useLayoutEffect 的回调、更新 **ref、**执行 componentDidMount/Update(类组件)

useLayoutEffect 执行细节

同步执行 在 commitLayoutEffects 函数中直接调用

// ReactFiberCommitWork.js
function commitLayoutEffects(root) {
  while (nextEffect !== null) {
    if (effectTag & Layout) {
      const effect = nextEffect.updateQueue.lastEffect;
      if (effect !== null) {
        do {
          if (effect.tag === Layout) {
            effect.destroy = effect.create(); // 同步执行回调
          }
          effect = effect.next;
        } while (effect !== firstEffect);
      }
    }
    nextEffect = nextEffect.nextEffect;
  }
}

 

阻塞流程 由于在浏览器绘制前执行,长时间运行会导致页面卡顿

useEffect 执行细节

异步调度 在 commitBeforeMutationEffects 阶段标记,由 Scheduler 异步执行

// ReactFiberWorkLoop.js
function scheduleCallback(priorityLevel, callback) {
  // 将 effect 放入调度队列
  Scheduler_scheduleCallback(priorityLevel, callback);
}

 

实际执行时机 React 17 使用 requestIdleCallback 的 polyfill,React 18 使用 Scheduler 包的优先级调度

fiber是什么

Fiber 是 React 16+ 的核心调度单元,解决了传统 Stack Reconciler(React 15 及之前)的不可中断问题

React 将组件树转换为 Fiber 链表(深度优先遍历),每个 Fiber 节点对应一个 React 元素(组件/DOM 节点),包含以下关键属性:

ts

//fiber数据结构
interface Fiber {
  // 标识信息
  tag: WorkTag;        // 组件类型(函数组件/类组件/DOM节点等)
  key: string | null;  // 同级节点唯一标识
  type: any;           // 组件构造函数或DOM标签名(如 'div')

  // 树结构关系
  return: Fiber | null; // 父节点
  child: Fiber | null;  // 第一个子节点
  sibling: Fiber | null;// 下一个兄弟节点
  alternate: Fiber | null; // 当前节点对应的旧Fiber(用于Diff)

  // 状态与副作用
  memoizedState: any;   // Hook链表(函数组件状态)
  stateNode: any;       // 实例(DOM节点/类组件实例)
  flags: Flags;         // 标记需要进行的操作(如Placement/Update)
  lanes: Lanes;         // 优先级(车道模型)

  // 工作进度
  pendingProps: any;    // 待处理的props
  memoizedProps: any;   // 上次渲染的props
}

React调度怎么做的

React 18 引入 车道模型(Lane) 管理任务优先级 1.优先级划分

export const SyncLane = 0b0001;       // 同步任务(如点击事件)
export const InputContinuousLane = 0b0010; // 连续输入(如滚动)
export const DefaultLane = 0b0100;     // 普通更新

 

2.优先级抢占

高优先级任务可中断低优先级任务:

function ensureRootIsScheduled(root) {
  // 获取最高优先级任务
  const nextLanes = getNextLanes(root);
  if (nextLanes === NoLanes) return;

  // 如果存在更高优先级任务,取消当前任务
  if (existingCallbackPriority !== newCallbackPriority) {
    cancelCallback(existingCallbackNode);
  }
}

 

react可执行中断渲染从底层怎么做到的

1.关键机制:时间切片

React 将渲染任务拆分为多个 Fiber 工作单元,通过浏览器 API(如 requestIdleCallback 或 MessageChannel)在空闲时段执行

2.双缓存技术

React 维护两棵 Fiber 树:

  • Current Tree:当前渲染的树(对应屏幕上显示的内容)。
  • WorkInProgress Tree:正在构建的新树(可中断修改)。

中断时:保存当前处理的 Fiber 节点指针(workInProgress恢复时:从上次中断的 Fiber 节点继续处理

function workLoopConcurrent() {
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
  // 如果被中断,workInProgress会保留当前节点
}

 

微信小程序架构具体怎么做的

微信小程序的架构设计采用了独特的双线程模型,将**视图层(View Layer)逻辑层(App Service Layer)**分离,并通过微信客户端(Native)进行通信,以实现高性能、安全性和跨平台一致性

双线程模型

  • 视图层(View Layer) :运行在单独的 WebView 线程,负责 UI 渲染。
  • 逻辑层(App Service Layer) :运行在 JavaScriptCore(iOS)/V8(Android) 线程,处理业务逻辑和数据管理。
  • Native 层:微信客户端作为中间层,负责通信、权限管理和原生能力调用,wx API底层依赖于Bridge机制

wasm是什么

WebAssembly(WASM)是一种二进制指令格式,设计用于在浏览器中高效执行接近原生速度的代码,WASM 的代码通常由 C/C++/Rust 等语言编译而来,可与 JavaScript 无缝交互使用 以 Rust + WASM 为例

rust代码用wasm-pack编译成wasm

// src/lib.rs
#[no_mangle]
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

 

js调用wasm

import init, { add } from './pkg/your_module.js';
await init();
console.log(add(2, 3)); // 输出 5

 

Rust所有权

Rust的所有权用于管理内存和资源的生命周期,确保内存安全而无需垃圾回收。简单来说,所有权规则主要包括三条:

1.每个值有且只有一个所有者(Owner)

String 类型的数据存储在堆上,赋值时发生 所有权转移(Move),s1 不再有效

let s1 = String::from("hello"); // s1 是所有者
let s2 = s1;                    // 所有权从 s1 转移到 s2
println!("{}", s1);             // 编译错误!s1 已失效

 

2.所有权可以转移(Move),但不能共享(除非借用)

同一作用域内可存在多个不可变引用,不允许同时存在可变引用(防止数据竞争)

let s = String::from("hello");
let len = calculate_length(&s); // 传递不可变引用

fn calculate_length(s: &String) -> usize {
    s.len()                     // 可读但不可修改
}

 

当所有者变量离开作用域时,该值会被自动清理释放内存(调用 drop)

fn main() {
    {
        // s 进入作用域
        let s = String::from("hello"); // 在堆上分配内存
        println!("{}", s); // 输出 "hello"
    } // s 离开作用域,自动调用 drop 释放内存
    // 此处无法再访问 s
}

 

用React做跨端小程序框架实现

1.技术选型

  • React:作为核心渲染库,利用其虚拟 DOM 能力。
  • 自定义渲染器(如 React Reconciler) :实现跨端渲染调度能力。
  • 平台适配层:将虚拟 DOM 映射到不同平台(如微信小程序、支付宝小程序、Web等)的原生组件。
  • JSX 语法:统一开发体验。
  • 打包工具:如 webpack、Vite,结合 Babel 进行代码转换。

2.平台适配

  • Web:直接用 ReactDOM 渲染。
  • 小程序:将虚拟 DOM 转换为小程序的 JSON 结构和 WXML。
  • 原生 App:可用 React Native 或自定义桥接。

3.运行时 提供一套跨端组件(如 View、Text、Image 等),在不同平台下有不同实现,统一事件、生命周期、样式等 API

怎么做node cli的插件化,扩展给外部开发者定制化

1. 插件化的基本思路

  1. 插件发现:CLI 能自动发现和加载外部插件。
  2. 插件注册:插件能注册命令、参数、钩子等。
  3. 插件通信:插件和主程序、插件之间可以通信。
  4. 插件隔离:插件运行环境与主程序隔离,避免冲突。

2.常见实现方式

插件 API 设计: 主程序暴露 API 给插件,插件通过 API 注册命令、钩子等

主程序


class CLI {
  constructor() {
    this.commands = {};
  }
  registerCommand(name, fn) {
    this.commands[name] = fn;
  }
  run(argv) {
    const cmd = argv[2];
    if (this.commands[cmd]) {
      this.commands[cmd]();
    }
  }
}

const cli = new CLI();
module.exports = cli;

 

插件

module.exports = function(api) {
  api.registerCommand('hello', () => {
    console.log('Hello from plugin!');
  });
};

 

最后如有错误,请告知提醒

原文:面试分享:二本靠7轮面试成功拿下大厂P6时隔两年再次更新了,本文主要给大家带来一些我面试的经历和经验,希望对正在求职京东 - 掘金

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值