最后
今天的文章可谓是积蓄了我这几年来的应聘和面试经历总结出来的经验,干货满满呀!如果你能够一直坚持看到这儿,那么首先我还是十分佩服你的毅力的。不过光是看完而不去付出行动,或者直接进入你的收藏夹里吃灰,那么我写这篇文章就没多大意义了。所以看完之后,还是多多行动起来吧!
可以非常负责地说,如果你能够坚持把我上面列举的内容都一个不拉地看完并且全部消化为自己的知识的话,那么你就至少已经达到了中级开发工程师以上的水平,进入大厂技术这块是基本没有什么问题的了。
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// 更新 state 使下一次渲染能够显示降级后的 UI
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// 我们可以将错误日志上报给服务器
logErrorToMyService(error, errorInfo);
}
render() {
if (this.state.hasError) {
// 我们可以自定义降级后的 UI 并渲染
return
Something went wrong.
;}
return this.props.children;
}
}
一个有责任心的开发一定不会放任错误的发生。错误边界可以包在任何位置并提供降级 UI,也就是说,一旦开发者’有责任心’ 页面就不会全白,这也是我之前说的方案一与之天然冲突且其他方案不稳定的情况。那么,在这同时我们上报异常信息,这里上报的异常一定会导致我们定义的白屏,这一推导是 100% 正确的。
100% 这个词或许不够负责,接下来我们来看看为什么我说这一推导是 100% 准确的:
React 渲染流程
我们来简单回顾下从代码到展现页面上 React 做了什么。我大致将其分为几个阶段:render => 任务调度 => 任务循环 => 提交 => 展示 我们举一个简单的例子来展示其整个过程(任务调度不再本次讨论范围故不展示):
const App = ({ children }) => (
<>
hello
{ children }
</>
);
const Child = () =>
I’m child
const a = ReactDOM.render(
,
document.getElementById(‘root’)
);
准备
首先浏览器是不认识我们的 jsx 语法的,所以我们通过 babel 编译大概能得到下面的代码:
var App = function App(_ref2) {
var children = _ref2.children;
return React.createElement(“p”, null, “hello”), children);
};
var Child = function Child() {
return React.createElement(“p”, null, “I’m child”);
};
ReactDOM.render(React.createElement(App, null, React.createElement(Child, null)), document.getElementById(‘root’));
babel 插件将所有的 jsx 都转成了 createElement
方法,执行它会得到一个描述对象 ReactElement
大概长这样子:
{
$$typeof: Symbol(react.element),
key: null,
props: {}, // createElement 第二个参数 注意 children 也在这里,children 也会是一个 ReactElement 或 数组
type: ‘h1’ // createElement 的第一个参数,可能是原生的节点字符串,也可能是一个组件对象(Function、Class…)
}
所有的节点包括原生的 <a></a>
、 <p></p>
都会创建一个 FiberNode
,他的结构大概长这样:
FiberNode = {
elementType: null, // 传入 createElement 的第一个参数
key: null,
type: HostRoot, // 节点类型(根节点、函数组件、类组件等等)
return: null, // 父 FiberNode
child: null, // 第一个子 FiberNode
sibling: null, // 下一个兄弟 FiberNode
flag: null, // 状态标记
}
你可以把它理解为 Virtual Dom 只不过多了许多调度的东西。最开始我们会为根节点创建一个 FiberNodeRoot
如果有且仅有一个 ReactDOM.render
那么他就是唯一的根,当前有且仅有一个 FiberNode
树。
我只保留了一些渲染过程中重要的字段,其他还有很多用于调度、判断的字段我这边就不放出来了,有兴趣自行了解
render
现在我们要开始渲染页面,是我们刚才的例子,执行 ReactDOM.render
。这里我们有个全局 workInProgress
对象标志当前处理的 FiberNode
-
首先我们为根节点初始化一个
FiberNodeRoot
,他的结构就如上面所示,并将workInProgress= FiberNodeRoot
。 -
接下来我们执行
ReactDOM.render
方法的第一个参数,我们得到一个ReactElement
:
ReactElement = {
$$typeof: Symbol(react.element),
key: null,
props: {
children: {
$$typeof: Symbol(react.element),
key: null,
props: {},
ref: null,
type: ƒ Child(),
}
}
ref: null,
type: f App()
}
该结构描述了 <App><Child /></App>
- 我们为
ReactElement
生成一个FiberNode
并把 return 指向父FiberNode
,最开始是我们的根节点,并将workInProgress = FiberNode
{
elementType: f App(), // type 就是 App 函数
key: null,
type: FunctionComponent, // 函数组件类型
return: FiberNodeRoot, // 我们的根节点
child: null,
sibling: null,
flags: null
}
-
只要
workInProgress
存在我们就要处理其指向的FiberNode
。节点类型有很多,处理方法也不太一样,不过整体流程是相同的,我们以当前函数式组件为例子,直接执行App(props)
方法,这里有两种情况 -
该组件 return 一个单一节点,也就是返回一个
ReactElement
对象,重复 3 - 4 的步骤。并将当前 节点的 child 指向子节点CurrentFiberNode.child = ChildFiberNode
并将子节点的 return 指向当前节点ChildFiberNode.return = CurrentFiberNode
-
该组件 return 多个节点(数组或者
Fragment
),此时我们会得到一个ChildiFberNode
的数组。我们循环他,每一个节点执行 3 - 4 步骤。将当前节点的 child 指向第一个子节点CurrentFiberNode.child = ChildFiberNodeList[0]
,同时每个子节点的 sibling 指向其下一个子节点(如果有)ChildFiberNode[i].sibling = ChildFiberNode[i + 1]
,每个子节点的 return 都指向当前节点ChildFiberNode[i].return = CurrentFiberNode
如果无异常每个节点都会被标记为待布局 FiberNode.flags = Placement
- 重复步骤直到处理完全部节点
workInProgress
为空。
最终我们能大概得到这样一个 FiberNode
树:
FiberNodeRoot = {
elementType: null,
type: HostRoot,
return: null,
child: FiberNode,
sibling: null,
flags: Placement, // 待布局状态
}
FiberNode {
elementType: f App(),
type: FunctionComponent,
return: FiberNodeRoot,
child: FiberNode
,
sibling: null,
flags: Placement // 待布局状态
}
FiberNode
{
elementType: ‘p’,
type: HostComponent,
return: FiberNode,
sibling: FiberNode,
child: null,
flags: Placement // 待布局状态
}
FiberNode {
elementType: f Child(),
type: FunctionComponent,
return: FiberNode,
child: null,
flags: Placement // 待布局状态
}
提交阶段
提交阶段简单来讲就是拿着这棵树进行深度优先遍历 child => sibling,放置 DOM 节点并调用生命周期。
那么整个正常的渲染流程简单来讲就是这样。接下来看看异常处理
错误边界流程
刚刚我们了解了正常的流程现在我们制造一些错误并捕获他:
const App = ({ children }) => (
<>
hello
{ children }
</>
);
const Child = () =>
I’m child {a.a}
const a = ReactDOM.render(
,
document.getElementById(‘root’)
);
执行步骤 4 的函数体是包裹在 try...catch
内的如果捕获到了异常则会走异常的流程:
do {
try {
workLoopSync(); // 上述 步骤 4
break;
} catch (thrownValue) {
handleError(root, thrownValue);
}
} while (true);
执行步骤 4 时我们调用 Child
方法由于我们加了个不存在的表达式 {a.a}
此时会抛出异常进入我们的 handleError
流程此时我们处理的目标是 FiberNode<Child>
,我们来看看 handleError
:
function handleError(root, thrownValue): void {
let erroredWork = workInProgress; // 当前处理的 FiberNode 也就是异常的 节点
throwException(
root, // 我们的根 FiberNode
erroredWork.return, // 父节点
erroredWork,
thrownValue, // 异常内容
);
completeUnitOfWork(erroredWork);
}
function throwException(
root: FiberRoot,
returnFiber: Fiber,
sourceFiber: Fiber,
value: mixed,
) {
// The source fiber did not complete.
sourceFiber.flags |= Incomplete;
let workInProgress = returnFiber;
do {
switch (workInProgress.tag) {
case HostRoot: {
workInProgress.flags |= ShouldCapture;
return;
}
case ClassComponent:
// Capture and retry
const ctor = workInProgress.type;
const instance = workInProgress.stateNode;
if (
(workInProgress.flags & DidCapture) === NoFlags &&
(typeof ctor.getDerivedStateFromError === ‘function’ ||
(instance !== null &&
typeof instance.componentDidCatch === ‘function’ &&
!isAlreadyFailedLegacyErrorBoundary(instance)))
) {
workInProgress.flags |= ShouldCapture;
return;
}
break;
default:
break;
}
workInProgress = workInProgress.return;
} while (workInProgress !== null);
}
总结
面试前要精心做好准备,简历上写的知识点和原理都需要准备好,项目上多想想难点和亮点,这是面试时能和别人不一样的地方。
还有就是表现出自己的谦虚好学,以及对于未来持续进阶的规划,企业招人更偏爱稳定的人。
万事开头难,但是程序员这一条路坚持几年后发展空间还是非常大的,一切重在坚持。
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
前端面试题汇总
JavaScript
前端资料汇总