React Hooks --- useState 和 useEffect

  React Hooks 都是函数,使用React Hooks,就是调用函数。React Hooks在函数组件中使用,当React渲染函数组件时(也是调用函数),组件里的每一行代码都会被执行,一个个的Hooks也就会被执行。

  useState() 使函数组件能够保存状态。通常来说,函数执行完毕,其内部的所有变量都会被销毁。再次执行函数,就会是一个全新的执行环境,全新的变量,两次执行之间,没有关联,是无法保存状态的。

function sum() {
    let a = 3;      
}

  sum函数的每一次执行,a都是3。组件函数也是函数,每一次执行,都伴随着它内部变量的创建与销毁,组件是如何保存状态的? 这就是useState起作用的地方,useState内部维护了一个变量,组件的每一次调用,它都是返回这一个变量,而不全新的变量,纵然组件内部的变量都销毁了,再重新创建,但只要变量引用useState的返回值,组件内的变量就是引用的同一个变量,也就相当于保存了状态。useState内部维护了变量,那也要有更改变量的方法,因为函数外部无法更改内部的变量的值,简单理解useStateuseState

let _val;
function useState(initState) {
    _val = initState;

    function setState(value) {
        _val = value
    }

    return [_val, setState];
}

  当然,它也可以接受一个参数,用于设置变量的初始值。useState把变量和更改方法暴露出去,组件调用它,获取到变量,组件也就有了状态,因为组件的每一个调用,都返回的是相同的变量。获取到更改方法,也就有能力改变变量的值,进而改变组件的状态,函数组件也就有状态了,也就不用class 组件了。组件内部获取到useState暴露的变量和函数,可以使用数组的解构。

const App= () => {
  const [message, setMessage]= useState('');
}

  可以看到,useState() 和普通函数没什么区别,都是传递参数,接收返回值。有了状态变量之后,就可以在函数组件中使用了,变量的使用没有任何区别,就是在某个地方引用它,获取它的值。比如,在jsx中引用它,组件状态就可以渲染到页面上。

  使用create-react-app 创建项目,修改App.js

const App = () => {
  const [message, setMessage]= useState('');

  return (
    <input value={message}></input>
  )
}

  npm start,页面上显示一个空输入框。组件渲染时,调用useState(),  useState()返回了空字符串,赋值给了message。 接着向下执行,返回一个jsx, message 赋值给value, 那就读取message变量的值,  此时message变量的值为空字符串,value的值也就为空字符串。 渲染完成后,页面中显示了一个input 输入框,值为空。给input添加onChange 事件

const App = () => {
  const [message, setMessage]= useState('');

  function handleChange(e) {
    setMessage(e.target.value)
  }
  return (
    <input value={message} onChange={handleChange}></input>
  )
}

  input中输入1,触发了onChange 事件,调用setMessage,  由于setMessage是useState暴露出来的,useState更新了内部维护的变量的值,触发了React 的更新机制。当然React 不会立刻更新组件,而是把它放到更新队列中,React 的渲染是异步的。当真正重新渲染时,React 又会调用App函数组件,还是从上到下,一行一行执行代码。先调用useState(),此时useState返回的是更改后的内部的变量的值(e.target.value),而不是初始值,函数的参数被忽略了。把useState返回的值,也就是你在输入框中输入的值1,赋值给了message. 接着向下执行,一个函数的创建,然后是jsx,jsx中的message 取当前值为1,然后赋值给value, 渲染完成,页面上input 中显示1,组件状态改变了。当再输入2时,setMessage再次调用,useState内部重新设置变量值,App 组件再次被调用,还是先执行useSate() 返回最新的状态12,赋值给message, 然后创建一个handleClick 函数,最后jsx 中message 取12, 组件渲染完成后,页面中的输入框中显示12. 整个过程如下

// 初始渲染。
const message = '';  // useState() 的调用
function handleChange(e) {
  setMessage(e.target.value)
}
return (
  <input value='' onChange={handleChange}></input>
)

// 输入1 更新渲染
const message = 1;  // useState() 的调用
function handleChange(e) {
  setMessage(e.target.value)
}
return (
  <input value=1 onChange={handleChange}></input>
)

