React16_4整理-上阶
# 前言
围绕 React 衍生出的思考有:声明式与命令式开发、与其它框架并存、组件式的开发(组件化)、单向数据流、视图层的框架、函数式编程。 还有副作用、纯函数、Thunk函数、promise、genarator、iterator 等等。
上篇文章中整了一个todolist应用练练手,后面也对他进行了优化。接下来说点深入一点的东西,比如怎么调试,类型校验,虚拟dom和diff,生命周期,以及使用工具来进行接口mock,css过渡动画,单、多元素过渡动画等等。
# 安装 React 开发调试工具
下载 React Developer Tools
插件 然后放到 Chrome 浏览器中去,在开发环境下使用这个插件,这个插件的图标会显示红色
。如果你的页面不是 React 开发的,那么图标会显示灰色
如果是线上的环境,那么图标会显示为黑色
。
# 使用 React 开发调试工具
打开开发人员工具,选择 React 选项卡,这时候就可以看到页面标签以 React 组件的方式显示。
使用这个组件来调试组件间传值的时候,就不需要使用 console.log()来打印了。只需要点击这个组件,然后在右侧实时的监控整个组件的状态即可。
# PropTypes 与 DefaultProps
对传递过来的属性的类型做校验,如果不符合要求,就会自动警告,用来限制传值时的数据类型,对开发是比较友好的。
// 你要下载这个模块儿,然后才能引入,脚手架工具中自带了
import PropTypes from 'prop-types';
// ...
// 对当前组件.props属性 使用 组件.propTypes 的数据类型进行校验
TodoItem.propTypes = {
{/*表示传递过来的content属性一定要是string类型*/}
content: PropTypes.string,
{/* 一定要是一个函数 */}
deleteItem: PropTypes.func,
{/* 一定要是一个数字*/}
index: PropTypes.number,
{/* 表示传递过来的test属性一定要是string类型的,并且这个test参数必须传递过来了,否则就警告*/}
test: PropTypes.string.isReuqired,
{/*表示test2 必须是一个数组,数组的组成内容可以一个string类型或者number类型*/}
test2: PropTypes.arrayOf(PropTypes.string, PropTypes.number),
{/*表示test3 必须是一个string类型或者number类型*/}
test3: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
}
// 给当前组件.props中的成员设置默认值
TodoItem.defaultProps = {
test: 'hello world'
}
# Props,State 与 render 函数
当组件的 state 或者 props 发生改变的时候,组件的 render 函数就会重新执行。
实际原理
还是 setState
方法被调用
了,然后执行了re-render
(patch)的方法,重新渲染页面的组件。
# 什么是虚拟 DOM
state 数据,JSX 模板。数据+模板 结合,生成虚拟 DOM ,由虚拟 DOM 生成真实 DOM 来显示。
state 发生改变,数据+模板 结合,生成新虚拟 DOM ,对比之前的旧的虚拟 DOM,找出差异点,不是整版替换,而是经过新旧虚拟 DOM 节点对比。
由框架直接操作 DOM,将变化的旧节点的 DOM 替换成新的节点的 DOM。
虚拟 DOM 就是使用 JS 模拟 DOM,目的是为了提升性能,因为直接操作太消耗性能,一个 DOM 对象有几百个成员,这样会造成进行节点对比的时候极大的损耗性能。
# 深入了解虚拟 DOM
JSX 语法其实是虚拟 dom 创建的语法糖,最后会被统一的转换为React.createElement()
方法,这个方法创建虚拟 dom。
因为虚拟 DOM 所以性能提升了,它使得跨端应用得以实现,如 React Native。
在 React Native 中让虚拟 DOM 不去生成真实的 DOM 而是生成一些原生的组件,那么就能实现很好的重用了,这样就能够让 React 即能生成网页应用,也能够生成原生应用。
# 虚拟 DOM 中的 Diff 算法
调用 setState 才会让数据发生变化,setState 是异步,这么做是为了提升性能。
如果你连续调用三次 setState,那么 React 会将三次操作合并成一次,然后再去调用 re-render 的方法渲染页面,这也是为什么 setState 是异步的原因。
同层虚拟 DOM 比较,从上往下进行比较,找到差异之后去更新真实的 DOM,如果有一层有差异就不会往下比较了,这样比对的速度非常快。
# key 属性
key 属性的设置,就是给虚拟 dom 的节点设置名字。
假如有一个数组,原来里面有五个数据,如果你又增加了一个数据,这时候你如果要新旧虚拟 dom 节点进行对比,就会出现双层 for 循环的对比方式了。
这样很麻烦,如果你设置了 key 属性,那么就直接遍历 key 值相同的进行对比,这样就只是单重 for 循环的对比了。
这样就只用把新增加的数据添加到 ul 的 dom 中就可以了。所以不要把 key 属性的值设置为 index,那样没法保证原始的虚拟 dom 节点的 key 值与新的虚拟 dom 节点的 key 值一致了。
因为可能会发生数组元素的位置变化,那时候新的虚拟 dom 节点的 key 值是整版替换的,相当于没有设置。
例如 你删除了一个数组元素,然后 key 值等于 index,这时候 key 值相当于整版替换了,因为 index 值是数组元素的全新的下标。
key 值很不稳定,key 值需要很稳定,例如你将 key 值设置为 item,这样就确定了唯一性。
能不用 index 作为 key 值的时候就不用 index 作为 key 值,key 值的设置是为了提高虚拟 dom 比对的性能。
# React 中 ref 的使用
之前需要通过e.target
来获取事件源
handleInput (e) {
// 通过事件源的方式获取页面标签了
const input = e.target;
}
现在可以通过 ref 来获取页面中的 dom
{/*在页面标签中添加ref*/}
<input ref = {(input) => {this.input = input;}}
handleInput () {
// 这样就能够获取 页面中的指定input了
const input = this.input;
}
但是不推荐使用 ref,React 中推荐使用以数据驱动的方式编写代码,不要直接去操作 DOM。
setState 操作是异步的,如果你在 setState 中清空了输入框的内容,然后再 setState 的下面通过 dom 获取了输入框的内容,这时候就会发现值还没有清空。
因为同步代码会被先执行,异步代码是等同步代码执行完毕后才会执行的。
虽然你可以通过 setState 的回调函数来使用 dom 实时的获取输入框中的内容,当 setState 异步操作结束后才会去执行回调函数,那时候就能够实时的获取 dom 中的内容了。
但是如果你设置 dom 的内容,会导致对比时出现错误。如果在做一些极其复杂的业务时可以考虑使用,如操作动画时。
# React 中的生命周期函数
生命周期函数是指在某一个时刻组件会自动调用执行的函数
# Initialization 初始化
Initialization
:在组件初始化的时刻自动执行,相当于constructor
构造函数里面做一些props
和state
的设置。
# Mounting 挂载
componentWillMount
:在组件即将被挂载到页面的时刻自动执行
render
:在组件挂载到页面的时刻自动执行
componentDidMount
:在组件挂载到页面之后的时刻自动执行
# Updation 更新
# props 发生变化
componentWillReceiveProps
:当前子组件第二次及第二次之后接收了父组件传递属性的时刻都会被执行,也就是父组件执行 render 更新操作时,再次给子组件传递属性时会执行这个生命周期函数。
shouldComponentUpdate
:在组件确定需要更新之前自动执行,需要返回一个 bool 值,这个 bool 值用来确定是否需要更新当前组件,如果返回 false 就不会执行后面的更新操作了,也就是下面的函数都不会再执行了。
componentWillUpdate
:在组件确定需要更新之后,在组件真正更新之前会自动执行。
render
:组件真正更新的时刻自动执行,也就是重新渲染 DOM。
componentDidUpdate
:组件真正更新完成之后会自动执行。
# states 发生变化
shouldComponentUpdate
:在组件确定需要更新之前自动执行,需要返回一个 bool 值,这个 bool 值用来确定是否需要更新当前组件,如果返回 false 就不会执行后面的更新操作了,也就是下面的函数都不会再执行了。
componentWillUpdate
:在组件确定需要更新之后,在组件真正更新之前会自动执行。
render
:组件真正更新的时刻自动执行,也就是重新渲染 DOM。
componentDidUpdate
:组件真正更新完成之后会自动执行。
# Unmounting
componentWillUnmount
:在组件在页面上即将被去除的时刻会被执行,比如子组件是一个列表项,你删除这个列表项时就会自动执行这个函数了。
# 整个生命周期,每一个组件都可以有生命周期
初始化 constructor
首次挂载组件到页面之前 componentWillMount
首次挂载组件到页面时 render
首次挂载组件到页面之后 componentDidMount
第二次及第二次之后从父组件那里获取 props 时 componentWillReceiveProps
确定是否需要更新组件 shouldComponentUpdate
真正更新组件之前 componentWillUpdate
真正更新组件时 render
真正更新组件之后 componentDidUpdate
组件在页面中被卸载时 componentWillUnmount
# 生命周期函数的使用场景
组件继承的Component
中默认预置
了所有
的生命周期函数
,唯独没有预置render函数
。
render
函数必须有,因为它用来生成虚拟 DOM 的,没有虚拟 DOM 那就完蛋了。
shouldComponentUpdate
可以用来父组件render
时自动
调用子
组件的render前
的拒绝操作,也就是父组件更新时不更新子组件,子组件只会进行首次渲染。
主要是减少子组件 render 函数的不必要执行,虽然虚拟 dom 的对比比真实 dom 的对比要性能更优,但是减少虚拟 dom 的生成与对比就能够让性能更优。
在拒绝更新操作之前要进行判断,如判断传递过来的 props 是否与当前的 props 不同
shouldComponentUpdate (nextProps, nextState) {
// 如果传递过来的props与当前的props不一样,说明子组件需要更新了
if (nextProps.content !== this.props.content) {
return true;
} else {
// 说明子组件没有重新被渲染的必要
return false;
}
}
constructor
可以用来绑定当前方法的作用域,只需要绑定一次即可,减少
了多次绑定
方法的作用域
,这样就可以在事件绑定时不需要每次都.bind(this)
。
constructor (props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
componentDidMount
可以用来存放 ajax 请求,它永远都不会有问题,因为它只会执行一次。其实你也可以放到 constructor 中,因为它也是只执行一次的,但是它里面已经放了很多初始化 props 和 state 的操作。操作 ajax 可以使用 axios
import Axios from 'axios';
componentDidMount () {
Axios.get('/api/todolist').then(() => {alert('success');}).catch(() => {alert('bad')})
}
# 使用 Charles 进行接口数据模拟
使用教程:
下载 Charles
打开 Charles 后选择选项卡 Tools
选择 Map Local 选项
点击 Add 按钮 设置代理协议、主机、端口、路径
然后选择被代理的文件(json 文件) 点击 ok
之后选中 Enable Map Local 点击 OK 按钮
本次设置完成
它的作用是抓取请求,并且对请求做出处理。它相当于一个中间的代理服务器。
在前端开发的时候,没有这个接口的数据,你可以使用 json 自己模拟这个接口的数据。使用 Charles 来进行模拟。
josn 文件中的对象或者数组,键值对必须使用双引号,单引号会被认为整个对象或者数组是一个字符串。
# React 的 CSS 过渡动画
过渡
/*需要过渡的属性 持续时间 过渡效果类型 延迟时间*/
transition: all 1s ease-in 0s;
动画
/*先定义动画*/
@keyframes dong {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
@keyframes dong2 {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
/*使用动画: 动画名(必写) 持续时间(必写) 执行次数 动画方向 延迟时间 动画类型 结束时的状态*/
animation: dong 2s 1 normal 0s ease-in forwards;
# 使用 react-transition-group 实现单个元素的动画
先打开github
搜索这个开源项目
查看 里面对应的文档
使用命令安装:yarn add react-transition-group
或者npm install react-transition-group --save
使用 react-transition-group
中的CSSTransition
import {CSSTransition} from 'react-transition-group';
<CSSTransition
{/* 出入场动画设置,通过in的属性值来进行切换 true为入场 false 为出场*/}
in={this.state.show}
{/* 动画持续时间 */}
timeout={1000}
{/* 切换的css类名前缀 css文件中会有对应的类 如.fade-enter */}
classNames='fade'
{/* 出场动画执行完毕之后 组件就会从页面上被卸载 */}
unmountOnExit
onExited={(el)=>{
//直接进行DOM操作
//el.style.color="#f00";
console.log(el);
}}
>
<div>只是一个div</div>
</CSSTransition>
/* 第一次入场 */
.fade-appear {
opacity: 0;
}
/* 第一次入场中 */
.fade-appear-active {
opacity: 1;
transition: opacity 1s ease-in;
}
/*-----------------------------------*/
/* 入场动画执行前的时候 第一个时刻 */
.fade-enter {
opacity: 0;
}
/* 入场动画在执行的时候 第二个时刻 */
.fade-enter-active {
opacity: 1;
transition: opacity 1s ease-in;
}
/* 入场动画完全执行完毕后 第三个时刻*/
.fade-enter-done {
opacity: 1;
}
/*---------------------------*/
/* 出场动画执行前的时候 第四个时刻 */
.fade-exit {
opacity: 1;
}
/* 出场动画在执行的时候 第五个时刻*/
.fade-exit-active {
opacity: 0;
transition: opacity 1s ease-in;
}
/* 出场动画完全执行完毕的时候 第六个时刻 */
.fade-exit-done {
opacity: 0;
}
# 使用 react-transition-group 中的 CSSTransition 有许多特性
可以切换 css 中定义的类,前缀可以改
,后缀是固定
的。
前缀改了
之后,CSSTransition
中的classNames
也要改为新
的前缀
名。
.fade-appear
只在第一次入场时自动切换
.fade-appear-active
只在第一次入场中自动切换
.fade-enter
入场前
.fade-enter-active
入场中
.fade-enter-done
入场后
.fade-exit
出场前
.fade-exit-active
出场中
.fade-exit-done
出场后
还可以操作JS DOM
因为CSSTransition
里面内置
了许多的生命周期钩子
函数,css状态类
与下面的生命周期钩子函数对应
onEnter
入场前
onEntering
入场中
onEntered
入场后
onExit
出场前
onExiting
场中
onExited
出场后
/* 传进去的参数是 CSSTransition中最外层的DOM */
onExited = {(el) => {
//直接进行DOM操作
//el.style.color = "#f00";
console.log(el);
}}
unmountOnExit
:表示在CSSTransition
中的出场动画执行完毕后
就会从页面中被移除
掉。
# 使用 react-transition-group 实现多个元素间的动画
使用 react-transition-group
的TransitionGroup
配合CSSTransition
来实现多
个组件的动画
效果
import {CSSTransition,TransitionGroup} from 'react-transition-group'
{/* 在外面嵌套一层 TransitionGroup 标签*/}
<TransitionGroup>
{
this.state.list.map((item,index) => {
return (
{/* 每个元素都设置一下 动画,只不过in属性不要了,因为自动为true了*/}
<CSSTransition
timeout = {1000} // 持续时间
classNames = "fade" // 切换的类前缀
unmountOnExit // 出场时移除这个组件
appear = {true} // 首次入场 是否自动切换 入场动画
onEntered = {(el) => { // 出场后的钩子函数
//直接进行DOM操作
el.style.color = "red";
}}
key = {index}
>
<li>{item}</li>
</CSSTransition>
)
})
}
</TransitionGroup>
# 总结
react调试有专门的浏览器插件。react的props类型校验用的是prop-types。
虚拟dom和diff,这两个也是笔记常见的,我写过一篇实现vdom和diff的文章 https://juejin.cn/post/7084503737213354014 (opens new window)。
生命周期只是16.4的时候,现在的话,很多生命周期都变了,主要的componentDidMount、shouldComponentUpdate、componentDidUpdate、componentWillUnmount都没变化。react的vdom之前是树,循环加递归的去判断它是否发生变化。现在是fiber链表,循环加打打标记的来判断它是否发生变化。
fiber的diff算法是在render中执行的,为了保证应对fiber的链式渲染,所以render之前的那些方法 比如 componentWillMount、componentWillReceiveProps、componentWillUpdate,它们就不需要了。
因为它可能会在render之前改变state属性,会造成优先级不正确的问题,于是react觉得这些方法会存在风险,所以把它们直接干掉了。不过shouldComponentUpdate保留下来了,它不会改变state。
使用工具来进行接口mock,这个现在有插件可以直接实现,几乎是基于mockjs、webpack-dev-server、express,或者其它更好的工具,但原理大都一样,都是做个中间层代理。
css过渡 transition、动画 animation,单CSSTransition、多TransitionGroup元素过渡动画等等,都是基于CSS3做的封装,其实也并不难,当然深入会有难度,但是深入得靠自己实践研究了。之后有空的话,我把CSS3的笔记和案例也进行整理做下输出。