React函数组件的学习

101 阅读13分钟

设计理念

UI = f( data ) =>为了 逻辑拆分与重用的组件表达形式

目的

为函数组件提供状态

解决了什么问题?

  1. 逻辑的复用

在hook出现之前,react先后尝试了mixins混入 => 数据来源不清晰,HOC高阶组件 => 嵌套问题,render-props等模式,

  1. class组件自身的问题

生命周期和this指向

缺陷

  • 组件不会被重新渲染
  • 假如会重新渲染 => 函数会被重新执行,会重新给变量赋值为初始值(函数每次调用都会产生新的临时变量)
  • 类似于生命周期的回调函数,也是没有的
function HelloWord(props){
	les msg = 'hello word' //重新执行,又会被重新初始化赋值
	return(
		<div>
			{msg}
			<button onClick={e => msg ='aaa'}></button>
		</div>
	)
}
import {memo} from 'react'
function CounterHook(props){
   
}
export default memo(CounterHook)

使用规则

  • 可以在自定义hook(use开头命名)的函数中使用,普通函数中不能使用

useState

使用和返回值

import {useState} from 'react'
const [count, setCount]= useState(0);

=>
[count, setCount] 是结构赋值的写法,
useState() => 初始值为undefined
useState返回值是一个数组
名字可以自定义吗? => 可以,自定义保持语义化
顺序可以换吗? => 不可以,第一个参数是数据状态,第二个参数是修改数据的方法

setCount( 基于原值计算得到的新值 )
=> 作用: 用来修改count
=> 不能直接修改原值,应为生成一个新值覆盖原值

状态的读取和修改

const [count, setCount]= useState(0);
{count} // 0
<button onClick={e => setCount(count + 1)}>++</button>

当调用setCount时,组件的更新过程

1.首次渲染

组件内部的代码会被执行一次,其中useState也会跟着执行

2.更新渲染

setCount都会更新

app组件再次渲染,这个函数会再次执行,count用新值渲染

注意

  1. useState可以执行多次 => 可以有多个数组
  2. 只能出现在函数组件中
  3. 不能嵌套在if/for其他函数中 => react按照hooks的调用顺序识别每一个hook
let num = 1;
function List() {
	num++
	// 不能这样写在if函数中
	if(num / 2 === 0){
		const[name, setName] = useState('cp');
	}
	const [list, setList] = useState([])
}
//两个hook的顺序不是固定的,这是不可以的
  1. hook可以写在函数里吗? => 不行
//以下写法是不行的
function App () {
	const [count, setCount] = useState(0);
	return(
		<button onClick={() => {setCount(count + 1)}}></button>
	)
}

useEffect

为react函数组件提供副作用处理

一个函数组件中,可以存在多个useEffect(逻辑分离)

=> 多个useEffect按顺序依次执行

=> 每个useEffect可以抽到自定义hook中

DOM把我该显示的东西已经显示到界面上之后才会触发该回调

副作用

对于react组件来说

主作用: 根据数据(state/props)渲染UI

副作用 side effects:

  1. 数据请求ajax发送;
  2. 2.手动修改dom;
  3. 3.localstorage操作

使用依赖项控制执行时机

  1. 不添加依赖项

组件首次渲染完成后执行一次,以及不管是那个状态更改引起组件更新时都会重新执行

  1. 组件初始渲染
  2. 组件更新(不管是哪个状态引起的更新)
useEffect(() => {
	console.log('副作用执行') // 函数组件的本质是为了渲染当前组件视图(DOM操作),所以一切副作用都是完成渲染后执行的?
})

effect的清除机制

// 返回值:回调函数
//=> 组件被重新渲染或者组件卸载时执行
useEffect(() => {
    //1.监听事件
    console.log('监听redux中数据变化,监听eventBus中的事件')
    //2.取消监听(可选的清除机制)
    return () => { // 对比类组件,要在componentDidMount添加监听,componentWillUnmount中取消监听,增加了代码的内聚性
     // 在这进行取消监听的操作   
    }
})
  1. 添加空数组

