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

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 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>&nbsp;
                <button onClick={this.add}>+</button>
                &nbsp;
                <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>&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.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:如 生产者-消费者(适用于插件封装)
Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