1.安装React开发调试工具(React Developer Tools):对React项目进行调试
Google -> 更多工具 -> 扩展程序 -> 左下角打开Chrome网上应用店 -> 搜索React -> React Developer Tools -> 添加至Chrome
React Developer Tools调试工具颜色:
-
灰色: 没有用React开发
红色: React项目,线下版本
黑色: React项目,线上版本(相对本地代码有压缩,精悍一点~)
React Developer Tools工具作用:
查看地点:控制栏 -> React菜单
作用一:查看React组件结构
作用二:查看右侧的Props、State等进行调试
PropTypes与DefaultProps
PropTypes:做属性接收的强校验,限制父组件传递给子组件属性的类型
引入PropTypes
import PropTypes from 'prop-types';
使用PropTypes进行校验
import React,{Component} from 'react'; import PropTypes from 'prop-types'; class TodoItem extends Component{ constructor(props) { super(props); this.handleClick = this.handleClick.bind(this);//为了性能考虑,改变函数的this指向通常在constructor中定义 } render(){ const { content,test } = this.props; //结构赋值 return( <div onClick={this.handleClick}> {test} - {content} </div> ) } handleClick(){ const { deleteItem,index } =this.props;//结构赋值 deleteItem(index); } } //对TodoItem组件对类型进行校验 TodoItem.propTypes = { content:PropTypes.oneOfType([PropTypes.string, PropTypes.number]), //表示content属性必须是String类型或Number类型中的一个 deleteItem:PropTypes.func,//表示deleteItem属性必须是function类型 index:PropTypes.number,//表示index属性必须是number类型 test:PropTypes.string.isRequired //isRequired表示test必须要从父组件传递过来,且类型是string类型,如果没传则报出警告 } //如果父组件没有向子组件传递对应的属性,则在这里定义属性的默认值 TodoItem.defaultProps = { test: 'helloworld' } export default TodoItem;
(1)使用
组件名.propTypes
表示对组件属性进行校验(2)使用
属性名:PropTypes.类型
表示属性的类型必须是设定的类型(3)使用
属性名:PropTypes.类型.isRequired
表示属性的类型必须是设定的类型,且必须由父组件传递过来(4)使用
属性名:PropTypes.oneOfType([PropTypes.string,PropTypes.number])
表示属性的类型必须是string或number中的一个(5)…等等验证方式请参见相关文档,如:
PropTypes.instanceOf(object)/PropTyps.oneOf(['News','Photos'])/PropTypes.arrayOf(PropTypes.number)...
如果传值类型与PropTypes不同,则会在控制台给出警告,但对开发并不影响,用来查看对应的相关错误信息。
对于个别属性父类没有传递但在子类却要使用,则使用
组件名.defaultProps={属性名:属性值}
的方式进行设定。【具体可参见上方代码示例】Props,State与render函数(解决数据发生变化,页面就要重新渲染的原理)
render函数在页面初始化的时候会先被执行一次,当state或者props发生变化时,render函数会重新执行,从而重新渲染页面,实现联动
- 当父组件的render函数被执行时,他的子组件的render函数都将被重新执行
2.虚拟DOM
版本一:基本DOM生成流程
- 定义state数据
- 定义JSX模板
- 数据+模板 结合,生成真实的DOM来显示
- 当state发生改变
- 数据+模板 结合,生成真实的DOM,替换原来的DOM
- 缺陷:
- 第一次生成了一个完整的DOM片段
- 第二次生成了一个完整的DOM片段
- 第二次的DOM替换第一次的DOM,非常耗性能
版本二:改良DOM生成流程
- 定义state数据
- 定义JSX模板
- 数据+模板 结合,生成真实的DOM来显示
- 当state发生改变
- 数据+模板 结合,生成真实的DOM,并不直接替换原始的DOM
- 新的DOM(js底层的DocumentFragment)和原始的DOM做比对,找差异
- 找出input框发生的变化
- 只用新的DOM中的input元素,替换掉老的DOM中的input元素
- 缺陷:
- 性能的提升并不明显
版本三:React的虚拟DOM
定义state数据
定义JSX模板
数据+模板 结合,生成虚拟DOM(虚拟DOM就是一个JS对象,用它来描述真实DOM)【js创建js对象损耗很少的性能;但如果js创建jsDOM则有很大的损耗】
Eg.
['div',{id:'abc'},['span',{},'hello world']]
用虚拟DOM的结构,生成真实的DOM来显示
Eg.
<div id="abc"><span>hello world</span></div>
state发生变化
数据+模板 生成新的虚拟DOM(极大的提升了性能)
Eg.
['div',{id:'abc'},['span',{},'bye bye']]
比较原始虚拟DOM和新的虚拟DOM的区别,找到区别是span中的内容(比较虚拟DOM即比较js对象,极大的提升了性能)
直接操作DOM改变span中的内容
优点:
- 减少了对真实DOM的创建与真实DOM的对比
- 利用js对象比较代替DOM比较
3.深入虚拟DOM底层原理
- React生成DOM流程
- JSX => React.createElement => 虚拟DOM(JS对象) => 真实的DOM
return <div>item</div>
等同于return React.createElement('div',{},'item')
。React.createElement是更偏向于底层的接口,提供了一个类似于js对象的内容传递给createElement方法,将该对象变成虚拟DOM,再被转换成真实的DOM。
- 虚拟DOM带来的好处:
- 性能提升了(将DOM比对变成JS对象的比对)
- 使得跨端应用得以实现(React Native)
- 虚拟DOM是JS对象在原生应用和网页应用里都可以被识别
- 利用虚拟DOM转化成原生组件
4. 虚拟DOM中的Diff算法(Difference,原始虚拟DOM与新的虚拟DOM的差异比对的算法)
setState
异步方法,提升React性能
设计成一步函数的初衷:假设连续调用三次setState变更三组数据,时间间隔非常小,React将三次SetState合并成一个SetState,只做成一次虚拟DOM的比对,更新一次DOM,省去性能耗费。
Diff算法
Diff算法 是虚拟DOM进行比对时用到的算法,采用同级比对的概念。
- 比较流程:先比第一层、再比第二层…
- 如果在一层不一致,下面不会继续比,将原始页面的虚拟DOM下面所有虚拟DOM替换
- 优点:
- 同级比较,算法简单从而带来比对的速度快
key值
引入key值的原因:提高虚拟DOM比对的性能
例子:
假设有一个数组,数组上面包含五个数据,在页面第一次渲染时将5个数据映射成五个虚拟DOM节点,即虚拟DOM树。接着向数组中添加一个数据,生成新的虚拟DOM树,将虚拟DOM进行比对,没有key值没法确认节点之间的关系,要想进行比对需要进行两层循环。
如果添加key值,虚拟DOM的比对则根据key值做关联,一致则可以复用。
前提:key值在前一个虚拟DOM上和后一个虚拟DOM上相同。故,在循环中key值不要是index,没法保证在原始虚拟DOM书上和新的虚拟DOM树上的key值一致。
如在todolist中,若以index为key, 添加a、b、c三个元素,key值分别是0、1、2;当删除a后,b和c的key值分别是0、1,则无法建立关系,key值失去存在意义。
若以item为key则a->a,b->b,c->c;删除a后,b->b,c->c,可以建立关系。
key值要保持稳定,在项目开发时能不使用index作为key值就不使用index作为key值
与Diff算法的关系:同层比对和key值比对都是Diff算法中的一部分…
5.React中ref的使用
作用:帮助在React中直接获取DOM元素,尽量不要使用ref
应用实例:
//TodoList.js中的部分代码 render(){ //JSX模板语法 => js对象 => 真实的DOM return( <Fragment> <div> <label htmlFor="insertArea">输入内容</label> <input id="insertArea" className="input" value={this.state.inputValue} onChange={this.handleInputChange} ref={(input)=>{this.testinput=input}} /> <button onClick={this.handleBtnClick}>提交</button> </div> <ul> {this.getTodoItem()} </ul> </Fragment> ) } handleInputChange(){ console.log(this.testinput); //e.target表示input框对应的DOM节点 this.setState((preState)=>({ //preState表示修改前的state inputValue : this.testinput.value })); }
- 取DOM元素的方法
- 利用函数调用后的参数e,e.target获取对应备操作的DOM元素
- 利用标签的ref属性,ref的属性值为函数,函数中的参数input为当前元素的DOM,将该DOM元素作为this.testinput的值,函数调用时,输出this.testinput即会找到之前赋值时的DOM元素
- 取DOM元素的方法
与setState合用时的坑
坑的出现:
- 实例代码
import React ,{ Component , Fragment}from 'react'; import TodoItem from './TodoItem'; import './style.css'; class TodoList extends Component{ constructor(props) { super(props); //调用Component的构造函数 this.handleInputChange = this.handleInputChange.bind(this); this.handleBtnClick = this.handleBtnClick.bind(this); this.handleItemDelete = this.handleItemDelete.bind(this); this.state = { inputValue:'', //input中的内容 list:[] //列表中的每一项 }; } render(){ //JSX模板语法 => js对象 => 真实的DOM return( <Fragment> <div> <label htmlFor="insertArea">输入内容</label> <input id="insertArea" className="input" value={this.state.inputValue} onChange={this.handleInputChange} ref={(input)=>{this.testinput=input}} /> <button onClick={this.handleBtnClick}>提交</button> </div> <ul ref={(ul)=>{this.ul=ul}}> {this.getTodoItem()} </ul> </Fragment> ) } getTodoItem(){ return( this.state.list.map((item,index)=> { return (//循环组件应包含key值,key值应该放在return的最外层元素上 <TodoItem key={index} content={item} index={index} deleteItem={this.handleItemDelete}/> ) }) ) } handleInputChange(){ console.log(this.testinput); //e.target表示input框对应的DOM节点 this.setState((preState)=>({ //preState表示修改前的state inputValue : this.testinput.value })); } handleBtnClick(){ this.setState((preState)=>({//preState表示修改前的state,也就是this.state list:[...preState.list,preState.inputValue],//...this.state.list es6的展开运算符 inputValue:'' })) console.log(this.ul.querySelectorAll('div').length); } handleItemDelete(index){ //console.log(index); //array.splice(index,howmany,item1,.....,itemX) //index下标,howmany表示删除多少个,item1...itemX表示要添加的元素 this.setState((preState)=>{ const list = [...preState.list];//拷贝一份list数组 list.splice(index,1); return {list} }) } //注意:箭头函数this.setState(()=>({...}))表示返回...是一个对象 // this.setState(()=>{...})表示返回...是一个函数,需要些return } export default TodoList;
上述代码中,利用ul的ref属性将this.ul设置成ul的DOM元素,并在handleBtnClick()中打印该DOM下的所有的div元素的个数。但输入内容并提交后,页面渲染出一个新的DOM元素,但控制台打印的却是0;再添加后,ul下共有两个div,但控制台打印的却是1。如下所示:
解决坑:
问题原因:setState是异步函数,即使console.log()在声明时在setState下面,但由于setState的异步,导致先执行console.log再执行setState
爬坑:setState的第二个参数为一个回调函数,将想在setState执行后进行的操作,以箭头函数的形式放到第二个参数中即可。只需对handleBtnClick函数做以下修改:
handleBtnClick(){ this.setState((preState)=>({//preState表示修改前的state,也就是this.state list:[...preState.list,preState.inputValue],//...this.state.list es6的展开运算符 inputValue:'' }),()=>{ console.log(this.ul.querySelectorAll('div').length); }) }
6.React生命周期函数[针对于组件而言的,每一个组件都有生命周期函数,生命周期函数又称为生命周期钩子]
什么是生命周期函数?
生命周期函数指在某一个时刻组件会自动调用执行的函数
生命周期过程
- Initialization初始化过程
- 执行constructor()函数,初始化数据,如props、state【constructor()不算做React生命周期函数,而是ES6的规定初始化函数】
- Mounting挂载(挂载指的是组件第一次被放到页面上的时候)
- componentWillMount 在组件即将被挂载到页面的时刻执行(只在挂载的时候执行,即第一次放到页面上的时候执行)
- render 在页面被挂载的时刻执行
- componentDidMount 在组件被挂载到页面之后自动执行(只在挂载的时候执行,即第一次放到页面上的时候执行)
- Updation组件更新(发生在props或states发生变化的时刻)
- props
- componentWillReceiveProps在组件要从父组件接受参数,且只要父组件的render函数被重新执行了,子组件的这个生命周期函数会被执行(即这个组件第一次存在于父组件中,不会执行;如果这个组件之前已经存在于父组件中,才会执行)
- shouldComponentUpdate在组件被更新之前被执行,返回boolean类型的返回结果
- return true表示需要更改组件,继续调用componentWillUpdate方法
- return false表示不需要更改组件,不继续调用其他生命周期方法
- componentWillUpdate在组件更新之前被自动执行,在shouldComponentUpdate之后执行,如果shouldComponentUpdate返回true才执行,如果返回false此函数则不会被执行
- render执行页面渲染
- componentDidUpdate在组件更新完成之后被自动执行
- states
- shouldComponentUpdate在组件被更新之前被执行,返回boolean类型的返回结果
- return true表示需要更改组件,继续调用componentWillUpdate方法
- return false表示不需要更改组件,不继续调用其他生命周期方法
- componentWillUpdate在组件更新之前被自动执行,在shouldComponentUpdate之后执行,如果shouldComponentUpdate返回true才执行,如果返回false此函数则不会被执行
- render执行页面渲染
- componentDidUpdate在组件更新完成之后被自动执行
- shouldComponentUpdate在组件被更新之前被执行,返回boolean类型的返回结果
- props
- Unmounting取消挂载
- componentWillUnmount当这个组件即将被从页面中剔除的时候会被执行
- Initialization初始化过程
7.生命周期函数的使用场景
除render()函数,其他的生命周期函数都可以重写
- 原因:继承Component,Component默认内置了其他所有的生命周期函数,除render函数
当父组件的render函数被执行,子组件的render函数也会被执行
上述操作消耗组件性能,故可以对子组件的shouldComponentUpdate()函数进行如下修改
shouldComponentUpdate(nextProps,nextState){ //nextProps和nextState表示当组件被更新时,nextProps表示props会变成什么样,nextState表示state会变成什么样 //利用组件内数据是否变化的比对,从而决定是否执行updation相关函数,避免多余的渲染 if(nextProps.content !== this.props.content){ return true; }else{ return false; } //作用:避免一个组件做无谓的render()操作 }
ajax请求放到componentDidMount()中,保证请求只会被执行一次
使用ajax请求
借助axios扩展工具发送ajax请求:yarn add axios
应用实例
//引入axios import axios from 'axios'; //使用axios componentDidMount(){ axios.get('/api/todolist') .then(()=>{alert("succ")}) //发送成功的回调 .catch(()=>{alert("error")}) //发送失败的回调 }
使用Charles实现本地数据的模拟mock
安装Charles:https://www.charlesproxy.com/latest-release/download.do直接下载安装即可
Charles是抓包工具(同Fiddler),实现本地mock的原理是:拦截指定请求,返回模拟数据。
实现本地数据的模拟:
本地创建json文件
eg.json文件中的内容 ["Jack","Jerry","Sam"]
开启Charles -> Tools -> Map Local -> 选择Encable Map Local -> Add 添加路径并将Local path选择本地json文件 -> OK
即完成本地模拟数据的操作
显示请求内容
componentDidMount(){ axios.get('/api/todolist') .then((res)=>{ this.changeList(res.data); }) .catch(()=>{alert("error")}) } changeList(data){ this.setState((preState)=>({ list:[...preState.list,...data] })) } //由于请求的地址有响应数据,且响应数据中的data对象为要返回的数据信息,则将data中的数据添加到list中从而更新页面内容
总结:整个请求流程是通过axios发送ajax请求访问指定url,使用charles工具拦截url并返回json数据,通过then方法获取回调成功的数据并显示。
8.React的CSS过渡动画
应用实例:
//App.js import React, { Component , Fragment} from 'react'; import './App.css'; class App extends Component { constructor(props) { super(props); this.handleToggle = this.handleToggle.bind(this); this.state = { show:true }; } render() { return ( <Fragment> <div className={this.state.show?'show':'hide'}>hello</div> <button onClick={this.handleToggle}>toggle</button> </Fragment> ); } handleToggle(){ this.setState((PreState)=>({ show:!PreState.show })) } } export default App; //App.css .show{ opacity: 1; transition: all 1s ease-in; } .hide{ opacity: 0; transition: all 1s ease-in; }
分析:对button设定监听事件,当点击时切换state中show的值为相反,从而切换div组件的class样式值,通过设置css中opacity:0为透明(即隐藏),并结合transition属性实现过渡样式(all 1s ease-in的含义是:所有样式的切换都用1s做过度)
9.React中CSS的动画效果
CSS3的动画效果是指通过@keyframes定义css动画
//App.js import React, { Component , Fragment} from 'react'; import './App.css'; class App extends Component { constructor(props) { super(props); this.handleToggle = this.handleToggle.bind(this); this.state = { show:true }; } render() { return ( <Fragment> <div className={this.state.show?'show':'hide'}>hello</div> <button onClick={this.handleToggle}>toggle</button> </Fragment> ); } handleToggle(){ this.setState((PreState)=>({ show:!PreState.show })) } } export default App; //App.css .show{ animation: show-item 2s ease-in forwards; } .hide{ animation:hide-item 2s ease-in forwards; } @keyframes hide-item{ 0% { opacity: 1; color: red; } 50%{ opacity: 0.5; color: green; } 100%{ opacity: 0; color: blue; } } @keyframes show-item{ 0% { opacity: 0; color: red; } 50%{ opacity: 0.5; color: green; } 100%{ opacity: 1; color: blue; } }
分析:CSS3的动画效果是指通过@keyframes定义css动画(定义简单动画),利用animation属性引用,使用hide-item的动画效果,动画时长2s,动画曲线ease-in,设置最终效果forward即停留在最后一帧的样式(如果不设置则显示至0帧),从而实现使用对应自定义的动画效果。
10.使用react-transition-group实现动画(react第三方动画模块)
安装react-transition-group模块
#npm npm install react-transition-group --save #yarn yarn add react-transition-group
react-transition-group中的CSSTransition是动画组件(实现复杂的动画效果),自动进行class的增加和移除工作
//App.js import React, { Component , Fragment} from 'react'; import {CSSTransition} from 'react-transition-group'; import './App.css'; class App extends Component { constructor(props) { super(props); this.handleToggle = this.handleToggle.bind(this); this.state = { show:true }; } render() { return ( <Fragment> <CSSTransition in={this.state.show} timeout={300} classNames="fade" unmountOnExit onEntered={(el)=>{el.style.color='blue'}} appear={true} > <div>hello</div> </CSSTransition> <button onClick={this.handleToggle}>toggle</button> </Fragment> ); } handleToggle(){ this.setState((PreState)=>({ show:!PreState.show })) } } export default App; //App.css .fade-enter{ opacity:0; } .fade-enter-active{ opacity: 1; transition: opacity 1s ease-in; } .fade-enter-done{ opacity: 1; color: red; } .fade-exit{ opacity:1; } .fade-exit-active{ opacity: 1; transition: opacity 1s ease-in; } .fade-exit-done{ opacity: 0; } .fade-appear{ opacity: 0; color: yellow; } .fade-appear-active{ opacity: 1; color: gray; transition: opacity 2s ease-in; }
- in:当前状态,用于确定是出场动画(in的值从false变成true)还是入场动画(in的值从true变成false)
- timeout:动画持续时间(单位:毫秒)
- 入场动画css
- fade-enter:in的值从false变成ture时第一个时刻执行,CSSTransition会向其中元素添加fade-enter的css样式
- fade-enter-active:入场动画执行的第二个时刻到入场动画执行完成期间,被CSSTransition包裹的元素使用的是fade-enter-active的css样式
- fade-enter-down:整个入场动画执行完成之后,被CSSTransition包裹的元素将使用fade-enter-down的css样式
- 出场动画css
- fade-exit:出场动画执行的第一个时刻
- fade-exit-active:出场动画执行的第二个时刻到出场动画执行完成
- fade-exit-down:出场动画执行完成之后
- className值是指定css中”-“前面的内容【即css的样式为fade-enter】,故className属性值应该是fade,从而让组件找到对应的css样式
- unmountOnExit:执行完出场动画后,移除内部DOM元素
- 钩子属性(即在某一时刻会执行的函数)利用js去执行相关操作
- onEnter(入场动画执行的第一帧的时候触发),函数第一个参数为内部元素
- onEntering(入场动画执行的第二帧到动画结束的期间触发),函数第一个参数为内部元素
- onEntered(入场动画结束之后执行的函数),函数第一个参数为内部元素
- onExit(出场动画执行的第一帧),函数第一个参数为内部元素
- onExiting(出场动画执行的第二帧到动画结束的期间触发),函数第一个参数为内部元素
- onExited(出场动画结束之后执行的函数),函数第一个参数为内部元素
- 等…
- appear:值为true表示内部元素第一次渲染到页面时也要动画效果;默认false
- 使用fade-appear样式:第一次渲染时出场动画执行的第一时刻
- 使用fade-appear-active样式:第一次渲染时出场动画执行的第二时刻到出场动画执行完成
- 更多:Transition组件是CSSTransition更底层的组件,如果有CSSTransition无法实现的功能,则可以考虑使用Transition组件进行动画效果的实现。
11.使用react-transition-group实现多个元素的动画效果(使用TransitionGroup搭配CSSTransition使用)
- 使用react-transition-group中的TransitionGroup组件,包裹要进行动画效果的一组组件
- TransitionGroup组件中的多个组件外应使用CSSTransition包裹要添加动画的元素,此时in的属性可以去掉,因为并不是做切换入场出场而是添加元素的操作
应用实例:【实现点击按钮添加元素动画】
//App.js import React, { Component , Fragment} from 'react'; import {CSSTransition,TransitionGroup} from 'react-transition-group'; import './App.css'; class App extends Component { constructor(props) { super(props); this.handleAddItem = this.handleAddItem.bind(this); this.state = { list:[] }; } render() { return ( <Fragment> <TransitionGroup> { this.state.list.map((item,index)=>{ return( <CSSTransition key={index} timeout={300} classNames="fade" unmountOnExit onEntered={(el)=>{el.style.color='blue'}} appear={true} > <div>item</div> </CSSTransition> ) }) } </TransitionGroup> <button onClick={this.handleAddItem}>toggle</button> </Fragment> ); } handleAddItem(){ this.setState((PreState)=>({ list:[...PreState.list,'item'] })) } } export default App; //App.css同上方代码