组件只在首次渲染时执行一次

useEffect(() => {
	console.log('副作用执行了')//模拟componentDidMount
    return () => {
        //模拟 componentWillUnmount
    }  
},[])
  1. 添加特定依赖项

副作用函数在首次渲染时执行,在依赖项发生变化时重新执行

该useEffect在哪些state发生变化时,才重新执行(受谁的影响)

function App() {
	const [count, setCount] = useState(0)
	useEffect(() => {
		console.log('副作用执行了')
	},[count])
	return(
		<div>
			<button onClick={() => setCount(count + 1)}></button>
		</div>
	)
}

注意

useEffect回调函数中用到的数据(比如,count)就是依赖数据,就应该出现在依赖数组中,如果不添加依赖项,就会有bug出现

hook的出现,就是不想用生命周期概念也可以写业务代码

//首次渲染初始化 + count/name被修改时,都会执行
useEffect(() => {
		console.log('副作用执行了')
	},[count,name])

多个useEffect,逻辑分离


useContext

通过hook来直接获取某个Context的值

在以前的开发中,组件中使用共享的Context有两种方式

  1. 类组件通过 类名.contextType = MyContext方式,在类中通过this.context获取context
  2. 多个Context或者函数式组件中通过MyContext.Consumer方式共享context
//1.新建文件放置context文件
import {createContext} from 'react'
const UserContext = createContext;
const ThemeContext = createContext;
export {UserContext, ThemeContext}
//2.包裹祖先组件
<UserContext.Prowider value={{name:'haha',level:99}}>
	<ThemeContext.Provider>	
		<App />
    </ThemeContext.Provider>
</UserContext.Prowider>
//3.在需要使用context的子组件中,使用
import {useContext} from 'react'
import {UserContext, ThemeContext} from './context'

const App = memo(() => {
	const user = useContext(UserContext) //当useContext依赖的数据UserContext发生改变的时候,会根据最新值重新render
	const theme = useContext(ThemeContext)
	
	return(
		<div>
			{user.name}---{user.level}
			<h2 style={{color:theme.color, fontSize: theme.size}}>Theme</h3>
		</div>
	)
})
// 当组件上层最近的<MyContext.Provider>更新时,该Hook会重新触发渲染,并使用最新传给MyContext.provider的context 值

useReducer

是useState的一种替代方案

1.如果state的处理逻辑比较复杂 => 通过useReducer来对其进行拆分

2.这次修改的state需要依赖之前的state时

function reducer (state,action) {
    switch(action.type){
    case "increment":          return{...state,counter:state.counter+1}
    case "decrement":
 return {...state,counter:state.counter -1}
        case "add_num":
     return {...state,counter:state.counter + action.num}
      default :
            return state
    }   
}

const App = memo(() => {
    const [state,dispatch] = useReducer(reducer, { counter:0})
    const [counter, setCounter] = useState(0)
})

未写完


useCallback / useMemo

用来做性能优化

useCallback(fn, deps) === useMemo( () => fn, deps)

useCallback

返回一个函数的memoized(有记忆的)回调函数

在依赖不变的情况下,多次定义的时候,返回的值时相同的

const [count, setCount] = useState(0)
// 每次调用都会创建一个函数,但是如果没有引用指向的函数,会被立即销毁
//useCallback,不会多次创建,每次访问的都是同一个函数
const increment = useCallback(function 
// increment函数命名可以省略(匿名函数)
increment(){
	setCount(count + 1)
},[])//在依赖不发生改变的时候,useCallback依然是之前的那个
  • 闭包陷阱
