React 面向组件编程

React 面向组件编程

尚硅谷 2021 版 React 技术全家桶全套完整版(零基础入门到精通/男神天禹老师亲授)

React Developer Tools 开发工具

函数式组件

执行了 ReactDOM.render() 之后发生了什么?

  1. React 解析组件标签,找到了 MyCompoent 组件
  2. 发现组件是使用函数定义的,随后调用该函数,将返回的虚拟 DOM 转换为真实 DOM,呈现到页面上
function MyComponent() {
  console.log(this) // 此处this是undefined,因为babel编译后开启了严格模式
  return <h2>函数式组件</h2>
}

ReactDOM.render(<MyComponent />, document.getElementById('test'))

类组件

  1. 类中的构造器不是必须写的,要对实例进行一些初始化的操作,如添加指定属性时才写
  2. 如果 A 类继承了 B 类,且 A 类中写了构造器,那么 A 类构造器中的 super 是必须要调用的
  3. 类中所定义的方法,都是放在类的原型对象上,供实例去使用

执行了 ReactDOM.render() 之后发生了什么?

  1. React 解析组件标签,找到了 MyCompoent 组件
  2. 发现组件是使用类定义的,随后 new 出来该类的实例,并通过该实例调用原型上的 render 方法
  3. 将 render 返回的虚拟 DOM 转为真实 DOM,随后呈现在页面中
class MyComponent extends React.Component {
  render() {
    return <h2>类组件</h2>
  }
}

ReactDOM.render(<MyComponent />, document.getElementById('test'))

原生 JS 铺垫

  • 原生 JS 事件绑定
<button id="btn1">按钮1</button>
<button id="btn2">按钮2</button>
<button onclick="demo()">按钮3</button>

<script type="text/javascript">
  const btn1 = document.getElementById('btn1')
  btn1.addEventListener('click', () => {
      
      
    alert('按钮1被点击了')
  })
  const btn2 = document.getElementById('btn2')
  btn2.onclick = () => {
      
      
    alert('按钮2被点击了')
  }
  function demo() {
      
      
    alert('按钮3被点击了')
  }
</script>
  • 原生 JS 中的 this 指向
class Person {
    
    
  constructor(name, age) {
    
    
    this.name = name
    this.age = age
  }
  study() {
    
    
    console.log(this)
  }
}
const p1 = new Person('tom', 18)
p1.study() // Person {name: "tom", age: 18}
const x = p1.study
x() // undefined
  • 原生 JS 中的扩展运算符
// 构造字面量对象时使用展开语法
let p = {
    
     name: 'tom', age: 18 }
let p2 = {
    
     ...p }
// console.log(...p) // Found non-callable @@iterator
let p3 = {
    
     ...p, name: 'jack' } // 合并
  • 对象相关的知识
let a = 'name'
let obj = {
    
     name: 'tom' }
console.log(obj[a]) // 'tom'

高阶函数:如果一个函数符合下面 2 个规范中的任何一个,那该函数就是高阶函数

  1. 若 A 函数接收的参数是一个函数,那么 A 就可以称之为高阶函数
  2. 若 A 函数调用的返回值依然是一个函数,那么 A 就可以称之为高阶函数
  • 常见的高阶函数:Promise、setTimeout、arr.map() 等等

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

function sum(a) {
    
    
  return b => {
    
    
    return c => {
    
    
      return a + b + c
    }
  }
}

组件实例三大属性 state

  1. state 值是对象(可包含多个 key-value 组合)
  2. 通过更新组件的 state 来更新对应的页面显示(重新渲染组件)

注意 this 问题:

  1. 由于 changeWeather 是作为 onClick 的回调,所以不是通过实例调用的,是直接调用,所以 this 应该为 window(非严格模式下)
  2. 但类中的方法默认开启了局部的严格模式,所以执行 changeWeather 时,如不使用 bind 更改 this 执行,changeWeather 中的 this 为 undefined
  3. 状态 state 不可以直接更改,状态必须通过 setState 进行更新,且更新是一种合并,不是替换

组件自定义方法中的 this 问题(undefined):

  1. <button onClick={(ev) => {this.handle(ev)} }>click</button>
  2. <button onClick={this.handle.bind(this)}>click</button>
  3. handle = (ev) => { console.log(this, ev) } (保证 this 是当前类的实例)
