一步步实现React-Hooks核心原理

news/2024/10/23 9:25:16/

React Hooks已经推出一段时间,大家应该比较熟悉,或者多多少少在项目中用过。写这篇文章简单分析一下Hooks的原理,并带大家实现一个简易版的Hooks。

这篇写的比较细,相关的知识点都会解释,给大家刷新一下记忆。

Hooks

Hooks是React 16.8推出的新功能。以这种更简单的方式进行逻辑复用。之前函数组件被认为是无状态的。但是通过Hooks,函数组件也可以有状态,以及类组件的生命周期方法。

useState用法示例:

import React, { useState } from 'react';function Example() {// count是组件的状态const [count, setCount] = useState(0);return (<div><p>You clicked {count} times</p><button onClick={() => setCount(count + 1)}>        Click me      </button></div>);
}

闭包

开始之前,我们来简单回顾一下闭包的概念,因为Hooks的实现是高度依赖闭包的。

闭包(Closure),Kyle Simpson在《你不知道的Javascript》中总结闭包是:

Closure is when a function is able to remember and access its lexical scope even when that function is executing outside its lexical scope.

闭包就是,函数可以访问到它所在的词法作用域,即使是在定义以外的位置调用。

闭包的一个重要应用就是,实现内部变量/私有数据。

var counter = 0;// 给计数器加1
function add() {counter += 1;
}// 调用 add() 3次
add(); // 1
add(); // 2
counter = 1000;
add(); // 1003

这里因为counter不是内部变量,所以谁都能修改它的值。我们不想让人随意修改counter怎么办?这时候就可以用闭包:

function getAdd() {var counter = 0;return function add() {counter += 1;}
}
var add = getAdd();
add(); // 1
add(); // 2
add(); // 3
counter = 1000 // error! 当前位置无法访问counter

我们还可以把函数的定义挪到调用的位置,用一个立即执行函数表达式IIFE(Immediately Invoked Function Expression):

var add = (function getAdd() {var counter = 0;return function add() {counter += 1;}
})();
add(); // 1
add(); // 2
add(); // 3

这种通过IIFE创建闭包的方式也叫做模块模式(Module Pattern),它创建了一个封闭的作用域,只有通过返回的对象/方法来操纵作用域中的值。这个模式由来已久了,之前很多Javascript的库,比如jQuery,就是用它来导出自己的实例的。

开始动手实现

理清闭包的概念后可以着手写了。从简单的入手,先来实现setState。

function useState(initialValue) {var _val = initialValue; // _val是useState的变量function state() {// state是一个内部函数,是闭包return _val;}function setState(newVal) {_val = newVal;}return [state, setState];
}
var [foo, setFoo] = useState(0);
console.log(foo()); // 0
setFoo(1);
console.log(foo()) // 1

根据useState的定义来实现。比较简单不需要多解释。

参考 前端进阶面试题详细解答

将useState应用到组件中

现在我们将这个简易版的useState应用到一个Counter组件中:

function Counter() {const [count, setCount] = useState(0);return {click: () => setCount(count() + 1),render: () => console.log('render:', { count: count() })}
}
const C = Counter();
C.render(); // render: { count: 0 }
C.click();
C.render(); // render: { count: 1 }

这里简单起见,就不render真实DOM了,因为我们只关心组件的状态,所以每次render的时候打印count的值。

这里点击click之后,counter的值加一,useState的基本功能实现了。但现在state是一个函数而不是一个变量,这和React的API不一致,接下来我们就来改正这一点。

过期闭包

function useState(initialValue) {var _val = initialValue// 去掉了state()函数function setState(newVal) {_val = newVal}return [_val, setState] //直接返回_val
}
var [foo, setFoo] = useState(0)
console.log(foo) // 0
setFoo(1) // 更新_val
console.log(foo) // 0 - BUG!

如果我们直接把state从函数改成变量,问题就出现了,state不更新了。无论点击几次,Counter的值始终不变。这个是过期闭包问题(Stale Closure Problem)。因为在useState返回的时候,state就指向了初始值,所以后面即使counter的值改变了,打印出来的仍然就旧值。我们想要的是,返回一个变量的同时,还能让这个变量和真实状态同步。那如何来实现呢?

模块模式

解决办法就是将闭包放在另一个闭包中。