// 原理
function foo (name) {
    function bar(){
        console.log(name)
    }
    return bar
}
const bar1 = foo('why')
bar1() //why
bar1() // why
const bar2 = foo('kobe')
bar2() // kobe
bar1() //why => 并不会打印kebe
// 闭包陷阱的产生
const [cout, setCount] = useState(0)
const increment = useCallback(function foo() {
    console.log('increment') //正常打印
    setCount(count + 1) //永远是1,因为拿到的count都是第一次的值;由于没有添加依赖,所以每次不会重新创建新的函数,但是cout发生改变的时候,没有传入新函数,会触发闭包陷阱
},[])

// 闭包陷阱的解决1:添加依赖count
const increment = useCallback(function foo() {
    console.log('increment') //正常打印
    setCount(count + 1) //正常更新,传入了count,其实每次都会创建一个新的函数foo; 会造成子组件多次渲染
},[count])

// 闭包陷阱的解决2:使用useRef,不添加依赖,手动更新每次的值
const countRef = useRef()
countRef.current = count //手动更新
const increment = useCallback(function foo() {
    console.log('increment') //正常打印
    setCount(countRef.current + 1) //正常更新,不会重新渲染HYHome
},[])

// 父组件
{count}
<button onClick = {increment}>+1</button>
<HYHome increment = {increment} /> 	

// 子组件
const HYHome = memo(function(props){
    const {increment} = props
    console.log('HYHome被渲染')
    return(
     <button onClick = {increment}>increment+1</button>
    )
})

useMemo

优化的不是内部的回调函数,优化的是回调函数的返回值

const App = memo(() => {
    const [count, setCount] = useState(0)
    
    let result = useMemo(() => {
        return calcNumTotal(50);// 计算0-50累加和的函数 => 不希望每次都重新计算一次累加和函数
    },[]) //参数2: 依赖项 => 空数组则只会调用一次calcNumTotal
    return(
        <div>
            {result}
             <button onClick = {increment}>increment+1</button>
        </div>
    
    )
})
// 案例2:使用useMemo对子组件渲染进行优化

// 父组件
const info = {name: '222'}
<Son info={info} /> //这样直接传入,每次都是一个新对象,每次父组件视图更新,子组件也会更新 => 如果是基本类型的传递,则用不用useMemo都不会重新渲染子组件

const info = useMemo(() =>(
{name: '222'})) // 不会重新渲染子组件

useCallback和useMemo总结

代替

const increment = useCallback(fn, []) //返回值是函数
//等同于
const increment2 = useMemo( () => fn, [])

优化以及应用场景

useCallback

useCallback定义函数,并不会对函数有什么优化

当把我们返回的函数,传递给子组件的时候才会对性能有优化

=> 不希望子组件进行多次渲染,并不是为了函数缓存

useMemo

进行大量的计算操作,是否有必要每次渲染都重新计算?

对子组件传递相同内容的对象时,使用useMemo进行性能优化

useRef

useRef 返回一个ref对象,返回的ref对象在组件的整个生命周期保持不变

用法:

1:引入DOM(或者组件,但是需要是class组件)元素

2:保存一个数据,这个对象在整个生命周期中可以保存不变

应用1:绑定DOM

const App = memo(() => {
    const titleRef = useRef()
    const inputRef = useRef()
    function showTitleDom(){
        console.log(titleRef.current) //<h2>HELLO word</h2>
        inputRef.current.focus() //点击按钮,获取输入框的焦点
    }
    return(
    	<div>
        	<h2 ref={titleRef}>HELLO word</h2>
            <input type="text" ref={inputRef} />
            <button onClick={showTitleDom}>查看title的dom</button>
        </div>
    )
})

应用2:解决闭包

// 1.如果通过字面量的方式创建一个对象,函数每次重新执行render的时候,都会创建一个新的对象
const nameRef = {}

// 2.如果创建的对象是ref对象,则在整个生命周期里面,都是同一个ref对象
	// a === b : 两个引用指向同一个对象
const nameRef = useRef()

// 3:解决闭包
const [count, setCount] = useState(0)
const countRef = useRef()
countRef.current = count
const increment = useCallback(() => {
    setCount(countRef.current + 1)
},[])

