1.写在前面
本方案特别适合希望在历史遗留的原生JavaScript项目中实现简单轻量级数据驱动机制的开发者。无需引入任何框架或第三方库,即可按照此方法封装出类似于React中useState
的功能,轻松为项目添加状态管理能力,既保持了项目的轻量性,又提升了开发效率;
追求轻量,推荐直接看7轻量版的实现!
2.优势总结 ★ 了解
1. 轻量级响应式系统
-
无虚拟DOM:直接监听状态变化并更新真实DOM,避免虚拟DOM的diff计算开销
-
精准更新:只有订阅了状态变化的DOM元素会更新(相比React的组件级重渲染更精确)
2. 类React开发体验
-
提供
useState
+setState
的API设计,降低学习成本 -
支持函数式更新:
setState(prev => prev + 1)
3. 状态不可变性
-
自动深拷贝状态,避免意外修改
-
每次更新都生成新状态,便于实现时间旅行调试
4. 批量更新优化
-
batch()
可合并多次更新为单次渲染 -
避免频繁DOM操作导致的布局抖动
5. 多实例隔离
-
不同模块可以使用独立的状态实例,避免全局污染
3.单例模式 ★ 重要
单例模式效果展示
单例模式封装
/*** 单例模式状态管理* 整个应用共享同一个状态实例*/// ==================== 深拷贝工具 ====================
function deepClone(obj, hash = new WeakMap()) {if (obj == null) return obj;if (typeof obj !== 'object') return obj;const constructor = obj.constructor;const specialTypes = ['Date', 'RegExp', 'Map', 'Set'];if (specialTypes.includes(constructor.name)) {return new constructor(obj);}if (hash.has(obj)) return hash.get(obj);const clone = Array.isArray(obj) ? [] : Object.create(Object.getPrototypeOf(obj));hash.set(obj, clone);[...Object.getOwnPropertySymbols(obj), ...Object.keys(obj)].forEach(key => {clone[key] = deepClone(obj[key], hash);});return clone;
}// ==================== 核心实现 ====================
const subscribers = new Map();
let batchQueue = [];
let isBatching = false;function batchNotify(proxy) {const callbacks = subscribers.get(proxy);if (!callbacks) return;Promise.resolve().then(() => {callbacks.forEach(cb => {try {cb(proxy.value);} catch (e) {console.error('回调执行失败:', e);}});});
}export const useState = (initialState) => {if (typeof initialState === 'undefined') {throw new Error('初始状态不能为undefined');}const proxy = new Proxy({ value: deepClone(initialState) }, {set(target, key, value) {if (key !== 'value') return false;target[key] = deepClone(value);if (!isBatching) batchNotify(proxy);return true;}});return {get state() { return proxy.value; },setState: (updater) => {if (isBatching) {batchQueue.push({ proxy, updater });} else {proxy.value = typeof updater === 'function' ? updater(proxy.value) : updater;}},subscribe: (callback) => {if (typeof callback !== 'function') {throw new Error('回调必须是函数');}if (!subscribers.has(proxy)) {subscribers.set(proxy, new Set());}subscribers.get(proxy).add(callback);return () => {subscribers.get(proxy)?.delete(callback);};}};
};export const batch = (callback) => {if (isBatching) return callback();isBatching = true;batchQueue = [];try {callback();const updatesByProxy = new Map();batchQueue.forEach(({ proxy, updater }) => {if (!updatesByProxy.has(proxy)) {updatesByProxy.set(proxy, []);}updatesByProxy.get(proxy).push(updater);});updatesByProxy.forEach((updaters, proxy) => {let state = proxy.value;updaters.forEach(updater => {state = typeof updater === 'function' ? updater(state) : updater;state = deepClone(state);});proxy.value = state;batchNotify(proxy);});} finally {isBatching = false;batchQueue = [];}
};
单例模式HTML测试
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><title>单例模式测试</title><script type="module">import { useState, batch } from './singleton-state.js';const counter = useState(0);counter.subscribe(count => {document.getElementById('count').textContent = count;console.log('当前计数:', count);});// 普通更新document.getElementById('increment').addEventListener('click', () => {counter.setState(c => c + 1);});// 批量更新document.getElementById('increment-5').addEventListener('click', () => {batch(() => {counter.setState(c => c + 1);counter.setState(c => c + 1);counter.setState(c => c + 1);counter.setState(c => c + 1);counter.setState(c => c + 1);});});</script>
</head>
<body>
<h1>单例模式测试</h1>
<div>计数: <span id="count">0</span></div>
<button id="increment">+1</button>
<button id="increment-5">+5 (批量)</button>
</body>
</html>
4.多例模式 ★ 重要
双例模式效果展示
双例模式封装
/*** 多例模式状态管理工具* 允许创建多个独立的状态管理实例,每个实例拥有独立的状态和订阅系统* * 主要特点:* 1. 可创建多个隔离的状态管理实例* 2. 每个实例拥有独立的useState和batch方法* 3. 完整的状态不可变性保证* 4. 支持批量更新优化性能* * 使用方式:* const store = createStateStore();* const counter = store.useState(0);* * counter.subscribe(state => console.log(state));* counter.setState(prev => prev + 1);* store.batch(() => { ... });*/// ==================== 深拷贝工具函数 ====================/*** 高性能深拷贝函数* @param {any} obj - 需要拷贝的对象* @param {WeakMap} [hash=new WeakMap()] - 用于存储已拷贝对象的WeakMap(防止循环引用)* @returns {any} 深拷贝后的对象* * 实现特点:* 1. 处理基本数据类型:直接返回* 2. 处理循环引用:使用WeakMap缓存已拷贝对象* 3. 保留特殊对象类型:Date、RegExp等* 4. 原型链继承:保持原型链关系* 5. 性能优化:使用Object.keys+Symbol属性遍历*/
function deepClone(obj, hash = new WeakMap()) {// 处理null和undefinedif (obj == null) return obj;// 处理基本数据类型(string, number, boolean, symbol, bigint)if (typeof obj !== 'object') return obj;// 处理特殊对象类型const constructor = obj.constructor;const specialTypes = ['Date', 'RegExp', 'Map', 'Set', 'WeakMap', 'WeakSet'];if (specialTypes.includes(constructor.name)) {return new constructor(obj);}// 检查循环引用if (hash.has(obj)) return hash.get(obj);// 根据对象类型创建空对象或数组const clone = Array.isArray(obj) ? [] : Object.create(Object.getPrototypeOf(obj));// 缓存当前对象,防止循环引用hash.set(obj, clone);// 拷贝Symbol类型属性const symKeys = Object.getOwnPropertySymbols(obj);if (symKeys.length > 0) {symKeys.forEach(symKey => {clone[symKey] = deepClone(obj[symKey], hash);});}// 拷贝普通属性Object.keys(obj).forEach(key => {clone[key] = deepClone(obj[key], hash);});return clone;
}// ==================== 状态管理工厂函数 ====================/*** 创建新的状态管理实例* @returns {Object} 包含useState和batch方法的对象* * 每个实例包含:* 1. 独立的订阅者系统* 2. 独立的批量更新队列* 3. 独立的状态树*/
export function createStateStore() {/*** 订阅者集合* Map结构:* key: 状态代理对象(Proxy)* value: 该状态的订阅者回调集合(Set)*/const subscribers = new Map();/*** 批量更新队列* 数组结构,每个元素包含:* - proxy: 状态代理对象* - updater: 更新函数或值*/let batchQueue = [];/*** 批量更新标志* @type {boolean}*/let isBatching = false;// ==================== 内部工具方法 ====================/*** 通知订阅者状态变更* @param {Proxy} proxy - 状态代理对象* * 实现特点:* 1. 使用微任务(Promise)异步执行通知* 2. 错误处理避免影响其他订阅者* 3. 自动清理无效订阅*/function batchNotify(proxy) {// 获取当前状态的所有订阅者const callbacks = subscribers.get(proxy);if (!callbacks || callbacks.size === 0) return;// 使用微任务异步执行通知Promise.resolve().then(() => {// 获取当前状态值const state = proxy.value;// 遍历执行所有订阅回调callbacks.forEach(callback => {try {callback(state);} catch (error) {console.error('[状态通知错误] 订阅回调执行失败:', error);}});});}// ==================== 公开API ====================/*** 创建响应式状态* @param {any} initialState - 初始状态* @returns {Object} 包含state、setState和subscribe方法的对象* * @throws {Error} 当initialState为undefined时抛出错误*/function useState(initialState) {// 参数校验if (typeof initialState === 'undefined') {throw new Error('useState: 初始状态不能为undefined');}// 创建响应式代理对象const proxy = new Proxy({ value: deepClone(initialState) },{/*** 代理set陷阱* @param {Object} target - 目标对象* @param {string} key - 属性名* @param {any} value - 新值* @returns {boolean} 是否设置成功*/set(target, key, value) {// 只处理value属性的变更if (key !== 'value') return false;// 深拷贝新值,确保状态不可变target[key] = deepClone(value);// 非批量模式下立即通知订阅者if (!isBatching) {batchNotify(proxy);}return true;}});/*** 订阅状态变更* @param {Function} callback - 状态变更回调函数* @returns {Function} 取消订阅的函数* * @throws {Error} 当callback不是函数时抛出错误*/function subscribe(callback) {// 参数校验if (typeof callback !== 'function') {throw new Error('subscribe: 回调必须是函数');}// 初始化该状态的订阅者集合if (!subscribers.has(proxy)) {subscribers.set(proxy, new Set());}// 添加订阅者const callbacks = subscribers.get(proxy);callbacks.add(callback);// 返回取消订阅函数return function unsubscribe() {callbacks.delete(callback);// 清理空订阅集合if (callbacks.size === 0) {subscribers.delete(proxy);}};}/*** 更新状态* @param {Function|any} updater - 更新函数或新状态值* * 更新规则:* 1. 如果是函数:updater(prevState) => newState* 2. 如果是值:直接替换状态*/function setState(updater) {if (isBatching) {// 批量模式下将更新操作加入队列batchQueue.push({proxy,updater});} else {// 直接更新模式proxy.value = typeof updater === 'function'? updater(proxy.value): updater;}}// 返回状态访问接口return {/*** 获取当前状态值* @returns {any} 当前状态*/get state() {return proxy.value;},setState,subscribe};}/*** 批量更新状态* @param {Function} callback - 包含多个状态更新的回调函数* * 实现特点:* 1. 合并多个setState调用为一次更新* 2. 自动处理状态依赖关系* 3. 最终只触发一次订阅通知*/function batch(callback) {// 如果已经在批量模式中,直接执行回调if (isBatching) {callback();return;}// 进入批量模式isBatching = true;batchQueue = [];try {// 执行用户回调,收集所有setState操作callback();// 按状态代理分组更新操作const updatesByProxy = new Map();batchQueue.forEach(({ proxy, updater }) => {if (!updatesByProxy.has(proxy)) {updatesByProxy.set(proxy, []);}updatesByProxy.get(proxy).push(updater);});// 处理每个状态代理的更新updatesByProxy.forEach((updaters, proxy) => {let currentState = proxy.value;// 按顺序应用所有更新器updaters.forEach(updater => {currentState = typeof updater === 'function'? updater(currentState): updater;// 确保每次更新都是不可变的currentState = deepClone(currentState);});// 最终更新状态值proxy.value = currentState;// 通知该状态的订阅者batchNotify(proxy);});} finally {// 确保无论是否出错都退出批量模式isBatching = false;batchQueue = [];}}// 返回实例方法return { useState, batch };
}// ==================== 可选默认实例 ====================/*** 默认导出的状态管理实例* 为方便使用,同时提供创建新实例和默认实例两种方式*/
const defaultStore = createStateStore();
export const { useState: defaultUseState, batch: defaultBatch } = defaultStore;
双例模式HTML测试
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>多例模式状态管理测试</title><style>body {font-family: Arial, sans-serif;max-width: 600px;margin: 0 auto;padding: 20px;}.counter {margin: 20px 0;padding: 15px;border: 1px solid #ddd;border-radius: 5px;}button {padding: 8px 16px;margin-right: 10px;cursor: pointer;}</style>
</head>
<body><h1>多例模式状态管理测试</h1><div class="counter"><h2>独立计数器1</h2><div>当前值: <span id="counter1-value">0</span></div><button id="counter1-increment">增加</button><button id="counter1-batch">批量增加(+3)</button></div><div class="counter"><h2>独立计数器2</h2><div>当前值: <span id="counter2-value">100</span></div><button id="counter2-increment">增加</button></div><script type="module">// 从模块导入创建方法import { createStateStore } from './multi-state.js';// 创建两个完全独立的状态管理实例const store1 = createStateStore();const store2 = createStateStore();// 实例1:计数器1const counter1 = store1.useState(0);counter1.subscribe(state => {document.getElementById('counter1-value').textContent = state;console.log('计数器1更新:', state);});document.getElementById('counter1-increment').addEventListener('click', () => {counter1.setState(prev => prev + 1);});document.getElementById('counter1-batch').addEventListener('click', () => {store1.batch(() => {counter1.setState(prev => prev + 1);counter1.setState(prev => prev + 1);counter1.setState(prev => prev + 1);});});// 实例2:计数器2 (完全独立)const counter2 = store2.useState(100);counter2.subscribe(state => {document.getElementById('counter2-value').textContent = state;console.log('计数器2更新:', state);});document.getElementById('counter2-increment').addEventListener('click', () => {counter2.setState(prev => prev + 10);});// 暴露到全局方便测试window.stores = { store1, store2, counter1, counter2 };</script><div style="margin-top: 30px; color: #666;"><h3>测试说明:</h3><p>1. 两个计数器使用完全独立的状态管理实例</p><p>2. 打开控制台可以查看状态变化日志</p><p>3. 在控制台输入 <code>stores</code> 可以访问状态实例</p></div>
</body>
</html>
5.单例模式和双例模式的区别 ★ 了解
-
单例模式:
-
全局共享一个状态树
-
直接导出
useState
和batch
-
适合中小型应用
-
-
多例模式:
-
通过
createStateStore()
创建独立实例 -
每个实例有自己的状态和订阅系统
-
适合大型应用或需要隔离状态的场景
-
6.兼容性分析 ★ 了解
1. 支持的浏览器
特性 | 最低支持版本 | 覆盖率 |
---|---|---|
Proxy | Chrome 49+ | ~98% |
Firefox 18+ | ||
Edge 12+ | ||
Safari 10+ | ||
WeakMap | IE 11+ | ~99% |
Promise (微任务) | ES6+ | ~98% |
2. 不兼容场景
-
IE 11及以下:不支持Proxy(可用
Object.defineProperty
降级) -
老旧移动浏览器:部分Android 4.x设备不支持ES6
3. Polyfill方案
// 在入口文件添加以下polyfill
import 'core-js/stable'; // 提供Promise/WeakMap等
import 'proxy-polyfill'; // 提供Proxy的简单实现
7.轻量版 ★ 推荐
7.1 核心机制说明:
-
依赖收集 (track):
-
当读取
state.value
时,如果存在activeEffect
,会将这个 effect 收集起来 -
存储结构:
WeakMap<target, Map<key, Set<effect>>>
-
-
触发更新 (trigger):
-
当设置
state.value
时,会查找并执行所有收集到的 effect
-
-
响应式循环:
-
初始化时执行
updateDOM
会触发getter
收集依赖 -
当值变化时触发
setter
,执行所有依赖的 effect(包括updateDOM
)
-
-
双向绑定:
-
自动为
input
元素添加input
事件监听 -
输入变化时更新状态,状态变化时更新输入框值
-
-
清理机制:
-
每个绑定元素存储了自己的清理函数
-
可以在不需要时移除所有事件监听,防止内存泄漏
-
7.2 封装的js文件
// 使用 WeakMap 来存储目标对象与其依赖映射的关系
// WeakMap 的键是对象,值是一个 Map,用于存储该对象属性的依赖集合
const targetMap = new WeakMap();// 当前正在运行的 effect(副作用函数),用于依赖收集
let activeEffect = null;// 唯一ID计数器,用于为没有指定标识符的状态生成唯一ID
let uid = 0;/*** 跟踪依赖关系,建立属性与 effect 之间的联系* @param {object} target - 目标对象(被代理的对象)* @param {string|symbol} key - 被访问的属性名*/
function track(target, key) {// 如果没有活跃的 effect,直接返回(不需要收集依赖)if (!activeEffect) return;// 从 targetMap 中获取目标对象的依赖映射let depsMap = targetMap.get(target);// 如果没有依赖映射,则为该目标对象创建一个新的 Map 并存入 targetMapif (!depsMap) targetMap.set(target, (depsMap = new Map()));// 获取该属性对应的依赖集合let dep = depsMap.get(key);// 如果没有依赖集合,则为该属性创建一个新的 Set 并存入 depsMapif (!dep) depsMap.set(key, (dep = new Set()));// 将当前活跃的 effect 添加到该属性的依赖集合中dep.add(activeEffect);
}/*** 触发更新,当属性值变化时执行所有相关的 effect* @param {object} target - 目标对象(被代理的对象)* @param {string|symbol} key - 发生变化的属性名*/
function trigger(target, key) {// 获取目标对象的依赖映射const depsMap = targetMap.get(target);// 如果没有依赖映射,直接返回(说明没有依赖需要触发)if (!depsMap) return;// 获取该属性对应的依赖集合const dep = depsMap.get(key);// 如果存在依赖集合,则遍历执行所有 effectdep && dep.forEach(effect => effect());
}/*** 创建响应式状态* @param {any} initialValue - 初始值* @param {string} [identifier] - 可选标识符,用于DOM绑定* @returns {Array} - 返回一个数组,包含状态值、更新函数和清理函数*/
export function useState(initialValue, identifier) {// 生成绑定键名,如果未提供标识符则自动生成(state_0, state_1...)const bindKey = identifier || `state_${uid++}`;// 创建响应式代理对象,只代理 value 属性const state = new Proxy({ value: initialValue }, {/*** 拦截属性读取操作* @param {object} target - 目标对象* @param {string|symbol} key - 被访问的属性名* @returns {any} - 返回属性值*/get(target, key) {// 只有访问 value 属性时才进行依赖收集if (key === 'value') track(target, key);// 使用 Reflect 获取原始值return Reflect.get(target, key);},/*** 拦截属性设置操作* @param {object} target - 目标对象* @param {string|symbol} key - 被设置的属性名* @param {any} value - 新值* @returns {boolean} - 返回是否设置成功*/set(target, key, value) {// 只有设置 value 属性时才触发更新if (key === 'value') {const oldValue = target.value;// 如果新旧值相同,则不触发更新(性能优化)if (oldValue === value) return true;// 设置新值target.value = value;// 触发该属性的所有依赖更新trigger(target, key);}return true;}});/*** 更新DOM的函数,将状态值同步到所有绑定的DOM元素*/const updateDOM = () => {// 获取所有绑定该状态的DOM元素const elements = document.querySelectorAll(`[data-bind="${bindKey}"]`);elements.forEach(el => {// 如果是input元素,更新其value(双向绑定)if (el.tagName.toLowerCase() === 'input') {// 只有值不同时才更新,避免不必要的DOM操作el.value !== state.value && (el.value = state.value);} else {// 其他元素更新其文本内容el.textContent = state.value;}});};// 初始化双向绑定(为input元素添加事件监听)document.querySelectorAll(`[data-bind="${bindKey}"]`).forEach(el => {if (el.tagName.toLowerCase() === 'input') {// input事件处理函数const handler = e => {// 当input值变化时,更新状态值state.value = e.target.value;};// 添加事件监听el.addEventListener('input', handler);// 在元素上存储清理函数,用于后续移除监听el._cleanup = () => el.removeEventListener('input', handler);}});// 初始渲染流程:// 1. 设置当前活跃的effect为updateDOMactiveEffect = updateDOM;// 2. 执行updateDOM(会触发getter,从而收集依赖)updateDOM();// 3. 重置activeEffectactiveEffect = null;// 返回数组,包含:return [// 1. 当前状态值state.value,// 2. 更新函数,支持直接传值或函数式更新(newValue) => {if (typeof newValue === 'function') {// 函数式更新:传入当前值,返回新值state.value = newValue(state.value);} else {// 直接设置新值state.value = newValue;}},// 3. 清理函数,用于移除所有事件监听() => {document.querySelectorAll(`[data-bind="${bindKey}"]`).forEach(el => {// 执行存储在元素上的清理函数el._cleanup?.();// 删除清理函数引用delete el._cleanup;});}];
}
7.3 测试的html文件
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>原生JS响应式数据绑定示例</title><style>body {font-family: Arial, sans-serif;max-width: 600px;margin: 0 auto;padding: 20px;}.container {border: 1px solid #ddd;padding: 20px;border-radius: 8px;margin-top: 20px;}button {padding: 8px 16px;margin-right: 10px;cursor: pointer;}input {padding: 8px;width: 100%;box-sizing: border-box;margin-bottom: 10px;}.output {margin: 15px 0;padding: 10px;background-color: #f5f5f5;border-radius: 4px;}</style>
</head>
<body>
<h1>原生JS响应式数据绑定</h1><div class="container"><h2>双向绑定示例</h2><input type="text" data-bind="message" placeholder="在这里输入..."><div class="output"><p>当前内容: <span data-bind="message"></span></p></div><button id="changeBtn">更改消息</button><button id="resetBtn">重置</button>
</div><div class="container"><h2>计数器示例</h2><p>当前计数: <span data-bind="counter"></span></p><button id="incrementBtn">增加</button><button id="decrementBtn">减少</button>
</div><script type="module">// 导入 reactivity.js 的 useState 函数import { useState } from './reactivity.js';// 1. 双向绑定示例const [message, setMessage, cleanupMessage] = useState("初始消息", "message");// 按钮事件[更改消息]document.getElementById('changeBtn').addEventListener('click', () => {setMessage("这是新消息 " + new Date().toLocaleTimeString());});// 按钮事件[重置]document.getElementById('resetBtn').addEventListener('click', () => {setMessage("");});// 2. 计数器示例const [counter, setCounter] = useState(0, "counter");// 增加document.getElementById('incrementBtn').addEventListener('click', () => {setCounter(c => c + 1);});// 减少document.getElementById('decrementBtn').addEventListener('click', () => {setCounter(c => c - 1);});// 在页面卸载时清理(防止内存泄漏)window.addEventListener('beforeunload', () => {cleanupMessage();// 如果有其他 state 也需要清理...});
</script>
</body>
</html>
7.4 效果展示
用法
7.5 useState参数说明:
const [value, setValue, cleanup] = useState(initialValue, identifier);
initialValue
(必需)
-
类型:任意(数字、字符串、对象等)
-
作用:状态的初始值
-
示例:
useState(0) // 数字 useState("hello") // 字符串 useState({ a: 1 }) // 对象
identifier
(可选)
-
类型:字符串
-
作用:唯一标识符,用于关联 DOM 元素的
data-bind
属性 -
默认值:自动生成
state_0
、state_1
等(通过uid++
) -
示例:
useState("hello", "message") // 手动指定标识符
7.6 useState返回值:
返回一个数组,包含三个元素:
const [value, setValue, cleanup] = useState(...);
value
当前状态值(响应式,变化会自动更新 DOM)
setValue
更新状态的函数,支持两种用法:
setValue("新值") // 直接设置新值
setValue(prev => prev + 1) // 基于旧值计算新值
cleanup
(可选)
// 示例:在页面卸载时清理
window.addEventListener('beforeunload', cleanup);
-
类型:函数
-
作用:清理与该状态关联的所有 DOM 事件监听器(防止内存泄漏)
-
调用时机:组件卸载或不再需要该状态时
7.7 useState使用示例:
1. 基本用法(自动生成标识符):
const [count, setCount] = useState(0);
// DOM 绑定:<span data-bind="state_0"></span>
2. 指定标识符(推荐):
const [text, setText] = useState("", "message");
// DOM 绑定:<input data-bind="message">
3. 带清理的用法:
const [user, setUser, cleanupUser] = useState(null, "userData");// 不再需要时清理
cleanupUser();
7.8 useState注意事项:
1.必须通过 data-bind
属性关联 DOM 元素,例如:
<div data-bind="message"></div>
<input data-bind="message">
2.双向绑定仅对 input 元素自动生效,其他元素需手动处理事件。
3.如果多个 useState 需要联动(如计算属性),需在 setValue 回调中处理:
const [firstName, setFirstName] = useState("", "firstName");
const [lastName, setLastName] = useState("", "lastName");
const [fullName, setFullName] = useState("", "fullName");// 当 firstName 或 lastName 变化时更新 fullName
setFirstName(name => {setFullName(`${name} ${lastName}`);return name;
});setLastName(name => {setFullName(`${firstName} ${name}`);return name;
});