前端开发基础(5)React


React 是一个用于构建用户界面的 JavaScript 库,主要是通过组件来构建 UI 界面。

一. 基本概念

1. JSX

JavaScript 的语法扩展。在 JS 中使用类似 html 的结构。JSX 代码本身不能被浏览器读取,必须使用 Babel 和 webpack 等工具将其转换为传统的 JS。JSX 中可以嵌入 JS 表达式,用 { }包裹,字符串插值 `${ }` 。

const element = <h1>Hello, world!</h1>;		// 字符串插值 `Hello, ${str}`

2. Props

React 通过 props 实现父子组件之间的通讯。由于 React 采用单向数据流的模式,所以子组件只能读取从父组件传递的 props 的值,而不能直接更改。所以说 props 是不可变的,子组件要想改变数据,就需要父组件传递新的 props 对象。

// 除了可以传递 props 给子组件之外(Avatar),还可以将 JSX 作为子组件传递(Card)
// 可以将带有 children prop 的组件看作有一个“洞”,可以由其父组件使用任意 JSX 来“填充”

const Card = ({ children }) => {
  return (
    <div className="card">
      {children}
    </div>
  );
}

const Avatar = ({size}) => {
  return (
	<img 
	  width={size}
	  height={size}
	  src=""	
	/>
  )
}

export default function Profile() {
  return (
    <Card>
      <Avatar size={100} />
    </Card>
  );
}

3. State

React 通过 state 来实现响应式页面,当 state 的值发生变化时,React 会自动重新渲染组件,并更新与该 state 相关的部分。

4. 虚拟 DOM

由 React 元素构成的轻量级 JavaScript 对象。当组件的 state 或 props 发生变化时,React 会重新渲染组件,生成新的虚拟 DOM树,通过比较前后两个虚拟 DOM 树的差异,React 会确定需要更新的部分,生成一系列更新操作指令,应用于实际的 DOM 元素。

二. Hooks

Hook 是 React16.8 新增的特性,在此之前,只有类组件可以使用 state 管理状态,Hook 使函数组件可以使用 state 和其他 React 特性。
Hook 只能在函数顶层或者自定义 Hook 中调用。

1. useState

https://react.docschina.org/learn/state-as-a-snapshot

import React, { useState } from 'react';

export default () => {
  const [count, setcount] = useState(0);
  const increase = () => setCount(count + 1);
		
  return (
	<Button onClick={increase}>{count}</Button>
  )
}
  • useState 返回两个值,[ count, setcount ] 其实是数组的解构赋值。

  • setCount 只能在事件处理函数(onChange 等)和生命周期方法(useEffect)中调用。

  • 一个 state 变量的值永远不会在一次渲染的内部发生变化, 即使其事件处理函数的代码是异步的。 设置 state 并不会改变现有的 state 值,而是会触发重新渲染,重新渲染后的 state 值是计算后的结果。

    • 批量更新:当调用 setCount 多次时,React 会将这些更新操作放入一个队列中,并在适当的时机执行批量更新,对多条操作进行合并,只针对最后一条更新进行操作。
    const increase = () => {
      setCount(count + 1);
      setCount(count + 2);
      setCount(count + 1);	// 页面展示 1,由于批量更新,最后只会执行最后一条setCount,使count加一
      console.log(count);	// 后台输出 0,count的值在一次渲染中始终固定为0
    }
    
    • 想在下次渲染之前多次更新同一个 state,可以像 setCount(n => n + 1) 这样传入一个根据队列中的前一个 state 计算下一个 state 的 函数,而不是像 setCount(count + 1) 这样传入下一个 state 值。
    const increase = () => {
      setCount(count + 2);
      setCount(n => n + 1);	// 前端展示为 3
    }
    

    注意比较上下两段代码的区别,搞明白它们的展示结果为什么不同

    const increase = () => {
      setCount(n => n + 1);
      setCount(n + 2);	// 前端展示为 2
    }
    
  • 更改 state 中的对象,需要拷贝创建一个新的对象并把它传给 state 的设置函数。

const [person, setPerson] = useState({
  name: 'Niki de Saint Phalle',
  artwork: {
    title: 'Blue Nana',
    city: 'Hamburg',
    image: 'https://i.imgur.com/Sd1AgUOm.jpg',
  }
});

const updateCity = (e) => {
  setPerson({
	...person,				
	artwork: {
	  ...person.artwork,
	  city: e.target.value,	
	}
  })
}

除此之外,还可以使用 Immer 更加简洁的更新。

npm install use-immer

import { useImmer } from 'use-immer'

const [person, updatePerson] = useImmer({
  name: 'Niki de Saint Phalle',
  artwork: {
  	title: 'Blue Nana',
    city: 'Hamburg',
    image: 'https://i.imgur.com/Sd1AgUOm.jpg',
  }
});

