众所周知,React有类组件和函数组件两种形式的组件,开发者可以使用类组件和函数组件达到相同的目的,构建完全一样的页面。自从去年八月份接触React以来,我一直使用的就是函数组件,因为公司推荐使用函数组件。不得不说函数组件比类组件好用很多。使用函数组件,不用去考虑复杂的组件生命周期,因为组件的生命周期都被hook替代了,你可以使用useEffect实现类组件的componentDidUpdate
、componentDidMount
和componentWillUnmount
。在感叹hook强大的同时,我也经常在想React到底是怎么实现useEffect和useState hook的呢?函数组件每次渲染,都仅仅是将组件函数执行一遍,函数不是instance(无this指针),那么组件的状态是如何记录和更新的呢?可能你也和我一样,都会猜到函数组件的状态是通过**闭包(closure)**实现的,那么恭喜你,你猜的是对的,hook就是利用了闭包的思想。对闭包不了解的伙伴,请先移步MDN,学习下闭包的概念,在来到这里继续阅读。
本篇文章时长大约15分钟,相信你看完后必定有所收获。 文章内容主要是通过自己手动实现一个模拟的React加深了解hook的工作原理。包括最常用的useEffect和useState的实现。我参考了一些博客和资料,然后自己手动整理实践。
本篇文章代码github: https://github.com/pengfeiw/smallJsCodeDemo/blob/master/realizeReactHook.js
在模拟实现React之前,我们必须了解一些概念。
JSX就是一个普通的javascript对象,Babel 会把 JSX 转译成一个名为 React.createElement() 函数调用。 例如下面两段代码,是等价的。
const element = (
<h1 className="greeting">
Hello, world!
</h1>
);
// 经过babel转译后
const element = React.createElement(
'h1',
{className: 'greeting'},
'Hello, world!'
);
React.createElement返回的就是一个普通的javascript Object.
函数组件每次重新渲染,其实就是组件的函数执行一次。
例如下面的App组件每次重新渲染,其实就是执行App(props)。
const App = (props) => {
...
};
首先我们自己实现一个最简单的,只有render函数的React。render函数接受一个Component(组件)作为参数,exampleProps表示传递给组件的Props。
const React = {
render: Component => {
const exampleProps = {
unit: "likes"
};
const compo = Component(exampleProps);
compo.render();
return compo;
}
}
为了测试这个简易版的React,我们需要一个组件,组件的render函数输出了一些信息。
const Component = (props) => {
return {
render: () => {
console.log("render", {
type: "div",
inner: `${props.unit}`
})
}
};
}
接着实际应用看下效果。
let App = React.render(Component); // log: render {type: "div", inner: "likes"}
App = React.render(Component); // log: render {type: "div", inner: "likes"}
我们Component渲染了两次,两次渲染都打印了log信息。
接下来,我们扩展React,实现useState。useState可以返回一个值和一个可以更新该值的dispatcher,useState接受一个初始值参数,可以设定state的初始值。
Returns a stateful value, and a function to update it.
函数组件通过useState,来创建和更新组件的状态,是react中最常用hook之一。下面我们自己实现useState。
const React = {
index: 0,
state: [],
useState: defaultProp => {
const cachedIndex = React.index;
if (!React.state[cachedIndex]) {
React.state[cachedIndex] = defaultProp;
}
const currentState = React.state[cachedIndex];
const currentSetter = newValue => {
React.state[cachedIndex] = newValue;
};
React.index++;
return [currentState, currentSetter];
},
render: Component => {
const exampleProps = {
unit: "likes"
};
const compo = Component(exampleProps);
compo.render();
React.index = 0; // reset index
return compo;
}
}
这里用到了闭包的功能。我们把state值存储到React.state数组中,然后创建了一个currentSetter,用来更新state值。最后增加index,用于存储新的state值。由于闭包的特性,在组件函数内有多个useState值时,每个cachedIndex都是独立的,所以每次currentSetter对应的state也是独立的。还有一点需要注意的是在render函数中必须重置React.index = 0
,因为组件函数每次执行,index都必须为0,因为hook也是每次都执行的。
更新Component,为其添加状态。
const Component = (props) => {
const [count, setCount] = React.useState(0);
const [name, setName] = React.useState("Steve");
return {
click: () => setCount(count + 1),
personArrived: person => setName(person),
render: () => {
console.log("render", {
type: "div",
inner: `${count} ${props.unit} for ${name}`
})
}
};
}
测试组件渲染。
let App = React.render(Component); // log: render {type: "div", inner: "0 likes for Steve"}
App = React.render(Component); // log: render {type: "div", inner: "0 likes for Steve"}
App.click();
App = React.render(Component); // log: render {type: "div", inner: "1 likes for Steve"}
App.click();
App.personArrived("Peter");
App = React.render(Component); // log: render {type: "div", inner: "2 likes for Peter"}
我们模拟的useState功能符合预期。虽然与实际的React中的useState相差很大,但是足够帮助我们理解了。
在之前的文章我有讲解过useEffect和useLayoutEffect的区别。useEffect是异步执行的,它在组件函数执行后再执行。文档是这样描述useEffect的,我摘抄了几点最重要的特性。
- The function passed to
useEffect
will run after the render is committed to the screen.- By default, effects run after every completed render, but you can choose to fire them only when certain values have changed.
- the function passed to
useEffect
may return a clean-up function.
第一点就是表示useEffect是异步的,第二点表示我们可以给useEffect传递第二个参数(依赖)控制useEffect中的第一个函数参数是否执行,第三点就是我们传递的函数可以返回一个函数,作为cleanup或者unsubscribe函数。
我们就按照上面三个特性,实现自己的useEffect。
const React = {
index: 0,
state: [],
useState: defaultProp => {
const cachedIndex = React.index;
if (!React.state[cachedIndex]) {
React.state[cachedIndex] = defaultProp;
}
const currentState = React.state[cachedIndex];
const currentSetter = newValue => {
React.state[cachedIndex] = newValue;
};
React.index++;
return [currentState, currentSetter];
},
useEffect: (callback, dependencies) => {
const cachedIndex = React.index;
const hasChanged = dependencies !== React.state[cachedIndex];
if (dependencies === undefined || hasChanged) {
callback();
React.state[cachedIndex] = dependencies;
}
React.index++;
return () => console.log("unsubscribed effect");
},
render: Component => {
const exampleProps = {
unit: "likes"
};
const compo = Component(exampleProps);
compo.render();
React.index = 0; // reset index
return compo;
}
}
将dependencies存储在React.state中,通过比较dependencies与React.state[cachedIndex](之前存储的dependencies)来判断依赖是否改变。如果改变了就执行callback,并更新React.state中存储的dependencies,用于下次执行判断。
同样我们更新下Component,加入useEffect hook。
const Component = (props) => {
const [count, setCount] = React.useState(0);
const [name, setName] = React.useState("Steve");
const exitThis = React.useEffect(() => {
console.log("Effect ran");
}, name);
return {
click: () => setCount(count + 1),
personArrived: person => setName(person),
unsubscribe: () => exitThis(),
render: () => {
console.log("render", {
type: "div",
inner: `${count} ${props.unit} for ${name}`
})
}
};
}
最后返回对象包含了一个unsubscribe,用于模拟useEffect的cleanup工作。
测试useEffect功能。
let App = React.render(Component);
// log: Effect ran
// render {type: "div", inner: "0 likes for Steve"}
App = React.render(Component); // log: render {type: "div", inner: "0 likes for Steve"}
App.click();
App = React.render(Component); // log: render {type: "div", inner: "1 likes for Steve"}
App.click();
App.personArrived("Peter");
App = React.render(Component);
// log: Effect ran
// render {type: "div", inner: "2 likes for Steve"}
App.unsubscribe(); // log: unsubscribed effect
可以看到,useEffect的函数在组件首次加载时执行了,在name更改时,也执行了,最后调用unsubscribe执行清理工作。
到这里,内容差不多结束了,你们一定对hook有了更深入的了解。还有更多类似的hook,你们也可以尝试自己去模拟实现,例如useCallback。
附参考资料:
react官方文档:https://react.docschina.org/docs/getting-started.html
under-the-hood of react:https://itnext.io/under-the-hood-of-react-hooks-805dc68581c3
(完)