千峰React:Hooks(上)

什么是Hooks

ref引用值

普通变量的改变一般是不好触发函数组件的渲染的,如果想让一般的数据也可以得到状态的保存,可以使用ref

javascript 复制代码
import { useState ,useRef} from 'react'



function App() {
       const [count, setCount] = useState(0)
       let num = useRef(0)
       const handleClick = () => {
         setCount(count + 1)
         num.current++ //current,当前值
         console.log(num.current)
       }
  return (
    <div>
      <button onClick={handleClick}>计数</button>
    </div>
  )
}
export default App

那你就要问了:这和useState有什么区别?

更改时不会触发渲染,例如这里,我们注释掉setCount的语句:

javascript 复制代码
import { useState ,useRef} from 'react'



function App() {
       const [count, setCount] = useState(0)
       let num = useRef(0)
       const handleClick = () => {
        //  setCount(count + 1)
         num.current++ //current,当前值
         console.log(num.current)
       }
  return (
    <div>
          <button onClick={handleClick}>计数</button>
          {count},{num.current}
    </div>
  )
}
export default App

可以看出,num的值变了,但是并没有渲染出来

所以我们一般不会把ref写在jsx的结构里,但是可以在渲染过程之外随意修改和更新current的值,不需要像useState一样使用里面的方法修改的值才有效

如果我们在里面开启定时器,在这里一秒触发一次,如果单击按钮,num的值一秒增加一次,如果点击按钮多次,就会同时开启多个定时器,数值就会涨的飞快

javascript 复制代码
import { set } from 'lodash'
import { useState ,useRef} from 'react'

function App() {
    const [count, setCount] = useState(0)
    let timer=null
       let num = useRef(0)
       const handleClick = () => {
           setCount(count + 1)
           setInterval(() => {
            console.log(123)
           }, 1000);
       }
  return (
    <div>
          <button onClick={handleClick}>计数</button>
          {count},{num.current}
    </div>
  )
}
export default App

一般的解决办法是每次新开一个定时器,就关掉旧定时器

javascript 复制代码
import { set } from 'lodash'
import { useState ,useRef} from 'react'



function App() {
    const [count, setCount] = useState(0)
    let timer=null
       const handleClick = () => {
           clearInterval(timer)
           timer=setInterval(() => {
            console.log(123)
           }, 1000);
       }
  return (
    <div>
          <button onClick={handleClick}>计数</button>
          {count}
    </div>
  )
}
export default App

但是如果对整个函数重新调用(也就是启用useState)就无法销毁定时器了

因为整个函数重新调用,定时器是在上一次调用产生的,这一次删不掉上一次的定时器,作用域不同

javascript 复制代码
import { set } from 'lodash'
import { useState ,useRef} from 'react'



function App() {
    const [count, setCount] = useState(0)
    let timer=null
       const handleClick = () => {
           setCount(count + 1)
           clearInterval(timer)
           timer=setInterval(() => {
            console.log(123)
           }, 1000);
       }
  return (
    <div>
          <button onClick={handleClick}>计数</button>
          {count}
    </div>
  )
}
export default App

这时候就需要对timer做记忆,也就是使用ref,就算走了多次渲染函数,也可以销毁

javascript 复制代码
import { set } from 'lodash'
import { useState, useRef } from 'react'


function App() {
  const [count, setCount] = useState(0)
    let timer = useRef(null)
  const handleClick = () => {
    setCount(count + 1)
    clearInterval(timer.current)
    timer.current = setInterval(() => {
      console.log(123)
    }, 1000)
  }
  return (
    <div>
      <button onClick={handleClick}>计数</button>
      {count}
    </div>
  )
}
export default App

试了试,如果timer放全局定义是可以的,但是感觉这样不利于函数的封装性

javascript 复制代码
import { useState, useRef } from 'react'

let timer = null
function App() {
  const [count, setCount] = useState(0)

  const handleClick = () => {
    setCount(count + 1)
    clearInterval(timer)
    timer = setInterval(() => {
      console.log(123)
    }, 1000)
  }
  return (
    <div>
      <button onClick={handleClick}>计数</button>
      {count}
    </div>
  )
}
export default App

