数据响应式实现的发展历程
现代JS框架的重要特征就是MVVM模式。MVVM是一种架构模式,它将应用程序分为三个部分:模型、视图和视图模型。其中,视图模型是连接模型和视图的关键组件,它负责将模型的数据转换为视图可以使用的格式,并将视图的用户输入转换为模型可以使用的格式。
MVVM和数据响应式是密切相关的概念。数据响应式是指当数据发生变化时,系统能够自动更新相关的界面元素。在MVVM中,视图模型通常会使用数据绑定技术来实现数据响应式。例如,可以使用绑定表达式将视图模型的属性与视图中的元素绑定在一起,当属性值发生变化时,相应的元素就会自动更新。
实现数据响应式,在JS发展史上有过几种实现方式,值得我们回顾和探讨。

AngularJS的脏检查
AngularJS是以HTML为模板的,它的数据绑定采用HTML标记的形式,并且数据可以绑定到任何的JS对象。它的数据响应式是通过脏检查来实现的。
1 | <div ng-controller="firstController"> |
1 | angular.module('app', []).controller('firstController', function($scope) { |
启动时,它会创建一个$rootScope对象,该对象包含应用程序的所有作用域。每个作用域都包含一个$$watchers数组,该数组包含要监视的模型。
在AngularJS中,当事件触发时(例如用户输入),AngularJS会遍历所有作用域中的$$watchers数组,并比较当前模型与先前模型的值。如果值发生了变化,AngularJS会调用相应的回调函数来更新视图。
AngularJS中的脏检查是通过$apply()方法来实现的。代码修改绑定值后,必须调用$apply()方法来通知AngularJS进行脏检查。$apply()方法会遍历所有作用域,并检查是否有任何模型发生了变化。如果有变化,AngularJS会更新相应的视图。
这种方法的好处是任何JavaScript对象都可以用作模板中的数据绑定源,并且更新可以正常工作。缺点是每次更新都必须执行大量脏检查,在大型应用中,数据绑定对象会非常多,性能也会很差。
AngularJS是我学习JS接触的第一个框架,当时还写了一些学习笔记,在我的51cto旧博客里,现在重新看了下,真的是相当难用的框架,但在当时却不这么觉得。
React的setState
React引入了setState()。这使得React知道何时应该对vDOM进行脏检查。这样做的好处是,与AngularJS不同,AngularJS对每个异步任务运行脏检查,React仅在开发人员告诉它时才执行。因此,尽管React vDOM脏检查在计算上比AngularJS更昂贵,但它运行的频率会更低。
另外,React引入了从父级到子级的严格数据流。这是迈向框架级别状态管理的第一步,而AngularJS没有。
1 | function Counter() { |
AngularJS和React是粗粒度的响应式,这意味着数据的变化会触发大量的状态变化,如果应用中有频繁变化的属性,比方动画,会有严重的性能问题。因此需要细粒度的响应式,只将有变化的数据反映到UI中。

Knockout的观察者模式和计算属性
Knockout的数据响应式是通过使用观察者模式来实现的。在Knockout中,每个绑定属性都有一个对应的观察者对象,该对象会监听绑定属性的变化,并在变化发生时更新视图。
当ViewModel对象中的属性被修改时,Knockout会通知相应的观察者对象。观察者对象会比较新值和旧值,如果值发生了变化,它会将新值反映到绑定的元素上。这样,视图就能够自动更新。
1 | var ViewModel = function() { |
在上面的代码中,使用ko.observable()方法来定义name属性。当使用self.name()方法设置name属性的值时,Knockout会通知name属性对应的观察者对象,并将新值反映到绑定的元素上。
1 | <button data-bind="click: setName">Set Name</button> |
click
是一个绑定属性,它指定当按钮被点击时调用setName
方法。
Knockout还创新性地引入了计算属性的概念,写法如下
1 | var ViewModel = function(first, last) { |
Svelte的原生JS脏检查
Svelte没有采用vDOM,而是将Svelte编译为原生JS代码,将一切视图更新转化为直接的DOM操作。
当有变量赋值时,比如:
1 | let firsName = ''; |
代码将被Svelte编译为用$$invalidate
函数包裹:
1 | function handleClick () { |
- 1、当数据发生变化时,就会调用
$$invalidate
函数 - 2、
$$invalidate
函数内会对当前绑定数据进行脏检查,脏检查判定为dirty的组件放入脏组件数组中,并预约下一个microTask的周期更新 - 3、在microTask中遍历脏组件数组,调用每个组件的
update
方法,也就是更新对应组件的DOM - 4、重置脏检查状态
相比AngularJS,Svelte能做到细粒度的脏检查,同时又不需要React的vDOM来增加复杂度,用纯粹的DOM操作,在Runtime无需依赖框架,是一种原生的响应式实现。
Vue的Proxy/defineProperty
Vue在编译过程中收集依赖,基于Proxy(3.x) ,defineProperty(2.x)的getter
/setter
实现在数据变更时通知watcher。
细粒度响应式的存储状态方式 - Signal
Signal和State之间的主要区别在于Signal
返回一个getter
和一个setter
,而State
返回一个值和一个setter
。
1 | useState() => value + setter |
通过返回getter
,可以将状态引用的传递与状态值的读取分开。
1 | function App() { |
为什么传递getter
比直接传递value
有优势呢?因为为了具有响应式,就需要进行依赖收集。
如果传递value
,无法提供有关实际使用该值的具体位置信息,当状态变化时,数据传递链路下的组件将会大规模更新,也就是粗粒度的响应式。
如果传递的是getter
,因为有getCount
和getCount()
的区别,就知道自顶而下哪些组件在数据变化时需要更新,也就做到了细粒度的响应式。
使用Signal的框架通常能获得不错的运行时性能,不需要显式指明依赖和额外的性能优化API,所以对开发者更友好。
React团队承认Signal的优点,但改变现有的设计理念(快照理念:UI反映状态在某一刻的快照)很难,所以不会改造为Signal模式。相信未来的响应式框架,必定是Signal模式的。