即使一个人,也要活得像军队一样!

Odoo13-- 以onchange()为例,深入源码,一探究竟

写在前头

有些看官,对之前的文章 《Onchange方法的使用扩展》还不是很明白。那么为此,特地抽空花时间整理了一下。带各位看官一起深入底层,一探究竟。

写本文之前,又怕各位看官的Python功底不深,又先写了几篇文章。以下传送门:

为了各位看官能够理解,花了不少时间。

内容较多,下面正式开始


onchange()加载过程

首先,我们先抛出来几个问题:
1. 我们写的@api.onchange()方法,为什么就能被执行?
2. @api.onchange()参数中指定了字段A,为什么字段A改变时候就会被触发?
3. 不用@api.onchange()方法,自己写一个myonchange()行不行?
接下来,我们就带着问题,一起来寻找答案。

onchange的定义

首先,我们先看一下,我们正常情况下是如何定义一个onchange的触发器的。这里以销售订单为例:
sale.py

1
2
3
4
@api.onchange('user_id')
def onchange_user_id(self):
if self.user_id and self.user_id.sale_team_id:
self.team_id = self.user_id.sale_team_id

这里当销售员改变时候,销售团队随着销售员做出相应的改变。
也就是在后台的模型类定义中,定义一个函数并且以装饰器api.onchange进行装饰且传入触发的字段作为参数。
此时,我们查看api.onchange是什么?(在之前文章已经带大家看过)
打开api.py
onchange定义在157行:

1
2
3
4
5
6
def onchange(*args):
"""
此处注释省略.......
返回一个装饰器装饰的方法
"""
return attrsetter('_onchange', args)

我们看到,api.onchange就是一个方法,作用是返回了一个attrsetter函数并传入了相关参数并执行。
再继续看一下attrsetter这个函数的定义。110行:

1
2
3
def attrsetter(attr, value):
""" Return a function that sets ``attr`` on its argument and returns it. """
return lambda method: setattr(method, attr, value) or method

不难发现,作用是返回了一个函数,并给函数设置了相关属性以及值。
结合onchange的定义可以看出,api.onchange就是给函数设置了一个_onchange的属性并且值为传入的参数
(如果不理解装饰器的同学,请先看完开头给大家提供的文章。我是特意写的。)
我就把api.onchange换一个写法,把它还原:

1
2
3
4
5
6
def onchange(*args):
def attrsetter(attr, value):
def wrapfunc(method):
return setattr(method, attr, value) or method
return wrapfunc
return attrsetter(attr='_onchange', value=args)

这就是api.onchange的真实面目。很简单了吧?那么,有看官就问了:我知道是这样写的,可是我写上了这个装饰器,它是怎么被调用的,这个方法是怎么被执行的?不急,我们慢慢来。

onchange的调用过程

其实,在onchange方法,都是通过前端页面触发的。我们在给字段定义onchange时候,前端页面改变时候,会执行一个rpc方法。这部分代码在js中。我就粗略带大家看一眼在何处定义的。(不明白rpc的,自行百度)
路径odoo/addons/web/static/src/js/views/basic/basic_model.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
_performOnChange: function (record, fields, viewType) {
...
...

return self._rpc({
model: record.model,
method: 'onchange',
args: [idList, currentData, fields, onchangeSpec],
context: context,
})
.then(function (result) {
...
});
},

方法_performOnChange中会调用执行后台的onchange方法。而_performOnChange方法被_applyChange方法和_makeDefaultRecord方法(即后台对应的default_get方法)调用。js代码就不带大家看了,不是本篇重点。_applyChange方法在1423行,_makeDefaultRecord方法在3903行。

以上就是说,所有模型字段的onchange触发器都是调用了一个方法onchange,那么是如何区分每个模型,以及模型的字段呢?模型里可以有很多个api.onchange装饰的方法。
接下来,转到后台onchange定义。
路径odoo/odoo/models.py, 5744行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
def onchange(self, values, field_name, field_onchange):
...
...
...
todo = list(names or nametree)


