[ 响应式系统与 React | 青训营笔记 ]

78 阅读8分钟

这是我参与「第五届青训营」伴学笔记创作活动的第 15 天

1.1 起步及JSX介绍

React是用于构建界面的JavaScript库,优点:

  • 组件化开发,提升效率
  • 虚拟DOM + 优秀的DIFF算法,减少与真实DOM的交互
  • 可以使用React Native开发移动端

第一个app

  1. 引入React ReactDOM 和 Babel

      <script src="https://unpkg.com/react@16/umd/react.development.js" crossorigin></script>
      <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script>
      <script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
    
  2. 创建容器,虚拟DOM并挂载

    <body>
      <!-- 挂载的容器 -->
      <div id="app"></div>
    
      <!-- 此处一定要写babel -->
      <script type="text/babel">  
        // 创建虚拟DOM
        const VirtualDOM = <h1>hello, react</h1>
        // 渲染虚拟DOM到页面
        ReactDOM.render(VirtualDOM, document.getElementById('app'))
      </script>
    </body>
    

关于虚拟DOM

  1. 本质上是个object类型的对象
  2. 虚拟DOM的属性比真实DOM少,更轻量
  3. 虚拟DOM最终会被React转化为真实DOM

JSX语法规则

  1. 定义虚拟DOM时,不要加引号
  2. 标签中混入JS表达式,要加花括号;JSX中不能引入语句,只能有表达式
  3. 引用样式时类名用className
  4. 写行内样式要用双花括号,第一个花括号代表JS表达式,第二个花括号代表对象
  5. 不能有多个根标签,标签必须闭合
  6. 标签首字母小写,则转换为html元素;若标签首字母大写,则转换为对应组件
<script type="text/babel">  
  const id = "20221260148"
  const name = 'guo'
  const data = ['angular','react','vue']
  // 创建虚拟DOM
  const VirtualDOM = (
  <div>
    <h1 style={{color: 'red', fontSize: '16px'}} className="student" id={id}>hello, {name}</h1>
    <input type="text"/>
    <ul>
      {data.map((item, index) => <li id={index}>{item}</li>)}
    </ul>
  </div>
  )
  // 渲染虚拟DOM到页面
  ReactDOM.render(VirtualDOM, document.getElementById('app'))
</script>

2.2 组件三大属性

state

state是组件对象最重要的属性, 值是一个对象,通过更新组件的state来更新对应的页面显示

<script type="text/babel">  
  class Demo extends React.Component {
    //初始化状态用赋值语句,不将其放在原型对象上,而是作为私有属性放在实例本身
    state = {
      isHot: true
    }
    render() {
      const {isHot} = this.state
      return <h1 onClick={this.change}>今天天气很{isHot ? '炎热' : '凉爽'}</h1>
    }
    //自定义方法,用箭头函数(箭头函数没有自己的this,自动找外层的this。类中this正好指向实例对象)
    change = () => {
      const {isHot} = this.state
      this.setState({isHot: !isHot})
    }
  }
  ReactDOM.render(<Demo/>, document.getElementById('app'))
</script>

注意事项

  1. 组件render方法中的this为组件实例对象

  2. 组件自定义的方法中this为undefined,如何解决?

    • 强制绑定this: 通过函数对象的bind()
    • 箭头函数
  3. 状态数据,不能直接修改或更新,要用setState()

props

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

注意:组件内部不要修改props数据

类组件props传值

<script type="text/babel">
  //创建组件
  class Person extends React.Component {
    render() {
      const {name, sex, age} = this.props
      return (
        <ul>
        <li>姓名:{name}</li>
        <li>性别:{sex}</li>
        <li>年龄:{age}</li>
        </ul>
      )
    }
  }
​
  //单个传值
  ReactDOM.render(<Person name="小明" sex="男" age="19"/>, document.getElementById('app1'))
  //批量传值
  const data = {name: "张三", sex: "男", age: 18}
  ReactDOM.render(<Person {...data}/>, document.getElementById('app2'))
</script>

函数组件props传值

<script type="text/babel">
  //创建组件
  function Person(props) {
    const {name, sex, age} = props
    return (
      <ul>
        <li>姓名:{name}</li>
        <li>性别:{sex}</li>
        <li>年龄:{age}</li>
      </ul>
    )
  }
​
  ReactDOM.render(<Person name="小明" sex="男" age="19"/>, document.getElementById('app1'))
</script>

refs

refs可以获取到元素真实DOM节点

字符串形式ref(不推荐)

class Demo extends React.Component {
  showData = () => {
    alert(this.refs.curNode.value)
  }
​
  render() {
    return (
      <div>
        <input ref="curNode" type="text"/>
        <button onClick={this.showData}>点我提示数据</button>  
      </div>
    )
  }
}

回调函数ref

class Demo extends React.Component {
  showData = () => {
    alert(this.curNode.value)
  }

  render() {
    return (
      <div>
        <input ref={curNode => this.curNode = curNode} type="text"/>
        <button onClick={this.showData}>点我提示数据</button>  
      </div>
    )
  }
}

