React结合js快速理解学习
作者:秋了秋 发表时间:2023年06月12日
React是用javascript写的框架,本质是数据驱动,跟其它框架不同的是它是个”js和html混合体”,js里面一切皆对象,包括React也不另外,如果它长得不像对象,一定会有一个处理过程把它变成对象。
写react之前一定得引入框架代码:
<script src="https://unpkg.com/react@16/umd/react.development.js"></script> <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script> <!-- 生产环境中不建议使用,这个文件就是编译jsx的,一般生产环境使用webpack来编译,不需要引入该文件,因为性能极差 --> <script src="https://unpkg.com/babel-standalone@6.15.0/babel.min.js"></script>
本文为何要强调结合js来理解,也是为了区分js,否则写着写着react跟js搞混了忘记js怎么写了,比如以下代码:
const url = 'http://netblog.cn'; function H(obj) { return ( <div> <h1>Hello, world! {obj.name}</h1> <h2>现在是 {new Date().toLocaleTimeString()}.</h2> <h3>{url}</h3> </div> ) } ReactDOM.render( <H name="秋了秋" />, document.getElementById('example') );
它的写法看起来是不伦不类,既有js又有html,这是它为了让开发者更直观的书写代码,直接写html,不至于写json这种长对象,所谓所见即所得, 对前端新人很友好,因为前端入门就是html。
这种写法放在js文件里面执行一定冒烟,它根本不是js语法,它是React专门定义的jsx文件里面的代码,React会将这种文件代码进行编译,编译成js,提取里面的html,把html转成若干属性的对象。再通过js对象构造真实的html结构,所以说jsx是虚拟DOM。
我们来剖析下为什么写这么不伦不类的代码:
function H(obj) { return ( <div> <h1>Hello, world! {obj.name}</h1> <h2>现在是 {new Date().toLocaleTimeString()}.</h2> <h3>{url}</h3> </div> ) }
return 后面跟的是(),也可以写中括号[], 括号里面放的才是html代码,这在js里面应该是引号才合理:
function H(obj) { return ` <div> <h1>Hello, world! {obj.name}</h1> <h2>现在是 {new Date().toLocaleTimeString()}.</h2> <h3>{url}</h3> </div>` }
Reac之所以用括号应该是刻意区分js的引号,他要兼容js的写法,能保证既可以写js也可以写jsx,如果碰到引号它就不解析和转化成对象,可以节省性能。
Tips: 括号可以省略,但是所有dom需要写在一行,一般是简短的可适用:
function H(obj) { return <h1>Hello, world! {obj.name}</h1>; }
需要注意组件只能包含一个顶层标签,否则会报错。也就是它只能返回一个节点,子节点不限,如果真的不需要外面的容器父节点怎么处理?可以写空标签:
function H(obj) { return ( <> <h1>Hello, world! {obj.name}</h1> <h2>现在是 {new Date().toLocaleTimeString()}.</h2> <h3>{url}</h3> </> ) }
在jsx的虚拟dom中可以写表达式js代码和变量,通过大括号包住
function H(obj) { return ( <div> <h1 data-age= {obj.name}>Hello, world! {obj.name}</h1> <h2>现在是 {new Date().toLocaleTimeString()}.</h2> </div> ) }
这在js里面叫做封装了一个复用函数,在react里面叫复用组件,它可以通过ReactDOM渲染到页面上。
var myName = ‘秋了秋’; ReactDOM.render( <H name={myName } age=”18” />, document.getElementById('example') );
第二个参数即是容器对象,着重说第一个参数,标签名就是组件名字,属性就是传给组件的参数,跟以下代码是相同效果:
ReactDOM.render( H({name: ‘秋了秋’, age: 18}), document.getElementById('example') );
这样更符合js的写法,因为属性可能是多个,所以它是个对象。
一切皆对象,所以对于样式的表达也不另外,
ReactDOM.render( <H name="秋了秋" style={{width:’100px’,height:’100px’}} />, document.getElementById('example') );
这里为什么是双大括号,其实还是一个大括号,包的是一个对象{width:’100px’,height:’100px’},里面的括号是对象本身的...而且函数体内部style不能加引号
function H(obj) { return ( <div> <h1 style=”{obj.style}”>Hello, world! {obj.name}</h1> <h2>现在是 {new Date().toLocaleTimeString()}.</h2> </div> ) }
这样是会报错的,必须
function H(obj) { return ( <div> <h1 style={obj.style}>Hello, world! {obj.name}</h1> <h2>现在是 {new Date().toLocaleTimeString()}.</h2> </div> ) }
即便是其它属性也不能加引号,否则就会当字符串输出,不会编译:
function H(obj) { return ( <div> <h1 dd=”{obj.style}”>Hello, world! {obj.name}</h1> <h2>现在是 {new Date().toLocaleTimeString()}.</h2> </div> ) }
这样dd解释出来还是dd=”{obj.style}”
注意: 这里有一些跟原生html有些区分点:
class 属性变为 className
tabindex 属性变为 tabIndex
for 属性变为 htmlFor
textarea 的值通过需要通过 value 属性来指定
style 属性的值接收一个对象,css 的属性变为驼峰写法,如:backgroundColor。
上面是通过函数来创建的组件,如果是通过类创建的组件则可以通过this.props访问参数,前提是要继承 React.Component父类并且虚拟dom输出必须写在render方法内,相当于重写父类的render方法。
class Clock extends React.Component { render() { return ( <div> <h1>Hello, world!</h1> <h2>现在是 {this.props.date.toLocaleTimeString()}.</h2> </div> ); } } ReactDOM.render( <Clock date={new Date()} />, document.getElementById('example') );
需要注意的是class 和 for 不能作为 XML 属性名。作为替代,React DOM 使用 className 和 htmlFor 来做对应的属性。
ReactDOM.render( <Clock className=”netblog-cn” htmlFor=”xxx” />, document.getElementById('example') );
原生 HTML 元素名以小写字母开头,而自定义的 React 类名以大写字母开头,比如 HelloMessage 不能写成 helloMessage。
组件里面也可以包含组件:
function H(obj) { return ( <div> <h1 dd=”{obj.style}”>Hello, world! {obj.name}</h1> <h2>现在是 {new Date().toLocaleTimeString()}.</h2> </div> ) } function Div(obj) { return ( <div class={obj.className}> <H name=”秋了秋”> </div> ) }
前面说了react也是数据驱动,只需要更改数据就会重新渲染dom,那么并不是改任何数据都会渲染的,需要把数据挂在在state属性上,并且修改数据需要使用setState方法更改才行
class Clock extends React.Component { constructor(props) { super(props); this.state = { date: new Date(), name: '秋了秋', count: 0 }; } changeData() { this.setState({ count: this.state.count + 1, }); } render() { return ( <div> <h1>{this.state.name}{this.state.count}</h1> <h2>现在是 {this.state.date.toLocaleTimeString()}.</h2> </div> ); } } const Instance = ReactDOM.render( <Clock />, document.getElementById('example') ); setInterval( () => { Instance.changeData(); }, 1000 )
在js里面类的实例化需要使用new Clock(),创建实例才能调用实例的方法,在jsx中 ReactDOM.render会返回实例化对象,挂载dom的时候就是实例化的时候。以上setInterval代码每秒钟调用实例方法changeData,changeData通过父组件的setState方法让state 对象的count 字段加一,触发了state数据的变更所以dom每秒都在重新渲染(调用组件里面的render函数)。
然而定时器这样写在dom移除的时候容易造成内存泄漏,我们可以监听父组件的componentDidMount(挂载时候的钩子)和componentWillUnmount(卸载时候的钩子)方法管理定时器
class Clock extends React.Component { constructor(props) { ... } componentDidMount() { this.timerID = setInterval( () => this.changeData(), 1000 ); } componentWillUnmount() { clearInterval(this.timerID); } changeData() { ... } render() { ... } }
关于绑定事件,react有个很烦人的坑需要特别注意:
class A extends React.Component { handleClick() { console.log('this is:', this);// undefined 这里写this指向的是undefined } render() { return ( <button onClick={this.handleClick}> Click me </button> ); } }
要解决这个问题需要手动绑定this
class A extends React.Component { constructor(props) { super(props); this.handleClick = this.handleClick.bind(this); } handleClick() { console.log('this is:', this);// undefined 这里写this指向的是undefined } render() { return ( <button onClick={this.handleClick}>Click me</button> ); } } //或者 class A extends React.Component { handleClick = () => { console.log('this is:', this); } render() { return ( <button onClick={this.handleClick}>Click me</button> ); } } //或者 class A extends React.Component { handleClick() { console.log('this is:', this); } render() { return ( <button onClick={(e) => this.handleClick(e)}>Click me</button> ); } }
事件函数的传参
<button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button> <button onClick={this.deleteRow.bind(this, id)}>Delete Row</button> class A extends React.Component { handleClick(param, e) {//注意第二个参数才是事件对象 console.log('this is:', param); } }
注意属性名不要使用key,跟class一样是系统保留关键字,key是用来标识元素的唯一身份,便于增删的时候识别到具体哪个dom。
const content = posts.map((post) => <Post key={post.id} title={post.title} /> );
Post 组件读不到props.key的,请使用其它属性名。
React的api用得最多的就是setState,想类似的还有replaceState,他们都是异步的,有回调函数setState是会把传入参数合并到原先的state数据,而replaceState是替换:
this.state = { name: ‘秋了秋’, age: 18 } this.setState({ age: 16 });
最终数据是
this.state = { name: ‘秋了秋’, age: 16 }
而replaceState是替换,跟js逻辑差不多:
this.replaceState({ age: 16 }, function(){ console.log(‘可恶!我被别人减了两岁还把我的名字删了’); });
最终数据是
this.state = { age: 16 }
第一个参数也可以是个函数,函数的返回值作为最终设置的值
this.setState((prevState, props) => ({ age: prevState.age + props.age; }));
相类似的api还有setProps,replaceProps,还有一个手动调用render的方法,即强制更新forceUpdate:
this.forceUpdate(function(){ console.log(‘我被强制更新了’); });
除了这些api,还提供了一些钩子函数:
class Content extends React.Component { componentWillMount() { console.log('我在组件挂载之前执行!') } componentDidMount() { console.log('我在组件挂载之后执行!') } componentWillReceiveProps(newProps) { console.log('组件感受到即将prop更新但是还未更新,即将渲染之前调用!') } shouldComponentUpdate(newProps, newState) { return true;//当 props 或 state 发生变化时,我会在渲染执行之前被调用。 } componentWillUpdate(nextProps, nextState) { console.log('组件更新之前调用!'); } componentDidUpdate(prevProps, prevState) { console.log('组件更新之后调用!') } componentWillUnmount() { console.log('组件卸载之前调用!') } render() { return ( <div> <h3>{this.props.myNumber}</h3> </div> ); } }
Jsx还有一个特殊的属性是ref,相当于js里面的选择器,所以在绑定普通属性的时候也要注意不要用ref,react是通过ref定位到组件里面的哪个dom元素。
class MyComponent extends React.Component { handleClick() { // 使用原生的 DOM API 获取焦点 this.refs.myInput.focus(); } render() { // 当组件插入到 DOM 后,ref 属性添加一个组件的引用于到 this.refs return ( <div> <input type="text" ref="myInput" /> <input type="button" value="点我输入框获取焦点" onClick={this.handleClick.bind(this)} /> </div> ); } } ReactDOM.render( <MyComponent />, document.getElementById('example') );
以上钩子只在通过类来创建组件中可以用,那普通函数创建组件怎么办?react 16.8 新增了一些特性,hooks可以在函数组件里面使用。
1. useState通过调用简单的钩子函数就可以建立对某个数据监听,创建state。
function App () { const [ count, setCount ] = useState(0);// 相当于类里面的this.state = {count: 0} return ( <div> 点击次数: { count } <button onClick={() => { setCount(count + 1)}}>点我</button> </div> ) }
setCount 相当于类里面的this.setState函数。useState函数的参数为state的默认值,支持具有返回值的函数逻辑
const [ count, setCount ] = useState(function() {return 666*888;});
useState函数返回的是一个数组,数组第一项是state的值,第二个参数是设置state的函数,只有通过这个函数赖修改state才会触发组件更新,函数的第一个参数是组件上一个状态的state值:
setCount((count => count + 1);
useState使用的地方都可以用useReducer,他们有相似的功能,只是useReducer功能适用更复杂的state的更新,type有多种情况的时候,才会发挥useReducer的优势,否则,不如不用,其参数也不一样:
function App () { const reducer = (state, action) => { switch (action.type) { case "increment": return { ...state, score: state.score + action.payload }; case "decrement": return { ...state, score: state.score - action.payload }; default: return state; } } const [state, dispatch] = React.useReducer(reducer, { name: "秋了秋", score : 0 }); console.log(state); return ( <div> <button onClick={()=>{dispatch({type:"increment",payload:2})}}>Increment</button> <button onClick={()=>{dispatch({type:"decrement",payload:2})}}>Decrement</button> </div> ) } ReactDOM.render(<App />, document.getElementById('test'));
useReducer第一个参数为dispatch传参的处理函数,第二个参数为state的默认值。
2. 使用useEffect函数赖监听任意数据:
const [name, setName] = useState('秋了秋'); const [age, setAge] = useState(18); useEffect(function() { alert('My name is ' + name + ', My age is ' + age); }, [name,age])
第一个参数是数据变化时候执行的函数,当然useEffect不只有监听数据变化的功能,它的功能主要取决于第二个参数。
-
什么都不传,组件每次 render 之后 useEffect 都会调用,相当于类组件里面的componentDidMount 和 componentDidUpdate
-
传入一个空数组 [], 只会调用一次,相当于类组件里面的componentDidMount 和 componentWillUnmount
-
传入一个数组,其中包括变量,只有组件挂载时(相当于类组件里面的componentDidMount)和这些变量变动时,useEffect 才会执行
其实第二点和第三点属于一个点,主要看第三点,数组里面没有数据监听自然只会执行一次
function Test(data) { const [name, setName] = React.useState('秋了秋'); const [age, setAge] = React.useState(18); React.useEffect(function() { console.log('My name is ' + name + ', My age is ' + (age + data.age)); }, [name, age]); React.useEffect(function() { console.log(1, 'http://netblog.cn'); }, []); React.useEffect(function() { console.log(2, 'http://netblog.cn'); }); const handleClick = function(e) { setName('Bob'); setAge(20) } return ( <div onClick={handleClick}> <div>My name is {name}</div> <div>My age is {(age + data.age)}</div> </div> ) } const addAge = -2; ReactDOM.render(<Test age={addAge}/>, document.getElementById('example'));
useLayoutEffect跟useEffect相类似,只是useEffect是异步的要等所有渲染结束才执行,而useLayoutEffect是同步的,所以涉及到操作dom的代码建议使用useLayoutEffect,这样能及时访问到dom的当时状态,修改也只会造成一次回流。useEffect 的函数会在组件渲染到屏幕之后执行,此时对 DOM 进行修改,会触发浏览器再次进行回流、重绘,增加了性能上的损耗。
3. useContext的使用顾名思义就是创建作用域,在创建的作用域范围内都可以使用统一提供的参数,主要用来解决输出多个组件时候不需要多个组件传参的问题。
function Test1(obj) { return ( <div>My name is {obj.name}1</div> ) } function Test2(obj) { return ( <div>My name is {obj.name}2</div> ) } function Test3(obj) { return ( <div> <Test1 name=”秋了秋” /> <Test2 name=”秋了秋”/> </div> ) } ReactDOM.render(<Test3 name={'秋了秋'}/>, document.getElementById('test'));
如果使用useContext:
const Context = React.createContext(); function Test1() { const name = React.useContext(Context); return ( <div>My name is {name}1</div> ) } function Test2() { const name = React.useContext(Context); return ( <div>My name is {name}2</div> ) } function Test3(obj) { return ( <Context.Provider value={obj.name}> <Test1/> <Test2/> </Context.Provider> ) } ReactDOM.render(<Test3 name={'秋了秋'}/>, document.getElementById('test'));
这里需要注意的是要先createContext,再用createContext出来的对象提供统一参数Context.Provider,在这个Provider包裹下的组件都可以使用它的参数value的值,跟js里面的闭包类似原理, 然后组件里面要调取参数就用useContext(Context)
4. useMemo和useEffect极其相似,只是useMemo通常用来定义一个函数,让这个函数只在监听数据变化的时候执行,纵使你在dom中调用这个函数多次,如果监听数据未变化,它将不再执行而是使用上一次的缓存数据。
跟useEffect的区别是useMemo是在dom渲染前执行,类比生命周期就是shouldComponentUpdate,useEffect在渲染后执行。useEffect函数里面可以操作dom,发请求这些,而useMemo不应该做这些。
useMemo相对useEffect起到了性能优化的作用。
function Test(obj) { // 产品名称、价格 let [price, setPrice] = React.useState(1000); let [name, setName] = React.useState('秋了秋'); let count = 0; //如果不用useMemo每次渲染count都会被初始化未0,getCount 返回的值会在0和1不断跳动 const getCount = React.useMemo(function() { console.log('执行获取count'); return ()=>++count; }, [count]); // 假设有一个业务函数 获取产品的名字 const getProductName = React.useMemo(function() { console.log('getProductName触发'); name = name + '--netblog.cn'; return () => { return name; } }, [name]); //对比js的普通写法 const getProductName2 = function() { console.log('getProductName触发'); name = name + '--netblog.cn';//如果不用useMemo,这个代码每次渲染都会执行造成不必要的性能消耗 return name; } return ( <React.Fragment> <p>名称:{getProductName()}</p> <p>价格:{price}¥</p> <button onClick={() => setPrice(price+1)}>价钱+1</button> <button onClick={() => setName((name) => {return name.replace(/\d/g, '') + getCount()})}>修改名字</button> </React.Fragment> ) } ReactDOM.render(<Test />, document.getElementById('test'));
只有组件挂载和点“修改名字”的时候才会打印“getProductName触发”。
5. useCallback跟useMemo具有相同的效果,但是有些区别
useMemo用于缓存计算结果,确保只有在依赖项发生变化时才会重新计算。避免每次渲染都执行相同代码计算的性能损耗
useCallback用于缓存函数,确保只有在依赖项发生变化时才会重新创建函数。避免每次渲染创建一个新函数的开销。
如果需要经常使用某个函数,而这个函数的计算量很大,那么可以使用useMemo进行函数的缓存,而如果需要将该函数传递给子组件,那么可以使用useCallback进行函数的缓存。
function Test(obj) { // 产品名称、价格 let [price, setPrice] = React.useState(1000); //useCallback第一个参数为一个普通函数,useMemo是函数里面返回函数 const handleClick = React.useCallback(function() { alert(price); }, [price]); return ( <React.Fragment> <button onClick={handleClick}>查看价格</button> <button onClick={()=>{setPrice(2000);alert('修改成功')}}>修改价格</button> </React.Fragment> ) } ReactDOM.render(<Test />, document.getElementById('test'));
useMemo和useCallback本质都是用来优化性能的,属于react的打补丁函数,用来避免react的渲染机制导致的一些性能损耗问题。useMemo在有些时候也可以充当useEffect来使用,但是失去了设计它的初衷。
function Test(obj) { // 产品名称、价格 let [price, setPrice] = React.useState(1000); React.useEffect(function() { alert('useEffect'); }, [price]); React.useMemo(function() { alert('useMemo'); }, [price]); const handleClick = function() { setPrice((prePrice) => prePrice + 1); } return <div>看看弹几次框,哪个框先弹的。价格:{price}<br/><button onClick={handleClick}>点我修改价格</button></div>; } ReactDOM.render(<Test />, document.getElementById('test'));
以上代码组件初始化的时候分别会弹useMemo、useEffect,修改价格的时候也会按这个顺序弹框,且弹useMemo的时候UI还没渲染出来,UI渲染完成后弹useEffect,只有触发时机不一样,效果都是同等的。
6. useRef是在函数组件里面是定义变量的,如果用js原生定义变量var myRef = undefined,组件每次渲染都会初始化一个变量,如何防止这种事情发生可以使用useState,但是useState创建的变量在不同渲染状态之间不共享,如果需要共享需要在外部创建全局变量,但是react提供了一个hack函数,就是使用let myRef = React.useRef(undefined);能在函数里面创建属于该组件独有的全局变量,同样是为了修正函数式编程组件做的hack,相当于类编程的this.myRef = undefined;
function App () { let like = React.useRef(0); function handleAlertClick() { setTimeout(() => { alert(`you clicked on ${like.current}`); }, 3000); } return ( <div> <button onClick={() => {like.current = like.current + 1;}}>{like.current}赞</button> <button onClick={handleAlertClick}>Alert</button> </div> ) } ReactDOM.render(<App />, document.getElementById('test'));
修改和访问ref的值通过ref.current,更改ref创建的组件内的全局变量不会触发组件更新
上面在讲类组件的时候dom上可以放一个特殊的ref属性<button ref="myInput">netblog.cn</button>,这样就可以通过this.refs.myInput在类函数里面访问到这个dom,在函数式组件里面的hack方法是ref属性不绑定字符创而是绑定ref变量<button ref={like}>netblog.cn</button>,这样就可以通过like.current访问到该dom。
7. forwardRef 顾名思义是给ref辅助用的,意思是传递ref,把父组件的ref传到子组件里面去,这样子组件就能在html上绑定这个ref,绑定后父组件就能拿到这个ref绑定的dom。通常的如果不用forwardRef也能实现这样的功能:
function App () { let input; let [value, setValue] = React.useState(''); React.useEffect(()=>{ console.log(input); }, []); return [ <div key={0}> <Child callback={(ref) => { input = ref; }} changeValue={(value)=>{setValue(value)}}/> <div>{value}</div> </div> ]; } function Child(props) { const ref = React.useRef(); React.useEffect(()=>{ props.callback(ref.current); }, []); const handleInput = React.useCallback(function() { props.changeValue(ref.current.value); }, [ref]); return ( <div> <input ref={ref} onInput={handleInput} /> </div> ); } ReactDOM.render(<App />, document.getElementById('test'));
但是使用forwardRef就不需要在dom里面写函数,直接写ref属性即可,一句话就是少些一些代码,逻辑更简单:
function App () { let input = React.useRef(); let [value, setValue] = React.useState(''); React.useEffect(()=>{ console.log(input.current); }, []); return [ <div key={0}> <Child ref={input} changeValue={(value)=>{setValue(value)}}/> <div>{value}</div> </div> ]; } const Child = React.forwardRef((props, ref) => { const handleInput = React.useCallback(function() { props.changeValue(ref.current.value); }, [ref]); return ( <div> <input ref={ref} onInput={handleInput} /> </div> ); }) ReactDOM.render(<App />, document.getElementById('test'));
8. useImperativeHandle 是forwardRef 的补救函数,是子组件限制父组件对传递出去的ref的一些属性和方法的访问,如果不使用useImperativeHandle方法,则父组件可以对传递出来的ref任意方法进行使用,为了防止这种越权,可以明确暴露一些方法出去,只允许使用这些方法,并且方法可以子组件定义:
React.useImperativeHandle(ref, () => ({ focus: () => { inputRef.current.focus(); }, value: ()=>{ return '访问我要先交5毛钱'; } }));
8. 自定义hook,即js里面的复用函数,只是这种函数必须用use开头命名,且函数内部必须使用了react内部hook的调用。
function App () { const resetTimer = React.useRef(true); const [age, setAge] = useSetAge(15, resetTimer); const handleSetAge = React.useMemo(()=> { return ()=> { setAge(10); resetTimer.current = true; } },[]); return [ <div key={0}> <div>实际年龄:{age}</div> <button onClick={handleSetAge}>点击设置为10岁</button> </div> ]; } function useSetAge(defaultAge, resetTimer) { const [age, setAge] = React.useState(defaultAge); let timer = React.useRef(null); if(age > 18) { setAge(18); if(timer.current) { clearInterval(timer.current); timer.current = null } console.error('永远18!'); } React.useEffect(function() { if(resetTimer.current) { timer.current = setInterval(function() { setAge(function(preAge) { return preAge + 1; }); }, 1000); } resetTimer.current = false; }); return [age, setAge]; } ReactDOM.render(<App />, document.getElementById('test'));