class Weather extends React.Component {
  constructor(props) {
    super(props)
    this.state = { isHot: true, wind: '微风' }
    // 解决changeWeather中this指向问题
    this.changeWeather = this.changeWeather.bind(this)
  }
  render() {
    const { isHot, wind } = this.state
    return (
      <h2 onClick={this.changeWeather}>
        今天天气很{isHot ? '炎热' : '凉爽'},{wind}
      </h2>
    )
  }
  changeWeather() {
    // state必须通过setState进行更新
    this.setState({ isHot: !this.state.isHot })
  }
}
  • 简写 state
class Weather extends React.Component {
  state = { isHot: true, wind: '微风' }
  render() {
    const { isHot, wind } = this.state
    return (
      <h1 onClick={this.changeWeather}>
        今天天气很{isHot ? '炎热' : '凉爽'},{wind}
      </h1>
    )
  }
  // 赋值语句+箭头函数
  changeWeather = () => {
    const isHot = this.state.isHot
    this.setState({ isHot: !isHot })
  }
}

组件实例三大属性 props

  • 通过标签属性从组件外向组件内传递变化的数据

对 props 进行限制

  • 使用 类.propTypes
  • React 15.5 版本之前 name: React.PropTypes.string ,如果一直在 React 核心对象上加属性,导致 React 这个核心对象很大
  • React 16 版本之后,需要引入 prop-types 库,用于对组件标签属性进行限制

注意:

  1. props 是只读的,this.props.name = 'xxx' 会报错
  2. React.Component 子类实现构造函数时,应在其它语句前调用 super(props)
  3. 函数式组件不能用 state、refs(不使用 Hooks),但可以使用 props
class Person extends React.Component {
  state = {}
  // 对标签属性进行类型、必要性限制
  static propTypes = {
    name: PropTypes.string.isRequired,
    age: PropTypes.number,
    sex: PropTypes.string,
    speak: PropTypes.func,
  }
  // 指定默认标签属性值
  static defaultProps = {
    sex: '不男不女',
    age: 18,
  }

  render() {
    const { name, age, sex } = this.props
    return (
      <ul>
        <li>姓名:{name}</li>
        <li>年龄:{age + 1}</li>
        <li>性别:{sex}</li>
      </ul>
    )
  }
}

const p = { name: 'tom', speak: () => {} }
ReactDOM.render(<Person {...p} />, document.getElementById('test'))
  • 函数式组件使用 props(函数式组件可以接收参数)
function Person(props) {
  const { name, age, sex } = props
  return (
    <ul>
      <li>姓名:{name}</li>
      <li>年龄:{age + 1}</li>
      <li>性别:{sex}</li>
    </ul>
  )
}

Person.propTypes = {
  name: PropTypes.string.isRequired,
  age: PropTypes.number,
  sex: PropTypes.string,
  speak: PropTypes.func,
}
Person.defaultProps = {
  sex: '不男不女',
  age: 18,
}

const p = { name: 'tom', speak: () => {} }
ReactDOM.render(<Person {...p} />, document.getElementById('test'))

组件实例三大属性 refs

  • 字符串形式的 ref ref="input" (现在不推荐使用,效率不高)
  • 函数形式的 ref ref={c => this.input1 = c} (回调函数)
  • createRef 创建 ref 容器【React 最推荐的写法】

在条件允许的情况下,尽可能不使用 ref="input"

关于回调 refs 的说明:

  • 如果 ref 回调函数是以内联函数方式定义的,在更新过程中它会执行两次,第一次传入参数 null,第二次传入参数 DOM 元素

  • 因为在每次渲染时会创建一个新的函数实例,React 情况旧的 ref 并设置新的【无关紧要】

    如需解决,可以把内联函数放到一个函数里面,这样每次调用都不是一个新的函数,也就不会执行两次了(定义成 class 的绑定)

class Demo extends React.Component {
  state = { isHot: false }
  changeWeather = () => {
    this.setState({ isHot: !this.state.isHot })
  }
  saveInput = c => {
    this.input1 = c
    console.log('@', c)
  }
  render() {
    const { isHot } = this.state
    return (
      <div>
        <h2>今天天气很{isHot ? '炎热' : '凉爽'}</h2>
        {/*<input
          ref={c => {
            this.input1 = c
            console.log('@', c)
          }}
          type="text"
        />*/}
        <input ref={this.saveInput} type="text" />
        <button onClick={this.changeWeather}>切换天气</button>
      </div>
    )
  }
}

React.createRef 调用后可以返回一个容器,该容器可以存储被 ref 所标识的节点,该容器是"专人专用"的

