seo技术网站建设,做室内概念图的网站,免费装wordpress,网站设计酷站本文来自#React系列教程#xff1a;https://mp.weixin.qq.com/mp/appmsgalbum?__bizMzg5MDAzNzkwNAactiongetalbumalbum_id1566025152667107329) 一. 生命周期
1.1. 认识生命周期
很多的事物都有从创建到销毁的整个过程#xff0c;这个过程称之为是生命周期https://mp.weixin.qq.com/mp/appmsgalbum?__bizMzg5MDAzNzkwNAactiongetalbumalbum_id1566025152667107329) 一. 生命周期
1.1. 认识生命周期
很多的事物都有从创建到销毁的整个过程这个过程称之为是生命周期
React组件也有自己的生命周期了解组件的生命周期可以让我们在最合适的地方完成自己想要的功能
生命周期和生命周期函数的关系
生命周期是一个抽象的概念在生命周期的整个过程分成了很多个阶段 比如装载阶段Mount组件第一次在DOM树中被渲染的过程比如更新过程Update组件状态发生变化重新更新渲染的过程比如卸载过程Unmount组件从DOM树中被移除的过程 React内部为了告诉我们当前处于哪些阶段会对我们组件内部实现的某些函数进行回调这些函数就是生命周期函数 比如实现componentDidMount函数组件已经挂载到DOM上时就会回调比如实现componentDidUpdate函数组件已经发生了更新时就会回调比如实现componentWillUnmount函数组件即将被移除时就会回调
我们可以在这些回调函数中编写自己的逻辑代码来完成自己的需求功能
我们谈React生命周期时主要谈的类的生命周期因为函数式组件是没有生命周期函数的后面我们可以通过hooks来模拟一些生命周期的回调
1.2. 常用生命周期解析
我们先来学习一下最基础、最常用的生命周期函数 上图第一个区域解析
当我们挂载一个组件时会先执行constructor构造方法来创建组件紧接着调用render函数获取要渲染的DOM结构jsx并且开始渲染DOM当组件挂载成功DOM渲染完成会执行componentDidMount生命周期函数
上图第二个区域解析
当我们通过修改props或者调用setState修改内部状态或者直接调用forceUpdate时会重新调用render函数进行更新操作当更新完成时会回调componentDidUpdate生命周期函数
上图第三个区域解析
当我们的组件不再使用会被从DOM中移除掉卸载这个时候会回调componentWillUnmount生命周期函数
1.3. 生命周期函数
constructor
constructor(props)如果不初始化 state 或不进行方法绑定则不需要为 React 组件实现构造函数。
constructor中通常只做两件事情
通过给 this.state 赋值对象来初始化内部的state为事件绑定实例this
componentDidMount
componentDidMount()componentDidMount() 会在组件挂载后插入 DOM 树中立即调用。
componentDidMount中通常进行哪里操作呢
依赖于DOM的操作可以在这里进行在此处发送网络请求就最好的地方官方建议可以在此处添加一些订阅会在componentWillUnmount取消订阅
componentDidUpdate
componentDidUpdate(prevProps, prevState, snapshot)componentDidUpdate() 会在更新后会被立即调用首次渲染不会执行此方法。
当组件更新后可以在此处对 DOM 进行操作如果你对更新前后的 props 进行了比较也可以选择在此处进行网络请求例如当 props 未发生变化时则不会执行网络请求。
componentDidUpdate(prevProps) {// 典型用法不要忘记比较 propsif (this.props.userID ! prevProps.userID) {this.fetchData(this.props.userID);}
}componentWillUnmount
componentWillUnmount()componentWillUnmount() 会在组件卸载及销毁之前直接调用。
在此方法中执行必要的清理操作例如清除 timer取消网络请求或清除在 componentDidMount() 中创建的订阅等
代码验证所有的生命周期函数
import React, { Component } from react;class HYTestCpn extends Component {render() {return h2HYTestCpn/h2}componentWillUnmount() {console.log(HYTestCpn componentWillUnmount);}
}export default class App extends Component {constructor(props) {super(props);this.state {counter: 0}console.log(调用constructor方法);}render() {console.log(调用render方法)return (divh2当前计数: {this.state.counter}/h2{this.state.counter 5 HYTestCpn/}button onClick{e this.increment()}1/button/div)}increment() {this.setState({counter: this.state.counter 1})}componentDidMount() {console.log(调用componentDidMount方法);}componentDidUpdate() {console.log(调用componentDidUpdate方法);}componentWillUnmount() {console.log(调用componentWillUnmount方法);}
}1.4. 不常用生命周期
除了上面介绍的生命周期函数之外还有一些不常用的生命周期函数 getDerivedStateFromPropsstate 的值在任何时候都依赖于 props时使用该方法返回一个对象来更新stategetSnapshotBeforeUpdate在React更新DOM之前回调的一个函数可以获取DOM更新前的一些信息比如说滚动位置shouldComponentUpdate该生命周期函数很常用但是我们等待讲性能优化时再来详细讲解
另外React中还提供了一些过期的生命周期函数这些函数已经不推荐使用。
更详细的生命周期相关的内容可以参考官网https://zh-hans.reactjs.org/docs/react-component.html
二. setState
在 constructor() 函数中不要调用 setState() 方法。如果你的组件需要使用内部state请直接在构造函数中为this.state 赋值初始 state
constructor(props) {super(props);// 不要在这里调用 this.setState() this.state { counter: 0};this.handleClick this.handleClick.bind(this);
}只能在构造函数中直接为 this.state 赋值。如需在其他方法中赋值你应使用 this.setState() 替代。 你可以用类似的方式改写代码来避免可变对象的产生。例如我们有一个叫做 colormap 的对象。我们希望写一个方法来将 colormap.right 设置为 blue。我们可以这么写
function updateColorMap(colormap) { colormap.right blue;
}为了不改变原本的对象我们可以使用 Object.assign 方法
function updateColorMap(colormap) {return Object.assign({}, colormap, {right: blue});
}现在 updateColorMap 返回了一个新的对象而不是修改老对象。Object.assign 是ES6的方法需要polyfill。
不可变数据的力量
避免该问题最简单的方式是避免更改你正用于props 或 state的值。例如上面handleClick 方法可以用 concat 重写
handleClick() {this.setState(state ({words: state.words.concat([marklar]) }));
}ES6数组支持扩展运算符这让代码写起来更方便了。如果你在使用Create React App该语法已经默认支持了。
handleClick() {this.setState(state ({words: [...state.words, marklar], }));
}; 参数一为带有形式参数的 updater 函数(state, props) stateChange
state 是对应用变化时组件状态的引用。当然它不应直接被修改。你应该使用基于 state 和 props 构建的新对象来表示变化。
例如假设我们想根据 props.step 来增加 state
this.setState((state, props) {return {counter: state.counter props.step};
});updater函数中接收的state和props都保证为最新。updater的返回值会与state进行浅合并。
setState异步更新
setState的更新是异步的
import React, { Component } from reactexport default class App extends Component {constructor(props) {super(props);this.state {message: Hello World}}render() {return (divh2{this.state.message}/h2button onClick{e this.changeText()}改变文本/button/div)}changeText() {this.setState({message: 你好啊,李银河})console.log(this.state.message); // Hello World}
}最终打印结果是Hello World可见setState是异步的操作我们并不能在执行完setState之后立马拿到最新的state的结果。
为什么setState设计为异步呢
setState设计为异步其实之前在GitHub上也有很多的讨论React核心成员Redux的作者Dan Abramov也有对应的回复有兴趣的同学可以参考一下https://github.com/facebook/react/issues/11527#issuecomment-360199710
我对其回答做一个简单的总结
setState设计为异步可以显著的提升性能 如果每次调用 setState都进行一次更新那么意味着render函数会被频繁调用界面重新渲染这样效率是很低的最好的办法应该是获取到多个更新之后进行批量更新 如果同步更新了state但是还没有执行render函数那么state和props不能保持同步 state和props不能保持一致性会在开发中产生很多的问题
那么如何可以获取到更新后的值呢
setState接受两个参数第二个参数是一个回调函数这个回调函数会在更新后会执行格式如下setState(partialState, callback)
changeText() {this.setState({message: 你好啊,李银河}, () {console.log(this.state.message); // 你好啊,李银河});
}当然我们也可以在生命周期函数
componentDidUpdate(prevProps, provState, snapshot) {console.log(this.state.message);
}setState一定是异步
疑惑setState一定是异步更新的吗
验证一在setTimeout中的更新
changeText() {setTimeout(() {this.setState({message: 你好啊,李银河});console.log(this.state.message); // 你好啊,李银河}, 0);
}验证二原生DOM事件
componentDidMount() {const btnEl document.getElementById(btn);btnEl.addEventListener(click, () {this.setState({message: 你好啊,李银河});console.log(this.state.message); // 你好啊,李银河})
}其实分成两种情况
在 React 组件生命周期或 React 合成事件中setState 是异步在setTimeout或者原生 DOM 事件中setState 是同步
React中其实是通过一个函数来确定的enqueueSetState部分实现react-reconciler/ReactFiberClassComponent.js
enqueueSetState(inst, payload, callback) {const fiber getInstance(inst);// 会根据React上下文计算一个当前时间const currentTime requestCurrentTimeForUpdate();const suspenseConfig requestCurrentSuspenseConfig();// 这个函数会返回当前是同步还是异步更新准确的说是优先级const expirationTime computeExpirationForFiber(currentTime,fiber,suspenseConfig,);const update createUpdate(expirationTime, suspenseConfig);...
}computeExpirationForFiber函数的部分实现
Sync是优先级最高的即创建就更新
export function computeExpirationForFiber(currentTime: ExpirationTime,fiber: Fiber,suspenseConfig: null | SuspenseConfig,
): ExpirationTime {const mode fiber.mode;if ((mode BlockingMode) NoMode) {return Sync;}const priorityLevel getCurrentPriorityLevel();if ((mode ConcurrentMode) NoMode) {return priorityLevel ImmediatePriority ? Sync : Batched;}setState的合并
数据的合并
假如我们有这样的数据
this.state {name: coderwhy,message: Hello World
}我们需要更新message
changeText() {this.setState({message: 你好啊,李银河});
}我通过setState去修改message是不会对name产生影响的
为什么不会产生影响呢源码中其实是有对 原对象 和 新对象 进行合并的 事实上就是使用 Object.assign(target, ...sources) 来完成的
多个setState合并
比如我们还是有一个counter属性记录当前的数字
如果进行如下操作那么counter会变成几呢答案是1为什么呢因为它会对多个state进行合并 increment() {this.setState({counter: this.state.counter 1});this.setState({counter: this.state.counter 1});this.setState({counter: this.state.counter 1});}其实在源码的processUpdateQueue中有一个do...while循环就是从队列中取出多个state进行合并的 如何可以做到让counter最终变成3呢
increment() {this.setState((state, props) {return {counter: state.counter 1}})this.setState((state, props) {return {counter: state.counter 1}})this.setState((state, props) {return {counter: state.counter 1}})
}为什么传入一个函数就可以变出3呢
原因是多个state进行合并时每次遍历都会执行一次函数
三. setState性能优化
React更新机制
我们在前面已经学习React的渲染流程
那么React的更新流程呢 React在props或state发生改变时会调用React的render方法会创建一颗不同的树。
React需要基于这两颗不同的树之间的差别来判断如何有效的更新UI
如果一棵树参考另外一棵树进行完全比较更新那么即使是最先进的算法该算法的复杂程度为 O(n^3 )其中 n 是树中元素的数量https://grfia.dlsi.ua.es/ml/algorithms/references/editsurvey_bille.pdf如果在 React 中使用了该算法那么展示 1000 个元素所需要执行的计算量将在十亿的量级范围这个开销太过昂贵了React的更新性能会变得非常低效
于是React对这个算法进行了优化将其优化成了O(n)如何优化的呢
同层节点之间相互比较不会垮节点比较不同类型的节点产生不同的树结构开发中可以通过key来指定哪些节点在不同的渲染下保持稳定
Diffing算法
1. 对比不同类型的元素
当节点为不同的元素React会拆卸原有的树并且建立起新的树
当一个元素从 a 变成 img从 Article 变成 Comment或从 Button 变成 div 都会触发一个完整的重建流程当卸载一棵树时对应的DOM节点也会被销毁组件实例将执行 componentWillUnmount() 方法当建立一棵新的树时对应的 DOM 节点会被创建以及插入到 DOM 中组件实例将执行 componentWillMount() 方法紧接着 componentDidMount() 方法
比如下面的代码更改
React 会销毁 Counter 组件并且重新装载一个新的组件而不会对Counter进行复用
divCounter /
/divspanCounter /
/span2. 对比同一类型的元素
当比对两个相同类型的 React 元素时React 会保留 DOM 节点仅比对及更新有改变的属性。
比如下面的代码更改
通过比对这两个元素React 知道只需要修改 DOM 元素上的 className 属性
div classNamebefore titlestuff /
div classNameafter titlestuff /比如下面的代码更改
当更新 style 属性时React 仅更新有所更变的属性。通过比对这两个元素React 知道只需要修改 DOM 元素上的 color 样式无需修改 fontWeight。
div style{{color: red, fontWeight: bold}} /
div style{{color: green, fontWeight: bold}} /如果是同类型的组件元素
组件会保持不变React会更新该组件的props并且调用componentWillReceiveProps() 和 componentWillUpdate() 方法下一步调用 render() 方法diff 算法将在之前的结果以及新的结果中进行递归
3. 对子节点进行递归
在默认条件下当递归 DOM 节点的子元素时React 会同时遍历两个子元素的列表当产生差异时生成一个 mutation。
我们来看一下在最后插入一条数据的情况
ullifirst/lilisecond/li
/ulullifirst/lilisecond/lilithird/li
/ul前面两个比较是完全相同的所以不会产生mutation最后一个比较产生一个mutation将其插入到新的DOM树中即可
但是如果我们是在中间插入一条数据
ulli星际穿越/lili盗梦空间/li
/ululli大话西游/lili星际穿越/lili盗梦空间/li
/ulReact会对每一个子元素产生一个mutation而不是保持 li星际穿越/li和li盗梦空间/li的不变这种低效的比较方式会带来一定的性能问题
keys的优化
我们在前面遍历列表时总是会提示一个警告让我们加入一个key属性 我们来看一个案例
import React, { Component } from reactexport default class App extends Component {constructor(props) {super(props);this.state {movies: [星际穿越, 盗梦空间]}}render() {return (divh2电影列表/h2ul{this.state.movies.map((item, index) {return li{item}/li})}/ulbutton onClick{e this.insertMovie()}插入数据/button/div)}insertMovie() {}
}方式一在最后位置插入数据
这种情况有无key意义并不大
insertMovie() {const newMovies [...this.state.movies, 大话西游];this.setState({movies: newMovies})
}方式二在前面插入数据
这种做法在没有key的情况下所有的li都需要进行修改
insertMovie() {const newMovies [大话西游, ...this.state.movies];this.setState({movies: newMovies})
}当子元素(这里的li)拥有 key 时React 使用 key 来匹配原有树上的子元素以及最新树上的子元素
在下面这种场景下key为111和222的元素仅仅进行位移不需要进行任何的修改将key为333的元素插入到最前面的位置即可
ulli key111星际穿越/lili key222盗梦空间/li
/ululli key333Connecticut/lili key111星际穿越/lili key222盗梦空间/li
/ulkey的注意事项
key应该是唯一的key不要使用随机数随机数在下一次render时会重新生成一个数字使用index作为key对性能是没有优化的
SCU的优化shouldComponentUpdate
render函数被调用
我们使用之前的一个嵌套案例
import React, { Component } from react;function Header() {console.log(Header Render 被调用);return h2Header/h2
}class Main extends Component {render() {console.log(Main Render 被调用);return (divBanner/ProductList//div)}
}function Banner() {console.log(Banner Render 被调用);return divBanner/div
}function ProductList() {console.log(ProductList Render 被调用);return (ulli商品1/lili商品2/lili商品3/lili商品4/lili商品5/li/ul)
}function Footer() {console.log(Footer Render 被调用);return h2Footer/h2
}export default class App extends Component {constructor(props) {super(props);this.state {counter: 0}}render() {console.log(App Render 被调用);return (divh2当前计数: {this.state.counter}/h2button onClick{e this.increment()}1/buttonHeader/Main/Footer//div)}increment() {this.setState({counter: this.state.counter 1})}
}在App中我们增加了一个计数器的代码当点击1时会重新调用App的render函数而当App的render函数被调用时所有的子组件的render函数都会被重新调用
那么我们可以思考一下在以后的开发中我们只要是修改了App中的数据所有的组件都需要重新render进行diff算法性能必然是很低的
事实上很多的组件没有必须要重新render它们调用render应该有一个前提就是依赖的数据state、props发生改变时再调用自己的render方法
如何来控制render方法是否被调用呢
通过shouldComponentUpdate方法即可
shouldComponentUpdate
React给我们提供了一个生命周期方法 shouldComponentUpdate很多时候我们简称为SCU这个方法接受参数并且需要有返回值
该方法有两个参数
参数一nextProps 修改之后最新的props属性参数二nextState 修改之后最新的state属性
该方法返回值是一个boolean类型
返回值为true那么就需要调用render方法返回值为false那么就不需要调用render方法默认返回的是true也就是只要state发生改变就会调用render方法
shouldComponentUpdate(nextProps, nextState) {return true;
}我们可以控制它返回的内容来决定是否需要重新渲染。
比如我们在App中增加一个message属性
export default class App extends Component {constructor(props) {super(props);this.state {counter: 0,message: Hello World}}render() {console.log(App Render 被调用);return (divh2当前计数: {this.state.counter}/h2button onClick{e this.increment()}1/buttonbutton onClick{e this.changeText()}改变文本/buttonHeader/Main/Footer//div)}increment() {this.setState({counter: this.state.counter 1})}changeText() {this.setState({message: 你好啊,李银河})}
}jsx中并没有依赖这个message那么它的改变不应该引起重新渲染但是因为render监听到state的改变就会重新render所以最后render方法还是被重新调用了
这个时候我们可以通过实现shouldComponentUpdate来决定要不要重新调用render方法
shouldComponentUpdate(nextProps, nextState) {if (nextState.counter ! this.state.counter) {return true;}return false;
}这个时候我们改变counter时会重新渲染如果我们改变的是message那么默认返回的是false那么就不会重新渲染
但是我们的代码依然没有优化到最好因为当counter改变时所有的子组件依然重新渲染了
所以事实上我们应该实现所有的子组件的shouldComponentUpdate
比如Main组件可以进行如下实现
class Main extends Component {shouldComponentUpdate(nextProps, nextState) {return false;}render() {console.log(Main Render 被调用);return (divBanner/ProductList//div)}
}shouldComponentUpdate默认返回一个false在特定情况下需要更新时我们在上面添加对应的条件即可
PureComponent 和 memo
如果所有的类我们都需要手动来实现 shouldComponentUpdate那么会给我们开发者增加非常多的工作量。
我们来设想一下shouldComponentUpdate中的各种判断的目的是什么
props或者state中的数据是否发生了改变来决定shouldComponentUpdate返回true或者false
事实上React已经考虑到了这一点所以React已经默认帮我们实现好了如何实现呢
将class继承自PureComponent。
比如我们修改Main组件的代码
class Main extends PureComponent {render() {console.log(Main Render 被调用);return (divBanner/ProductList//div)}
}PureComponent的原理是什么呢
对props和state进行浅层比较
查看PureComponent相关的源码
react/ReactBaseClasses.js中 在PureComponent的原型上增加一个isPureReactComponent为true的属性
React-reconcilier/ReactFiberClassComponent.js 这个方法中调用 !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)这个shallowEqual就是进行浅层比较 那么如果是一个函数式组件呢
我们需要使用一个高阶组件memo
我们将之前的Header、Banner、ProductList都通过memo函数进行一层包裹Footer没有使用memo函数进行包裹最终的效果是当counter发生改变时Header、Banner、ProductList的函数不会重新执行而Footer的函数会被重新执行
import React, { Component, PureComponent, memo } from react;const MemoHeader memo(function() {console.log(Header Render 被调用);return h2Header/h2
})class Main extends PureComponent {render() {console.log(Main Render 被调用);return (divMemoBanner/MemoProductList//div)}
}const MemoBanner memo(function() {console.log(Banner Render 被调用);return divBanner/div
})const MemoProductList memo(function() {console.log(ProductList Render 被调用);return (ulli商品1/lili商品2/lili商品3/lili商品4/lili商品5/li/ul)
})function Footer() {console.log(Footer Render 被调用);return h2Footer/h2
}export default class App extends Component {constructor(props) {super(props);this.state {counter: 0,message: Hello World}}render() {console.log(App Render 被调用);return (divh2当前计数: {this.state.counter}/h2button onClick{e this.increment()}1/buttonbutton onClick{e this.changeText()}改变文本/buttonMemoHeader/Main/Footer//div)}increment() {this.setState({counter: this.state.counter 1})}shouldComponentUpdate(nextProps, nextState) {if (nextState.counter ! this.state.counter) {return true;}return false;}changeText() {this.setState({message: 你好啊,李银河})}
}memo的原理是什么呢
react/memo.js 最终返回一个对象这个对象中有一个compare函数 默认是进行了浅比较
不可变数据的力量
我们通过一个案例来演练我们之前说的不可变数据的重要性
import React, { PureComponent } from reactexport default class App extends PureComponent {constructor(props) {super(props);this.state {friends: [{ name: lilei, age: 20, height: 1.76 },{ name: lucy, age: 18, height: 1.65 },{ name: tom, age: 30, height: 1.78 }]}}render() {return (divh2朋友列表/h2ul{this.state.friends.map((item, index) {return (li key{item.name}span{姓名:${item.name} 年龄: ${item.age}}/spanbutton onClick{e this.incrementAge(index)}年龄1/button/li)})}/ulbutton onClick{e this.insertFriend()}添加新数据/button/div)}insertFriend() {}incrementAge(index) {}
}我们来思考一下inertFriend应该如何实现
实现方式一
insertFriend() {this.state.friends.push({name: why, age: 18, height: 1.88});this.setState({friends: this.state.friends})
}这种方式会造成界面不会发生刷新添加新的数据原因是继承自PureComponent会进行浅层比较浅层比较过程中两个friends是相同的对象
实现方式二
insertFriend() {this.setState({friends: [...this.state.friends, {name: why, age: 18, height: 1.88}]})
}[...this.state.friends, {name: why, age: 18, height: 1.88}]会生成一个新的数组引用在进行浅层比较时两个引用的是不同的数组所以它们是不相同的
我们再来思考一下incrementAge应该如何实现
实现方式一
incrementAge(index) {this.state.friends[index].age 1;this.setState({friends: this.state.friends})
}和上面方式一类似
实现方式二
incrementAge(index) {const newFriends [...this.state.friends];newFriends[index].age 1;this.setState({friends: newFriends})
}和上面方式二类似
所以在真实开发中我们要尽量保证state、props中的数据不可变性这样我们才能合理和安全的使用PureComponent和memo。
当然后面项目中我会结合immutable.js来保证数据的不可变性。
总之更新state的原则是不要直接修改state中的原始对象或数组而是要通过新创建一个对象或数组的拷贝在拷贝的对象上进行修改之后再通过setState设置更新给state中的原始对象。
官网对 shouldComponentUpdate 的描述 四. React的脚手架create-react-app
create-react-app 创建项目以及 npm、yarn 包管理工具的使用 参考这里
快捷创建react应用的脚手架命令 启动React项目 编译React项目 五. React开发依赖 认识 Babel 引入 React