当前位置: 首页 > Web前端 > JavaScript

一步一步体会React-Hooks的核心原理_1

时间:2023-03-26 20:22:22 JavaScript

ReactHooks上线有一段时间了,应该对它比较熟悉,或者在项目中或多或少用过。写这篇文章简单分析一下Hooks的原理,带你实现一个简易版的Hooks。这篇文章写的很详细,相关的知识点会进行讲解,以刷新大家的记忆。HooksHooks是React16.8引入的新特性。以这种更简单的方式进行逻辑复用。以前功能组件被认为是无状态的。但是有了Hooks,函数组件也可以拥有类组件的状态和生命周期方法。useState用法示例:importReact,{useState}from'react';functionExample(){//count是组件的状态const[count,setCount]=useState(0);return(

你点击了{count}次

setCount(count+1)}>点击我
);}闭包开始,先简单回顾一下闭包的概念,因为Hooks的实现高度依赖于闭包。闭包,KyleSimpson在《你不知道的Javascript》中总结说闭包是:闭包是当一个函数能够记住和访问它的词法作用域,即使该函数在它的词法作用域之外执行。闭包是,函数可以访问它所在的词法范围,即使是从定义之外调用。闭包的一个重要应用是实现内部变量/私有数据。varcounter=0;//计数器加1函数add(){counter+=1;}//调用add()3次add();//1添加();//2counter=1000;add();//1003这里,因为counter不是内部变量,任何人都可以修改它的值。如果我们不希望人们随意修改计数器怎么办?这时候可以用一个闭包:functiongetAdd(){varcounter=0;返回函数add(){counter+=1;}}varadd=getAdd();add();//1添加();//2添加();//3counter=1000//错误!当前位置无法访问计数器。我们还可以使用IIFE(立即调用的函数表达式)将函数的定义移动到调用位置:varadd=(functiongetAdd(){varcounter=0;returnfunctionadd(){counter+=1;}})();添加();//1添加();//2添加();//3这种通过IIFE创建闭包的方式也称为模块模式(ModulePattern),它创建了一个封闭的范围,只能通过返回的对象/方法来操作范围内的值。这种模式由来已久。许多Javascript库,例如jQuery,都使用它来导出自己的实例。开始实现弄清楚闭包的概念后,就可以开始写了。从简单的开始,我们先实现setState。函数useState(initialValue){var_val=initialValue;//_val是useStatefunctionstate()的变量{//state是一个内部函数,是一个闭包return_val;}函数setState(newVal){_val=newVal;}return[state,setState];}var[foo,setFoo]=useState(0);控制台日志(foo());//0setFoo(1);console.log(foo())//1根据useState的定义来实现。它比较简单,不需要太多解释。将useState应用到组件现在我们将这个简单版本的useState应用到Counter组件:functionCounter(){const[count,setCount]=useState(0);return{click:()=>setCount(count()+1),render:()=>console.log('render:',{count:count()})}}constC=Counter();C。使成为();//render:{count:0}C.click();C.render();//render:{count:1}为了简单起见,我们这里不渲染真正的DOM,因为我们只关心组件的状态,所以每次渲染值时都打印count。点击这里后,counter的值加1,实现了useState的基本功能。但是现在state是一个函数而不是一个变量,这与React的API不一致,我们接下来会修正这个问题。过期闭包函数useState(initialValue){var_val=initialValue//移除state()函数functionsetState(newVal){_val=newVal}return[_val,setState]//直接返回_valvar[foo,setFoo]=useState(0)console.log(foo)//0setFoo(1)//更新_valconsole.log(foo)//0-BUG!如果我们直接把状态从函数改成变量,问题就来了,状态没有更新。无论点击多少次,Counter的值都保持不变。这就是陈旧的关闭问题。因为当useState返回的时候,state指向的是初始值,所以即使后面counter的值发生变化,打印出来的还是旧值。我们想要的是返回一个变量,同时将变量与真实状态同步。那么如何实现呢?详见前端进阶面试题的详细解答。模块模式的解决方案是将闭包放在另一个闭包中。constMyReact=(function(){let_val//将_val提升到外层闭包return{render(Component){constComp=Component()Comp.render()returnComp},useState(initialValue){_val=_val||initialValue//每次刷新函数setState(newVal){_val=newVal}return[_val,setState]}}})()我们使用之前提到的模块模式创建一个MyReact模块(第一层闭包),返回的对象包含useState方法(第二层闭包)。useState返回值中的state指向useState闭包中的_val,每次调用useState时,_val都会反弹到上层的_val,保证返回的state值是最新的。修复了过期关闭的问题。MyReact还提供了另外一个方法render,调用组件的render方法来“渲染”组件,同样是为了在不渲染DOM的情况下进行测试。functionCounter(){const[count,setCount]=MyReact.useState(0)return{click:()=>setCount(count+1),render:()=>console.log('render:',{count})}}letAppApp=MyReact.render(Counter)//render:{count:0}App.click()App=MyReact.render(Counter)//render:{count:1}这里每次调用MyReact.render(Counter),会生成一个新的Counter实例,并调用该实例的render方法。MyReact.useState()在render方法中被调用。在多次执行MyReact.useState()之间,外层闭包中的_val值保持不变,因此count会绑定到当前的_val上,这样才能打印出正确的count值。实现useEffect实现useState之后,接下来实现useEffect。constMyReact=(function(){let_val,_deps//将状态和依赖数组保存到外部层的封闭包中return{render(Component){constComp=Component()Comp.render()returnComp},useEffect(callback,depArray){consthasNoDeps=!depArrayconsthasChangedDeps=_deps?!depArray.every((el,i)=>el===_deps[i]):trueif(hasNoDeps||hasChangedDeps){回调()_deps=depArray}},useState(initialValue){_val=_val||initialValuefunctionsetState(newVal){_val=newVal}return[_val,setState]}}})()//usagefunctionCounter(){const[count,setCount]=MyReact.useState(0)MyReact.useEffect(()=>{console.log('effect',count)},[count])return{click:()=>setCount(count+1),noop:()=>setCount(count),render:()=>console.log('render',{count})}}letAppApp=MyReact.render(Counter)//效果0//渲染{count:0}App.click()App=MyReact.render(Counter)//effect1//render{count:1}App.noop()App=MyReact.render(Counter)////没有执行effect//render{count:1}App.click()App=MyReact.render(Counter)//effect2//render{count:2}在MyReact.useEffect中,我们将依赖数组保存到_deps中,每次调用都是用之前的依赖数组触发回调仅当比较发生变化时。请注意,在比较依赖项时使用Object.is,React在比较状态变化时也会使用它。请注意,Object.is在比较时不执行类型转换(与==不同)。此外,NaN===NaN返回false,但Object.is(NaN,NaN)返回true。(为了简单起见,我们实现的useEffect的回调函数是同步执行的,所以打印的log是先执行effect,再执行render,实际在React中useEffect的回调函数应该是异步执行的)多这里支持Hooks到目前为止,我们只是简单地实现了useState和useEffect。但是还有一个问题,就是useState和useEffect在每个组件中只能使用一次。那么如何才能支持使用多个钩子呢?我们可以将钩子保存到一个数组中。constMyReact=(function(){lethooks=[],currentHook=0//存储hooks数组,以及数组指针return{render(Component){constComp=Component()//执行effectComp.render()currentHook=0//每次渲染后,钩子的指针被清除returnComp},useEffect(callback,depArray){consthasNoDeps=!depArrayconstdeps=hooks[currentHook]consthasChangedDeps=deps?!depArray.some((el,i)=>!Object.is(el,deps[i])):trueif(hasNoDeps||hasChangedDeps){callback()hooks[currentHook]=depArray}currentHook++//每次调用时递增指针},useState(initialValue){hooks[currentHook]=hooks[currentHook]||initialValueconstsetStateHookIndex=currentHook//注意??这句话不是没用,是为了避免过期闭包的问题constsetState=newState=>(hooks[setStateHookIndex]=newState)return[hooks[currentHook++],setState]}}})()注意这里用了一个新的变量setStateHookIndex来保存curr的值entHook。这是为了避免useState闭包包装旧的currentHook值。将更改应用于组件:functionCounter(){const[count,setCount]=MyReact.useState(0)const[text,setText]=MyReact.useState('foo')//第二个useStateMyReact。useEffect(()=>{console.log('effect',count,text)},[count,text])return{click:()=>setCount(count+1),类型:txt=>setText(txt),noop:()=>setCount(count),render:()=>console.log('render',{count,text})}}letAppApp=MyReact.render(Counter)//效果0foo//render{count:0,text:'foo'}App.click()App=MyReact.render(Counter)//effect1foo//render{count:1,text:'foo'}App.type('bar')App=MyReact.render(Counter)//effect1bar//render{count:1,text:'bar'}App.noop()App=MyReact.render(Counter)////不运行effect//render{count:1,text:'bar'}App.click()App=MyReact.render(Counter)//effect2bar//render{count:2,text:'bar'}实现多个钩子support的基本思路是用一个数组来存放hooks。每次使用hooks时,hooks指针加1。每次渲染后,指针被清除。CustomHooks接下来,你可以在已经实现的hooks的帮助下继续实现自定义hooks:txt),render:()=>console.log({text})}}functionuseSplitURL(str){const[text,setText]=MyReact.useState(str)constmasked=text.split('.')返回[masked,setText]}letAppApp=MyReact.render(Component)//{text:['www','google','com']}App.type('www.reactjs.org')App=MyReact.render(Component)//{text:['www','reactjs','org']}}重新理解Hooks的规则理解Hooks的实现可以帮助我们理解Hooks的使用规则。还记得Hooks的使用原理吗?Hooks只能用在组件的最外层代码中,不能包裹在if或loops中。原因是在React内部,hooks是通过数组存储的。因此,需要保证每次渲染时hook的顺序和数量保持不变,以便比较deps。