【JavaScript】Object.observe()带来的数据绑定变革

enter image description here

Object.observe()带来的数据绑定变革

引言

一场变革即将到来。一项Javascript中的新特性将会改变你对于数据绑定的全部认识。它也将改变你所使用的MVC库观察模型中发生的修改以及更新的实现方式。你会看到,那些全部在乎属性观察的应用性能将会获得巨大的提高。javascript

咱们很高兴的看到,Object.observe()已经正式加入到了Chrome 36 beta版本中。php

Object.observe()是将来ECMAScript标准之一,它是一个能够异步观察Javascript中对象变化的方法,而无需你去使用一个其余的JS库。它容许一个观察者接收一个按照时间排序的变化记录序列,这个序列描述的是一列被观察的对象所发生的变化。html

// 假设咱们有一个模型来存储数据 var model = {};  // 而后咱们对他进行观察 Object.observe(model, function(changes){  // 这个异步毁掉函数将会运行 changes.forEach(function(change) {  // 让咱们获知变化 console.log(change.type, change.name, change.oldValue); }); }); 

当被观察的对象发生任何变化时,回调函数将会汇报这些变化:html5

经过使用Object.observe(),你能够不须要使用任何框架就能实现双向数据绑定。java

但这并不意味着你就不该该使用一个框架。对于一些有着复杂业务逻辑的项目,通过精心设计的框架的重要性不言而喻,你应该继续使用它们。这些框架减轻了开发新手的工做量,并且只须要编写不多的代码就可以维护和实现一些模式并达到咱们想要的目的。若是你不须要一个框架,你可使用一个体积更小,针对性更强的库,好比Polymer(它已经开始使用Object.observe()了)。web

即使你已经重度依赖于一个MV*框架,Object.observe()任然能为你的应用带来一些性嫩各方面的提高,它可以更快更简单的实现一些功能并维持一样的API。例如,在Angular的一个Benchmark测试中,对于一个Model中发生的变化,脏值检查对每次更新会花费40ms,而Object.observe()只会花费1-2ms(至关于20-40倍的性能提高)。算法

不须要冗长代码来实现的数据双向绑定还意味着你不须要经过轮询来发现变化,这将带来更长的电池使用时间!数组

若是你已经对Object.observe()有了一些了解,能够直接跳过简介这一节,或者接着阅读了解更多关于它可以解决的问题。浏览器

咱们想要观察什么

当咱们在讨论数据观察时,咱们一般指的是对一些特定类型的数据变化保持关注:缓存

  • 原始JavaScript对象中的变化
  • 当属性被添加、改变、或者删除时的变化
  • 当数组中的元素被添加或者删除时的变化
  • 对象的原型发生的变化

数据绑定的重要性

当你开始关心模型-视图的控制分离时,数据绑定就会变成一件重要的事。HTML是一个很是好的声明机制,可是它彻底是静态的。理想状态下,你想要在数据和DOM之间声明它们的关系,以便让DOM保持更新。这可让你剩下不少时间来编写重复来代码在DOM和应用内部或者服务器端请求或者发送数据。

当你拥有一个复杂的用户界面,你须要理清楚许多数据属性和许多视图中元素的关系时,数据绑定是很是有用的。这在咱们今天须要建立的单页应用中很是常见。

经过在浏览器中原生的观察数据,咱们给予了JavaScript框架(或者你编写的一些功能库)一种方式来实现对模型数据的变化进行观察而不须要依赖于咱们今天正在使用的一些hack方法。

今天的世界看起来是怎样的

脏值检查

你之前曾经在那里看到过数据绑定?若是你在你的web应用中使用过一个现代MV*框架(例如Angular,Knockout),那么你或许已经使用过数据绑定将数据绑定到你的DOM上了。为了复习一下,下面是一个电话列表应用的例子,在其中咱们会将一个phones数组中的值(在JavaScript中定义)绑定到一个列表项目中以便于咱们的数据和UI保持同步:

<html ng-app> <head> ... <script src="angular.js"></script> <script src="controller.js"></script> </head> <body ng-controller="PhoneListCtrl"> <ul> <li ng-repeat="phone in phones"> {{phone.name}} <p>{{phone.snippet}}</p> </li> </ul> </body> </html> 

咱们的JavaScript代码这样写:

var phonecatApp = angular.module('phonecatApp', []); phonecatApp.controller('PhoneListCtrl', function($scope) { $scope.phones = [ {'name': 'Nexus S', 'snippet': 'Fast just got faster with Nexus S.'}, {'name': 'Motorola XOOM with Wi-Fi', 'snippet': 'The Next, Next Generation tablet.'}, {'name': 'MOTOROLA XOOM', 'snippet': 'The Next, Next Generation tablet.'} ]; }); 

任什么时候候只要是底层的model数据发生了变化,咱们在DOM中的列表也会跟着更新。Angular是怎么作到这一点的呢?在Angular的背后,有一个叫作脏值检查的东西。

脏值检查的基本原理就是只要任什么时候候数据发生了变化,这个库都会经过一个digest或者change cycle去检查变化是否发生了。在Angular中,一个digest循环意味着全部全部被监视的表达式都会被循环一遍以便查看其中是否有变化发生。它智斗一个模型以前的值所以当变化发生时,一个change事件将会被触发。对于开发者来讲,这带来的一大好处就是你可使用原生的JavaScript对象数据,它易于使用及整合。下面的图片展现的是一个很是糟糕的算法,它的开销很是大。

这个操做的开销和被监视的对象的数量是成正比的。我可能须要作不少的脏治检查。同时我也须要一种方式去触发脏值检查,当某些数据可能发生改变时。有不少的框架使用了一些很是聪明的方法来解决这个问题,可是它们是否足够好目前还尚无定论。

web生态系统应该拥有更多的能力去创新和进化它本身的声明机制,例如:

  • 有约束的模型系统
  • 自动的保存系统(例如:将变化保存在IndexedDB或者localStorage中)
  • 容器对象(Ember,Backbone)

容器对象是一个框架建立的对象,它可以在其中保存一些数据。它们拥有一些存取器去获取数据而且可以在你设置或者获取对象时捕获到这些行为并在内部进行广播。这是一种很是好的方式。它的性能很好,从算法上来讲也不错。下面是一个使用Ember容器对象的一个简单例子:

// 容器对象 MyApp.president = Ember.Object.create({ name: "Barak Obama" });