通过ref操作原生dom

react里可以使用js的方法操作dom,例如什么querySelector这种css选择器,但是更推荐我们使用React的方法操作dom

javascript 复制代码
import { set } from 'lodash'
import { useState, useRef } from 'react'

function App() {
  const myRef = useRef(null)
    const handleClick = () => {
        //通过ref操作原生dom
    console.log(myRef.current.innerHTML)
    myRef.current.style.background = 'red'
  }
  return (
    <div>
      <button onClick={handleClick}>计数</button>
      <div ref={myRef}>hello React</div>
    </div>
  )
}
export default App

这么写是会报错的,钩子是按顺序使用的,在循环里这么写会报错,所以这么写不符合规范;而且也不建议把const myRef=useRef(null)写在结构里

javascript 复制代码
import { set } from 'lodash'
import { useState, useRef } from 'react'

function App() {
  const list = [
    { id: 1, text: 'aaa' },
    { id: 2, text: 'bbb' },
    { id: 3, text: 'ccc' },
  ]
  const handleClick = () => {
    //通过ref操作原生dom
    console.log(myRef.current.innerHTML)
    myRef.current.style.background = 'red'
  }
  return (
    <div>
      {list.map((item) => {
        const myRef = useRef(null)
        return <li key={item.id} ref={myRef}>{item.text}</li>
      })}
    </div>
  )
}

export default App

在循环操作里ref可以使用回调函数的写法:

javascript 复制代码
import { useState, useRef } from 'react'

function App() {
  const list = [
    { id: 1, text: 'aaa' },
    { id: 2, text: 'bbb' },
    { id: 3, text: 'ccc' },
  ]
  return (
    <div>
      <ul>
        {list.map((item) => {
          return (
            <li
              key={item.id}
              ref={(myRef) => {
                myRef.style.background = 'red'
              }}
            >
              {item.text}
            </li>
          )
        })}
      </ul>
    </div>
  )
}

export default App

但是我报了很多错,说是

其实就算style在卸载的时候为空,加个逻辑判断就好了

铭记励志轩

javascript 复制代码
import { useState, useRef } from 'react'

function App() {
  const list = [
    { id: 1, text: 'aaa' },
    { id: 2, text: 'bbb' },
    { id: 3, text: 'ccc' },
  ]
  return (
    <div>
      <ul>
        {list.map((item) => {
          return (
            <li
              key={item.id}
              ref={(myRef) => {
                if (myRef) {
                  myRef.style.background = 'red'
                }
              }}
            >
              {item.text}
            </li>
          )
        })}
      </ul>
    </div>
  )
}

export default App

组件设置ref需要forwardRef进行转发

forwardRef 是 React 提供的一个高阶函数,用于将 ref 从父组件传递到子组件中的 DOM 元素或组件实例。它主要用于解决在函数组件中直接使用 ref 时无法访问子组件内部 DOM 元素的问题。

MyInput 组件中,ref 被绑定到 <input> 元素,也就是把ref转发到内部的input身上,然后=在 handleClick 函数中,ref.current 用于直接操作 <input> 元素:

javascript 复制代码
import {  useRef ,forwardRef} from 'react'

const MyInput = forwardRef(function MyInput(props, ref) {
  return (
  <input type='text' ref={ref}></input>
)
})

function App() {
  const ref = useRef(null)
  const handleClick = () => {
    ref.current.focus()
    ref.current.style.background='red'
  }
  return (
    <div>
      <button onClick={handleClick}>点我</button>
      <MyInput ref={ref} />
    </div>
  )
}

export default App

useImperativeHandle自定义ref

你想要它其中两个方法:focusscrollIntoView。为此,用单独额外的 ref 来指向真实的浏览器 DOM。然后使用 useImperativeHandle 来暴露一个句柄,它只返回你想要父组件去调用的方法