class Demo extends React.Component {
  myRef = React.createRef()
  showData = () => {
    console.log(this.myRef.current.value)
  }
  render() {
    return (
      <div>
        <input ref={this.myRef} type="text" />
        <button onClick={this.showData}>提示左侧数据</button>
      </div>
    )
  }
}

事件处理

  1. 通过 onXxx 属性指定事件处理函数(注意大小写)

    React 使用的是自定义(合成)事件,而不是使用的原生 DOM 事件 --> 为了更好的兼容性

    React 中的事件是通过事件委托方式处理的(委托给组件最外层元素) --> 为了高效

  2. 通过 event.target 得到发生事件的 DOM 元素对象

    不要过度使用 refs,发生事件的元素和操作的元素相同,可以通过 event.target 获取

class Demo extends React.Component {
  myRef = React.createRef()
  myRef2 = React.createRef()
  showData = () => {
    console.log(this.myRef.current.value)
  }
  showData2 = event => {
    console.log(event.target.value)
  }
  render() {
    return (
      <div>
        <input ref={this.myRef} type="text" placeholder="点击按钮提示数据" />
        <button onClick={this.showData}>提示左侧数据</button>
        <input onBlur={this.showData2} type="text" placeholder="失去焦点提示数据" />
      </div>
    )
  }
}

受控组件与非受控组件

表单的组件分类:

  1. 受控组件

    页面中表单元素随着输入将其维护到状态 state 里去,需要用时直接去状态中取即可(Vue 中双向数据绑定)

class Login extends React.Component {
  handleSubmit = event => {
    event.preventDefault()
    const { username, password } = this
    console.log(username.value, password.value)
  }
  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        用户名:
        <input ref={c => (this.username = c)} type="text" name="username" />
        密码:
        <input ref={c => (this.password = c)} type="password" name="password" />
        <button>登录</button>
      </form>
    )
  }
}
  1. 非受控组件

    页面中表单元素现用现取 ref

    如果不使用函数柯里化,可以这么写

    <input onChange={e => this.saveFormData('username', e)} />

class Login extends React.Component {
  state = {
    username: '',
    password: '',
  }
  handleSubmit = event => {
    event.preventDefault()
    const { username, password } = this.state
    console.log(username, password)
  }
  saveFormData = dataType => {
    return event => {
      this.setState({ [dataType]: event.target.value })
    }
  }
  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        用户名:
        <input onChange={this.saveFormData('username')} type="text" name="username" />
        密码:
        <input onChange={this.saveFormData('password')} type="password" name="password" />
        <button>登录</button>
      </form>
    )
  }
}

组件生命周期(旧)

Clock 组件第一次被渲染到 DOM 中的时候,就为其设置一个定时器,在 React 中被称为挂载(mount)

当 DOM 中Clock 组件被删除的时候,应该清除定时器,在 React 中被称为卸载(unmount)

  1. 组件从创建到死亡它会经历一些特定的阶段
  2. React 组件中包含一系列钩子函数(生命周期回调函数),会在特定的时候调用
  3. 我们在定义组件时,会在特定的生命周期回调函数中做特定的工作