const updateCity = (e) => {
  updatePerson(draft => {
    draft.artwork.city = e.target.value;
  });
}
  • 更改 state 中的数组,不能直接修改原数组,而是要创建一个新的数组来修改 state。即使拷贝了数组,也不能不能直接修改其内部的元素,可以通过 [ …arr, newItem] 来向数组中添加元素,可以通过 filter() 和 map() 来创建一个删除或修改元素后的数组。
const [ products, setProducts ] = useState([
  { id: 0, name: 'Baklava', count: 1},
  { id: 1, name: 'Cheese', count: 5},
  {id: 2, name: 'Spaghetti', count: 2},
])

const increase = (productId) => {
  const newList = products.map(product => {
	if (product.id === productId){
	  return { ...product, count: product.count + 1}
	} else {
	  return product
	}
  })
  setProducts(newList);
}
避免使用 (会改变原始数组)推荐使用 (会返回一个新数组)
添加元素push,unshiftconcat,[…arr] 展开语法
删除元素pop,shift,splicefilter,slice
替换元素splice,arr[i] = … 赋值map
排序reverse,sort先将数组复制一份

2. useEffect

  • 基本用法:作用类似 class 的生命周期函数
import React, { useEffect } from 'react';

useEffect(() => {
  setup();
  return () => {
    cleanup();
  };
}, [dependencies?]);	// 如果不传递依赖项数组,Effect会在组件每次重新渲染后运行

/*
1. 组件挂载到页面时运行 setup 代码
2. 依赖项dependencies发生变化触发重新渲染
   首先,使用旧的props和state运行cleanup代码
   然后使用新的props和state运行setup代码
3. 组件从页面卸载后,最后运行一次 cleanup 代码
*/
  • useEffect 的作用是让组件和外部系统保持同步。这里的外部系统是指任何不受 React 控制的代码,如网络 API、浏览器 API 或者第三方库。如果没有连接外部系统,有可能不需要使用 Effect。

3. useRef

  • 如果希望组件记住一些数据,当这条数据用于页面渲染时,要将它保存在 state 中。当这条数据只被事件处理器需要,改变它不需要重新渲染时,使用 Ref 更高效。
import React, { useState, useRef } from 'react';

export default function Stopwatch = () => {
  const [startTime, setStartTime] = useState(null);
  const [now, setNow] = useState(null);	// 由此计算的secondsPassed需要在浏览器渲染,所以使用useState
  const intervalRef = useRef(null);		// intervalID只在事件处理中使用,不需要渲染,所以使用useRef

  const handleStart = () => {
    setStartTime(Date.now());
    setNow(Date.now());

    clearInterval(intervalRef.current);
    intervalRef.current = setInterval(() => {	// 可以修改current的值
      setNow(Date.now());
    }, 10);
  }

  const handleStop = () => {
    clearInterval(intervalRef.current);
  }

  // secondsPassed 可以根据现有的state计算得出,所以不需要使用useEffect,而是在渲染的过程中直接计算
  let secondsPassed = 0;
  if (startTime != null && now != null) {
    secondsPassed = (now - startTime) / 1000;	
  }

  return (
    <>
      <h1>时间过去了: {secondsPassed.toFixed(3)}</h1>
      <button onClick={handleStart}>
        开始
      </button>
      <button onClick={handleStop}>
        停止
      </button>
    </>
  );
}
  • ref 和 state 的区别
    • useRef(value) 返回 { current: value },useState 返回 [ value, setValue ]
    • ref 可以在渲染过程中修改 current 的值,不会触发重新渲染;state 只能使用 setValue 修改 value 的值,从而重新渲染页面。
  • 如果需要访问一个由 React 管理的 DOM 元素,React 没有内置的方法,所以需要一个指向 DOM 节点的 ref 来实现。
import React, { useRef } from 'react';

export default function Form() {
  const inputRef = useRef(null);

  const handleClick = () => {
    inputRef.current.focus();	// 可以使用任意浏览器 API
  }

  return (
    <>
      <input ref={inputRef} />	// 传递ref使React将ref的current属性设置为相应的DOM节点(<input/>)
      <button onClick={handleClick}>
        聚焦输入框
      </button>
    </>
  );
}
  • 默认情况下,React 不允许访问其他组件的 DOM 节点,即使是自己的子组件。所以父组件传递 ref 参数需要用到 forwardRef API。
import React, { forwardRef, useRef } from 'react';

const MyInput = forwardRef((props, ref) => {
  return <input {...props} ref={ref} />;
});

export default function Form() {
  const inputRef = useRef(null);

  const handleClick = () => {
    inputRef.current.focus();
  }

  return (
    <>
      <MyInput ref={inputRef} />
      <button onClick={handleClick}>
        聚焦输入框
      </button>
    </>
  );
}

  • 配合 useImperativeHandle,向父组件暴露自定义的 ref 句柄,使父组件能够调用子组件中的方法
import { forwardRef, useRef, useImperativeHandle } from 'React';
	
