调用方法时为什么需要bind this
第一次使用React的事件处理时,会有些疑惑为什么需要进行手动绑定this
。官网对 事件处理这样解释:
你必须谨慎对待 JSX 回调函数中的
this
,在 JavaScript 中,class 的方法默认不会绑定this
。如果你忘记绑定this.handleClick
并把它传入了onClick
,当你调用这个函数的时候this
的值为undefined
。这并不是 React 特有的行为;这其实与 JavaScript 函数工作原理有关。通常情况下,如果你没有在方法后面添加
()
,例如onClick={this.handleClick}
,你应该为这个方法绑定this
。
如果深究原因这主要是由于React的事件处理机制:
简单理解就是:React在组件加载(Mount)和更新(Update)时,将事件通过addEventListener
统一注册到document
上,然后会有一个事件池统一存储所有事件,当事件触发的时候,通过dispatchEvent
派发事件。
帮助我们理解为什么需要bind this
,就可以理解为:事件处理程序会被当作回调函数进行使用。
由于JavaScript的this指向问题,回调函数会丢失this指向,默认指向undifined
setState 同步还是异步
1. setState 是同步还是异步?
我的回答是执行过程代码同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形式了所谓的“异步”,所以表现出来有时是同步,有时是“异步”。
2. 何时是同步,何时是异步呢?
只在合成事件和钩子函数中是“异步”的,在原生事件和 setTimeout/setInterval等原生 API 中都是同步的。简单的可以理解为被 React 控制的函数里面就会表现出“异步”,反之表现为同步。
3. 那为什么会出现异步的情况呢?
为了做性能优化,将 state 的更新延缓到最后批量合并再去渲染对于应用的性能优化是有极大好处的,如果每次的状态改变都去重新渲染真实 dom,那么它将带来巨大的性能消耗。
React生命周期
挂载
组件首次被实例化创建并插入DOM
中需要执行的生命周期函数:
-
constructor():
需要在组件内初始化
state
或进行方法绑定时,需要定义constructor()
函数。不可在constructor()
函数中调用setState()
。 -
static getDerivedStateFromProps():
执行
getDerivedStateFromProps
函数返回我们要的更新的state
,React
会根据函数的返回值拿到新的属性。 -
render():
函数类组件必须定义的
render()
函数,是类组件中唯一必须实现的方法。render()
函数应为纯函数,也就是说只要组件state
和props
没有变化,返回的结果是相同的。其返回结果可以是:1、React
元素;2、数组或 fragments;3、Portals;4、字符串或数值类型;5、布尔值或null。不可在render()
函数中调用setState()
。 -
componentDidMount():
组件被挂载插入到
Dom
中调用此方法,可以在此方法内执行副作用操作,如获取数据,更新state
等操作。
更新
当组件的props
或state
改变时会触发更新需要执行的生命周期函数:
-
static getDerivedStateFromProps():
getDerivedStateFromProps
会在调用render
方法之前调用,并且在初始挂载及后续更新时都会被调用。它应返回一个对象来更新state
,如果返回null
则不更新任何内容。 -
shouldComponentUpdate():
根据
shouldComponentUpdate()
的返回值,判断React
组件的是否执行更新操作。React.PureComponent
就是基于浅比较进行性能优化的。一般在实际组件实践中,不建议使用该方法阻断更新过程,如果有需要建议使用React.PureComponent
。 -
render():
在组件更新阶段时如果
shouldComponentUpdate()
返回false
值时,将不执行render()
函数。 -
getSnapshotBeforeUpdate():
该生命周期函数执行在
pre-commit
阶段,此时已经可以拿到Dom
节点数据了,该声明周期返回的数据将作为componentDidUpdate()
第三个参数进行使用。 -
componentDidUpdate():
shouldComponentUpdate()
返回值false
时,则不会调用componentDidUpdate()
。
卸载
-
componentWillUnmount()
:会在组件卸载及销毁之前直接调用。一般使用此方法用于执行副作用的清理操作,如取消定时器,取消事件绑定等。
React Diff算法
React diff 作为 Virtual DOM 的加速器,其算法上的改进优化是 React 整个界面渲染的基础,以及性能提高的保障。
Diff算法并不是由React首发,Diff算法早已存在。但是传统的Diff算法,通过循环递归对比依次对比,效率低下,算法复杂度达到 O(n^3)。而React则改进Diff算法引入React。
React 分别对 tree diff、component diff 以及 element diff 进行算法优化
Tree Diff
由于Web UI 中对DOM节点的跨层级操作很少,对树进行分层比较,两棵树只会对同一层级的节点进行比较,即同一个父节点下的所有子节点。
当发现节点不存在时,则该节点及其子节点都会被删除,不会用于进一步的比较。
Component Diff
React 是基于组件构建应用的,对于组件间的比较所采取的策略也是简洁高效。
- 如果是同一类型的组件,按照原策略继续比较 virtual DOM tree。
- 如果不是,则将该组件判断为 dirty component,从而替换整个组件下的所有子节点。
- 对于同一类型的组件,有可能其 Virtual DOM 没有任何变化,如果能够确切的知道这点那可以节省大量的 diff 运算时间,因此 React 允许用户通过 shouldComponentUpdate() 来判断该组件是否需要进行 diff。
如下图,当 component D 改变为 component G 时,即使这两个 component 结构相似,一旦 React 判断 D 和 G 是不同类型的组件,就不会比较二者的结构,而是直接删除 component D,重新创建 component G 以及其子节点。虽然当两个 component 是不同类型但结构相似时,React diff 会影响性能,但正如 React 官方博客所言:不同类型的 component 是很少存在相似 DOM tree 的机会,因此这种极端因素很难在实现开发过程中造成重大影响的。
Element Diff
当节点处于同一层级时,React diff 提供了三种节点操作,分别为:INSERT_MARKUP(插入)、MOVE_EXISTING(移动)和 REMOVE_NODE(删除)。
- INSERT_MARKUP,新的 component 类型不在老集合里, 即是全新的节点,需要对新节点执行插入操作。
- MOVE_EXISTING,在老集合有新 component 类型,且 element 是可更新的类型,generateComponentChildren 已调用 receiveComponent,这种情况下 prevChild=nextChild,就需要做移动操作,可以复用以前的 DOM 节点。
- REMOVE_NODE,老 component 类型,在新集合里也有,但对应的 element 不同则不能直接复用和更新,需要执行删除操作,或者老 component 不在新集合里的,也需要执行删除操作。
如下图,老集合中包含节点:A、B、C、D,更新后的新集合中包含节点:B、A、D、C,此时新老集合进行 diff 差异化对比,发现 B != A,则创建并插入 B 至新集合,删除老集合 A;以此类推,创建并插入 A、D 和 C,删除 B、C 和 D。
而React则Diff不会进行这种繁杂冗余操作,React则允许开发者对同一层级的同一组子节点,添加唯一key进行区分。新老集合进行 diff 差异化对比,通过 key 发现新老集合中的节点都是相同的节点,因此无需进行节点删除和创建,只需要将老集合中节点的位置进行移动,更新为新集合中节点的位置,
React 的 虚拟DOM
本质上是JavaScript对象,这个对象就是更加轻量级的对DOM的描述。
React实现了其对DOM节点的控制,但是DOM节点是非常复杂的,对节点的操作非常耗费资源,其实例属性非常多,对于节点操作很多冗余属性。大部分属性对于Diff操作并没有用处,所以就使用更加轻量级的虚拟DOM对DOM进行描述。
那么现在的过程就是这样:
- 维护一个使用 JS 对象表示的 Virtual DOM,与真实 DOM 一一对应
- 对前后两个 Virtual DOM 做 diff ,生成变更(Mutation)
- 把变更应用于真实 DOM,生成最新的真实 DOM
通过以上,我们发现 虚拟DOM优点是通过Diff算法减少JavaScript操作真实DOM性能消耗,但这仅仅只是其中之一的优点。
Virtual DOM更多作用
-
Virtual DOM通过牺牲了部分性能的前提下,增加了可维护性。
-
实现了对DOM的集中化操作,当数据改变时先修改Virtual DOM,再统一反映到真实DOM中,用最小的代价更新DOM,提高了效率。
-
抽象了原本的渲染过程,实现了跨平台的能力,而不仅仅局限于浏览器DOM中,也可以使用到安卓和IOS的原生组件。
-
打开了函数式UI编程的大门。
Virtual DOM的缺点
- 首次渲染DOM的时候,由于多一层Virtual DOM,会比
innerHTML
插入慢。 - 虚拟DOM需要在内存中维护一份DOM副本。
React 性能优化
函数组件性能优化
主要讲函数组件的性能优化方式,对类组件暂不说明。
React 函数组件优化思路主要有两个:
- 减少
render
的次数,因为在React所花的时间最多的就是进行render
- 减少计算的量。主要是减少重复计算的量,因为函数组件重新渲染时会从头开始进行函数调用。
在使用类组件的时候,使用的 React 优化 API 主要是:shouldComponentUpdate
和 PureComponent
,这两个 API 所提供的解决思路都是为了减少重新 render 的次数,主要是减少父组件更新而子组件也更新的情况。
但是我们使用函数时组件,并没有生命周期和类,我们就需要换种方式进行性能优化。
减少render
次数
通常来说,有三种原因会进行重新render
:
- 自身状态改变
- 父组件重新渲染,导致子组件重新渲染,但是父组件传递的
props
并没有发生改变。 - 父组件重新渲染,导致子组件重新渲染,但是组件传递的的
props
发生改变。
React.memo
首先要介绍的就是 React.memo
,这个 API 可以说是对标类组件里面的 PureComponent
,这是可以减少重新 render 的次数的。
我们在开发时会遇到,更改父组件状态,父组件进行重新渲染,子组件的props
并没有发生改变,但子组件依然会重新渲染。
我们就可以使用React.memo
包裹组件,如果组件Props
未发生变化的话就不会进行重新渲染。
import React from "react";
function Child(props) {
console.log(props.name)
return <h1>{props.name}</h1>
}
export default React.memo(Child)
默认情况下,React.memo
只会进行浅层比较,如果想要自己控制可以考虑,传入第二个参数,自定义进行控制。
function MyComponent(props) {
/* 使用 props 渲染 */
}
function areEqual(prevProps, nextProps) {
/*
如果把 nextProps 传入 render 方法的返回结果与
将 prevProps 传入 render 方法的返回结果一致则返回 true,
否则返回 false
*/
}
export default React.memo(MyComponent, areEqual);
useCallback
当父组件传递给子组件一个函数进行调用的时候,如果父组件重新渲染,由于组件中函数会被重新调用一遍,那么前后两个传递的函数引用并不会是一个,所以 父组件传递给子组件的props发生了改变。
这种情况就可以使用useCallback
,在函数没有改变的时候,返回记忆化的函数,传递给子组件。
// index.js
import React, { useState, useCallback } from "react";
import ReactDOM from "react-dom";
import Child from "./child";
function App() {
const [title, setTitle] = useState("这是一个 title");
const [subtitle, setSubtitle] = useState("我是一个副标题");
const callback = () => {
setTitle("标题改变了");
};
// 通过 useCallback 进行记忆 callback,并将记忆的 callback 传递给 Child
const memoizedCallback = useCallback(callback, [])
return (
<div className="App">
<h1>{title}</h1>
<h2>{subtitle}</h2>
<button onClick={() => setSubtitle("副标题改变了")}>改副标题</button>
<Child onClick={memoizedCallback} name="桃桃" />
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
减少计算的量
useMemo
假设我们有一个函数内部每次都有一个很大的重复计算量,如果每次都是重复调用函数,那么性能会被很大的消耗。
我们可以使用useMemo
进行计算的缓存
function computeExpensiveValue() {
// 计算量很大的代码
return xxx
}
const memoizedValue = useMemo(computeExpensiveValue, [a, b]);
useMemo 的第一个参数就是一个函数,这个函数返回的值会被缓存起来,同时这个值会作为 useMemo 的返回值,第二个参数是一个数组依赖,如果数组里面的值有变化,那么就会重新去执行第一个参数里面的函数,并将函数返回的值缓存起来并作为 useMemo 的返回值 。
通用React 性能优化
懒加载组件
导入多个文件合并到一个文件中的过程叫打包,使应用不必导入大量外部文件。所有主要组件和外部依赖项都合并为一个文件,通过网络传送出去以启动并运行 Web 应用。这样可以节省大量网络调用,但这个文件会变得很大,消耗大量网络带宽。应用需要等待这个文件的加载和执行,所以传输延迟会带来严重的影响。
为了解决这个问题,我们引入代码拆分的概念。像 webpack 这样的打包器支持就支持代码拆分,它可以为应用创建多个包,并在运行时动态加载,减少初始包的大小。
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>
)
}
}
上面的代码中有一个条件语句,它查找 props 值,并根据指定的条件加载主组件中的两个组件。
我们可以按需懒惰加载这些拆分出来的组件,增强应用的整体性能。
不可变的数据结构
按照React的渲染规则,会有很多性能浪费在不必要的渲染上。所以我们使用Immutable
进行精确渲染,简化shouldComponentUpdate
比较。
Immutable Data
就是一旦创建,就不能再被更改的数据。对 Immutable 对象的任何修改或添加删除操作都会返回一个新的 Immutable 对象。Immutable 实现的原理是 Persistent Data Structure
(持久化数据结构),也就是使用旧数据创建新数据时,要保证旧数据同时可用且不变。同时为了避免 deepCopy 把所有节点都复制一遍带来的性能损耗,Immutable 使用了 Structural Sharing
(结构共享),即如果对象树中一个节点发生变化,只修改这个节点和受它影响的父节点,其它节点则进行共享。
Hooks
React 一直都提倡使用函数组件,但是有时候需要使用 state 或者其他一些功能时,只能使用类组件,因为函数组件没有实例,没有生命周期函数,只有类组件才有,而Hooks就是解决这种问题。
为什么要使用Hooks
类组件的不足:
- 趋向复杂难以维护:在生命周期函数中混杂不相干的逻辑(如:在
componentDidMount
中注册事件以及其他的逻辑,在componentWillUnmount
中卸载事件,这样分散不集中的写法,很容易写出 bug ) - this 指向问题:父组件给子组件传递函数时,必须绑定 this
- 很难在组件之间复用状态逻辑:在组件之间复用状态逻辑很难,可能要用到 render props (渲染属性)或者 HOC(高阶组件),但无论是渲染属性,还是高阶组件,都会在原先的组件外包裹一层父容器(一般都是 div 元素),导致层级冗余
Hooks的优势:
- 优化类组件的三大问题
- 能将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据)
- 副作用的关注点分离:副作用指那些没有发生在数据向视图转换过程中的逻辑,如
ajax
请求、访问原生dom
元素、本地持久化缓存、绑定/解绑事件、添加订阅、设置定时器、记录日志等。
useEffect
- effect(副作用):指那些没有发生在数据向视图转换过程中的逻辑,如
ajax
请求、访问原生dom
元素、本地持久化缓存、绑定/解绑事件、添加订阅、设置定时器、记录日志等。 - useEffect 就是一个 Effect Hook,给函数组件增加了操作副作用的能力。它跟 class 组件中的
componentDidMount
、componentDidUpdate
和componentWillUnmount
具有相同的用途,只不过被合并成了一个 API - useEffect 接收一个函数,该函数会在组件渲染到屏幕之后才执行,该函数有要求:要么返回一个能清除副作用的函数,要么就不返回任何内容
function Counter(){
let [number,setNumber] = useState(0);
let [text,setText] = useState('');
// 相当于componentDidMount 和 componentDidUpdate
useEffect(()=>{
console.log('开启一个新的定时器')
let $timer = setInterval(()=>{
setNumber(number=>number+1);
},1000);
// useEffect 如果返回一个函数的话,该函数会在组件卸载和更新时调用
// useEffect 在执行副作用函数之前,会先调用上一次返回的函数
// 如果要清除副作用,要么返回一个清除副作用的函数
/* return ()=>{
console.log('destroy effect');
clearInterval($timer);
} */
});
// },[]);//要么在这里传入一个空的依赖项数组,这样就不会去重复执行
return (
<>
<input value={text} onChange={(event)=>setText(event.target.value)}/>
<p>{number}</p>
<button>+</button>
</>
)
}
useState
React 假设当你多次调用 useState 的时候,你能保证每次渲染时它们的调用顺序是不变的。
通过在函数组件里调用它来给组件添加一些内部 state,React会 在重复渲染时保留这个 state
useState 唯一的参数就是初始 state
useState 会返回一个数组:一个 state,一个更新 state 的函数
- 在初始化渲染期间,返回的状态 (state) 与传入的第一个参数 (initialState) 值相同
- 你可以在事件处理函数中或其他一些地方调用这个函数。它类似 class 组件的 this.setState,但是它不会把新的 state 和旧的 state 进行合并,而是直接替换
// 这里可以任意命名,因为返回的是数组,数组解构
const [state, setState] = useState(initialState);
特点:
-
每次渲染都是一个独立的闭包:
- 每一次渲染都有它自己的 Props 和 State
- 每一次渲染都有它自己的事件处理函数
- 当点击更新状态的时候,函数组件都会重新被调用,那么每次渲染都是独立的,取到的值不会受后面操作的影响
-
函数式更新
- 如果新的 state 需要通过使用先前的 state 计算得出,那么可以将回调函数当做参数传递给 setState。该回调函数将接收先前的 state,并返回一个更新后的值。
-
惰性初始化state
- initialState 参数只会在组件的初始化渲染中起作用,后续渲染时会被忽略
- 如果初始 state 需要通过复杂计算获得,则可以传入一个函数,在函数中计算并返回初始的 state,此函数只在初始渲染时被调用