javascript 复制代码
import {  useRef ,forwardRef,useImperativeHandle} from 'react'

const MyInput = forwardRef(function MyInput(props, ref) {
  const inputRef=useRef(null)
  useImperativeHandle(ref,()=>{
    return{
     focus(){
       inputRef.current.focus()
      }
    }
  })

  return (
  <input type='text' ref={inputRef}></input>
)
})

function App() {
  const ref = useRef(null)
  const handleClick = () => {
ref.current.focus()
  }
  return (
    <div>
      <button onClick={handleClick}>点我</button>
      <MyInput ref={ref} />
    </div>
  )
}

export default App

点击按钮获取焦点

然后你就要问了(因为我也想问):这个获取焦点的操作为什么要写成这样?

这样会更加灵活,不用完全写在dom里,可以把你想要写的功能放在useImperativeHandle的第二个参数里,也就是里面的回调函数,来实现你自己的功能、自定义的功能

相当于你自己写了一个组件,可以像普通的组件使用,在里面添加属性和属性值,你也可以写自己的组件实现的功能

例如这个button的属性是他自己自带的,我们写的组件也可以写一些自己带的属性,focusscrollIntoView就是这个作用

javascript 复制代码
 <button onClick={handleClick}>点我</button>

比如我们再添加一个获取焦点以后背景变红色的功能:

javascript 复制代码
import {  useRef ,forwardRef,useImperativeHandle} from 'react'

const MyInput = forwardRef(function MyInput(props, ref) {
  const inputRef=useRef(null)
  useImperativeHandle(ref, () => {
    return {
      focus() {
        inputRef.current.focus()
      },
      focusAndStyle() {
        inputRef.current.focus()
        inputRef.current.style.background = 'red'
      }
    }
  })

  return (
  <input type='text' ref={inputRef}></input>
)
})

function App() {
  const ref = useRef(null)
  const handleClick = () => {
    if (ref) {
      ref.current.focusAndStyle()
    
}
  }
  return (
    <div>
      <button onClick={handleClick}>点我</button>
      <MyInput ref={ref} />
    </div>
  )
}

export default App

纯函数如何处理副作用

纯函数之前提过,只关注输入和输出、两次执行函数是否结果一样axios

上面图的意思是:组件是纯函数,但是有时候组件不得不写一些违背纯函数的功能,例如Ajax调用、还有我们用ref操作dom等都是副作用

事件本身可以去除副作用

javascript 复制代码
import { useRef, forwardRef, useImperativeHandle } from 'react'

function App() {
  const ref = useRef(null)
  //副作用,不符合纯函数的规范
  //        setTimeout(() => {
  //     ref.current.focus()
  // },1000)
  //副作用,但是符合纯函数的规范,因为事件可以处理副作用
  const handleClick = () => {
    ref.current.focus()
  }
  return (
    <div>
      <button onClick={handleClick}>点击</button>
      <input type='text' ref={ref} />
    </div>
  )
}

export default App

但不是所有的副作用都可以用事件去除,而且有时候需要初始化副作用

所以在react里时间操作可以处理副作用,比如useEffect钩子

javascript 复制代码
import { useRef, forwardRef, useImperativeHandle, useEffect } from 'react'

function App() {
  const ref = useRef(null)
  //副作用,不符合纯函数的规范
  //        setTimeout(() => {
  //     ref.current.focus()
  // },1000)
  //副作用,但是符合纯函数的规范,因为事件可以处理副作用
  //   const handleClick = () => {
  //     ref.current.focus()
  //   }
  //可以在初始的时候进行副作用操作
  //useEffect触发的时机是jsx渲染后触发的,这样就会消除副作用的影响
  useEffect(() => {
    if (ref) {
      ref.current.focus()
    }
  })

  return (
    <div>
      <button onClick={handleClick}>点我</button>
      <input type='text' ref={ref} />
    </div>
  )
}

export default App

一刷新就自动获取焦点

初次渲染和更新渲染,都会触发useEffect(),因为每次渲染jsx以后,会触发useEffect(),整个当前函数组件作用域的最后时机触发的