const MyReact = (function() {let _val //将_val提升到外层闭包return {render(Component) {const Comp = Component()Comp.render()return Comp},useState(initialValue) {_val = _val || initialValue //每次刷新function setState(newVal) {_val = newVal}return [_val, setState]}}
})()

我们运用之前提到的模块模式,创建一个MyReact模块(第一层闭包),返回的对象中包含useState方法(第二层闭包)。useState返回值中的state,指向的是useState闭包中的_val,而每次调用useState,_val都会重新绑定到上层的_val上,保证返回的state的值是最新的。解决了过期闭包的问题。

MyReact还提供了另外一个方法render,方法中调用组件的render方法来“渲染”组件,也是为了不渲染DOM的情况下进行测试。

function Counter() {const [count, setCount] = MyReact.useState(0)return {click: () => setCount(count + 1),render: () => console.log('render:', { count })}
}
let App
App = MyReact.render(Counter) // render: { count: 0 }
App.click()
App = MyReact.render(Counter) // render: { count: 1 }

这里每次调用MyReact.render(Counter),都会生成新的Counter实例,调用实例的render方法。render方法中调用了MyReact.useState()。MyReact.useState()在多次执行之间,外层闭包中的_val值保持不变,所以count会绑定到当前的_val上,这样就可以打印出正确的count值了。

实现useEffect

实现了useState之后,接下来实现useEffect。

const MyReact = (function() {let _val, _deps // 将状态和依赖数组保存到外层的闭包中return {render(Component) {const Comp = Component()Comp.render()return Comp},useEffect(callback, depArray) {const hasNoDeps = !depArrayconst hasChangedDeps = _deps ? !depArray.every((el, i) => el === _deps[i]) : trueif (hasNoDeps || hasChangedDeps) {callback()_deps = depArray}},useState(initialValue) {_val = _val || initialValuefunction setState(newVal) {_val = newVal}return [_val, setState]}}
})()// usage
function Counter() {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 })}
}
let App
App = MyReact.render(Counter)
// effect 0
// render {count: 0}
App.click()
App = MyReact.render(Counter)
// effect 1
// render {count: 1}
App.noop()
App = MyReact.render(Counter)
// // 没有执行effect
// render {count: 1}
App.click()
App = MyReact.render(Counter)
// effect 2
// render {count: 2}

在MyReact.useEffect中,我们将依赖数组保存到_deps,每次调用,都和前一次的依赖数组进行比对。发生变化才触发回调。

注意这里在比较依赖时用的是Object.is, React在比较state变化时也是用它。注意Object.is在比较时不会做类型转换(和==不同)。另外NaN === NaN返回false,但是Object.is(NaN, NaN)会返回true。

(简单起见,我们实现的useEffect,回调函数是同步执行的,所以打印出来的log是effect先执行,然后才是render。实际React中useEffect的回调函数应该是异步执行的)

支持多个Hooks

到此为止我们已经简单实现了useState和useEffect。但还有一个问题,就是useState和useEffect每个组件中只能用一次。

那么怎么才能支持使用多次hooks呢,我们可以将hooks保存到一个数组中。

const MyReact = (function() {let hooks = [],currentHook = 0 // 存储hooks的数组,和数组指针return {render(Component) {const Comp = Component() // 执行effectComp.render()currentHook = 0 // 每次render后,hooks的指针清零return Comp},useEffect(callback, depArray) {const hasNoDeps = !depArrayconst deps = hooks[currentHook]const hasChangedDeps = deps ? !depArray.some((el, i) => !Object.is(el, deps[i])) : trueif (hasNoDeps || hasChangedDeps) {callback()hooks[currentHook] = depArray}currentHook++ // 每调用一次指针加一},useState(initialValue) {hooks[currentHook] = hooks[currentHook] || initialValueconst setStateHookIndex = currentHook // 注意⚠️这句不是没用。是避免过期闭包问题。const setState = newState => (hooks[setStateHookIndex] = newState)return [hooks[currentHook++], setState]}}
})()

注意这里用了一个新的变量setStateHookIndex来保存currentHook的值。这是为了避免useState闭包包住旧的currentHook的值。

将改动应用到组件中:

function Counter() {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),type: txt => setText(txt),noop: () => setCount(count),render: () => console.log('render', { count, text })}
}
let App
App = MyReact.render(Counter)
// effect 0 foo
// render {count: 0, text: 'foo'}
App.click()
App = MyReact.render(Counter)
// effect 1 foo
// render {count: 1, text: 'foo'}
App.type('bar')
App = MyReact.render(Counter)
// effect 1 bar
// render {count: 1, text: 'bar'}
App.noop()
App = MyReact.render(Counter)
// // 不运行effect
// render {count: 1, text: 'bar'}
App.click()
App = MyReact.render(Counter)
// effect 2 bar
// render {count: 2, text: 'bar'}

