手把手教你学React

107 阅读11分钟

React

一、state

用于改变dom的声明式数据,相当于vue中的data属性里的数据。不同的是,vue中data 里的数据用Object.definepropty()处理过,对每个数据里的getter和setter做了逻辑处理,使得只要改变了数据,页面会自动更新。

在react中,需要使用setState()把state的改变挂载到dom中更新

二、setState

用法: class组件中:this.setState({foo:value})

难点:

在同步的逻辑中,异步更新状态,

所以不能在如下情况下实时获取状态

    this.setState({
        foo:value
    })
    console.log(this.state.foo)

可以通过传第二个参数实现实时获取状态

    this.setState({
        foo:value
    },console.log(this.state.foo))

在异步的逻辑中,同步更新状态

    setTimeout({
        this.setState({
        foo:value
        })
        console.log(this.state.foo)
    },0)

三、prop

传变量、js表达式用{}包裹,相当于vue中的v-bind

父:<child foo="123" bar={true}/>

class子:

接受:let {foo} = this.props;

验证与默认属性:

import propTypes from 'prop-types'
//prop-types中有验证方法
class A extends Components{
    //staic 类属性
    static propTypes = {
        foo:string,
        bar:bool
    }
    //默认属性
    static defaultProps = {
        bar:true
    }
}
​
    

四、ref

react16之前直接用ref

<child ref="myRef"/>

this.$refs.myRef.

之后推荐用

//类组件
myRef = React.createRef()
<child ref={this.myRef}/>
​
this.myRef.current.

dangerouslySetInnerHTML

默认不能输入html,会转为字符串,用这个属性可以显示html结构

myText = <div>dsf</div>
<span dangerouslySetInnerHTML={
    {
        __html:myText
    }
}</span>

五、组件通信

1、父子通信

prop
  • 传递数据(父传子)
  • 传递方法(子传父),子调用父方法,父方法更改父状态。相当于vue中prop中传函数,在子中调用
ref
  • 父引用子,调用子的方法

2、非父子通信

状态提升(中间人模式)

用于兄弟组件,以父组件的状态为中间人,子传父(传递方法)传子(传递数据)

发布订阅模式
let bus = {
  list: [],
  // 订阅者函数
  subscribe(callback) {
    this.list.push(callback);
  },
  publish(params) {
    console.log();
    this.list.forEach((callback) => {
      callback && callback(params);
    });
  },
};
// 订阅者,创建函数
bus.subscribe((params) => {
  console.log(params);
});
bus.subscribe((params) => {
  console.log(params + 1);
});
// 发布者,调用函数
setTimeout(() => {
  bus.publish(1);
}, 0);
​
context方案 (供应商-消费者模式)

跨组件通信方案,用于有共同的父组件的子、孙、...组件

在父组件用供应商组件,子组件用消费者组件,共同用父组件的状态state

const GlobalContext = React.createContext() //创建context对象
// 父组件A
class A extends Component{
  constructor(){
    super()
    this.info = ''
  }
  return (
    // 供应商
    <GlobalContext.Provider value={{
      info:this.state.info,
      changeInfo:(val)=>{
        this.setState({
          info:val
        })
      }
    }} >
      <span>AAA</span>
      <B/>
      <C/>
    </GlobalContext.Provider>
  )
}
class B extends Component{
  constructor(){
    super()
    this.info="B"
  }
  return(
    <GlobalContext.Consumer>
      {
        // 获得供应商的value值
        (value)=>{
          // 调用供应商的方法,改变父组件A的info,进而改变C组件中的info
          <span onClick={()=>{
            value.changeInfo(this.state.info)
          }}>BBB-{value.info}</span>
        }
      } 
    </GlobalContext.Consumer>
  )
}
class C extends Component{
  constructor(){
    super()
  }
  return(
    <GlobalContext.comsuer>
      {
          // 获得供应商的value值
        (value)=>{
            <span>CCC-value.info</span>
        }
      }
    </GlobalContext.comsuer>
  )
}

六、插槽

prop的特殊属性

作用:

  • 为了复用
  • 一定程度减少父子通信,虽然是子组件的内容,可以改父组件的状态
class App extends Component{
  state={
    info:''
  }
  return(
    <Swiper>
    // 虽然是子组件的内容,可以改父组件的状态
      <div onClick=()=>{
        this.setState({
          info:'1111'
        })
      }>11111</div>
      <div>22222</div>
      <div>22222</div>
    </Swiper>
  )
}
// 插槽
class Swiper extends Component{
  return(
    <div>
      {this.props.children}
    </div>
  )
}
// 类似具名插槽
class Swiper extends Component{
  return(
    <div>
      {this.props.children[0]}
      {this.props.children[1]}
      {this.props.children[2]}
    </div>
  )
}

