OWL教程3 OWL五大组成部分之二组件系统
参考文档:https://github.com/odoo/owl/blob/master/doc/reference/component.md
粗略的讲,Owl有五个主要部分:
- 虚拟DOM系统(src/blockdom)
- 组件系统(src/component)
- 模板编译器(src/compiler 目录)
- 一个小的运行时让各层组合在一起(src/app)
- 响应式系统(src/reactivity.ts)
2.组件
2.1 概览
一个Owl组件是一个小的类,描述了部分用户接口,它是组件树的一部分,它有一个环境变量(env),从父组件传播到子组件. (被所有组件共享)
Owl组件通过继承Component类来定义,例如,这里是一个Counter组件的实现:
const { Component, xml, useState } = 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++;
}
}
这个例子中,我们使用xml助手定义了内联模板,使用了useState钩子,它返回一个响应式的参数.
2.2 属性和方法
Component类有一个很小的API
env(object) 组件环境变量
props(object)
父组件传递给子组件的属性值. 注意: props由父组件拥有,所以它不应该由子组件修改.(否则可能会意想不到的风险,因为父组件不知道它被修改了)
props可以动态的被父组件修改, 在这种情况下,子组件会依次调用下列声明周期函数
willUpdateProps
, willPatch
and patched
.
render(deep=false)
调用这个方法会直接触发一次重新渲染. 注意在响应式系统中,这很少需要手动去做. 另外,渲染操作是异步的,所以Dom只会有一点点延迟(在下一个动画帧,如果没有组件延迟的话)
默认情况下, 只渲染组件自己本身,如果需要渲染所有的子组件,需要设置deep=true
2.3 静态属性
-
template (string)
: 组件渲染使用的模板名称,注意可以使用xml助手来定义一个内联模板. -
components (object, optional)
: 模板中使用的所有子组件class ParentComponent extends owl.Component { static components = { SubComponent }; }
-
props (object, optional)
: 用于验证父组件传递给子组件的参数, (开发模式下) See Props Validation for more information.class Counter extends owl.Component { static props = { initialValue: Number, optional: true, }; }
-
defaultProps (object, optional)
: 默认属性, 当props传递给对象的 时候,如果有参数没有赋值,会用默认值代替,注意一点,不会修改初始化的对象,而是新建一个对象来代替See default props for more informationclass Counter extends owl.Component { static defaultProps = { initialValue: 0, }; }
2.4 生命周期循环
一个坚实的,健壮的组件系统需要完整的生命周期系统来帮助开发者来编写组件,这里是关于Owl组件生命周期的完整描述
Method | Hook | Description |
---|---|---|
setup | none | setup |
willStart | onWillStart | async, before first rendering |
willRender | onWillRender | just before component is rendered |
rendered | onRendered | just after component is rendered |
mounted | onMounted | just after component is rendered and added to the DOM |
willUpdateProps | onWillUpdateProps | async, before props update |
willPatch | onWillPatch | just before the DOM is patched |
patched | onPatched | just after the DOM is patched |
willUnmount | onWillUnmount | just before removing component from DOM |
willDestroy | onWillDestroy | just before component is destroyed |
error | onError | catch and handle errors (see error handling page) |
总结一波:
11个钩子可以分成五组:
1 error 处理错误
2 setup willDestroy 总是被执行,setup类似构造函数, willDestroy 最后做一些清理操作.
3 mounted unmounted 一对相反的方法,不一定会被执行,但是要么都执行,要不都不执行, 适合添加事件监听器
4 willstart(只执行一次,可以用来异步加载资源) willrender rendered
5 willupdateprops( 更新props的时候可以异步加载资源) willpatch patched
fatux:
owl的渲染分为两部分,虚拟dom的渲染和物理dom的渲染
mouted、patched、unmouont是针对物理dom的,其他的是针对虚拟dom的,个人理解,不知道对不对。
在组件构建好之后会立刻执行setup函数,这是一个生命周期方法,类似于构造函数,除了它不接收任何参数.
这里是调用钩子函数的合适地方, 请注意,在组件生命周期中使用setup钩子的一个主要原因是可以对其进行monkey patch。这是Odoo生态系统中的一个常见需求。
setup() {
useSetupAutofocus();
}
什么叫monkey patch ?( 来自chatgpt)
“Monkey patch”(猴子补丁)是指在运行时修改现有的代码或类的行为的一种技术。猴子补丁是动态语言(如Python)中的一种常用技巧,允许程序员在不修改原始源代码的情况下进行临时或局部的修改。
通过猴子补丁,您可以在运行时动态地更改类、方法或函数的行为,添加新的功能,修改现有的行为或修复bug。这种技术在开发过程中非常有用,尤其是当您无法修改或无权访问原始源代码时。
猴子补丁的原理是利用动态类型语言的特性,可以在运行时修改对象的方法和属性。这允许您在不修改原始类定义的情况下,通过重新定义方法或添加属性来改变类的行为。
以下是一个简单的Python示例,演示了如何使用猴子补丁来修改现有类的行为:
class MyClass:
def my_method(self):
return "Original behavior"
def modified_method(self):
return "Modified behavior"
obj = MyClass()
print(obj.my_method()) # 输出:Original behavior
MyClass.my_method = modified_method # 猴子补丁,将my_method重新定义为modified_method
print(obj.my_method()) # 输出:Modified behavior
在这个例子中,我们定义了一个类MyClass
和一个名为my_method
的方法。然后,我们使用猴子补丁技术,将my_method
重新定义为modified_method
。这样,实例化的对象obj
调用my_method
时,将输出"Modified behavior",而不是原始的"Original behavior"。
猴子补丁虽然可以灵活地修改现有的代码行为,但也需要谨慎使用。过度使用猴子补丁可能导致代码难以理解、调试和维护。因此,应在慎重思考和评估后,选择是否使用猴子补丁来解决特定的问题。
fatux: 为啥叫猴子补丁呢? 中国有“猴子偷桃”的故事,猴子补丁的意思也类似吧,偷偷的修改代码…
willstart是一个异步的钩子在初始化渲染组件之间用来执行一些操作(大多数时候是异步的)
它在初始化渲染之前只执行一次, 有时候很有用,比如,
1 在渲染组件之前加载一些js文件
, 2 或者从服务器加载数据
setup() {
onWillStart(async () => {
this.data = await this.loadData()
});
}
在这一时刻,组件还没有渲染出来,注意, 过慢的willstart代码会减慢用户界面的渲染. 因此,有时候要让这个方法尽快执行.
不太常用,但是如果在需要渲染组件之前执行一些代码的话(更准确的说,它编译好的模板函数执行时),
setup() {
onWillRender(() => {
// do something
});
}
willRender 钩子在渲染模板之前执行,父组件先,然后子组件.
不太常用,但是如果需要在组件渲染后,(更准确的说,当它的编译模板函数执行完后)调用
setup() {
onRendered(() => {
// do something
});
}
rendered钩子仅仅在渲染完模板后调用,注意: 在这一刻,真实的dom可能还不存在(特别是第一次渲染),或者还没有更新, 这将在下一个动画帧中被显示,只要所有的组件都准备好了.
fatux: 所以willrender和rendered指的是在虚拟内存中渲染。 而mounted是真实的在页面上渲染,此时dom元素已经生成。可以添加各种事件处理函数。 所以,owl的渲染是分两部分的,虚拟渲染和物理渲染。
mounted钩子在每次组件被附加到dom的时候执行, 在初始化渲染完成后, 在这一刻,组件已经激活.
这是个合适的位置用来添加监听器,或者跟Dom交互
跟它相反的是willUnmount, 如果一个组件加载了,那么他在未来某一时刻一定会执行unmount.
mounted方法会在子组件递归的调用,首先子组件,然后父组件.
修改Mounted钩子的状态是被允许的(但是不鼓励). 这样做会引发重新渲染,这将不会被用户察觉,但是会稍微减慢组件的速度.
setup() {
onMounted(() => {
// do something here
});
}
willUpdateProps是一个异步的钩子,在新的props设置之前被调用, 如果组件需要执行一些异步的任务这将很有用,例如, 假设props是记录id,根据id获取记录数据.
onWillUpdateProps 钩子用来注册一个函数在这一刻执行.
setup() {
onWillUpdateProps(nextProps => {
return this.loadData({id: nextProps.id});
});
}
注意: 它将接收新的props作为参数
这个钩子在第一次被渲染的时候不会执行, 但是willstart会执行来类似的事情,另外,跟大多数钩子一样,根据通常的顺序执行,先父组件,然后子组件.
这个钩子在DOM patching进程开始的时候执行,第一次渲染的时候不执行, 这可以用来从DOM中获取信息,例如,当前scrollbar的位置.
注意, 不允许修改状态, 这个方法仅仅在给真实DOM patch的时候调用,仅仅用来保存真实DOM的状态,另外组件不在真实dom中也不允许调用.
setup() {
onWillPatch(() => {
this.scrollState = this.getScrollSTate();
});
}
跟大多数钩子一样,根据通常的顺序执行,先父组件,然后子组件.
当组件更新完DOM的时候会调用这个钩子(最有可能通过改变了它的状态/props或者环境)
这个组件在第一次渲染的时候不会调用, 它用来跟DOM交互, 如果组件不在真实dom中,它不会执行.
setup() {
onPatched(() => {
this.scrollState = this.getScrollSTate();
});
}
在这个钩子里更新组件状态是可能的,但是不鼓励. 注意,这里的更新会产生额外的重新渲染,这里一定要小心,要避免死循环式的重新渲染.
像mounted一样,patched钩子的执行顺序 先子后父.
当组件从DOM卸载的时候会调用这个钩子,这里是移除监听器的好地方.
setup() {
onMounted(() => {
// add some listener
});
onWillUnmount(() => {
// remove listener
});
}
这个方法是mounted方法相反的方法,注意,如果一个组件在mount之前就被销毁了,那么这个方法就不执行了.
执行顺序,先父后子.
有时候,组件需要在setup中做些动作,然后当他们不活跃的时候需要清理. 然后willUnmount钩子并不适合用来做清理操作,因为,组件可能在安装之前就被销毁了, willDestroy就很有用了,因为它总是被执行.
setup() {
onWillDestroy(() => {
// do some cleanup
});
}
执行顺序: 先子后父
不幸的是,组件可能会在运行时崩溃, 这是一个不幸的消息, 这就是Owl为啥要提供一种方式来处理这些错误
当我们需要拦截并正确响应某些子组件中发生的错误时,onError钩子非常有用。有关详细信息,请参阅有关错误处理的页面。
setup() {
onError(() => {
// do something
});
}
2.5 子组件
用子组件来定义父组件是很方便的,这叫做合成,实践中很有用. 在Owl中要做到这点只需要两点:
1 在模板中通过首字母大写的标签来引用子组件并且可以传递参数
2 在组件的静态属性Component中注册子组件
class Child extends Component {
static template = xml`<div>child component <t t-esc="props.value"/></div>`;
}
class Parent extends Component {
static template = xml`
<div>
<Child value="1"/>
<Child value="2"/>
</div>`;
static components = { Child };
}
2.6 动态子组件
这不常用,但是有时候我们需要一个动态的子组件名称,这种情况, t-component指令可以用来接收动态的值.这应该是一个表达式用来计算组件类,例如
class A extends Component {
static template = xml`<div>child a</div>`;
}
class B extends Component {
static template = xml`<span>child b</span>`;
}
class Parent extends Component {
static template = xml`<t t-component="myComponent"/>`;
state = useState({ child: "a" });
get myComponent() {
return this.state.child === "a" ? A : B;
}
}
2.7 status助手
有时候需要一种方法来查看当前的组件处于哪种状态,要做到这点,可以使用status助手
const { status } = owl;
// assume component is an instance of a Component
console.log(status(component));
// logs either:
// - 'new', if the component is new and has not been mounted yet
// - 'mounted', if the component is currently mounted
// - 'destroyed' if the component is currently destroyed