这里写自定义目录标题
React的核心思想:view是state的输出
view = f(state)
上面的f代表函数关系,只要state发生变化,view就会相应的改变。
React 的本质是将图形界面(GUI)函数化。
key的必要性与重要性
React中渲染数组的时候,一定要加上key属性。
key必须是字符串类型,它的取值可以用数据对象的某个唯一属性,或是对数据进行hash来生成key。用来标记同父同层级的兄弟元素。只要子元素有key属性,便会去原V-dom树中相应位置寻找是否有相同key的元素,比较它们是否完全相同,如果相同的话就直接复用该元素,提升渲染性能。
React的diff算法是通过js对象默认dom树,对新旧dom同层级的元素挨个比较。
diff算法把树按照层级进行分解,只比较同级元素。只会匹配组件名称相同的component,(class 名称相同),例如:组件
<Header />
被组件<NewHeader />
替换了,react会把旧组件删除掉,直接创建一个新的组件<NewHeader />
,不会浪费时间去比较两个不可能有相似之处的component。大大提高了效率。
// 旧v-dom
<ul>
<li key="1">first</li>
<li key="2">second</li>
</ul>
// 新v-dom
<ul>
<li key="0">阿萨德</li>
<li key="1">first</li>
<li key="2">second</li>
</ul>
react通过diff算法比较发现,只是新增了key为“0”的元素,其他2个只是调换了一下位置,只需要给它们2个打上一个调换顺序的标记即可。
但是强烈不推荐用数组index来作为key。如果数据更新仅仅是数组重新排序或在其中间位置插入新元素,那么视图元素都将重新渲染。来看下例子:
// 旧v-dom
<ul>
<li key="0">first</li>
<li key="1">second</li>
</ul>
// 新v-dom
<ul>
<li key="0">阿萨德</li>
<li key="1">first</li>
<li key="2">second</li>
</ul>
React发现key为0,1,2的元素的text都变了,将会新建dom节点去替换原来的节点,这样的话key就失去了意义。而如果用唯一id,react会知道同key节点没有变化,只是换了位置,只要打个移动节点的patch到dom上,而不是新建替换
react性能优化
- 减少
setState
的使用。因为每调用一次,就会重新计算一次子树 - 通过使用
shouldComponentUpdate
减少大的子树的重新计算,避免不必要的渲染 - 通过
key
标记列表中的子元素,提高diff算法的性能
react+redux的工作流程
- React-Redux 将所有组件分成两大类:UI 组件(presentational component)和容器组件(container component)。UI组件负责UI的渲染,没有状态,参数通过props提供;容器组件负责管理数据和业务逻辑,带有内部状态。
- 通过
Provider
拿到store,Provider
的作用就是让容器组件获取到store,store放在context属性中 connect
把UI组件生成容器组件。参数有两个(mapStateToProps, mapDispatchToProps),这2个参数就定义了组件的业务逻辑。第一个是输入逻辑:把state映射到UI组件的参数,第二个是输出逻辑:把用户发出的动作映射到action,并从UI组件发出去。- connect生成容器组件之后,需要让容器组件拿到state对象,才能生成UI组件的参数。而Provider就是实现这一功能。
- 在根组件外面包一层Provider,这样子组件就可以都能拿到state
- Reducer对应的是项目中的ViewModel,通过
createStore(Reducer)
生成store
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import ReactDOM from 'react-dom'
import { createStore } from 'redux'
import { Provider, connect } from 'react-redux'
// React component
class Counter extends Component {
render() {
const { value, onIncreaseClick } = this.props
return (
<div>
<span>{value}</span>
<button onClick={onIncreaseClick}>Increase</button>
</div>
)
}
}
Counter.propTypes = {
value: PropTypes.number.isRequired,
onIncreaseClick: PropTypes.func.isRequired
}
// Action
const increaseAction = { type: 'increase' }
// Reducer
function counter(state = { count: 0 }, action) {
const count = state.count
switch (action.type) {
case 'increase':
return { count: count + 1 }
default:
return state
}
}
// Store
const store = createStore(counter)
// Map Redux state to component props
function mapStateToProps(state) {
return {
value: state.count
}
}
// Map Redux actions to component props
function mapDispatchToProps(dispatch) {
return {
onIncreaseClick: () => dispatch(increaseAction)
}
}
// Connected Component
const App = connect(
mapStateToProps,
mapDispatchToProps
)(Counter)
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
虚拟dom为什么能提升性能(必考)
如何理解虚拟dom
虚拟DOM相当于在js对象和真实DOM树之间加了一个缓存,利用diff算法避免了一些不必要的dom操作,提升了性能。
- 利用js对象结构表示dom树的结构,然后用这个树构建一个真正的dom树,插入到文档当中
- 当状态变化的时候就重新渲染一个新的虚拟DOM树,然后利用diff算法比较新树和旧树之间的差别
- 把二者之间的差异应用到步骤1中的真实dom树上,视图就更新了。
用js对象模拟dom树结构很简单,只需要记录她的节点类型、属性、以及子节点即可。
var element = {
tagName: 'ul', // 节点标签名
props: { // DOM的属性,用一个对象存储键值对
id: 'list'
},
children: [ // 该节点的子节点
{tagName: 'li', props: {class: 'item'}, children: ["Item 1"]},
{tagName: 'li', props: {class: 'item'}, children: ["Item 2"]},
{tagName: 'li', props: {class: 'item'}, children: ["Item 3"]},
]
}
对应的HTML是这样的
<ul id='list'>
<li class='item'>Item 1</li>
<li class='item'>Item 2</li>
<li class='item'>Item 3</li>
</ul>
上面说的节点的差异指的是什么呢?对 DOM操作可能会:
- 替换掉原来的节点,例如把上面的div换成了section
- 移动、删除、新增子节点,例如上面div的子节点,
- 把p和ul顺序互换修改了节点的属性对于文本节点,
- 文本内容可能会改变。例如修改上面的文本节点2内容为Virtual DOM 2。
React diff算法原理(常考,大厂必考)
- 把生成的树形结构的虚拟dom按层级分解,只比较同级元素
- 给列表元素增加表示唯一性的标记符key属性,方便比较
- React只会匹配相同class的component(这里的class指的是组件名字)
- 合并操作,调用 component的setState方法的时候,React会将其标记为dirty,在一次事件循环结束的时候,react会统计所有被标记为dirty的component,并重新绘制。
- 选择性子树渲染。可在shouldComponentUpdate方法中编写高性能的逻辑,提升性能
React中refs的作用:
Refs 是 React 提供给我们的安全访问 DOM 元素或者某个组件实例的句柄。我们可以为元素添加 ref 属性然后在回调函数中接受该元素在 DOM 树中的句柄,该值会作为回调函数的第一个参数返回:
class CustomForm extends Component {
handleSubmit = () => {
console.log("Input Value: ", this.input.value)
}
render () {
return (
<form onSubmit={this.handleSubmit}>
<input
type='text'
ref={(input) => this.input = input} />
<button type='submit'>Submit</button>
</form>
)
}
}
调用setState方法之后发生了什么
在代码中调用 setState 函数之后,React 会将传入的参数对象与组件当前的状态合并,然后触发所谓的调和过程(Reconciliation)。经过调和过程,React 会以相对高效的方式根据新的状态构建 React 元素树并且着手重新渲染整个 UI 界面。在 React 得到元素树之后,React 会自动计算出新的树与老树的节点差异,然后根据差异对界面进行最小化重渲染。在差异计算算法中,React 能够相对精确地知道哪些位置发生了改变以及应该如何改变,这就保证了按需更新,而不是全部重新渲染。
要知道setState本质是通过一个队列机制实现state更新的。 执行setState时,会将需要更新的state合并后放入状态队列,而不会立刻更新state,队列机制可以批量更新state。
如果不通过setState而直接修改this.state,那么这个state不会放入状态队列中,下次调用setState时对状态队列进行合并时,会忽略之前直接被修改的state,这样我们就无法合并了,而且实际也没有把你想要的state更新上去。
import React, {Component} from "react";
export default class Example extends React.Component {
constructor() {
super();
this.state = {
val: 0
};
}
componentWillMount() {
this.setState({val: this.state.val + 1});
console.log(this.state.val); // 第 1 次 log 0
this.setState({val: this.state.val + 1});
console.log(this.state.val); // 第 2 次 log
}
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
this.setState({
val: this.state.val + 6
}, function(){
console.log(3, this.state.val);
});
setTimeout(() => {
console.log(this.state.val); // 第 3 次 log
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 (
<div>
12asf
</div>
);
}
};
最终的结果是 0 0 1 1 7 7 8 9
原因是,在setTimeout中,state已经更新,拿到的值是之前最后一次更新的数据(即 1)。
setState在原生事件,setTimeout,setInterval,Promise等异步操作中,state会同步更新(立即改变)
setStatePromise(updator) {
return new Promise(((resolve, reject) => {
this.setState(updator, resolve);
}));
}
componentWillMount() {
this.setStatePromise(({ num }) => ({
num: num + 1,
})).then(() => {
console.log(this.state.num);
});
}
传入第二个参数(回调函数),在状态更新成功之后调用,此时取到的state已经是更新之后的值
this.setState({
val: this.state.val + 6
}, function(){
console.log(this.state.val); // 更新之后的state
});
- componentDidMount 中拿到的state也是更新后的数据
state会在每次render执行之后更新state,所以render之后的声明周期函数取到的state都是已经更新过的。
setState和replaceState的区别
setState是修改其中的部分状态,相当于Object.assign,只是覆盖,不会减少原来的状态
replaceState是完全替换原来的状态,相当于赋值,将原来的state替换为另一个对象,如果新状态属性减少,那么state中就没有这个状态了