七、生命周期(只在类组件中有,因为函数组件没有状态)

1、初始化阶段

componentWillMount(vue中的beforeMount)

要被废弃,不推荐使用

可以访问到状态,改变状态;没有创建dom

第一次上树(dom树)前,最后一次更改状态的机会

componentDidMount(vue中的mounted)

已经创建dom,可以获取dom

  • 数据请求axios
  • 订阅函数调用
  • setInterval等事件
  • 基于创建完的dom进行初始化
render

只能访问state和props,不允许修改状态和dom输出

2、运行中阶段

componentWillReceiveProps

不安全,不推荐使用,一定要用可以用UNSAFE_componentWillReceiveProps

将要接收props,只能用于子组件

  • 最先获得父组件传来的属性,可以利用属性进行ajax或者逻辑处理
  • 把属性转化为自己的状态
componentWillReceiveProps(nextProps){
    //this.props 老的props
    //nextProps  新的props
    if(JSON.stringify(this.state) !== JSON.stringify(nextState)){
		return true; //应该更新
    } 
	return false; //阻止更新
}
shouldComponentUpdate

应不应该更新,根据逻辑决定是否更新,防止无效的虚拟dom对比,提高性能。只要父组件更新,子组件的都更新,如果子组件数量多,可以通过这个周期函数限制更新,提高性能

可获得新的props和state

shouldComponentUpdate(nextProps,nextState){
    //this.state 老的状态
    //nextState  新的状态
    if(JSON.stringify(this.state) !== JSON.stringify(nextState)){
		return true; //应该更新
    } 
	return false; //阻止更新
}
componentWillUpdate

不安全,不推荐使用,一定要用可以用UNSAFE_componentWillUpdate

不能修改属性和状态

render

只能访问state和props,不允许修改状态和dom输出

componentDidUpdate

更新后,可以获取dom节点,执行dom操作

可获得旧的props和state

componentDidUpdate(prevPros,prevState){
	//避免有数据时还重复new
	if(prevState.list.length == 0){
		new BetterScroll("#wrapper")
	}
}

3、销毁阶段

componentWillUnmount

销毁window事件,比如定时器和事件监听。因为组件被销毁,绑定在window的事件不会销毁,可以在这里手动销毁

componentDidMount(){
    window.onresize = ()={}
    this.timer = setInterval(()=>{},1000)
}
componentWillUnmount(){
	window.onresize = null;
	clearInterval(this.timer)
}

老生命周期的问题

componentWillMount,在ssr中这个方法将会被多次调用,所以会重复触发多遍,同时在这里如果绑定事件,将无法解绑,导致内存泄漏,变得不够安全高效逐步废弃。

componentWillReceiveProps,外部组件多次频繁更新传入多次不同props,会导致不必要的异步请求。

componentWillUpdate,更新前记录Dom状态,可能会做一些处理,与componentDidUpdate相隔时间如果过长,会导致状态不太可信

新生命周期

getDerivedStateFromProps(从属性上获取衍生状态)

类属性,没有this

初始化和改变自身状态都会执行,所以可以替代废弃的生命周期componentWillMountcomponentWillReceiveProps

state={
    myName:'111'
}
static getDerivedStateFromProps(nextProps,nextState){
	//可以直接改state
	return {
		myName:'123'
	}
    //可以做处理,如首字母大写
    return{
        myName:nextState.myName.substring(0,1).toUpperCase()+nextState.myName.substring(1)
    }
}
getSnapShotBeforeUpdate(在更新前拍快照)

更新**render后会执行记录当前状态,所以可以替代废弃的生命周期componentWillUpdate**

例子:滚动条内容在增加后,滚动到原来的地方看原来在看的内容,而不是一开始只看到更新后的内容

getSnapShotBeforeUpdate记录滚动高度scrollHeight,在更新后componentDidUpdate获取滚动高度scrollHeight,再两者相减就是滚动条需要滚动的高度,最终滚动到原来的地方

getSnapShotBeforeUpdate(){
  return this.myRef.current.scrollHeight
}
componentDidUpdate(prevProps,prevState,value){
  // value是getSnapShotBeforeUpdate返回的值
  this.myRef.current.scrollTop += this.myRef.current.scrollHeight - value
}

八、React性能优化方案

1、shouldComponentUpdate

控制组件自身或者子组件是否需要更新,尤其在子组件非常多的情况下,需要进行优化

2、PureComponent