# process names in order
while todo:
# apply field-specific onchange methods
for name in todo:
if field_onchange.get(name):
record._onchange_eval(name, field_onchange[name], result)
done.add(name)

# determine which fields to process for the next pass
todo = [
name
for name in nametree
if name not in done and snapshot0.has_changed(name)
]

if not env.context.get('recursive_onchanges', True):
todo = []

# make the snapshot with the final values of record
snapshot1 = Snapshot(record, nametree)

# determine values that have changed by comparing snapshots
self.invalidate_cache()
result['value'] = snapshot1.diff(snapshot0)

# format warnings
warnings = result.pop('warnings')
if len(warnings) == 1:
title, message, type = warnings.pop()
if not type:
type = 'dialog'
result['warning'] = dict(title=title, message=message, type=type)
elif len(warnings) > 1:
# concatenate warning titles and messages
title = _("Warnings")
message = '\n\n'.join([warn_title + '\n\n' + warn_message for warn_title, warn_message, warn_type in warnings])
result['warning'] = dict(title=title, message=message, type='dialog')

return result

我们通过注释可以看到,这个确确实实,是所有的onchange事件的入口函数。可是,我们通过装饰器写的onchange函数,到底是怎么执行的?我们仔细的阅读源码!(这是一个好习惯)
注意,有一段代码,引起我们注意。

1
2
3
4
5
6
7
# process names in order
while todo:
# apply field-specific onchange methods
for name in todo:
if field_onchange.get(name):
record._onchange_eval(name, field_onchange[name], result)
done.add(name)

猜猜看?循环todo? 里面的注释, 处理字段的onchange方法?好,我们就看看,这个_onchange_eval这个方法。是做什么的?这个方法就位于onchange的上方。5716行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def _onchange_eval(self, field_name, onchange, result):
""" Apply onchange method(s) for field ``field_name`` with spec ``onchange``
on record ``self``. Value assignments are applied on ``self``, while
domain and warning messages are put in dictionary ``result``.
"""
onchange = onchange.strip()

def process(res):
if not res:
return
if res.get('value'):
res['value'].pop('id', None)
self.update({key: val for key, val in res['value'].items() if key in self._fields})
if res.get('domain'):
result.setdefault('domain', {}).update(res['domain'])
if res.get('warning'):
result['warnings'].add((
res['warning'].get('title') or _("Warning"),
res['warning'].get('message') or "",
res['warning'].get('type') or "",
))

if onchange in ("1", "true"):
for method in self._onchange_methods.get(field_name, ()):
method_res = method(self)
process(method_res)
return

通过注释,可以发现,确实是在处理字段的onchange方法。主要代码就是下面这个:

1
2
3
4
5
if onchange in ("1", "true"):
for method in self._onchange_methods.get(field_name, ()):
method_res = method(self)
process(method_res)
return

我们看到,循环self._onchange_methods.get(field_name, ())执行每一个方法。那么我们大胆推测,我们写的onchange方法,一定就存在self._onchange_methods之中(大胆推测、谨慎证明是提高的过程)。我们再继续看self._onchange_methods到底是个什么玩意?通过搜索,我们发现_onchange_methods是个方法,只不过被@property装饰为一个属性了。代码619行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@property
def _onchange_methods(self):
""" Return a dictionary mapping field names to onchange methods. """
def is_onchange(func):
return callable(func) and hasattr(func, '_onchange')

# collect onchange methods on the model's class
cls = type(self)
methods = defaultdict(list)
for attr, func in getmembers(cls, is_onchange):
for name in func._onchange:
if name not in cls._fields:
_logger.warning("@onchange%r parameters must be field names", func._onchange)
methods[name].append(func)

# add onchange methods to implement "change_default" on fields
def onchange_default(field, self):
value = field.convert_to_write(self[field.name], self)
condition = "%s=%s" % (field.name, value)
defaults = self.env['ir.default'].get_model_defaults(self._name, condition)
self.update(defaults)