生命周期流程图(旧)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iPbvOddi-1622542494199)(https://gitee.com/lilyn/pic/raw/master/company-img/2_react生命周期(旧)].png)

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

    • constructor()

    • componentWillMount()

    • render()

    • componentDidMount()

      常用:一般在这个钩子做一些初始化的事,例如:开启定时器、

      发送网络请求、订阅消息

  2. **更新阶段:**由组件内部 this.setState() 或父组件重新 render

    • shouldComponentUpdate()

    • componentWillUpdate()

    • render()

      初始化渲染或更新渲染调用

    • componentDidUpdate()

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

    • componentWillUnmount()

      常用:一般在这个钩子中做一些收尾的事,例如:清理定时器、取消订阅消息

class Count extends React.Component {
  constructor(props) {
    console.log('Count---constructor')
    super(props)
    this.state = { count: 0 }
  }
  add = () => {
    const { count } = this.state
    this.setState({ count: count + 1 })
  }
  death = () => {
    ReactDOM.unmountComponentAtNode(document.getElementById('test'))
  }
  force = () => {
    this.forceUpdate()
  }
  render() {
    console.log('Count---render')
    const { count } = this.state
    return (
      <div>
        <h2>当前求和为:{count}</h2>
        <button onClick={this.add}>更改状态 +1</button>
        <button onClick={this.death}>卸载组件</button>
        <button onClick={this.force}>强制更新</button>
      </div>
    )
  }
  //组件将要挂载的钩子
  componentWillMount() {
    console.log('Count---componentWillMount')
  }
  //组件挂载完毕的钩子
  componentDidMount() {
    console.log('Count---componentDidMount')
  }
  //控制组件更新的 阀门
  shouldComponentUpdate() {
    console.log('Count---shouldComponentUpdate')
    return true
  }
  //组件将要更新的钩子
  componentWillUpdate() {
    console.log('Count---componentWillUpdate')
  }
  //组件更新完毕的钩子
  componentDidUpdate() {
    console.log('Count---componentDidUpdate')
  }
  //组件将要卸载的钩子
  componentWillUnmount() {
    console.log('Count---componentWillUnmount')
  }
}

注意:componentWillReceiveProps 组件将要接收新的 props 的钩子(第一次初始化不算)

//父组件A
class A extends React.Component {
  state = { carName: '奔驰' }
  changeCar = () => {
    this.setState({ carName: '奥拓' })
  }
  render() {
    return (
      <div>
        <div>我是A组件</div>
        <button onClick={this.changeCar}>换车</button>
        <B carName={this.state.carName} />
      </div>
    )
  }
}

//子组件B
class B extends React.Component {
  componentWillReceiveProps(props) {
    console.log('B---componentWillReceiveProps', props)
  }
  shouldComponentUpdate() {
    console.log('B---shouldComponentUpdate')
    return true
  }
  componentWillUpdate() {
    console.log('B---componentWillUpdate')
  }
  componentDidUpdate() {
    console.log('B---componentDidUpdate')
  }
  render() {
    console.log('B---render')
    return <div>我是B组件,接收到的车是:{this.props.carName}</div>
  }
}

组件生命周期(新)

生命周期流程图(新)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rjVWYiit-1622542494202)(https://gitee.com/lilyn/pic/raw/master/company-img/3_react生命周期(新)].png)

  1. 新版本废弃 3 个钩子:需要给 componentWillMountcomponentWillUpdatecomponentWillReceiveProps,都需要加 UNSAFE_。所有带 will(除了 componentWillUnmount),都需要加 UNSAFE_

    异步渲染之更新

  2. 新版本新增 2 个钩子:getDerivedStateFromProps (state 的值在任何时候都取决于 props)、getSnapshotBeforeUpdate

    static getDerivedStateFromProps()

    getSnapshotBeforeUpdate()

class Count extends React.Component {
  constructor(props) {
    console.log('Count---constructor')
    super(props)
    this.state = { count: 0 }
  }
  add = () => {
    const { count } = this.state
    this.setState({ count: count + 1 })
  }
  death = () => {
    ReactDOM.unmountComponentAtNode(document.getElementById('test'))
  }
  force = () => {
    this.forceUpdate()
  }
  render() {
    console.log('Count---render')
    const { count } = this.state
    return (
      <div>
        <h2>当前求和为:{count}</h2>
        <button onClick={this.add}>更改状态 +1</button>
        <button onClick={this.death}>卸载组件</button>
        <button onClick={this.force}>强制更新</button>
      </div>
    )
  }
  // 若state的值在任何时候都取决于props,那么可以使用这个钩子
  static getDerivedStateFromProps(props, state) {
    console.log('Count---getDerivedStateFromProps', props, state)
    return null
  }
  // 在更新之前获取快照
  getSnapshotBeforeUpdate() {
    console.log('Count---getSnapshotBeforeUpdate')
    return 'bird'
  }
  //组件挂载完毕的钩子
  componentDidMount() {
    console.log('Count---componentDidMount')
  }
  //控制组件更新的 阀门
  shouldComponentUpdate() {
    console.log('Count---shouldComponentUpdate')
    return true
  }
  //组件更新完毕的钩子
  componentDidUpdate(preProps, preState, snapshotVal) {
    console.log('Count---componentDidUpdate', preProps, preState, snapshotVal)
  }
  //组件将要卸载的钩子
  componentWillUnmount() {
    console.log('Count---componentWillUnmount')
  }
}

getSnapshotBeforeUpdate

  • 在完成更新之前获取一些信息
class NewsList extends React.Component {
  state = { newsArr: [] }
  componentDidMount() {
    setInterval(() => {
      const { newsArr } = this.state
      const news = '新闻' + (newsArr.length + 1)
      this.setState({ newsArr: [news, ...newsArr] })
    }, 1000)
  }
  getSnapshotBeforeUpdate() {
    return this.refs.list.scrollHeight
  }
  componentDidUpdate(preProps, preState, height) {
    this.refs.list.scrollTop += this.refs.list.scrollHeight - height
  }
  render() {
    return (
      <div className="list" ref="list">
        {this.state.newsArr.map((n, index) => {
          return (
            <div key={index} className="news">
              {n}
            </div>
          )
        })}
      </div>
    )
  }
}

DOM 的 Diffing 算法

注意:Diff 最小的力度是标签(节点)

面试常问:

  1. React/Vue 中的 key 有什么作用(key 内部原理)
  2. 为什么遍历列表时,key 最好不用 index

虚拟 DOM 中 key 的作用:

  1. 简单来说:key 是虚拟 DOM 对象的标识,在更新显示 key 起着极其重要的作用

  2. 详细的说:当状态中的数据发生变化时,React 会 新数据 生成 新的虚拟 DOM ,随后 React 进行 新的虚拟 DOM旧虚拟 DOMdiff 比较,比较规则如下:

    • 旧虚拟 DOM 中找到了与新虚拟 DOM 相同的 key

      若虚拟 DOM 中内容没变,直接使用之前的真实 DOM

      若虚拟 DOM 中内容变了,则生成新的真实 DOM,随后替换掉页面中之前的真实 DOM

    • 旧虚拟 DOM 中未找到与新虚拟 DOM 相同的 key

      根据数据创建真实 DOM,随后渲染到页面

用 index 作为 key 可能会引发的问题:

  1. 若对数据进行:逆序添加、逆序删除等破坏顺序操作

    会产生没有必要的真实 DOM 更新(页面效果没问题,但效率低)

  2. 如果结构中还包含输入类的 DOM

    会产生错误 DOM 更新(页面有问题)

  3. 注意:如果不存在对数据的逆序添加、逆序删除等破坏顺序操作

    禁用于渲染列表用于展示,使用 index 作为 key 是没有问题的

开发中如何选择 key:

  1. 最好使用每条数据的唯一标识作为 key,比如 id、手机号、身份证号、学号等唯一值
  2. 如果确定只是简单的展示数据,用 index 也是可以的
/*
* 使用index索引作为key
初始数据:
  { id: 1, name: 'bird', age: 18 },
  { id: 2, name: 'dog', age: 19 },
初始的虚拟DOM:
  <li key=0>bird---18<input type="text" /></li>
  <li key=1>dog---18<input type="text" /></li>
更新后的数据:
  { id: 3, name: 'cat', age: 20 },
  { id: 1, name: 'bird', age: 18 },
  { id: 2, name: 'dog', age: 19 },
更新后的虚拟DOM(出现数据错乱问题)
  <li key=0>cat---20<input type="text" /></li>
  <li key=1>bird---18<input type="text" /></li>
  <li key=2>dog---18<input type="text" /></li>
*/

/*
* 使用id唯一标识作为key
初始数据:
  { id: 1, name: 'bird', age: 18 },
  { id: 2, name: 'dog', age: 19 },
初始的虚拟DOM:
  <li key=1>bird---18<input type="text" /></li>
  <li key=2>dog---18<input type="text" /></li>
更新后的数据:
  { id: 3, name: 'cat', age: 20 },
  { id: 1, name: 'bird', age: 18 },
  { id: 2, name: 'dog', age: 19 },
更新后的虚拟DOM
  <li key=3>cat---20<input type="text" /></li>
  <li key=1>bird---18<input type="text" /></li>
  <li key=2>dog---18<input type="text" /></li>
*/
class Person extends React.Component {
  state = {
    persons: [
      { id: 1, name: 'bird', age: 18 },
      { id: 2, name: 'dog', age: 19 },
    ],
  }

  add = () => {
    const { persons } = this.state
    const p = { id: persons.length + 1, name: 'cat', age: 20 }
    this.setState({ persons: [p, ...persons] })
  }

  render() {
    return (
      <div>
        <h2>展示信息</h2>
        <button onClick={this.add}>添加一个人</button>
        <h3>使用index(索引值)作为key</h3>
        <ul>
          {this.state.persons.map((personObj, index) => {
            return (
              <li key={index}>
                {personObj.name}---{personObj.age}
                <input type="text" />
              </li>
            )
          })}
        </ul>
        <hr />
        <hr />
        <h3>使用id(数据唯一标识)作为key</h3>
        <ul>
          {this.state.persons.map((personObj, index) => {
            return (
              <li key={personObj.id}>
                {personObj.name}---{personObj.age}
                <input type="text" />
              </li>
            )
          })}
        </ul>
      </div>
    )
  }
}

Guess you like

Origin blog.csdn.net/qq_38689395/article/details/117449097