React

一、组件通讯

组件是独立且封闭的单元,为了实现某些功能,需要打破组件的独立封闭性,让其与外界沟通

1.1.组件的props

props:接收传递给组件的数据

  • 传递数据:给组件标签添加属性
  • 接收数据【以对象的形式】:函数组件通过参数props接收数据,类组件通过this.props接收数据
  • 特点
    可以传递任何类型的数据:字符串、数字、数组、函数、jsx结构
    props是只读的,只能读取,不能修改
    在类组件中,如果写了构造函数,应该将props传递给super(),否则无法在构造函数中获取到props
  • children属性
    只有引用子组件时内部有内容才会有children属性
    可以是任意值:文本、React元素、数组、组件、函数
  • props校验
    props校验可以保证组件使用者传入数据的格式,给出明确的错误提示,增加组件健壮性
    允许在创建数组是,就使用propTypes来指定props的类型和格式
    使用步骤
    1.安装包(yarn add prop-types)
    2.导入包(import PropTypes from 'prop-types')
    3.添加校验规则(组件名.propTypes={属性名:PropTypes.类型.isRequired})
    约束类型:number、string、bool、array、object、func、symbol、element【React元素】
    必填项:isRequired【设置后必须传入这个参数,否则报错】
    特定结构对象:filter:PropTypes.shape({color:PropTypes.array,fontSize:PropTypes.number})
  • props默认值
    在引入组件时不传入参数,也可以拥有的默认值组件名.defaultProps={name:'小花'},场景pageSize页大小
    不传入使用默认值,传入使用传入的值
1.2.通信方式
  • 父组件 >> 子组件
    父组件给子组件传参
  • 子组件 >> 父组件
    父组件给子组件传回调函数,子组件调用并将要传递的数据return回去
  • 兄弟组件 >> 兄弟组件
    将共享状态提升到最近的公共父组件中,由公共父组件管理这个状态【状态提升】,要通信的子组件只需要通过props接受状态或操作状态的方法即可
  • 状态提升
    用户对子组件操作,子组件不改变自己的状态,通过自己的props把这个操作改变的数据传递给父组件,改变父组件的状态,从而改变受父组件控制的所有子组件的状态,这也是React单项数据流的特性决定的。
import React from "react";

// 父组件【类组件】
class Parent extends React.Component {
  state = {
    lastName: "杨",
    firstName: "",
  };
  constructor(props) {
    super(props);
  }
  render() {
    return (
      <>
        <div>这是父组件</div>
        <Child1
          lastName={this.state.lastName}
          // 父组件提供回调函数,用于接受数据
          getFirstName={(firstName) => {
            // 在这里可以接收到子组件传递给父组件的值
            this.setState({
              firstName,
            });
          }}
        />
        {/* 通过Child1获取firstName的值并传递给Child2 */}
        <Child2 firstName={this.state.firstName}>
       		{/* “<p>这是第二个子节点</p>”会作为children属性被子节点的props.children接收 */}
        	{/* <p>这是第二个子节点</p> */}
        	{/* 还可以传递函数 */}
        	{()=>console.log('这是一个函数子节点')}
        </Child2>
      </>
    );
  }
}
// 子组件1【函数式组件】
const Child1 = (props) => {
  const { lastName, getFirstName } = props;
  return (
    <>
      <div>接收父组件的数据:{lastName}</div>
      <button
        onClick={() => {
          getFirstName("超");
        }}
      ></button>
    </>
  );
};
// 给Child1添加props校验
Child1.propTypes = {
	lastName:propTypes.string.isRequired,
	getFirstName:propTypes.func
}

// 子组件2【与子组件1是兄弟组件,通过状态提升与子组件1进行通讯】
const Child2 = (props) => {
  const { firstName,children,color } = props;
  // 接收函数子节点
  children() // 会在控制台打印出:这是一个函数子节点
  return (
    <>
      <div>接收父组件的数据:{firstName}喜欢{color}</div>
      {/* 接收文本子节点 */}
      {/* <div>{children}</div> */}
    </>
  );
};
// 给Child2设置一个默认props值
Child2.defaultProps={
	color:'蓝色'
}

