useState & useEffect
发现了一个对 React + useState + useHooks
的简单实现:
let React = (function() {
let global = {}; // define a global variable where we store information about the component
let index = 0; // index to keep track of the component's state
function render(Component) {
global.Component = Component;
const instance = Component(); // get the instance of the component
index = 0;
instance.render(); // call the component's render function
global.instance = instance; // store the component's instance for any future calls of the component's functions
return global; // return the global variable
}
function useState(initialState) {
if (!global) {
throw new Error("Need a global");
}
if (!global.hooks) {
global.hooks = []; // this array holds the state of the component
}
const hooks = global.hooks;
const currentState = global.hooks[index] || initialState;
hooks[index] = currentState; // memoize the state for future access
firstrender = true;
const setState = (function() {
let currentIndex = index; // copy the index so each useState call will have it's own "closed" value over index (currentIndex)
return function(value) {
global.hooks[currentIndex] = value;
render(global.Component); //re-render the component after state change
};
})();
index = index + 1;
return [currentState, setState];
}
function useEffect(cb, deps) {
const hooks = global.hooks;
// getting older dependencies from the hooks array since
// we are storing dependencies as a sub-array inside the hooks array
let oldDeps = hooks[index];
// if no dependencies are provided,
// the callback function will be called at each re-render
let hasChanged = true;
if (oldDeps) {
// checking if the old dependencies are different from older dependencies
hasChanged = deps.some((d, index) => !Object.is(d, oldDeps[index]));
}
if (hasChanged) cb(); // if dependencies has changed call the callback function.
hooks[index] = deps; //store dependencies inside the hooks array as a sub-array
index++; // increment index for any other useEffect calls
}
return { render, useState, useEffect };
})();
function Component() {
// Component is called at each re-render. index is reset to 0.
const [count, setCount] = React.useState(0);
const [word, setWord] = React.useState("");
const countSetter = () => { setCount(count + 1) };
const wordSetter = word => { setWord(word); };
function render() {
console.log(`Count is: ${count}, Word is: ${word}`);
}
React.useEffect(() => {
console.log("hookssss!!!!");
}, [count, word]);
React.useEffect(() => {
console.log("hooks2!!!!!");
}, []);
return { render, countSetter, wordSetter };
}
const global = React.render(Component);
global.instance.countSetter();
global.instance.countSetter();
global.instance.countSetter();
global.instance.wordSetter("yooo");
global.instance.wordSetter("ssup");
首先使用 IIFE 来持有两个变量 global
和 index
; 还有三个方法 render
, useState
和 useEffect
.
index
的作用是记录当前的 hooks 数组下标. 当使用 useState
或者 useEffect
时候进行移位.
在 render
方法中, 需要将 index
重置为 0, 不管是第一次 render 还是状态变化导致的 rerender.
useState
里面初始化了 hooks
数组, 当第一次 render, 会将初始的 state
存放到 hooks
数组内, 然后 index++
. 如果是 rerender, 则取出 hooks
内的数据(缓存).
有趣的是 setState
方法, 它使用 IIFE 来记录了当前 state
所在 hooks
的下标 index
, 然后是对 hooks
之前的旧变量进行覆盖, 最后再调用 render
方法进行 rerender.
useEffect
也是比较有趣, 它也是利用的 hooks
数组, 当第一次调用, 也就是第一次 render 时候, hooks
当前 index
数据为空, 所以第一次 render 必定会执行回调函数. 然后将 deps
存入到 hooks[index]
, 这样不管是哪次 render 都是记录了上一次的数据. 当 deps
是个空数组时, if
内的 some
永远是 false
, 所以就达成了: 空数组代表着 componentDidMount
. 当非首次 render时, 要进行判断:
hasChanged = deps.some((d, index) => !Object.is(d, oldDeps[index]))
传入的 deps
的每个元素, 是否在 oldDeps
下有变化, 如果有变化, 则需要执行回调.
useEffect
使用了 hooks
而且可以多次调用 useEffect
所以到最后也需要将 index++
.
这样, 不管是第一次 render 还是第 n 次 render, 都是顺序使用的 index
, hooks
都不会乱, 正式因为这个原因:
Don’t call Hooks inside loops, conditions, or nested functions.
这个实现的不足点
- 只能 render 一个 Component, 因为此实现里的 React 只有一个
global
和index
, 如果存在多个Component
, 则共用同一份数据, 则会出错. deps.some((d, index) => !Object.is(d, oldDeps[index]))
这样写, 逻辑比较混乱.- BUG: 当 Component 不使用
useState
而使用useEffect
, 或者useEffect
在useState
前使用, 则hooks
不会被初始化而报错. useEffect
并不是每次 render 后执行回调, 而是立即调用.
自己实现了一个简易的 useState
let state = [];
let index = 0;
let global = {};
function createSetter(index) {
return function(newVal) {
state[index] = newVal;
// TODO: re-render
};
}
function useState(initVal) {
const value = state[index] || initVal;
state[index] = value;
const setter = createSetter(index);
index++;
return [value, setter];
}
function Component1() {
const [firstName, setFirstName] = useState("Fai");
const [lastName, setLastName] = useState("Chou");
console.log(firstName);
console.log(lastName);
return {
setFirstName,
setLastName,
}
}
function render(Component) {
var componentSetters = Component();
console.log(componentSetters)
for (const [key, value] of Object.entries(componentSetters)) {
global[key] = value;
}
}
function APP() {
index = 0; // reset
render(Component1);
}
console.log(state); // []
APP();
console.log(state); // First-render: ['Fai', 'Chou']
APP();
console.log(state); // Subsequent-render: ['Fai', 'Chou']
global.setFirstName('Hui');
console.log(state); // After: ['Hui', 'Chou']