Skip to content

🦉 Hooks 🦉

概述

Hooks 最初由 React 推广,其目的是为了解决以下问题:

  • 帮助在组件之间复用带有状态的逻辑
  • 在复杂组件中按功能组织代码
  • 在函数组件中使用状态,而无需编写类组件

Owl 中的 hooks 也服务于相同的目的,不同的是它们可以用于类组件(注意:React 的 hooks 不能用于类组件,这可能导致一些人误以为 hooks 与类组件是对立的。然而 Owl 的 hooks 很好地证明了这种观点是错误的)。

Hooks 与 Owl 的组件机制结合得非常好:它们能很好地解决上述问题,尤其是可以让你的组件实现响应式,这是一种非常理想的方式。

Hook 使用规则

只有一条规则:每个组件中的 hook 必须在 setup 方法中调用,或者在 类字段(class field)中调用

js
// ✅ 正确
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 更新前执行
onWillPatchDOM 即将被更新前执行
onPatchedDOM 更新完成后执行
onWillUnmount组件从 DOM 中移除前执行
onWillDestroy组件即将被销毁前执行
onError捕获并处理错误(详见错误处理页

其他 Hooks

useState

useState 是 Owl 中最重要的 hook 之一:它使组件具备响应性,能够对状态变化作出响应。

该 hook 必须接收一个对象或数组作为参数,并返回一个响应式版本(通过 Proxy 实现)。

js
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 元素:

xml
<div>
    <input t-ref="someInput"/>
    <span>hello</span>
</div>

在这个例子中,组件可以使用 useRef 访问该 input

js
class Parent extends Component {
  inputRef = useRef("someInput");

  someMethod() {
    // 如果组件已挂载,refs 已激活:
    // - this.inputRef.el 就是该 input 的 HTMLElement 实例
  }
}

你可以通过 .el 来访问对应的原生 DOM 元素。

t-ref 指令也支持动态值(字符串插值):

xml
<div t-ref="div_{{someCondition ? '1' : '2'}}"/>

相应地,可以这样设置引用:

js
this.ref1 = useRef("div_1");
this.ref2 = useRef("div_2");

⚠️ 只有当父组件已挂载时,引用才是活跃的;否则,访问 .el 将返回 null


useSubEnvuseChildSubEnv

env 有时用于在所有组件间共享一些通用信息。但有时我们希望限定信息的作用范围仅在某个子树内。

例如,在一个表单组件中,我们可能希望把一个 model 对象提供给其所有子组件使用,而不是整个应用。此时可以使用 useChildSubEnv

js
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 上监听点击事件来关闭菜单:

js
useExternalListener(window, "click", this.closeMenu, { capture: true });

useComponent

用于构建自定义 hook 时获取当前组件实例的引用。

js
function useSomething() {
  const component = useComponent();
  // component 即为当前组件实例
}

useEnv

用于构建自定义 hook 时获取当前组件的 env 引用。

js
function useSomething() {
  const env = useEnv();
  // env 即为当前组件的环境对象
}

useEffect

该 hook 在组件挂载和更新(patch)后运行一个副作用函数;在组件即将更新或卸载时执行清理函数(前提是依赖发生了变化)。

它的 API 类似于 React 的 useEffect,但依赖是通过函数返回的,而不是数组

该 hook 接收两个函数:

  • 副作用函数(effect):执行某个操作,并可返回清理函数
  • 依赖函数:返回一个依赖列表,依赖变化时重新执行副作用

示例(无依赖):

js
useEffect(
  () => {
    window.addEventListener("mousemove", someHandler);
    return () => window.removeEventListener("mousemove", someHandler);
  },
  () => []
);

依赖为空时,仅在组件卸载时清理。

如果不提供依赖函数,副作用将在每次 patch 时重新执行。

实现一个 useAutofocus 的例子:

js
function useAutofocus(name) {
  let ref = useRef(name);
  useEffect(
    (el) => el && el.focus(),
    () => [ref.el]
  );
}

使用方式:

js
class SomeComponent extends Component {
  static template = xml`
    <div>
        <input />
        <input t-ref="myinput"/>
    </div>`;

  setup() {
    useAutofocus("myinput");
  }
}

示例:鼠标位置追踪 Hook

以下是一个追踪鼠标位置的非平凡 Hook 示例:

js
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,这只是社区约定,不是强制要求。

本站内容仅供学习与参考