前言
最近有被面试官问到一个问题: 你能在Vue的项目里实现一套React Hooks吗?
实话讲,这个问题最难的是如何兼容Vue 和 React的渲染机制。最惨的是,我是被要求现场写代码的。(幸好,我写得一手好字,面试官都惊叹了。咦?我好像嘚瑟错地方了。)
好吧。我是想告诉你两点,简历上千万别写精通。否则,你很有可能遇见和我一样的情况。/手动狗头
手写一套React Hooks前的思考
那,如何描述React Hooks
?
- 对函数型组件进行增强。让函数型组件能去完成类组件做的事情。
- 让函数型组件可以存储状态
- 可以拥有处理副作用的能力
- 让开发者在不使用类组件的情况下,实现相同的功能
那,类组件有什么缺点?类组件的不足 (React Hooks
要解决的问题)
-
缺少逻辑复用机制
- 为了复用逻辑增加无实际渲染效果的组件,增加了组件的嵌套层级,十分臃肿。
- 增加了调试的难度,实际运行的效率被降低。
-
类组件经常会变得很复杂,难以维护。
- 将一组相干的业务逻辑拆分到了多个生命周期函数中
- 在一个生命周期函数里存在多个不想干的业务逻辑。
- 类成员方法不能保证
this
指向的正确性 - 当我们给一个元素绑定事件,在事件处理函数当中,我们要更改状态的时候,通常需要更正函数内部的
this
指向,不如this
就指向undefined
。 - 解决方法就是
bind
或者函数嵌套函数的方式去更改this
指向。
**那,React Hooks
有什么明显的特征? **
特征点 | React Hooks | Class |
---|---|---|
使用场景 | function 内部 | 没什么限制,哪都能写class |
更改状态 | const [state,setState] = useState(initState) | this.setState |
生命周期 | 不存在,但可以通过Effect Hooks模拟 | 有严格的生命周期函数 |
状态存储 | State Hooks | 状态变量都存放在this.state里面 |
状态监控单位 | 状态变量为单位 | 无此概念,只能在生命周期函数里写入监控代码 |
useState分析与实现
先看看官方提供的 useState 栗子。
import React, { useState } from 'react';
function Example() {
// 声明一个新的叫做 “count” 的 state 变量
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
那,如果我们再改改。例如这样:
import React, { useState } from 'react';
function Example() {
// 声明一个新的叫做 “count” 的 state 变量
const [count, setCount] = useState([0]);
const [name, setName] = useState('胖子');
const [meizhi, setMeiZhi] = useState(()=>'大佬');
return (
<div>
<p>You clicked {count} times</p>
<p>Call me {name}</p>
<p>{name} is meizhi ?</p>
<button onClick={() => {
setCount([count[0] + 1]);
setName('胖子是渣男');
setMeiZhi((preMeizhi)=>`女装${preMeizhi}`);
}}>
Click me
</button>
</div>
);
}
我们,可以总结一下useState的用法.
- 接收任意类型的入参数,可以是数组、对象、函数等。
- 可以被多次调用,每调用一次都可以解构出不同的状态变量。
setState
也可以传入回调函数。改回调函数会被自动传入当前对应的状态变量。- 被结构出来的状态变量和更改状态变量的方法是1对1绑定的。
- 调用
setState
后,要重新render
函数组件
开始写
// import React from 'react';
import ReactDOM from 'react-dom';
// 先把render写出来,备着。
function render () {
ReactDOM.render(<App />, document.getElementById('root'));
}
// App 组件也先备好
function App() {
const [count, setCount] = useState([0]);
const [name, setName] = useState('胖子');
const [meizhi, setMeiZhi] = useState(()=>'大佬');
return (
<div>
<p>You clicked {count} times</p>
<p>Call me {name}</p>
<p>{name} is meizhi ?</p>
<button onClick={() => {
setCount([count[0] + 1]);
setName('胖子是渣男');
// setMeiZhi((preMeizhi)=>`女装${preMeizhi}`);
}}>
Click me
</button>
</div>
);
}
// step 1 , 先从结构useState(...) 开始
let state;
function useState(initState) {
if (!state) state = initState; // 如果state为undefined,则说明是第一次调用useState。即页面没被重新render过
let setState = (newState) => {
if (state !== newState) {
state = newState;
}
render()
}
return [state, setState]
}
点击Click me
之前
点击Click me
之后
问题很明显了,因为代码里只有一个state变量,每次调用useState返回的状态变量都是state。
我们要做的是,每次调用useState都要结构出不同的[state、setState]
。 再来。
let state = []; // 存放状态变量
let setters = []; // 存放更改状态变量的方法
let stateIndex = 0; // 用来绑定state[...] 和 setters[...],即状态变量和更改它的方法要保证1对1关系。
function useState (initialState) {
state[stateIndex] = state[stateIndex] ? state[stateIndex] : initialState; // 同理。这是把状态存放到数组当中。
setters.push(createSetter()); // 根据索引stateIndex来创建setState方法,并push到setters数组当中。
let value = state[stateIndex]; // 从状态变量数组中取值
let setter = setters[stateIndex]; // 从setState方法数组中取出对应的setState
stateIndex++
return [value, setter]; // 返回,供调用者解构。
}
function createSetter () {
return (newState) => {
state[stateIndex]= newState;
render();
};
}
看结果,是:
只有第一行变了。 说明,只有第一个状态变量的值被重新render了。 而代码里,明明已经将stateIndex
交给createSetter
方法了。 仔细品品,就能发现一个问题。 当你调用多次setState的时候,stateIndex都没有被保留下来。
这里通过闭包来解决这个问题。
let state = []; // 存放状态变量
let setters = []; // 存放更改状态变量的方法
let stateIndex = 0; // 用来绑定state[...] 和 setters[...],即状态变量和更改它的方法要保证1对1关系。
function useState (initialState) {
state[stateIndex] = state[stateIndex] ? state[stateIndex] : initialState; // 同理。这是把状态存放到数组当中。
setters.push(createSetter(stateIndex)); // 根据索引stateIndex来创建setState方法,并push到setters数组当中。
let value = state[stateIndex]; // 从状态变量数组中取值
let setter = setters[stateIndex]; // 从setState方法数组中取出对应的setState
stateIndex++
return [value, setter]; // 返回,供调用者解构。
}
function createSetter (index) {
// 通过返回一个新方法,将传入的index保留下来。 这里就是闭包
return function (newState) {
state[index] = newState;
render ();
}
}
再看结果:
再把useState 和setState 传递的参数类型function 做一次处理。 完整代码如下:
let state = [];
let setters = [];
let stateIndex = 0;
const getStateByFn = (v, params) => {
if (typeof v === 'function') {
const _newState = v(params);
if (!_newState) throw 'You must be return state'
return _newState
}
return v
}
function createSetter(index) {
return function (newState) {
state[index] = getStateByFn(newState,state[index])
render();
}
}
function useState(initialState) {
state[stateIndex] = state[stateIndex] ? getStateByFn(state[stateIndex]) : getStateByFn(initialState);
setters.push(createSetter(stateIndex));
let value = state[stateIndex];
let setter = setters[stateIndex];
stateIndex++;
return [value, setter];
}
function render() {
stateIndex = 0;
ReactDOM.render(<App />, document.getElementById('root'));
}
function App() {
const [count, setCount] = useState([0]);
const [name, setName] = useState('胖子');
const [meizhi, setMeiZhi] = useState(() => '大佬');
return (
<div>
<p>You clicked {count} times</p>
<p>Call me {name}</p>
<p>{name} is {meizhi} ?</p>
<button onClick={() => {
console.log('count:', count, 'name:', name, 'meizhi:', meizhi)
setCount([count[0] + 1]);
setName('胖子是渣男');
setMeiZhi((preMeizhi) => `女装${preMeizhi}`);
}}>
Click me
</button>
</div>
);
}
点击 Click me
后效果如下:
未完待续
写得很细。 如果有疑惑的地方,欢迎留言提问。
下篇会讲解useEffect的实现。