Ember.js 的視圖層
本指導(dǎo)會(huì)詳盡闡述 Ember.js 視圖層的細(xì)節(jié)。為想成為熟練 Ember 開(kāi)發(fā)者準(zhǔn)備,且包 含了對(duì)于入門(mén) Ember 不必要的細(xì)節(jié)。
Ember.js 有一套復(fù)雜的用于創(chuàng)建、管理并渲染連接到瀏覽器 DOM 上的層級(jí)視圖的系 統(tǒng)。視圖負(fù)責(zé)響應(yīng)諸如點(diǎn)擊、拖拽以及滾動(dòng)等的用戶(hù)事件,也在視圖底層數(shù)據(jù)變更時(shí)更 新 DOM 的內(nèi)容。
視圖層級(jí)通常由求值一個(gè) Handlebars 模板創(chuàng)建。當(dāng)模板求值后,會(huì)添加子視圖。當(dāng) 那些 子視圖求值后,會(huì)添加它們的子視圖,如此遞推,直到整個(gè)層級(jí)被創(chuàng)建。
即使你并沒(méi)有在 Handlebars 模板中顯式地創(chuàng)建子視圖,Ember.js 內(nèi)部仍使用視圖系 統(tǒng)更新綁定的值。例如,每個(gè) Handlebars 表達(dá)式 {{value}}
幕后創(chuàng)建一個(gè)視圖, 這個(gè)視圖知道當(dāng)值變更時(shí)如何更新綁定值。
你也可以在應(yīng)用運(yùn)行時(shí)用 Ember.ContainerView
類(lèi)對(duì)視圖層級(jí)做出修改。一個(gè)容器 視圖暴露一個(gè)可以手動(dòng)修改的子視圖實(shí)例數(shù)組,而非模板驅(qū)動(dòng)。
視圖和模板串聯(lián)工作提供一套用于創(chuàng)建任何你夢(mèng)寐以求的用戶(hù)界面的穩(wěn)健系統(tǒng)。最終用 戶(hù)應(yīng)從諸如當(dāng)渲染和事件傳播是的計(jì)時(shí)事件之類(lèi)的復(fù)雜東西中隔離開(kāi)。應(yīng)用開(kāi)發(fā)者應(yīng)可 以一次性把他們的 UI 描述成 Handlebars 標(biāo)記字符串,然后繼續(xù)完成他們的應(yīng)用,而 不必?zé)烙诖_保它一直是最新的。
它解決了什么問(wèn)題?
子視圖
在典型的客戶(hù)端應(yīng)用中,視圖同時(shí)在本身和 DOM 中表示嵌套的元素。在解決這個(gè)問(wèn)題 的天真方案中,獨(dú)立的視圖對(duì)象表示單個(gè) DOM 元素,專(zhuān)門(mén)的引用解決不同種類(lèi)的視圖 保持對(duì)概念中嵌套在它們內(nèi)部的視圖的跟蹤。
這里是一個(gè)簡(jiǎn)單的例子,表示一個(gè)應(yīng)用主視圖,里面有一集合嵌套視圖,且獨(dú)立的元素在集合內(nèi)嵌套。
這個(gè)系統(tǒng)第一眼看上去毫無(wú)異樣,但是想象我們要在上午 8 點(diǎn)而不是上午 9 點(diǎn)開(kāi)放喬 的七鰓鰻小屋。在這種情況下,我們會(huì)想要重新渲染應(yīng)用視圖。因?yàn)殚_(kāi)發(fā)者需要構(gòu)建指 向在一個(gè)特殊基礎(chǔ)上的子視圖的引用,這個(gè)重渲染過(guò)程存在若干問(wèn)題。
為了重新渲染應(yīng)用視圖,應(yīng)用視圖也必須手動(dòng)重新渲染子視圖并重新把它們插入到應(yīng)用 視圖的元素中。如果實(shí)現(xiàn)得完美,這個(gè)過(guò)程會(huì)正常工作,但它依賴(lài)于一個(gè)完美的,專(zhuān)門(mén) 的視圖層級(jí)實(shí)現(xiàn)。如果任何一個(gè)視圖沒(méi)有精確地實(shí)現(xiàn)它,整個(gè)重新渲染過(guò)程會(huì)失敗。
為了避免這些問(wèn)題,Ember 的視圖層級(jí)從概念上就帶有子視圖的烙印。
當(dāng)應(yīng)用時(shí)圖重新渲染時(shí),Ember 而不是應(yīng)用代碼負(fù)責(zé)重新渲染并插入子視圖。這也意味 著 Ember 可以為你執(zhí)行任何內(nèi)存管理,比如清理觀(guān)察者和綁定。
這不僅在一定程度上消滅了樣板代碼,也破除了有瑕疵的視圖層級(jí)實(shí)現(xiàn)帶來(lái)的未期失敗的可能。
#p#
事件委派
在過(guò)去,web 開(kāi)發(fā)者已經(jīng)用在獨(dú)立的單個(gè)元素上添加事件監(jiān)聽(tīng)器來(lái)獲知什么時(shí)候用戶(hù)與 它們交互。例如,你會(huì)有一個(gè) <div>
元素,其上注冊(cè)了一個(gè)當(dāng)用戶(hù)點(diǎn)擊它時(shí)觸發(fā)的 函數(shù)。
盡管如此,這個(gè)途徑在處理大數(shù)量交互元素上不會(huì)縮放。比如,想象一個(gè)帶有 100 個(gè) <li>
的 <ul>
,每個(gè)項(xiàng)目后都有一個(gè)刪除按鈕。既然所有的這些項(xiàng)目行為都是一 致的,為每個(gè)刪除按鈕創(chuàng)建共計(jì) 100 個(gè)事件監(jiān)聽(tīng)器無(wú)疑是低效的。
要解決這個(gè)問(wèn)題,開(kāi)發(fā)者發(fā)現(xiàn)了一種名為“事件委派”的技術(shù)。你可以在容器元素上注冊(cè) 一個(gè)監(jiān)聽(tīng)器并使用 event.target
來(lái)識(shí)別哪個(gè)元素是用戶(hù)點(diǎn)擊的,而不是為問(wèn)題中的 每個(gè)項(xiàng)目創(chuàng)建一個(gè)監(jiān)聽(tīng)器。
實(shí)現(xiàn)這有一些微妙,因?yàn)橐恍┦录ū热?focus
、 blur
和 change
)不會(huì)冒 泡。幸運(yùn)的是,jQuery 已經(jīng)徹底解決了這個(gè)問(wèn)題;用 jQuery 的 on
方法可以可靠 地處理所有原生瀏覽器事件。
其它 JavaScript 框架用兩種方法中的其一來(lái)處理這個(gè)問(wèn)題。第一種是,它們要你自己 實(shí)現(xiàn)原生解決方案,為每個(gè)項(xiàng)目創(chuàng)建獨(dú)立的視圖。當(dāng)你創(chuàng)建視圖,它在視圖的元素上設(shè) 置一個(gè)監(jiān)聽(tīng)器。如果你有一個(gè)含有 500 個(gè)項(xiàng)目的列表,你會(huì)創(chuàng)建 500 個(gè)視圖并且每個(gè) 視圖都會(huì)在它自己的元素上設(shè)置一個(gè)監(jiān)聽(tīng)器。
第二種方法是,框架在視圖層內(nèi)置事件委派。當(dāng)創(chuàng)建一個(gè)視圖,你可以提供一個(gè)事件列 表來(lái)在事件發(fā)生時(shí)委派一個(gè)方法來(lái)調(diào)用。這只剩下識(shí)別接受事件的方法的點(diǎn)擊上下文 (比如,列表中的哪個(gè)項(xiàng)目)。
你現(xiàn)在要面對(duì)兩個(gè)令人不安的選擇:為每個(gè)項(xiàng)目創(chuàng)建一個(gè)新視圖,這樣會(huì)喪失事件委派 的優(yōu)勢(shì),或是為所有項(xiàng)目創(chuàng)建單個(gè)視圖,這樣必須存儲(chǔ) DOM 中底層 JavaScript 的信息。
要解決這個(gè)問(wèn)題,Ember 用 jQuery 把所有事件委派到應(yīng)用的根元素(通常是文檔的 body
)。當(dāng)一個(gè)事件發(fā)生,Ember 識(shí)別出最近的處理事件視圖并調(diào)用它的事件處理 器。這意味著你可以創(chuàng)建視圖來(lái)保存一個(gè) JavaScript 上下文,但仍然從事件委派上受 益。
進(jìn)一步地,因?yàn)?Ember 只為整個(gè) Ember 應(yīng)用注冊(cè)一個(gè)事件,創(chuàng)建新視圖永遠(yuǎn)都不需要 設(shè)置事件監(jiān)聽(tīng)器,這使得重渲染高效且免于出錯(cuò)。當(dāng)視圖有一個(gè)子視圖,這也意味著不 需要手動(dòng)取消委派重新渲染過(guò)程中替換掉的視圖。
渲染管道
大多數(shù) web 應(yīng)用用特殊的模板語(yǔ)言標(biāo)記來(lái)指定它們的用戶(hù)界面。對(duì)于 Ember.js,我們 已經(jīng)完成用可在值修改的時(shí)候自動(dòng)更新模板的 Handlebars 模板語(yǔ)言來(lái)編寫(xiě)模板。
雖然顯示模板的過(guò)程對(duì)開(kāi)發(fā)者是自動(dòng)的,但其遮蓋了把原始模板轉(zhuǎn)換為最終模板、生成 用戶(hù)可見(jiàn)的 DOM 表示的一系列必要步驟。
這是 Ember 視圖的近似生命周期:
1. 模板編譯
應(yīng)用的模板通過(guò)網(wǎng)絡(luò)加載或以字符串形式作為應(yīng)用的載荷。當(dāng)應(yīng)用加載時(shí),它發(fā)送模板 字符串到 Handlebars 來(lái)編譯成函數(shù)。一經(jīng)編譯,模板函數(shù)會(huì)被保存,且可以被多個(gè)視 圖重復(fù)使用,每次都它們都需重新編譯。
這個(gè)步驟會(huì)在應(yīng)用中服務(wù)器預(yù)編譯模板的地方發(fā)出。在那些情況下,模板不作為原始的 人類(lèi)可讀的模板傳輸,而是編譯后的代碼。
因?yàn)?Ember 負(fù)責(zé)模板編譯,你不需要做任何額外的工作來(lái)保證編譯后的模板可以重用。
#p#
2. 字符串的連接
當(dāng)應(yīng)用在視圖上調(diào)用 append
或 appendTo
時(shí),一個(gè)視圖渲染過(guò)程會(huì)被啟動(dòng)。 append
或 appendChild
調(diào)用 安排 視圖渲染并在之后插入。這允許應(yīng)用中的 延遲邏輯(譬如綁定同步)在渲染元素之前執(zhí)行。
要開(kāi)始渲染過(guò)程,Ember 創(chuàng)建一個(gè) RenderBuffer
并把它呈遞給視圖來(lái)把視圖的內(nèi)容 附加到上面。在這個(gè)過(guò)程中,視圖可以創(chuàng)建并渲染子視圖。當(dāng)它這么做時(shí),父視圖創(chuàng)建 并分配一個(gè) RenderBuffer
給子視圖,并把它連接到父視圖的 RenderBuffer
上。
Ember 在渲染每個(gè)視圖前刷新綁定同步隊(duì)列。這樣,Ember 保障不會(huì)渲染需要立即替換 的過(guò)期數(shù)據(jù)。
一旦主視圖完成渲染,渲染過(guò)程會(huì)創(chuàng)建一個(gè)視圖樹(shù)(即“視圖層級(jí)”),連接到緩沖區(qū)樹(shù) 上。通過(guò)向下遍歷緩沖區(qū)樹(shù)并把它們轉(zhuǎn)換為字符串,我們就有了一個(gè)可以插入到 DOM 的字符串。
這里是一個(gè)簡(jiǎn)單的例子:
除子節(jié)點(diǎn)之外(字符串和其它 RenderBuffer
), RenderBuffer
也會(huì)封裝元素標(biāo) 簽名稱(chēng)、id、class、樣式和其它屬性。這使得渲染過(guò)程修改這些屬性(例如樣式)成 為可能,即使在子字符串已經(jīng)渲染完畢。因?yàn)檫@些屬性的許多都可以通過(guò)綁定(例如用 bindAttr
)控制,這使得渲染過(guò)程穩(wěn)健且透明。
3. 元素的創(chuàng)建和插入
在渲染過(guò)程的最后,根視圖向 RenderBuffer
請(qǐng)求它的元素。 RenderBuffer
獲得 它的完整字符串并用 jQuery 把它轉(zhuǎn)換成一個(gè)元素。視圖把那個(gè)元素分配到它的 element
屬性并把把它放置到 DOM 中正確的位置( appendTo
指定的位置,如果 應(yīng)用使用 append
即是應(yīng)用的根元素)。
雖然父視圖直接分配它的元素,但每個(gè)子視圖惰性查找它的元素。它通過(guò)查找 id
匹 配它的 elementId
屬性的元素來(lái)完成這。除非顯式提供,渲染過(guò)程生成一個(gè) elementId
屬性比你更分配它的值給視圖的 RenderBuffer
,RenderBuffer
允 許視圖按需查找它的元素。
4. 重新渲染
在視圖把自己插入到 DOM 后,Ember 和應(yīng)用都會(huì)要重新渲染視圖。它們可以在視圖上 調(diào)用 rerender
方法來(lái)出發(fā)一次重渲染。
重新渲染會(huì)重復(fù)上面的步驟 2 和步驟 3,有兩點(diǎn)例外:
rerender
用新元素替換已有的元素,而不是把元素插入到顯式定義的位置。- 除了渲染新元素,它也刪除舊元素并銷(xiāo)毀它的子元素。這允許 Ember 在重新渲染視 圖時(shí)自動(dòng)處理撤銷(xiāo)合適的綁定和觀(guān)察者。這使得路徑上的觀(guān)察者可行,因?yàn)樽?cè)和撤銷(xiāo) 注冊(cè)所有的嵌套觀(guān)察者都是自動(dòng)的。
最常見(jiàn)的導(dǎo)致視圖重新渲染的原因是當(dāng)綁定到 Handlebars 表達(dá)式( {{foo}}
)變 更。Ember 內(nèi)部為每個(gè)表達(dá)式創(chuàng)建一個(gè)簡(jiǎn)單的視圖,并且在路徑上注冊(cè)一個(gè)觀(guān)察者。當(dāng) 路徑變更時(shí),Ember 用新值更新那個(gè)區(qū)域的 DOM。
另一個(gè)常見(jiàn)的情況是一個(gè) {{#if}}
或 {{#with}}
塊。當(dāng)渲染一個(gè)模板時(shí),Ember 為這些塊輔助標(biāo)創(chuàng)建虛擬的視圖。這些虛擬的視圖不會(huì)出現(xiàn)在公共可訪(fǎng)問(wèn)的視圖層級(jí)里 (當(dāng)從視圖獲取 parentView
和 childViews
時(shí)),但它們的存在啟用了一致的重 渲染。
當(dāng)傳遞到 {{#if}}
或 {{#with}}
的路徑變更,Ember 自動(dòng)重新渲染虛擬視圖替換 它的內(nèi)容,重要的是,也會(huì)銷(xiāo)毀所有的子視圖來(lái)釋放內(nèi)存。
除了這些情景,應(yīng)用有時(shí)也會(huì)要顯式地重新渲染視圖(通常是一個(gè) ContainerView
,見(jiàn)下)。在這種情況下,應(yīng)用可以直接調(diào)用 rerender
,且 Ember 會(huì)把一項(xiàng)重渲染工作加入隊(duì)列,用相同的語(yǔ)義元素。
這個(gè)過(guò)程像是這樣:
#p#
視圖層級(jí)
父與子
當(dāng) Ember 渲染一個(gè)模板化的視圖,它會(huì)生成一個(gè)視圖層級(jí)。讓我們假設(shè)已有一個(gè)模板 form
。
原文鏈接:
- {{view App.Search placeholder="Search"}}
- {{#view Ember.Button}}Go!{{/view}}
然后我們像這樣把它插入到 DOM 中:
- var view = Ember.View.create({
- templateName: 'form'
- }).append();
這會(huì)創(chuàng)建一個(gè)如下小巧的視圖等級(jí):
你可以用 parentView
和 childViews
屬性在視圖層級(jí)中游走。
- var children = view.get('childViews') // [ <App.Search>, <Ember.Button> ]
- children.objectAt(0).get('parentView') // 視圖
一個(gè)常見(jiàn)的 parentView
使用方法是在子視圖的實(shí)例里。
- App.Search = Ember.View.extend({
- didInsertElement: function() {
- // this.get('parentView') 指向 `view`
- }
- })
生命周期鉤子
為了容易地在視圖的生命周期的不同點(diǎn)上執(zhí)行行為,有若干你可以實(shí)現(xiàn)的鉤子。
willInsertElement
: 這個(gè)鉤子在視圖渲染后插入 DOM 之前調(diào)用。它不提供對(duì)視圖的element
的訪(fǎng)問(wèn)。didInsertElement
: 這個(gè)鉤子在視圖被插入到 DOM 后立即調(diào)用。它提供到視圖的element
的訪(fǎng)問(wèn),且對(duì)集成到外部庫(kù)非常有用。任何顯式的 DOM 設(shè)置代碼應(yīng)限于這個(gè)鉤子。willDestroyElement
: 這個(gè)鉤子在元素從 DOM 移除前立即調(diào)用。這提供了銷(xiāo)毀任何與 DOM 節(jié)點(diǎn)關(guān)聯(lián)的外部狀態(tài)的機(jī)會(huì)。像didInsertElement
一樣,它對(duì)于集成外部庫(kù)非常有用。willRerender
: 這個(gè)鉤子在視圖被重新渲染前立即調(diào)用。如果你想要在視圖被重新渲染前執(zhí)行一些銷(xiāo)毀操作,這會(huì)很有用。becameVisible
: 這個(gè)鉤子在視圖的isVisible
或它的祖先之一的isVisible
變?yōu)檎嬷?,且關(guān)聯(lián)的元素也變?yōu)榭梢?jiàn)后調(diào)用。注意這個(gè)鉤子只在所有可見(jiàn)性由isVisible
屬性控制的時(shí)候可靠。becameHidden
: 這個(gè)鉤子在視圖的isVisible
或它的祖先之一的isVisible
變?yōu)榧僦?,且關(guān)聯(lián)的元素也變?yōu)殡[藏后調(diào)用。注意這個(gè)鉤子只在所有可見(jiàn)性由isVisible
屬性控制的時(shí)候可靠。
應(yīng)用可以通過(guò)在視圖上定義一個(gè)與鉤子同名的方法來(lái)實(shí)現(xiàn)鉤子?;蛘?,在視圖上為鉤子 注冊(cè)一個(gè)監(jiān)聽(tīng)器也是可行的。
- view.on('willRerender', function() {
- // do something with view
- });
虛擬視圖
正如上文所述,Handlebars 在視圖層級(jí)內(nèi)創(chuàng)建視圖來(lái)表現(xiàn)綁定值。每次你使用 Handlebars 表達(dá)式,無(wú)論是一個(gè)簡(jiǎn)單值還是一個(gè)諸如 {{#with}}
或 {{#if}}
的 塊表達(dá)式,Handlebars 會(huì)創(chuàng)建一個(gè)新視圖。
因?yàn)?Ember 只把這些視圖用于內(nèi)部簿記,它們對(duì)于視圖的公共 parentView
和 childViews
API 是隱藏的。公共視圖層級(jí)只反射用 {{view}}
輔助標(biāo)記或通過(guò) ContainerView
創(chuàng)建的視圖(見(jiàn)下)。
例如,考慮下面的 Handlebars 模板:
- <h1>Joe's Lamprey Shack</h1>
- {{controller.restaurantHours}}
- {{#view App.FDAContactForm}}
- 如果你在喬的七鰓鰻小屋用餐后不適,請(qǐng)用下面的表格向 FDA 提交申訴。
- {{#if controller.allowComplaints}}
- {{view Ember.TextArea valueBinding="controller.complaint"}}
- <button {{action submitComplaint}}>提交</button>
- {{/if}}
- {{/view}}
渲染這個(gè)模板會(huì)創(chuàng)建這樣的層級(jí):
幕后,Ember 跟蹤為 Handlebars 表達(dá)式創(chuàng)建的額外的虛擬視圖:
#p#
在TextArea
中, parentView
會(huì)指向 FDAContactForm
,并且 FDAContactForm
的 childViews
會(huì)是一個(gè)只包含 TextArea
的數(shù)組。
你可以通過(guò) _parentView
和 _childViews
來(lái)查看內(nèi)部視圖層級(jí),這會(huì)包含虛擬視 圖:
- var _childViews = view.get('_childViews');
- console.log(_childViews.objectAt(0).toString());
- //> <Ember._HandlebarsBoundView:ember1234>
警告! 你不應(yīng)該在應(yīng)用代碼中依賴(lài)于這些內(nèi)部 API。它們會(huì)在任何時(shí)候更改并且 沒(méi)有任何公共合約。返回值也不能被觀(guān)察或被綁定。它可能不是 Ember 對(duì)象。如果覺(jué) 得有使用它們的需求,請(qǐng)聯(lián)系我們,這樣我們可以為你的使用需求暴露一個(gè)更好的公共 API。
底線(xiàn):這個(gè) API 就像是 XML。如果你覺(jué)得你需要用到它,那么你很可能沒(méi)有足夠理解 問(wèn)題。三思!
事件冒泡
視圖的一個(gè)任務(wù)是響應(yīng)原始用戶(hù)事件并把它們翻譯成對(duì)你應(yīng)用而言有語(yǔ)義的事件。
例如,一個(gè)刪除按鈕把原始的 click
事件翻譯成應(yīng)用特定的“把這個(gè)元素從數(shù)組中刪 除”。
為了響應(yīng)用戶(hù)事件,創(chuàng)建一個(gè)視圖的子類(lèi)來(lái)把事件實(shí)現(xiàn)為方法:
- App.DeleteButton = Ember.View.create({
- click: function(event) {
- var stateManager = this.getPath('controller.stateManager');
- var item = this.get('content');
- stateManager.send('deleteItem', item);
- }
- });
當(dāng)你創(chuàng)建一個(gè)新的 Ember.Application
實(shí)例,它用 jQuery 的事件委派 API 給每個(gè) 原生瀏覽器事件注冊(cè)一個(gè)事件處理器。當(dāng)用戶(hù)觸發(fā)一個(gè)事件,應(yīng)用事件分配器會(huì)找出離 事件最近的視圖并實(shí)現(xiàn)那個(gè)事件。
一個(gè)視圖通過(guò)定義與事件同名的方法來(lái)實(shí)現(xiàn)事件。當(dāng)事件名稱(chēng)由多個(gè)詞組成(如 mouseup
)方法名會(huì)用 Camel 命名法把事件名作為方法名( mousUp
)。
事件會(huì)在視圖層級(jí)中冒泡,直到事件到達(dá)根視圖。一個(gè)事件處理器可以用與常規(guī) jQuery 事件處理器相同的技術(shù)來(lái)停止事件傳播:
- 在視圖中
return false
event.stopPropagation
例如,假設(shè)你已經(jīng)定義了如下的視圖類(lèi):
- App.GrandparentView = Ember.View.extend({
- click: function() {
- console.log('Grandparent!');
- }
- });
- App.ParentView = Ember.View.extend({
- click: function() {
- console.log('Parent!');
- return false;
- }
- });
- App.ChildView = Ember.View.extend({
- click: function() {
- console.log('Child!');
- }
- });
這是使用它們的 Handlebars 模板。
- {{#view App.GrandparentView}}
- {{#view App.ParentView}}
- {{#view App.ChildView}}
- <h1>點(diǎn)擊這里!</h1>
- {{/view}}
- {{/view}}
- {{/view}}
如果你點(diǎn)擊 <h1>
,你會(huì)在瀏覽器控制臺(tái)里看見(jiàn)下面的輸出:
- Child!
- Parent!
你可以看出 Ember 在接受事件的最深層級(jí)視圖上調(diào)用了處理器。事件繼續(xù)上浮到 ParentView
,但不會(huì)到達(dá) GrandparentView
因?yàn)?ParentView
從它的事件處理 器中返回了 false
。
你可以使用常規(guī)事件冒泡技術(shù)來(lái)實(shí)現(xiàn)常見(jiàn)的模式。例如,你可以實(shí)現(xiàn)一個(gè)帶有 submit
方法的 FormView
。因?yàn)闉g覽器在用戶(hù)向文本域輸入回車(chē)的時(shí)候會(huì)觸發(fā) submit
事件,在表單視圖上定義一個(gè) submit
方法會(huì)“剛好完成任務(wù)”。
- App.FormView = Ember.View.extend({
- tagName: "form",
- submit: function(event) {
- // 會(huì)在任何用戶(hù)觸發(fā)瀏覽器的
- // `submit` 方法時(shí)被調(diào)用
- }
- });
- {{#view App.FormView}}
- {{view Ember.TextFieldView valueBinding="controller.firstName"}}
- {{view Ember.TextFieldView valueBinding="controller.lastName"}}
- <button type="submit">確定</button>
- {{/view}}
#p#
添加新事件
Ember 內(nèi)置了如下原生瀏覽器事件的支持:
事件名 |
方法名 |
---|---|
touchstart | touchStart |
touchmove | touchMove |
touchend | touchEnd |
touchcancel | touchCancel |
keydown | keyDown |
keyup | keyUp |
keypress | keyPress |
mousedown | mouseDown |
mouseup | mouseUp |
contextmenu | contextMenu |
click | click |
dblclick | doubleClick |
mousemove | mouseMove |
事件名 |
方法名 |
---|---|
focusin | focusIn |
focusout | focusOut |
mouseenter | mouseEnter |
mouseleave | mouseLeave |
submit | submit |
change | change |
dragstart | dragStart |
drag | drag |
dragenter | dragEnter |
dragleave | dragLeave |
dragover | dragOver |
drop | drop |
dragend | dragEnd |
當(dāng)你創(chuàng)建一個(gè)新應(yīng)用時(shí),你可以向事件分配器添加額外的事件:
- App = Ember.Application.create({
- customEvents: {
- // 添加 loadedmetadata 媒體播放器事件
- 'loadedmetadata': "loadedMetadata"
- }
- });
要使這能對(duì)自定義事件奏效,HTML5 規(guī)范必須定義事件為“bubbling”,否則 jQuery 必 須為這個(gè)事件提供一個(gè)事件委派折中方案。
模板化視圖
如同迄今你在本指導(dǎo)中所見(jiàn),你在應(yīng)用中會(huì)用的大多數(shù)視圖是依靠模板的。當(dāng)使用模板 時(shí),你不需要編寫(xiě)你的視圖層級(jí),因?yàn)槟0鍟?huì)為你創(chuàng)建它。
渲染時(shí),視圖模板可以把視圖附加到它的子視圖數(shù)組中。模板的 {{view}}
輔助標(biāo)記 內(nèi)部會(huì)調(diào)用視圖的 appendChild
方法。
調(diào)用 appendChild
會(huì)做兩件事:
- 把視圖添加到
childViews
數(shù)組。 - 立即渲染子視圖并把它添加到父視圖的渲染緩沖區(qū)。
appendChild
。模板渲染出“混合內(nèi)容”(包含 視圖和純文本),所以當(dāng)渲染過(guò)程完成后,父視圖不知道到底把新的子視圖插入到哪 里。
在上例中,想象試圖把一個(gè)新視圖插入到父視圖的 childViews
數(shù)組中。它應(yīng)該立即 放在 App.MyView
的閉合標(biāo)簽 </div>
后?還是在整個(gè)視圖的閉合標(biāo)簽 </div>
后?這個(gè)答案不總是正確的。
因?yàn)檫@種含糊性,創(chuàng)建視圖層級(jí)的唯一方法就是用模板的 {{view}}
輔助標(biāo)記,它總 是把視圖插入到相對(duì)任何純文本的正確位置。
雖然這個(gè)機(jī)制對(duì)大多數(shù)情景奏效,偶爾你也會(huì)想要直接程序控制一個(gè)視圖的子視圖。在 這種情況下,你可以用 Ember.ContainerView
,它顯式地暴露了實(shí)現(xiàn)此目的的 API。
#p#
容器視圖
容器視圖不包含純文本。它們完全由子視圖(可能依靠模板)構(gòu)成。
ContainerView
暴露兩個(gè)用于修改本身內(nèi)容的公共 API:
- 一個(gè)可寫(xiě)的
childViews
數(shù)組,你可以把Ember.View
實(shí)例插入到其中。 - 一個(gè)
currentView
屬性,設(shè)置時(shí)會(huì)把新值插入到子視圖數(shù)組。如果存在早先的currentView
值,它會(huì)被從childViews
數(shù)組刪除。
這里是一個(gè)用 childViews
API 創(chuàng)建新視圖的例子,由假想的 DescriptionView
開(kāi)始,并可以在任何時(shí)候用 addButton
方法添加一個(gè)新按鈕:
- App.ToolbarView = Ember.ContainerView.create({
- init: function() {
- var childViews = this.get('childViews');
- var descriptionView = App.DescriptionView.create();
- childViews.pushObject(descriptionView);
- this.addButton();
- return this._super();
- },
- addButton: function() {
- var childViews = this.get('childViews');
- var button = Ember.ButtonView.create();
- childViews.pushObject(button);
- }
- });
如你在上例中所見(jiàn),我們以?xún)蓚€(gè)視圖初始化 ContainerView
,并且可以在運(yùn)行時(shí)添 加額外的視圖。存在一個(gè)方便的捷徑來(lái)設(shè)置視圖,而不用覆蓋 init
方法:
- App.ToolbarView = Ember.ContainerView.create({
- childViews: ['descriptionView', 'buttonView'],
- descriptionView: App.DescriptionView,
- buttonView: Ember.ButtonView,
- addButton: function() {
- var childViews = this.get('childViews');
- var button = Ember.ButtonView.create();
- childViews.pushObject(button);
- }
- });
如上,當(dāng)用這個(gè)速記方法時(shí),你把 childViews
指定為一個(gè)字符串?dāng)?shù)組。在初始化 時(shí),每個(gè)字符串會(huì)作為在查找視圖實(shí)例或類(lèi)的關(guān)鍵字。那個(gè)視圖會(huì)被自動(dòng)實(shí)例化,如果 必要,會(huì)加入到 childViews
數(shù)組中。
- {{#if controller.isAuthenticated}}
- <h1>歡迎 {{controller.name}}</h1>
- {{/if}}
- {{#with controller.user}}
- <p>你有 {{notificationCount}} 條通知。</p>
- {{/with}}
在上面的模板中,當(dāng) isAuthenticated
屬性從 false
變?yōu)?true
時(shí),Ember 會(huì) 重新渲染這個(gè)塊,用原始的外部作用域作為它的上下文。
{{#with}}
輔助標(biāo)記把它的塊的上下文修改為當(dāng)前控制器的 user
屬性。當(dāng) user
屬性被修改。Ember 重新渲染塊,并用 controller.user
的新值作為它的上 下文。
#p#
視圖作用域
除了 Handlebars 上下文,Ember 中的模板也有當(dāng)前視圖的概念。無(wú)論當(dāng)前上下文是什 么, view
屬性總是引用到最近的視圖。
注意 view
屬性不會(huì)引用由 {{#if}}
之類(lèi)的塊表達(dá)式創(chuàng)建的內(nèi)部視圖。這允許你 區(qū)分 Handlebars 上下文,在 Handlebars 中和在視圖層級(jí)中的工作方式是一樣的。
因?yàn)?view
指向一個(gè) Ember.View
實(shí)例,你可以用 view.propertyName
之類(lèi)的 表達(dá)式訪(fǎng)問(wèn)視圖上的任何屬性。你可以用 view.parentView
訪(fǎng)問(wèn)視圖的父視圖。
例如,想象你有一個(gè)帶有如下屬性的視圖:
- App.MenuItemView = Ember.View.create({
- templateName: 'menu_item_view',
- bulletText: '*'
- });
……和下面的模板:
- {{#with controller}}
- {{view.bulletText}} {{name}}
- {{/with}}
盡管 Handlebars 上下文已經(jīng)變?yōu)楫?dāng)前的控制器,你仍然可以用 view.bulletText
訪(fǎng)問(wèn)視圖的 bulletText
。
模板變量
迄今為止,我們已經(jīng)在 Handlebars 模板中邂逅了 controller
屬性。它是從哪來(lái)的呢?
Ember 中的 Handlebars 上下文可以繼承它們的父上下文中的變量。在 Ember 在當(dāng)前 上下文中查找變量之前,它首先檢查它的模板變量。當(dāng)一個(gè)視圖創(chuàng)建了一個(gè)新的 Handlebars 作用域,它們自動(dòng)繼承它們父作用域的變量。
Ember 定義了這些 view
和 controller
變量,所以當(dāng)一個(gè)表達(dá)式使用 view
或 controller
變量名,它們總是最先被找到。
如上所述,Ember 設(shè)置了 Handlebars 上下文中的 view
變量,無(wú)論何時(shí)模板中使用 了 {{#view}}
輔助標(biāo)記。起初,Ember 把 view
變量設(shè)置為正在渲染模板的視 圖。
Ember 設(shè)置了 Handlebars 上下文中的 controller
變量,無(wú)論已渲染的視圖是否存 在 controller
屬性。如果視圖沒(méi)有 controller
屬性,它從時(shí)間上最近的擁有該 屬性的視圖上繼承 controller
變量。
其它變量
Ember 中的 Handlebars 輔助標(biāo)記也會(huì)指定變量。例如, {{#with controller.person as tom}}
形式指定一個(gè) tom
變量,它的后代作用域 是可訪(fǎng)問(wèn)的。即使一個(gè)子上下文有 tom
屬性,這個(gè) tom
變量會(huì)廢除它。
這個(gè)形式的最大好處是,它允許你簡(jiǎn)寫(xiě)長(zhǎng)路徑,而不喪失對(duì)父作用域的訪(fǎng)問(wèn)權(quán)限。
在 {{#each}}
輔助標(biāo)記中,提供 {{#each person in people}}
形式尤其重要。 在這個(gè)形式中,后代上下文可以訪(fǎng)問(wèn) person
變量,但在模板調(diào)用 each
的地方 保留相同的作用域。
- {{#with controller.preferences}}
- <h1>Title</h1>
- <ul>
- {{#each person in controller.people}}
- {{! prefix here is controller.preferences.prefix }}
- <li>{{prefix}}: {{person.fullName}}</li>
- {{/each}}
- <ul>
- {{/with}}
注意這些變量繼承了 ContainerView
中的那些,即使它們不是 Handlebars 上下文 層級(jí)中的一部分。
從視圖中訪(fǎng)問(wèn)模板變量
在大多數(shù)情況下,你會(huì)需要從模板中訪(fǎng)問(wèn)這些模板變量。在一些不尋常的情景下,你會(huì) 想要在視圖的 JavaScript 代碼中訪(fǎng)問(wèn)范圍內(nèi)的變量。
你可以訪(fǎng)問(wèn)視圖的 templateVariables
屬性來(lái)達(dá)成此目的,它會(huì)返回一個(gè)包含當(dāng)視 圖渲染后存在于其作用于的變量的 JavaScript 對(duì)象。 ContainerView
也可以訪(fǎng)問(wèn) 這個(gè)屬性,它指向時(shí)間上最近的模板依賴(lài)的視圖的模板變量。
目前,你不能觀(guān)察或綁定一個(gè)包含 templateVariables
的路徑。
原文鏈接:http://emberjs.torriacg.org/guides/view_layer/#toc_