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.创建虚拟 DOM
const 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 是创建真实 DOM
React.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) // undefined
return <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 React.Component {
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 React.Component{
constructor(props) {
super(props); // 按照上面所说,有继承且写了 constructor 就必须要调用 super
this.state = { isHot: true } // 初始化 state。当前组件的 state 是一个对象,便于保存多种类型的数据
}
render() {
const { isHot } = this.state
return <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 = name
this.age = age
}
speak() { // 默认是开启了局部严格模式
console.log(this)
}
}
const p1 = new Person('tom', 19)
p1.speak() // Person {name: 'tom', age: 19}
const varSpeak = p1.speak
varSpeak() // undefined
</script>
1.3.2.3. React 绑定事件
如下是演示,我们在不知道 react 怎么绑定事件的过程:首先想到的是传函数调用结果 -> 传表达式调用结果 -> 传函数名 -> 解决 this -> this.xxx 调用
<script>
class MyApp extends React.Component{
// ...
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 实例对象的 state
class MyApp extends React.Component{
// ...
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 React.Component{
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 React.Component{
render() {
const { name, age, sex } = this.props
return (
<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) // 6
sum(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 React.Component{
render() {
const { name, age, sex, speak } = this.props
return (
<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 React.Component{
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 React.Component{
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} = props
return <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 React.Component{
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 React.Component{
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 React.Component{
idRef = React.createRef()
pwdRef = React.createRef()
render() {
// 默认表单提交 get 请求,并携带所有的 k-v 如:https://www.baidu.com/?username=admin&password=123
return (
<div>
<h1>欢迎登录</h1>
<form action="https://www.baidu.com" 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.value
const password = this.pwdRef.current.value
alert(`账号:${username},密码:${password}`)
e.preventDefault() // 阻止表单默认提交行为
}
clearInput = () => {
this.idRef.current.value = ''
this.pwdRef.current.value = ''
}
}
受控组件:随着用户操作,就把值绑定到 state 中 【推荐使用,毕竟不需要 ref 】
class Login extends React.Component {
state = {
username: '',
password: ''
}
render() {
// 默认表单提交 get 请求,并携带所有的 k-v 如:https://www.baidu.com/?username=admin&password=123
return (
<div>
<h1>欢迎登录</h1>
<form action="https://www.baidu.com" 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.state
const {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() 触发 — 初次渲染
- constructor
- componentWillMount
- render【必用】
- componentDidMount【常用于初始化,如开启定时器、发送请求、订阅消息】
2.更新阶段:由 this.setState() 或父组件重新 render() 触发
- shouldComponentUpdate
- componentWillUpdate
- render
- componentDidUpdate
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 React.Component {
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.state
this.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-app
create-react-app my-app # 创建一个 react 项目
cd my-app
npm start
npm 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 React.Component{ render(){ // 这里调用即可 this.props.addCallback(3,4) } } - 两个有用的包
npm i nanoid # 生成 UUID npm 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
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 React.Component{
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 React.Component{
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.css - link 下 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、search
this.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.js
import { createStore, applyMiddleware, combineReducers } from "redux"; // 专用于创建 store
import thunk from 'redux-thunk' // 用于支持异步 action
import 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.js
import {DECREMENT, INCREMENT} from "./constant";
const initState = 0
export default function sumReducer(preState = initState, action){ // reducer 可以接参数,说明是个函数。主要用于初始化和更新 store
const {type,data} = action
switch (type) {
case INCREMENT:
return preState+data
case DECREMENT:
return preState-data
default:
return preState
}
}
// count_action.js
import {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 来执行,因此会传入 dispatch
return (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>
<button onClick={this.add}>+</button>
<button onClick={this.addAsync}>increment async</button>
</div>
);
}
componentDidMount() {
// 监听 store 数据发送改变,调用 render
store.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 和 redux
import { 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>
<button onClick={this.add}>+</button>
<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.xxx
export 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 -g
serve build # build 是打包后的文件夹
1.8. 扩展知识
1.8.1. setState
-
setState 是个同步方法,但是引起的后续动作是异步的
-
一般来说,如果不依赖原来的状态,就用
对象式;依赖的话,就用函数式

// 函数式 setState
add = () => { // 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 React.Component{
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,不会因为再次调用而覆盖成0
function 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.jsx
import {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.首先创建一个 context
const { Consumer } = MyContext
class 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 = MyContext
render() {
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.state
obj.username = 'xxx' // this.state === obj ==> true
this.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){
// 一旦子组件发生错误,就会在这里进行处理,会自动更新 state
return {
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:如 生产者-消费者(适用于插件封装)
更多推荐
所有评论(0)