MyApp.country = Ember.Object.create({ //使用Binding去建立一个属性以便告诉Ember建立一个到presidentName属性的绑定 presidentNameBinding: "MyApp.president.name" });

// 而后,Ember会解析绑定 MyApp.country.get("presidentName"); // "Barack Obama"

//来自服务器的数据须要被如今的代码转换整合

在上面的例子中,发现什么地方发生了变化的开销和发生改变的东西有着直接联系。如今你存在的另外一个问题是你须要使用不一样种类的对象。总的来讲你须要将从服务器获取的数据进行转换以便它们是可以被观察到的。

目前的JS代码并不能很好的整合生成数据,由于这些代码通常会假设它们操做的是原生JavaScript对象,而不是一些特定的对象相似类型。

介绍Object.observe()

咱们真正想要的多是两个世界中最好的东西 – 一种支持对原生数据对象(普通JavaScript对象)进行观察的方法,同时不须要每次都对全部东西进行脏值检查。它须要有良好的算法表现。它还须要可以很好的整合到各个平台中。这些都是Object.observe()可以带给咱们的东西。

它容许咱们对一个对象或者变异属性进行观察,而且在变化发生时获得及时通知。可是咱们在这里不想看什么理论,让咱们来看看代码!

Object.observe()和Object.unobserve()

让咱们假设咱们如今有一个简单的JavaScript对象,它表明一个模型:

// 一个模型多是一个简单的JavaScript对象 var todoModel = { label: 'Default', completed: false }; 

咱们能够制定一个比回调函数,用来处理对象上的变化:

function observer(changes){ changes.forEach(function(change, i){ console.log('what property changed? ' + change.name); console.log('how did it change? ' + change.type); console.log('whats the current value? ' + change.object[change.name]); console.log(change); // 全部的变化 }); } 

注意:当观察者回调函数被调用时,被观察的对象可能已经发生了屡次改变,所以对于每一次变化,新的值(即每次变化之后的值)和当前值(最终的值)并不必定是相同的。

咱们可使用Object.observe()来观察这些变化,只要将对象做为第一个参数,而将回调函数做为第二个参数:

Object.observe(todoModel, observer); 

咱们如今对咱们的Todos的模型对象作一些改变:

todoModel.label = 'Buy some more milk'; 

看看控制台,咱们如今获得了一些有用的信息!咱们知道什么属性发生了变化,它是怎样变化的以及新的值是什么。

再见,脏值检查!你的墓碑应该被刻上Comic Sans字体。咱们再来改变其余的属性。此次改变的是completeBy:

todoModel.completeBy = '01/01/2014'; 

正如咱们所见的,咱们又再一次获得了关于变化的报告:

很是好。要是咱们如今决定从对象中删除’completed’属性会怎么样:

delete todoModel.completed; 

正如咱们所见的,返回的变化报告包含了关于删除的信息。正如咱们所期待的,新的值如今是undefined。那么,咱们如今知道了你能够知道属性何时被添加。何时被删除。基本上来讲,你能够知道一个对象上的属性集(’new’,’deleted’,’recongigured’)以及它的原型(proto)的变化。

在任何观察系统中,老是存在一个方法来中止观察。在这里,咱们有Object.unobserve()方法,它的用法和Object.observe()同样可是能够像下面同样被调用:

Object.unobserve(todoModel, observer); 

正以下面所示,在使用该方法以后,任何的变化都再也不做为一个变化列表记录返回。

指定感兴趣的变化

如今咱们已经了解到了咱们如何去获取一个被观察对象的变化列表。可是若是咱们仅仅只对一个对象中的某些属性感兴趣该怎么办?人人都须要一个垃圾邮件过滤器。Observer能够经过一个列表指定一些咱们想要看到的变化。咱们须要经过Object.observe()的第三个参数来指定:

Object.observe(obj, callback, opt_acceptList) 

如今咱们来看一个如何使用的例子:

// 就像前面的例子同样,一个模型能够是一个简单的对象 var todoModel = { label: 'Default', completed: false };  // 指定一个回调函数 function observer(changes){ changes.forEach(function(change, i){ console.log(change); }) };  // 指定一个咱们想要观察的变化类型 Object.observe(todoModel, observer, ['delete']); todoModel.label = 'Buy some milk';  // 注意到变化没有被报告 

若是咱们删除了这个标签,注意到这个类型的变化将会被报告:

delete todoModel.label; 

若是你不指定一个列表,它默认将会报告“固有的”对象变化类型 (”add”, “update”, “delete”, “reconfigure”, “preventExtensions” (丢与那些不可扩展的对象是不可观察的))。

通知

Object.observe()也带有一些通知。它们并不像是你在手机上看到了通知,而是更加有有用。通知和变异观察者比较相似。它们发生在微任务的结尾。在浏览器的上下文,它几乎老是位于当前事件处理器的结尾。

这个时间点很是的重要由于基本上来讲此时一个工做单元已经结束了,如今观察者已经开始它们的共走了。这是一个很是好的回合处理模型。

使用一个通知器的工做流程以下所示:

如今咱们经过一个例子来如何经过自定义一个通知器来处理一个对象的属性被设置或者被获取的状况。注意看代码中的注释:

// 定义一个简单的模型 var model = { a: {} };

// 定义一个单独的变量,咱们即将使用它来做为咱们的模型中的getter

var _b = 2;

// 在’a’下面定义一个新的属性’b’,并自定义一个getter和setter

Object.defineProperty(model.a, 'b', { get: function () { return _b; }, set: function (b) {

 // 当'b'在模型中被设置时,注意一个特定类型的变化将会发生  // 这将给你许多关于通知的控制器 Object.getNotifier(this).notify({ type: 'update', name: 'b', oldValue: _b });  // 在值发生变化时将会输出信息 console.log('set', b); _b = b; } 

});

// 设置咱们的观察者 function observer(changes) { changes.forEach(function (change, i) { console.log(change); }) }

// 开始观察model.a Object.observe(model.a, observer);

如今当数据属性发生变化时(’update’)咱们将会获得报告。以及任何对象的实现也将会被报告(notifier.notifyChange())。

多年的web平台开发经验告诉咱们整合方法是你应该最早尝试的事情,由于它最容易去实现。可是它存在的问题是以它会创造一个从根本上来看就很未下的处理模型。若是你正在编写代码而且更新了一个对象的属性,你实际上并不想陷入这样一种困境:更新模型中的属性会最终致使任意一段代码去作任意一件事情。当你的函数正好运行到一半时,假设失效并非什么理想的情况。

若是你是一个观察者,你并不想当某人正在作某事的时候被调用。你并不像在不连续的状态下被调用。由于这最终每每会致使更多的错误检查。你应该试着去容忍更多的情形,而且基本上来讲它是一个很难去合做的模型。异步是一件更难处理的事情可是最终它会产生更好的模型。

上述问题的解决办法是变化合成记录(synthetic change records)。

变化合成记录

基本上来讲,若是你想要存取器或者计算属性的话,你应该复杂在这些值发生改变时发出通知。这会致使一些额外的工做,可是它是这种机制第一类的特征,而且这些通知会连同来自余下的底层数据对象的通知一块儿被发布出来。

观察存取器或者计算属性的问题能够经过使用notifier.notify来解决 – 它也是Object.observe()的另一部分。大多数的观察系统想要某些形式的观察导出值。有不少方法能够实现它。Object.observe()并无用“正确的”方式进行判断。计算属性应该是存取器,当内部的(私有的)状态发生改变时它应该发出通知。

再一次声明,在web中应该有一些库来帮助咱们进行通知而且帮助咱们更好的实现计算属性(以及减小模板的使用)。

咱们在这里会假设一个例子,这个例子中有一个circle类。在这里,咱们有一个citcle,它有一个radius属性。在这里的情形中,radius是一个存取器,而且当它的值发生变化时它实际上会去通知本身值已经发生变化了。这些通知将会连同其余变化被传递到这个对象或者其余对象。本质上来讲,若是你正在实现一个对象,你必定会想要拥有整合或者计算属性的对象,或者你想要想出一个策略如何让它运行。一旦你作了这件事,它将会适应你的整个系统。

看看下面的代码在开发者工具中是如何运行的:

function Circle(r) { var radius = r; var notifier = Object.getNotifier(this); function notifyAreaAndRadius(radius) { notifier.notify({ type: 'update', name: 'radius', oldValue: radius }) notifier.notify({ type: 'update', name: 'area', oldValue: Math.pow(radius * Math.PI, 2) }); } Object.defineProperty(this, 'radius', { get: function() { return radius; }, set: function(r) { if (radius === r) return; notifyAreaAndRadius(radius); radius = r; } }); Object.defineProperty(this, 'area', { get: function() { return Math.pow(radius, 2) * Math.PI; }, set: function(a) { r = Math.sqrt(a)/Math.PI; notifyAreaAndRadius(radius); radius = r; } }); } function observer(changes){ changes.forEach(function(change, i){ console.log(change); }) } 

存取器属性

在这里咱们对于存取器属性有一个简短的提示。在前面咱们提到了对于数据属性来讲只有值得变化是可以被观察到的。而存取器属性和计算属性则没法被观察到。这是由于JavaScript中的存取器并无真正的值的变化。一个存取器仅仅是一个函数集合。

若是你为一个存取器属性赋值,你仅仅只是调用了这个函数,而且在它看来值并无发生变化。它仅仅只是让一些代码运行起来。

这里的问题在于咱们在上面的例子中将存取器属性赋值为5.咱们应该可以知道这里究竟发生了什么。这其实是一个未解决的问题。这个例子说明了缘由。对任何系统来讲知道这究竟意味着什么是不可能的,由于在这里能够运行任意代码。每当存取器属性被访问时,它的值都会发生改变,所以询问它何时会发生变化并无多大的意义。

使用一个回调函数观察多个对象

Object.observe()上的另外一个模式是使用单个回调观察者。这容许咱们使用同一个回调函数堆多个不一样的对象进行观察。这个回调函数在“微任务”的结尾将会把全部的变化都传递给它所观察的对象。

大规模的变化

也许你正在编写一个很是大的应用,而且常常须要处理大规模的变化。此时咱们但愿用一种更加紧凑的方式来描述影响不少属性的语义变化。

Object.observe()使用两个特定的函数来解决这个问题:notifier.performChange()以及notifier.notify(),咱们在上面已经介绍过这两个函数了。

咱们能够从下面的例子中看到咱们如何来描述大规模变化,在这个例子中定义了一个叫作Thingy的对象,其中包含几个数计算功能(multiply, increment, incrementAndMultiply)。只要其中一个功能被使用,它就会告诉系统一些包含特定变化的事情发生了。

例如:notifier.performChange(‘foo’, performFooChangeFn)

function Thingy(a, b, c) { this.a = a; this.b = b; } Thingy.MULTIPLY = 'multiply'; Thingy.INCREMENT = 'increment'; Thingy.INCREMENT_AND_MULTIPLY = 'incrementAndMultiply'; Thingy.prototype = { increment: function(amount) { var notifier = Object.getNotifier(this);  // 告诉系统一系列事情包含一个特定的变化。例如: // notifier.performChange('foo', performFooChangeFn); // notifier.notify('foo', 'fooChangeRecord'); notifier.performChange(Thingy.INCREMENT, function() { this.a += amount; this.b += amount; }, this); notifier.notify({ object: this, type: Thingy.INCREMENT, incremented: amount }); }, multiply: function(amount) { var notifier = Object.getNotifier(this); notifier.performChange(Thingy.MULTIPLY, function() { this.a *= amount; this.b *= amount; }, this); notifier.notify({ object: this, type: Thingy.MULTIPLY, multiplied: amount }); }, incrementAndMultiply: function(incAmount, multAmount) { var notifier = Object.getNotifier(this); notifier.performChange(Thingy.INCREMENT_AND_MULTIPLY, function() { this.increment(incAmount); this.multiply(multAmount); }, this); notifier.notify({ object: this, type: Thingy.INCREMENT_AND_MULTIPLY, incremented: incAmount, multiplied: multAmount }); } } 

咱们能够为咱们的对象定义两个观察者: 一个用来捕获全部的变化,另外一个将只会汇报咱们定义的特定类型的变化 (Thingy.INCREMENT, Thingy.MULTIPLY, Thingy.INCREMENT_AND_MULTIPLY)。

var observer, observer2 = { records: undefined, callbackCount: 0, reset: function() { this.records = undefined; this.callbackCount = 0; }, }; observer.callback = function(r) { console.log(r); observer.records = r; observer.callbackCount++; }; observer2.callback = function(r){ console.log('Observer 2', r); } Thingy.observe = function(thingy, callback) {  // Object.observe(obj, callback, opt_acceptList) Object.observe(thingy, callback, [Thingy.INCREMENT, Thingy.MULTIPLY, Thingy.INCREMENT_AND_MULTIPLY, 'update']); } Thingy.unobserve = function(thingy, callback) { Object.unobserve(thingy); 

咱们如今能够开始玩弄一下代码了。咱们先定义一个新的Thingy:

var thingy = new Thingy(2,4); 

对它进行观察并进行一些变化。有趣的事情发生了!

// 观察thingy Object.observe(thingy, observer.callback); Thingy.observe(thingy, observer2.callback);  // 把玩一下thing暴露的方法 thingy.increment(3);  // { a: 5, b: 7 } thingy.b++;// { a: 5, b: 8 } thingy.multiply(2);// { a: 10, b: 16 } thingy.a++;// { a: 11, b: 16 } thingy.incrementAndMultiply(2, 2); // { a: 26, b: 36 } 

位于这个“perform function”中的一切东西均可以被看做是“大型变化”进行的工做。接受“大型变化”的观察者仅仅只会接受“大型变化”的记录。那些不会接受底层变化的观察者都来源于“perform function”所作的事。

观察数组

咱们已经讨论了如何观察一个对象,可是应该如何观察数组呢?

Array.observe()是一个针对自身大型变化的方法 – 例如 – splice,unshift或者任何可以隐式影响数组长度的东西。在内部它使用了Internally it uses notifier.performChange(“splice”,…)。

下面是一个咱们如何观察一个模型“数组”的例子,当底层数据发生一些变化时,咱们将可以获得一个变化的列表。

var model = ['Buy some milk', 'Learn to code', 'Wear some plaid']; var count = 0; Array.observe(model, function(changeRecords) { count++; console.log('Array observe', changeRecords, count); }); model[0] = 'Teach Paul Lewis to code'; model[1] = 'Channel your inner Paul Irish'; 

性能

考虑Object.observe()性能的方式是将它想成读缓存。基本上来讲,在如下几种情形中,一个缓存是最佳选择(按照重要性排序):

  1. 读的频率决定着写的频率
  2. 你能够创造一个缓存,它能够在读数据期间将涉及到写数据的操做进行算法上的优化
  3. 写数据减慢的时间常数是能够接受的

Object.observe()是为上述第一种情形设计的。

脏值检查须要保留一个你所要观察数据的副本。这意味着在脏值检查中你须要一个额外的结构内存开销。脏值检查,一个做为权宜之计的解决方案,同时根本上也是一个脆弱的抽象,它可能会致使应用中一些没必要要的复杂性。

脏值检查在任何数据可能发生变化的时候都必需要运行。这很明显并非一个很是鲁棒的方法,而且任何实现脏值检查的途径都是有缺陷的(例如,在轮询中进行检查可能会形成视觉上的假象以及涉及到代码的紊乱状况)。脏值检查也须要注册一个全局的观察者,这极可能会形成内存泄漏,而Object.observe()会避免这一点。

咱们如今来看一些数据。

下面的基准测试容许咱们比较脏值检查和Object.observe()。图中比较的数据是Observed-Object-Set-Size 和 Number-Of-Mutations。

总的结果代表:脏值检查的性能和被观察的对象成正比,而Object.observe()的性能和咱们所作的改变成正比。

  • 脏值检查

  • 开启了Object.observe()的Chrome浏览器

为Object.observe()提供垫片

Object.observe()如今已经能够在Chrome 36 beta中使用,可是若是咱们想要在其余浏览器中使用它该怎么办?Polymer中的Observe-JS是一个针对于那些没有原生实现Object.observe()浏览器的一个垫片,可是它不只仅是做为垫片,同时也包含了许多有用的语法糖。它提供了一种整合的视角,它可以将全部变化总结起来而且提交一份关于变化的报告。它的好处主要体如今两点:

  1. 你能够观察路径。这意味着你能够说,我想要从一个给定的对象中观察’foo.bar.baz’,只要这个路径的值发生了改变,你会获得通知。若是路径是错误的,将会返回undefined。

下面是一个例子:

var obj = { foo: { bar: 'baz' } }; var observer = new PathObserver(obj, 'foo.bar'); observer.open(function(newValue, oldValue) {  // 针对于 obj.foo.bar 已经改变的值进行响应 }); 
  1. 它可以告诉你数组的拼接。数组拼接基本上来讲是你为了将旧版本数组转换为新版本数组是须要进行了最基本的拼接操做。这是一种转换的类型或者是这个数组的不一样视图。它是你想要将数组从旧状态变为新状态时须要进行的最基本的工做。

下面是一个例子

var arr = [0, 1, 2, 4]; var observer = new ArrayObserver(arr); observer.open(function(splices) {  // 响应arr元素的变化 splices.forEach(function(splice) { splice.index; // 变化发生的位置 splice.removed; // 一个表明被移除的元素的序列值数组 splice.addedCount; // 被插入元素的个数 }); }); 

框架和Object.observe()

正如上面所提到的,使用Object.observe()可以给予框架和库中关于数据绑定的性能巨大的提高。

来自Ember的Yehuda Katz和Erik Bryn已经肯定将会在Ember最近的修改版本中添加对Object.observe()的支持。来自Angular的Misko Hervy写了一份关于Angular 2.0的设计文档,其中的内容关于改善变化探测(change detection)。在未来,当Object.observe()在Chrome稳定版中出现时,Angular会使用Object.observe()来实现变化探测的功能,在此以前它们会选择使用Watchtower.js – Angular本身的变化探测的实现方式。实在是太使人激动了。

总结

Object.observe()是一个添加到web平台上很是强大的特性,你如今就能够开始使用它。

咱们但愿这项特征可以及时的登录到更多的浏览器中,它可以容许JavaScript框架从本地对象观察的能力中得到更多性能上的提高。Chrome 36 beta及其以上的版本都能使用这项特性,在将来Opera发布的版本中这项特性也会获得支持。

如今就和JavaScript框架做者谈谈Object.observe()如何可以提升他们框架中数据绑定的性能。将来还有更多让人激动的时刻。

参考材料


本文译自Data-binding Revolutions with Object.observe(),原文地址http://www.html5rocks.com/en/tutorials/es7/observe/#toc-notifications

若是你以为本文对你有帮助,请为我提供赞助 https://me.alipay.com/jabez128

 

http://www.html-js.com/article/A-day-to-learn-Objectobserve-JavaScript-binding-data-change

相关文章
相关标签/搜索