MobX设计思想与实现

简介: 最近在搜集MobX相关资料,意外的找到MobX作者一篇深度解析MobX工作机制的blog,看过之后受益匪浅。本篇文章将结合MobX源码来整理并理解这篇blog的内容。这篇博客主要是想为读者解读MobX的实现机制,其中穿插介绍了一些作者的设计思路。

最近在搜集MobX相关资料,意外的找到MobX作者一篇深度解析MobX工作机制的blog,看过之后受益匪浅。本篇文章将结合MobX源码来整理并理解这篇blog的内容。这篇博客主要是想为读者解读MobX的实现机制,其中穿插介绍了一些作者的设计思路。

几个重要的概念

首先,作者先澄清了几个MobX中的重要概念

  • Observable State, 所有可以改变的值。
  • Computed Value(又称Derivation), 可以通过Observable State直接计算(pure function)得出的值。
  • Reaction, 与Computed Value类似也是基于Observable State 。但是不是返回一个结果,而是产生一个动作(side effects)
  • Action, 所有修改Observable State的动作

这几个概念的关系如下图:

image.png

这个图解释了,在MobX体系里各个角色的作用。

首先是发生一个Action修改State,接着State的更新会自动触发与其相关联的DerivationReaction

需要注意的是Derivation在这张图中的双重角色。在观察者模式视角下,它不仅是observable也是observer。或者说对于State来说,它是一个observer监视State的变化;但是对于Reaction来说,它可能还是一个observable,它的变化会引发Reaction发生。

一个简单的示例

接着作者给出了一个使用MobX例子,来说明这些概念。

classPerson {
@observablefirstName="Michel";
@observablelastName="Weststrate";
@observablenickName;
@computedgetfullName() {
returnthis.firstName+" "+this.lastName;
  }
}
constmichel=newPerson();
// Reaction: log the profile info whenever it changesautorun(() =>console.log(person.nickName?person.nickName : person.fullName));
// Example React component that observes stateconstprofileView=observer(props=> {
if (props.person.nickName)
return<div>{props.person.nickName}</div>elsereturn<div>{props.person.fullName}</div>});
// Action:setTimeout(() =>michel.nickName="mweststrate", 5000)
React.render(React.createElement(profileView, { person: michel }), document.body);


这个例子的逻辑很简单,通过判断Person对象是否存在nickName属性来决定展示在界面(profileView)上的内容(nickName还是fullName)。

它们的依赖关系大致如下图:

image.png

如果你仔细再看这张图中的fullName,大概你会更清楚为什么我之前说Derivation有着双重角色。实际上Derivation的特殊性也体现在实现里。

MobX实现里,充当observable角色的类都会有一个lowestObserverState标示当前的状态(Stale,UP_TO_DATE等)。(像之前文章里提到的observable体系里类例如ObservableValueObservableArray都有这样的属性。) 同样的observer也会有一个dependenciesState标示当前状态。 然而ComputedValue类却同时拥有这两个属性。

动态更新依赖

回到刚刚的示例中,在最后我们触发了一个Action

// Action:setTimeout(() =>michel.nickName="mweststrate", 5000)


Person对象添加nickName 属性。所以界面profileView也会发生相应的变化。此时的依赖关系已经发生了微妙的变化。

image.png

profileView已经不再依赖或者说监视fullName。如果这时,firstName或者fullName发生了变化,也不会对profileView有任何影响。

这就是MobX里的动态更新依赖。这样的设计,好处在于保证observer只依赖于它需要的依赖。永远也不会出现undersubscribe (forgetting to listen for updates leading to subtle staleness bugs)或者oversubscribe (continue subscribing to a value or store that is no longer used in a component)。

MobX能实现依赖动态更新,是因为它的依赖关系是由框架在运行时计算得到的。或者说,这个依赖关系并不是由用户在写代码时手动的去关联起来,而是由框架自己在运行时自动确定的。

比如在示例中,当执行profileView部分时:

constprofileView=observer(props=> {
if (props.person.nickName)
return<div>{props.person.nickName}</div>elsereturn<div>{props.person.fullName}</div>});


它首先判断是否存在nickName属性,如果不存在就返回fullName属性,在执行profileView部分时就用到了nickNamefullName两个observable,所以现在profileView就依赖于nickNamefullName

但是注意fullName可不是一个普通的observable,它是一个computed value。同样执行fullNamegetter部分:

@computedgetfullName() {
returnthis.firstName+" "+this.lastName;
  }


所以fullName又依赖于firstNamelastName

整个过程如下图:

image.png

这样我们的依赖关系就形成了。


这里体现出了MobX的设计思想:

Reacting to state changes is always better than acting on state changes.

这句话初看时似懂非懂,reacting to changeacting on change似乎没什么差别。实际上这里一个细微的用词决定change发生后动作的主动权归属。acting的主动权在用户,动作是否发生,发生哪些动作都由用户来决定(依赖关系由用户手动订阅);而reacting的主动权在框架(依赖关系由框架自动生成)。


同样当Action被触发,profileView重新计算自己的依赖关系,这次nickName存在,不会用到fullName,所以profileView依赖更新,只依赖于nickName.

这一部分的最后提一下这个依赖更新的算法。本质上是一个两个数组无重复合并的算法,MobX的实现把这个算法的时间复杂度降到了o(n)。具体可以参考这篇文章


从懒加载到MobX响应机制

image.png

让我们再回到这张图,不知道你是否注意到此时,fullName不再被监视时,它的状态已经被修改为lazy。 这代表着,如果现在修改firstName后者lastNamefullName不会立即执行。这就是MobX的性能优化之一——懒加载机制。

想要深入理解懒加载机制是如何实现的,需要我们首先明白MobX是如何响应变化的。当一个action产生时MobX内部发生了什么。


仍然拿之前的例子来说明,假设这样一个场景,依赖关系仍然是完整时(profileView仍然依赖fullName),此时firstName被修改。

  1. action发生,firstName值被修改,firstName状态变更为stale

image.png


2. firstName通知它的observerfullNamefullName状态变更为possible change

image.png

3. fullName通知它的observerprofileViewprofileView状态变更为possible change

image.png

4. MobX监控到所有的状态变更和修改都已经完成

5. MobX通知fullName开始执行, fullName通过firstName的新值重新计算自己的值,fullName修改firstName和自己的状态为new

image.png

6. 这里出现了一个分支,通过fullName的新值和旧值做比较

 6.1  如果fullName新值和旧值相同,即fullName其实没有发生变化,则直接修改profileView状态为new,不需要执行

 6.2  如果fullName确实发生了变化,则profileView重新执行,并且修改自己的状态为new

至此一次完整的更新结束。

如果把上面的过程做一个小结,我大致可以把它分为两个阶段:

  • 冒泡阶段,即被修改的observable去通知它的observer修改状态,这个过程是级联的(或者说是递归发生的) 。在冒泡阶段除了修改状态,实际上并没有发生其他事情。
  • 执行阶段,当冒泡阶段结束,所有的状态都已经被修改完成后,开始执行需要的操作。

为什么一定要分成两个阶段而不是修改状态使立即执行呢?

这实际上也是MobX性能优化的一个设计。它的思路实际就是批处理。因为上面的例子只是一个非常简单的更新。当有多个action一起发生的时候,所以如果每次都立即执行,可能每个action都导致同一个computed value会需要重复执行很多次,求出很多无用的中间结果。 而等到所有状态更新和action都结束后,再执行求值,就只需要执行一次求出最终的结果。

到这里,我比较在意的是MobX的实现是怎么区分这两个阶段的。它怎么知道什么时候冒泡阶段完全结束了需要开始执行阶段论。这牵扯到MobX的另一个概念—— trasnaction 


transaction在数据库中经常出现的概念,在数据库中它定义了一个原子性的操作,并且用它来管理并发访问。