PureComponent会帮你比较新Props跟旧的props,新的state和老的state(值相等或者对象含有相同的属性、且属性值相等),决定shouldComponentUpdate返回true或者false,从而决定要不要呼叫render function

注意

如果你的state或props永远都会变,那么PureComponent并不会比较快,因为shallowEqueal也需要花时间

import React,{PureComponent} from 'react'
class A extends PureComponent{

}

九、React Hooks

1、使用hooks的理由

1、高阶组件为了复用,导致代码层级复杂

2、生命周期太复杂

3、写function组件无状态,要改为有状态需要改为class,成本太高

2、useState(保存组件状态)

useState返回一个数组,第一个参数为默认值,第二个参数为函数

import React, { useState } from "react";
function A() {
  const [name, setName] = useState("123");
  return (
    <div
      onClick={() => {
        setName("456");
      }}
    >
      {name}
    </div>
  );
}

3、useEffect(处理副作用)和useLayoutEffect(同步处理副作用)

一个基于依赖的执行函数,可以做到类组件中生命周期做的事。如请求、更新后执行、销毁事件等。但function组件是没有生命周期的,不能用于与类组件的生命周期对比。

useEffect的第一个函数参数在创建时会执行,里面的return的函数取决于有无依赖项会在销毁时或依赖项发生变化时执行。

import React, { useState, useEffect } from "react";
function A() {
  const [name, setName] = useState("xiaoming");
  // 没有依赖项,仅初始化执行一次
  useEffect(() => {
    axios.get();
  }, []);
  // 有依赖项,依赖项发生变化时执行.用到依赖项时必须在数组里加上
  useEffect(() => {
    setName(name.substring(0, 1).toUpperCase + name.substring(1));
  }, [name]);
  // 在没依赖项时return里的函数,仅会在销毁时执行。
  // 在有依赖项时return里的函数,依赖项发生变化时执行。
  useEffect(() => {
    window.onresize = () => {
      console.log("resize");
    };
    let timer = setInterval(() => {
      console.log("timer");
    }, 1000);

    return () => {
      window.onresize = null;
      clearInterval(timer);
    };
  }, []);
  return (
    <div
      onClick={() => {
        setName("xiaohong");
      }}
    >
      {name}
    </div>
  );
}
必要性

function 组件不同于类组件,当状态改变,类组件更新时只会执行render函数,function组件会整个组件一起更新。如果我们要执行一个方法如发送请求,不用useEffect,就会一直执行请求。所以有必要使用useEffect限制方法调用时机。

useEffect和useLayoutEffect的区别

调用时机不同,useLayoutEffect和类组件中的componentDidMountcomponentDidUpdate的调用时机一致,在react的Dom更新后马上同步调用代码,会阻塞页面渲染。(完成Dom树后,dom树还在内存中,需要完成渲染树才能展示页面)而useEffect是在整个页面渲染完成后才执行代码

官方建议优先使用useEffect

在实际使用时如果出现页面抖动(在useEffect中修改Dom很有可能出现),可以把需要Dom操作的代码放在useLayoutEffct。在这里做Dom操作,这些Dom修改会和react做出的更改一起被一次性渲染到页面上,只有一次重排和重绘的代价。

4、useCallback(记忆函数)

当状态改变,function组件会整个组件一起更新,function组里的方法也会被重新创建。为了防止组件更新导致方法被重新创建useCallback起到缓存作用,只有第二个参数变了,才重新声明一次

// 依赖项改变后才会声明一次,依赖项不变时不会重新创建,会用缓存的数据
// 如果传入空数组,第一次被创建后就缓存,name后期改变了,拿到的还是老的name
// 如果不传第二个参数,每次都会声明一次,拿到的是最新的name
  const handleChange = useCallback(() => {
    setName("name" + name);
  }, [name]);

5、useMemo(记忆组件)(像vue中的计算属性)

useCallback完全可以由useMemo替代

useCallback(fn,inputs) is equivalent to useMemo(()=>fn,inputs)

唯一的区别是:useCallback不会执行函数,而是把参数返回,useMemo会执行函数并将结果返回。更适合经过函数计算后返回一个确定的值,如记忆组件

6、useRef(保存引用值)

等价于类组件中的React.createRef()

const myRef = useRef();
<swiper ref={myRef}/>

用于保存function组件里的变量,不让变量被重新创建

import React, { useState, useRef } from "react";
function A() {
  const [name, setName] = useState("xiaoming");
  // 赋初始值0的变量total
  let total = useRef(0);
  return (
    <div
      onClick={() => {
        setName("xiaohong");
        total.current++;
      }}
    >
      {name}
    </div>
  );
}