// 再次输入2,更新渲染
const message = 12;  // useState() 的调用
function handleChange(e) {
  setMessage(e.target.value)
}
return (
  <input value=12 onChange={handleChange}></input>
)

  组件每一次渲染,都会形成它自己独有的一个版本,在每次渲染中,都拥有着属于它本次渲染的状态和事件处理函数,每一次的渲染都是相互隔离,互不影响的,也就是说,在每一次的渲染中,如果jsx和事件处理函数中引用状态变量,它们仅仅能够获取到本次渲染中的状态变量的值,状态变量的值,在渲染开始时,调用useState就已经决定了。状态变量,只不过是每一次调用useState 的返回值。React 负责状态的管理,而我们只是声明变量,使用状态。状态的更新,也不过是组件的重新渲染,React 重新调用了组件函数,重新获取useState 返回的值。useState() 返回的永远都是最新的状态值。Each Render Has Its Own Props and State,Each Render Has Its Own Event Handlers。

   一定要注意useState的参数,它只在第一次渲染时起作用,给状态变量赋初始值,使组件拥有初始状态。在以后的渲染中,不管是调用更新函数导致的组件渲染,还是父组件渲染导致的它的渲染,参数都不会再使用了,直接被忽略了,组件中的state状态变量,获取的都是最新值。如果你想像下面的代码一样,使用父组件每次传递过来的props 来更新state,

const Message= (props) => {
   const messageState = useState(props.message);
    /* ... */
}

  就会有问题,因为props.message, 只会在第一次渲染中使用,以后组件的更新,它就会被忽略了。useState的参数只在初次渲染的时候使用一次,有可能也是useState 可以接受函数的原因,因为有时候,组件初始状态,是需要计算的,比如 我们从localStorage中去取数据作为初始状态。如果在组件中直接写

const Message= (props) => {

let name = localStorage.getItem('name');
const messageState = useState(name);
/* ... */
}

  那么组件每一次的渲染都会调用getItem, 没有必要,因为我们只想获取初始状态,调用一次就够了。useState如果接受函数就可以解决这个问题,因为它的参数,就是只在第一次渲染时才起作用,对于函数来说,就是在第一次渲染的时候,才会调用函数,以后都不会再调用了。

const Message= (props) => {
const messageState = useState(() => {return localstorage.getItem('name')});
/* ... */
}

   更新函数的参数还可以是函数,函数参数是当前状态值。如果想使用当前状态,生成一个新状态,就可以使用函数。

function handleChange(e){
   const val = e.target.value;
   setMessage(prev => prev + val);
}

  当组件的状态是引用类型,比如数组和对象的时候,情况要稍微复杂一点,首先我们不能只更改这个状态变量的属性值,我们要生成一个新的状态值。

const App = () => {
    const [messageObj, setMessage] = useState({ message: '' }); // 状态是一个对象

    function handleChange(e) {
        messageObj.message = e.target.value; // 只是改变状态的属性
        setMessage(messageObj)
    }
    return (
        <input type="text" value={messageObj.message} onChange={handleChange}/>
    );
};

  无法在input中输入内容。React更新状态时,会使用Object.js() 对新旧状态进行比较,如果它俩相等,就不会重新渲染组件。对象的比较是引用的比较,相同的引用, React 不会重新渲染。所以handleChange 要改成如下

 function handleChange(e) {
        const newMessageObj = { message: e.target.value }; // 重新生成一个对象
        setMessage(newMessageObj);
    }

   这又引出了另外一个问题,react 状态更新使用的是整体替换原则,使用新的状态去替换掉老的状态,而不是setState 的合并原则。如果使用setState,我们只需要setState那些要改变的状态就可以了,React会把这次所做的改变和原来没有做改变的状态进行合并,形成新的整个组件的状态。但这里的setMessage() 不行,

const App = () => {
    const [messageObj, setMessage] = useState({ message: '', id: 1 });

    return (
        <div>
            <input value={messageObj.message}
                onChange={e => {
                    const newMessageObj = { message: e.target.value };
                    setMessage(newMessageObj); 
                }}
            />
            <p>{messageObj.id} : {messageObj.message}</p>
        </div>
    );
};

  在输入框中输入内容的时候,发现id 属性不见了,新的状态去替换掉了整个旧的状态。onChange 要修改如下