ReactDOM.render(<Parent />, document.getElementById("root"));
1.3.Context

全局状态管理:跨组件传递数据(多层嵌套)【Redux和hook的useContext】

  • 使用步骤
    1.调用React,createContext()创建Provider(提供数据)和Consumer(消费数据)两个组件
    2.使用Provider组件作为父节点来包裹应用
    3.设置value属性,表示要传递的数据
    <Provider value="pink">
    4.调用Consumer组件使用回调函数的参数接收数据
    <Consumer>{(value)=><span>接收到的数据为{value}</span>}</Consumer>
import React from "react";
// Provider:数据提供者,Consumer数据消费者
const { Provider, Consumer } = React.createContext();

const App = () => {
  // 使用Provider组件作为父节点
  return (
    // 设置value属性,表示要传递的数据
    <Provider value="pink">
      <div className="app">
        <Node />
      </div>
    </Provider>
  );
};

const Node = () => {
  return (
    <>
      <div className="node">
        <Child />
      </div>
    </>
  );
};

const Child = () => {
  return (
    <>
      <div className="child">
        {/* 子组件中调用Consumer组件接收数据 */}
        <Consumer>
          {(data) => {
            <span>从App中接收的值为:{data}</span>;
          }}
        </Consumer>
      </div>
    </>
  );
};

ReactDOM.render(<App />, document.getElementById("root"));

二、组件的生命周期

只有类组件才有生命周期

  • 生命周期
    从组件创建到挂载搭到页面中运行,再到组件不用时卸载的过程
  • 钩子函数
    生命周期的每个阶段伴随着一些方法的调用,这些方法就是生命周期的钩子函数,可以让开发人员在不同阶段操作组件
  • 注意:
    在render中调用this.setState会出现递归调用:因为每次状态更新都会调用render
    在componentDidMount时页面已经完成渲染,就可以使用document.getElementById(‘root’)来获取页面元素了
    当接收新属性、setState更新状态、调用强制更新forceUpdate()方法都会导致组件更新
    在componentDidUpdate不要直接调用setState,因为会导致递归更新,要将setState放到if条件中,条件中判断更新前后的props是否相同,componentDidUpdate的参数可以获取上一次的状态值,this.props获取改变后的状态值,发送ajax请求也要写在if条件中,因为请求完数据也要间接的进行setState操作
    componentDidUpdate(prevProps){if(prevProps.count !== this.props.count){this.setState({})}}

在函数组件中

  • 使用useEffect,如果useEffect不传参数,每一次重新渲染都会执行一次,如果useEffect传一个空数组,相当于componentDidMount挂载时会执行一次,如果useEffect传一个有值的数组,第一次执行渲染和每次状态发生变化的时候都会执行,useEffect的return是可选的清除机制相当于类组件的componentWillUnMount

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

三、组件复用

3.1.render props模式
  • render props模式【一种写法】:prop是一个函数并且告诉组件要渲染什么内容的技术
    如何拿到组件中的state:在使用组件时添加一个值为函数的prop,通过函数参数来获取
    如何渲染任意UI:使用该函数的返回值作为渲染的UI内容
    可以使用children代替render属性
import React from "react";
import img from "img";
const App = () => {
  return (
    <div className="app">
      {/* 4.在使用组件时添加一个值为函数的prop,通过函数参数来获取 */}
      {/* 使用该函数的返回值作为渲染的UI内容 */}
      {/* <Mouse
        updateCoordinate={(mouse) => {
          return (
            <p>
              鼠标当前的位置{mouse.x} {mouse.y}
            </p>
          );
        }}
      /> */}
      {/* 状态逻辑复用 */}
      <Mouse>
        {/* 使用children方式代替函数类型的prop */}
        {(mouse) => {
          return (
            <img
              src={img}
              alt="猫"
              style={{ position: "absolute", top: mouse.y, left: mouse.x }}
            />
          );
        }}
      </Mouse>
    </div>
  );
};
// 封装复用状态的逻辑代码
class Mouse extends React.component {
  // 【1.状态】
  state = {
    x: 0, // 鼠标的坐标
    y: 0,
  };
  // 【2.操作状态的方法】
  handleMouseMove = (e) => {
    this.setState({
      x: e.clientX,
      y: e.clientY,
    });
  };
  // 监听鼠标移动的事件
  componentDidMount() {
    window.addEventListener("mousemove", this.handleMouseMove);
  }
  // 在组件卸载是移除鼠标移动事件
  componentDidUnMount() {
    window.addEventListener("mousemove", this.handleMouseMove);
  }
  render() {
    // 3.返回父组件提供的updateCoordinate方法将当前鼠标位置传递到父组件中
    // (1).使用函数型prop
    // return this.props.updateCoordinate(this.state);
    // (2).使用children
    return this.props.children(this.state);
  }
}

