1、hooks 为什么不能放在条件判断里 ?
在 react 内部,每个组件(Fiber)的 hooks 都是以链表的形式存在 memoizeState 属性中:
update 阶段,每次调用 setState,链表就会执行 next 向后移动一步。如果将 setState 写在条件判断中,假设条件判断不成立,没有执行里面的 setState 方法,会导致接下来所有的 setState 的取值出现偏移,从而导致异常发生。
2、fiber ?
React Fiber 是一种基于浏览器的单线程调度算法。
React Fiber 用类似 requestIdleCallback 的机制来做异步 diff。但是之前数据结构不支持这样的实现异步 diff,于是 React 实现了一个类似链表的数据结构,将原来的递归diff 变成了现在的遍历diff,这样就能做到异步可更新了。
3、diff ?
传统 diff 算法的时间复杂度是 O(n^3),这在前端 render 中是不可接受的。为了降低时间复杂度,react 的 diff 算法做了一些妥协,放弃了最优解,最终将时间复杂度降低到了 O(n)。
那么 react diff 算法做了哪些妥协呢?,参考如下:
1、tree diff:只对比同一层的 dom 节点,忽略 dom 节点的跨层级移动
如下图,react 只会对相同颜色方框内的 DOM 节点进行比较,即同一个父节点下的所有子节点。当发现节点不存在时,则该节点及其子节点会被完全删除掉,不会用于进一步的比较。
这样只需要对树进行一次遍历,便能完成整个 DOM 树的比较。
这就意味着,如果 dom 节点发生了跨层级移动,react 会删除旧的节点,生成新的节点,而不会复用。
2、component diff:如果不是同一类型的组件,会删除旧的组件,创建新的组件
3、element diff:对于同一层级的一组子节点,需要通过唯一 id 进行来区分
如果没有 id 来进行区分,一旦有插入动作,会导致插入位置之后的列表全部重新渲染。
这也是为什么渲染列表时为什么要使用唯一的 key。
4、调用setState之后发生了什么?
- 在
setState的时候,React 会为当前节点创建一个updateQueue的更新列队。 - 然后会触发
reconciliation过程,在这个过程中,会使用名为 Fiber 的调度算法,开始生成新的 Fiber 树, Fiber 算法的最大特点是可以做到异步可中断的执行。 - 然后
React Scheduler会根据优先级高低,先执行优先级高的节点,具体是执行doWork方法。 - 在
doWork方法中,React 会执行一遍updateQueue中的方法,以获得新的节点。然后对比新旧节点,为老节点打上 更新、插入、替换 等 Tag。 - 当前节点
doWork完成后,会执行performUnitOfWork方法获得新节点,然后再重复上面的过程。 - 当所有节点都
doWork完成后,会触发commitRoot方法,React 进入 commit 阶段。 - 在 commit 阶段中,React 会根据前面为各个节点打的 Tag,一次性更新整个 dom 元素。
5、React 组件间有那些通信方式 ?
1、父组件向子组件通信
1、 通过 props 传递
2、子组件向父组件通信
1、 主动调用通过 props 传过来的方法,并将想要传递的信息,作为参数,传递到父组件的作用域中
3、跨层级通信
1、 使用 react 自带的 Context 进行通信,createContext 创建上下文, useContext 使用上下文。
import React, { createContext, useContext } from 'react';
const themes = {
light: {
foreground: "#000000",
background: "#eeeeee"
},
dark: {
foreground: "#ffffff",
background: "#222222"
}
};
const ThemeContext = createContext(themes.light);
function App() {
return (
<ThemeContext.Provider value={themes.dark}>
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar() {
return (
<div>
<ThemedButton />
</div>
);
}
function ThemedButton() {
const theme = useContext(ThemeContext);
return (
<button style={{ background: theme.background, color: theme.foreground }}>
I am styled by theme context!
</button>
);
}
export default App;
2、使用 Redux 或者 Mobx 等状态管理库
3、使用订阅发布模式
6、React 父组件如何调用子组件中的方法 ?
1、如果是在方法组件中调用子组件(>= react@16.8),可以使用 useRef 和 useImperativeHandle:
const { forwardRef, useRef, useImperativeHandle } = React;
const Child = forwardRef((props, ref) => {
useImperativeHandle(ref, () => ({
getAlert() {
alert("getAlert from Child");
}
}));
return <h1>Hi</h1>;
});
const Parent = () => {
const childRef = useRef();
return (
<div>
<Child ref={childRef} />
<button onClick={() => childRef.current.getAlert()}>Click</button>
</div>
);
};
2、如果是在类组件中调用子组件(>= react@16.4),可以使用 createRef:
const { Component } = React;
class Parent extends Component {
constructor(props) {
super(props);
this.child = React.createRef();
}
onClick = () => {
this.child.current.getAlert();
};
render() {
return (
<div>
<Child ref={this.child} />
<button onClick={this.onClick}>Click</button>
</div>
);
}
}
class Child extends Component {
getAlert() {
alert('getAlert from Child');
}
render() {
return <h1>Hello</h1>;
}
}
7、React有哪些优化性能的手段 ?
1、类组件中的优化手段
1、使用纯组件 PureComponent 作为基类。
2、使用 React.memo 高阶函数包装组件。
3、使用 shouldComponentUpdate 生命周期函数来自定义渲染逻辑。
2、方法组件中的优化手段
1、使用 useMemo。
2、使用 useCallBack。
3、其他方式
1、在列表需要频繁变动时,使用唯一 id 作为 key,而不是数组下标。
2、必要时通过改变 CSS 样式隐藏显示组件,而不是通过条件判断显示隐藏组件。
3、使用 Suspense 和 lazy 进行懒加载,例如:
import React, { lazy, Suspense } from "react";
export default class CallingLazyComponents extends React.Component {
render() {
var ComponentToLazyLoad = null;
if (this.props.name == "Mayank") {
ComponentToLazyLoad = lazy(() => import("./mayankComponent"));
} else if (this.props.name == "Anshul") {
ComponentToLazyLoad = lazy(() => import("./anshulComponent"));
}
return (
<div>
<h1>This is the Base User: {this.state.name}</h1>
<Suspense fallback={<div>Loading...</div>}>
<ComponentToLazyLoad />
</Suspense>
</div>
)
}
}
8、为什么 React 元素有一个 $$typeof 属性 ?
目的是为了防止 XSS 攻击。因为 Symbol 无法被序列化,所以 React 可以通过有没有 $$typeof 属性来断出当前的 element 对象是从数据库来的还是自己生成的。
如果没有 $$typeof 这个属性,react 会拒绝处理该元素。
9、React 如何区分 Class组件 和 Function组件 ?
一般的方式是借助 typeof 和 Function.prototype.toString 来判断当前是不是 class,如下:
function isClass(func) {
return typeof func === 'function'
&& /^class\s/.test(Function.prototype.toString.call(func));
}
但是这个方式有它的局限性,因为如果用了 babel 等转换工具,将 class 写法全部转为 function 写法,上面的判断就会失效。
React 区分 Class组件 和 Function组件的方式很巧妙,由于所有的类组件都要继承 React.Component,所以只要判断原型链上是否有 React.Component 就可以了:
AComponent.prototype instanceof React.Component
10、什么是 suspense 组件 ?
Suspense 让组件“等待”某个异步操作,直到该异步操作结束即可渲染。在下面例子中,两个组件都会等待异步 API 的返回值:
const resource = fetchProfileData();
function ProfilePage() {
return (
<Suspense fallback={<h1>Loading profile...</h1>}>
<ProfileDetails />
<Suspense fallback={<h1>Loading posts...</h1>}>
<ProfileTimeline />
</Suspense>
</Suspense>
);
}
function ProfileDetails() {
// 尝试读取用户信息,尽管该数据可能尚未加载
const user = resource.user.read();
return <h1>{user.name}</h1>;
}
function ProfileTimeline() {
// 尝试读取博文信息,尽管该部分数据可能尚未加载
const posts = resource.posts.read();
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.text}</li>
))}
</ul>
);
}
Suspense 也可以用于懒加载,参考下面的代码:
const OtherComponent = React.lazy(() => import('./OtherComponent'));
function MyComponent() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<OtherComponent />
</Suspense>
</div>
);
}
11、为什么 JSX 中的组件名要以大写字母开头 ?
因为 React 要知道当前渲染的是组件还是 HTML 元素。
12、redux 是什么 ?
Redux 是一个为 JavaScript 应用设计的,可预测的状态容器。
它解决了如下问题:
- 跨层级组件之间的数据传递变得很容易
- 所有对状态的改变都需要 dispatch,使得整个数据的改变可追踪,方便排查问题。
但是它也有缺点:
- 概念偏多,理解起来不容易
- 样板代码太多
13、react-redux 的实现原理 ?
通过 redux 和 react context 配合使用,并借助高阶函数,实现了 react-redux。
14、reudx 和 mobx 的区别 ?
得益于 Mobx 的 observable,使用 mobx 可以做到精准更新;对应的 Redux 是用 dispath 进行广播,通过Provider 和 connect 来比对前后差别控制更新粒度。
15、redux 有哪些异步中间件 ?
1、redux-thunk
源代码简短优雅,上手简单
2、redux-saga
借助 JS 的 generator 来处理异步,避免了回调的问题
3、redux-observable
借助了 RxJS 流的思想以及其各种强大的操作符,来处理异步问题
16、react父组件更新会,为什么子组件也会更新 ?
React 会创建一个虚拟 DOM(virtual DOM)。当一个组件中的状态改变时,React 首先会通过 "diffing" 算法来标记虚拟 DOM 中的改变,第二步是调节(reconciliation),会用 diff 的结果来更新 DOM。
只要父组件的render了,那么默认情况下就会触发子组件的render过程,子组件的render过程又会触发它的子组件的render过程,一直到React元素(即jsx中的
是因为React不能检测到是否给子组件传了属性,所以它必须进行这个重渲染过程(术语叫做reconciliation)。但是这不会使得react有多低效,因为reconciliation过程是执行的JavaScript,而重渲染的性能开销主要是更新DOM导致的,最后diff算法会介入,决定是否要真正更新DOM,JavaScript的执行速度很快的,所以即使父组件render会触发所有后代组件的render过程(reconciliation过程),这个效率也不会有太大影响。
当然,从道理上讲,既然没有给子组件传递属性,或者程序能够判断出传递的属性并没有发生变化,那么自然无需进行子组件的reconciliation过程。但是react无法自动检测这一点,于是它提供了shouldComponentUpdate回调函数,让程序员根据情况决定是否决定是否要重render本组件。如果某个组件的shouldComponentUpdate总是返回false, 那么当它的父组件render了,会触发该组件的render过程,但是进行到shouldComponentUpdate判断时会被阻止掉,从而就不调用它的render方法了,它自己下面的组件的render过程压根也不会触发了。
具体可以参考 文档