一次网易外包技术面试的复盘总结

166 阅读19分钟

一次网易外包技术面试的复盘总结

最近去面了汉克时代外包给网易的前端岗位,base杭州,16k*12,感觉面试官问的问题还挺有深度的。今天整理一下这次面试的题目和我的思考,希望能帮到正在准备面试的朋友们。

手写题环节

面试官让我现场手写防抖和节流函数。说实话,这两个函数平时项目里用lodash比较多,但是原理还是要理解的。

防抖函数实现

function debounce(func, wait, immediate = false) {
  let timeout;
  return function executedFunction(...args) {
    const later = () => {
      timeout = null;
      if (!immediate) func.apply(this, args);
    };
    
    const callNow = immediate && !timeout;
    clearTimeout(timeout);
    timeout = setTimeout(later, wait);
    
    if (callNow) func.apply(this, args);
  };
}

// 支持取消功能的版本
function debounceWithCancel(func, wait) {
  let timeout;
  
  const debounced = function(...args) {
    clearTimeout(timeout);
    timeout = setTimeout(() => func.apply(this, args), wait);
  };
  
  debounced.cancel = function() {
    clearTimeout(timeout);
    timeout = null;
  };
  
  return debounced;
}

写完基础版本后,面试官问我还能想到什么优化。我提到了immediate参数和cancel方法。immediate用于首次调用立即执行,这在某些场景下很有用,比如按钮点击防重复提交。

节流函数实现

// 时间戳版本
function throttleByTimestamp(func, wait) {
  let previous = 0;
  return function(...args) {
    const now = Date.now();
    if (now - previous > wait) {
      previous = now;
      func.apply(this, args);
    }
  };
}

// 定时器版本
function throttleByTimer(func, wait) {
  let timeout;
  return function(...args) {
    if (!timeout) {
      timeout = setTimeout(() => {
        timeout = null;
        func.apply(this, args);
      }, wait);
    }
  };
}

// 结合版本 - 首次立即执行,结束后再执行一次
function throttle(func, wait, options = {}) {
  let timeout, context, args, result;
  let previous = 0;
  
  const later = function() {
    previous = options.leading === false ? 0 : Date.now();
    timeout = null;
    result = func.apply(context, args);
    if (!timeout) context = args = null;
  };
  
  return function(...params) {
    const now = Date.now();
    if (!previous && options.leading === false) previous = now;
    
    const remaining = wait - (now - previous);
    context = this;
    args = params;
    
    if (remaining <= 0 || remaining > wait) {
      if (timeout) {
        clearTimeout(timeout);
        timeout = null;
      }
      previous = now;
      result = func.apply(context, args);
      if (!timeout) context = args = null;
    } else if (!timeout && options.trailing !== false) {
      timeout = setTimeout(later, remaining);
    }
    
    return result;
  };
}

面试官问我这两种版本的区别。时间戳版本首次调用会立即执行,但最后一次可能不会执行。定时器版本首次调用不会立即执行,但会保证最后一次执行。结合版本可以通过options控制这些行为。

正式面试问题

1. 讲一下最近的项目以及难点亮点

我的回答:

"我最近做的项目是一个中后台管理系统,主要负责数据可视化模块。技术栈用的React + TypeScript + Ant Design。

遇到的最大难点是性能问题。因为页面要展示大量的图表和实时数据,初始版本在数据量大的时候会出现明显的卡顿。我从几个方面进行了优化:

首先是数据层面,实现了虚拟滚动,对于长列表只渲染可视区域的内容。然后是组件层面,用React.memo和useMemo减少不必要的重新渲染。最关键的是实现了一个自定义的useVirtualChart hook,对图表进行懒加载和按需渲染。

亮点的话,我觉得是自己实现了一套图表组件的缓存机制。因为用户经常在不同的数据维度间切换,我用WeakMap存储组件实例,结合LRU算法管理缓存,这样切换的时候几乎是秒级响应。最终页面性能提升了60%左右,用户体验有了质的提升。"

深入思考:

这个回答其实还能引出更多技术问题:

问题延伸:虚拟滚动的实现原理是什么?

虚拟滚动的核心是计算可视区域,只渲染用户能看到的内容:

const useVirtualList = (items, itemHeight, containerHeight) => {
  const [scrollTop, setScrollTop] = useState(0);
  
  const visibleCount = Math.ceil(containerHeight / itemHeight);
  const startIndex = Math.floor(scrollTop / itemHeight);
  const endIndex = Math.min(startIndex + visibleCount + 1, items.length);
  
  const visibleItems = items.slice(startIndex, endIndex);
  const offsetY = startIndex * itemHeight;
  
  return {
    visibleItems,
    offsetY,
    totalHeight: items.length * itemHeight,
    onScroll: (e) => setScrollTop(e.target.scrollTop)
  };
};