ReactDOM.render(<App />, document.getElementById("root"));
3.2.高阶组件(HOC)
  • 高阶组件(HOC【一个函数】):实现状态逻辑复用,采用包装(装饰)模式
    接收要包装的组件,返回增强后的组件
  • 使用步骤
    1.创建一个函数,名称约定为with开头
    2.指定函数参数,参数应该以大写字母开头(作为要渲染的组件)
    3.在函数内部创建一个类组件,提供复用的状态逻辑代码,并将创建好的内部组件返回
    4.在该组件中,渲染参数组件,同时将状态通过prop传递给参数组件
    5.调用高阶组件,传入要增强的组件,通过返回值拿到增强后的组件,并将其渲染到页面中
import React from "react";
import img from "img";

// 高阶组件创建的增强组件名称是相同的,不利于调试,使用displayName设置不同的名称,用来区分
function getDisplayName(WrappedComponent) {
  return (
    WrappedComponent.getDisplayName || WrappedComponent.name || "Component"
  );
}
// 创建高阶组件,里面指定函数参数为要渲染的组件
function withMouse(WrappedComponent) {
  // 该组件提供复用的状态逻辑
  class Mouse extends React.component {
    // 【1.状态】
    state = {
      x: 0, // 鼠标的坐标
      y: 0,
    };
    // 【2.操作状态的方法】
    handleMouseMove = (e) => {
      this.setState({
        x: e.clientX,
        y: e.clientY,
      });
    };
    // 监听鼠标移动的事件
    componentDidMount() {
      window.addEventListener("mousemove", this.handleMouseMove);
    }
    // 在组件卸载是移除鼠标移动事件
    componentDidUnMount() {
      window.addEventListener("mousemove", this.handleMouseMove);
    }
    render() {
      // 将组件的状态和props借用参数传递出去,如果不传递props会造成props丢失【高阶组件并没有往下传递props】
      return <WrappedComponent {...this.state} {...this.props} />;
    }
  }
  // 修改增强后的函数名称,最后得到的名字分别为WithMousePosition与WithMouseCat
  Mouse.displayName = `WithMouse${getDisplayName(WrappedComponent)}`;
  return Mouse;
}
// 鼠标位置组件
const Position = (props) => {
  <p>
    鼠标当前的位置:(x:{props.x},y:{props.y})
  </p>;
};
// 猫捉老鼠组件
const Cat = (props) => {
  <img
    src={img}
    alt="猫"
    style={{ position: "absolute", top: props.y - 64, left: props.x - 64 }}
  />;
};
// 获取增强后的组件:Position和Cat就是WrappedComponent
const MousePosition = withMouse(Position);
const MouseCat = withMouse(Cat);
// 将组件渲染到页面中
const App = () => {
  return (
    <div className="app">
      {/* 渲染增强后的组件 */}
      <MousePosition />
      <MouseCat />
    </div>
  );
};
ReactDOM.render(<App />, document.getElementById("root"));

四、原理

4.1.setState

setState是异步更新的,【在自定义方法中是异步的,但在异步方法中是同步的】

  • 注意后面的setState()不要依赖于前面的setState()
  • 多次调用setState(),只会触发一次render重新渲染:为了性能的考虑
  • 推荐语法this.setState((state,props)=>{}),也是异步更新,但state是最新值,调用多次setState会多次触发render
  • 第二个参数:callback,在状态更新并且重新渲染后立即执行的某个操作