javascript 复制代码
import {useState,useEffect} from 'react'

function App() {
  const [count, setCount] = useState(0) 
  useEffect(() => {
    console.log(count)
  })
  const handleClick = () => {
    setCount(count + 1)
  }
  return (
    <div>
      <button onClick={handleClick}>点我</button>
    </div>
  )
}
export default App

执行顺序:

组件首次渲染(调用 useState(0),初始化 count0,渲染组件返回jsx)->点击按钮(触发handleClick函数调用useCount)->重新渲染组件->执行useEffect中的副作用函数

useEffect的依赖项使用

一个组件里可能有多个副作用,原则上来说多个副作用可以放在同一个useEffect里,但是比较杂

可以使用useEffect的依赖项进行拆解,因为useEffect本身就是个函数

我们可以使用多个useEffect来控制副作用

javascript 复制代码
import {useState,useEffect} from 'react'

function App() {
  const [count, setCount] = useState(0) 
  const [msg, setMsg] = useState('hello React')
   useEffect(() => {
    console.log(msg)
  })
  useEffect(() => {
    console.log(count)
  })
  const handleClick = () => {
    setCount(count + 1)
  }
  return (
    <div>
      <button onClick={handleClick}>点我</button>
    </div>
  )
}
export default App

两个根一个一样,也可以正常的更新、渲染

但是有时候根据不同的应用场景,有些副作用需要重新触发,有的不需要,可以指定哪些可以触发、哪些不可以。

添加useEffect的依赖项目

javascript 复制代码
import {useState,useEffect} from 'react'

function App() {
  const [count, setCount] = useState(0) 
  const [msg, setMsg] = useState('hello React')
   useEffect(() => {
    console.log(msg)
  },[msg])
  useEffect(() => {
    console.log(count)
  },[count])//这就是依赖项
  const handleClick = () => {
    setCount(count + 1)
  }
  return (
    <div>
      <button onClick={handleClick}>点我</button>
    </div>
  )
}
export default App

初始都触发,更新以后,只有对应依赖项发生改变时才触发,像这里msg没改变所以不触发

内部是怎么判别有没有发生变化的?是通过Object.is()方法来判定是否改变

像这样,依赖项是静态的空数组,只有初始会触发,以后发生改变都不会渲染他

不过尽量不要这么写,尽量还是要把里面用到的状态写在依赖项里

除了状态变量,还有props、计算变量等也可以写在依赖项里

计算变量:是指通过其他变量或数据动态计算得出的值,而不是直接存储的静态值。计算变量通常用于根据某些条件或逻辑动态生成数据,而不是手动维护这些数据。

函数也可以做依赖项

函数也可能成为计算变量,所以也可以作为依赖项

但是会出现一个问题,Object.is()方法在判别函数的时候会看引用类型的地址,两个函数是两个函数,内存地址不一样Object.is()就会判别为false

useCallBack可以修复这个问题,可以缓存函数,一般用于组件性能优化,其他情况不推荐使用,这里先不细说

最好的解决办法是把函数定义在useEffect内部

javascript 复制代码
import {useState,useEffect} from 'react'

function App() {
  const [count, setCount] = useState(0) 
  
  useEffect(() => {
    const foo = () => {
        console.log(count)
    }
    foo()
  },[count])//这就是依赖项
  return (
    <div>
    </div>
  )
}
export default App

useEffect清理操作

先写一个简易的关闭聊天室的功能

javascript 复制代码
import {useState,useEffect} from 'react'

function Chat() {
  return (
    <div>
      我是聊天室
    </div>
  )
}

function App() {
  const [show,setShow] = useState(true)
  const handleClick = () => {
    setShow(false)
  }
 
  return (
    <div>
      <button onClick={handleClick}>点我关闭聊天室</button>
      {show&&<Chat/>}
    </div>
  )
}
export default App

useEffect可以在卸载组件的时候清理

javascript 复制代码
import {useState,useEffect} from 'react'