实现多个hooks支持的基本思路,就是用一个数组存放hooks。每次使用hooks时,将hooks指针加1。每次render以后,将指针清零。

Custom Hooks

接下来,可以借助已经实现的hooks继续实现custom hooks:

function Component() {const [text, setText] = useSplitURL('www.google.com')return {type: txt => setText(txt),render: () => console.log({ text })}
}
function useSplitURL(str) {const [text, setText] = MyReact.useState(str)const masked = text.split('.')return [masked, setText]
}
let App
App = 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或者循环里,原因是在React内部,通过数组来存储hooks。所以必须保证每次render,hooks的顺序不变,数量不变,才能做deps的比对。


http://www.ppmy.cn/news/10109.html

相关文章

回顾2022年的历程,展望2023年目标

这里写目录标题回顾2022年博客之星你参加了吗&#xff1f;学习方面写博客方面在涨粉丝方面展望2023回顾2022年 时间如梭&#xff0c;转眼间已经2023年了。 你开始做总结了吗&#xff1f; 博客之星你参加了吗&#xff1f; 这是 2022 博客之星 的竞选帖子&#xff0c; 请你在这…

非对称加密实战(一):JDK生成keystore获取公钥私钥及代码验证【附源码】

目录使用说明非对称加密生成keystore文件公钥私钥互相解密获取fd-alias.keystore中的公钥私钥使用生成公钥私钥进行解密源码地址使用说明 非对称加密 非对称加密算法主要有&#xff1a;RSA、Elgamal、背包算法、Rabin、D-H、ECC&#xff08;椭圆曲线加密算法&#xff09;。下…

JEECGboot数据规则篇

使用 一、功能说明 列表数据权限&#xff0c;主要通过数据权限控制行数据&#xff0c;让不同的人有不同的查看数据规则&#xff1b; 比如&#xff1a; 销售人员只能看自己的数据&#xff1b;销售经理可以看所有下级销售人员的数据&#xff1b;财务只看金额大于5000的数据等等…

flex布局

flex布局 以下6个属性设置在容器上 flex-direction &#xff1a;主轴的方向 flex-wrap &#xff1a;一条轴线排不下&#xff0c;如何换行 flex-flow justify-content &#xff1a;项目在主轴上的对齐方式&#xff08;水平轴&#xff09; align-items &#xff1a;项目在交叉轴上…

数据在内存中存储☞(超详解)

目录 一.数据类型大家族 1.了解类型的意义 2.数据类型大家族的分类 二.详解☞数据储存之整形 1.储存方式 &#xff08;1&#xff09;.原码反码补码的概念 &#xff08;2&#xff09;.原码反码补码出现的原因&#xff1a; 计算机中只有加法器没有减法器&#xff0c;所有只…

python数据分析:采集分析岗位数据,看看薪资的高低都受什么因素影响呢

前言 嗨喽&#xff0c;大家好呀~这里是爱看美女的茜茜呐 又到了学Python时刻~ 在我们学习的时候,通常会产生疑问:这个行业前景好不好呢? 今天我们就用python的数据分析这个就业方向来举例 看一下都有哪些因素影响了薪资的高低呢&#xff1f; 数据采集 模块使用: reques…

VMware Workstation安装ESXI8.0

&#x1f388; 作者&#xff1a;互联网-小啊宇 &#x1f388; 简介&#xff1a; CSDN 运维领域创作者、阿里云专家博主。目前从事 Kubernetes运维相关工作&#xff0c;擅长Linux系统运维、开源监控软件维护、Kubernetes容器技术、CI/CD持续集成、自动化运维、开源软件部署维护…

php学习笔记-php运算符,类型转换,打印输出语句相较于其他语言的特殊部分-day02

php运算符&#xff0c;类型转换&#xff0c;打印输出语句相较于其他语言的特殊部分php运算符php的类型转换php打印输出语句php运算符 1.php运算符与其他高级语言相同的部分 算术运算符&#xff08;&#xff0c;-&#xff0c;*&#xff0c;/&#xff0c;%&#xff09;&#xff0…