return <div>
        	<h2 }>HELLO word{count}</h2>
            <button onClick={increment}>+1</button>
        </div>

useImperativeHandle

子组件对父组件传进来的ref进行处理

父组件中绑定子组件的方法

// 子组件
const Son = memo(forwardRef((props,ref) => {
    const inputRef = useRef() // 子组件自己绑定dom实例
    
    // 对父组件传入的ref进行处理;参数1:父组件传入的ref,参数2:对象=> 暴露给父组件的方法(只允许父组件触发的方法)
    useImperativeHandle(ref, () => {
        return {
            focus(){
                inputRef.current.focus();
            },
            setValue(value) {
                inputRef.current.value = value;//父组件点击按钮,只能触发focus和setValue,其余的操作都做不了
            }
        }
    })
    
    return <input type='text' ref={inputRef} />
}))

// 父组件
const App = memo(() => {
    const inputRef = useRef()
    
    function handleDOM(){
        inputRef.current.focus;
        inputRef.current.setValue('hahahaha') // 设置输入框的内容
    }
    return (
        <button onClick={handleDOM(){

}} >按钮</button>
    	<Son ref={inputRef} /> // 不能直接绑定ref,要把inputRef传给子组件
    )
})

如何绑定子组件里面的dom实例?

forwardRef

// 子组件要用forwardRef包裹,参数2:传进来的ref
const Son = memo(forwardRef((props,ref) => {
    return <input ref={ref}/>
}))

// 父组件
const App = memo(() => {
    const inputRef = useRef()
    
    return (
        
    	<Son ref={inputRef} /> // 不能直接绑定ref,要把inputRef传给子组件
    )
})

useLayoutEffect

useEffect会在渲染的内容更新到DOM上后执行,不会阻塞DOM更新

useLayoutEffect会在渲染的内容更新到DOM上之前执行,会阻塞DOM更新

  • 应用场景
    • 希望某些操作发生之后再更新DOM
    • 触发顺序 => 1:render, 2: useLayoutEffect, 3:useEffect

自定Hook

本质

自定义Hook本质上只是一种代码逻辑的抽取,严格意义上并不算react的特性

// 需求:组件每次被创建和销毁时都做一次打印
function useLogLife (cName) {
    useEffect(() =>{
        console.log(cName+'组件被创建')
        return () => {
            console.log(cName+'组件被销毁')
        }
    },[cName]) // [] 也可以
}

// 自定义hook的使用

const App = memo(() =>{
    useLogLife('App') // 直接使用,比高阶组件方便
})
// context共享
// 1.创建一个context文件
import { createContext } from 'react'
const UserContext = createContext()
const TokenContext = createContext() // 用户信息

export {UserContext,TokenContext}

// 2.在最外层的根组件中导入
import {UserContext,TokenContext} from './xxx';
	//对父组件做一个包裹
<UserContext.Provider value={}>
	<TokenContext.Provider>
    	<App />
    </TokenContext.Provider>
</UserContext.Provider>

// 3.配置自定义hook
	// 3.1 创建一个hooks文件夹
	// 3.1.1 创建useUerToken.js
import {useContext} from 'react'
import {UserContext,TokenContext} from './xxx'
function useUserToken(){
   const user = useContext(UserContext)
   const token = useContext(TokenContext)
   return [user,token]
}
export default useUserToken
	// 3.1.2 创建index.js 作为统一导出出口
import useUserToken from './useUserToken';
export {useUserToken}

// 4.在组件中使用
import {useUserToken} from './hooks'

const Home = memo(() => {
    const [user,token] = useUserToken
    return <h1>{user.name}</h1>
})
// 获取窗口滚动位置
/* 滚动的时候,window和document有什么区别?
=> 1. 窗口发生滚动,会把这个事件传递给document
*/