class Index extends Component {
  state: {
    count: 1,
  };
  handleClick = () => {
    // this.setState((state, props)=>{修改state},callback回调函数显示当前state)
    this.setState(
      (state, props) => {
        return {
          count: state.count + 1,
        };
      },
      () => {
        console.log("状态更新完成", this.state.count);
      }
    );
  };
  render() {
    return (
      <div>
        <h1>{this.state.count}</h1>
        <button onClick={this.handleClick}></button>
      </div>
    );
  }
}
4.2.JSX

JSX语法转化过程

  • JSX语法被@babel/preset-react 插件编译为createElement()方法,再转化为React元素显示在屏幕上
4.3.组件更新机制

组件之间更新渲染

  • 父组件重新渲染,其子组件和孙子组件都会重新渲染【渲染当前组件子数(当前组件及所有子组件)】

避免组件间不必要的重新渲染:父组件变化时若子组件没有任何变化也会重新渲染

  • 使用钩子函数shouldComponentUpdate(nextProps,nextState),返回true表示重新渲染,返回false表示不重新渲染
    触发时机:在更新阶段的钩子函数,组件重新渲染前执行(shouldComponentUpdate - - > render)

组件内部更新渲染

  • state变化就会重新渲染视图

避免组件内部不必要的重新渲染:组件中只有一个DOM元素发生更新时要把整个组件内容重新渲染

  • 使用虚拟DOM + Diff算法实现部分更新

  • 虚拟DOM:本质上是一个JS对象,用来描述HTML结构+数据

  • 部分更新执行过程
    1.组件render方法调用后,初次渲染,React会根据初始state(Model)和JSX结构创建一个虚拟DOM对象(树)
    2.根据虚拟DOM生成真正的DOM,渲染到页面中
    3.当使用setState使状态发生改变时,重新根据新的数据,创建新的虚拟DOM对象(树)
    4.与上一次得到的虚拟DOM对象,使用Diff算法对比(找不同),得到需要更新的内容
    5.React将变化的内容更新到DOM中,重新渲染到页面

  • render方法的调用并不意味着浏览器的重新渲染,仅仅说明要进行diff

在这里插入图片描述

五、路由

SPA:单页应用程序,就是一个html页面的应用程序,因为用户体验更好,为有效使单个页面来管理多页面的功能,就出现了路由
路由功能:一套映射规则,让URL路径组件相对应,让用户从一个视图(页面)导航到另一个视图(页面)
使用步骤

  • 1.安装:yarn add react-router-dom
  • 2.导入路由三个核心组件:import { BrowserReoter as Router, Route, Link } from "react-router-dom";
  • 3.使用Router组件包裹整个应用
  • 4.使用Link组件作为导航菜单(路由入口),最后会转换为href :<Link to="/first">跳转到页面一</Link>
  • 5.使用Route组件配置路由规则和要展示的组件(路由出口):<Route path="/first" component={First}></Route>

** 路由执行过程**

  • 点击Link组件(a标签),修改浏览器地址栏中的url:
  • React路由监听到地址栏url的变化
  • React路由内部遍历所有Route组件,使用路由规则(path)与pathname进行匹配
  • 当路由规则(path)能够匹配地址栏中的pathname时,就展示Route组件所在的内容

常用组件说明

  • Router组件:包裹整个应用,一个React应用只需要使用一次
  • 两种常用Router:HashRouter和BrowerRouter
    HashRouter:使用URL的哈希值实现,http://loaclhost:3000/#/first
    BrowerRouter:使用URL的哈希值实现,http://loaclhost:3000/first

编程式导航:通过JS代码来实现页面跳转

  • this.props.history.push('/home'):去/home路由匹配的组件页面
  • this.props.history.go(-1):去上一个页面

默认路由:刚进入页面就会显示的页面组件

  • <Route path="/" component={First}></Route>

