typora-copy-images-to: assects
一、React简介 React全家桶
React-Router(路由)、PubSub(消息管理)、Redux(集中式状态管理) Ant-Design(UI组件库)
React定义
React是用户构建用户界面的JavaScript库,react只关注页面(视图) 发送请求获取数据(axios、fetch、ajax) 处理数据(过滤、整理格式) 操作DOM呈现页面(react工作)
React是一个将数据渲染为HTML视图的开源JS库
React谁开发的
Facebook开发,且开源,Facebook工程师Jordan Walke创建,2011年部署于newsfeed
2012年部署于ins,2013年开源
React被阿里、腾讯等一线大厂使用
为什么要学习React?
原生js操作DOM效率低且繁琐 document.getElementById(‘btn1’)
DOM API操作UI(样式)
使用js直接操作DOM,浏览器会进行大量的重绘重排
原生js没有组件化(模块化)编码方案,代码复用率低
React的特点
采用组件化模式、声明式编码,提高开发效率及组件复用率
React-Native中可以使用React语法进行移动端开发
使用虚拟DOM+优秀的DOM Diffing算法,尽量减少了与真实DOM的交互
React学习之前要掌握的JS基础知识
判断this的指向
class类
ES6语法规范
npm包管理器
原型、原型链
数组常用方法
模块化
二、hello-react案例 官网
React基本使用
相关js库 react.js:React核心库 react-dom.js:提供操作DOM的react扩展库 babel.min.js:解析JSX语法代码转换为js代码的库 引用顺序react>react-dom
三、虚拟Dom的两种创建方式 jsx创键虚拟Dom js创建虚拟Dom(不用) 四、虚拟DOM与真实DOM 虚拟dom
本质上是object类型的对象
虚拟dom比较轻,属性少,真实dom重
虚拟dom是react内部用,无需真实dom上那么多属性
虚拟dom最终会被react转换为真实dom呈现在页面上
五、jsx语法规则 jsx
全称是javascript xml
react定义的一种类似于xml的js拓展语法
本质上是React.createElement(componment,props,…children)方法的语法糖
作用:用来简化虚拟DOM的创建
1 2 3 写法 const (var ) ele = <h1 > 111</h1 > 注意1 :他不是字符串,也不是HTML/XML标签 注意2 :它最终产生的就是一个js对象
语法规则
六、jsx小练习 在React JSX语法中,有时需要对数组或对象等进行遍历,数组的遍历有2种方式:forEach和map; 但是需要知晓,JSX语法最终是要return返回html标签或对应内容的,而数组的遍历方法中forEach没有返回值,重在执行;map可以设置返回值。所以,在jsx中只能使用map方法进行遍历,需要知晓!
1 2 3 4 5 <div> {dataArr.map((item,index )=> { return <h1 key ={index} > {item}</h1 > })} </div>
需要设置key属性,这是虚拟dom实现diffing算法的重要标志
七、组件与模块 模块(js按照功能分为不同模块)
理解:向外提供特定功能的js程序,一般就是一个js文件
为什么要拆成模块:随着业务逻辑增加,代码越来越多和复杂
作用:复用js,简化js编写,提高js运行效率
组件(拼乐高)
理解:用来实现局部功能效果的代码和资源的集合(html/css/js/img)等
why: 一个界面复杂且繁琐
作用:复写编码,简化项目编码,提高运行效率
模块化
当应用的js都以模块来编写,这个应用就是一个模块化应用
组件化
当应用是以多组件的方式实现,这个应用就是一个组件化的应用
八、开发者工具的安装
安装完React-Developer-Tools插件以后chrome调试页面会多出componment和profiler选项卡,componments用来查看当前页面有多少组件,profiler用来查看页面性能
九、函数式组件:适用于简单组件定义 代码 1 2 3 4 5 6 7 function Demo ( ) { console .log(this ) return <h2 > 这是一个函数式组件</h2 > } ReactDOM.render(<Demo /> ,document .getElementById('test' ))
过程
执行了ReactDOM.render之后发生了什么? 1、react解析组件标签,找到了Demo组件 2、发现组件是函数定义的,调用该函数,将返回的虚拟DOM转为真实DOM,随后呈现在页面中
十、类的基本指示复习 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 class Person { constructor (name,age ) { this .name = name; this .age = age; } speak ( ) { console .log(`我叫${this .name} ,我的年龄是${this .age} ` ) } } class Student extends Person { constructor (name,age,grade ) { super (name,age); this .grade = grade; } speak ( ) { console .log(`我叫${this .name} ,我的年龄是${this .age} ,我读的是${grade} 年纪` ) } study ( ) { console .log('我很努力学习' ); } } const p1 = new Person('tom' ,18 );pi.speak(); pi.speak.call({})
总结
类中的构造器不是必须要写,要对实例进行一些初始化操作,如添加指定属性时才写
如果A类继承了B类,且A类写了构造器,那么构造器中super()必须要调用
类中所定义的非static方法,都是放在了类的原型对象上,供实例去使用
十一、类式组件 1 2 3 4 5 6 7 8 9 10 11 class MyComponent extends React .Component { render ( ) { return <h2 > 这是一个函数式组件</h2 > } } ReactDOM.rend(<MyComponent /> ,document .getElementById('test' ))
过程
执行了ReactDOM.render之后发生了什么? 1、react解析组件标签,找到了Demo组件 2、发现组件是类定义的,随后new出来该类的实例 3、并且通过该实例调用到原型上的render方法 4、将render返回的虚拟DOM转为真实DOM,随后呈现在页面中
十二、组件实例的三大属性之一:state 理解
state是组件最重要的属性,值是对象(可以包含多个key value的组合)
组件被称为状态机,通过更新组件的state来更新对应的页面显示
注意
组件中render()方法中的this指向实例化对象
组件自定义方法中this指向为undefined,如何解决? 强制绑定this:通过函数对象bind() 箭头函数
状态数据:不能直接修改或更新
实例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 <script type="text/babel" > class Weather extends React .Component { constructor (props ) { super (props); this .state = {isHot :true }; this .changeWeather = this .changeWeather.bind(this ) } render ( ) { console .log(this ); if (this .state.isHot){ return <h1 onClick ={this.changeWeather} > 热</h1 > }else { return <h1 onClick ={() => {this.changeWeather()}}>不热</h1 > } } changeWeather ( ) { if (this .state.isHot){ this .setState({ isHot:false }) }else { this .setState({ isHot:true }) } } } ReactDOM.render(<Weather /> ,document .getElementById('test' )) </script>
简写 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <script type="text/babel" > class Weather extends React .Component { state = {isHot :false } render ( ) { return <h1 onClick ={this.changeWeather} > {this.state.isHot?'热':'不热'}</h1 > } changeWeather = ()=> { const isHot = this .state.isHot; this .setState({isHot :!isHot}) } } ReactDOM.render(<Weather /> ,document .getElementById('test' )) </script>
十三、react三大组件之二:props 简单例子 1 2 3 4 5 6 7 8 9 10 11 <script type="text/babel" > class Weather extends React .Component { render ( ) { return <h1 > {this.props.weather}</h1 > } } ReactDOM.render(<Weather weather ='晴天' /> ,document .getElementById('test' )) ReactDOM.render(<Weather weather ='阴天' /> ,document .getElementById('test2' )) </script>
扩展运算符回顾 1 <script>let arr1 = [1 ,3 ,5 ,7 ,9 ];let arr2 = [2 ,4 ,6 ,8 ,10 ];console .log(...arr1);
批量传递props 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class Person extends React .Component { render ( ) { console .log(this ); const {name,age,sex} = this .props; return ( <ul> <li>姓名:{name}</li> <li>年龄:{age}</li> <li>性别:{sex}</li> </ul> ) } } ReactDOM.render(<Person name ="kobe" age ={18} sex ="男" /> ,document .getElementById("test1" )); const data = { name : "curry" , age : 20 , sex : "男" } ReactDOM.render(<Person {...data }/> ,document .getElementById("test2" ))
props属性限制 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 class Person extends React .Component { render ( ) { console .log(this ); const {name,age,sex} = this .props; return ( <ul> <li>姓名:{name}</li> <li>年龄:{age}</li> <li>性别:{sex}</li> </ul> ) } } Person.propTypes = { name:PropTypes.string.isRequired, age:PropTypes.number, sex:PropTypes.string, speak:PropTypes.func } Person.defaultProps={ sex:'不男不女' , age:'年龄不详' , name:'名字不详' } function speak ( ) {}
props简写
1 class Person extends React .Component {
类中constructor构造器详解 1 class Person extends React .Component { constructor (props ) {
函数式组件中props属性
函数式组件因为没有this,不能使用state和refs
但是函数式组件是一个方法,可以接受参数,因此可以使用Props
1 function Person (props ) { const {name,age,sex} = props; return ( <ul > <li > 姓名:{name}</li > <li > 年龄:{age}</li > <li > 性别:{sex}</li > </ul > )}
十四、react三大之间之三:refs 例子
自定义组件,功能说明如下 1、点击按钮,提示输入第一个输入框中的值,如果第一个输入框有值,则alert 2、第二个输入框失去焦点时,如果输入框中有值。alert这个输入框中的值
组件内的标签可以定义ref属性来标识自己,类似原生js的id
字符串形式ref 过时API
字符串类型ref效率不高
字符串形式ref已经不被官方推荐使用,官方也有可能在之后版本去除字符串形式ref使用
回调函数形式ref
<input ref={(currentNode)=>{this.input1=currentNode}}/>
这样就可以通过this.input1获取input节点
回调函数形式ref中调用次数问题
如果 ref 回调函数是以内联函数的方式定义的,在更新过程中它会被执行两次,第一次传入参数 null,然后第二次会传入参数 DOM 元素。这是因为在每次渲染时会创建一个新的函数实例,所以 React 清空旧的 ref 并且设置新的。通过将 ref 的回调函数定义成 class 的绑定函数的方式可以避免上述问题,但是大多数情况下它是无关紧要的。
createRef的使用
React.createRef调用后可以返回一个容器,该容器可以存储被ref所表示的节点真实DOM,该容器是”专人专用的”
jsx注释格式: {/**/}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 class Demo extends React .Component { myRef1 = React.createRef(); myRef2 = React.createRef(); render ( ) { return ( <div> <input ref={this .myRef1} placeholder="点击右边按钮弹出值" type="text" /> <button onClick={this .showText1}>点击显示左边输入框的值</button> <input ref={this .myRef2} onBlur={this .showText2} placeholder="失去焦点弹出值" type="text" /> </div> ) } showText1 = ()=> { const value = this .myRef1.current.value; if (value){ alert(value); } } showText2 = ()=> { const value = this .myRef2.current.value; if (value){ alert(value); } } }
十五、ref总结
ref的四种进阶使用方式,推荐程度递增 1、字符串ref 不推荐 2、内联函数ref 3、class绑定函数ref 4、React.createRef方法创建ref (目前最为推荐的一种方式)
十六、react中的事件处理
通过onXXX属性指定事件处理函数(注意大小写) a. react使用的是自定义合成事件,而不是原生DOM事件 ——————为了更好的兼容性 b. react中事件是通过事件委托方式处理的(委托给最外层的元素) ——————为了更高效
如果发生事件的元素刚好是你要操作的元素(例如下例input2),那么就可以不同ref,直接使用e.target可以获取到DOM。不要过度使用ref
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Demo extends React .Component { render ( ) { return ( <div> <input type="text" placeholder="失去焦点弹出值" onBlur={this .changeText2} /> </div> ) } changeText2 = (e )=> { alert(e.target.value); } } ReactDOM.render(<Demo /> ,document .getElementById("test1" ))
十七、非受控组件
要编写一个非受控组件,而不是为每个状态更新都编写数据处理函数,你可以 使用 ref 来从 DOM 节点(输入框、下拉菜单…)中获取表单数据。
在大多数情况下,我们推荐使用 受控组件 来处理表单数据,因为非受控组件要使用ref,不推荐使用过多ref
非受控组件,即用即取,表单的输入值不存储在state中
组件的状态不受React控制的组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class Login extends React .Component { render ( ) { return ( <form action="http://www.atguigu.com/" onSubmit={this .handleSubmit}> 用户名:<input type ="text" ref ={(c) => {this.username = c}}/><br /> 密 码:<input type ="text" ref ={(c) => {this.password = c}}/><br /> <button>提交</button> </form> ) } handleSubmit = (e ) => { e.preventDefault(); alert(`用户名:${this .username.value} ,密码:${this .password.value} ` ); } changeText2 = (e )=> { alert(e.target.value); } } ReactDOM.render(<Login /> ,document .getElementById("test1" ))
十八、受控组件
受控组件就是组件的状态受React控制。通过onChange方法实现和表单中元素的输入值和React中的状态实时联动更新
vue默认就实现了双向绑定,react没有实现,需要结合onChange和setState实现绑定
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 class Login extends React .Component { state = { username:"" , password:"" } handleSubmit = ()=> { alert(`用户名:${this .state.username} ,密码:${this .state.password} ` ) } usernameChange = (e ) => { this .setState({ username:e.traget.value }) } passwordChange = (e ) => { this .setState({ password:e.traget.value }) } render ( ) { return ( <form action="http://www.atguigu.com" onSubmit={this .handleSubmit}> 用户名:<input type ="text" onChange ={this.usernameChange} /> <br /> 密 码:<input type ="text" onChange ={this.passwordChange} /> <br /> <button>提交</button> </form> ) } } ReactDOM.render(<Login /> ,document .getElementById('test1' ));
十九、高阶函数–函数柯里化 对象属性名是变量时如何处理:用方括号表示 1 2 3 4 5 6 let a = "name" ;let b = { [a]:"111" } console .log(b);
高阶函数定义
如果一个函数符合下面2个规范中的任何一个,那该函数就是高阶函数 1、若A函数,接受的参数是一个函数,那么A就可以称之为高阶函数 2、若A函数,调用的返回值依然是一个函数,那么A就可以称之为高阶函数
函数的柯里化:通过函数调用继续返回函数的方式,实现多次接受参数最后统一处理的函数编码形式
高阶函数实例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 class Login extends React .Component { state = { username:"" , password:"" } handleSubmit = ()=> { alert(`用户名:${this .state.username} ,密码:${this .state.password} ` ) } formdataChange (datatype ) { return (event ) => { this .setState({ [datatype]:event.target.value }) } } render ( ) { return ( <form action="http://www.atguigu.com" onSubmit={this .handleSubmit}> 用户名:<input type ="text" onChange ={this.formdataChange( "username ")} /> <br /> 密 码:<input type ="text" onChange ={this.formdataChange( "password ")} /> <br /> <button>提交</button> </form> ) } } ReactDOM.render(<Login /> ,document .getElementById('test1' ));
不用高阶函数实现上面那个效果 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 class Login extends React .Component { state = { username:"" , password:"" } handleSubmit = ()=> { alert(`用户名:${this .state.username} ,密码:${this .state.password} ` ) } formdataChange (datatype,event ) { this .setState({ [datatype]:event.target.value }) } render ( ) { return ( <form action="http://www.atguigu.com" onSubmit={this .handleSubmit}> 用户名:<input type ="text" onChange ={event => this.formdataChange("username",event)} /><br /> 密 码:<input type ="text" onChange ={event => this.formdataChange("password",event)} /><br /> <button>提交</button> </form> ) } } ReactDOM.render(<Login /> ,document .getElementById('test1' ));
二十、组件生命周期 例子
定义组件实现以下功能 1、让指定的文本做显示/隐藏的渐变动画 2、从完全可见,到彻底消失,耗时2s 3、点击不活了按钮从界面中卸载组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 class Life extends React .Component { state = {opacity :1 } componentDidMount ( ) { console .log(this ); this .timer = setInterval (() => { let {opacity} = this .state; opacity -= 0.1 ; if (Math .abs(opacity) < Number .EPSILON){ opacity = 1 ; } this .setState({opacity}) }, 200 ); } componentWillUnmount ( ) { clearInterval (this .timer) } goDead = () => { ReactDOM.unmountComponentAtNode(document .getElementById("test1" )); } render ( ) { return ( <div> <span style={{opacity :this .state.opacity}}>显示的文本</span><br/ > <button onClick={this .goDead}>不活了</button> </div> ) } } ReactDOM.render(<Life /> ,document .getElementById('test1' ));
生命周期(旧)——组件挂载流程
组件从创建到死亡会经历一些特定的阶段
React组件中包含一系列钩子函数(生命周期回调函数),会在特定时刻调用
我们定义组件时,会在特定得阶段时执行生命周期回调函数,做特定的工作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 class Count extends React .Component { constructor (props ) { console .log("Count--constructor" ); super (props); this .state = {count :0 }; } componentWillMount ( ) { console .log("Count--componentWillMount" ); } componentDidMount ( ) { console .log("Count--componentDidMount" ); } componentWillUnmount ( ) { console .log("Count--componentWillUnmount" ) } shouldComponentUpdate ( ) { console .log("Count--shouldComponentUpdate" ); return true ; } componentWillUpdate ( ) { console .log("Count--componentWillUpdate" ) } componentDidUpdate ( ) { console .log("Count--componentDidUpdate" ) } add = () => { let {count} = this .state; this .setState({ count:count+1 }) } force = ()=> { this .forceUpdate(); } goDead = ()=> { ReactDOM.unmountComponentAtNode(document .getElementById('test1' )) } render ( ) { console .log("Count--render" ) return ( <div> <h2>当前求和为{this .state.count}</h2> <button onClick={this .add}>点我+1 </button> <button onClick={this .goDead}>点我卸载组件</button> <button onClick={this .force}>不更改任何状态中的数据,强制更新以下</button> </div> ) } } ReactDOM.render(<Count /> ,document .getElementById("test1" ))
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 class A extends React .Component { state = {carname :"奔驰" } changeCar = () => { this .setState({ carname:this .state.carname==="奔驰" ?"宝马" :"奔驰" }) } render ( ) { return ( <div> <h1>A标签内容</h1> <button onChange={this .changeCar}>换车</button> <B carname={this .state.carname}/> </div> ) } } class B extends React .Component { componentWillReceiveProps ( ) { console .log("componentWillReceiveProps" ); } shouldComponentUpdate ( ) { console .log("shouldComponentUpdate" ); return true ; } componentWillUpdate ( ) { console .log("componentWillUpdate" ); } componentDidUpdate ( ) { console .log("componentDidUpdate" ); } render ( ) { console .log("render" ); return ( <h1>B标签内容:{this .props.carname}</h1> ) } }
常用的三个钩子函数
componentDidMount 一般在这个钩子做一些初始化的事情,开启定时器、发送网络请求、订阅消息
componentWillUnmount 一般在这个钩子中做一些收尾的事情,例如:关闭定时器、取消订阅消息
render 必须用
新旧生命周期的对比
由于react之后计划推出异步渲染效果,因此新的生命周期相比旧的生命周期,废弃了三个钩子(componentWillMount componentWillUpdate componentWillReceiveProps),这三个钩子在之前版本被滥用。新增了两个钩子getDerivedStateFromProps和getSnapshotBeforeUpdate
新版本(>React 17.0)中不可以再写componentWillMount,必须要加上UNSAFE前缀 componentWillMount –> UNSAFE_componentWillMount componentWillReceiveProps –> UNSAFE_componentWillReceiveProps componentWillUpdate –> UNSAFE_componentWillUpdate
新钩子 getDerivedStateFromProps(props, state)
getDerivedStateFromProps会在调用render方法之前调用,并且在初始挂载及后续更新时都会被调用。他会返回一个状态来更新state,如果返回null则不更新任何内容
此方法用于罕见的案例,即state的值在任何情况下都取决于props
派生状态会导致代码冗余,并使组件难以维护
1 2 3 4 5 6 7 8 9 10 class A extends React .Component { static getDerivedStateFromProps (props,state ) { console .log('getDerivedStateFromProps' ,props,state); return {count :1 } } }
新钩子getSnapshotBeforeUpdate
在更新之前获取快照
getSnapshotBeforeUpdate()在最近一次渲染输出(提交到DOM节点)之前调用,它使得组件能在发生更改之前从DOM捕获一些信息(例如:滚动位置)。此生命周期的任何返回值将作为参数传递给componentDidUpdate(preProps,preState,snapshotValue)
1 2 3 4 5 6 7 8 9 10 getSnapshotBeforeUpdate (preProps, preState ) { console .log(preProps,preState) return "aguigu" ; } componentDidUpdate (preProps,preState,snapshotValue ) { console .log(snapshowValue); }
getSnapshotBeforeUpdate的使用场景
实现一个滚动的新闻列表,滚动条最上方不变
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 class NewsList extends React .Component { state = {newlist :[]} componentDidMount ( ) { setInterval (() => { let {newlist} = this .state; let newValue = `新闻${newlist.length+1 } ` ; this .setState({ newlist:[newValue,...newlist] }) }, 1000 ); } getSnapshotBeforeUpdate (preProps,preValue ) { return this .refs.list.scrollHeight } componentDidUpdate (preProps,preValue,snapValue ) { this .refs.list.scrollTop += this .refs.list.scrollHeight - snapValue; } render ( ) { return ( <div className="list" ref="list" > {this .state.newlist.map((value,index )=> { return <div className ="news" key ={index} > {value}</div > })} </div> ) } } ReactDOM.render(<NewsList /> ,document .getElementById('test1' ))
二十一、Dom的diffing算法详解 举个例子 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 class Time extends React .Component { state = {date :new Date ()} componentDidMount ( ) { setInterval (()=> { this .setState({ date:new Date () }) },1000 ) } render ( ) { return ( <div> <h1>hello</h1> <input type="text" /> <span><input type ="text" /> 现在是{this .state.date.toTimeString()}</span> </div> ) } } ReactDOM.render(<Time /> ,document .getElementById("test1" ))
DOM Diffing算法中key的作用
面试题: 1、react/vue中的key有什么作用?(key的内部原理是什么?) 2、为什么遍历列表时,key最好不要用index?
回答:
1、虚拟DOM中key的作用 1)简单地说,key是虚拟DOM对象的标识,在更新显示key起着极其重要的作用 2)详细的说:当状态中的数据发生变化时,react会根据【新数据】生成【新的虚拟DOM】,然后react将【新的虚拟DOM】与【旧的虚拟DOM】进行Diff比较,比较规则如下
a.旧虚拟DOM中找到了与新虚拟DOM相同的key
(1)比较新旧虚拟DOM中的内容,如果内容没变,直接使用之前的真实DOM,如果虚拟DOM内容变了,则生成新的真实DOM,替换到页面中之前的真实DOM
b.旧虚拟DOM中未找到与新虚拟DOM相同的key,会根据数据创建新的真实DOM,随后渲染到页面
2、用index作为key可能会引发的问题? (1)若对数据进行:逆序添加,逆序删除等破坏顺序操作:会产生没有必要的真实DOM更新 ==》界面效果没问题,但是效率低 (2)如果结构中还包含有输入类DOM,例如input框,会产生错误的DOM更新 ==》界面有问题 (3)注意:如果不存在对数据的逆序添加、逆序删除等破坏顺序操作,仅用于前端渲染列表展示,使用index作为key是没有问题的
3、开发中如何选择key? 数据唯一表示:id、手机号、身份证、学号 如果确定只是用于简单的数据展示,使用index作为key也是可以的
虚拟DOM,输入类DOM,没有value值,只有真实DOM展现在页面上,用户才能输入,输入类虚拟DOM比较也只是一句标签结构进行比较
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 class Person extends React .Component { state = { persons:[ {id :1 ,name :'小张' ,age :18 }, {id :2 ,name :'小李' ,age :19 } ] } add = ()=> { const {persons} = this .state; const p = {id :persons.length+1 ,name :"小王" ,age :20 }; this .setState({ persons:[p,...persons] }) } render ( ) { return ( <div> <h2>展示人员信息</h1> <button onClick={this .add}>添加一个小王</button> <ul> {this .state.persons.map((person,index )=> { return <li key ={index} > {person.name}----{person.age}</li > })} </ul> </div> ) } } ReactDOM.render(<Person /> ,document .getElementById("test1" ))
二十二 杂七杂八(create-react-app脚手架) vscode代码块快速生成 文件–首选项–用户代码片段,使用https://snippet-generator.app/可以快速辅助生成代码
Boolean有toString()方法
Number.toFixed(2),将数字类型转换为带有两位小数的浮点数
Array.filter((item,inex) => { return item.index>2 }) filter方法中回调要求返回值是布尔值
Array.map方法返回值没有要求
Array.slice(0,3) 选中index为 0 1 2的数据,前闭后开
Array.splice(2,1,3) 在原数组的身上删除索引从2开始的一条数据并将3填入。返回值是被删除的数组
undefined和null没有toString()方法,可以用String()方法转换
typeof undefined结果是undefined
typeof null的结果是Object
jsx可以显示数组类型数据
div span a h2等元素都有title属性,用来提示信息
jsx中不能使用js的关键字,类似class用className代替,label的for属性用htmlFor代替
jsx动态修改添加类className = {"box title " + this.state.active ? "active" : ""}
jsx样式 <div style={{color:'red',fontSize:'50px'}}></div> 属性名用小驼峰命名法,属性值必须是字符串,否则会被识别为变量
事件绑定
原生:onclick
jsx中:onClick={this.handleClick},react合成事件
{isLogin && “你好”} js逻辑与,前面条件不成立,后面都不会判断,返回false,假如说最后一个值为真,将最后一个值作为整个表达式的结果返回
react模拟vue中v-show方法
react模拟vue中slot插槽 1、将jsx作为props属性传递 , 子组件通过this.props.leftJsx获取
2、将jsx作为children传递,子组件通过this.props.children[index]获取
react中列表渲染
vue中使用v-for
微信小程序中使用wx:for
react中使用js中map方法,不要用forEach
函数方式创建Jsx jsx仅仅是React.createElement(component,props,…children)函数的语法糖
所有的jsx都最终会被转换为React.createElement函数调用
虚拟DOM的创建过程 jsx –> createElement函数 –> ReactElement(虚拟DOM对象树) –> ReactDOM.render –> 真实DOM
为什么使用虚拟DOM?
虚拟DOM轻,装填方便轻松
原始DOM重,状态难以跟踪
频繁操作真实DOM,会引起DOM的频繁回流和重绘,效率低
Virtural DOM是一种编程理念,在这个理念中,UI以一种理想化或者说虚拟化的方式保存在内存中,他是一个相对简单的js对象
可以通过ReactDOM.render()方法将虚拟DOM映射到真实DOM,这个过程叫协调
npm和yarn对照表
脚手架使用 1 2 3 npx create-react-app hello-react 项目名称不能包含大写字母 cd hello-react yarn start 运行项目
执行yarn enjet展开webpack配置,慎用
react中组件名必须大写,jsx对标签名大小写敏感,必须小写。html对大小写不敏感
函数组件没有this
props属性限制
1 2 3 4 5 6 7 8 9 10 import PropTypes from 'prop-types' ChildCpn.propTypes = { name:PropTypes.string.isRequired } ChildCpn.defaultProps = { name:'alice' }
生命周期
text组件 小程序 view text (text中不能包含换行符,否则会原样解析,文字可以不包裹)
react-native View Text(文字必须用Text组件包裹)
react div (文字可以不包裹)
context
setState同步异步
合成事件onClick,react核心库的运行环境不仅包括浏览器,还有手机端,浏览器和手机端的事件对象不同,为了统一和高效率,react做了事件合成
setState在setTimeout和dom原生事件中同步
setState在组件生命周期和合成事件中异步
setState是对象合并并覆盖,基于Object.assign({},{message:”aaa”},this.state)
虽然做了三次+1,但是由于setState是批量异步处理,因此实际还是做了一次+1,那么怎么解决呢?
可以用,实现+3
1 2 3 4 5 6 7 8 9 10 11 setTimeout (()=> { this .setState({ count:this .state.count+1 }) this .setState({ count:this .state.count+1 }) this .setState({ count:this .state.count+1 }) },0 )
更好的方式是使用函数方式的setState,实现+3
1 2 3 4 5 6 7 8 9 this .setState(preState => { count:preState+1 }) this .setState(preState => { count:preState+1 }) this .setState(preState => { count:preState+1 })
函数方式执行setState的时候,不能用这种形式 ,state的索引没有变化,不会触发更新
1 2 3 4 5 6 7 8 9 this .setState(state => { words:state.words.concat(['mark' ]) }) this .setState(state => { words:[...state.words,'mark' ] })
react更新机制
index作为key,如果数据做插入、删除、逆序等操作时,index对效率提高很少。只有对于静态的展示的数据,才可以用index作为key提高效率
嵌套组件的render调用以及优化 情况:点击按钮时,触发父组件的render,子组件Header Main Footer也会重新触发render渲染
优化:
函数组件中使用memo()高阶组件包裹,类组件中使用PureComponent代替React.Component。
组件的方法属性可以用useCalllback包裹,省得每次渲染,都会生成一个不同的箭头函数地址值
注意:PuserComponent的子组件也必须时PureComponent
类组件
子组件中使用shouldComponentUpdate进行浅比较,不建议用深比较(效率低)
子组件继承自PureComponent,纯组件,会被对修改前后props和state对象进行浅比较
函数组件
全局事件传递 使用events库yarn add events
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 import React, { useState,useEffect,memo } from 'react' import {EventEmitter} from 'events' const eventBus = new EventEmitter()const Header = () => { return ( <div> <button onClick={()=> { eventBus.emit('changeText' ,"改变啦! " +Date .now()) }}>点击改变文字</button> </div> ) } const App = () => { const [text, settext] = useState("我是现在的文字" ) const handleChageText = (newText ) => { settext(newText) } useEffect(() => { eventBus.addListener('changeText' ,handleChageText) return () => { eventBus.removeListener('changeText' ,handleChageText) } }, []) return ( <div> {text} <Header/> </div> ) } export default memo(App)
ref 类组件中 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 1 、字符串形式 不推荐<h1 ref="ref1" >测试</h1> 使用 this .refs.ref1.current 2 、回调函数形式将节点挂接到this 对象 不推荐<h1 ref={(node )=> {this .node = node}}>测试</h1> 使用 this .node 3 、React.createRef() 推荐在constructor 函数中 使用this .ref1 .current const class App = (props ) => { constructor ( ) { super (props) this .ref1 = React.createRef() } render ( ) { return ( <h1 ref={this .ref1}>测试</h1> ) } }
函数组件中 使用useRef hook
类组件中可以使用forwardRef进行组件转发
https://www.cnblogs.com/yky-iris/p/12355243.html
高阶组件(High-Order Component)简称HOC 输入或者输出是组件
高阶函数的输入或者输出是函数
Portals的使用
很多第三方库都是基于Portals实现的,这里以一个简单的modal的实现为例
1、在html中添加一个id为modal的div,之后modal元素就渲染在这个div中,一般的第三方组件(例如ant-design中的modal) 没法要求你在index.html中新增一个div,他们是通过document.createElement(‘div’)创建div然后document.body.appendChild()将他动态挂载到页面
2、设置居中css
3、定义modal组件并使用
Fragment(片段) 不会被渲染的div容器,提高效率
Fragment还有一种短语法写法<></>,短语法不能设置key属性
StrictMode严格模式
react中样式 简介
内联样式
普通css(包括Less,Sass)
css modules
create-react-app脚手架已经内置了css modules规则,使用时如下
CSS的规则都是全局的,任何一个组件的样式规则,都对整个页面有效。
产生局部作用域的唯一方法,就是使用一个独一无二的class的名字 ,不会与其他选择器重名。这就是 CSS Modules 的做法。其他选择器不会产生局部作用域
上面是在react中使用css modules的过程,很简单。但是注意,只有title类选择器实现了局部作用域。虽然其他选择器虽然也能显示样式,但是他们仍然和css一样,是全局作用域的,仍然有可能出现覆盖问题。因此前端工程化开发中一般都推荐用类选择器
css in js
styled-components的原理:标签模板字符串,可以通过模板字符串的方式对一个函数进行调用
vscode中安装vscode-styled-components来支持语法
styled-components常用用法,偷懒的写法,当标签很多时,要是我们虽每个标签都要进行修饰,那岂不是要写好多的组件,但是在有些情况下我们没必要分这木多组件,那我们不妨可以试试以下的写法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 import React, { Component,Fragment} from 'react' ;import styled from 'styled-components' const StyleDiv = styled.div` &>h1{ font-size:30px; color:red; } &>div>h3{ font-size:20px; color:green; } ` class App extends Component { render ( ) { return ( <StyleDiv> <h1>大红牛</h1> <div> <h3>小绿牛</h3> </div> </StyleDiv> ) } } export default App;
在书写样式时,类似less,也可以使用各种选择器、伪类、伪元素
样式继承
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import React, { Component,Fragment} from 'react' ;import styled from 'styled-components' const Button = styled.button` color: palevioletred; font-size: 1em; margin: 1em; padding: 0.25em 1em; border: 2px solid palevioletred; border-radius: 3px; ` const YellowButton = styled(Button)` background-color:yellow ` class App extends Component { render ( ) { return ( <Fragment> <Button>红牛</Button> <YellowButton>枸杞</YellowButton> </Fragment> ) } } export default App;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 import React, { Component,Fragment} from 'react' ;import styled from 'styled-components' const StyleDiv = styled.div` &>h1{ font-size:30px; color:red; } &>div>h3{ font-size:20px; color:${props => props.smallcolor} ; } ` class App extends Component { state={ color:'green' } componentDidMount ( ) { setTimeout (() => { this .setState({ color:'blue' }) }, 3000 ); } render ( ) { return ( <StyleDiv smallcolor={this .state.color}> <h1>大红牛</h1> <div> <h3>小绿牛</h3> </div> </StyleDiv> ) } } export default App;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import React, { Component,Fragment} from 'react' ;import styled from 'styled-components' const Button = styled.button.attrs({ title:"aaa" , id:'bbb' , 'data-role' :(props )=> props.hello })` padding: 0.5em; margin: 0.5em; border: none; border-radius: 3px; ` class App extends Component { render ( ) { return ( <Fragment> <Button hello="hi" >红牛啊</Button> </Fragment> ) } } export default App;
动态添加类名
Ant Design组件库 介绍
使用
注意:安装less 和 less-loader
网络请求 短链接app请求
长连接直播请求(socket)
jquery ajax
fetch和axios
主流是axios库
测试地址 http://www.httpbin.org/
axios() 通用请求
axios.get() get请求
axios.post() post请求
axios.all() 本地就是Promise.all
默认配置
开发中有可能会遇到多个服务器地址的情况,这种时候baseURL该如何配置
创建多个实例axios.instance,分别配置
qs请求参数序列化
react动画 react-transition-group
CSSTransition Demo
Demo,结合antd和css-in-js实现卡片样式的刷新动画、出现动画、消失动画
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 import React, { useState } from 'react' import { CSSTransition } from 'react-transition-group' import StyleDiv from './style' import { Card, Button } from 'antd' ;const { Meta } = Card;export default function Demo ( ) { const [inProp, setInProp] = useState(true ); return ( <StyleDiv> <CSSTransition in ={inProp} unmountOnExit timeout={250 } classNames="card" appear onEnter = {el => console .log('开始进入' )} onEntering = {el => console .log('正在进入' )} onEntered = {el => console .log('完成进入' )} onExit = {el => console .log('开始退出' )} onExiting = {el => console .log('正在退出' )} onExited = {el => console .log('完成退出' )} > <Card hoverable style={{ width : 240 }} cover={ <img alt="example" src="https://os.alipayobjects.com/rmsportal/QBnOOoLaAfKPirc.png" />} > <Meta title="Europe Street beat" description="www.instagram.com" /> </Card> </CSSTransition> <Button type="primary" onClick={()=> setInProp(!inProp)}>显示/隐藏</Button> </StyleDiv> ); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 import styled from "styled-components" ;const StyleDiv = styled.div` width: 200px; text-align: center; Button { margin-top: 20px; } .card-enter,.card-appear { opacity:0; transform: scale(0); } .card-enter-active,.card-appear-active { opacity : 1; transform:scale(1); transition : opacity 300ms,transform 300ms ease-in-out; } .card-enter-done, .card-appear-done { } .card-exit { opacity : 1; transform:scale(1); } .card-exit-active { opacity : 0; transform:scale(0); transition : opacity 300ms,transform 300ms ease-in-out; } .card-exit-done { } ` ;export default StyleDiv;
SwitchTransition Demo
TransitionGroup Demo TransitionGroup主要用于给循环的列表数据添加动画
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 import React, { useState } from 'react' import { CSSTransition, TransitionGroup } from 'react-transition-group' import StyleDiv from './style' import { Button } from 'antd' ;export default function Demo ( ) { const [data,setData] = useState([ "first-child" , "second-child" , "third-child" , "forth-child" ]) return ( <StyleDiv> {} <TransitionGroup> { data.map((v,i )=> { console .log(v) return ( <CSSTransition key={v} timeout={500 } classNames="item" > <div>{v}</div> </CSSTransition> ) }) } </TransitionGroup> <Button type='primary' onClick={()=> { setData(["newData" +Date .now(),...data]) }}>添加一条数据</Button> </StyleDiv> ); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import styled from "styled-components" ;const StyleDiv = styled.div` .item-enter{ opacity:0; } .item-enter-active{ opacity:1; transition:opacity 300ms; } ` ;export default StyleDiv;
纯函数 必须满足以下两点
1、确定的输入产生确定的输出(不能有文件IO、随机数等)
2、不能有副作用(修改外部变量)
React中纯函数
1、组件必须像纯函数一样,保护他的props不会变化
2、redux中reducer也必须是一个纯函数
redux 简介
基础使用(无需依赖react) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 const initState = { count: 0 } function reducer (state = initState, action ) { switch (action.type) { case 'INCREMENT' : return { ...state, count : state.count + 1 } case 'DECREMENT' : return { ...state, count : state.count - 1 } case 'ADD_NUMBER' : return {...state, count : state.count + action.num} case 'SUB_NUMBER' : return {...state, count : state.count - action.num} } } const store = Redux.createStore(reducer)store.subscribe(() => { console .log('state发生了改变' ,store.getState().count) }) const action1 = { type:'INCREMENT' } const action2 = { type:'DECREMENT' } const action3 = { type: 'ADD_NUMBER' , num:3 } const action4 = { type: 'SUB_NUMBER' , num:3 } store.dispatch(action1) store.dispatch(action2) store.dispatch(action3) store.dispatch(action4)
进阶使用
将将redux拆分成
constant.js //存储aciton.type,因为reducer和createActions页面都需要,保持一致
1 2 3 4 export const ADD_NUMBER = 'ADD_NUMBER' export const SUB_NUMBER = 'SBU_NUMBER' export const INCREMENT = 'INCREMENT' export const DECREMENT = 'DECREMENT'
createActions.js 封装方法创建不同类型action
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import { ADD_NUMBER, SUB_NUMBER, INCREMENT, DECREMENT } from './constant' export const addAction = num => ({ type: ADD_NUMBER, num }) export const subAction = num => ({ type: SUB_NUMBER, num }) export const incrementAction = () => ({ type: INCREMENT }) export const decrementAction = () => ({ type: DECREMENT })
reducer.js 根据不同的action.type对数据做不用处理,返回新数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import { INCREMENT, DECREMENT, ADD_NUMBER, SUB_NUMBER } from "./constant" const initState = { count: 0 } export default function reducer (state = initState, action ) { switch (action.type) { case INCREMENT: return { ...state, count : state.count + 1 } case DECREMENT: return { ...state, count : state.count - 1 } case ADD_NUMBER: return {...state, count : state.count + action.num} case SUB_NUMBER: return {...state, count : state.count - action.num} default : return state } }
index.js 创建store并导出
1 2 3 4 import * as Redux from 'redux' import reducer from './reducer' const store = Redux.createStore(reducer)export default store
使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 import React, { PureComponent } from 'react' import store from '../demo/store' import { addAction } from '../demo/store/createActions' export default class Home extends PureComponent { state={ count:0 } componentDidMount ( ) { store.subscribe(() => { console .log('state发生了改变' , store.getState().count) this .setState({ count:store.getState().count }) }) } handleClick = (num ) => { store.dispatch(addAction(num)) } render ( ) { const {count} = this .state return ( <div> <h1>Home</h1> <div>当前计数:{count}</div> <button onClick={() => this .handleClick(1 )}>+1 </button> <button onClick={() => this .handleClick(5 )}>+5 </button> </div> ) } }
进一步封装 每个页面中都要写state,组件挂载完毕后订阅监听,很多公共代码,封装一个connect函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 import { PureComponent } from "react" import store from '../demo/store/index' export function connect (mapStateToProps, mapDispatchToProps ) { return function enhanceHOC (WrappedComponent ) { return class extends PureComponent { constructor (props ) { super (props) this .state = { storeData: mapStateToProps(store.getState()) } } componentDidMount ( ) { this .unsubscribe = store.subscribe(() => { this .setState({ storeData: mapStateToProps(store.getState()) }) }) } componentWillUnmount ( ) { this .unsubscribe() } render ( ) { return <WrappedComponent {...this.props } {...mapStateToProps(store.getState())} {...mapDispatchToProps(store.dispatch)} /> } } } }
使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 import React, { PureComponent } from 'react' import { addAction } from '../demo/store/createActions' import { connect } from '../utils/connect' class Home extends PureComponent { render ( ) { const {count,addNum} = this .props return ( <div> <h1>Home</h1> <div>当前计数:{count}</div> <button onClick={() => addNum(1 )}>+1 </button> <button onClick={() => addNum(5 )}>+5 </button> </div> ) } } const mapStateToProps = state => { return { count:state.count } } const mapDispatchToProps = dispatch => ({ addNum:function (num ) { dispatch(addAction(num)) } }) export default connect(mapStateToProps,mapDispatchToProps)(Home)
完全封装 函数内部还依赖import store from ‘../demo/store/index’中的store,需要用户手动导入文件,要想使用户可以配置,创建一个StoreContext包裹住根组件,让用户配置value为它自己的store即可。
1、index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 import React from 'react' ;import ReactDOM from 'react-dom' ;import App from './App' ;import StoreContext from './components/utils/context' import store from './components/demo/store/index' ReactDOM.render( <StoreContext.Provider value={store}> <App /> </StoreContext.Provider>, document .getElementById('root' ) );
2、connect.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 import { PureComponent } from "react" import StoreContext from './context' export function connect (mapStateToProps, mapDispatchToProps ) { return function enhanceHOC (WrappedComponent ) { return class extends PureComponent { static contextType = StoreContext constructor (props,context ) { super (props) this .state = { storeData: mapStateToProps(context.getState()) } } componentDidMount ( ) { this .unsubscribe = this .context.subscribe(() => { this .setState({ storeData: mapStateToProps(this .context.getState()) }) }) } componentWillUnmount ( ) { this .unsubscribe() } render ( ) { return <WrappedComponent {...this.props } {...mapStateToProps(this.context.getState())} {...mapDispatchToProps(this.context.dispatch)} /> } } } }
3、context.js
1 2 3 import React from 'react' const StoreContext = React.createContext()export default StoreContext
4、使用
仍然同上
使用react-redux 实际上我们已经实现了react-redux,了解其原理,这里进行使用。
npm install react-redux
使用react-redux的StoreContext和connect api取代自己封装的即可
1、index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 import React from 'react' ;import ReactDOM from 'react-dom' ;import App from './App' ;import store from './components/demo/store/index' import { Provider } from 'react-redux' ;ReactDOM.render( <Provider store={store}> <App /> </Provider>, document .getElementById('root' ) );
2、使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 import React, { PureComponent } from 'react' import { addAction } from '../demo/store/createActions' import { connect } from 'react-redux' class Home extends PureComponent { render ( ) { const {count,addNum} = this .props return ( <div> <h1>Home</h1> <div>当前计数:{count}</div> <button onClick={() => addNum(1 )}>+1 </button> <button onClick={() => addNum(5 )}>+5 </button> </div> ) } } const mapStateToProps = state => { return { count:state.count } } const mapDispatchToProps = dispatch => ({ addNum:function (num ) { dispatch(addAction(num)) } }) export default connect(mapStateToProps,mapDispatchToProps)(Home)
redux处理异步
redux-thunk
0、添加新数据相关文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 export const ADD_NUMBER = 'ADD_NUMBER' export const SUB_NUMBER = 'SBU_NUMBER' export const INCREMENT = 'INCREMENT' export const DECREMENT = 'DECREMENT' export const CHANGEIMAGE = 'CHANGEIMAGE' import { INCREMENT, DECREMENT, ADD_NUMBER, SUB_NUMBER, CHANGEIMAGE } from "./constant" const initState = { count: 0 , imageUrl: '' } export default function reducer (state = initState, action ) { switch (action.type) { case INCREMENT: return { ...state, count : state.count + 1 } case DECREMENT: return { ...state, count : state.count - 1 } case ADD_NUMBER: return { ...state, count : state.count + action.num } case SUB_NUMBER: return { ...state, count : state.count - action.num } case CHANGEIMAGE: return { ...state, imageUrl : action.url } default : return state } }
1、store\index.js
1 2 3 4 5 6 7 8 9 10 import * as Redux from 'redux' import thunkMiddleware from 'redux-thunk' import reducer from './reducer' const storeEnhancer = Redux.applyMiddleware(thunkMiddleware)const store = Redux.createStore(reducer,storeEnhancer)export default store
2、createActions.js
1 2 3 4 5 6 7 8 9 10 11 12 13 export const changeImgAction = (url ) => ({ type:CHANGEIMAGE, url }) export const imgAsyncAction = async (dispatch,getState) => { console .log('进入异步action' ) const state = getState() const res = await axios.get('https://yesno.wtf/api' ) dispatch(changeImgAction(res.data.image)) }
3、test\index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import React, { useEffect,useState } from 'react' import axios from 'axios' import { connect } from 'react-redux' import { imgAsyncAction } from '../demo/store/createActions' const Test = (props ) => { useEffect(()=> { props.getImage() },[]) return ( <div> <img src={props.imgUrl} style={{width :'100px' ,height :'50px' }}/> </div> ) } export default connect( state => ({ imgUrl:state.imageUrl }), dispatch => ({ getImage:() => dispatch(imgAsyncAction) }) )(Test)
redux-devtool是一个谷歌浏览器,方便在浏览器里面查看redux存储的状态和派发的action。主要配置在store\index.js中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import * as Redux from 'redux' import thunkMiddleware from 'redux-thunk' import reducer from './reducer' const storeEnhancer = Redux.applyMiddleware(thunkMiddleware)const composeEnhancers = typeof window === 'object' && window .__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? window .__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ trace:true }) : Redux.compose; const store = Redux.createStore(reducer,composeEnhancers(storeEnhancer))export default store
redux-saga处理异步 首先了解一下生成器、迭代器(async await 的底层原理)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 function * fun1 ( ) { console .log(1 ) const a = 2 d = yield a console .log('d' ,d) e = yield b = 3 console .log('e' ,e) } const it = fun1()console .log(it.next()) console .log(it.next()) console .log(it.next(6 )) function * fun2 ( ) { for (let i = 0 ; i<10 ;i++){ yield i } } const it2 = fun2()console .log(it2.next().value)console .log(it2.next().value)console .log(it2.next().value)console .log(it2.next().value)function * fun3 ( ) { console .log('进入fun3' ) const res = yield new Promise ((resolve,reject )=> { setTimeout (()=> { resolve('data' ) },3000 ) }) console .log('res' ,res) } const it3 = fun3()it3.next().value.then(value => { console .log(value) it3.next(value) })
1、安装redux-saga
npm install redux-saga
2、store\index.js中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import * as Redux from 'redux' import thunkMiddleware from 'redux-thunk' import createSagaMiddleware from 'redux-saga' import reducer from './reducer' import saga from './saga' const sagaMiddleware = createSagaMiddleware()const storeEnhancer = Redux.applyMiddleware(thunkMiddleware,sagaMiddleware)const composeEnhancers = typeof window === 'object' && window .__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? window .__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ trace:true }) : Redux.compose; const store = Redux.createStore(reducer,composeEnhancers(storeEnhancer))sagaMiddleware.run(saga) export default store
2、saga.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import {takeEvery,put,all} from 'redux-saga/effects' import { CHANGEIMAGESAGA } from './constant' import axios from 'axios' import { changeImgAction } from './createActions' function * getImageData (action ) { const res = yield axios.get('https://yesno.wtf/api' ) console .log(res) yield all([ yield put(changeImgAction(res.data.image)) ]) } function * saga ( ) { yield all([ takeEvery(CHANGEIMAGESAGA,getImageData) ]) } export default saga
3、使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import React, { useEffect,useState } from 'react' import axios from 'axios' import { connect } from 'react-redux' import { imgAsyncAction, imgSagaAsyncAction } from '../demo/store/createActions' const Test = (props ) => { useEffect(()=> { props.getImage() },[]) return ( <div> <img src={props.imgUrl} style={{width :'100px' ,height :'50px' }}/> </div> ) } export default connect( state => ({ imgUrl:state.imageUrl }), dispatch => ({ getImage:() => dispatch(imgSagaAsyncAction) }) )(Test)
redux-thunk与redux-saga的区别 1、thunk容易,只是将actionCreater从对象变成了一个回调函数,可以在回调函数中发送异步请求,更符合函数化编程的思想,缺点是异步代码存储在action中,耦合度较高
2、saga使用起来更复杂,基于生成器和yield的语法更难以理解。但是saga可以将异步请求与action分离,代码耦合度更低。action是普通对象,与redux相同,saga.js中集中处理了所有异步操作,方便接口测试
3、在小项目中,thunk基本足够使用。大项目中使用saga较多
reducer代码拆分 reducer和reduce语法有相似之处,所以才叫reducer,平时多逛逛MDN
reducer函数switch 分支过多,处理内容过多
1、第一步拆分,修改一下reducer文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 import { INCREMENT, DECREMENT, ADD_NUMBER, SUB_NUMBER, CHANGEIMAGE } from "./constant" const initHomeInfo = { count:0 } const homeReducer = (state = initHomeInfo,action ) => { switch (action.type) { case INCREMENT: return { ...state, count : state.count + 1 } case DECREMENT: return { ...state, count : state.count - 1 } case ADD_NUMBER: return { ...state, count : state.count + action.num } case SUB_NUMBER: return { ...state, count : state.count - action.num } default : return state } } const initTestInfo = { imageUrl:'' } const testReducer = (state = initTestInfo,action ) => { switch (action.type){ case CHANGEIMAGE: return { ...state, imageUrl : action.url } default : return state } } export default function reducer (state = {}, action ) { return { homeInfo:homeReducer(state.homeInfo,action), testInfo:testReducer(state.testInfo,action) } }
2、第二步拆分独立文件
每个组件,对应自己的reducer
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 export const ADD_NUMBER = 'ADD_NUMBER' export const SUB_NUMBER = 'SBU_NUMBER' export const INCREMENT = 'INCREMENT' export const DECREMENT = 'DECREMENT' import { ADD_NUMBER, SUB_NUMBER, INCREMENT, DECREMENT} from './constant' export const addAction = num => ({ type: ADD_NUMBER, num }) export const subAction = num => ({ type: SUB_NUMBER, num }) export const incrementAction = () => ({ type: INCREMENT }) export const decrementAction = () => ({ type: DECREMENT }) import { INCREMENT, DECREMENT, ADD_NUMBER, SUB_NUMBER } from "./constant" const initHomeInfo = { count:0 } export const homeReducer = (state = initHomeInfo,action ) => { switch (action.type) { case INCREMENT: return { ...state, count : state.count + 1 } case DECREMENT: return { ...state, count : state.count - 1 } case ADD_NUMBER: return { ...state, count : state.count + action.num } case SUB_NUMBER: return { ...state, count : state.count - action.num } default : return state } import {homeReducer} from './reducer' export default homeReducer
总reducer中
1 2 3 4 5 6 7 8 9 10 11 import homeReducer from "./home" ;import testReducer from "./test" ;export default function reducer (state = {}, action ) { return { homeInfo:homeReducer(state.homeInfo,action), testInfo:testReducer(state.testInfo,action) } }
3、使用redux提供的compineReducers函数进行reducer合并
总reducer中
1 2 3 4 5 6 7 8 9 10 import homeReducer from "./home" ;import testReducer from "./test" ;import { combineReducers } from "redux" ;const reducer = combineReducers({ homeInfo:homeReducer, testInfo:testReducer }) export default reducer
到底用什么管理state
monkey patching 给内置对象拓展方法
react路由 三大阶段
前端路由的原理
react-router
基本使用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 import 'antd/dist/antd.less' import { HashRouter, Link, NavLink, BrowserRouter, Route } from 'react-router-dom' import Home from './components/react-router/home' import Profile from './components/react-router/profile' import About from './components/react-router/about' export default function App ( ) { return ( <HashRouter> {} <Link to="/" >home </Link> <Link to="/about" >about </Link> <Link to="/profile" >profile </Link> {} {} {} <Route path='/' exact component={Home}/> <Route path='/about' component={About}/> <Route path='/profile' component={Profile}/> </HashRouter> ); }
Demo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 import 'antd/dist/antd.less' import './reset.css' import { HashRouter, Link, NavLink, BrowserRouter, Route, Switch } from 'react-router-dom' import Home from './components/react-router/home' import Profile from './components/react-router/profile' import About from './components/react-router/about' import NoMatch from './components/react-router/nomatch' import User from './components/react-router/user' export default function App ( ) { return ( <BrowserRouter> {} <NavLink exact to="/" activeStyle={{ color : 'red' }} activeClassName="item-active" >home </NavLink> <NavLink to="/about" activeStyle={{ color : 'red' }} activeClassName="item-active" >about </NavLink> <NavLink to="/profile" activeStyle={{ color : 'red' }} activeClassName="item-active" >profile </NavLink> <NavLink to="/id:111" activeStyle={{ color : 'red' }} activeClassName="item-active" >user </NavLink> <NavLink to="/222" activeStyle={{ color : 'red' }} activeClassName="item-active" >nomatch </NavLink> <Switch> {} {} {} <Route path='/' exact component={Home} /> <Route path='/about' component={About} /> <Route path='/profile' component={Profile} /> <Route path='/id:userid' component={User} /> <Route component={NoMatch} /> </Switch> </BrowserRouter> ); }
Demo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import React, { useEffect,useState } from 'react' import { Redirect } from 'react-router-dom' export default function Home ( ) { const [login, setLogin] = useState(false ) if (!login) { return ( <Redirect to='/login' /> ) } return ( <div> Home </div> ) }
Demo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 import React from 'react' import { NavLink, Switch, Route } from 'react-router-dom' function aboutHostory ( ) { return ( <div> 500 年历史 </div> ) } function aboutCulture ( ) { return ( <div> 源远流长 </div> ) } function aboutContact ( ) { return ( <div> 电话:025 -8888 </div> ) } export default function About ( ) { return ( <div> <NavLink to='/about' >企业历史</NavLink> <NavLink to='/about/culture' >企业文化</NavLink> <NavLink to='/about/contact' >联系我们</NavLink> <Switch> {} {} {} <Route exact component={aboutHostory} path='/about' /> <Route component={aboutCulture} path='/about/culture' /> <Route component={aboutContact} path='/about/contact' /> </Switch> </div> ) } import 'antd/dist/antd.less' import './reset.css' import { HashRouter, Link, NavLink, BrowserRouter, Route, Switch, Redirect } from 'react-router-dom' import Home from './components/react-router/home' import Profile from './components/react-router/profile' import About from './components/react-router/about' import NoMatch from './components/react-router/nomatch' import User from './components/react-router/user' import { useState, useEffect } from 'react' import Login from './components/react-router/login' export default function App ( ) { return ( <BrowserRouter> {} <NavLink exact to="/" activeStyle={{ color : 'red' }} activeClassName="item-active" >home </NavLink> <NavLink to="/about" activeStyle={{ color : 'red' }} activeClassName="item-active" >about </NavLink> <NavLink to="/profile" activeStyle={{ color : 'red' }} activeClassName="item-active" >profile </NavLink> <NavLink to="/login" activeStyle={{ color : 'red' }} activeClassName="item-active" >login </NavLink> <NavLink to="/id:111" activeStyle={{ color : 'red' }} activeClassName="item-active" >user </NavLink> <NavLink to="/222" activeStyle={{ color : 'red' }} activeClassName="item-active" >nomatch </NavLink> <Switch> {} {} {} <Route path='/' exact component={Home} /> <Route path='/about' component={About} /> <Route path='/profile' component={Profile} /> <Route path='/login' component={Login} /> <Route path='/id:userid' component={User} /> <Route component={NoMatch} /> </Switch> </BrowserRouter> ); }
props.history跳转路由
注意:只有路由组件可以通过this.props.history进行路由跳转。非路由组件可以使用withRouter高阶组件包裹后,也可以使用this.props.history
函数组件也可以使用useHistory hook
react-router的history api如下
HashRouter和BrowserRouter的区别
URL的表现形式不一样 BrowseRouter使用HTML5的history API,保证UI界面和URL同步。HashRouter使用URL的哈希部分来保持UI和URL的同步。哈希历史记录不支持location.key和location.state。
HashRouter不支持location.state和location.key 首先我们需要了解页面之间传值的方式,参考我之前写的文章React页面传值的做法 通过state的方式传值给下一个页面的时候,当到达新的页面,刷新或者后退之后再前进,BrowseRouter传递的值依然可以得到。
路由传参
1、动态路由传简单参数
通过Link的to属性传递通过this.props.match.params获取动态路由传递过来的参数,刷新不会丢失
1 2 3 4 5 6 7 8 9 10 11 12 13 <NavLink to="/user/1234" activeStyle={{ color : 'red' }} activeClassName="item-active" >user </NavLink> <Route path='/user/:userid' component={User} /> import React from 'react' export default function User (props ) { return ( <div> {props.match.params.userid} </div> ) }
2、search传参(已经不推荐使用)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <NavLink to="/user?name=why&age=18" activeStyle={{ color : 'red' }} activeClassName="item-active" >user </NavLink> <Route path='/user' component={User} /> import React from 'react' export default function User (props ) { console .log(props.location.search) console .log(props.history.location.search) return ( <div> {props.location.search.slice(1 ).replace('&' ,' and ' )} </div> ) }
3、Link(NavLink)标签的to属性传入一个对象(复杂参数推荐使用,但是要注意HashRouter包裹的话,刷新会丢失state。BrowserRouter包裹根组件则不会丢失)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 NavLink to={{ pathname:'/user' , search:'?test=1&test2=2' , state:{ name:'why' , age:28 } }} activeStyle={{ color : 'red' }} activeClassName="item-active" >user </NavLink> <Route path='/user' component={User} /> import React from 'react' export default function User (props ) { console .log(props.location.search) console .log(props.location.state) return ( <div> {JSON .stringify(props.location.state)} </div> ) }
4、props.history.方法传参(但是要注意HashRouter包裹的话,刷新会丢失state。BrowserRouter包裹根组件则不会丢失)
1 2 3 4 5 6 7 8 9 <div onClick={this .click.bind(this )}>About</div> click ( ) { this .props.history.push({ pathname : "/about" , state : { id } }); } this .props.location.state.id
5、监听路由退出当前页面弹出提示框(多用于表单未填写完毕用户返回或者关闭窗口)
1 2 3 4 5 6 <Prompt when={formIsHalfFilledOut} message="Are you sure you want to leave?" /> message:可以是回调函数或者字符串,提示信息 when:布尔型,true 代表用户退出时会弹出提示框,false 代表用户退出时不会弹出提示框
react-router-config 配置路由 npm install react-router-config
新增一个routes.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 import Home from "../home" ;import NoMatch from "../nomatch" ;import {About,aboutContact,aboutHostory,aboutCulture} from "../about" ;const routes = [ { path:'/' , exact:true , component:Home }, { path:'/about' , component:About, routes:[ { path:'/about' , exact:true , component:aboutHostory }, { path:'/about/culture' , component:aboutCulture }, { path:'/about/contact' , component:aboutContact } ] }, { component:NoMatch } ] export default routes
1 2 3 4 5 6 7 8 import routes from './components/react-router/routes' ;jsx中{renderRoutes(routes)} {} {} {renderRoutes(props.route.routes)}
react-router路由拦截鉴权 react-router路由懒加载 react hooks
函数组件依然需要import React from 'react'
严格模式下有些组件会被调用两次来测试代码是否有问题
hook必须在顶层代码使用
setXXX必须传入一个新数据,否则不会刷新。
setCount(preCount => preCount+1) 类似this.setState(preState => ({count:preState.count+1}))。都是为了防止多行setState同时运行修改某一项,只修改一次。传入回调函数的形式能确保当前回调执行完毕再执行下一行
useState useEffect 1 2 3 4 5 6 7 8 9 10 11 12 13 14 useEffect(()=> { }) useEffect(()=> { return () => { } },[]) useEffect(()=> { },[count])
useContext 以前往往需要用Context.Consumer,如果有多个还需要嵌套,使用useContext hook则非常简单
useReducer 只是useState的升级版,不能取代redux,没法共享数据。当state的逻辑处理过于复杂,可以使用useReducer
useCallback
useMemo useRef
类组件可以通过ref拿到实例对象,函数组件不行,可以使用forwardRef包裹函数组件转发ref,然后给函数组件中的某个DOM设置ref。函数组件可以通过转发ref获取到具体的DOM节点
DEMO
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import React,{useState,useEffect,useRef} from 'react' export const Test = (props ) => { const [count,setCount] = useState(0 ) const dataRef = useRef({ count:0 }) useEffect(()=> { dataRef.current.count = count },[count]) return ( <div> <h1>之前的值:{dataRef.current.count}</h1> <h1>现在的值: {count}</h1> <button onClick={()=> {setCount(count + 1 )}}>+1 </button> </div> ) }
useImperativeHandle 一般结合forwardRef+函数组件使用
类组件可以通过ref拿到实例对象,函数组件不行,可以使用forwardRef包裹函数组件转发ref,然后给函数组件中的某个DOM设置ref。函数组件可以通过转发ref获取到具体的DOM节点。
如果只使用forwardRef,为了获得子组件的一个DOM节点,结果给整个子组件挂载了ref,可操纵子组件的内容过多,自由度太多
改造:子组件中使用useImperativeHandle方法,接收一个参数时ref,第二个参数是一个回调函数,返回值是一个可被父组件获取的方法/属性,这样当父组件调用inputRef.current.focus()时实际上执行的时useImperativeHanle的focus方法。再这个方法里面,通过子组件自身的ref获取到input节点,执行focus方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 import 'antd/dist/antd.less' import './reset.css' import React,{useState,useEffect,useRef, forwardRef, useImperativeHandle} from 'react' const HYInput = forwardRef((props,ref ) => { const inputRef = useRef() const [inputText,setInputText] = useState('' ) useImperativeHandle(ref,()=> ({ focus:()=> {inputRef.current.focus()}, value:inputText }),[inputText]) return ( <input value={inputText} onChange={(e )=> {setInputText(e.target.value)}} ref={inputRef} /> ) }) const App = (props ) => { const inputref = useRef() return ( <div> <HYInput ref={inputref}/> {} {} {} <button onClick={()=> {inputref.current.focus()}}>获取焦点</button> <button onClick={()=> {console .log(inputref.current.value)}}>获取值</button> </div> ) } export default App
useLayoutEffect
自定义hook hooks原理 Fiber React16推出,
电脑屏幕参数:刷新率,一秒钟刷新多少次,单位hz(赫兹)
GUI渲染和JS代码执行是在一个线程,互斥
网易云官网项目 项目规范
1、项目创建
2、文件夹创建
3、重置样式reset.css和common.css
4、安装antd并配置
5、安装craco配置文件夹快捷路径,避免层层../
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 const CracoLessPlugin = require ('craco-less' );const path = require ("path" );const resolve = dir => path.resolve(__dirname, dir);module .exports = { plugins: [ { plugin: CracoLessPlugin, options: { lessLoaderOptions: { lessOptions: { modifyVars: { '@primary-color' : '#1DA57A' }, javascriptEnabled: true , }, }, }, }, ], webpack: { alias: { '@' : resolve("src" ) } } };
6、页面划分AppHeader AppFooter
7、路由配置react-router-dom react-router-config
8、导入规范
9、使用styled-component编写AppHeader和AppFooter样式(背景图片用精灵图)
10、主页面路由优化 、 发现二级路由配置
11、封装axios 12、redux react-redux redux-thunk redux-devtool搭建,每一个子页面一个reducer
13、使用redux获取推荐页面轮播图数据
14、使用redux hooks替代connect mapStateToProps useSelecter useDispatch shallowEqual
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import React, { memo, useEffect } from 'react' import { getBannersAction } from './store/actionCreator' import { useSelector, shallowEqual } from 'react-redux' import { useDispatch } from 'react-redux' function Recommend (props ) { const banners = useSelector(state => state.recommendInfo.banners,shallowEqual) const dispatch = useDispatch() useEffect(()=> { dispatch(getBannersAction()) },[dispatch]) return ( <div> Recommend: { banners && banners.length } </div> ) } export default memo(Recommend)
15、ImmutableJS解决对象拷贝性能问题
16、redux-immutable的combineReducer函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 import { CHANGEBANNER } from "./constant" ;import {Map } from 'immutable' const initState = Map ({ banners:[] }) export function reducer (state=initState,action ) { switch (action.type){ case CHANGEBANNER: return initState.set("banners" ,action.data) default : return state } } import recommendReducer from "../pages/find-music/children-pages/recommend/store" ;import { combineReducers } from "redux-immutable" ;const reducer = combineReducers({ recommendInfo:recommendReducer }) export default reducerconst {banners} = useSelector(state => { return { banners:state.getIn(['recommendInfo' ,'banners' ]) } },shallowEqual)
17、antd轮播图(走马灯),轮播图组件封装
18、热门推荐header组件封装
19、热门推荐数据请求
20、song-cover封装,优化,图片太大,图片后缀使用param=140*140
新碟上架组件开发
21、播放器功能实现audio+Slider
二十三 dva 简介 dva是一个基于redux和redux-saga的数据流方案,同时为了方便开发,集成了react-router和fetch,也可以理解为一个轻量级的应用框架
特性
易学易用 ,仅有 6 个 api,对 redux 用户尤其友好,配合 umi 使用 后更是降低为 0 API
elm 概念 ,通过 reducers, effects 和 subscriptions 组织 model
插件机制 ,比如 dva-loading 可以自动处理 loading 状态,不用一遍遍地写 showLoading 和 hideLoading
支持 HMR ,基于 babel-plugin-dva-hmr 实现 components、routes 和 models 的 HMR
快速上手 安装 dva-cli 通过 npm 安装 dva-cli 并确保版本是 0.9.1 或以上。
1 2 3 $ npm install dva-cli -g $ dva -v dva-cli version 0.9.1
创建新应用 安装完 dva-cli 之后,就可以在命令行里访问到 dva 命令(不能访问? )。现在,你可以通过 dva new 创建新应用。
1 $ dva new dva-quickstart
这会创建 dva-quickstart 目录,包含项目初始化目录和文件,并提供开发服务器、构建脚本、数据 mock 服务、代理服务器等功能。
然后我们 cd 进入 dva-quickstart 目录,并启动开发服务器:
1 2 $ cd dva-quickstart $ npm start
几秒钟后,你会看到以下输出:
1 2 3 4 5 6 7 8 Compiled successfully! The app is running at: http://localhost:8000/ Note that the development build is not optimized. To create a production build, use npm run build.
在浏览器里打开 http://localhost:8000 ,你会看到 dva 的欢迎界面。
使用 antd 通过 npm 安装 antd 和 babel-plugin-import 。babel-plugin-import 是用来按需加载 antd 的脚本和样式的,详见 repo 。
1 $ npm install antd babel-plugin-import --save
编辑 .webpackrc,使 babel-plugin-import 插件生效。
1 2 3 4 5 { + "extraBabelPlugins": [ + ["import", { "libraryName": "antd", "libraryDirectory": "es", "style": "css" }] + ] }
注:dva-cli 基于 roadhog 实现 build 和 dev,更多 .webpackrc 的配置详见 roadhog#配置
定义路由 我们要写个应用来先显示产品列表。首先第一步是创建路由,路由可以想象成是组成应用的不同页面。
新建 route component routes/Products.js,内容如下:
1 2 3 4 5 6 7 import React from 'react' ;const Products = (props ) => ( <h2>List of Products</h2> ); export default Products;
添加路由信息到路由表,编辑 router.js :
1 2 3 + import Products from './routes/Products'; ... + <Route path="/products" exact component={Products} />
然后在浏览器里打开 http://localhost:8000/#/products ,你应该能看到前面定义的 <h2> 标签。
编写 UI Component 随着应用的发展,你会需要在多个页面分享 UI 元素 (或在一个页面使用多次),在 dva 里你可以把这部分抽成 component 。
我们来编写一个 ProductList component,这样就能在不同的地方显示产品列表了。
新建 components/ProductList.js 文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 import React from 'react' ;import PropTypes from 'prop-types' ;import { Table, Popconfirm, Button } from 'antd' ;const ProductList = ({ onDelete, products } ) => { const columns = [{ title: 'Name' , dataIndex: 'name' , }, { title: 'Actions' , render: (text, record ) => { return ( <Popconfirm title="Delete?" onConfirm={() => onDelete(record.id)}> <Button>Delete</Button> </Popconfirm> ); }, }]; return ( <Table dataSource={products} columns={columns} /> ); }; ProductList.propTypes = { onDelete: PropTypes.func.isRequired, products: PropTypes.array.isRequired, }; export default ProductList;
定义 Model 完成 UI 后,现在开始处理数据和逻辑。
dva 通过 model 的概念把一个领域的模型管理起来,包含同步更新 state 的 reducers,处理异步逻辑的 effects,订阅数据源的 subscriptions 。
新建 model models/products.js :
1 2 3 4 5 6 7 8 9 export default { namespace: 'products' , state: [], reducers: { 'delete' (state, { payload : id }) { return state.filter(item => item.id !== id); }, }, };
这个 model 里:
namespace 表示在全局 state 上的 key
state 是初始值,在这里是空数组
reducers 等同于 redux 里的 reducer,接收 action,同步更新 state
然后别忘记在 index.js 里载入他:
1 2 // 3. Model + app.model(require('./models/products').default);
connect 起来 到这里,我们已经单独完成了 model 和 component,那么他们如何串联起来呢?
dva 提供了 connect 方法。如果你熟悉 redux,这个 connect 就是 react-redux 的 connect 。
编辑 routes/Products.js,替换为以下内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import React from 'react' ;import { connect } from 'dva' ;import ProductList from '../components/ProductList' ;const Products = ({ dispatch, products } ) => { function handleDelete (id ) { dispatch({ type: 'products/delete' , payload: id, }); } return ( <div> <h2>List of Products</h2> <ProductList onDelete={handleDelete} products={products} /> </div> ); }; export default connect(({ products } ) => ({ products, }))(Products);
最后,我们还需要一些初始数据让这个应用 run 起来。编辑 index.js:
1 2 3 4 5 6 7 8 9 - const app = dva(); + const app = dva({ + initialState: { + products: [ + { name: 'dva', id: 1 }, + { name: 'antd', id: 2 }, + ], + }, + });
刷新浏览器,应该能看到以下效果:
构建应用 完成开发并且在开发环境验证之后,就需要部署给我们的用户了。先执行下面的命令:
几秒后,输出应该如下:
1 2 3 4 5 6 7 8 9 10 > @ build /private/tmp/myapp > roadhog build Creating an optimized production build... Compiled successfully. File sizes after gzip: 82.98 KB dist/index.js 270 B dist/index.css
build 命令会打包所有的资源,包含 JavaScript, CSS, web fonts, images, html 等。然后你可以在 dist/ 目录下找到这些文件。
简单DEMO(不包含网络请求和路由)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 import dva from 'dva' ;import './index.css' ;const app = dva({ initialState:{ products:[ {name :'dva' ,id :1 }, {name :'antd' ,id :2 } ] } }); app.model(require ('./models/product' ).default); app.router(require ('./router' ).default); app.start('#root' );
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import React from 'react' ;import { Router, Route, Switch } from 'dva/router' ;import IndexPage from './routes/IndexPage/IndexPage' ;import ProductPage from './routes/ProductPage/ProductPage' ;function RouterConfig ({ history } ) { return ( <Router history={history}> <Switch> <Route path="/" exact component={IndexPage} /> <Route path="/product" component={ProductPage} /> </Switch> </Router> ); } export default RouterConfig;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 export default { namespace: 'products' , state: [ ], reducers: { delete (state, { payload: id } ) { return state.filter(item => item.id !== id); }, 'add' (state,{data}){ console .log(data) return [...state,data] } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 import React, { useCallback } from 'react' ;import {v4 as uuid} from 'uuid' import { connect } from 'dva' ;import { Button } from 'antd' ;import ProductList from '../../components/ProductList/ProductList' ;const Products = ({ dispatch, products } ) => { function handleDelete (id ) { dispatch({ type: 'products/delete' , payload: id, }); } const handleAdd = useCallback( () => { dispatch({ type:'products/add' , data:{name :'随机姓名:' +uuid(),id :uuid()} }) }, [], ) return ( <div> <h2>List of Products</h2> <ProductList onDelete={handleDelete} products={products} /> <Button type="primary" onClick={handleAdd}>添加</Button> </div> ); }; export default connect(({ products } ) => ({ products, }))(Products);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 import React,{memo} from 'react' ;import PropTypes from 'prop-types' ;import { Table, Popconfirm, Button } from 'antd' ;const ProductList = memo(function ProductList ({ onDelete, products } ) { const columns = [{ title: 'Name' , dataIndex: 'name' , }, { title: 'Actions' , render: (_, record ) => { return ( <Popconfirm title="Delete?" onConfirm={() => onDelete(record.id)}> <Button>Delete</Button> </Popconfirm> ); }, }]; return ( <Table dataSource={products} columns={columns} /> ); }) ProductList.propTypes = { onDelete: PropTypes.func.isRequired, products: PropTypes.array.isRequired } export default ProductList
路由使用 dva中路由集成的是react-router,与react-router的使用一致,使用Link或者NavLink的to属性跳转到某个地址,router.js中通过设置component配置访问指定地址时要显示的page
1 2 3 4 5 6 <NavLink to='/product' >跳转到product页面</NavLink> <Switch> <Route path="/" exact component={IndexPage} /> <Route path="/product" component={ProductPage} /> </Switch>
js方法调用,详见上面路由,如果是在dva脚手架中非路由组件中跳转,需要import {withRouter} from 'dva/router'对组件进行包裹,然后组件中props.history.push('/product')即可跳转
因为dva中react-router进行了一层封装,因此需要去dva/router中导入
mock使用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 module .exports = { "GET /api/product" :{"name" :"高粱" } } export default { ...require('./mock/product' ) }; import request from '../utils/request' export function getProduct ( ) { return request("/api/product" ) } useEffect(()=> { getProduct() .then(v => { console .log(v) }) },[])
使用mockjs很方便,可以快速模拟指定图片和自增信息,可以随机数字,可以快速生成城市、文字、颜色、图片地址等信息
npm install mockjs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 const Mock = require ('mockjs' )module .exports = { "GET /api/product" : { "name" : "高粱" }, [`GET /api/user` ](req, res) { res.json(Mock.mock({ "data|20" : [{ "goodsClass" : "女装" , "goodsId|+1" : 1 , "goodsName" : "@ctitle(10)" , "goodsAddress" : "@county(true)" , "goodsStar|1-5" : "★" , "goodsImg" : "@Image('100x100','@color','小甜甜')" , "goodsSale|30-500" : 30 }] })) } }
异步请求DEMO
页面中不能使用services中封装的方法请求数据,应该直接dispatch一个异步的action
1 2 3 4 5 6 7 8 useEffect(()=> { dispatch({ type:'products/fetchProduct' , payload:{} }) },[])
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 import { getProduct } from "../services/product" ;export default { namespace: 'products' , state: [ ], reducers: { delete (state, { payload: id } ) { return state.filter(item => item.id !== id); }, 'add' (state,{data}){ console .log(data) return [...state,data] }, set (state,{payload} ) { console .log(payload) return [...payload] } }, effects:{ *fetchProduct ({ payload }, { call, put } ) { const res = yield call(getProduct,payload) yield put({ type:'set' , payload:res.data }) } } }
1 2 3 4 import request from '../utils/request' export function getProduct ( ) { return request("/api/product" ) }
1 2 3 4 5 6 7 8 9 10 11 const Mock = require ('mockjs' )module .exports = { "GET /api/product" :(req,res ) => { const params = req.query res.send([{ name : 'dva' , id : 1 },{ name : 'antd' , id : 2 }]) } }
redux-actions使用 避免每次创建action的麻烦
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const increment = (num )=> ({type :"increment" ,payload :num})const decrement = () => ({type :'decrement' ,payload :num})dispatch(increment(10 )) import { createAction } from 'redux-actions' ;export const increment = createAction('cincrement' );export const decrement = createAction('decrement' );dispatch(increment()) dispatch(increment(10 ))