问题延伸:WeakMap为什么适合做缓存?

WeakMap的key必须是对象,且是弱引用。当key对象被垃圾回收时,对应的键值对也会自动被清除,避免了内存泄漏。这对于组件缓存来说很重要,因为组件卸载后不用手动清理缓存。

2. 讲一下vue和react的区别?

我的回答:

"我两个框架都用过,最直观的区别是数据绑定方式。Vue是双向绑定,React是单向数据流。

从实现原理上说,Vue的响应式系统更智能一些。Vue 2用的Object.defineProperty,Vue 3用Proxy,都能自动追踪依赖,数据变化时自动更新视图。React需要手动调用setState或者用hooks来管理状态。

模板语法也不一样,Vue更接近传统的HTML,学习成本低一些。React的JSX需要理解JavaScript表达式,但表达能力更强。

架构设计上,Vue是渐进式框架,可以局部使用。React更像是一个库,需要配合其他工具形成完整的解决方案。

性能方面,Vue的编译时优化做得更好,比如静态提升、patch flag等。React在运行时优化更多,像时间切片、并发渲染这些。"

深入技术分析:

Vue的响应式原理深度解析:

// Vue 2 的响应式实现(简化版)
function defineReactive(obj, key, val) {
  const dep = new Dep();
  
  Object.defineProperty(obj, key, {
    get() {
      if (Dep.target) {
        dep.depend();
      }
      return val;
    },
    set(newVal) {
      if (newVal !== val) {
        val = newVal;
        dep.notify();
      }
    }
  });
}

// Vue 3 的Proxy实现
function reactive(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      track(target, key);
      return Reflect.get(target, key, receiver);
    },
    set(target, key, value, receiver) {
      const oldValue = target[key];
      const result = Reflect.set(target, key, value, receiver);
      if (value !== oldValue) {
        trigger(target, key);
      }
      return result;
    }
  });
}

React的状态管理演进:

从Class组件的setState到Hooks的useState,React的状态管理变得更加函数式:

// Class组件时代
class Counter extends Component {
  state = { count: 0 };
  
  increment = () => {
    this.setState(prevState => ({
      count: prevState.count + 1
    }));
  };
}

// Hooks时代
function Counter() {
  const [count, setCount] = useState(0);
  
  const increment = useCallback(() => {
    setCount(prev => prev + 1);
  }, []);
}

3. 讲一下闭包优缺点

我的回答:

"闭包简单说就是函数能访问其外部作用域的变量,即使外部函数已经执行完毕。

优点主要有几个:一是可以创建私有变量,实现数据封装。比如模块模式,可以暴露部分方法,隐藏内部实现。二是可以保持状态,比如计数器、缓存函数等。三是实现高阶函数,像柯里化、偏函数应用这些。

缺点的话,最主要是可能造成内存泄漏。因为闭包会持有外部变量的引用,导致这些变量无法被垃圾回收。另外就是性能问题,访问闭包变量比访问局部变量慢一些,因为需要沿着作用域链查找。"

技术深度分析:

闭包的内存模型:

function createCounter() {
  let count = 0; // 这个变量会被保存在堆内存中
  
  return {
    increment() {
      count++;
      return count;
    },
    decrement() {
      count--;
      return count;
    },
    getCount() {
      return count;
    }
  };
}

const counter1 = createCounter();
const counter2 = createCounter();

// 每个counter都有自己独立的count变量
console.log(counter1.increment()); // 1
console.log(counter2.increment()); // 1
console.log(counter1.increment()); // 2

闭包的高级应用 - 函数式编程:

// 柯里化
const curry = (fn) => {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    } else {
      return function(...args2) {
        return curried.apply(this, args.concat(args2));
      };
    }
  };
};

// 偏函数应用
const partial = (fn, ...presetArgs) => {
  return (...laterArgs) => fn(...presetArgs, ...laterArgs);
};

// 记忆化
const memoize = (fn) => {
  const cache = new Map();
  return (...args) => {
    const key = JSON.stringify(args);
    if (cache.has(key)) {
      return cache.get(key);
    }
    const result = fn(...args);
    cache.set(key, result);
    return result;
  };
};

内存泄漏的典型场景:

// 危险的闭包使用
function attachListeners() {
  const bigData = new Array(1000000).fill('some data');
  
  document.getElementById('button').onclick = function() {
    // 即使没有使用bigData,闭包也会持有它的引用
    console.log('clicked');
  };
}

// 正确的处理方式
function attachListeners() {
  const processClick = function() {
    console.log('clicked');
  };
  
  document.getElementById('button').onclick = processClick;
  
  // 或者在合适的时候清理
  return function cleanup() {
    document.getElementById('button').onclick = null;
  };
}

4. 为什么react useState要用数组解构,而不是对象?

