一. React developer tools安装及使用
这是React项目的调试工具,方便我们在开发过程中对React项目进行调试。
安装步骤:
1. 去github下载react-devtools压缩包,也可以通过git bash下载:
git clone https://github.com/facebook/react-devtools.git
2. 下载成功后,进入react-devtools文件夹,通过git bash安装依赖,安装时间会比较长:
npm --registry https://registry.npm.taobao.org install
3. 安装成功后,我们便可以打包一份扩展程序出来:
npm run build:extension:chrome
4. 进入react-devtools\shells\chrome\build目录下,将里面的unpacked文件夹拖进chrome扩展程序里面即可。
注意:此方法不太好使,建议使用别的方法。
二. PropTypes与DefaultProps的应用
1. PropTypes,子组件可以对父组件传过来的属性或者方法做一个类型校验,不符合时会警告。
使用方法:
在TodoItem组件中引入PropTypes
在class的后面使用PropTypes来对父组件传过来的值做类型校验:
当传过来的值类型不符合时,控制台会警告。
知识点:
当我们需要某个属性必须传的时候,我们可以这么写:
TodoItem.propTypes = {
test: PropTypes.string.isRequired, //此处要求test时必须传,且是字符串类型
content: PropTypes.string,
deleteItem: PropTypes.func,
index: PropTypes.number
}
2. DefaultProps, 默认值,当我们子组件要求父组件传某个属性,但是父组件却没有传,我们可以自己定义一个默认值。
使用方法:
还有一些关于props的功能,可以自行查阅react官网。
三. props,state与render函数的关系
1. React中数据与页面之间互相联动的底层运行机理。
当组件的state或者props发生改变的时候,render函数就会重新执行。
- 拿我们前面做的TodoList为例。当我们往input框输入内容的时候,onchange事件就会被执行,而onchange事件对应的方法就会改变state,一旦state发生改变,那么render函数就会重新地被执行,那么页面就会重新渲染,渲染的数据也是最新的。我们可以通过在render函数中console.log()内容进行验证,可以发现每次输入都会打印。
- props改变的场景:当我们父组件向子组件传值的时候,子组件通过props接收。父组件向子组件传递的值发生了改变,那么props也就发生了改变,相应地,子组件中的render函数就会重新执行。还有一种情况子组件的render函数也会被重新执行,当父组件的render函数重新执行的时候,由于子组件也是在父组件的render函数中被调用的,所以也会导致子组件被重新渲染,也就是子组件的render函数被重新执行。
四. React中的虚拟DOM
假如我们需要自己实现一个React数据重新渲染的功能,我们需要做如下准备:
1、初始方案
之所以上面的准备耗性能,是因为我们上面每次都是把整个dom重新生成,而不是只生成数据改变了的那部分dom,这消耗了性能,另一方面替换掉完整的dom,这也会消耗性能。
2、第一次改进
上面的改进方案并不直接替换全部的原始dom,这节约了一部分性能。但是新的dom和原始dom进行比对又消耗了一部分性能,所以性能的提升并不明显。
3、React提出了虚拟DOM的方案
上面便是React的虚拟DOM,在这里需要知道一个点:js创建或者操作对象比创建或者操作真实的dom节省了很大的性能,这就是React提出虚拟DOM的原因。
五. 深入了解虚拟DOM
1. 在实际过程中, React的虚拟dom和上面有一些区别,上面是为了方便用户理解。实际上是这样的:
2. 我们项目中render函数里面的标签内容不是真实的dom,它只是模板,当render函数执行的时候模板与数据相结合,生成虚拟的dom。
过程: JSX模板-------> createElement-----> 虚拟dom(JS对象)------->真实的dom
原理:
render() {
return (
<div>item</div>
)
}
上面的jsx语法执行以后实际上是调用了下面的方法
render() {
return React.createElement('div', {}, 'item');
}
这两次代码在页面中的效果是一样的,只不过第二次的代码调用了更底层的实现方法,没有使用jsx语法。实际开发中之所以不采用createElement方法,是因为jsx语法更简洁,更方便我们编写。Vue框架中的虚拟dom机制和React是一致的。
3. 虚拟DOM的优点。
- 性能提升了
- 它使得跨端应用得以实现。React Native
React Native之所以能做app,依靠的就是虚拟DOM。当我们运行在浏览器端的时候,虚拟DOM就会生成真实的DOM,当我们运行在原生应用平台的时候,虚拟DOM就会生成相应的原生应用组件。
六. 虚拟DOM中的Diff算法
两个虚拟DOM比对的方式我们就称为Diff算法。
1. 为什么React中要把setstate设置成为异步
比如说我们连续调用了三次setstate,React就会把三次合并成一次setstate,只去做一次虚拟DOM的比对,然后去更新一次dom,这样就可以省去额外的两次DOM比对带来的性能耗费。这么设置本质上是为了提高React底层的性能。
2. 为什么不建议用index来做key值
React中虚拟DOM比对的Diff算法是同级比对的,当数据发生改变了以后,又会生成一个新的虚拟DOM,两者进行比对,找到差异以后就会生成新真实的DOM。首先比对最顶层的虚拟DOM,假设他俩一致就会去比对第二层。如果他俩不一致,于是他就会把原始页面上的虚拟DOM,对应的下面的所有DOM,全部都删除掉。重新生成一遍节点下的所有的DOM,用重新生成的这些DOM,替换掉原始页面上的DOM。也就是它如果比对的这层虚拟DOM不一致,下面几层就不去比了,整个的就对原始的DOM进行替换了。这么比对有些可重复利用的dom还是会被销毁掉,消耗了一些性能,但是好处是算法简单,这样带来的好处就是比对的速度会非常的快,大大减少了比对算法的性能上的消耗。
我们平时给数组列表设置key值,使得两个虚拟DOM之间有名字,方便两两进行比对。而我们不推荐使用index值来作为key值,因为比如说第二次的虚拟DOM是我们删除了数组的某一项,此时一部分元素的index值就发生了改变,这样我们算法根据key值进行比对的就发生了错误。这就是为什么我们一般不推荐使用index来作为key值得原因。
七. React中ref的使用
1、希望content的类型可以是字符串或者是数组
2、 ref的使用
在React中除了可以使用e.target来获取对应的dom元素以外,还可以使用ref来获取dom元素。
1. 之前的TodoList中handleInputChange方法里面的e.target可以用ref替换。方法:
首先给render函数里面的input添加ref(注意:inputDom是一个参数,可以自己随便定义)
接着将handleInputChange方法里面的e.target替换即可。实际效果一样:
但是React是数据驱动的框架,我们不推荐使用ref直接操作dom,除非遇到很复杂的业务必须操作dom。
2、使用ref遇到的一个问题:别忘了setState是异步的。
我们希望在TodoList中实现一个功能,点击提交按钮,打印出ul里面div的长度。
首先:
接着:
我们通过控制台发现结果不对:每次打印都会少一个。
原因是React的setState方法是异步的,每次打印出来以后setState方法才可能会执行。解决方法:
使用setState方法的第二个参数,该参数里面的函数会等setState异步执行完成之后才会被执行。
结果:
八、React的生命周期钩子
React的生命周期主要经历了四个阶段:
Initialization: 初始化阶段
Mounting:组件挂载到页面阶段
Updating:组件内容更新阶段
Unmounting:把组件从页面去除阶段
1、初始化阶段:
该阶段的生命周期函数在组件刚被创建的那个时刻就会调用。由于constructor这个函数并非React所独有,ES6中也存在,所以React并没有将它归为生命周期函数,但是它就是在该阶段调用的。在该阶段会定义state,会接收props。
2、挂载阶段:
该阶段有三个生命周期函数被调用
- componentWillMount 在组件即将第一次被挂载到页面的时刻自动执行
- render 组件挂载到页面的时刻自动执行
- componentDidMount 组件第一次被挂载到页面之后,自动地执行
我们分别在不同阶段打印相应的生命周期函数,结果如下:
我们在输入框输入文字,发现只有render函数被调用,这是因为另外两个只在第一次被挂载的时候会调用,之后就不会被调用了:
3、组件内容更新阶段
有两种情况会进入该阶段,一种是props发生改变,另一种是state发生改变。从上面的图可以发现两者的区别是props比state开始多调用一个生命周期函数(因为归根结底props改变的原因是因为父组件的state改变)。
两者都有的:
- shouldComponentUpdate 组件被更新之前,他会自动被执行。有三种情况
1.
shouldComponentUpdate() {
console.log('shouldComponentUpdate');
}
然后在输入框中输入,但是输入框却不现实文字,结果:
这是因为这个生命周期函数最后需要return一个布尔值。
2. return ture
shouldComponentUpdate() {
console.log('shouldComponentUpdate');
return true;
}
为了方便我们记忆,上面代码可以这么理解:应该让组件更新吗?return true的话就让组件更新,相应的输入框也能显示文字,组件更新也就能够调用rener函数了。结果页面:
3. return false
shouldComponentUpdate() {
console.log('shouldComponentUpdate');
return false;
}
return false组件不会更新,所以输入框不会显示我们输入的文字,但是控制台不会报错,因为我们return 了一个布尔值。结果页面:
- componentWillUpdate
//组件被更新之前,它会自动执行。但是它在shouldComponentUpdate之后执行
//如果shouldComponentUpdate返回true它才会执行
//如果返回false,这个函数就不会被执行了
componentWillUpdate() {
console.log('componentWillUpdate');
}
下面是return true的情况:
- render
- componentDidUpdate 组件更新完毕之后,它会被执行
componentDidUpdate() {
console.log('componentDidUpdate')
}
结果:
props独有的:
- componentWillReceiveProps 前提是一个组件要从父组件接收参数。如果这个组件第一次存在于父组件中不会被执行,如果这个组件已经存在于父组件中了,才会被执行。
该生命周期函数是props独有的,它执行的前提是该组件必须有props参数,也就是父组件给该组件传值。所以我们把这个生命周期函数写在TodoItem这个子组件中。
效果演示:
TodoItem:
componentWillReceiveProps() {
console.log('child componentWillReceiveProps');
}
结果:我们输入完毕第一次点击提交以后,不会打印出来,再次输入以后,此时父组件的render函数重新执行后,才会执行该生命周期函数,即打印出来。
4、把组件从页面去除阶段
- componentWillUnmount 当这个组件即将被从页面剔除的时候执行
TodoItem:
componentWillUnmount() {
console.log('child componentWillUnmount');
}
结果:当我们点击列表项时,该列表项被删除,此时该生命周期函数被执行
九、生命周期函数的使用场景
1. 注意: render这个生命周期函数必须存在。
这是因为React的组件是继承自Component这个组件的,该组件默认内置了所有的生命周期函数,唯独没有内置render这个生命周期函数的实现。
2. 利用生命周期函数提高性能
案例:
TodoItem中的render函数中添加console.log('child render');
结果:
实际中上面每次输入导致TodoItem这个子组件都会重新被渲染是耗性能的,我们可以利用shouldComponentUpdate这个生命周期函数来实现。
TodoItem:
shouldComponentUpdate(nextProps, nextState) {
//nextProps表示组件如果更新后的Props
//nextState表示组件如果更新后的state
if(nextProps.content !== this.props.content) {
return true;
} else {
return false;
}
}
效果:
可以发现,此时当我们只输入没有提交的时候,TodoItem这个组件不会重新被渲染,提高了性能。
3. 在哪个生命周期函数中发送ajax请求?
不要放在render函数中,因为它在页面中经常会被反复地执行。推荐放在componentDidMount这个生命周期函数中。
10、使用 mockjs 实现本地数据 mock
1、课程讲的 charles 折腾了一下午也用不了,应该是版本更新后的问题。查找资料后使用 mockjs 成功实现了
- 安装 mockjs : npm install mockjs ;
- 在 src 目录下建立 mock 文件夹,里面建立 mockdata.js 文件:
import Mock from 'mockjs';
Mock.mock('/todolist', {
data: ['a', 'b', 'c']
}) //第一个参数是自己写的,到时候 axios 也写这个路径即可, 第二个参数放数据
- TodoList.js 文件引入 mockdata.js , 然后添加代码:
componentDidMount() {
axios.get('/todolist')
.then((res) => {
const data = res.data.data;
this.setState(() => ({
list: [...data]
}))
})
.catch(() => {console.log('error')});
}
此时页面就渲染出模拟的数据了。
11、React中实现CSS过渡动画
删除之前的 TodoList 后的目录结构:
index.js:
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(<App />, document.getElementById('root'));
App.js:
import React, { Component, Fragment } from 'react';
import './style.css';
class App extends Component {
constructor(props) {
super(props);
this.state = {
show: true
};
this.handleToggle = this.handleToggle.bind(this);
}
render() {
return (
<Fragment>
<div className={this.state.show ? 'show' : 'hide'}>hello world</div>
<button onClick={this.handleToggle}>toggle</button>
</Fragment>
)
}
handleToggle() {
this.setState({
show: this.state.show ? false : true
})
}
}
export default App;
style.css:
.show {
opacity: 1;
transition: all 1s ease-in; /*所有的属性都用 1s 的时间来切换*/
}
.hide {
opacity: 0;
transition: all 1s ease-in; /*所有的属性都用 1s 的时间来切换*/
}
启动项目就实现了一个切换显示与否的渐变效果。
12、React中使用CSS动画效果
之前代码不变,就改变 style.css 的效果:
.show {
animation: show-item 2s ease-in forwards;
}
.hide {
animation: hide-item 2s ease-in forwards; /*forwards是让动画结束的时候保存最后一帧的样式,不加它的话就不会隐藏*/
}
@keyframes hide-item {
0% {
opacity: 1;
color: red;
}
50% {
opacity: .5;
color: green;
}
100% {
opacity: 0;
color: blue;
}
}
@keyframes show-item {
0% {
opacity: 0;
color: red;
}
50% {
opacity: .5;
color: green;
}
100% {
opacity: 1;
color: blue;
}
}
此时运行后就有了动画的效果。
13、使用react-transition-group实现动画(一)
简单的动画我们可以自己写,但是比较复杂的我们可以借助这个第三方模块 : react-transition-group
- 首先进行安装 :
npm install react-transition-group --save
- App.js :
import React, { Component, Fragment } from 'react';
import { CSSTransition } from 'react-transition-group';
import './style.css';
class App extends Component {
constructor(props) {
super(props);
this.state = {
show: true
};
this.handleToggle = this.handleToggle.bind(this);
}
render() {
return (
<Fragment>
<CSSTransition
in={this.state.show} //根据show的值来判断是出场动画还是入场动画
timeout={300} //动画的时间
classNames='fade' //给类添加前缀名
>
<div>hello world</div>
</CSSTransition>
<button onClick={this.handleToggle}>toggle</button>
</Fragment>
)
}
handleToggle() {
this.setState({
show: this.state.show ? false : true
})
}
}
export default App;
- style.css :
/*执行入场动画的时候,也就是show从false变成true的时候。在入场动画执行的第一个时刻CSSTransition这个组件会往div上挂载一个样式,名字就是fade-enter*/
.fade-enter {
opacity: 0;
}
/*入场动画执行的第二个时刻到入场动画完成之前的时刻,这段时间div上面一直会有fade-enter-active这个class*/
.fade-enter-active {
opacity: 1;
transition: opacity 1s ease-in;
}
/*当整个入场动画执行完以后,这个class就会被增加到div上面*/
.fade-enter-done {
opacity: 1;
}
/*出场的第一个时刻,该class会被加上去*/
.fade-exit {
opacity: 1;
}
/*执行出场动画的这段时间都会存在这个class*/
.fade-exit-active {
opacity: 0;
transition: opacity 1s ease-in;
}
/*整个出场动画完成之后*/
.fade-exit-done {
opacity: 0;
}
此时我们就实现了和之前一样的动画效果。
虽然看起来我们实现一样的效果,使用这个模块来做更复杂,但是这个第三方的库有着很多特别实用的地方:
- 给 CSSTransition 这个组件添加 unmountOnExit ,可以发现动画结束后 div 元素就不存在,动画开始它又出来了。
- 这个模块还有很多钩子函数(类似生命周期函数),我们可以使用这些钩子函数实现特定的动画。
render() {
return (
<Fragment>
<CSSTransition
in={this.state.show} //根据show的值来判断是出场动画还是入场动画
timeout={300} //动画的时间
classNames='fade' //给类添加前缀名
unmountOnExit
onEntered={(el) => {el.style.color='red'}} //el 指的就是内部的 div 元素,意思是当入场动画执行结束之后,给元素添加红色的字体颜色
>
<div>hello world</div>
</CSSTransition>
<button onClick={this.handleToggle}>toggle</button>
</Fragment>
)
添加配置:
appear={true} //第一次展示的时候就有动画效果
一般这个组件都能满足我们 react 开发中需要的动画效果,到时候查文档即可。
14、使用react-transition-group实现动画(二)
1、实现多个元素的动画效 App.js:
import React, { Component, Fragment } from 'react';
import { CSSTransition, TransitionGroup } from 'react-transition-group';
import './style.css';
class App extends Component {
constructor(props) {
super(props);
this.state = {
list: []
};
this.handleAddItem = this.handleAddItem.bind(this);
}
render() {
return (
<Fragment>
<TransitionGroup>
{
this.state.list.map((item, index) => {
return (
<CSSTransition
key={index}
timeout={300}
classNames='fade'
unmountOnExit
onEntered={(el) => {el.style.color='red'}}
appear={true}
>
<div>{item}</div>
</CSSTransition>
)
})
}
</TransitionGroup>
<button onClick={this.handleAddItem}>toggle</button>
</Fragment>
)
}
handleAddItem() {
this.setState((prevState) => {
return {
list: [...prevState.list, 'item']
}
})
}
}
export default App;
其实就是用 TransitionGroup 包起来, 内部每一项还是使用 CSSTransition 。