🦉 Hooks 🦉
概述
Hooks 最初由 React 推广,其目的是为了解决以下问题:
- 帮助在组件之间复用带有状态的逻辑
- 在复杂组件中按功能组织代码
- 在函数组件中使用状态,而无需编写类组件
Owl 中的 hooks 也服务于相同的目的,不同的是它们可以用于类组件(注意:React 的 hooks 不能用于类组件,这可能导致一些人误以为 hooks 与类组件是对立的。然而 Owl 的 hooks 很好地证明了这种观点是错误的)。
Hooks 与 Owl 的组件机制结合得非常好:它们能很好地解决上述问题,尤其是可以让你的组件实现响应式,这是一种非常理想的方式。
Hook 使用规则
只有一条规则:每个组件中的 hook 必须在 setup
方法中调用,或者在 类字段(class field)中调用:
// ✅ 正确
class SomeComponent extends Component {
state = useState({ value: 0 });
}
// ✅ 也正确
class SomeComponent extends Component {
setup() {
this.state = useState({ value: 0 });
}
}
// ❌ 错误:这是在构造函数调用后执行的
class SomeComponent extends Component {
async willStart() {
this.state = useState({ value: 0 });
}
}
生命周期钩子
所有生命周期钩子在其对应的专属章节中有详细文档说明。
钩子名 | 描述 |
---|---|
onWillStart | 异步钩子,首次渲染前执行 |
onWillRender | 组件即将渲染前执行 |
onRendered | 组件刚渲染完成后执行 |
onMounted | 组件渲染完成并插入到 DOM 后立即执行 |
onWillUpdateProps | 异步钩子,组件 props 更新前执行 |
onWillPatch | DOM 即将被更新前执行 |
onPatched | DOM 更新完成后执行 |
onWillUnmount | 组件从 DOM 中移除前执行 |
onWillDestroy | 组件即将被销毁前执行 |
onError | 捕获并处理错误(详见错误处理页) |
其他 Hooks
useState
useState
是 Owl 中最重要的 hook 之一:它使组件具备响应性,能够对状态变化作出响应。
该 hook 必须接收一个对象或数组作为参数,并返回一个响应式版本(通过 Proxy
实现)。
const { useState, Component } = owl;
class Counter extends Component {
static template = xml`
<button t-on-click="increment">
Click Me! [<t t-esc="state.value"/>]
</button>`;
state = useState({ value: 0 });
increment() {
this.state.value++;
}
}
⚠️ 注意:useState
只能用于对象或数组。这是必须的,因为 Owl 需要能够追踪和响应状态的变化。
useRef
useRef
在需要与组件内部某个由 Owl 渲染的 HTML 元素交互时非常有用。它只能用于带有 t-ref
指令的 HTML 元素:
<div>
<input t-ref="someInput"/>
<span>hello</span>
</div>
在这个例子中,组件可以使用 useRef
访问该 input
:
class Parent extends Component {
inputRef = useRef("someInput");
someMethod() {
// 如果组件已挂载,refs 已激活:
// - this.inputRef.el 就是该 input 的 HTMLElement 实例
}
}
你可以通过 .el
来访问对应的原生 DOM 元素。
t-ref
指令也支持动态值(字符串插值):
<div t-ref="div_{{someCondition ? '1' : '2'}}"/>
相应地,可以这样设置引用:
this.ref1 = useRef("div_1");
this.ref2 = useRef("div_2");
⚠️ 只有当父组件已挂载时,引用才是活跃的;否则,访问 .el
将返回 null
。
useSubEnv
和 useChildSubEnv
env
有时用于在所有组件间共享一些通用信息。但有时我们希望限定信息的作用范围仅在某个子树内。
例如,在一个表单组件中,我们可能希望把一个 model
对象提供给其所有子组件使用,而不是整个应用。此时可以使用 useChildSubEnv
:
class FormComponent extends Component {
setup() {
const model = makeModel();
useSubEnv({ model }); // 当前组件及其所有子组件都可访问 model
useChildSubEnv({ someKey: "value" }); // 仅子组件可访问 someKey
}
}
这两个 hook 接收一个对象参数,内部会创建一个新的 env
对象:
useSubEnv
会将新 env 应用于当前组件及所有子组件useChildSubEnv
只将新 env 应用于子组件(当前组件保持原样)
Owl 中通过这两个 hook 创建的环境都是冻结的(frozen),不可被修改。
这两个 hook 都可以调用多次,最终合并成一个完整的环境。
useExternalListener
useExternalListener
用于在组件挂载/卸载时自动添加和移除监听器。它接收目标对象作为第一个参数,后续参数会传递给 addEventListener
。
例如,一个下拉菜单可能需要在 window
上监听点击事件来关闭菜单:
useExternalListener(window, "click", this.closeMenu, { capture: true });
useComponent
用于构建自定义 hook 时获取当前组件实例的引用。
function useSomething() {
const component = useComponent();
// component 即为当前组件实例
}
useEnv
用于构建自定义 hook 时获取当前组件的 env
引用。
function useSomething() {
const env = useEnv();
// env 即为当前组件的环境对象
}
useEffect
该 hook 在组件挂载和更新(patch)后运行一个副作用函数;在组件即将更新或卸载时执行清理函数(前提是依赖发生了变化)。
它的 API 类似于 React 的 useEffect
,但依赖是通过函数返回的,而不是数组。
该 hook 接收两个函数:
- 副作用函数(effect):执行某个操作,并可返回清理函数
- 依赖函数:返回一个依赖列表,依赖变化时重新执行副作用
示例(无依赖):
useEffect(
() => {
window.addEventListener("mousemove", someHandler);
return () => window.removeEventListener("mousemove", someHandler);
},
() => []
);
依赖为空时,仅在组件卸载时清理。
如果不提供依赖函数,副作用将在每次 patch 时重新执行。
实现一个 useAutofocus
的例子:
function useAutofocus(name) {
let ref = useRef(name);
useEffect(
(el) => el && el.focus(),
() => [ref.el]
);
}
使用方式:
class SomeComponent extends Component {
static template = xml`
<div>
<input />
<input t-ref="myinput"/>
</div>`;
setup() {
useAutofocus("myinput");
}
}
示例:鼠标位置追踪 Hook
以下是一个追踪鼠标位置的非平凡 Hook 示例:
const { useState, onWillDestroy, Component } = owl;
// 自定义 hook:追踪鼠标位置
function useMouse() {
const position = useState({ x: 0, y: 0 });
function update(e) {
position.x = e.clientX;
position.y = e.clientY;
}
window.addEventListener("mousemove", update);
onWillDestroy(() => {
window.removeEventListener("mousemove", update);
});
return position;
}
// 根组件
class Root extends Component {
static template = xml`<div>Mouse: <t t-esc="mouse.x"/>, <t t-esc="mouse.y"/></div>`;
// 该 hook 将状态绑定到 mouse 属性
mouse = useMouse();
}
👉 注意:我们遵循 React 的命名约定,以 use
开头命名 hook,这只是社区约定,不是强制要求。