我的回答:

"这个问题很有意思。主要是为了命名的灵活性。如果用对象解构,属性名是固定的,当你在一个组件里用多个useState的时候就会有命名冲突的问题。

比如说,如果是对象解构,可能是这样:const {value, setValue} = useState(0)。但如果你有多个状态,就得这样写:const {value: count, setValue: setCount} = useState(0),每次都要重命名,很麻烦。

用数组解构就简单多了,直接const [count, setCount] = useState(0),想叫什么名字都可以。而且React的其他hooks,比如useReducer返回的也是数组,保持了API的一致性。

从实现角度看,数组在内存中是连续存储的,访问效率稍微高一点点,虽然这个差异可以忽略不计。"

深入源码分析:

useState的内部实现机制:

// React内部简化实现
let hookIndex = 0;
let hooks = [];

function useState(initialValue) {
  const currentHookIndex = hookIndex++;
  
  // 初始化或获取已有的hook
  hooks[currentHookIndex] = hooks[currentHookIndex] || {
    state: initialValue,
    queue: []
  };
  
  const hook = hooks[currentHookIndex];
  
  // 处理更新队列
  hook.queue.forEach(action => {
    hook.state = typeof action === 'function' ? action(hook.state) : action;
  });
  hook.queue = [];
  
  const setState = (action) => {
    hook.queue.push(action);
    // 触发重新渲染
    scheduleUpdate();
  };
  
  return [hook.state, setState];
}

// 组件重新渲染前重置索引
function resetHooks() {
  hookIndex = 0;
}

为什么不能在条件语句中使用hooks:

// 错误的用法
function MyComponent({ condition }) {
  if (condition) {
    const [count, setCount] = useState(0); // 💥 违反了hooks规则
  }
  
  const [name, setName] = useState('');
  
  return <div>{name}</div>;
}

// 这会导致hooks的索引混乱,因为React依赖调用顺序来匹配状态

其他返回数组的hooks设计:

// useReducer也返回数组
const [state, dispatch] = useReducer(reducer, initialState);

// 自定义hook也经常返回数组
function useToggle(initialValue = false) {
  const [value, setValue] = useState(initialValue);
  const toggle = useCallback(() => setValue(v => !v), []);
  return [value, toggle];
}

// 如果返回对象,命名就受限了
function useToggleObject(initialValue = false) {
  const [isToggled, setIsToggled] = useState(initialValue);
  const toggle = useCallback(() => setIsToggled(v => !v), []);
  return { isToggled, toggle }; // 用户必须使用这些名字
}

5. 讲一下react生命周期

我的回答:

"React生命周期现在主要分两个时代,Class组件时代和Hooks时代。

Class组件的生命周期比较复杂,分为挂载、更新、卸载三个阶段。挂载阶段有constructor、componentDidMount。更新阶段有componentDidUpdate、getSnapshotBeforeUpdate。卸载阶段有componentWillUnmount。还有错误处理的componentDidCatch。

Hooks出现后,用useEffect基本可以替代所有生命周期方法。useEffect的依赖数组控制执行时机,空数组相当于componentDidMount,有依赖相当于componentDidUpdate,返回清理函数相当于componentWillUnmount。

不过Hooks的心智模型和Class组件不太一样。Class组件是面向生命周期编程,Hooks是面向状态和副作用编程。useEffect让你更关注'what'而不是'when'。"

生命周期的完整技术图谱:

// Class组件完整生命周期
class LifecycleDemo extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
    console.log('1. constructor');
  }
  
  static getDerivedStateFromProps(props, state) {
    console.log('2. getDerivedStateFromProps');
    return null;
  }
  
  componentDidMount() {
    console.log('3. componentDidMount');
    // 适合进行:API调用、订阅、DOM操作
  }
  
  shouldComponentUpdate(nextProps, nextState) {
    console.log('4. shouldComponentUpdate');
    // 性能优化的关键点
    return true;
  }
  
  getSnapshotBeforeUpdate(prevProps, prevState) {
    console.log('5. getSnapshotBeforeUpdate');
    // 获取DOM更新前的信息,比如滚动位置
    return null;
  }
  
  componentDidUpdate(prevProps, prevState, snapshot) {
    console.log('6. componentDidUpdate');
    // 适合进行:DOM操作、网络请求(基于props变化)
  }
  
  componentWillUnmount() {
    console.log('7. componentWillUnmount');
    // 清理工作:取消订阅、清除定时器、移除事件监听
  }
  
  static getDerivedStateFromError(error) {
    console.log('8. getDerivedStateFromError');
    return { hasError: true };
  }
  
  componentDidCatch(error, errorInfo) {
    console.log('9. componentDidCatch');
    // 错误上报
  }
  
  render() {
    console.log('render');
    return <div>{this.state.count}</div>;
  }
}

