前言
最近有被面试官问到一个问题: 你能在Vue的项目里实现一套React Hooks吗?
实话讲,这个问题最难的是如何兼容Vue 和 React的渲染机制。最惨的是,我是被要求现场写代码的。(幸好,我写得一手好字,面试官都惊叹了。咦?我好像嘚瑟错地方了。)
好吧。我是想告诉你两点,简历上千万别写精通。否则,你很有可能遇见和我一样的情况。/手动狗头
useEffect分析
useEffect
的执行时机
可以把useEffect
看作 componentDidMount
, componentDidUpdate
, componentWillMount
这三个生命周期函数的组合。
useEffect(()=>{})
<=>componentDidMount,componentDidUpdate
useEffect(()=>{},[])
<=>componentDidMount
useEffect(()=>()=>{})
<=>componentWillUnmount
// 来看个实际使用的例子
function App4(props) {
const [count, setCount] = useState(()=>{
return props.count || 0; // 组件第一次被渲染的时候执行
});
useEffect(()=>{
console.log('我被 did mount/update 了');
})
useEffect(()=>{
console.log('我只被 did mount 了');
},[])
useEffect(()=>{
return ()=>{
console.log('我被 unmount 了')
}
})
return <div>
<span>{count}</span>
<button onClick={() => setCount(count + 1)}> 点击给 count + 1</button>
<button onClick={() => ReactDom.unmountComponentAtNode(document.getElementById('root'))}> 卸载组件</button>
</div>
}
复制代码
我们可以看到,useEffect
有三种使用场景。 分别是组件初次挂载、组件更新、组件卸载。大家可以拿这个demo去跑一下,实际操作一下。 我这里就不截图了。
那 useEffect
不仅仅只是这些吧? 我要就写到这,怕不是要被小伙伴们打死。????
我们知道,useEffect
除了能直接处理副作用,还能起到监听状态变量
的作用。 如果说副作用只是某件事情,那监听状态变量就是为了从状态变量上控制由状态变量引起的副作用发生。说得可能有点绕,还是来个to do list。
useEffect(()=>{
console.log('监听 空的[], 代表当前副作用只会被执行一次');
},[])
useEffect(()=>{
console.log('监听了[state0,state1,state2] 数组里的三个state, 代表当前副作用只有当[state0,state1,state2] 里的三个状态变量发生了改变才会被执行 ');
},[state0,state1,state2]);
复制代码
噢。那我们再分析一下,
useEffect
可以接收两个参数,第一个是callback(副作用处理回调),。第二个是个list,但非必传。useEffect
不需要被结构出实际的值useEffect
也可以被多次调用useEffect
一旦被调用就会产生一个监听useEffect
的监听对象可以是state,也可以是props
useEffect 实现
- 先对useEffect 的 入参数类型进行校验。
- 对第二个入参进行判断有无的逻辑处理
- 一旦起了监听,就必须将监听状态变量保存一份。
- 最难实现的一段逻辑: 如何判断副作用是否需要被执行。
// Last Dependency value
let prevDepsAry = [];
let effectIndex = 0;
function useEffect1(callback, depsAry) {
if (Object.prototype.toString.call(callback) !== '[object Function]') throw new Error('useEffect. The first argument must be a function method')
if (typeof depsAry === 'undefined') {
// no state
callback();
} else {
// Determine whether depsAry is an array.
if (Object.prototype.toString.call(depsAry) !== '[object Array]') throw new Error('useEffect. The next argument must be a Array')
// Gets the last status value.
let prevDeps = prevDepsAry[effectIndex];
// Compare the current dependency value with the last dependency value. If there is any change, to call back fn.
let hasChange = prevDeps ? depsAry.every((dep, index) => dep === prevDeps[index]) === false : true
if (hasChange) {
callback();
}
prevDepsAry[effectIndex] = depsAry;
effectIndex++;
}
}
function App11() {
const [count, setCount] = useState1(0);
const [name, setName] = useState1('胖子');
useEffect1(() => {
console.log(`hello ${count}`)
}, [count])
useEffect1(() => {
console.log(`hello ${name}`)
}, [name])
return <div>
<span>{count}</span>
<button onClick={() => setCount(count + 1)}> 点击给 count + 1</button>
<span>{name}</span>
<button onClick={() => setName('渣男')}> setName</button>
</div>
}
复制代码
我错了,我把我写的很辣鸡的注释改成中文???
请重新看看
// 上一次的依赖值
let prevDepsAry = [];
let effectIndex = 0;
function useEffect(callback, depsAry) {
// 判断callback是不是函数
if (Object.prototype.toString.call(callback) !== '[object Function]') throw new Error('useEffect函数的第一个参数必须是函数');
// 判断depsAry有没有被传递
if (typeof depsAry === 'undefined') {
// 没有传递
callback();
} else {
// 判断depsAry是不是数组
if (Object.prototype.toString.call(depsAry) !== '[object Array]') throw new Error('useEffect函数的第二个参数必须是数组');
// 获取上一次的状态值
let prevDeps = prevDepsAry[effectIndex];
// 将当前的依赖值和上一次的依赖值做对比 如果有变化 调用callback
let hasChanged = prevDeps ? depsAry.every((dep, index) => dep === prevDeps[index]) === false : true;
// 判断值是否有变化
if (hasChanged) {
callback();
}
// 同步依赖值
prevDepsAry[effectIndex] = depsAry;
effectIndex++; // 在render 方法里面 effectIndex 要重置为 0;
}
}
复制代码
好了,去试试吧。
顺便把 useReducer 写了吧。。 因为很简单。。
上代码,看了你就知道为啥简单了。 ????
function useReducer (reducer, initialState) {
const [state, setState] = useState(initialState);
function dispatch (action) {
const newState = reducer(state, action);
setState(newState);
}
return [state, dispatch];
}
function App() {
function reducer (state, action) {
switch (action.type) {
case 'increment':
return state + 1;
case 'decrement':
return state - 1;
default:
return state;
}
}
const [count, dispatch] = useReducer(reducer, 0);
return <div>
{count}
<button onClick={() => dispatch({type: 'increment'})}>+1</button>
<button onClick={() => dispatch({type: 'decrement'})}>-1</button>
</div>;
}
复制代码
Yeah! 你没看错,这里的dispatch
和 Redux
里面的 dispatch
很像。 但重点是,这里使用了useState
! 关于useState
的实现,请回头看 useState
所以很简单吧 ?
小结一下
简单伐? 这波终于简单了伐? 不要再说我写的难了?????
下篇可能会写 memo
以及 useMemo
。 如果写的话,那还得先写 Fiber
。 因为 memo 就是要影响Fiber节点上的 effectTag
(当前 Fiber 要被执行的操作 (新增, 删除, 修改)
) 。
放心吧。 我对 Fiber
也实现了一套,要求再高的话,那就得去实现React 协调层和调度层了 ?♂️ ?♂️ ?♂️