createRef

class Demo extends React.Component {
  inputRef = React.createRef()

  showData = () => {
    alert(this.inputRef.current.value)
  }

  render() {
    return (
      <div>
        <input ref={this.inputRef} type="text"/>
        <button onClick={this.showData}>点我提示数据</button>  
      </div>
    )
  }
}

2.3 生命周期与diff算法

组件生命周期:组件从创建到死亡它会经历一些特定的阶段

React组件中包含一系列钩子函数(生命周期回调函数), 会在特定的时刻调用

我们在定义组件时,会在特定的生命周期回调函数中,做特定的工作

生命周期的三个阶段

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

  1. constructor()
  2. componentWillMount()
  3. render()
  4. componentDidMount()

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

  1. shouldComponentUpdate()
  2. componentWillUpdate()
  3. render()
  4. componentDidUpdate()

3. 卸载组件: 由ReactDOM.unmountComponentAtNode()触发

  1. componentWillUnmount()

三个重要的钩子函数

  1. render() :初始化渲染或更新渲染调用
  2. componentDidMount() :开启定时器,发送ajax请求登
  3. componentWillUnmount() :做一些收尾工作, 如:清理定时器,取消事件监听等

diff算法

当状态中数据发生变化时,React会根据新数据生成新的虚拟DOM,随后将新的虚拟DOM与旧的虚拟DOM进行diff算法比较

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

    逐层对比新旧虚拟DOM的内容,若内容没变,直接用之前的真实DOM;若内容改变,生成新的真实DOM并替换掉旧的真实DOM

  2. 当旧虚拟DOM中没有找到与新虚拟DOM相同的key:直接创建新的真实DOM渲染到页面

用遍历的index作为key可能引发的问题

  1. 对数据进行改变顺序的操作时,会产生没有必要的DOM更新,效率低
  2. 如果结构中包含输入类的DOM,会发生渲染错误

开发中最好选择每条数据的唯一标识作为key

2.5 React Hooks

React Hooks可以让函数式组件也可以使用state及其他特性

Hooks可以反复多次使用,并且每个Hook相互独立

使用Hook的两条规则

  • 只在函数最顶层使用Hook(不在循环或条件语句中使用Hook)
  • 只在React函数组件中使用Hook(不在普通的JavaScript函数中使用Hook)

useState

useState() 传入一个初始值,返回一个包含数据和修改数据的方法

import React, { useState } from "react";

export default function Demo() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>当前count {count}</p>
      <button onClick={() => setCount(count + 1)}>点击+1</button>
    </div>
  );
}

setCount 可以有两种写法。一种是直接传递一个新的值,另一种是传递一个函数,现在的值将以参数的形式传给该函数,函数的返回值用于更新状态

<button onClick={() => setCount((count) => count + 1)}>点击+1</button>

useEffect

基本使用

useEffect 可以让你在函数组件中执行副作用操作(不知道执行的操作会发生什么,例如网络请求等等),用于替代类组件中的生命周期函数

import React, { useState, useEffect } from "react";

export default function EffectHook() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    //更新网页的标题为count次数
    document.title = `count = ${count}`;
  });

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>点击+1</button>
    </div>
  );
}

react在首次渲染之后的每次渲染都会调用一遍传给 useEffect 的函数,而类组件中需要用两个生命周期函数来分别表示首次渲染(componentDidMount),和之后的更新渲染(componentDidUpdate)

useEffect 中定义的副作用函数的执行不会阻碍浏览器更新视图,也就是说这些函数是异步执行的,而类组件的生命周期函数则是同步执行的

react提供了同步执行的副作用钩子 useLayoutEffect ,使用方法和 useEffect 是一样的

解绑副作用

这种场景很常见,当我们在componentDidMount里添加了一个注册,我们得马上在 componentWillUnmount 中,也就是组件被注销之前清除掉我们添加的注册,否则内存泄漏的问题就出现了

useEffect 中,我们只需要让副作用函数 return 一个新的函数即可,这个新的函数将会在组件下一次重新渲染之后执行

要注意不同点,componentWillUnmount 只会在组件被销毁前执行一次而已,而 useEffect 里的函数,每次组件渲染后都会执行一遍,包括副作用函数返回的这个清理函数也会重新执行一遍

useEffect的第二个参数

之前说到每次重新渲染都要执行一遍这些副作用函数,显然是不经济的。怎么跳过一些不必要的计算呢?只需要给 useEffect 传第二个参数即可

用第二个参数来告诉react只有当这个参数的值发生改变时,才执行我们传的副作用函数

useEffect(() => {
  document.title = `count = ${count}`;
}, [count]); // 只有当count的值发生变化时,才会重新执行`document.title`这一句

第二个参数可以是

  • 不传值(在首次渲染和每次更新渲染时都会重新执行,相当于 componentDidMountcomponentDidUpdatecomponentWillUnmount 的结合)
  • 空数组(只在首次渲染的时候执行,相当于 componentDidMountcomponentWillUnmount 的结合)
  • 数组(只有当数组内的状态修改时才会重新执行)

useContext

