在 Ember 中處理異步
許多 Ember 的概念,比如綁定和計算屬性,其設計是為了完成異步行為的處理。
沒有 Ember 的情況
我們首先來看一看用 jQuery 或基于事件的 MVC 框架如何管理異步行為。
讓我們使用一個 web 應用中最常見的異步行為——發起一個 Ajax 請求——來作為例子。 瀏覽器發起 Ajax 請求的 API 提供了一個異步的 API。jQuery 包裝器也可以實現:
- jQuery.getJSON('/posts/1', function(post) {
- $("#post").html("<h1>" + post.title + "</h1>" +
- "<div>" + post.body + "</div>");
- });
在純 jQuery 應用中,你會用這個回調來隨意進行要更改到 DOM 的更改。
當使用一個基于事件的 MVC 框架,會把邏輯從回調中拿出而放進模型和視圖對象里。 這改進了很多東西,但仍未擺脫顯式處理異步回調的需求:
- Post = Model.extend({
- author: function() {
- return [this.salutation, this.name].join(' ')
- },
- toJSON: function() {
- var json = Model.prototype.toJSON.call(this);
- json.author = this.author();
- return json;
- }
- });
- PostView = View.extend({
- init: function(model) {
- model.bind('change', this.render, this);
- },
- template: _.template("<h1><%= title %></h1><h2><%= author %></h2><div><%= body %></div>"),
- render: function() {
- jQuery(this.element).html(this.template(this.model.toJSON());
- return this;
- }
- });
- var post = Post.create();
- var postView = PostView.create({ model: post });
- jQuery('#posts').append(postView.render().el);
- jQuery.getJSON('/posts/1', function(json) {
- // set all of the JSON properties on the model
- post.set(json);
- });
這個例子沒有用任何特殊的 JavaScript 庫,但它的實現途徑是事件驅動的 MVC 框架 中的典型。它實現了異步事件的組織,但異步行為仍然在核心程序模型中。
Ember 的方法
總體而言,Ember 的目的是消除顯式形式的異步行為。如我們之后會見到的,這給予了 Ember 合并多個具有相同結果事件的能力。
它也提供了高層抽象,消滅手動注冊/反注冊事件監聽器來執行大多數任務的需求。
你一般會為這個例子使用用 ember-data ,當讓我們看看如何用 jQuery 來為 Ember 中的 Ajax 模型化上面的例子。
- App.Post = Ember.Object.extend({
- });
- App.PostController = Ember.ObjectController.extend({
- author: function() {
- return [this.get('salutation'), this.get('name')].join(' ');
- }.property('salutation', 'name')
- });
- App.PostView = Ember.View.extend({
- // the controller is the initial context for the template
- controller: null,
- template: Ember.Handlebars.compile("<h1>{{title}}</h1><h2>{{author}}</h2><div>{{body}}</div>")
- });
- var post = App.Post.create();
- var postController = App.PostController.create({ content: post });
- App.PostView.create({ controller: postController }).appendTo('body');
- jQuery.getJSON("/posts/1", function(json) {
- post.setProperties(json);
- });
與上面的例子相反,Ember 的實現方法消滅了當 post 的屬性變更時顯式注冊觀察者 的需求。
{{title}} 、 {{author}} 和 {{body}} 模板元素被限定到 PostController 上的那些元素中。當 PostController 的內容更改,它自動傳播那些變更到 DOM。
為 author 使用一個計算屬性消滅了在底層屬性變更時顯式地調用回調中計算的需 求。
除此之外,Ember 的綁定系統自動跟蹤從 getJSON 回調中設置的 salutation 和 name 到 PostController 中的計算屬性,并且始終把變化傳播到 DOM 中。
益處
因為 Ember 經常負責傳播變更,它可以保證在響應每個用戶事件上一個變更只傳播一 次。
讓我們再看一看 author 計算屬性。
- App.PostController = Ember.ObjectController.extend({
- author: function() {
- return [this.get('salutation'), this.get('name')].join(' ');
- }.property('salutation', 'name')
- });
因為我們已經指定了它依賴于 salutation 和 name ,這兩個依賴的任何一個的變 更都會使屬性無效,這會觸發對 DOM 中 {{author}} 的更新。
想象在響應一個用戶事件時,我要做這些事:
- post.set('salutation', "Mrs.");
- post.set('name', "Katz");
你會臆斷這些變更會導致計算屬性被無效兩次,導致更新 DOM 兩次。而事實上,這在 使用事件驅動的框架上確實會發生。
在 Ember 中,計算屬性會只重新計算一次,并且 DOM 也只會更新一次。
這是如何實現的呢?
當你對 Ember 中的一個屬性做出更改,它不會立即傳播變更。除此之外,它立即無效 任何有依賴的屬性,但把實際的修改放入隊列來讓它在之后發生。
對 salutation 和 name 屬性都修改會無效 author 屬性兩次,但隊列會智能地 合并那些變更。
一旦所有當前用戶事件的事件處理器完成,Ember 刷新隊列,把變更向下傳播。在這種 情況下,這意味著被無效的 author 屬性會無效 DOM 中的 {{author}} ,這會讓 單次請求只重新計算信息并更新自己一次。
這個機制是 Ember 的根基。 在 Ember 中,應該總是假定一個你所做變更的副作 用會在之后發生。通過做這個假設,你允許 Ember 來合并單次調用中相同的副作用。
總而言之,事件驅動系統的目標是從監聽器產生的負效用中解耦數據操作,所以你不應 該假定同步的副作用,即使在一個更關注事件的系統中。事實上,在 Ember 中副作用 不會立刻傳播,消除了欺騙并偶然地把應該分開的代碼耦合在一起的誘因。
副作用回調
既然你不能依賴同步的副作用,你會好奇如何確保特定的行為在恰好的時間發生。
例如,想象你有一個包含一個按鈕的視圖,并且你想用 jQuery UI 來樣式化這個按 鈕。因為視圖的 append 方法,如同 Ember 中的其它東西,推遲了它的副作用,怎 樣在正確的時間執行 jQuery UI 代碼?
答案是生命周期回調。
- App.Button = Ember.View.extend({
- tagName: 'button',
- template: Ember.Handlebars.compile("{{view.title}}"),
- didInsertElement: function() {
- this.$().button();
- }
- });
- var button = App.Button.create({
- title: "Hi jQuery UI!"
- }).appendTo('#something');
這種情況下,一旦按鈕真正出現在 DOM 中,Ember 會觸發 didInsertElement 回 調,然后你可以做任何你想要做的工作。
生命周期回調方法有很多好處,即使我們并不需要擔憂延遲的插入。
首先,依賴同步的插入意味著 appendTo 的調用者要來觸發任何需要在附加元素后立 即運行的行為。當你的應用變大后,你會發現在許多地方創建相同的視圖,并且需要擔 心它對每個地方的聯系。
生命周期回調消滅了實例化視圖和它的提交插入行為兩部分代碼的耦合。一般地,我們 發現不依賴于同步副作用導致了整體上的優良設計。
第二,因為關于視圖生命周期的一切都在視圖本身內部,按需重渲染 DOM 的部分對 Ember 是非常容易的。
例如,如果這個按鈕在一個 {{#if}} 塊中,并且 Ember 需要從主分支切換到 else 節,Ember 可以輕易實例化視圖并調用生命周期回調。
因為 Ember 強迫你定義一個完整定義的視圖,它可以控制在合適的場合創建并插入視 圖。
這也意味著所有的與 DOM 相關的代碼只在應用中封裝好的幾個部分,所以 Ember 在 這些在回調之外的渲染過程中的部分有更多的自由。
觀察者
在一些罕見的情況,你會想要在屬性變更已經傳播之后執行特定的行為。正如前面一節 中所述,Ember 提供了一個機制來掛鉤到屬性變更通知。
讓我們返回“稱呼”的例子。
- App.PostController = Ember.ObjectController.extend({
- author: function() {
- return [this.get('salutation'), this.get('name')].join(' ');
- }.property('salutation', 'name')
- });
如果我們想在作者變更時被通知,我們可以注冊一個觀察者。讓我們表示為視圖函數 想要被通知:
- App.PostView = Ember.View.extend({
- controller: null,
- template: Ember.Handlebars.compile("<h1>{{title}}</h1><h2>{{author}}</h2><div>{{body}}</div>"),
- authorDidChange: function() {
- alert("New author name: " + this.getPath('controller.author'));
- }.observes('controller.author')
- });
Ember 在它成功傳播變更后觸發觀察者。在本例中,這意味著 Ember 會只對每個用戶 事件調用一次 authorDidChange ,即使 salutation 和 name 都有變更。
這為你提供了在屬性變更后執行代碼的便利,而不強制所有的屬性變更同步。這基本上 意味著如果你需要對一個計算屬性的變更做一些手動工作,你會得到相同的合并便利, 如同在 Ember 的綁定系統中一樣。
最后,你也可以手動注冊觀察者,在對象定義之外:
- App.PostView = Ember.View.extend({
- controller: null,
- template: Ember.Handlebars.compile("<h1>{{title}}</h1><h2>{{author}}</h2><div>{{body}}</div>"),
- didInsertElement: function() {
- this.addObserver('controller.name', function() {
- alert("New author name: " + this.getPath('controller.author'));
- });
- }
- });
盡管如此,當你使用對象定義語法,Ember 會自動在對象銷毀時銷毀觀察期。例如,一 個 {{#if}} 語句從真值變為假值,Ember 銷毀塊內定義的所有試圖。作為這個過程 的一部分,Ember 也會斷開所有綁定和內聯觀察者。
如果你手動定義了一個觀察者,你需要確保移除了它。一般地,你會想要在與創建時相 反的回調中移除觀察者。在本例中,你會想要移除 willDestroyElement 的回調。
- App.PostView = Ember.View.extend({
- controller: null,
- template: Ember.Handlebars.compile("<h1>{{title}}</h1><h2>{{author}}</h2><div>{{body}}</div>"),
- didInsertElement: function() {
- this.addObserver('controller.name', function() {
- alert("New author name: " + this.getPath('controller.author'));
- });
- },
- willDestroyElement: function() {
- this.removeObserver('controller.name');
- }
- });
如果你在 init 方法中添加了觀察者,你會想要在 willDestroy 回調中銷毀它。
總而言之,你會極少用這種方法注冊一個手動的觀察者。為了保障內存管理,我們強烈 建議盡可能把觀察者定義為對象定義的一部分。