一次网易外包技术面试的复盘总结
最近去面了汉克时代外包给网易的前端岗位,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基础是非常重要的。
对于正在准备面试的朋友,我的建议是:
- 不要只背概念,要理解原理和设计思想
- 多动手实践,自己实现一些核心功能
- 关注技术演进的历史,理解为什么要这样设计
- 准备一些有深度的项目经历,能体现技术思考和解决问题的能力
16k*12的薪资在杭州市场还算不错,特别是对于外包岗位来说。不过我觉得更重要的是技术成长空间和项目经验的积累。
最后,面试是相互选择的过程。在展示自己技术能力的同时,也要了解公司的技术氛围和发展前景。希望这次分享对大家有帮助!
前端大大大
微信公众号:【前端大大大】