Hooks的生命周期等价物:

function HooksLifecycle() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');
  
  // componentDidMount
  useEffect(() => {
    console.log('组件挂载');
    
    // componentWillUnmount
    return () => {
      console.log('组件卸载');
    };
  }, []);
  
  // componentDidUpdate (仅当count变化时)
  useEffect(() => {
    console.log('count更新了:', count);
  }, [count]);
  
  // 等价于componentDidUpdate (每次渲染后)
  useEffect(() => {
    console.log('组件更新了');
  });
  
  // 自定义生命周期hook
  useLifecycle({
    onMount: () => console.log('挂载'),
    onUpdate: () => console.log('更新'),
    onUnmount: () => console.log('卸载')
  });
  
  return <div>{count}</div>;
}

// 自定义生命周期hook
function useLifecycle({ onMount, onUpdate, onUnmount }) {
  const mountedRef = useRef(false);
  
  useEffect(() => {
    if (!mountedRef.current) {
      mountedRef.current = true;
      onMount?.();
    } else {
      onUpdate?.();
    }
  });
  
  useEffect(() => {
    return () => {
      onUnmount?.();
    };
  }, [onUnmount]);
}

性能优化相关的生命周期:

// Class组件的性能优化
class OptimizedComponent extends React.PureComponent {
  // PureComponent自动实现了shouldComponentUpdate的浅比较
  
  // 或者手动实现
  shouldComponentUpdate(nextProps, nextState) {
    return nextProps.id !== this.props.id || 
           nextState.count !== this.state.count;
  }
}

// Hooks的性能优化
function OptimizedHooksComponent({ id, onCallback }) {
  const [count, setCount] = useState(0);
  
  // 等价于PureComponent
  const MemoizedComponent = React.memo(({ id, onCallback }) => {
    // 组件逻辑
  }, (prevProps, nextProps) => {
    return prevProps.id === nextProps.id &&
           prevProps.onCallback === nextProps.onCallback;
  });
  
  // useMemo优化计算结果
  const expensiveValue = useMemo(() => {
    return heavyCalculation(count);
  }, [count]);
  
  // useCallback优化函数
  const handleClick = useCallback(() => {
    onCallback(count);
  }, [count, onCallback]);
  
  return <div onClick={handleClick}>{expensiveValue}</div>;
}

6. 讲一下react render的全流程

我的回答:

"React的渲染流程可以分为几个主要阶段。

首先是触发更新,可能是setState、useState的setter、或者props变化、forceUpdate这些。

然后进入调度阶段,这是React 16之后新增的。Scheduler会根据任务的优先级决定什么时候开始工作,比如用户交互的优先级就比数据获取高。

接下来是协调阶段,也叫Reconciliation。这里会构建新的Fiber树,通过Diff算法对比新旧虚拟DOM,标记哪些节点需要增删改。这个阶段是可以被中断的,这就是React并发模式的基础。

最后是提交阶段,也叫Commit阶段。这里会真正操作DOM,执行生命周期方法,处理副作用。这个阶段必须是同步的,不能被中断,因为用户不能看到中间状态。

整个过程用了很多优化手段,比如批量更新、时间切片、优先级调度等。"

渲染流程的源码级分析:

// 简化的React渲染流程
function scheduleWork(fiber, expirationTime) {
  // 1. 标记更新
  markUpdateTimeFromFiberToRoot(fiber, expirationTime);
  
  // 2. 调度更新
  if (expirationTime === Sync) {
    performSyncWork();
  } else {
    scheduleCallbackWithExpirationTime(expirationTime);
  }
}

function performSyncWork() {
  // 3. 开始工作循环
  performWorkUntilDeadline();
}

function performWorkUntilDeadline() {
  while (workInProgress !== null && !shouldYieldToHost()) {
    // 4. 执行工作单元
    workInProgress = performUnitOfWork(workInProgress);
  }
  
  if (workInProgress !== null) {
    // 还有工作要做,继续调度
    scheduleCallback(performWorkUntilDeadline);
  } else {
    // 工作完成,提交更新
    commitRoot();
  }
}

function performUnitOfWork(unitOfWork) {
  // 5. 开始工作
  const next = beginWork(unitOfWork);
  
  if (next === null) {
    // 6. 完成工作
    completeUnitOfWork(unitOfWork);
  }
  
  return next;
}

Diff算法的具体实现:

function reconcileChildren(current, workInProgress, nextChildren) {
  if (current === null) {
    // 首次挂载
    workInProgress.child = mountChildFibers(
      workInProgress,
      null,
      nextChildren
    );
  } else {
    // 更新阶段
    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,
      nextChildren
    );
  }
}