const MyInput = forwardRef((props, ref) => {
  const handleSearch = () => {
	// ...
  }
	
  useImperativeHandle(ref, ()=> ({
	handleSearch,
  }));
  return <input onchange={handleSearch}/>;	// 注意,ref 已不再被转发到 <input> 中
});

export default function Form() {
  const inputRef = useRef(null);

  return (
    <>
      <MyInput ref={inputRef} />
      <button onClick={inputRef.current.handleSearch()}>
        搜索
      </button>
    </>
  );
}

4. useContext

通常来说,父组件通过 props 向子组件传递数据。但是,如果必须通过很多的中间组件向下传递props,会使代码变得十分冗长,这时可以使用 useContext。

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

const ThemeContext = createContext(null);	// 1. 创建context

export default function MyApp() {
  const [theme, setTheme] = useState('light');
  
  return (
    <ThemeContext.Provider value={theme}>	// 2. 把子组件用 context provider 包裹起来,以提供ThemeContext给它们,如果任意子组件中请求ThemeContext,就给它们theme的值
      <SignUp />
      <Checkbox
		checked={theme === 'dark'}
        onChange={(e) => {
          setTheme(e.target.checked ? 'dark' : 'light')
        }}
	  >暗黑模式</Checkbox>
    </ThemeContext.Provider>
  )
}

const SignUp = ({ children }) => {
  return (
    <Panel title="Welcome">
      <Btn>注册</Btn>
      <Btn>登录</Btn>
    </Panel>
  );
}

const Panel = ({ title, children }) => {
  const theme = useContext(ThemeContext);	// 3. 使用context,使用上层最近的<ThemeContext.Provider>传递过来的值
  const className = 'panel-' + theme;
  return (
    <div className={className}>
      <h1>{title}</h1>
      {children}
    </div>
  )
}

const Btn = ({ children }) => {
  const theme = useContext(ThemeContext);
  const className = 'button-' + theme;
  return (
    <Button className={className}>
      {children}
    </Button>
  );
}

5. useMemo

  • 默认情况下,React 每次重新渲染都会重新运行整个组件。如果某些计算代价昂贵,而数据没有变化,可以将计算函数包裹在 useMemo 中,跳过重复计算以实现性能优化。
import { useMemo } from 'React';

const TodoList = ({todos, theme, tab }) => {
  const visibleTodos = useMemo(
    () => filterTodos(todos, tab),
    [todos, tab]
  );
  return (
	<ul>
      {visibleTodos.map(todo => (
        <li key={todo.id}> { todo.text } </li>
      ))}
    </ul>
  )
}

React 首次渲染时会调用 filterTodos 函数,在之后的渲染中,如果依赖列表没有发生变化,React 将直接返回相同值。否则将再次调用 filterTodos 函数返回最新结果,然后缓存该结果以便下次重复使用。

  • 默认情况下,当一个组件重新渲染时,React 会重新渲染它的所有子组件,即使子组件的 props 没有发生变化。这时,可以通过将子组件包裹在 memo 中,使得 props 不变时跳过本次渲染。
import React, { memo, useMemo } from 'react';

const List = memo(function List({ items }) {
  // ...
});

export default function TodoList({todos, theme, tab}){
  const visibleTodos = useMemo(		// 此处需要使用useMemo是因为引用数据类型每次计算都会得到一个不用的数组,即使子组件使用memo包裹也会重新渲染
  	() => filterTodos(todos, tab), 
  	[todos, tab]
  );
  return (
	<List items={visibleTodos} />
  )
}

6. useCallback

useCallback 和 useMemo 的用法类似,也是一个性能优化的 hook。不同之处在于,useMemo缓存函数调用的结果,而 useCallback 缓存函数本身。
在父组件将函数作为 props 传递给子组件的过程中,父组件每次重复渲染都会生成一个不同的函数(引用数据类型,引用地址变动),即使子组件使用 memo 包裹,也会重复渲染。这时就需要 useCallback 和 memo 配合来使依赖项不变时跳过此次渲染。

import React, { useCallbck, memo } from 'react';

const ShippingForm = memo(function ShippingForm(onSubmit){
	// ...
});

export default function ProductPage({ productId, theme }) {
  const handleSubmit = useCallback((orderDetails) => {
    post('/product/' + productId + '/buy', {
      orderDetails,
    });
  }, [productId]);

  return (
	<ShippingForm onSubmit={handleSubmit} />
  )

使用 useCallback 时,如果没有将依赖数组指定为第二个参数,那么每次渲染都会返回一个新的函数。依赖数组为空数组时,会在组件首次渲染时创建一次函数,之后重复使用。

7. 自定义Hook

  • 如果多个组件之间共用一套逻辑,那么可以从组件中提取自定义 Hook 来简化重复代码
  • 自定义 Hook 的名称必须永远以 use 开头
  • 没有调用 Hook 的函数不需要写成 Hook,可以直接写成一个通用的普通函数
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值