但是MobX里的transaction不是干这个的。它被引入的目的就是为了标注出一次完整的更新冒泡阶段的开始和结束。在一个tranaction内是不会执行reaction或者computed value

/** * During a transaction no views are updated until the end of the transaction. * The transaction will be run synchronously nonetheless. * * @param action a function that updates some reactive state * @returns any value that was returned by the 'action' parameter. */exportfunctiontransaction<T>(action: () =>T, thisArg=undefined): T {
startBatch()
try {
returnaction.apply(thisArg)
    } finally {
endBatch()
    }
}


也就是说,在一个当endBatch()被调用后,MobX就知道冒泡阶段结束了,可以开始执行阶段了。

这里还要多说一句,transaction是一个底层的API,它在上层的封装就是@action。这也是为什么MobX的最佳实践里要求你把所有的state修改都加上action,实际就是把多个修改放到一个transaction里,batch update。


还有一点是执行阶段时的执行顺序。

这里的顺序让我联想到了构建一个最小堆(或者最大堆)时的shift down的过程,在建立一个堆时需要保证它的子树满足最小堆的规则。所以会从n个结点的完全二叉树最后一个分支节点floor( (n-2) / 2 )开始执行shif down。

image.png

而执行阶段也与之类似,它需要保证从整个依赖图(注意不是树)中最底层的一个父节点开始执行(实际就是按照拓扑排序的顺序)。因为执行的规则是保证当前节点所有子节点的值是最新的。

image.png


这个顺序是如何确定的呢?在冒泡阶段,MobX会把经过的Reaction放到一个待执行队列里。执行阶段就直接从队列里取出执行。注意我这里说的是Reaction而不包括Computed Value。所以fullName是不会加入队列的。

但是我刚刚一直在说执行阶段会先执行fullName。这其实并不矛盾。在执行阶段开始,MobX拿到profileView后,检查它的所有依赖,结果找到了fullName是处于possible change状态。它当然会先确定fullName的值,然后才能确定自己的值。

还记得我之前提到的懒加载吗?它就是这样实现的。因为Comupued Value不会加入队列里而是通过Reaction检查依赖获取的。所以如果Computed Value 没有任何observer依赖,就不会有Reaction能到达它。它就当然不会被执行了。只有当以后某个机会,它又与某个Reaction关联起来后,那时才会被执行。

所以懒加载的本质是只有冒泡阶段没有执行阶段。

小结

本篇文章介绍了MobX里的依赖动态更新机制以及变更响应机制的设计与实现。

