React的设计初衷
声明式的写法
React 使创建交互式 UI 变得轻而易举。为你应用的每一个状态设计简洁的视图,当数据改变时 React 能有效地更新并正确地渲染组件。
以声明式编写 UI,可以让你的代码更加可靠,且方便调试。
组件化的思维
创建拥有各自状态的组件,再由这些组件构成更加复杂的 UI。
组件逻辑使用 JavaScript 编写而非模版,因此你可以轻松地在应用中传递数据,并使得状态与 DOM 分离。
面试考点
虚拟DOM是什么
虚拟DOM作为React的骨架,面试必问题,首先虚拟DOM在React的代码中其实就是一个树的结构,里面存着和真实dom之间的映射关系,例如:
['div', {id: 'a'}, ['span', {}, 'hello']]
对应的真实dom:
<div id = 'a'><span>hello</span></div>
有了这份结构,实现跨平台的RN也就是水到渠成了。
虚拟dom是何时创建的呢?
在某一时间节点调用 React 的 render()
方法,会创建一棵由 React 元素组成的树。在下一次 state 或 props 更新时,相同的 render()
方法会返回一棵不同的树。
render() {
return React.createElement('div', {id: 'a'}, React.createElement('span', {}, 'hello'))
}
那么React是如何把这个虚拟dom的优势发挥它的作用呢?React 需要基于这两棵树之间的差别来判断如何有效率的更新 UI 以保证当前 UI 与最新的树保持同步。这就要说到它的另一个配套diff算法了。
Diff算法做了哪些事情
当对比两棵不同的树的时候会从根节点开始,有几个策略:
1、对比不同类型的元素
当根节点为不同的类型的元素的时候,React会之间废弃掉原有的虚拟DOM,卸载所有的真实DOM,触发一次完整的重建流程,不再往下比较。举个例子,当一个元素从 <a>
变成 <img>
,从 <Article>
变成 <Comment>
,或从 <Button>
变成 <div>
都会触发一个完整的重建流程。
当拆卸一棵树时,对应的 DOM 节点也会被销毁。组件实例将执行 componentWillUnmount()
方法。当建立一棵新的树时,对应的 DOM 节点会被创建以及插入到 DOM 中。组件实例将执行 componentWillMount()
方法,紧接着 componentDidMount()
方法。所有跟之前的树所关联的 state 也会被销毁。
2、对比相同类型的元素
React只会对比属性的变化,更新对应的属性值,例如className,style等。
处理完之后需要继续递归子元素处理。
3、对比相同的React组件
React 将更新该组件实例的 props 以跟最新的元素保持一致,并且调用该实例的 componentWillReceiveProps()
和 componentWillUpdate()
方法。
下一步,调用 render()
方法,diff 算法将在之前的结果以及新的结果中进行递归,找出具体的差异。
4、遍历子元素需要依赖key来提升性能
当遍历子元素的时候,需要借助key的概念了,因为React并不知道子元素是否做了位置移动,比如在前面插入了一个节点,又或者做重新排序的场景,这些都会可能导致React性能底下。
如果有了key,React就只需要对比相同key的节点差异即可,同时也能分辨出新增或者删除的节点有哪些。
那么这个key的值如何设置呢?大部分情况下我们可以使用数组的下标作为key的值,比如:
lists.map(item, index => <li key={index}>aa</li>)
但是当<li>里面包含非受控组件的时候,用下标作为key可能会出现无法预期的结果,比如:
lists.map(item, index => {
return (
<li key={index}>
<input />
</li>
)
})
当对列表元素进行排序的时候,元素的key会跟着数组下标被改变,就会导致input中的内容并不会跟着元素移动,从而导致功能出现异常。
所以建议第一选择用每一条数据的唯一id作为key。
setState原理
1、为什么setState是个异步的过程?
因为React是个处处都考虑性能优化的框架,如果每一次setState都进行一次render的话,这会导致很多没必要的渲染,特别是组件层级嵌套深的时候,每一次父级结点的render都会带动所有子节点的render。
回答这题的时候,需要知道提到一个概念batchUpdate,批量合并更新,也就是说你在一次事件函数内触发多次setState并不会实时触发更新,而是合并成一次更新,这样可以避免不必要的渲染重构。所以正常情况下setState是个异步的过程,但是在setTimeout等宏任务中React还无法做到批量更新,如果需要在宏任务里做到批量更新需要借用React的一个方法unstable_batchedUpdates
setTimeout(() => {
unstable_batchedUpdates(() => {
this.setState({a: 1});
this.setState({a: 2});
})
}, 100)
ps:这个方法是极其不优雅的一种做法,但是React又没有更好的办法来解决,所以也是没办法的办法,最新版引进了Fiber,这个或许在未来的版本中能够完美的解决,而不需要开发者去处理,所以unstable_batchedUpdates是会逐渐在新版本中淘汰掉的。
HOC(higherOrderComponent)
HOC又叫高阶组件,具体而言,高阶组件是参数为组件,返回值为新组件的函数。
代表有Redux的connect方法,Mobx的@observer,我们来看看Redux的connect简单实现:
function connect(mapStateToProps, mapDispatchToProps, ...){
return function(Wrapper) {
return class extent React.Component{
constructor(){
this.state = {
nextStates: {}
}
}
componentWillMount(){
const { store } = this.context;
store.subscribe(() => {
this.updateProps();
}
}
updateProps() {
const { store } = this.context;
this.setState(state => {
nextStates: {
...mapStateToProps(store.getState()),
...mapDispatchToProps(),
...this.props
}
});
} render() {
return <Wrapper {...this.nextStates} />
}
}
}
}
以上为简单的实现,只为了说明一下高阶组件的原理,但是有几点需要注意:
1.HOC不能改变原有组件的任何结构,只做props的透传
2.不要在render中对任何一个组件使用高阶函数,这样会导致React渲染效率大打折扣
性能优化
React中的性能优化无非就是减少不必要的渲染,特别是嵌套层次比较多的时候,很容易一个不小心引起一连串的反应。
1、善于使用工具
React开发了一个基于Chrome的插件,这个插件可以查看到React的dom树,还有每次渲染的情况。
2、有没有用过shouldComponentUpdate
这个生命周期函数是控制组件是否渲染,可以跳过render及之后的所有流程,默认情况下是直接返回true渲染的。如果你能够知道组件什么时候才需要渲染的话,就可以重写这个函数,只让符合条件的情况返回true,默认都返回false即可。例如:
shouldComponentUpdate(nextProps, nextState) {
if (this.props.color !== nextProps.color) {
return true;
}
if (this.state.count !== nextState.count) {
return true;
}
return false;
}
这种写法适合功能比较单一,用到的属性也比较少的情况,如果复杂化就不优雅了,一个个列出来也不适合。
那如果数据很多的情况下怎么办呢?(面试官开始装逼了,因为他堵你不知道,哈哈哈)
对于这种情况React提供了一个封装好的组件PureComponent,我们只要继承这个组件即可
class CounterButton extends React.PureComponent {
constructor(props) {
super(props);
this.state = {count: 1};
}
render() {
return (
<button
color={this.props.color}
onClick={() => this.setState(state => ({count: state.count + 1}))}>
Count: {this.state.count}
</button>
);
}
}
什么是PureComponent?它和普通组件的区别是啥?(你竟然答上来了,他不服了,你惨了,他要深挖你:))))
PureComponent只是对shouldComponentUpdate做了一次封装,但是内部用的是浅比较
shouldComponentUpdate(nextProps, nextState) {
if (this.props !== nextProps) {
return true;
}
if (this.state !== nextState) {
return true;
}
return false;
}
那么设计好的坑又来了,等着你跳下去,我下面的代码到底会不会导致更新?
class ListOfWords extends React.PureComponent {
render() {
return <div>{this.props.words.join(',')}</div>;
}
}
class WordAdder extends React.Component {
constructor(props) {
super(props);
this.state = {
words: ['marklar']
};
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
// 这部分代码很糟,而且还有 bug
const words = this.state.words;
words.push('marklar');
this.setState({words: words}); // 坑在着呢
}
render() {
return (
<div>
<button onClick={this.handleClick} />
<ListOfWords words={this.state.words} />
</div>
);
}
}
如果你没有发现坑的话,你的回答肯定是会渲染,哈哈哈。
这里还涉及到this.setState,如果按照上面words的写法的话,是基于原来的state对象去更新的,就像:
var xiaoming = {
girlFrends: ['lily', 'sandy', 'xiaomei']
}
// 这么多女朋友要同时约会咋办,小明发明了高科技给自己制作了好多替身,但是他们公用的是一个脑子
var xiaoming2 = xiaoming;
// 当小明2去和lily约会的时候,lily要验明正身,问了好多隐私问题,但是奈何xiaoming2都答上来了
// 因为啊
xiaoming2 === xiaoming; // true
// xiaoming2这时候又发展了一个女朋友
xiaoming2.girlFrends.push('lisa');
// 这下小明爽歪歪了,lisa也没办法分辨出来小明
xiaoming === xiaoming2; // true 互相共享大脑啊
所以按照上面的情况,PureComponent肯定发现不了小明换了一个替身,而且还多了一个女朋友lisa,哈哈哈
那要定义什么样的规则来约束小明这样的行为呢,这就要提到不可变数据了(immutable)。
handleClick() {
this.setState(state => ({
words: state.words.concat(['marklar'])
}));
}
function updateColorMap(colormap) {
return Object.assign({}, colormap, {right: 'blue'});
}
以上两种方式就是从复制的角度做了约束,对象生成了就不能改变内部的任何一个属性,可以通过生成一个新的对象来实现,这个新的对象可以把小明的女朋友们都复制过来,但是以后他们两个就没有关系了,再多几个女朋友也和小明没有关系,哈哈哈
但是一个对象里面可能嵌套很深,你需要一层层的去拷贝那得累死
所以这里又可以隐身出immutability-helper,如果数据非常复杂的情况下,可以引入这个插件,帮你解决这种深层嵌套的问题,这里就不展开更多了。
import update from 'immutability-helper';
const newData = update(myData, {
x: {y: {z: {$set: 7}}},
a: {b: {$push: [9]}}
});