React
# 1. React 中 setState 什么时候是同步的,什么时候是异步的?
查看答案
- 由 React 控制的事件处理程序,以及生命周期函数调用 setState 不会同步更新 state 。
- React 控制之外的事件中调用 setState 是同步更新的。比如原生 js 绑定的事件,setTimeout/setInterval 等。
# 2. React setState 笔试题
下面的代码输出什么?
class Example extends React.Component {
constructor() {
super()
this.state = { val: 0 }
}
componentDidMount() {
this.setState({ val: this.state.val + 1 })
console.log(this.state.val)
// 第 1 次 log
this.setState({ val: this.state.val + 1 })
console.log(this.state.val)
// 第 2 次 log
setTimeout(() => {
this.setState({ val: this.state.val + 1 })
console.log(this.state.val)
// 第 3 次 log
this.setState({ val: this.state.val + 1 })
console.log(this.state.val)
// 第 4 次 log
}, 0)
}
render() {
return null
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
查看答案
0, 0, 1, 2
# 3. redux 为什么要把 reducer 设计成纯函数?
查看答案
redux 的设计思想就是不产生副作用,数据更改的状态可回溯,所以 redux 中都是纯函数
# 4. 对 React 的 Virtual DOM 的误解
查看答案
React 从来没有说过 “React 比原生操作 DOM 快”。
React 的基本思维模式是 每次有变动就整个重新渲染整个应用。如果没有 Virtual DOM,简单来想就是直接重置 innerHTML。很多人都没有意识到,在一个大型列表所有数据都变了的情况下,重置 innerHTML 其实是一个还算合理的操作...
真正的问题是在 “全部重新渲染” 的思维模式下,即使只有一行数据变了,它也需要重置整个innerHTML,这时候显然就有大量的浪费。
我们可以比较一下 innerHTML vs. Virtual DOM 的重绘性能消耗:
- innerHTML: render html string O(template size) + 重新创建所有 DOM 元 素 O(DOM size)
- Virtual DOM: render Virtual DOM + diff O(template size) + 必要的 DOM 更新 O(DOM change)
Virtual DOM render + diff 显然比渲染 html 字符串要慢,但是!它依然是纯 js 层面的计算,比起后面的 DOM 操作来说,依然便宜了太多。可以看到, innerHTML 的总计算量不管是 js 计算还是 DOM 操作都是和整个界面的大小相关,但 Virtual DOM 的计算量里面,只有 js 计算和界面大小相关,DOM 操 作是和数据的变动量相关的。前面说了,和 DOM 操作比起来,js 计算是极其便宜的。
这才是为什么要有 Virtual DOM:
- 不管你的数据变化多少,每次重绘的性能都可以接受
- 你依然可以用类似 innerHTML 的思路去写你的应用。
# 5. MVVM vs. Virtual DOM
查看答案
相比起 React,其他 MVVM 系框架比如 Angular, Knockout 以及 Vue、Avalon 采用的都是数据绑定:通过 Directive/Binding 对象,观察数据变化并保留对实 际 DOM 元素的引用,当有数据变化时进行对应的操作。MVVM 的变化检查是 数据层面的,而 React 的检查是 DOM 结构层面的。 MVVM 的性能也根据变动检测的实现原理有所不同:Angular 的脏检查使得任 何变动都有固定的 O(watcher count) 的代价;Knockout/Vue/Avalon 都采用了依 赖收集,在 js 和 DOM 层面都是 O(change):
- 脏检查:scope digest O(watcher count) + 必要 DOM 更新 O(DOM change)
- 依赖收集:重新收集依赖 O(data change) + 必要 DOM 更新 O(DOM change)可以看到,Angular 最不效率的地方在于任何小变动都有的和 watcher 数量相关的性能代价。但是!当所有数据都变了的时候,Angular 其实并不吃 亏。依赖收集在初始化和数据变化的时候都需要重新收集依赖,这个代价在小 量更新的时候几乎可以忽略,但在数据量庞大的时候也会产生一定的消耗。
MVVM 渲染列表的时候,由于每一行都有自己的数据作用域,所以通常都是每 一行有一个对应的 ViewModel 实例,或者是一个稍微轻量一些的利用原型继 承的 "scope" 对象,但也有一定的代价。所以,MVVM 列表渲染的初始化几乎 一定比 React 慢,因为创建 ViewModel / scope 实例比起 Virtual DOM 来说要 昂贵很多。 这里所有 MVVM 实现的一个共同问题就是在列表渲染的数据源变动时,尤其 是当数据是全新的对象时,如何有效地复用已经创建的 ViewModel 实例和 DOM 元素。 假如没有任何复用方面的优化,由于数据是 “全新” 的,MVVM 实际上需要销 毁之前的所有实例,重新创建所有实例,最后再进行一次渲染!这就是为什么 题目里链接的 angular/knockout 实现都相对比较慢。相比之下,React 的变动 检查由于是 DOM 结构层面的,即使是全新的数据,只要最后渲染结果没变, 那么就不需要做无用功。
Angular 和 Vue 都提供了列表重绘的优化机制,也就是 “提示” 框架如何有效 地复用实例和 DOM 元素。比如数据库里的同一个对象,在两次前端 API 调用 里面会成为不同的对象,但是它们依然有一样的 uid。这时候你就可以提示 track by uid 来让 Angular 知道,这两个对象其实是同一份数据。那么原来这份 数据对应的实例和 DOM 元素都可以复用,只需要更新变动了的部分。或者, 你也可以直接 track by $index 来进行 “原地复用”:直接根据在数组里的位置 进行复用。在题目给出的例子里,如果 angular 实现加上 track by $index 的话, 后续重绘是不会比 React 慢多少的。甚至在 dbmonster 测试中,Angular 和 Vue 用了 track by $index 以后都比 React 快: dbmon (注意 Angular 默认版本 无优化,优化过的在下面) 顺道说一句,React 渲染列表的时候也需要提供 key 这个特殊 prop,本质上 和 track-by 是一回事。
在比较性能的时候,要分清楚初始渲染、小量数据更新、大量数据更新这些不 同的场合。Virtual DOM、脏检查 MVVM、数据收集 MVVM 在不同场合各有不 同的表现和不同的优化需求。Virtual DOM 为了提升小量数据更新时的性能, 也需要针对性的优化,比如 shouldComponentUpdate 或是 immutable data。
- 初始渲染:Virtual DOM > 脏检查 >= 依赖收集
- 小量数据更新:依赖收集 >> Virtual DOM + 优化 > 脏检查(无法优化) > Virtual DOM 无优化
- 大量数据更新:脏检查 + 优化 >= 依赖收集 + 优化 > Virtual DOM(无 法/无需优化)>> MVVM 无优化
不要天真地以为 Virtual DOM 就是快,diff 不是免费的,batching 么 MVVM 也 能做,而且最终 patch 的时候还不是要用原生 API。在我看来 Virtual DOM 真 正的价值从来都不是性能,而是它
- 为函数式的 UI 编程方式打开了大门
- 可以渲染到 DOM 以外的 backend,比如 ReactNative。
# 6. react-router里的Link标签和a标签有什么区别?
查看答案
Link 做了 3 件事情:
- 有 onclick 那就执行 onclick
- click 的时候阻止 a 标签默认事件(这样子点击
<a href="/abc">123</a>
就不会跳转和刷新页面) - 再取得跳转 href(即是 to),用 history(前端路由两种方式之一,
history & hash
)跳转,此时只是链接变了,并没有刷新页面
Link 点击事件 handleClick 部分源码:
if (_this.props.onClick) _this.props.onClick(event);
if (!event.defaultPrevented &&
// onClick prevented default
event.button === 0 &&
// ignore everything but left clicks
!_this.props.target &&
// let browser handle "target=_blank" etc.
!isModifiedEvent(event)
// ignore clicks with modifier keys
) {
event.preventDefault();
var history = _this.context.router.history;
var _this$props = _this.props,
replace = _this$props.replace,
to = _this$props.to;
if (replace) {
history.replace(to);
} else {
history.push(to);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 7. 对React-Fiber的理解,它解决了什么问题?
查看答案
React V15 在渲染时,会递归比对 VirtualDOM 树,找出需要变动的节点,然后同步更新它们, 一气呵成。
这个过程期间, React 会占据浏览器资源,这会导致用户触发的事件得不到响应,并且会导致掉帧,导致用户感觉到卡顿。为了给用户制造一种应用很快的“假象”,不能让一个任务长期霸占着资源。
可以将浏览器的渲染、布局、绘制、资源加载(例如 HTML 解析)、事件响应、脚本执行视作操作系统的“进程”,需要通过某些调度策略合理地分配 CPU 资源,从而提高浏览器的用户响应速率, 同时兼顾任务执行效率。
所以 React 通过Fiber 架构,让这个执行过程变成可被中断。“适时”地让出 CPU 执行权,除了可以让浏览器及时地响应用户的交互,还有其他好处:
- 分批延时对DOM进行操作,避免一次性操作大量 DOM 节点,可以得到更好的用户体验;给浏览器一点喘息的机会,它会对代码进行编译优化(JIT)及进行热代码优化,或者对 reflow 进行修正。
- 核心思想:Fiber 也称协程或者纤程。它和线程并不一样,协程本身是没有并发或者并行能力的(需要配合线程),它只是一种控制流程的让出机制。让出 CPU 的执行权,让 CPU 能在这段时间执行其他的操作。渲染的过程可以被中断,可以将控制权交回浏览器,让位给高优先级的任务,浏览器空闲后再恢复渲染。
# 8. React.Component 和 React.PureComponent 的区别
查看答案
PureComponent表示一个纯组件,可以用来优化React程序,减少render函数执行的次数,从而提高组件的性能。
在React中,当prop或者state发生变化时,可以通过在shouldComponentUpdate生命周期函数中执行return false来阻止页面的更新,从而减少不必要的render执行。
React.PureComponent会自动执行 shouldComponentUpdate。不过,pureComponent中的 shouldComponentUpdate() 进行的是浅比较,也就是说如果是引用数据类型的数据,只会比较不是同一个地址,而不会比较这个地址里面的数据是否一致。
浅比较会忽略属性和或状态突变情况,其实也就是数据引用指针没有变化,而数据发生改变的时候render是不会执行的。
如果需要重新渲染那么就需要重新开辟空间引用数据。PureComponent一般会用在一些纯展示组件上。
使用pureComponent的好处:当组件更新时,如果组件的props或者state都没有改变,render函数就不会触发。省去虚拟DOM的生成和对比过程,达到提升性能的目的。这是因为react自动做了一层浅比较。
# 9. 对有状态组件和无状态组件的理解及使用场景
查看答案
有状态组件 特点:
- 是类组件
- 有继承
- 可以使用this
- 可以使用react的生命周期
- 使用较多,容易频繁触发生命周期钩子函数,影响性能
- 内部使用 state,维护自身状态的变化,有状态组件根据外部组件传入的 props 和自身的 state进行渲染。
使用场景:
- 需要使用到状态的。
- 需要使用状态操作组件的(无状态组件的也可以实现新版本react hooks也可实现)总结:类组件可以维护自身的状态变量,即组件的 state ,类组件还有不同的生命周期方法,可以让开发者能够在组件的不同阶段(挂载、更新、卸载),对组件做更多的控制。类组件则既可以充当无状态组件,也可以充当有状态组件。
当一个类组件不需要管理自身状态时,也可称为无状态组件。
无状态组件 特点:
- 不依赖自身的状态state
- 可以是类组件或者函数组件。
- 可以完全避免使用 this 关键字。(由于使用的是箭头函数事件无需绑定)
- 有更高的性能。当不需要使用生命周期钩子时,应该首先使用无状态函数组件
- 组件内部不维护 state ,只根据外部组件传入的 props 进行渲染的组件,当 props 改变时,组件重新渲染。
使用场景: 组件不需要管理 state,纯展示
优点:
- 简化代码、专注于 render
- 组件不需要被实例化,无生命周期,提升性能。 输出(渲染)只取决于输入(属性),无副作用
- 视图和数据的解耦分离
缺点:
- 无法使用 ref
- 无生命周期方法
- 无法控制组件的重渲染,因为无法使用shouldComponentUpdate 方法,当组件接受到新的属性时则会重渲染
总结:组件内部状态且与外部无关的组件,可以考虑用状态组件,这样状态树就不会过于复杂,易于理解和管理。当一个组件不需要管理自身状态时,也就是无状态组件,应该优先设计为函数组件。比如自定义的 、 等组件。