目录
相关文章
|
数据可视化 数据挖掘 Java
提升代码质量与效率的利器——SonarQube静态代码分析工具从数据到洞察:探索Python数据分析与科学计算库
在现代软件开发中,保证代码质量是至关重要的。本文将介绍SonarQube静态代码分析工具的概念及其实践应用。通过使用SonarQube,开发团队可以及时发现和修复代码中的问题,提高代码质量,从而加速开发过程并减少后期维护成本。 在当今信息爆炸的时代,数据分析和科学计算成为了决策和创新的核心。本文将介绍Python中强大的数据分析与科学计算库,包括NumPy、Pandas和Matplotlib,帮助读者快速掌握这些工具的基本用法和应用场景。无论是数据处理、可视化还是统计分析,Python提供了丰富的功能和灵活性,使得数据分析变得更加简便高效。
|
监控 前端开发 JavaScript
AST 代码扫描实战:如何保障代码质量
2020 年 618 大促已经过去,作为淘系每年重要的大促活动,淘系前端在其中扮演着什么样的角色,如何保证大促的平稳进行?又在其中应用了哪些新技术?淘系前端团队特此推出「618 系列|淘系前端技术分享」,为大家介绍 618 中的前端身影。 本篇来自于频道与D2C智能团队的菉竹,为大家介绍本次 618 大促中是如何用代码扫描做资损防控的。
3974 0
AST 代码扫描实战:如何保障代码质量
|
10月前
|
人工智能 监控 安全
MCP与企业数据集成:ERP、CRM、数据仓库的统一接入
作为一名深耕企业级系统集成领域多年的技术博主"摘星",我深刻认识到现代企业面临的数据孤岛问题日益严重。随着企业数字化转型的深入推进,各类业务系统如ERP(Enterprise Resource Planning,企业资源规划)、CRM(Customer Relationship Management,客户关系管理)、数据仓库等系统的数据互联互通需求愈发迫切。传统的点对点集成方式不仅开发成本高昂,维护复杂度也呈指数级增长,更重要的是难以满足实时性和一致性要求。Anthropic推出的MCP(Model Context Protocol,模型上下文协议)为这一痛点提供了革命性的解决方案。MCP通过
604 0
|
数据采集 传感器 人工智能
AgiBot World:智元机器人开源百万真机数据集,数据集涵盖了日常生活所需的绝大多数动作
AgiBot World 是智元机器人开源的百万真机数据集,旨在推动具身智能的发展,覆盖家居、餐饮、工业等五大核心场景。
1462 9
AgiBot World:智元机器人开源百万真机数据集,数据集涵盖了日常生活所需的绝大多数动作
|
人工智能 自然语言处理 算法
AI时代的企业内训全景图:从案例到实战
作为一名扎根在HR培训领域多年的“老兵”,我越来越清晰地感受到,企业内训的本质其实是为企业持续“造血”。无论是基础岗的新人培训、技能岗的操作规范培训,还是面向技术中坚力量的高阶技术研讨,抑或是管理层的战略思维提升课,内训的价值都是在帮助企业内部提升能力水平,进而提高组织生产力,减少对外部资源的依赖。更为重要的是,在当前AI、大模型、Embodied Intelligence等新兴技术快速迭代的背景下,企业必须不断为人才升级赋能,才能在市场竞争中保持领先。
1961 13
|
资源调度 前端开发 API
React Suspense与Concurrent Mode:异步渲染的未来
React的Suspense与Concurrent Mode是16.8版后引入的功能,旨在改善用户体验与性能。Suspense组件作为异步边界,允许子组件在数据加载完成前显示占位符,结合React.lazy实现懒加载,优化资源调度。Concurrent Mode则通过并发渲染与智能调度提升应用响应性,支持时间分片和优先级调度,确保即使处理复杂任务时UI仍流畅。二者结合使用,能显著提高应用效率与交互体验,尤其适用于数据驱动的应用场景。
327 20
|
前端开发 Java
如何实现 Java SpringBoot 自动验证入参数据的有效性
如何实现 Java SpringBoot 自动验证入参数据的有效性
247 0
|
监控 JavaScript 前端开发
【TypeScript技术专栏】TypeScript的单元测试与集成测试
【4月更文挑战第30天】本文讨论了在TypeScript项目中实施单元测试和集成测试的重要性。单元测试专注于验证单个函数、类或模块的行为,而集成测试关注不同组件的协作。选用合适的测试框架(如Jest、Mocha),配置测试环境,编写测试用例,并利用模拟和存根进行隔离是关键。集成测试则涉及组件间的交互,需定义测试范围,设置测试数据并解决可能出现的集成问题。将这些测试整合到CI/CD流程中,能确保代码质量和快速响应变化。
541 0
|
SQL 前端开发 JavaScript
基于Springboot+MyBatisPlus+Vue前后端分离大学生毕业论文答辩系统
基于Springboot+MyBatisPlus+Vue前后端分离大学生毕业论文答辩系统
1352 0
基于Springboot+MyBatisPlus+Vue前后端分离大学生毕业论文答辩系统
问题解决:CMake Error at /home/sjh/anaconda3/lib/cmake/Boost-1.73.0/BoostConfig.cmake:141的问题
问题解决:CMake Error at /home/sjh/anaconda3/lib/cmake/Boost-1.73.0/BoostConfig.cmake:141的问题
941 0