写在前头
有些看官,对之前的文章 《Onchange方法的使用扩展》还不是很明白。那么为此,特地抽空花时间整理了一下。带各位看官一起深入底层,一探究竟。
写本文之前,又怕各位看官的Python功底不深,又先写了几篇文章。以下传送门:
为了各位看官能够理解,花了不少时间。
内容较多,下面正式开始
onchange()加载过程
首先,我们先抛出来几个问题:
1. 我们写的@api.onchange()方法,为什么就能被执行?
2. @api.onchange()参数中指定了字段A,为什么字段A改变时候就会被触发?
3. 不用@api.onchange()方法,自己写一个myonchange()行不行?
接下来,我们就带着问题,一起来寻找答案。
onchange的定义
首先,我们先看一下,我们正常情况下是如何定义一个onchange的触发器的。这里以销售订单为例:sale.py
1 |
|
这里当销售员改变时候,销售团队随着销售员做出相应的改变。
也就是在后台的模型类定义中,定义一个函数并且以装饰器api.onchange
进行装饰且传入触发的字段
作为参数。
此时,我们查看api.onchange
是什么?(在之前文章已经带大家看过)
打开api.py
onchange定义在157行:
1 | def onchange(*args): |
我们看到,api.onchange
就是一个方法,作用是返回了一个attrsetter
函数并传入了相关参数并执行。
再继续看一下attrsetter
这个函数的定义。110行:
1 | def attrsetter(attr, value): |
不难发现,作用是返回了一个函数,并给函数设置了相关属性以及值。
结合onchange
的定义可以看出,api.onchange
就是给函数设置了一个_onchange
的属性并且值为传入的参数
(如果不理解装饰器的同学,请先看完开头给大家提供的文章。我是特意写的。)
我就把api.onchange
换一个写法,把它还原:
1 | def onchange(*args): |
这就是api.onchange
的真实面目。很简单了吧?那么,有看官就问了:我知道是这样写的,可是我写上了这个装饰器,它是怎么被调用的,这个方法是怎么被执行的?不急,我们慢慢来。
onchange的调用过程
其实,在onchange方法,都是通过前端页面触发的。我们在给字段定义onchange
时候,前端页面改变时候,会执行一个rpc方法。这部分代码在js中。我就粗略带大家看一眼在何处定义的。(不明白rpc的,自行百度)
路径odoo/addons/web/static/src/js/views/basic/basic_model.js
1 | _performOnChange: function (record, fields, viewType) { |
方法_performOnChange
中会调用执行后台的onchange
方法。而_performOnChange
方法被_applyChange
方法和_makeDefaultRecord
方法(即后台对应的default_get
方法)调用。js代码就不带大家看了,不是本篇重点。_applyChange
方法在1423行,_makeDefaultRecord
方法在3903行。
以上就是说,所有模型字段的onchange触发器
都是调用了一个方法onchange
,那么是如何区分每个模型,以及模型的字段呢?模型里可以有很多个api.onchange
装饰的方法。
接下来,转到后台onchange
定义。
路径odoo/odoo/models.py
, 5744行。
1 | def onchange(self, values, field_name, field_onchange): |
我们通过注释可以看到,这个确确实实,是所有的onchange事件的入口函数。可是,我们通过装饰器写的onchange函数,到底是怎么执行的?我们仔细的阅读源码!(这是一个好习惯)
注意,有一段代码,引起我们注意。
1 | # process names in order |
猜猜看?循环todo? 里面的注释, 处理字段的onchange方法?好,我们就看看,这个_onchange_eval
这个方法。是做什么的?这个方法就位于onchange
的上方。5716行:
1 | def _onchange_eval(self, field_name, onchange, result): |
通过注释,可以发现,确实是在处理字段的onchange方法。主要代码就是下面这个:
1 | if onchange in ("1", "true"): |
我们看到,循环self._onchange_methods.get(field_name, ())
执行每一个方法。那么我们大胆推测,我们写的onchange方法,一定就存在self._onchange_methods
之中(大胆推测、谨慎证明是提高的过程)。我们再继续看self._onchange_methods
到底是个什么玩意?通过搜索,我们发现_onchange_methods
是个方法,只不过被@property
装饰为一个属性了。代码619行:
1 |
|
通过注释可以了解到这是一个返回字段
和onchange方法
映射关系的方法。我们看代码中is_onchange
这个方法,是不是很熟悉? 你品!你细细品!
1 | def is_onchange(func): |
不就是返回了一个可以被调用的并且含有_onchange
属性的方法吗?不就是之前我们通过api.onchange
装饰器装饰的方法吗?诶诶诶,到这里,我们就对上号了。呦西,搜嘎。原来在这里被用到了。再继续往下看。methods = defaultdict(list)
, 这是啥啊?看官,忘记我开头给你提供的传送门了么?请传送过去! 先告诉你,可以先把它当做一个字典来看。继续看。 重点来了哟!嘿嘿嘿嘿!
1 | for attr, func in getmembers(cls, is_onchange): |
报告,我不懂,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 | for model in models: |
接下来,就不带大家去分析了,已经涉及到Odoo的加载机制了。之前写的加载过程是个大概,看官们要是有兴趣自己去研究研究。
至此,我们就完成了整个过程的探索。
1、api.onchange装饰器是做了什么?
2、为什么会执行onchange方法?
3、api.onchange装饰器装饰的方法在哪里执行的?
4、api.onchange装饰器装饰的方法为什么会被加载?
这其中涉及到了很多知识,前端js(不是本篇重点)、Python的装饰器、内置模块、Odoo加载机制等等。希望大家能够仔细研读源代码,我们所有的问题,想要的答案,都在源码中。
自己的onchange装饰器
1 | def myonchange(*args): |
原理整明白了,就可以随处使用了。这是今天的分享!
以上。
– end –
看官,别忘了打赏哟!感谢阅读,不当之处,评论指正!