// 协调子节点(简化版)
function reconcileChildFibers(returnFiber, currentFirstChild, newChildren) {
  // 处理单个节点
  if (typeof newChildren === 'object' && newChildren !== null) {
    if (newChildren.$$typeof === REACT_ELEMENT_TYPE) {
      return placeSingleChild(
        reconcileSingleElement(returnFiber, currentFirstChild, newChildren)
      );
    }
  }
  
  // 处理数组节点
  if (Array.isArray(newChildren)) {
    return reconcileChildrenArray(returnFiber, currentFirstChild, newChildren);
  }
  
  // 删除剩余的旧节点
  return deleteRemainingChildren(returnFiber, currentFirstChild);
}

// 数组子节点的协调
function reconcileChildrenArray(returnFiber, currentFirstChild, newChildren) {
  let resultingFirstChild = null;
  let previousNewFiber = null;
  
  let oldFiber = currentFirstChild;
  let newIdx = 0;
  
  // 第一轮遍历:处理相同位置的节点
  for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
    if (oldFiber.index > newIdx) {
      break;
    }
    
    const newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx]);
    if (newFiber === null) {
      break;
    }
    
    if (shouldTrackSideEffects && oldFiber && newFiber.alternate === null) {
      deleteChild(returnFiber, oldFiber);
    }
    
    lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
    
    if (previousNewFiber === null) {
      resultingFirstChild = newFiber;
    } else {
      previousNewFiber.sibling = newFiber;
    }
    
    previousNewFiber = newFiber;
    oldFiber = oldFiber.sibling;
  }
  
  // 第二轮遍历:处理剩余的新节点和旧节点
  // ...
  
  return resultingFirstChild;
}

优先级调度系统:

// 优先级定义
const ImmediatePriority = 1;
const UserBlockingPriority = 2;
const NormalPriority = 3;
const LowPriority = 4;
const IdlePriority = 5;

// 根据事件类型确定优先级
function getEventPriority(domEventName) {
  switch (domEventName) {
    case 'click':
    case 'keydown':
    case 'keyup':
      return DiscreteEventPriority; // 高优先级
    
    case 'scroll':
    case 'mousemove':
      return ContinuousEventPriority; // 中优先级
    
    default:
      return DefaultEventPriority; // 默认优先级
  }
}

// 时间切片实现
function shouldYieldToHost() {
  const timeElapsed = getCurrentTime() - startTime;
  return timeElapsed >= frameLength; // 5ms
}

7. react协调算法是什么实现的,为什么需要Fiber

我的回答:

"React的协调算法经历了一个重大的重构。早期用的是Stack Reconciler,采用递归的方式遍历组件树,但这种方式有个致命问题:一旦开始就无法中断,如果组件树很深,就会长时间占用主线程,导致页面卡顿。

Fiber架构就是为了解决这个问题。Fiber把渲染工作分解成很多小的工作单元,每个工作单元执行完后都会检查是否还有剩余时间,如果没有就把控制权交回给浏览器,让浏览器处理用户交互、动画等高优先级任务。

Fiber的核心思想是时间切片和优先级调度。比如用户点击按钮这种交互任务优先级就很高,会中断正在进行的渲染任务。而数据请求完成后的更新优先级就比较低,可以延后处理。

从技术实现上,Fiber是一个链表结构,每个节点包含了组件信息、状态、副作用等。通过child、sibling、return这三个指针构成了整个组件树的遍历路径。这样设计的好处是可以随时暂停和恢复遍历过程。"

Fiber架构的深度技术分析:

Fiber节点的数据结构:

function FiberNode(tag, pendingProps, key, mode) {
  // 作为静态数据结构的属性
  this.tag = tag; // 组件类型:FunctionComponent、ClassComponent等
  this.key = key; // Diff算法中的key
  this.elementType = null; // ReactElement.type
  this.type = null; // 异步组件resolved之后的type
  this.stateNode = null; // 真实DOM节点
  
  // 用于连接其他Fiber节点形成Fiber树
  this.return = null; // 指向父级Fiber节点
  this.child = null; // 指向子Fiber节点
  this.sibling = null; // 指向右边第一个兄弟Fiber节点
  this.index = 0;
  
  this.ref = null;
  
  // 作为动态工作单元的属性
  this.pendingProps = pendingProps;
  this.memoizedProps = null;
  this.updateQueue = null;
  this.memoizedState = null;
  this.dependencies = null;
  
  this.mode = mode;
  
  // Effects
  this.effectTag = NoEffect;
  this.nextEffect = null;
  this.firstEffect = null;
  this.lastEffect = null;
  
  // 调度优先级相关
  this.lanes = NoLanes;
  this.childLanes = NoLanes;
  
  // 双缓存技术
  this.alternate = null;
}

双缓存技术(Double Buffering):

// React维护两棵Fiber树
// current树:当前屏幕上显示内容对应的Fiber树
// workInProgress树:正在内存中构建的Fiber树