//1. 创建useScrollPosition.js
function useScrollPosition () {
    const [scrollX, setScrollX] = useState(0);
    const [scrollY, setScrollY] = useState(0);
    useEffect(() => {
        function handleScroll() {
            //console.log(window.scrollX, window.scrollY)
            //记录滚动位置
            setScrollX(window.scrollX);
            setScrollY(window.scrollY)
        }
        window.addEventListener('scroll', handleScroll)
        return () => {
            window.removeEventListener('scroll', handleScroll)
        }
    },[])
    return [scrollX, scrollY]
}
export { useScrollPosition}

// 使用
const App = memo(() => {
    const [scrollX, scrollY] = useScrollPosition()
    return <div>{scrollY}</div>
})
// 本地存储:通过一个key直接从localStorage中获取一个数据 => 将state和localstorage产生联系

function useLoaclStorage(key) {
    // 1.从localStorage中获取数据,并且创建组件的state
    const [data, setData] = useState(JSON.parse(localStorage.getItem(key)))// 转对象
    // 副作用: 设置localStorage(监听data变化,一旦发生变化就储存data最新值)
    useEffect(() => {
        localStorage.setItem(key, JSON.stringify(data)) // 转字符串
    },[data])
    //返回给组件,让组件可以使用和修改data
    return [data, setData]
}
export default useLoaclStorage

// token和data名字不一样有关系吗? 没事, 函数中用了解构
const [token , setToken] = useLoaclStorage('token')


// 注意: useState允许传入一个函数,这个函数会被立即执行,1中的改写
  const [data, setData] = useState(() => {
      const item = localStorage.getItem(key)
      if(!item) return '' //一开始可能没有值 null不能执行parse操作
      return JSON.parse(item)
  })

redux hooks

redux7.1 => 提供了hook的方式,不需要编写connect以及对应的映射函数

useSelector

将state映射到组件中

参数1: 将state映射到需要的数据中

参数2:可以进行比较来决定是否需要组件重新渲染

useSelector会默认比较我们返回的两个对象是否相等

=> const refEquality =(a,b) => a === b ; 必须全等才不会引起重新渲染

useDispatch

直接获取dispatch函数

useld

用于ssr(服务器端渲染)

官方: useId是一个用于生成横跨服务端和客户端的唯一ID的同时避免hydration 不匹配的 hook

SSR

  • 什么是SSR => Server Side Rendering 服务器端渲染
    • 页面在服务器端已经生成了完整的HTML结构, 不需要浏览器解析
    • 对应的是CSR(Client Side Rendering),开发的SPA页面通常依赖的就是客户端渲染
  • 早期的客户端渲染:PHP, JSP, ASP; 现在使用Node执行js,提前完成页面渲染
  • SPA 单页面富应用的缺点
    • 首屏的渲染速度
      • 渲染步骤
        • 1.请求一个文件 index.html
        • webpack 的环境 => index.html
        • body => div id = root
        • 2.下载js文件, 浏览器执行一次js代码,创建相关dom生成html结构
    • seo优化
      • 爬虫只会下载index.html => body 和 meta里面的东西
  • SPA如何改进?
    • 用node提前下载js,给浏览器返回完整的html页面结构
    • 使用react / vue 提供的ssr API

SSR同构应用

应用程序的代码既可以在服务器端运行,又可以在客户端运行

特点

  • 同构是一种SSR的形态,是现代SSR的一种表现形式
    • 当用户发出请求时,先在服务器通过SSR渲染出首页的内容
    • 但是对应的代码同样可以在客户端被执行
    • 执行的目的包括事件绑定等以及其他页面切换时也可以在客户端被渲染

Hydration

在进行ssr时,页面呈现为HTML,为了使页面具有交互性,除了在Node.js中将页面呈现为HTML外,Vue / React还需要在浏览器中加载和呈现页面

创建页面的内部表示,然后将内部表示映射到我们在Node.js中呈现的HTML的DOM元素

这个过程叫Hydration