function Chat() {
  useEffect(() => { 
    console.log('进入聊天室')
    //useEffect返回一个函数,这个函数会在组件卸载的时候执行
    return () => {
      console.log('离开聊天室')
    }
    
  })
  return (
    <div>
      我是聊天室
    </div>
  )
}

function App() {
  const [show,setShow] = useState(true)
  const handleClick = () => {
    setShow(false)
  }
 
  return (
    <div>
      <button onClick={handleClick}>点我关闭聊天室</button>
      {show&&<Chat/>}
    </div>
  )
}
export default App

增加了useEffect更新时的清理功能,清理功能是整个作用域的结束,而不是下一次作用域的开始

javascript 复制代码
import {useState,useEffect} from 'react'

function Chat({title}) {
  useEffect(() => { 
    console.log('进入',title)
    //useEffect返回一个函数,这个函数会在组件卸载的时候执行
    return () => {
      console.log('离开',title)
    }
    
  },[title])
  return (
    <div>
      我是课堂
    </div>
  )
}

function App() {
  const [show, setShow] = useState(true)
  const [title,setTitle]=useState('电磁场与电磁波')
  const handleClick = () => {
    setShow(false)
  }
  const handleChange = (e) => {
    setTitle(e.target.value)
  }
  return (
    <div>
      <button onClick={handleClick}>点我退出课堂</button>
      <select value={title} onChange={handleChange}>
        <option value="电磁场与电磁波">电磁场与电磁波</option>
        <option value="半导体物理">半导体物理</option>
      </select>
      {show && <Chat title={title} />}
    </div>
  )
}
export default App

清理功能是很常见的需求,如果注释掉清理的代码就会变成这样:

没有退出只有进入,如果是严格模式其实会把函数执行两边,可以看到👇

实际上是违背正常逻辑的,如果加上清理功能,在严格模式执行的现象应该是

也就是说严格模式也可以起到提醒你需要自检的作用

再举一个栗子,如果我们要切换不同的课程,切换不同的课程耗时不同

javascript 复制代码
import {useState,useEffect} from 'react'

function fetchChat(title) {
  const delay = title==='电磁场与电磁波'?2000:1000
  return new Promise((resolve,reject)=>{
    setTimeout(() => {
      resolve([
        {id:1,text:title+'1'},
        {id:2,text:title+'2'},
        {id:3,text:title+'3'}
      ])
    }, delay);
  })
}

function Chat({ title }) {
  const [list,setList]=useState([])
  useEffect(() => { 
    fetchChat(title).then((data) => {
  setList(data)
})
    return () => {
    }
    
  },[title])
  return (
    <div>
    {list.map((item)=>{
     return <li key={item.id}>{ item.text}</li>
    })}
    </div>
  )
}

function App() {
  const [show, setShow] = useState(true)
  const [title,setTitle]=useState('电磁场与电磁波')
  const handleClick = () => {
    setShow(false)
  }
  const handleChange = (e) => {
    setTitle(e.target.value)
  }
  return (
    <div>
      <button onClick={handleClick}>点我退出课堂</button>
      <select value={title} onChange={handleChange}>
        <option value="电磁场与电磁波">电磁场与电磁波</option>
        <option value="半导体物理">半导体物理</option>
      </select>
      {show && <Chat title={title} />}
    </div>
  )
}
export default App

如果不添加useEffect,二者耗时不同,就会出现上一个还在加载,下一个作用域已经执行的问题

变成这样

加入清理,就可以解决问题

javascript 复制代码
import {useState,useEffect} from 'react'

function fetchChat(title) {
  const delay = title==='电磁场与电磁波'?2000:1000
  return new Promise((resolve,reject)=>{
    setTimeout(() => {
      resolve([
        {id:1,text:title+'1'},
        {id:2,text:title+'2'},
        {id:3,text:title+'3'}
      ])
    }, delay);
  })
}