function createWorkInProgress(current, pendingProps) {
  let workInProgress = current.alternate;
  
  if (workInProgress === null) {
    // 首次创建
    workInProgress = createFiber(
      current.tag,
      pendingProps,
      current.key,
      current.mode
    );
    workInProgress.elementType = current.elementType;
    workInProgress.type = current.type;
    workInProgress.stateNode = current.stateNode;
    
    // 建立双向连接
    workInProgress.alternate = current;
    current.alternate = workInProgress;
  } else {
    // 复用已有节点
    workInProgress.pendingProps = pendingProps;
    workInProgress.effectTag = NoEffect;
    workInProgress.nextEffect = null;
    workInProgress.firstEffect = null;
    workInProgress.lastEffect = null;
  }
  
  return workInProgress;
}

Fiber的工作循环:

function workLoopConcurrent() {
  // 可中断的工作循环
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}

function workLoopSync() {
  // 同步工作循环(不可中断)
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

function performUnitOfWork(unitOfWork) {
  const current = unitOfWork.alternate;
  
  // beginWork阶段:从父到子
  let next = beginWork(current, unitOfWork, subtreeRenderLanes);
  
  unitOfWork.memoizedProps = unitOfWork.pendingProps;
  
  if (next === null) {
    // 如果没有子节点,开始completeWork
    completeUnitOfWork(unitOfWork);
  } else {
    workInProgress = next;
  }
}

function completeUnitOfWork(unitOfWork) {
  let completedWork = unitOfWork;
  
  do {
    const current = completedWork.alternate;
    const returnFiber = completedWork.return;
    
    // completeWork阶段:从子到父
    const next = completeWork(current, completedWork, subtreeRenderLanes);
    
    if (next !== null) {
      workInProgress = next;
      return;
    }
    
    // 收集副作用
    if (returnFiber !== null && (returnFiber.effectTag & Incomplete) === NoEffect) {
      if (returnFiber.firstEffect === null) {
        returnFiber.firstEffect = completedWork.firstEffect;
      }
      if (completedWork.lastEffect !== null) {
        if (returnFiber.lastEffect !== null) {
          returnFiber.lastEffect.nextEffect = completedWork.firstEffect;
        }
        returnFiber.lastEffect = completedWork.lastEffect;
      }
      
      if (completedWork.effectTag > PerformedWork) {
        if (returnFiber.lastEffect !== null) {
          returnFiber.lastEffect.nextEffect = completedWork;
        } else {
          returnFiber.firstEffect = completedWork;
        }
        returnFiber.lastEffect = completedWork;
      }
    }
    
    const siblingFiber = completedWork.sibling;
    if (siblingFiber !== null) {
      workInProgress = siblingFiber;
      return;
    }
    
    completedWork = returnFiber;
    workInProgress = completedWork;
  } while (completedWork !== null);
}

优先级调度的实现:

// Lane模型(React 17+)
const TotalLanes = 31;

const NoLanes = 0;
const NoLane = 0;

const SyncLane = 1;                    // 同步优先级
const InputContinuousLane = 2;         // 连续输入优先级
const DefaultLane = 4;                 // 默认优先级
const TransitionLanes = 8 | 16 | 32;   // 过渡优先级
const IdleLane = 1073741824;           // 空闲优先级

function getHighestPriorityLane(lanes) {
  return lanes & -lanes; // 获取最右边的1
}

function markRootUpdated(root, updateLane, eventTime) {
  root.pendingLanes |= updateLane;
  
  // 处理优先级关系
  if (updateLane !== IdleLane) {
    root.suspendedLanes = NoLanes;
    root.pingedLanes = NoLanes;
  }
}

function getNextLanes(root, wipLanes) {
  const pendingLanes = root.pendingLanes;
  
  if (pendingLanes === NoLanes) {
    return NoLanes;
  }
  
  let nextLanes = NoLanes;
  const nonIdlePendingLanes = pendingLanes & NonIdleLanes;
  
  if (nonIdlePendingLanes !== NoLanes) {
    const nonIdleUnblockedLanes = nonIdlePendingLanes & ~root.suspendedLanes;
    if (nonIdleUnblockedLanes !== NoLanes) {
      nextLanes = getHighestPriorityLane(nonIdleUnblockedLanes);
    } else {
      const nonIdlePingedLanes = nonIdlePendingLanes & root.pingedLanes;
      if (nonIdlePingedLanes !== NoLanes) {
        nextLanes = getHighestPriorityLane(nonIdlePingedLanes);
      }
    }
  } else {
    const unblockedLanes = pendingLanes & ~root.suspendedLanes;
    if (unblockedLanes !== NoLanes) {
      nextLanes = getHighestPriorityLane(unblockedLanes);
    }
  }
  
  return nextLanes;
}

8. 什么是Fiber架构,空闲渲染有了解吗?

我的回答:

"Fiber架构是React 16引入的新的协调引擎,它的核心目标是实现增量渲染。

Fiber本质上是一个JavaScript对象,代表了组件及其输入输出。每个Fiber节点对应一个React元素,包含了组件类型、props、state等信息,还有指向父节点、子节点、兄弟节点的指针,形成了一个链表结构。

空闲渲染是Fiber架构的核心特性之一。React利用浏览器的空闲时间来执行渲染工作,具体是通过requestIdleCallback或者MessageChannel来实现时间切片。每个工作单元执行完后,React会检查是否还有剩余时间,如果没有就会暂停工作,把控制权还给浏览器。

这样做的好处是用户交互不会被阻塞。比如用户正在输入时,React可以中断正在进行的渲染工作,优先处理用户输入,然后再继续之前的渲染工作。

React还实现了一套复杂的优先级系统,不同类型的更新有不同的优先级。用户交互、动画这些优先级最高,数据获取完成后的更新优先级较低。"

时间切片的具体实现:

// React的时间切片实现
let frameLength = 5; // 5ms的时间片

function shouldYieldToHost() {
  return getCurrentTime() >= deadline;
}

// 使用MessageChannel实现调度
let schedulePerformWorkUntilDeadline;

if (typeof MessageChannel !== 'undefined') {
  const channel = new MessageChannel();
  const port1 = channel.port1;
  const port2 = channel.port2;
  
  port1.onmessage = () => {
    performWorkUntilDeadline();
  };
  
  schedulePerformWorkUntilDeadline = () => {
    port2.postMessage(null);
  };
} else {
  // 降级方案
  schedulePerformWorkUntilDeadline = () => {
    setTimeout(performWorkUntilDeadline, 0);
  };
}

function performWorkUntilDeadline() {
  if (scheduledHostCallback !== null) {
    const currentTime = getCurrentTime();
    deadline = currentTime + frameLength;
    
    const hasTimeRemaining = true;
    try {
      const hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime);
      if (!hasMoreWork) {
        scheduledHostCallback = null;
      } else {
        // 还有工作要做,继续调度
        schedulePerformWorkUntilDeadline();
      }
    } catch (error) {
      schedulePerformWorkUntilDeadline();
      throw error;
    }
  }
}