在react中,父子组件通信一般直接用 props ,但是如果有多层组件的嵌套呢?连续地使用 props 传值就会很麻烦

在祖先组件与后代组件进行通信时,可以使用 context 对象

import React, { useState, createContext, useContext } from "react";

//第一步,创建context容器对象,放在祖先组件和后代组件都能共享的位置,可以有初始值
const MyContext = createContext();

export default function ContextHook() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <h2>ContextHook</h2>
      <p>祖先组件的count {count}</p>
      <button onClick={() => setCount(count + 1)}>点击+1</button>

      {/* 第二步,渲染子组件时用容器名.Provider包裹,通过value属性给后代传递数据 */}
      <MyContext.Provider value={count}>
        <Child />
      </MyContext.Provider>
    </div>
  );
}

function Child() {
  return (
    <div>
      <Grand />
    </div>
  );
}

//第三步,使用数据时用容器名.Consumer包裹
function Grand() {
  return (
    <div>
      <MyContext.Consumer>
        {(value) => <p>后代组件的count {value}</p>}
      </MyContext.Consumer>
    </div>
  );
}

后代组件使用 useContext 改写,调用了 useContext 的组件总会在 context 值变化时重新渲染

//使用useContext钩子
function Grand() {
  const count = useContext(MyContext)
  return (
    <div>
      <p>后代组件的count {count}</p>
    </div>
  )
}

useRef

在类组件中,使用 ref 有三种方式,推荐使用 createRef 的写法创建一个容器

class Demo extends React.Component {
  inputRef = React.createRef()
​
  showData = () => {
    alert(this.inputRef.current.value)
  }
​
  render() {
    return (
      <div>
        <input ref={this.inputRef} type="text"/>
        <button onClick={this.showData}>点我提示数据</button>  
      </div>
    )
  }
}

在函数式组件中,可以使用 useRef 这个钩子完成同样的事

import React, { useRef } from "react";

export default function Demo() {
  const inputRef = useRef();

  return (
    <div>
      <h2>RefHook</h2>
      输入数据:
      <input type="text" ref={inputRef} />
      <button onClick={() => alert(inputRef.current.value)}>
        点击提示数据
      </button>
    </div>
  );
}

useReducer

useState 的进阶版,可以将 state 与其相关操作定义在 reducer 里,思想参考 redux

import React, { useReducer } from "react";

export default function Demo() {
  // 返回值1:状态值
  // 返回值2:修改状态的派发器,具体的修改行为由reducer函数执行
  // 参数1:reducer是一个整合函数,该函数的返回值将会作为新的状态值
  // 参数2:状态初始值
  const [count, countDispatch] = useReducer((state, action) => {
    // reducer函数有两个参数,第一个参数为当前state,第二个参数为action对象
    switch (action.type) {
      case "add":
        return state + action.payload;
      case "sub":
        return state - action.payload;
      default:
        break;
    }
  }, 0);

  const add = () => {
    countDispatch({ type: "add", payload: 1 });
  };

  const sub = () => {
    countDispatch({ type: "sub", payload: 1 });
  };

  return (
    <div>
      <h2>ReducerHook</h2>
      <p>当前count {count}</p>
      <button onClick={add}>点击+1</button>
      <button onClick={sub}>点击-1</button>
    </div>
  );
}

useMemo

react 组件发生重新渲染的情况

  • 当组件自身的 state 发生变化
  • 当组件的父组件重新渲染,子组件也会重新渲染

通过 React.memo(子组件) 方法,只有当子组件的 props 发生改变时,子组件才会重新渲染

通过 useMemo 钩子,可以实现数据的监听和缓存,只有当数据的依赖项修改时,才会重新计算

import React, { useMemo, useState } from "react";

export default function Demo() {
  const [countA, setCountA] = useState(0);
  const [countB, setCountB] = useState(0);

  // useMemo传入两个参数,第一个参数为计算的函数,第二个参数为依赖项
  const countC = useMemo(() => countA + countB, [countA, countB]);
  return (
    <div>
      <h2>MemoHook</h2>
      <p>当前countA {countA}</p>
      <p>当前countB {countB}</p>
      <button onClick={() => setCountA(countA + 1)}>点击countA+1</button>
      <button onClick={() => setCountB(countB + 1)}>点击countB+1</button>
      <p>当前countC {countC}</p>
    </div>
  );
}

useCallback

useCallback 和 useMemo 两者接收的参数是一样的,第一个参数表示一个回调函数,第二个表示依赖的数据

在依赖数据发生变化的时候,才会调用传进去的回调函数去重新计算结果,起到一个缓存的作用

不同点:useMemo 缓存的结果是回调函数中return回来的,useCallback 缓存的结果是函数

当组件的函数由 useCallback 返回时,只有依赖项发生改变才会重新渲染此函数

useCallback 应该和 React.memo 配套使用

自定义Hook

自己创建的可以调用其他钩子函数的Hook,命名规则:useXxx

在 src 下创建 hooks 文件夹存储自定义钩子,并暴露该函数

需要将一定数据或方法作为返回值,供调用时接收