function Chat({ title }) {
  const [list, setList] = useState([])
  useEffect(() => { 
    let ignore = false
    fetchChat(title).then((data) => {
      if (!ignore) {
        setList(data)//上一次没结束不能提前更新
      }
})
    return () => {
      ignore=true//做清理工作
    }
    
  },[title])
  return (
    <div>
    {list.map((item)=>{
     return <li key={item.id}>{ item.text}</li>
    })}
    </div>
  )
}

function App() {
  const [show, setShow] = useState(true)
  const [title,setTitle]=useState('电磁场与电磁波')
  const handleClick = () => {
    setShow(false)
  }
  const handleChange = (e) => {
    setTitle(e.target.value)
  }
  return (
    <div>
      <button onClick={handleClick}>点我退出课堂</button>
      <select value={title} onChange={handleChange}>
        <option value="电磁场与电磁波">电磁场与电磁波</option>
        <option value="半导体物理">半导体物理</option>
      </select>
      {show && <Chat title={title} />}
    </div>
  )
}
export default App

有很多人在遇到这样的异步操作时会用async和await,但是React不支持你这么写

你可以单独写一个async函数,然后把要执行的放进去:

javascript 复制代码
import { useState, useEffect } from 'react';

function fetchChat(title) {
  const delay = title === '电磁场与电磁波' ? 2000 : 1000;
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve([
        { id: 1, text: title + '1' },
        { id: 2, text: title + '2' },
        { id: 3, text: title + '3' }
      ]);
    }, delay);
  });
}

function Chat({ title }) {
  const [list, setList] = useState([]);

  useEffect(() => {
    let ignore = false;

    const fetchData = async () => {
      const data = await fetchChat(title);
      if (!ignore) {
        setList(data); // 上一次没结束不能提前更新
      }
    };

    fetchData();

    return () => {
      ignore = true; // 做清理工作
    };
  }, [title]);

  return (
    <div>
      {list.map((item) => (
        <li key={item.id}>{item.text}</li>
      ))}
    </div>
  );
}

function App() {
  const [show, setShow] = useState(true);
  const [title, setTitle] = useState('电磁场与电磁波');

  const handleClick = () => {
    setShow(false);
  };

  const handleChange = (e) => {
    setTitle(e.target.value);
  };

  return (
    <div>
      <button onClick={handleClick}>点我退出课堂</button>
      <select value={title} onChange={handleChange}>
        <option value="电磁场与电磁波">电磁场与电磁波</option>
        <option value="半导体物理">半导体物理</option>
      </select>
      {show && <Chat title={title} />}
    </div>
  );
}

export default App;

useEffectEvent

实验性版本提供的钩子

如果你的状态变量有多个,只想执行一个可以用实验版本的useEffectEvent

介于六月份同学说实验版本也用不了,所以不讲了

相关推荐
黄毛火烧雪下9 小时前
React Native (RN)项目在web、Android和IOS上运行
android·前端·react native
fruge9 小时前
前端正则表达式实战合集:表单验证与字符串处理高频场景
前端·正则表达式
baozj9 小时前
🚀 手动改 500 个文件?不存在的!我用 AST 撸了个 Vue 国际化神器
前端·javascript·vue.js
用户4099322502129 小时前
为什么Vue 3的计算属性能解决模板臃肿、性能优化和双向同步三大痛点?
前端·ai编程·trae
海云前端19 小时前
Vue首屏加速秘籍 组件按需加载真能省一半时间
前端
蛋仔聊测试9 小时前
Playwright 中route 方法模拟测试数据(Mocking)详解
前端·python·测试
零号机9 小时前
使用TRAE 30分钟极速开发一款划词中英互译浏览器插件
前端·人工智能
疯狂踩坑人10 小时前
结合400行mini-react代码,图文解说React原理
前端·react.js·面试
Mintopia10 小时前
🚀 共绩算力:3分钟拥有自己的文生图AI服务-容器化部署 StableDiffusion1.5-WebUI 应用
前端·人工智能·aigc
街尾杂货店&10 小时前
CSS - transition 过渡属性及使用方法(示例代码)
前端·css