React Scheduler的任务队列:

// 任务队列实现
import { push, pop, peek } from './SchedulerMinHeap';

// 两个队列:立即任务和延迟任务
let taskQueue = [];
let timerQueue = [];

function unstable_scheduleCallback(priorityLevel, callback, options) {
  const currentTime = getCurrentTime();
  
  let startTime;
  if (typeof options === 'object' && options !== null) {
    const delay = options.delay;
    if (typeof delay === 'number' && delay > 0) {
      startTime = currentTime + delay;
    } else {
      startTime = currentTime;
    }
  } else {
    startTime = currentTime;
  }
  
  let timeout;
  switch (priorityLevel) {
    case ImmediatePriority:
      timeout = IMMEDIATE_PRIORITY_TIMEOUT; // -1
      break;
    case UserBlockingPriority:
      timeout = USER_BLOCKING_PRIORITY_TIMEOUT; // 250ms
      break;
    case IdlePriority:
      timeout = IDLE_PRIORITY_TIMEOUT; // 1073741823ms
      break;
    case LowPriority:
      timeout = LOW_PRIORITY_TIMEOUT; // 10000ms
      break;
    case NormalPriority:
    default:
      timeout = NORMAL_PRIORITY_TIMEOUT; // 5000ms
      break;
  }
  
  const expirationTime = startTime + timeout;
  
  const newTask = {
    id: taskIdCounter++,
    callback,
    priorityLevel,
    startTime,
    expirationTime,
    sortIndex: -1,
  };
  
  if (startTime > currentTime) {
    // 延迟任务
    newTask.sortIndex = startTime;
    push(timerQueue, newTask);
    
    if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
      requestHostTimeout(handleTimeout, startTime - currentTime);
    }
  } else {
    // 立即任务
    newTask.sortIndex = expirationTime;
    push(taskQueue, newTask);
    
    if (!isHostCallbackScheduled && !isPerformingWork) {
      isHostCallbackScheduled = true;
      requestHostCallback(flushWork);
    }
  }
  
  return newTask;
}

function flushWork(hasTimeRemaining, initialTime) {
  isHostCallbackScheduled = false;
  
  if (isHostTimeoutScheduled) {
    isHostTimeoutScheduled = false;
    cancelHostTimeout();
  }
  
  isPerformingWork = true;
  try {
    return workLoop(hasTimeRemaining, initialTime);
  } finally {
    currentTask = null;
    currentPriorityLevel = NormalPriority;
    isPerformingWork = false;
  }
}