for name, field in cls._fields.items():
if field.change_default:
methods[name].append(functools.partial(onchange_default, field))

# optimization: memoize result on cls, it will not be recomputed
cls._onchange_methods = methods
return methods

通过注释可以了解到这是一个返回字段onchange方法映射关系的方法。我们看代码中is_onchange这个方法,是不是很熟悉? 你品!你细细品!

1
2
def is_onchange(func):
return callable(func) and hasattr(func, '_onchange')

不就是返回了一个可以被调用的并且含有_onchange属性的方法吗?不就是之前我们通过api.onchange装饰器装饰的方法吗?诶诶诶,到这里,我们就对上号了。呦西,搜嘎。原来在这里被用到了。再继续往下看。
methods = defaultdict(list), 这是啥啊?看官,忘记我开头给你提供的传送门了么?请传送过去! 先告诉你,可以先把它当做一个字典来看。继续看。 重点来了哟!嘿嘿嘿嘿!

1
2
3
4
5
for attr, func in getmembers(cls, is_onchange):
for name in func._onchange:
if name not in cls._fields:
_logger.warning("@onchange%r parameters must be field names", func._onchange)
methods[name].append(func)

报告,我不懂,getmembers(cls, is_onchange)这是个啥玩意?这个,你也到开头找到传送门,自己传过去。先告诉你,此处是获取对象中的所有含有_onchange属性的方法。 哈! 原来,我们定义的方法,都在这里获取到了。
接下来,通过methods[name].append(func)添加到了methods字典中。 继续看。cls._onchange_methods = methods将方法的映射关系存到类属性中,紧接着返回了这些方法。

至此,我们已经知道了一个大致的逻辑:
一、所有的onchange触发器,最开始都是执行了onchange方法,而并非是我们写的方法。
二、在onchange方法中,通过执行_onchange_eval函数,再获取到字段对应的api.onchange装饰的方法。再执行。
如此,通过一个简单的映射关系,就区分了不同字段对应的触发器函数。

那么,还有一个疑问:我通过api.onchange装饰的函数,到底在哪里被加载到了模型对象里呢?
不急,我们继续分析。(会涉及到Odoo的一些启动过程,读者可看我之前写过的文章。Odoo12: 启动运行的过程概览
我们通过搜索代码发现_onchange_methods_init_constraints_onchanges方法中被调用了。
看到方法名就笑了,初始化constraints和onchanges。不就是我们想要的答案吗?(其实到这里,你也应该发现constraints和onchange是一样的,你就假装不知道,自己研究一遍。)
_init_constraints_onchanges方法又是被谁调用了呢?答案是_setup_complete。而_setup_complete是在odoo/odoo/modules/registry.py中调用的。289行:

1
2
for model in models:
model._setup_complete()

接下来,就不带大家去分析了,已经涉及到Odoo的加载机制了。之前写的加载过程是个大概,看官们要是有兴趣自己去研究研究。
至此,我们就完成了整个过程的探索。
1、api.onchange装饰器是做了什么?
2、为什么会执行onchange方法?
3、api.onchange装饰器装饰的方法在哪里执行的?
4、api.onchange装饰器装饰的方法为什么会被加载?
这其中涉及到了很多知识,前端js(不是本篇重点)、Python的装饰器、内置模块、Odoo加载机制等等。希望大家能够仔细研读源代码,我们所有的问题,想要的答案,都在源码中。

自己的onchange装饰器

1
2
3
4
def myonchange(*args):
def attrsetter(method, attr='_onchange', args=args):
return setattr(method, attr, args) or method
return attrsetter

原理整明白了,就可以随处使用了。这是今天的分享!

以上。
– end –

看官,别忘了打赏哟!感谢阅读,不当之处,评论指正!

-------------本文结束感谢您的阅读-------------