读《react 进阶》时做的一些内容的笔记
# 组件生命周期
类组件才有生命周期,函数组件是没有的!
# 挂载阶段
- constructor。构造方法,一般用于初始化组件的 state 和绑定事件处理方法等。
- componentWillMount。
- render。必要方法,返回 UI。他必须是个纯函数
- componentDidMount。挂载后调用,一般在这个时候向服务端请求数据。在这里调用 this.setState 会引起组件重新渲染
# 更新阶段
- componentWillReceiveProps(nextProps)。只在 props 引起的组件更新过程中才会被调用。
- should ComponentUpdate (nextProps,nextState)。决定组件是否继续执行更新过程,可以通过比较两个参数来决定返回值。用于减少组件不必要的渲染,来优化性能。
- componentWillUpdate
- componentDidUpdate (prevProps,prevState)。组件更新后被调用。
# 卸载阶段
- componentWillUnmount。在组件被卸载前调用。用于执行一些清理操作,比如清除定时器,清除 componentDidMount 中手动创建的 DOM
# React 事件处理函数 this 问题
ES6class 语法实质上是 ES5 组合继承的语法糖,这我们都知道。
在 ES6class 中,并不会为方法自动绑定 this 到当前对象,为了解决这个 this 指向问题,有几种解决方案。
# 使用箭头函数
我们都知道箭头函数的 this 是定义时绑定,它会拿外层中最靠近的 this 来套给自己。
# 使用组件方法
直接在事件监听中写 this.handle
同时需要在构造函数中使用 bind 来绑定
this.handle = this.handle.bind(this); |
# React 的 ref
ref 用于获取元素,他可以获取 DOM 元素,甚至也可获取 React 组件实例。
可以用来控制元素的焦点、文本选择等操作。
但是 ref 破坏了以 props 为数据传递介质的典型数据流,应尽量避免使用
# 在 DOM 元素上使用 ref
在 DOM 元素上使用 ref 是最常见的使用场景。ref 接收一个回调函数作为值,同时向这个回调函数中传入 DOM 元素。
在组件被挂载时,回调函数会被调用,同时接收当前 DOM 元素作为参数。
在组件被卸载时,会接受 null 作为参数。
class AutoFocusTextInput extends React.Component {
componentDidMount(){
this.textInput.focus();
}
render(){
return (
<div>
<input
type="text"
ref={(input) => {this.textInput = input;}}/>
</div>
)
}
}
这个组件为 input 元素定义了 ref,在组件挂载后,通过 ref 获取到这个 input 元素并存在 textInput 上,让 input 自动获取焦点。
# 在组件上使用 ref
此时 ref 的回调函数接收的参数时当前组件的实例,提供了一种在组件外部操作组件的方式。
可以通过 ref 获取到组件的实例,并调用组件内部的方法。
先定义个内部组件
class AutoFocusTextInput extends React.Component {
constructor(props){
super(props);
this.blur = this.blur.bind(this);
}
componentDidMount(){
this.textInput.focus();
}
blur(){
this.textInput.blur();
}
render(){
return (
<div>
<input
type="text"
ref={(input) => {this.textInput = input;}}/>
</div>
)
}
}
然后定义一个组件
class Container extends React.Component {
constructor(props){
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick(){
this.inputInstance.blur();
}
render(){
return (
<div>
<AutoFocusTextInput ref={ (input) => {this.inputInstance = input}}/>
<button onClick={this.handleClick}>失去焦点</button>
</div>
)
}
}
只能为类组件定义 ref 属性,不能为函数组件定义 ref,因为函数组件没有实例。
但可以在函数组件内部使用 ref(利用 useRef),只要它指向一个 DOM 元素或者 class 组件。
function CustomTextInput(props) {
// 这里必须声明 textInput,这样 ref 才可以引用它
const textInput = useRef(null);
function handleClick() {
textInput.current.focus();
}
return (
<div>
<input
type="text"
ref={textInput} />
<input
type="button"
value="Focus the text input"
onClick={handleClick}
/>
</div>
);
}
useRef 返回一个可变的 ref 对象,其.current 属性被初始化为传入的参数。返回的 ref 对象在组件的整个生命周期内保持不变
本质上, useRef
就像是可以在其 .current
属性中保存一个可变值的 “盒子”。
当 ref 对象内容发生变化时,useRef 不会发出通知。
# 父组件访问子组件的 DOM 节点
其实和子组件修改父组件的数据有点相似
- 都是父组件传一个 prop 回调函数给子组件
- 然后子组件在内部调用这个函数,将值通过调用这个函数传给父组件,从而达到访问的目的
function Children(props){
return (
<div>
<input ref={props.inputRef}/>
</div>
)
}
class Parent extends React.Component{
render(){
return (
<Children
inputRef={el => this.inputElement = el}
/>
);
}
}
# 虚拟 DOM
# 概念
虚拟 DOM 与真实 DOM 对应,就是使用 JavaScript 对象来表示 DOM 元素的一种技术。
在浏览器中,操作真实 DOM 是效率会非常慢,每次操作都可能引起浏览器的回流和重绘,所以要尽量减少。
# DIff 算法
每次组件的状态或属性更新,组件的 render 方法都会返回一个新的虚拟 DOM 对象。
React 会通过比较更新前后两份虚拟 DOM(新的虚拟 DOM 和旧的虚拟 DOM),来找出差异部分更新到真实 DOM 上,从而减少在真实 DOM 上的操作。
正常情况下,比较两个树型结构差异的算法时间复杂度是 O (N 的 3 次方),但 React 通过总结 DOM 的实际使用场景,提出了两个绝大多数场景下都成立的假设。通过这两个假设,React 实现了 O (N) 复杂下完成两颗虚拟 DOM 树的比较。
- 如果两个元素的类型不同,那么它们将生成两棵不同的树。
- 对 DOM 节点跨层级移动的情况忽略不计。
比较过程:
# 当根结点不同类型时
这时候 React 会认为新的树和旧的树完全不同,会将整棵树拆掉重建(包括虚拟 DOM 树和真实 DOM 树),拆除的时候会触发组件的卸载生命钩子,重建的时候会触发挂载钩子。
# 当根结点是相同的 DOM 元素类型
React 会保留根结点,比较根结点的属性,只更新变化了的属性。
# 当根结点时相同的组件类型
这时对应的组件实例不会被销毁,只会执行更新操作,同步变化的属性到虚拟 DOM 树上。在这一过程中组件实例的 componentWillReceiveProps 和 componentWillUpdate 会被调用。
对于组件类型的节点,这时候 React 无法知道如何更新真实 DOm 树,需要在组件更新并且 render 方法执行完毕后,根据 render 返回的虚拟 DOM 结构决定如何更新真实 DOM 树。
根据这三个假设,React 递归遍历虚拟 DOM 树,比较完之后得到最终的差异,更新到 DOM 树中。
当一个节点有多个子节点的时候,默认情况下 React 会按照顺序逐一比较两棵树上对应的子节点。
<ul> | |
<li>first</li> | |
<li>second</li> | |
</ul> | |
<ul> | |
<li>first</li> | |
<li>second</li> | |
<li>third</li> | |
</ul> |
这个时候最终只会插入一个新的节点。
但如果我们在开始位置新增一个节点,或者把最后的节点调整位置到最开始,就会出现这种特殊情况。
React 会以为每一个结点都变化了,从而每个节点都被修改。
# key
为了避免前面那种特殊情况,提出了 key。key 时为了帮助 React 提高 Diff 算法的效率,使用 key 来匹配子节点,只要渲染之后子节点的 key 没有变化,React 就会认为这是同一个节点。
但是加上 key 也未必 “性能最优”,需要避免使用索引来作为元素的 key,如果使用索引,也会出现前面那个情况。
# 减少不必要组件渲染
之前大概知道,可以通过 should ComponentUpdate 钩子来决定是否更新组件。
最好的办法是通过比较当前 prop 和下次 props 看有没有属性变化,但是深比较的性能影响比较大。
所以提出了折中的办法,只比较第一层级的属性。
而 React 中提出了一个 Pure Component 组件,这个组件会使用浅比较来比较新旧 props 和 state,因此可以通过让组件继承 Pure Component 来代替手写 should ComponentUpdate 的逻辑。
class NumberList extends React.PureComponent{
...
}
# React Fiber
JavaScript 是单线程的,如果调用栈运行一个很耗时的脚本,比如解析一个图片,可能会长时间阻塞,使得其他事件都无法得到响应。
为了突破这个瓶颈,React 中试着将耗时高、易阻塞的长任务进行切片,分成子任务并异步执行他们。这样,在每个子任务的空隙间就有机会处理其他任务,如 UI 更新。