从Mixin到HOC再到Hook|博客系列
# 官方提供的Hooks
# State Hook
我们要使用class组件实现一个计数器功能,我们可能会这样写:
export default class Count extends Component {
constructor(props) {
super(props);
this.state = { count: 0 }
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => { this.setState({ count: this.state.count + 1 }) }}>
Click me
</button>
</div>
)
}
}
通过useState,我们使用函数式组件也能实现这样的功能:
export default function HookTest() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => { setCount(count + 1); setNumber(number + 1); }}>
Click me
</button>
</div>
);
}
useState是一个钩子,他可以为函数式组件增加一些状态,并且提供改变这些状态的函数,同时它接收一个参数,这个参数作为状态的默认值。
# Effect Hook
Effect Hook 可以让你在函数组件中执行一些具有 side effect(副作用)的操作
参数
useEffect方法接收传入两个参数:
- 1.回调函数:在第组件一次
render和之后的每次update后运行,React保证在DOM已经更新完成之后才会运行回调。 - 2.状态依赖(数组):当配置了状态依赖项后,只有检测到配置的状态变化时,才会调用回调函数。
useEffect(() => {
// 只要组件render后就会执行
});
useEffect(() => {
// 只有count改变时才会执行
},[count]);
回调返回值
useEffect的第一个参数可以返回一个函数,当页面渲染了下一次更新的结果后,执行下一次useEffect之前,会调用这个函数。这个函数常常用来对上一次调用useEffect进行清理。
export default function HookTest() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log('执行...', count);
return () => {
console.log('清理...', count);
}
}, [count]);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => { setCount(count + 1); setNumber(number + 1); }}>
Click me
</button>
</div>
);
}
执行上面的代码,并点击几次按钮,会得到下面的结果:

注意,如果加上浏览器渲染的情况,结果应该是这样的:
页面渲染...1
执行... 1
页面渲染...2
清理... 1
执行... 2
页面渲染...3
清理... 2
执行... 3
页面渲染...4
清理... 3
执行... 4
那么为什么在浏览器渲染完后,再执行清理的方法还能找到上次的state呢?原因很简单,我们在useEffect中返回的是一个函数,这形成了一个闭包,这能保证我们上一次执行函数存储的变量不被销毁和污染。
你可以尝试下面的代码可能更好理解
var flag = 1;
var clean;
function effect(flag) {
return function () {
console.log(flag);
}
}
clean = effect(flag);
flag = 2;
clean();
clean = effect(flag);
flag = 3;
clean();
clean = effect(flag);
// 执行结果
effect... 1
clean... 1
effect... 2
clean... 2
effect... 3
模拟componentDidMount
componentDidMount等价于useEffect的回调仅在页面初始化完成后执行一次,当useEffect的第二个参数传入一个空数组时可以实现这个效果。
function useDidMount(callback) {
useEffect(callback, []);
}
官方不推荐上面这种写法,因为这有可能导致一些错误。
模拟componentWillUnmount
function useUnMount(callback) {
useEffect(() => callback, []);
}
不像 componentDidMount 或者 componentDidUpdate,useEffect 中使用的 effect 并不会阻滞浏览器渲染页面。这让你的 app 看起来更加流畅。
# ref Hook
使用useRef Hook,你可以轻松的获取到dom的ref。
export default function Input() {
const inputEl = useRef(null);
const onButtonClick = () => {
inputEl.current.focus();
};
return (
<div>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</div>
);
}
注意useRef()并不仅仅可以用来当作获取ref使用,使用useRef产生的ref的current属性是可变的,这意味着你可以用它来保存一个任意值。
模拟componentDidUpdate
componentDidUpdate就相当于除去第一次调用的useEffect,我们可以借助useRef生成一个标识,来记录是否为第一次执行:
function useDidUpdate(callback, prop) {
const init = useRef(true);
useEffect(() => {
if (init.current) {
init.current = false;
} else {
return callback();
}
}, prop);
}
# 使用Hook的注意事项
# 声明约束
- 不要在循环,条件或嵌套函数中调用Hook。
Hook通过数组实现的,每次 useState 都会改变下标,React需要利用调用顺序来正确更新相应的状态,如果 useState 被包裹循环或条件语句中,那每就可能会引起调用顺序的错乱,从而造成意想不到的错误。
我们可以安装一个eslint插件来帮助我们避免这些问题。
// 安装
npm install eslint-plugin-react-hooks --save-dev
// 配置
{
"plugins": [
// ...
"react-hooks"
],
"rules": {
// ...
"react-hooks/rules-of-hooks": "error"
}
}
# 自定义Hook
像上面介绍的HOC和mixin一样,我们同样可以通过自定义的Hook将组件中类似的状态逻辑抽取出来。
自定义Hook非常简单,我们只需要定义一个函数,并且把相应需要的状态和effect封装进去,同时,Hook之间也是可以相互引用的。使用use开头命名自定义Hook,这样可以方便eslint进行检查。
下面我们看几个具体的Hook封装:
# 日志打点
我们可以使用上面封装的生命周期Hook。
const useLogger = (componentName, ...params) => {
useDidMount(() => {
console.log(`${componentName}初始化`, ...params);
});
useUnMount(() => {
console.log(`${componentName}卸载`, ...params);
})
useDidUpdate(() => {
console.log(`${componentName}更新`, ...params);
});
};
function Page1(props){
useLogger('Page1',props);
return (<div>...</div>)
}
# 修改title
根据不同的页面名称修改页面title:
function useTitle(title) {
useEffect(
() => {
document.title = title;
return () => (document.title = "主页");
},
[title]
);
}
function Page1(props){
useTitle('Page1');
return (<div>...</div>)
}
# 双向绑定
我们将表单onChange的逻辑抽取出来封装成一个Hook,这样所有需要进行双向绑定的表单组件都可以进行复用:
function useBind(init) {
let [value, setValue] = useState(init);
let onChange = useCallback(function(event) {
setValue(event.currentTarget.value);
}, []);
return {
value,
onChange
};
}
function Page1(props){
let value = useBind('');
return <input {...value} />;
}
当然,你可以向上面的HOC那样,结合context和form来封装一个更通用的双向绑定,有兴趣可以手动实现一下。