onChange = { e => {
    const val = e.target.value;
    setMessage(prevState => {
        return { ...prevState, message: val }
    });
}}

  也正因为如此,React 建议我们把复杂的状态进行拆分,拆成一个一个单一的变量,更新的时候,只更新其中的某个或某些变量。就是使用多个useState(), 生成多个状态变量和更新函数。

const App = () => {
    const [message, setMessage] = useState('');
    const [id, setId] = useState(1);

    return (
        <div>
            <input value={message}
                onChange={e => {
                    setMessage(e.target.value); 
                }}
            />
            <p>{id} : {message}</p>
        </div>
    );
};

  当然,复杂状态变量(比如,Object 对象)可以拆分,主要是每一个属性所代表的状态之间的关联不大。如果状态关联性特别强,就必须是一个复杂对象的时候,建议使用useReducer.

  useEffect()

  React的世界里,不是只有状态和改变状态,然后渲染组件,它还要和外界进行交互,比如,发送ajax请求,直接修改DOM。这些统称为副作用,因为React的主要作用就是把state转化为UI,偏离主要功能的都是副作用。怎么理解副作用呢?可以把副作用想像成,组件渲染完成后,稍带要做的事情,主要任务做完了,稍带做点其他事情。怎么管理副作用?使用useEffect()。useEffect 的第一个参数就是一个回调函数,函数里面实现副作用。组件渲染完成了,要改变dom,那就把改变dom的实现写到useEffect的回调函数中。

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

export default function App() {
    const [message, setMessage]= useState('');

    function handleChange(e) {
        setMessage(e.target.value)
    }
    useEffect(() => {
        document.title = `${message}`;
    })

    return <input value={message} onChange={handleChange}></input>
}

  effect中是怎么获取到message的最新状态值的?和事件处理函数一样,每一次的渲染都有属于这一次渲染的effect函数,在这次渲染中,它能够获取到属于本次渲染状态值。每一次渲染,effect函数都不相同。

// 初始渲染。
const message = '';  // useState() 的调用
useEffect(() => { // 初始渲染的effect函数
  document.title = `${''}`;
})

// 输入1 更新渲染
const message = 1;  // useState() 的调用
useEffect(() => { // 第二次渲染的effect函数
  document.title = `${1}`;
})

// 再次输入2,更新渲染
const message = 12;  // useState() 的调用
useEffect(() => {  // 第三次渲染的effect函数
  document.title = `${2}`;
})

  React记住了你的effect函数,组件渲染完成后,就会调用它,每一个effect都属于每一次特定的渲染。

  总结一下React的渲染过程,React调用组件函数,state是'',组件返回<input value='' onChange={handleChange}></input>,同时告诉React,渲染完成后,执行() => { document.title = `${''}`;},React更新DOM,浏览器在屏幕上画出输入框,时机已到,React执行effect函数(() => { document.title = `${''}`;})。当在输入框输入1,调用setMessage, 告诉React把状态更新为'1',React重新调用组件,state为'1',组件返回<input value='1' onChange={handleChange}></input>,同时告诉React,渲染完成后,执行() => { document.title = `${'1'}`;},React更新DOM,浏览器在屏幕上画出输入框,时机已到,React执行effect函数(() => { document.title = `${'1'}`;})。

   useEffect还提供了一个可选的清理函数,当useEffect的回调函数中返回一个函数时,这个函数称为清理函数

useEffect(() => {
  function log() {
    console.log(message);
  }
  window.addEventListener('resize', log);

  return () => {
    window.removeEventListener('resize', log);
  }
})

  初次渲染,useEffect 就是告诉React,渲染完成后,执行   