function workLoop(hasTimeRemaining, initialTime) {
  let currentTime = initialTime;
  advanceTimers(currentTime);
  currentTask = peek(taskQueue);
  
  while (currentTask !== null) {
    if (
      currentTask.expirationTime > currentTime &&
      (!hasTimeRemaining || shouldYieldToHost())
    ) {
      // 时间片用完,中断执行
      break;
    }
    
    const callback = currentTask.callback;
    if (typeof callback === 'function') {
      currentTask.callback = null;
      currentPriorityLevel = currentTask.priorityLevel;
      const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
      
      const continuationCallback = callback(didUserCallbackTimeout);
      currentTime = getCurrentTime();
      
      if (typeof continuationCallback === 'function') {
        currentTask.callback = continuationCallback;
      } else {
        if (currentTask === peek(taskQueue)) {
          pop(taskQueue);
        }
      }
      
      advanceTimers(currentTime);
    } else {
      pop(taskQueue);
    }
    
    currentTask = peek(taskQueue);
  }
  
  if (currentTask !== null) {
    return true; // 还有工作要做
  } else {
    const firstTimer = peek(timerQueue);
    if (firstTimer !== null) {
      requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
    }
    return false; // 没有更多工作
  }
}

并发特性的实现:

// Concurrent Mode的关键特性
function renderRootConcurrent(root, lanes) {
  const prevExecutionContext = executionContext;
  executionContext |= RenderContext;
  
  prepareFreshStack(root, lanes);
  
  do {
    try {
      workLoopConcurrent();
      break;
    } catch (thrownValue) {
      handleError(root, thrownValue);
    }
  } while (true);
  
  resetContextDependencies();
  executionContext = prevExecutionContext;
  
  if (workInProgress !== null) {
    // 还有工作未完成,返回进行中状态
    return RootInProgress;
  } else {
    // 工作完成
    workInProgressRoot = null;
    return workInProgressRootExitStatus;
  }
}

// Suspense的实现原理
function throwException(root, returnFiber, sourceFiber, value, rootRenderLanes) {
  if (typeof value === 'object' && value !== null) {
    if (typeof value.then === 'function') {
      // 这是一个thenable(Promise)
      const wakeable = value;
      
      // 标记Suspense边界
      let suspenseBoundary = returnFiber;
      do {
        if (suspenseBoundary.tag === SuspenseComponent) {
          // 找到最近的Suspense组件
          const suspenseState = suspenseBoundary.memoizedState;
          if (suspenseState === null) {
            // 首次suspended
            const primaryChildFragment = suspenseBoundary.child;
            const fallbackChildFragment = primaryChildFragment.sibling;
            
            // 显示fallback
            const fallbackFiber = createFiberFromFragment(
              fallbackChildFragment.pendingProps,
              fallbackChildFragment.mode,
              rootRenderLanes,
              fallbackChildFragment.key
            );
            
            suspenseBoundary.child = fallbackFiber;
            fallbackFiber.return = suspenseBoundary;
          }
          
          // 注册Promise回调
          wakeable.then(() => {
            // Promise resolved,重新调度渲染
            markRootUpdated(root, rootRenderLanes, getCurrentTime());
            ensureRootIsScheduled(root, getCurrentTime());
          });
          
          break;
        }
        suspenseBoundary = suspenseBoundary.return;
      } while (suspenseBoundary !== null);
    }
  }
}

总结和思考

这次面试让我对React的底层原理有了更深的理解。从防抖节流这样的基础工具函数,到Fiber架构这样的复杂系统设计,每个问题都可以挖得很深。

我觉得前端面试不只是在考察知识点的记忆,更重要的是看你对技术的理解深度和思考能力。比如为什么React要设计Fiber架构?这背后反映的是对用户体验的极致追求,也体现了前端技术栈的不断演进。

另外,我发现很多看似独立的知识点其实是相互关联的。比如闭包的概念在实现防抖节流、React Hooks、甚至Fiber的任务调度中都有体现。这说明扎实的JavaScript基础是非常重要的。

对于正在准备面试的朋友,我的建议是:

  1. 不要只背概念,要理解原理和设计思想
  2. 多动手实践,自己实现一些核心功能
  3. 关注技术演进的历史,理解为什么要这样设计
  4. 准备一些有深度的项目经历,能体现技术思考和解决问题的能力

16k*12的薪资在杭州市场还算不错,特别是对于外包岗位来说。不过我觉得更重要的是技术成长空间和项目经验的积累。

最后,面试是相互选择的过程。在展示自己技术能力的同时,也要了解公司的技术氛围和发展前景。希望这次分享对大家有帮助!

前端大大大

微信公众号:【前端大大大】

qrcode_for_gh_c467a0436534_258.jpg