模糊匹配:默认情况下React路由是模糊匹配的(新版的不会)

  • 只要pathname【Link组件的to属性】以path【Route组件的path属性】开头就会匹配成功
    在这里插入图片描述
    精确匹配:给Route组件添加exact属性
  • pathname【Link组件的to属性】和path【Route组件的path属性】必须一致才会匹配成功
  • <Route path="/" component={First} exact></Route>
import React from "react";
import { BrowserRouter as Router, Route, Link } from "react-router-dom";
// import { HashRouter as Router, Route, Link } from "react-router-dom";

const First = () => <p>页面一的内容</p>;
const Login = (props) => {
  const handleLogin = () => {
    // 编程式导航,去路由为/home的页面
    props.history.push("/home");
  };
  return (
    <>
      <p>登录页面</p>
      {/* 点击登录按钮跳转到后台主页面 */}
      <button onClick={handleLogin}>登录</button>
    </>
  );
};
const Home = (props) => {
  const handleBack = () => {
    // 编程式导航:返回上一个页面
    props.history.go(-1);
  };
  return (
    <>
      <p>后台首页</p>
      {/* 点击登录按钮返回到上一个登录页面 */}
      <button onClick={handleBack}>返回</button>
    </>
  );
};
const App = () => {
  <Router>
    <div>
      {/* 指定路由入口 */}
      <Link to="/first">跳转到页面一</Link>
      {/* 指定路由出口 */}
      {/* 点击Link后会跳转到/first路径,并将First这个组件显示到此页面 */}
      <Route path="/first" component={First}></Route>

      <h1>编程式导航:</h1>
      <Link to="/login">去登录页面</Link>
      <Route path="/login" component={Login} />
      <Route path="/home" component={Home} />
    </div>
  </Router>;
};

export default App;

六、Hook

系统自带Hook与自定义Hook都只能在1.其他Hook中运行 2.组件中运行

6.1.系统自带Hook
6.1.1.useState
6.1.2.useEffect
6.2.自定义Hook

Customer Hook是最优秀的组件代码复用方案,自定义组件必须以use开头

6.2.1.useMount

封装仅执行一次的useEffect,避免出现多个依赖为空数组的useEffect
如果不加use会被eslint认为是一个普通函数,而不是一个hook,在里面使用其他的hook就会报错
正常开发中直接使用带空数组的useEffect即可

export const useMount = (callback) => {
  useEffect(() => {
    callback();
  }, []);
};
useMount(() => {
  fetch(`${apiUrl}/users`).then(async (response) => {
    if (response.ok) {
      setUsers(await response.json());
    }
  });
});
6.2.2.useDebounce

防抖处理:任务频繁触发的情况下,只有任务触发的间隔超过指定间隔的时间,才执行代码一次,防止用户频繁操作,比如:按钮点击、文本编辑保存等

// useDebounce:连续执行多次事件,设置一个定时器,在此次任务执行完后,清除上一次的定时器id,无论执行多少次,最后都只会剩下一个定时任务,只会发一次请求
// value是useEffect的依赖,delay是防抖时间
export const useDebounce = (value, delay) => {
  const [debounceValue, setDebounceValue] = useState(value);
  useEffect(() => {
    // 每次在value变化之后设置一个定时器
    const timeout = setTimeout(() => setDebounceValue(value), delay);
    // 每次在上一个useEffect处理完以后再运行:清理上一个定时器id
    return () => clearTimeout(timeout);
  }, [value, delay]); // 监听value变化时执行useEffect,delay一般不会变化
  // 只有最后一个debounceValue能够保存下来并返回出去
  return debounceValue;
};
// 原来被监听的param
const [param, setParam] = useState({
 name: "",
  personId: "",
});
// 被useDebounce处理后的param
const debouncedParam = useDebounce(param, 1000);
useEffect(() => {
  fetch(
    `${apiUrl}/projects?${qs.stringify(cleanObject(debouncedParam))}`
  ).then(async (response) => {
    if (response.ok) {
      setProjects(await response.json());
    }
  });
}, [debouncedParam]); // 只监听最后一个定时器返回的param,只发送一次请求
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值