前端

Odoo patch 为什么不是“直接改原型就完了”:super、skeleton 与 unpatch 回滚机制讲透

很多人把 Odoo 前端的 `patch()` 理解成一个“官方版 monkey patch”。但 `core/utils/patch.js` 里真正精巧的地方,不是把方法盖上去,而是怎样保留原属性、重建 `super` 链、兼容 getter/setter,并在 unpatch 时按剩余扩展重新回放。它解决的是可维护补丁,而不是一次性篡改。

前端
进阶 开发者 2 分钟阅读
0 评论 0 点赞 0 收藏 5 阅读

前端开发里一提到 patch,很多人第一反应都是:

  • 临时覆盖一个方法;
  • 跑起来就行;
  • 以后升级再说。

如果只把 Odoo 的 patch() 当成这种“官方允许的 monkey patch”,会低估它很多。

addons/web/static/src/core/utils/patch.js,你会发现官方真正解决的是下面几个难题:

  1. 覆盖之后,super 还能不能正常工作;
  2. 同一个对象被多次 patch 时,回滚一层后剩下的扩展还能不能保持顺序;
  3. getter / setter 只改一半时,会不会把另一半搞丢;
  4. 类原型和普通对象在属性可枚举性上差异这么大,怎么尽量不破坏原语义。

所以 Odoo 的 patch 不是“一锤子把原方法改掉”,而是一种可叠加、可回放、可回滚的对象扩展机制

一、patch 的核心不是覆盖,而是先建立补丁描述表

源码先用一个 WeakMap

  • patchDescriptions

去保存每个被 patch 对象对应的描述信息,里面至少有三块:

  • originalProperties
  • skeleton
  • extensions

这三样东西决定了 Odoo 的 patch 和“直接 obj.method = fn”完全不是一个级别。

originalProperties:记住第一次 patch 前长什么样

originalProperties 只记录“第一次 patch 之前”的属性描述。

这意味着什么?

意味着之后不管叠多少层扩展,unpatch 的时候都能回到原始基线,而不是回到某次中间态。

skeleton:给 super 链搭骨架

skeleton 初始是:

  • Object.create(Object.getPrototypeOf(objToPatch))

然后每次 patch 前,如果对象上已经有旧属性,就把旧属性定义到 skeleton 上。

这是整套设计最有味道的地方。

因为它不是简单保存一个“旧函数引用”,而是在构造一条补丁可继承的中间原型链

extensions:记住所有仍然有效的补丁

每个 extension 都会放进 extensions 集合。这样在某个补丁被移除时,官方可以:

  1. 先恢复原始属性;
  2. 再把剩余 extension 按顺序重新 patch 一遍。

也就是说,Odoo 不是在“从当前态扣掉一层补丁”,而是在“从原始态重新重放有效补丁集”。

这才是多层 patch 可维护的关键。

二、为什么它要专门处理 class prototype

源码里有个 isClassPrototype(),专门判断当前 patch 的是不是类原型。

随后会做一件很细的事:

  • 如果 patch 的目标是 class prototype,就把新属性设成 enumerable = false

这一步很多人会忽略,但它特别工程化。

因为:

  • 普通对象字面量里的属性通常是 enumerable;
  • class prototype 上的方法默认不是 enumerable。

如果不处理,给类原型打补丁之后,枚举行为就会和原类不一致。短期也许没感觉,长期却可能在:

  • 序列化;
  • 遍历;
  • 调试;
  • 元编程兼容性;

这些场景里冒出很奇怪的问题。

官方显然不希望 patch 一下,就把对象的“类型气质”改坏。

三、super 为什么能在 patch 里继续工作

这是 patch.js 最值得讲透的一点。

最后那句:

  • description.skeleton = Object.setPrototypeOf(extension, description.skeleton)

决定了 extension 自己会被挂到当前 skeleton 上。

于是当 extension 里的方法使用 super.xxx() 时,它沿着的不是 JavaScript 原始类继承链,而是这条被 patch 系统重新组织出来的补丁骨架链

通俗点说:

  • 第一层 patch 的 super 指向原始实现;
  • 第二层 patch 的 super 指向第一层 patch;
  • 再往后继续类推。

这就让多层补丁不只是“后写覆盖前写”,而是能形成明确的调用接力。