7、useRducer和useContext(减少组件层级)

useContext

相当于context方案(供应商-消费者模式),但可以降低消费者的代码复杂度

import { useState, useContext } from "react";
const GlobalContext = React.createContext(); //创建context对象
// 父组件A
function A() {
  const [info, setInfo] = useState("");
  return (
    // 供应商
    <GlobalContext.Provider
      value={{
        info: info,
        changeInfo: (val) => {
          setInfo(val);
        },
      }}
    >
      <span>AAA</span>
      <B />
      <C />
    </GlobalContext.Provider>
  );
}
function B(props) {
  const [info, setInfo] = useState("B");
  // 获得供应商的value值
  const value = useContext(GlobalContext);
  return (
    // 调用供应商的方法,改变父组件A的info,进而改变C组件中的info
    <span
      onClick={() => {
        value.changeInfo(info);
      }}
    >
      BBB-value.info
    </span>
  );
}
function C() {
  const value = useContext(GlobalContext);
  return <span>CCC-{value.info}</span>;
}
useRducer

脱胎于Redux,从外部管理状态,以下是简单用法

import React,{useReducer} from 'react'
// 外部函数
const reducer =  (prevState,action)=>{
  // prevState是老的状态,action是dispatch传过来的对象
  let newState = {...prevState}
  if (action.type == 'add') {
    return newState.count++
  }else if(action.type == 'sub') {
    return newState.count--
  }else{
    return prevState
  }
}
// 外部对象
const initialState = {
  count:0
}

function A (){
  const [state,dispatch] = useReducer(reducer,initialState)
  return(
   <div>
    <button onClick={()=>{
      dispatch({
        type:'sub'
      })
    }}>-</button>
    {state.count}
    <button onClick={()=>{
      dispatch({
        type:'add'
      })
    }}>+</button>
   </div>
  )
}
useReducer结合useContext用法

实现跨组件通信

在共享的父组件创建useReducer,通过useContextstatedispatch传给子组件,由其中一个子组件改变另外子组件的状态

/**
 * useReducer结合useContext用法
 */
import React, { useReducer, useContext } from "react";

const GlobalContext = React.createContext();
// 外部函数
const reducer = (prevState, action) => {
  // prevState是老的状态,action是dispatch传过来的对象
  let newState = { ...prevState };
  switch (action.type) {
    case "change-a":
      newState.a = action.value;
      return newState;
    case "change-b":
      newState.b = action.value;
      return newState;
    default:
      return prevState;
  }
};
// 外部对象
const initialState = {
  a: "a",
  b: "b",
};
function APP() {
  // useReducer只能在共享的父组件里创建一个
  const [status, dispatch] = useReducer(reducer, initialState);
  return (
    <GlobalContext.Provider
      value={{
        status,
        dispatch,
      }}
    >
      <Child1 />
      <Childa />
      <Childb />
    </GlobalContext.Provider>
  );
}
function Child1() {
  // 必须传对应的context
  const { dispatch } = useContext(GlobalContext);
  return (
    <div>
      <button
        onClick={() => {
          dispatch({
            typ: "change-a",
            value: "1111",
          });
        }}
      >
        改变a
      </button>
      <button
        onClick={() => {
          dispatch({
            typ: "change-b",
            value: "2222",
          });
        }}
      >
        改变b
      </button>
    </div>
  );
}
function Childa() {
  const { state } = useContext(GlobalContext);
  return <div>Childa-{state.a}</div>;
}
function Childb() {
  const { state } = useContext(GlobalContext);
  return <div>Childb-{state.b}</div>;
}

8、自定义hooks

可以用react的hooks等功能的函数,必须用use开头,可以传参,要返回值。用于把js逻辑抽离出来复用。

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

function useName() {
  const [name, setName] = useState("");
  useEffect(() => {
    axios.get().then((res)=>setName(res.data.name))
  }, []);
  return { name };
}
function A() {
  const { name } = useName();
  return (
    <div>
      {name}
    </div>
  );
}

十、CSS Module

css模块化可以防止样式污染

方案
  • css文件名改为.module.css格式
  • 组件引入得到的是一个和{定义的类:自动重命名的类}的映射对象
  • style.value方式使用
import style from './a.module.css'
style //{value:..._value_...}
{style.value}

只对class选择器、id选择器有效

需要获取class做处理可以这样加class

<div className={style.value+" aaa" }</div>

会变成两个类,可以通过获取aaa这个类操作

<div class="formadf_value-seraf aaa"</div>
全局
:global(.active){

}

\