200字范文,内容丰富有趣,生活中的好帮手!
200字范文 > react 入门看这个就够了

react 入门看这个就够了

时间:2019-12-28 20:11:34

相关推荐

react 入门看这个就够了

1. React

1.1. 常识

React 是什么:用于构件用户界面的 JavaScript 库(将数据渲染为 HTML 视图,它不关注获取数据啥的,只给你渲染到 HTML 上面)

为什么要学:

原生 JavaScript 操作 DOM (jQuery 也是操作 DOM,只是写起来简单) 繁琐、效率低(比如 document.getElementById()一句话就是:DOM-API 操作 UI直接操作 DOM,浏览器会进行大量的重新绘制、重新排列,造成效率低下原生 JavaScript 没有组件化编码方案,代码复用率低

React 优点:

采用组件化模式、声明式编码(命令式和声明式就像手动挡与自动挡的区别,命令式的步骤不能缺少,才能完成一个任务;而声明式可以自动帮你完成)在 React Native 中可以使用 React 语法进行移动端开发(利用 JavaScript 开发 app)使用虚拟 DOM + Diffing 算法,尽可能地减少了与真实 DOM 交互

1.2. 旧版本使用

1.2.1. 库简介

babel.js

之前的作用有:es6 转 es5 语法,让浏览器能够识别;模块化的时候 -> import 也需要靠它进行工作现在的新的一个作用:将 jsx 转为 js

react.development.js

react 的核心库

react-dom.development.js

react 用于操作 DOM 的核心库

1.2.2. 编写 hello-world

<body><!-- 1.准备好容器 --><div id="root"></div><!-- 2.引入核心库 --><script src="js/react.development.js"></script> <!-- 必须要先引入 react 核心库 --><script src="js/react-dom.development.js"></script><script src="js/babel.min.js"></script><!-- 必须写成 babel,意思是里面的代码是 jsx,需要用 babel 来转换 --><script type="text/babel">// 3.创建虚拟 DOMconst VDOM = <h1>Hello React</h1> // 这里不是字符串,这就是个虚拟 DOM// 4.渲染虚拟 DOM 到 HTML 上ReactDOM.render(VDOM, document.getElementById('root')) // 引入 react 就会在 window 添加一个 React 成员;引入 react-dom 就会在 window 添加一个 ReactDOM 成员console.log(window)</script></body>

1.2.3. 为什么要用 jsx

需求:生成<div id="root"><span>Hello React</span></div>这样一个节点

原生 JavaScript:

// createElement 是创建真实 DOMReact.createElement('div', {id: 'root' }, React.createElement('span', {}, 'Hello World'))

jsx:

const VDOM = (<div><span>Hello React</span></div>)

总结:这里就可以看出来 jsx 的优势了,它能够很轻松地创建虚拟 DOM (但实际上,它还是会翻译成上面的结果,只是我们写起来方便,就是个语法糖而已)

1.2.4. VDOM 是什么

虚拟 DOM 实际上是 Object虚拟 DOM 比较,它只需要 react 内部使用,就不需要那么多真实 DOM 的属性虚拟 DOM 最终会被渲染成真实 DOM,放在页面上

1.2.5. jsx(JavaScript XML)

如下是 JSX 的一些语法规则:

定义虚拟 DOM 时,不能有引号标签内有表达式时,得用{ username }样式要用className = "username"内联样式用{{ color: 'red' }},可以看成 {} 传入了一个对象 { color: ‘#f00’ }只能有一个根标签,如果要写多个兄弟节点,就只能在外层包一层标签必须闭合,否则报错,如:<input type="text"/>尽量别乱写标签,如:<good>123</good>,小写开头的标签会自动转换成 HTML 元素,控制台会报错;大写开头就是组件,如果没有定义的话,也会报错

// 例子const id = 'username'const username = 'yuwan'const VDOM = (<h1 id={id} className="username">{ username.toLowerCase() }<span style={{ color: '#f00', fontSize: '30px' }}></span></h1>)

Tip:表达式代码的区别:

1.表达式返回一个值,比如:username、a+b、add(1, 3)【左侧写一个变量,能够接收到的就是表达式】

2.比如这些就是代码而不是表达式:if() 、{}、 for(){}、 switch(){}等

1.2.6. 小案例 - 动态生成前端框架名称

const title = '前端 js 框架列表'const frameworks = ['Angular.js', 'React.js', 'Vue.js']const VDOM = (<div><h1>{title}</h1><ul>{// 这里是不能写 forEach 来遍历,因为它没有返回值;写 map 才能动态生成出来frameworks.map((item, index) => {return <li key={index}>{item}</li>})}</ul><p>{}</p></div>)ReactDOM.render(VDOM, document.getElementById('root'))

1.2.7. 模块、组件、模块化、组件化的理解

1.2.7.1. 模块

理解:一般就是一个 js 文件,就是一个模块为什么分模块:按照业务逻辑增加,代码会增多且复杂作用:复用 js,简化 js 的编写,提高 js 运行效率

1.2.7.2. 组件

理解:用来实现一些功能的代码和资源的集合(每个组件有自己的 HTML、CSS、JS、Video、image,简言之就是什么都拆了)为什么分组件:一个界面的功能更加复杂的时候,分组件就能更好地复用代码作用:复用代码,简化项目编码,提高运行效率

1.2.7.3. 模块化

当应用的 js 都是用模块来编写的,那么这个应用就是一个模块化的应用

1.2.7.4. 组件化

当应用都是以组件方式来进行开发,那么这个应用就是一个组件化的应用

1.3. react 面向组件编程

1.3.1. 定义组件

react 中,组件有两种:函数式组件类式组件

1.3.1.1. 函数式组件

粘贴 CreateApp 代码,在线查看 babel 转换结果

渲染过程:

1.React 解析组件标签,找到 CreateApp 组件(若找不到则会报错)2.发现 CreateApp 是使用函数定义的,因此就会调用该函数,将返回的虚拟 DOM 渲染为真实 DOM,随后呈现在页面中

注意:

babel 在转换过程中开启了严格模式,导致 this 不能指向 window,因此是 undefined

<script type="text/babel">function CreateApp() {console.log(this) // undefinedreturn <h1>我是函数式组件,我适合于简单组件的创建</h1>}ReactDOM.render(<CreateApp/>, document.getElementById('app'))</script>

这里 render(CreateApp(), …) 也会呈现出内容,但是它不是个组件了,在 Console -> Components 下无法找到

1.3.1.2. 类式组件

类知识复习

<script>class Person {constructor(name, age) {// 构造器中的 this 指向的是实例对象,new Person() 的谁就指向谁this.name = name // 构造器是可以不写的,因为继承this.age = age}speak() {// 一般方法:speak 放在了哪里? 原型对象上,供所有实例都能够调用// 这里 this 指向函数调用的对象。可能是 Person 实例对象,也可能是 call、bind、apple 等改变 this 指向的对象console.log(`我叫${this.name},我今年 ${this.age}岁啦!`)}}const p1 = new Person('zhang', 18)console.log(p1)p1.speak()p1.speak.call({name:'yuwan', age:20})console.log('----------华丽的分割线-----------')class Student extends Person{constructor(name, age, grade) {super(name, age); // 一旦 Student 继承 Person 而且写了 constructor,必须调用 super() 而且是第一句调用this.grade = grade}speak() {console.log(`我叫${this.name},我今年 ${this.age}岁啦,我读${this.grade}年级`)}}const s1 = new Student('yuwan', 22, '研究生')console.log(s1)s1.speak() // 根据原型链的查找方式,会先找 Student 原型对象 - Person 的 speak 方法,从而实现覆盖 Person 原型对象的 speak 方法</script>

总结:

constructor 可写可不写,具体看要求

class Son extends Father,且 Son 写了 constructor,就必须调用 super()【可以简化编码】

类中定义的一般方法,放在了类的原型对象上,供所有实例使用

注意 this 的指向【构造器中,this 指向的是 new Person();一般方法的 this就要看具体情况了】

注意原型链查找方式

渲染过程:

1.react 解析标签,找到 MyApp 组件2.发现这个组件是类定义的,就会 new MyApp() 然后调用了 render 方法3.将 render() 返回的虚拟 DOM 渲染到页面的真实 DOM 中

<script type="text/babel">class MyApp extends ponent {render() {// 这个方法在 MyApp 的原型对象上,供所有 MyApp 实例对象调用return <h1>我是类式组件,适用于较复杂的组件</h1>}}ReactDOM.render(<MyApp/>, document.getElementById('app'))</script>

1.3.2. 组件核心属性之 state

理解:简单理解就是组件自身所需要的数据;组件被称为状态机,通过更新组件的 state 来更新对应的页面显示(重新渲染组件)

注意事项:

组件中 render 的 this 指向了组件实例组件中自定义方法中 this 为 undefined,怎么解决?(bind、()=>{})状态中的数据不可直接修改,通过setState修改

<script>class MyComponent extends ponent{constructor(props) {super(props); // 按照上面所说,有继承且写了 constructor 就必须要调用 superthis.state = {isHot: true } // 初始化 state。当前组件的 state 是一个对象,便于保存多种类型的数据}render() {const {isHot } = this.statereturn <h1>今天天气很{isHot? '炎热':'凉爽'}</h1>}}ReactDOM.render(<MyComponent/>, document.getElementById('app'))</script>

1.3.2.1. 点击事件

原生 JavaScript 的点击事件分为三种写法,React 推荐第三种写法

const btn1 = document.getElementById('btn1')btn1.addEventListener('click',() => {alert('hello world')})const btn2 = document.getElementById('btn2')btn2.onclick = () => {alert('hello world')}// <button οnclick="test()">按钮3</button>function test() {alert('hello world')}

1.3.2.2. 类中方法 this 指向

类中的函数默认开启了严格模式,导致直接通过 varSpeak() 调用时,会输出 undefined

<script>class Person {constructor(name,age) {this.name = namethis.age = age}speak() {// 默认是开启了局部严格模式console.log(this)}}const p1 = new Person('tom', 19)p1.speak() // Person {name: 'tom', age: 19}const varSpeak = p1.speakvarSpeak() // undefined</script>

1.3.2.3. React 绑定事件

如下是演示,我们在不知道 react 怎么绑定事件的过程:首先想到的是传函数调用结果 -> 传表达式调用结果 -> 传函数名 -> 解决 this -> this.xxx 调用

<script>class MyApp extends ponent{// ...render() {return <h1 onClick="test()"></h1>}}function test(){console.log('this is test message.')}// ERROR: Expected `onClick` listener to be a function, instead got a value of `string` type.// 1.本来点击事件要回调函数,但是这样写就是传给它一个 string,因此报错return <h1 onClick={test() }></h1>// ERROR: 控制台直接输出:this is test message.// 2.这相当于把 test() 的返回值传给了 onClick 作为回调函数,而 test() 执行就会打印输出,进而返回 undefined,因此此时点击是无响应的return <h1 onClick={test }></h1>// 3.这里点击就可以输出: this is test message.// 由于函数写在类外部,我们获取不到 MyApp 实例对象,因此在 test 方法中无法操作 MyApp 实例对象的 stateclass MyApp extends ponent{// ...render() {return <h1 onClick={test }></h1>}test(){console.log(this)console.log('this is test message.')}}// ERROR: test is not defined// 4.因为在一个类中的方法体里,调用类的其他方法,肯定得要用 this.xx 才能调用return <h1 onClick={this.test }></h1>// 点击 h1,执行 test() 输出 undefined// 正如前面一节,类中 this 指向,相当于 onClick = this.test; 在某个时刻,onClick() 这样调用了,而类中函数开启了严格模式,因此输出 undefined</script>

绑定事件的正确用法

有两种方式:bind 来改变 this利用箭头函数的特点

<script>class App extends ponent{constructor(){super()this.test = this.test.bind(this) // 解决 this 指向问题// 右侧:this.test,会找到原型的 test 方法【因为 test 方法在类中定义】;// .bind(this) 就把这个函数重新绑定了 this 指向,而且这个指向就是当前组件实例,右侧返回结果是一个函数// 左侧:this.test = 右侧,将右侧的结果赋值给了当前组件实例,因此这个组件实例也有个方法叫做 test 了}test(){// 方式一console.log(this)console.log('this is test message.')}test = () => {// 方式二console.log(this)console.log('this is test message.')}render(){// 执行 1(初始化执行)+N(state 发生改变就执行)return <h1 onClick={this.test }>绑定事件</h1>}}</script>

总结:

理解 react 为什么要通过上面方式来进行事件调用【主要是 this 指向问题】通过 setState 修改 state 里面的值,而且它是合并操作

1.3.3. 组件核心属性之 props

说明:props 是只读的,无法对里面的某个字段进行修改

1.3.3.1. 基本使用方法:

<script>class Person extends ponent{render() {const {name, age, sex } = this.propsreturn (<div><ul><li>姓名:{name }</li><li>性别:{sex }</li><li>年龄:{age }</li></ul><hr/></div>)}}// 这里给组件传的任何 k-v 都会被传入到 props 里;这里写起来麻烦,推荐下面方式ReactDOM.render(<Person name="Tom" age="20" sex="男"/>, document.getElementById('p1'))const bob = {name: "Bob", age: "22", sex: "男"}// 要求是 bob 这个对象的属性名要和传入的属性名相同,这里 babel+react 可以迭代对象,且仅适用于标签属性ReactDOM.render(<Person {...bob}/>, document.getElementById('p3')) // {...bob} --> { name: "Bob", age: "22", sex: "男"}</script>

1.3.3.2. … 运算符简单用法

let arr1 = [1,2,3]let arr2 = [6,7,8]// 展开数组console.log(...arr1) // 1 2 3// 连接数组let arr3 = [...arr1, ...arr2] // [1,2,3,6,7,8]// 不定长参数function sum(...nums){return nums.reduce((preV,curV) => preV+curV, 0)}sum(1,2,3) // 6sum(1,2) // 3// 使用字面量对象时用展开实现浅拷贝let p1 = {name: 'yuwan', age: 23 }let p2 = {...p1 } // p2 --> { name: 'yuwan', age: 23 }let p3 = {...p1, name: 'Jerry', sex: '女'} // 其实就是合并操作,将 yuwan 替换为 Jerry,并追加 sex 属性

1.3.3.3. 给每个标签指定类别和默认值

解释:类似于 Vue 父子传值时,规定好一些默认值和类型

需要引入prop-types.js,用于限定数据类型(16.0 以前都是通过 React.PropTypes.string 来进行限制的,导致 React 这个核心对象变大了而且也不一定需要这个,因此官方把它抽离出来形成了prop-types.js)

<!--引入这个,全局就多一个 PropTypes 对象--><script src="js/prop-types.js"></script><script type="text/babel">class Person extends ponent{render() {const {name, age, sex, speak } = this.propsreturn (<div><h2>{speak&&speak() }</h2><ul><li>姓名:{name }</li><li>性别:{sex }</li><li>年龄:{age }</li></ul><hr/></div>)}}function speak(){return '自我介绍中...'}Person.propTypes = {// 对属性类型进行限制name: PropTypes.string.isRequired, // name 必传且是字符串sex: PropTypes.string,age: PropTypes.number,speak: PropTypes.func // 传入函数}Person.defaultProps = {// 设置属性默认值sex: 'XXX'}ReactDOM.render(<Person name='Tom' age={20 } speak={speak }/>, document.getElementById('p1'))</script>

1.3.3.4. 简写方式

<script>class Person extends ponent{static propTypes = {//对 Person类本身加入属性 对属性类型进行限制name: PropTypes.string.isRequired, // name 必传且是字符串sex: PropTypes.string,age: PropTypes.number,speak: PropTypes.func // 传入函数}static defaultProps = {// 设置属性默认值sex: 'XXX'}// ...}</script>

1.3.3.5. 构造器进一步作用

是否需要如下写法,取决于是否需要在构造器中使用 this.props(很少用到,因此可以不写)

class Person extends ponent{constructor(props){super(props)console.log(this.props) // 这里调用了 super(props) 就能够在组件实例输出}}

1.3.3.6. 函数式组件中使用 props

16.0之前的版本,函数式组件仅可使用 props

function Person(props){// 传入的参数都在这个 props 对象里了const {name, age, sex} = propsreturn <div>{name } - {age } - {sex }</div>}const p1 = {name: 'Tom',age: 24,sex: 'M'}ReactDOM.render(<Person {...p1}/>, document.getElementById('app'))

1.3.4. 组件核心属性之 refs

解释:Vue 的 refs 和它是一样的,就是对某个元素打上标记,能够直接操作真实DOM

写法:

字符串类型的 ref (效率存在问题,因此不建议再使用了)回调形式 :ref={(curNode)=>this.rmsg = curNode}【公司常用】React.createRef【官方推荐】

注意:不可过度使用 ref,在本例中,第二个输入框的失去焦点回调函数可以通过 handleRight(e){ e.target.value } 获取到它的文本值,也就是发生事件的元素正好是当前元素就可以用这种

1.3.4.1. 小案例 - 输入数据并弹框

class MyComponent extends ponent{state = {leftVal:'',rightVal:''}render() {return (<div className="wrapper"><input type="text" placeholder="点击按钮提示消息" onChange={this.handleLeft} ref="lmsg"/><button className="btn" onClick={this.showLMessage }>弹窗显示</button><!-- 这里 curNode 代表 input 节点,给谁加就是谁;react 看到是个回调 ref,它就会自己调用 --><input type="text" onChange={this.handleRight} ref={(curNode) => {this.rmsg = curNode} }</div>)}handleLeft = () => {this.setState({leftVal: this.refs.lmsg.value // 通过 this.refs 调用})}handleRight = () =>{this.setState({rightVal: this.rmsg.value // 直接通过 this.rmsg 调用})}}

class App extends ponent{lRef = React.createRef() // 这个容器只能装下当前节点,函数返回一个容器render() {return (<div><input type="text" placeholder="点击按钮提示消息" onChange={this.handleLeft} ref={this.lRef}/></div>)}handleLeft = () => {console.log(this.lRef.current.value) // 通过容器的 this.lRef.current 调用}}

1.3.5. 事件处理

通过 onXxx 属性绑定事件的回调函数【注意大小写,跟原生相反】

因为 react 使用的是自定义事件(如 onClick vs onclick ),它考虑了兼容性react 把事件都委托给了最外层节点,因为事件冒泡原则,它就更高效(如 ul 添加点击事件就比给它的每个 li 添加事件效果好)

1.3.6. 收集表单数据

1.3.6.1. 小案例 - 输入账号和密码,弹窗显示

非受控组件:现用现取,如下面的 handleLogin() 调用的时候,采取获取 username 和 password

class Login extends ponent{idRef = React.createRef()pwdRef = React.createRef()render() {// 默认表单提交 get 请求,并携带所有的 k-v 如:/?username=admin&password=123return (<div><h1>欢迎登录</h1><form action="" onSubmit={this.handleLogin }>账号:<input ref={this.idRef} name="username" type="text"/><br/>密码:<input ref={this.pwdRef} name="password" type="password"/><br/><input type="submit" value="登录"/><button onClick={this.clearInput }>清空</button></form></div>)}handleLogin = (e) => {const username = this.idRef.current.valueconst password = this.pwdRef.current.valuealert(`账号:${username},密码:${password}`)e.preventDefault() // 阻止表单默认提交行为}clearInput = () => {this.idRef.current.value = ''this.pwdRef.current.value = ''}}

受控组件:随着用户操作,就把值绑定到 state 中 【推荐使用,毕竟不需要 ref 】

class Login extends ponent {state = {username: '',password: ''}render() {// 默认表单提交 get 请求,并携带所有的 k-v 如:/?username=admin&password=123return (<div><h1>欢迎登录</h1><form action="" onSubmit={this.handleLogin}>账号:<input name="username" type="text" onChange={this.inputUname} /><br/>密码:<input name="password" type="password" onChange={this.inputPwd} /><br/><input type="submit" value="登录"/><button onClick={this.clearInput}>清空</button></form></div>)}inputUname = (e) => {this.setState({username: e.target.value})}inputPwd = (e) => {this.setState({password: e.target.value})}handleLogin = (e) => {e.preventDefault() // 阻止表单默认提交行为const {username} = this.stateconst {password} = this.state}}

写通用输入方法(高阶函数写法)

func(callback) 或 func(){ return callback }则 func 是高阶函数(形参是函数或返回一个函数)

函数柯里化:通过函数调用继续返回函数的方式,实现多次接收参数,最后统一处理的函数编码形式

class Login {render(){return (<div>账号:<input name="username" type="text" onChange={this.inputData('username') } /></div>)}inputData = (inputType) => {// 这个方法就可通用决定输入return (e) => {// onChange 调用这个回调函数,自然会传入 event 形参this.setState({// 这里也算是函数柯里化[inputType]: e.target.value // [inputType] 在设置对象键的时候,转成字符串,因为它是变量,否则直接存入inputType这个键})}}}

不用函数柯里化:onChange = { e => this.inputData(‘username’, e) } // 再重写一下 inputData 就好

1.3.7. 组件生命周期

三大框架都有生命周期回调函数,如下是原理图

1.初始化阶段:由 ReactDOM.render() 触发 — 初次渲染

constructorcomponentWillMountrender【必用】componentDidMount【常用于初始化,如开启定时器、发送请求、订阅消息】

2.更新阶段:由 this.setState() 或父组件重新 render() 触发

shouldComponentUpdatecomponentWillUpdaterendercomponentDidUpdate

3.卸载阶段:由 ReactDOM.unmountComponentAtNode() 触发

componentWillUnmount【常用于收尾工作,如关闭定时器、取消订阅】componentDidMount()【执行一次,Vue 的 mounted 与它类似】componentWillUnmount()【执行一次,Vue 的 beforeDestroy 与它类似】render 【组件初始化 + 修改状态触发】

新版本的生命周期图

getDerivedStateFromProps(props):要求返回null 或 { state的东西 },并将这个作为渲染,null 不影响 constructor 的 state 对象

1.3.8. diffing 算法

比较的最小粒度是:标签

<li>xxx<input/> </li> // xxx等内容变了,但是如果 input 没变,input 仍会复用

经典面试题:vue/react 中的 key 有什么作用?(或遍历列表时,index 为什么不适合当 key)

diff 规则:

1.新的虚拟 DOM 中找到与 旧的虚拟 DOM 相同的 key的话

如果内容也相同 ==> 复用(即不重新生成真实 DOM)内容不同 ==> 重新生成真实 DOM

2.没有找到的话

直接生成真实 DOM

/*这里用 index 作为 key 的过程:原始数据:{id: 1, name: '张三', age: 19},{id: 2, name: '李四', age: 21}原始虚拟 DOM:<li key=0>张三-19</li><li key=1>李四-21</li>新的数据:{id: 3, name: '王五', age: 32},{id: 1, name: '张三', age: 19},{id: 2, name: '李四', age: 21}新的虚拟 DOM:<li key=0>王五-32</li><li key=1>张三-19</li><li key=2>李四-21</li>diff 比较:三个 <li> 都需要重新渲染;因此用 id 作为 key,要高效很多index 作为 key:对于逆序添加、删除操作会存在重新渲染的问题--效率低;如果只是展示数据,是可以使用的*/class Person extends ponent {state = {personArr: [{id: 1, name: '张三', age: 19},{id: 2, name: '李四', age: 21}]}render() {return (<div><h2>人员信息如下:</h2><ul>{this.state.personArr.map((person, index) => {return <li key={index}>{person.name} - {person.age}</li>})}</ul><button onClick={this.addPerson}>添加员工</button></div>)}addPerson = () => {const {personArr} = this.statethis.setState({personArr: [{id: personArr.length + 1, name: '王五', age: 32}, ...personArr] // 注意这里是放在了开始位置})}}

Tip:如果每个

含有 输入当前的 name,必须要用 id 作为 key了

1.4. 使用脚手架创建 APP

1.4.1. 安装脚手架

npm i -g create-react-appcreate-react-app my-app # 创建一个 react 项目cd my-appnpm startnpm build # 打包npm eject # 暴露 webpack 配置,此操作不可逆

还可以用npm init react-app my-app构件 react 应用

1.4.2. 目录结构

my-app├─ package-lock.json├─ package.json├─ public│ ├─ favicon.ico│ ├─ index.html│ ├─ logo192.png│ ├─ logo512.png│ ├─ manifest.json # 应用加壳的配置文件,也就是当成手机应用的一些常用配置│ └─ robots.txt # 爬虫规则文件 ├─ README.md└─ src├─ App.css├─ App.js # App 组件├─ App.test.js├─ index.css├─ index.js # 入口文件├─ logo.svg├─ reportWebVitals.js # 记录页面性能└─ setupTests.js # 组件测试

1.4.3. 小案例 - todoList

总结:

样式模块化:最好使用less等预处理,避免全局污染rcc(快速生成类组件代码片段)rfc(快速生成函数组件代码片段)如何确定数据放在哪个组件的 state 中呢? 如果多个组件都要用到,就放在它们的父组件上子组件要给父组件传值:

// 父组件通过 props 传递一个函数给子组件,子组件通过合适的时机调用,就可以在父组件拿到子组件的值<son addCallback = {this.add}/>add = (a,b) => {console.log(a,b) // 3,4}// 子组件中class Son extends ponent{render(){// 这里调用即可this.props.addCallback(3,4)}}

两个有用的包

npm i nanoid # 生成 UUIDnpm i prop-types # 限定标签类型和属性,用法同之前

CheckBox 的一个坑:defaultChecked只渲染一次,后面 checked 更新后,它不会再发生变化(类似地还有 defaultValue 和 value)

<input type="checkbox" defaultChecked={checked}/><!-- 解决办法:check 和 onChange 都写上 --><input type="checkbox" checked={checked} onChange={this.handleChange}/>

1.5. ajax

安装:

npm i axios

跨域问题:浏览器能发送请求,但是被 ajax 引擎拦截了响应,导致获取不到结果

解决办法:

修改package.json,增加如下语句,然后发送 ajax 请求【此时 ajax 请求的端口号变成 3000】

"proxy": "http://localhost:5000"

注意:并不是所有请求都会转发给服务器,因请求 localhost:3000/index.html 在本地存在这个文件,就不会发送;反之才会转发到服务器

当后台启动两个服务器,方式一就不适用了;src 下新建一个setupProxy.js内容如下:

const proxy = require('http-proxy-middleware')module.exports = function (app){app.use(proxy('/api1',{// 遇见 /api1 才会触发代理target:'http://localhost:5000', // 请求转发给的服务器地址changeOrigin: true, // 控制服务器收到的请求头中 Host 字段的值(request headers)不加的话,就是 3000,服务端还是可能会限制请求pathRewrite:{'^/api1': ''} // 重写请求路径:/api1/students -> students 才能访问到数据(必须要写)}),proxy('/api2',{target:'http://localhost:5001',changeOrigin: true,pathRewrite:{'^/api2': ''}}))}

// 调用getStudentData = ()=>{axios.get('http://localhost:3000/api1/students').then(res=>{// 触发 /api1 代理console.log(res)}).catch(err=>{console.log(err)})}getCarData = ()=>{axios.get('http://localhost:3000/api2/cars').then(res=>{// 触发 /api2 代理console.log(res)}).catch(err=>{console.log(err)})}

1.5.1. 小案例 - 搜索 github 用户

总结:

1.连续解构赋值并重命名:const {target:{value:keyWord}} = e // target 拿不到,它只是一个中间量

2.await 只能获取到 promise 成功的回调,失败的情况要通过 try-catch 捕捉

3.需要先订阅 (componentWillMount),再发布,组件销毁需要取消订阅 (componentWillUnmount)

try{const res = await reqxxx() }catch (e) {console.log(e)}

1.5.2. 兄弟组件通信(消息订阅-发布)

npm i pubsub-js

github上使用方法

1.5.3. fetch 发送请求(了解)

它是 promise 风格的、关注分离的、windows 自带的能够发送异步请求

github上使用方法

相关介绍博客

1.6. 路由

1.6.1. 一些常识

1.SPA的理解

整个应用是一个页面、多个组件组成不会刷新页面、只会局部刷新数据通过 ajax 请求,并在前端异步展现

2.路由的理解

工作原理:点击导航区特殊的路由链接,路由链接就改变了浏览器的地址,前端路由器监视到浏览器地址发生变化,就会展现相应的组件

一个路由就是一个映射关系(key-value,比如 path = /xx,value = XX组件/函数,后端路由就是 function,前端路由就是 component)

1.6.2. react-router-dom

npm i react-router-dom

1.6.2.1. 基本使用

import {BrowserRouter as Router, Link, Route } from 'react-router-dom'class App extends ponent{render() {return (<Router><Link to='/home'>Home</Link><Link to='/about'>About</Link>{/* 注册路由:路由组件 */}<Route path="/home" component={Home}/><Route path="/about" component={About}/></Router>)}}

路由组件和一般组件的区别:

写法不同

存放位置不同(pages/components)

是否默认接收了props

1.6.2.2. 高亮路由

// 默认类名是 active;对于多个路由还可以进一步封装<NavLink activeClassName = 'xxx' to='/home'>Home</NavLink><MyNavLink to="/home">Home</MyNavLink>class MyNavLink extends ponent{render() {<NavLink {...this.props} />}}

Tip:MyNavLink 中标签体内容 ‘Home’ 等价于<MyNavLink to="/home" children="home"/>于是它以 children 属性传给了 中,仍然是正常工作的

1.6.2.3. 路由匹配问题

// 访问 /home 默认是会向后匹配的,会展示 Home、Abc 两个组件的内容,效率低下import {Switch } from "react-route-dom";<Route path="/home" component={Home}/><Route path="/home" component={Abc}/>// 引入 Switch,避免向后再匹配<Switch><Route path="/home" component={Abc}/></Switch>

1.6.2.4. 多级路由问题

如果是多级路由+刷新会引起css 样式丢失

比如 react 创建的 SPA 里的 index.html有如下引用

<link href="./css/bootstrap.css" />

组件:

<Route path="/yuwan/home" component={ home }/>

解决办法:

link 下 href 路径写成/css/bootstrap.csslink 下 href 路径写成%PUBLIC_URL/css/bootstrap.css%将 BrowserRouter 改成HashRouter(不常用)

1.6.2.5. 精准匹配与模糊匹配

默认是模糊匹配,匹配方式是:

首先看<Route>的 path 属性再把<NavLink>的 to 属性按照/分割,并按照顺序对比

<NavLink to='/home/profile'></NavLink> // 能匹配<Route path='/home'></Route> // 它的 path 是 /home,而 to 拆分后,第一个就是 home,因此能够匹配<NavLink to='/home'></NavLink> <Route path='/home/profile'></Route> // 它的 path 是 /home/profile,而 to 拆分后,第一个是 /home,故无法匹配<Route exact={ true } path='/home/profile'></Route> // 开启精准匹配

Tip: 对于二级目录不要开启精准匹配,否则不会匹配到子路由

1.6.2.6. 路由重定向

import { Redirect } from "react-route-dom";<Route path='/home/profile'></Route><Redirect to="home"/> // 重定向到 home 页,一般写在最下方

1.6.2.7. 嵌套路由

注册子路由时要写上父路由的 path 值由于路由按照注册路由的顺序来的,因此二级目录匹配成功会显示一级目录那个组件

1.6.2.8. 路由传参

1.params 传参

<Link key={news.id } to={`/news/detail/${news.id}/${news.title}` }>{news.title }</Link><Route path='/news/detail/:id/:title' component={Detail }></Route>// Detail 如下方法获取:传过来了 {history,location,match} 对象const {id,title} = this.props.match.params

2.search 传参

import qs from 'querystring';<Link key={news.id } to={`/news/detail?id=${news.id}&title=${news.title}` }>{news.title }</Link><Route path='/news/detail' component={Detail }></Route>// Detail 得通过 props.location.search ==> '?id=1&title=新闻1' 解析出来const {id,title} = qs.parse(this.props.location.search.replace('?','')) // qs.stringify(obj) 把 obj 转成 urlencoded 编码

3.state 传参(非组件内的 state)

优势在于它不会在路由中体现传参,必须要传一个对象

<Link key={news.id } to={{pathname: '/news/detail', state:{id:news.id,title:news.title} }}>{news.title }</Link><Route path='/home/news/detail' component={Detail}></Route>// Detail 中可以直接通过 props.location.state 拿到数据

Tip:直接刷新页面,state 不会丢失,因为通过 history.location 记录着呢(但是HashRouter一刷新就会丢失,因为它没有用history记录)

1.6.2.9. replace 和 push

// 开启 replace 模式<Link replace={true } to={{pathname: '/news/detail', state: {id: news.id, title: news.title}}}>{news.title }</Link>

1.6.2.10. 编程式导航

this.props.history.replace('xxx') // params、searchthis.props.history.replace('xxx',{id,title}) // state 编程式导航

1.6.2.11. withRouter

把一般组件也用上 router 的 api

import {withRouter } from 'react-router-dom'// 直接给 withRouter 传递一个普通组件即可,返回一个新组建export default withRouter(Header);

1.6.2.12. BrowserRouter 和 HashRouter 区别

底层原理不一样:

BrowserRouter 底层调用H5的 history API,不兼容IE9及其一下HashRouter 因为含有 #(#后面的参数是不会发送到后端的),所以调用的时候能形成历史记录

1.7. redux

和 vuex 一样,用于集中式状态管理;能不用就不用

1.7.1. 原理图

流程:

1.组件通过store.getState()获取初始化状态;store 自动调用 reducer 返回初始化状态

2.组件通过store.dispatch({ type: 'xxx', data: 'xxx'})转发给 store,store 调用 reducer 更新状态

3.组件监听 store 里面的状态发生了变化,就重新 render 获取最新结果

1.7.2. 完整用法

action 是 Object(称为同步 action) 或 function(异步 action:因为只有函数体能够开启异步任务,store 会调用这个函数,拿到执行结果)

npm i redux-thunk # 用于支持异步 action

异步 action 不是必须要用的,看是否自己想在组件里发送请求

在 redux 目录 创建store.js、count_action.js、constant.js、count_reducer.js

// store.jsimport {createStore, applyMiddleware, combineReducers } from "redux"; // 专用于创建 storeimport thunk from 'redux-thunk' // 用于支持异步 actionimport countReducer from './reducers/count' // 需要引入 reducer 来创建import personReducer from "./reducers/person";const reducer = combineReducers({sum:countReducer,personArr:personReducer}) // 合并 reducer,才能在 state 里存下它们各自的数据const store = createStore(reducer, applyMiddleware(thunk)) // 创建 store 的时候,需要使用这个中间件export default store // 暴露 store

// constant.js// 定义 redux 中所需要使用的常量,防止写错export const INCREMENT = 'increment'export const DECREMENT = 'decrement'export const ADD_USER = 'add_user'

// count_reducer.jsimport {DECREMENT, INCREMENT} from "./constant";const initState = 0export default function sumReducer(preState = initState, action){// reducer 可以接参数,说明是个函数。主要用于初始化和更新 storeconst {type,data} = actionswitch (type) {case INCREMENT:return preState+datacase DECREMENT:return preState-datadefault:return preState}}

// count_action.jsimport {DECREMENT, INCREMENT} from "./constant";export const createIncrementAction = data => ({type: INCREMENT, data})export const createDecrementAction = data => ({type: DECREMENT, data})export const createIncrementAsyncAction = (data, time) => {// 异步 action,返回一个函数,默认是 store 来执行,因此会传入 dispatchreturn (dispatch) => {setTimeout(() => {dispatch(createIncrementAction(data))}, time)}}

组件中通过如下方式调用

import React, {Component} from 'react';import './index.css'import store from '../../redux/store'import {createIncrementAction,createIncrementAsyncAction} from '../../redux/count_action'class ReduxBasic extends Component {selRef = React.createRef()render() {return (<div>{/* getState 能够获取到初始值或最新值(此时reducer未合并,getState就能拿到里面的值) */}<h1>sum: {store.getState()}</h1> <select ref={this.selRef}><option value={1}>1</option><option value={2}>2</option><option value={3}>3</option></select>&nbsp;<button onClick={this.add}>+</button>&nbsp;<button onClick={this.addAsync}>increment async</button></div>);}componentDidMount() {// 监听 store 数据发送改变,调用 renderstore.subscribe(() => {console.log('数据发生改变时触发')this.setState({})})}add = () => {const val = parseInt(this.selRef.current.value)store.dispatch(createIncrementAction(val))}addAsync = () => {const val = parseInt(this.selRef.current.value)store.dispatch(createIncrementAsyncAction(val, 3000))}}export default ReduxBasic;

1.7.3. react-redux

1.7.3.1. 基本使用

UI 组件不能有任何 redux 的 api,只负责页面的呈现、交互;容器组件负责和 redux 通信,将结果交给 UI 组件如何创建一个容器组件呢?==> connect(mapStateToProps,mapDispatchToProps)(UI组件),参数一是映射 state,返回一个对象;参数二是映射操作状态的方法,返回一个对象容器组件的 store 靠父组件通过 props 传给它reducer/ 文件夹里还可新建 index.js,用于合并所有的 reducers,再暴露出去

Tip:mapDispatchToProps 也可以是一个对象(公司常用)react-redux 自动调用了

1.7.3.2. 优化版

之前的 redux 目录中的文件不用变;新建src/containers/Count/index.jsx,内容如下:

import {connect } from 'react-redux' // 用于连接 UI 和 reduximport {createIncrementAction, createIncrementAsyncAction } from '../../redux/sum_action'import React, {Component} from 'react';// 创建容器组件所连接的 UI 组件class CountUI extends Component {selRef = React.createRef()render() {return (<div><h1>sum: {this.props.sum}</h1><p>count 中读取到的人的个数:{this.props.personArr.length}</p><select ref={this.selRef}><option value={1}>1</option><option value={2}>2</option><option value={3}>3</option></select>&nbsp;<button onClick={this.add}>+</button>&nbsp;<button onClick={this.addAsync}>increment async</button></div>);}add = () => {const val = parseInt(this.selRef.current.value)this.props.createIncrementAction(val)}addAsync = () => {const val = parseInt(this.selRef.current.value)this.props.createIncrementAsyncAction(val,3000)}}// 创建并暴露一个 CountContainer 组件// 因为 reducer 合并了,这里要想拿到值,就得通过 state.xxxexport default connect((state) => ({sum: state.sum, personArr: state.personArr}),{createIncrementAction,createIncrementAsyncAction})(CountUI)// 相当于 <CountUI sum={1}/> 这里是传值给 UI,因此要写一个对象才能有 key-value// 因为是 react-redux 调用这个函数,因此它把 const state = store.getState() 的结果 state作为参数传进去了// 参数二就是传一些方法的键值对给 CountUI// 因为 react-redux 能够自动调用 dispatch,因此这里我们传 action 就可以了

import CountContainer from "./containers/Count";// app.js 引入这个 container<CountContainer />

import {Provider } from "react-redux";import store from "./redux/store";// app 里所有用到 store 的组件都能拿到,就不需要每写一个组件传一个 store 进去了ReactDOM.render(<Provider store={store}><App /></Provider>,document.getElementById('root'));

1.7.3.3. 纯函数

是一类函数:只要同样的输入(实参),就会得到同样的输出(返回值)

1.不能改写参数数据

function test(a){a=10 // ERROR:改写了参数}

2.不能产生任何副作用,如网络请求、文件读写啥的(反正不靠谱的都不行)

3.不能调用 Date.now 或 Math.random 等不纯的方法(每次运行结果都不一样)

1.7.3.4. redux 开发者工具

1.谷歌网上应用商店 搜索 redux

2.安装三方包

npm i redux-devtools-extension

3.修改 store.js

import {composeWithDevTools } from 'redux-devtools-extension'const store = createStore(reducer, composeWithDevTools(applyMiddleware(thunk)))

1.7.3.5. 服务器运行

npm i serve -gserve build # build 是打包后的文件夹

1.8. 扩展知识

1.8.1. setState

setState 是个同步方法,但是引起的后续动作是异步的

一般来说,如果不依赖原来的状态,就用对象式;依赖的话,就用函数式

// 函数式 setStateadd = () => {// prop 可用可不用this.setState((state,prop) => ({sum: state.sum + 1 }))}

1.8.2. 路由懒加载

// Suspense 主要用于当请求第一个懒加载路由特别慢的时候,呈现一个默认的路由,以免白屏import {lazy, Suspense} from 'react'const Home = lazy(()=>import('./Home'))class App extends ponent{render() {return (// ...<Suspense fallback={<h1>loading....</h1>}>// 注册路由<Route path='/home' component='Home'></Route></Suspense>)}}

1.8.3. hooks

hooks 可以让用户在函数式组件使用 state,以及其他的 react 特性(如生命周期函数)

function App() {// 调用 1+N 次// useState 返回两个值,一个 state,一个操作 state 的方法const [state, setState] = React.useState({sum: 0, name: 'default'}) // 第一次调,就缓存了state,不会因为再次调用而覆盖成0function add() {setState({...state, sum: state.sum + 1})}function changeName() {setState({...state,name: 'Tom'})}return (<div className="App"><h1>当前 sum: {state.sum}</h1><h1>当前 name: {state.name}</h1><button onClick={add}>+1</button><button onClick={changeName}>点我改名字</button></div>)}export default App;

function App() {// 调用 1+N 次const [state, setState] = React.useState({sum: 0 })console.log('state:', state)// 1.不传入第二个参数的话,就相当于 didUpdate,会执行 1+N 次(监测 state 所有状态)// 2.传入第二个参数,就相当于给它说只监视某个值,一旦这个值发生改变就会触发这个回调函数// 3.第二个参数是 []时,相当于 didMount,会执行 1 次// 4.useEffect 返回值相当于 componentWillUnmount 函数React.useEffect(() => {// 让函数式组件有生命周期const timer = setInterval(() => {setState((state) => ({sum: state.sum + 1 }))}, 1000)return () => {window.clearInterval(timer)}}, [])return (<div className="App"><h1>当前 sum: {state.sum }</h1><button onClick={destroy }>卸载组件</button></div>)function destroy() {// 卸载组件的回调ReactDOM.unmountComponentAtNode(document.getElementById('root'))}}export default App;

function App() {// 调用 1+N 次let inputRef = React.useRef() // React.createRef 也可以return (<div className="App"><input type='text' ref={inputRef}/><button onClick={showName}>show name</button></div>)function showName() {alert(inputRef.current.value)}}export default App;

1.8.4. Fragment

用于包裹标签,这样项目就不会生成多余的 div 标签了,最多添加一个key属性,其他属性会丢失

// Demo.jsximport {Fragment} from "react";// 它起到一个包裹的作用,但是不会生成额外的 div 标签,不然就只有用 div 来进行包裹// 也可以用 <>空标签包裹,但是它不能传 key 属性了</><Fragment><input type='text'/><input type='text'/></Fragment>

1.8.5. Context

适用于祖组件和后代组件之间通信【一般用于封装插件】

import React, {Component} from 'react';const MyContext = React.createContext() // 1.首先创建一个 contextconst {Consumer } = MyContextclass GrandFather extends Component {state = {username: 'Tom'}render() {return (<div><h1>我是 GrandFather 组件</h1>{/* 2.通过 Provider 提供 value,就可以让 Father 所有的后代组件都可使用 username */}<MyContext.Provider value={{username: this.state.username}}><Father/></MyContext.Provider></div>);}}class Father extends Component {render() {return (<div><Son/></div>);}}class Son extends Component {// 3.要想使用,就得声明,才能够从 this.context 获取static contextType = MyContextrender() {return (<div><h3>我是 son 组件,我接受到的 username:{this.context.username}</h3></div>);}}/*function Son(){return (<div><h3>我是 son 组件,我接受到的 username:<Consumer>{value => value.username}</Consumer></h3></div>);}*/export default GrandFather;

1.8.6. 组件优化

Component 存在两个问题:

只要调用 setState(),即使不修改状态(传入空对象),还是会重新执行 render()只要当前组件 render(),那么子组件(即使没通过 props 使用父组件任何东西)就会重新 render(),效率低下

原因:shouldComponentUpdate() 一直默认返回的是 true

高效的做法是:只有 state 或 props 数据发生改变才调用 render()

写法:

import {PureComponent} from 'react'class App extends PureComponent{// 这个类重写了 shouldComponentUpdate(),react 又是进行浅对比,就会存在一个小问题state = {username: 'yuwan'}changeName = () => {const obj = this.stateobj.username = 'xxx' // this.state === obj ==> truethis.setState(obj) // 这里是无法更新的,因为 obj 地址还是和 this.state 相同}}

1.8.7. render props

解释:这和 vue 的插槽作用差不多,预置某个位置,编写代码的时候,再填充进去

import React, {Component} from 'react';class App extends Component {render() {return (<div>{/* 2.编码的时候,确定是父子关系 */}<Parent render={(name) => <Child name={name}/>}/></div>);}}export default App;class Parent extends Component {state = {name: 'yuwan'}render() {return (<div><h2>我是父组件</h2>{/* 1.执行 render 那个函数,就能拿到 Child 组件【这就是 vue 插槽技术】 */}{this.props.render(this.state.name) }</div>)}}class Child extends Component {render() {return (<h3>我是子组件,收到的 name = {this.props.name}</h3>)}}

1.8.8. 错误边界

解释:子组件出问题就在子组件中显示,而不要影响到整个应用的运行

方式:在容易发送错误的组件的父组件进行处理

注意点:

在生产环境才会有效,开发环境效果看不出来只会捕获生命周期函数的逻辑,若果是自定义函数是不得行的

import React, {Component} from 'react';class App extends Component {render() {return (<Parent/>);}}export default App;class Parent extends Component {state = {hasError:false}static getDerivedStateFromError(err){// 一旦子组件发生错误,就会在这里进行处理,会自动更新 statereturn {hasError:true}}componentDidCatch(error, errorInfo) {console.log(error) // 这里可以统计出错次数,反馈给服务器,以便于调试}render() {return (<div><h2>我是父组件</h2>{this.state.hasError?<h3>糟糕,出问题了!</h3>: <Child/>}</div>)}}class Child extends Component {state = {userList:''}render() {return (<div><h3>我是子组件</h3>{this.state.userList.map(user=>{return <p key={user.id}>{user.name}</p>})}</div>)}}

1.8.9. 组建通信(总结)

props(适用于父子组件)消息订阅-发布:如 pub-sub(适用于兄弟组件、祖孙组件)集中式管理:如 redux(适用于兄弟组件、祖孙组件)context:如 生产者-消费者(适用于插件封装)

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。