所以在 Odoo 前端里,patch 最理想的用法不是把别人方法整段复制过来,而是:

  • 在必要处插逻辑;
  • super
  • 保留原有语义。

这也是为什么它比粗暴 monkey patch 更适合模块共存。

四、getter / setter 为什么要补全另一半

源码里有一段异或判断:

  • 如果新属性只定义了 get 或只定义了 set
  • 就去祖先描述符里把另一半补上

这其实是在补 JavaScript 属性描述符的一个坑。

因为 accessor property 往往是成对存在的。你只改其中一半时,如果直接 defineProperty,另一半可能就丢了。

Odoo 这里做的是:

  1. 顺着原型链找祖先 descriptor;
  2. 如果旧 descriptor 上另一半存在,就抄回来。

这保证 patch accessor 时更像“局部修改”,而不是“整扇门拆掉换半块门板”。

五、为什么 unpatch 不是简单恢复旧值,而是“先清空再重放”

patch() 返回一个 unpatch 函数。

很多人以为 unpatch 只会把当前这一层恢复掉。实际上官方做得更稳:

  1. 删除当前对象的 patch description;
  2. 先把所有属性恢复到第一次 patch 前的原始状态;
  3. extensions 里去掉当前 extension;
  4. 把剩余 extension 重新调用 patch() 再打一遍。

这套逻辑非常像“rebuild”,而不是“subtract”。

为什么这样更可靠?

因为多层 patch 下,后来的补丁已经可能建立在前一个补丁形成的 skeleton 之上。直接从当前态硬减一层,容易把链条拆歪。

从原始态重放有效补丁,虽然看起来笨一点,但语义最清楚,也最不容易留下幽灵状态。

六、这套设计背后的真正目标:让 patch 尽量像“受控扩展”,而不是“紧急入侵”

很多团队一说 patch,就默认它是最后手段。这没错,但 Odoo 的实现说明官方并不想让它成为“不可控的最后手段”。

相反,它希望 patch 至少满足几个条件:

  • 能叠加;
  • 能调用 super;
  • 能可靠撤销;
  • 尽量不破坏属性语义;
  • 多模块共存时不会一撤就全乱。

所以 Odoo patch 的真正定位,更接近:

在公开扩展点不够用时,提供一个相对可维护的底层切口。

它不是鼓励你到处乱 patch,而是在告诉你:如果不得不 patch,也别用最原始的方式。

七、实战里最常见的几个误区

误区 1:把 patch 当成“复制原方法后改几行”

这样最容易在升级时把上游修复一起覆盖掉。

更稳的姿势通常是:

  • 尽量局部插入;
  • 尽量调用 super
  • 只改你真正要改的分支。

误区 2:patch 类本身,而不是 patch prototype

源码注释已经明确提醒:

  • patch class 时,通常应该 patch prototype
  • 除非你要改的是静态属性/静态方法

这是 Odoo 前端 patch 最常见的基础错误之一。

误区 3:忘记 unpatch 语义会重放其他补丁

如果测试里多个补丁叠在同一对象上,unpatch 的结果不是“简单回到上一层”,而是“恢复原始态后重放剩余层”。理解错这一点,断言就很容易写偏。

误区 4:把 patch 当成 registry/service 的替代品

如果一个需求本来有公开注册点,就不该优先 patch。patch 适合的是:

  • 公开扩展面不够;
  • 你必须进入内部行为;
  • 而且你清楚升级成本。

八、结论

Odoo 的 patch() 真正厉害的地方,不是“官方也允许覆盖方法”,而是它把覆盖这件事做成了一种更受控的机制。

它通过:

  • originalProperties 记住原始基线;
  • skeleton 维持 super 链;
  • extensions 管理多层补丁集合;
  • accessor 补全逻辑保住 getter/setter 语义;
  • unpatch 重放机制维持可回滚的一致性。

所以更准确地说:

Odoo patch 不是简单改代码,而是在运行时重建一条可扩展、可撤销的方法继承链。

理解这一点后,你再决定“这事该用 registry、service、subclass 还是 patch”,判断会成熟很多。patch 依然要谨慎,但至少你会知道它到底在帮你兜什么底。

DISCUSSION

评论区

想参与讨论?先 登录 再发表评论。
还没有评论,你可以成为第一个留言的人。