() => {
  function log() {
    console.log(''); // 初次渲染时,message 为空
  }
  window.addEventListener('resize', log);

  return () => {
    window.removeEventListener('resize', log);
  }

  等到组件渲染到屏幕上时,effect函数执行,并注册了一个清理函数。组件再次渲染时,告诉react,渲染完成后,执行

() => {
  function log() {
    console.log('1'); // 再次渲染时,message为'1'
  }
  window.addEventListener('resize', log);

  return () => {
    window.removeEventListener('resize', log);
  }

  组件再次渲染到屏幕上,需要执行effect函数,不过上一次渲染完成后,注册了清理函数,所以先执行上一次渲染完成后注册的清理函数,再执行这一次渲染的effect函数,并再次注册一个清理函数。清理函数是在再次渲染完成后,需要执行effect函数前执行。但是在某些场景下,组件每次渲染后,都执行effect的函数,会带来问题,比如请求数据,组件渲染完成后,只请求一次就可以了。如下代码

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

export default function App() {
    const [message, setMessage]= useState('');

    function handleChange(e) {
        setMessage(e.target.value)
    }
    useEffect(() => {
        fetch('https://jsonplaceholder.typicode.com/todos/1')
            .then(response => response.json())
            .then(json => {
                console.log(json);
                setMessage(json.title);
            })
    })

    return <input value={message} onChange={handleChange}></input>
}

  打开控制台,发现不停地调用接口,因为状态更新会导致组件重新渲染,渲染完成,useEffect又会重新调用。请求数据-> 更新状态->重新请求数据->更新状态,死循环了。这就用到了useEffect的第二个参数,一个数组,用来告诉React ,再次渲染完成后,要不要调用useEffect 中的函数。怎样使用数组进行告知呢?就把useEffect 回调函数中的要用到的外部变量或参数,依次写到数组中。那么React 就知道回调函数的执行是依赖这些变量的,那么它就会时时地监听这些变量的变化,只要有更新,它就会重新调用useEfffect. 这个数组因此也称为依赖数数组,回调函数要再次执行的依赖。函数的依赖发生变化,才会重新调用函数,函数的依赖没有变化,就不用调用函数。现在看一下我们的回调函数fetch,  里面的内容都是写死的,没有任何外部变量依赖,那就写一个空数组。React 看到空数组,也就明白了,useEffect 中的回调函数不依赖任何变量,那它就执行一遍就好了。组件渲染完成后执行一次,以后更新就不用管了。

    useEffect(() => {
        fetch('https://jsonplaceholder.typicode.com/todos/1')
            .then(response => response.json())
            .then(json => {
                console.log(json);
                setMessage(json.title);
            })
    }, []) // 空数组,回调函数没有依赖作何外部的变量

  有的时候,不能只获取1(id)的todos, 用户传递出来的id 是几,就要显示id 是几的 todos.  那么fetch的url 就不是固定的了,而是变化的了。useEffect的回调函数也就有了依赖了,那就是一个id,这个id 是需要外界传递过来的,useEffect 的回调函数中用到了一个外部的变量id,那就需要把id写到依赖数组中。再写一个input 表示用户传递过来的id

export default function App() {
    const [todoTitle, setTodoTitle]= useState('');
    const [id, setId] = useState(1);

    function handleChange(e) {
        setTodoTitle(e.target.value)
    }
    function handleId(e) {
        setId(e.target.value);
    }
    useEffect(() => {
        fetch('https://jsonplaceholder.typicode.com/todos/' + id)
            .then(response => response.json())
            .then(json => {
                setTodoTitle(json.title);
            })
    }, [id]) // 回调函数依赖了一个外部变量id

    return( 
        <>
            <p>id:  <input value={id} onChange={handleId}></input></p>  
            <p>item title: <input value={todoTitle} onChange={handleChange}></input> </p>
        </>
    )
}

  可以把数组中的id 去掉,测试一下效果,只有初次加载的时候,发送了请求,以后不管你输入什么,再也不会发送请求了。其实effect函数组件中还依赖setTotoTitle, setMessage函数,按照React观点,effect函数,如果用到外部函数,也要把函数写到依赖数组。只要在effect函数中用到了react数据流中的值,无论是props,state和函数,都要放到effect依赖数组中。但React 保证 set* 函数,在整个组件的渲染进程是都是一致的,可以不写。

  其实还有几种方式,在某些特定的情况下,可以把依赖去掉,而不会影响effect函数的执行。

useEffect(() => {
    const id = setInterval(() => {
      setCount(count + 1);
    }, 1000);
    return () => clearInterval(id);
  }, [count]);

  转化成

  useEffect(() => {
    const id = setInterval(() => {
      setCount(c => c + 1);
    }, 1000);
    return () => clearInterval(id);
  }, []);

  使用useReducer,把更新和触发更新的action分离

const [state, dispatch] = useReducer(reducer, initialState);
const { count} = state;

useEffect(() => {
  const id = setInterval(() => {
    dispatch({ type: 'tick' }); 
  }, 1000);
  return () => clearInterval(id);
}, []);

  reducer可以放到函数组件的里面,从而读取组件属性来设置状态

function Counter({ step }) {
  const [count, dispatch] = useReducer(reducer, 0);

  function reducer(state, action) {
    if (action.type === 'tick') {
      return state + step;
    } else {
      throw new Error();
    }
  }

  useEffect(() => {
    const id = setInterval(() => {
      dispatch({ type: 'tick' });
    }, 1000);
    return () => clearInterval(id);
  }, [dispatch]);

  return <h1>{count}</h1>;
}

  如果reducer中没有使用到组件的属性,只是纯函数,可以放到组件的外面。

  到底函数要不要作为依赖放到useEffect的依赖数组里面,

function SearchResults() {
  // 🔴 Re-triggers all effects on every render
  function getFetchUrl(query) {
    return 'https://hn.algolia.com/api/v1/search?query=' + query;
  }

  useEffect(() => {
    const url = getFetchUrl('react');
    // ... Fetch data and do something ...
  }, [getFetchUrl]); // 🚧 Deps are correct but they change too often

  useEffect(() => {
    const url = getFetchUrl('redux');
    // ... Fetch data and do something ...
  }, [getFetchUrl]); // 🚧 Deps are correct but they change too often

  // ...
}

  会导致无限请求数据,因为在React 渲染的时候,每一次渲染都会重新生成函数,每一次渲染getFetchUrl都不一样?怎么处理? 如果函数和react的渲染数据流没有关系,就是,它不会引用React组件属性和状态,可以把它放到组件外面。

function getFetchUrl(query) {
  return 'https://hn.algolia.com/api/v1/search?query=' + query;
}

function SearchResults() {
  useEffect(() => {
    const url = getFetchUrl('react');
    // ... Fetch data and do something ...
  }, []); // ✅ Deps are OK

  useEffect(() => {
    const url = getFetchUrl('redux');
    // ... Fetch data and do something ...
  }, []); // ✅ Deps are OK

  // ...
}

  还有就是使用useCallback

function SearchResults() {
  // ✅ Preserves identity when its own deps are the same
  const getFetchUrl = useCallback((query) => {
    return 'https://hn.algolia.com/api/v1/search?query=' + query;
  }, []);  // ✅ Callback deps are OK

  useEffect(() => {
    const url = getFetchUrl('react');
    // ... Fetch data and do something ...
  }, [getFetchUrl]); // ✅ Effect deps are OK

  useEffect(() => {
    const url = getFetchUrl('redux');
    // ... Fetch data and do something ...
  }, [getFetchUrl]); // ✅ Effect deps are OK

  // ...
}

   useEffect 的执行时机,useEffect和componnentDidMount都是在组件挂载后才执行。但对于componentDidMount来说,如果组件挂载后,你同步设置一个状态,React知道会触发另外一次渲染,它就不会使用第一次的渲染结果,而是将使用第二次渲染的结果,画在屏幕上。而useEffect则是在第一次渲染结果画到页面上后,才执行,再将第二次的渲染结果画到屏幕上,这样会造成闪动。如果不想要这种闪动,就要使用useLayoutEffect

import React from "react";
import ReactDOM from "react-dom";

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      // Notice we never see question marks
      width: "???"
    };
    this.myRef = React.createRef();
  }

  componentDidMount() {
    this.setState({
      width: this.myRef.current.offsetWidth
    });
  }

  render() {
    return <div ref={this.myRef}>{this.state.width}</div>;
  }
}

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

"when this state changes, what side effects need to be re-ran".

  React 类组件是类的实例,调用的是实例上的方法,React 函数组件是函数,使用的是闭包

posted @ 2019-11-28 20:42  SamWeb  阅读(6363)  评论(0编辑  收藏  举报