原文: 來自 D3.js 做者 Mike Bostock 的How Selections Worksjavascript
譯者: ssthousehtml
在前一篇文章中, 我介紹了關於 D3 selection 的基礎, 這些基礎足以讓你開始使用 D3 selection.java
在這篇文章中, 我將介紹 d3-selection 的實現原理. 本文可能須要更長的時間來閱讀, 但它能揭開 selection 的原理 並讓你能真正掌握數據驅動文本的思想(D3的思想)node
本文會介紹 selection 內部的工做原理而不是 selection 的設計動機, 因此你剛開始可能會對爲何使用 selection 這種模式感到疑惑. 但等你讀到本文結尾時, 你天然會明白 selection 如此設計的緣由.git
D3 是一個 用於數據可視化的庫, 因此本文也用可視化的方式, 結合着文字對selection原理進行講解.github
我會用圓角矩形, 好比 thing
表示 JavsScript 的各類對象, 從 object ({foo:16}) 到 基礎數據類型 ("hello"), 到數組 ([1,2,3]) 再到 DOM 元素. 不一樣種類的對象會用不一樣的顏色來區分. 對象之間的關係會用灰色的線來表示, 好比一個包含數字 42 的數組會表示成這樣:api
var array = [42]
大部分狀況下, 圖像對應的代碼會出如今圖片的上方. 你能夠訪問這個網站, 並打開調試窗口對文中的代碼進行試驗, 這樣能幫助你更好的理解本文.數組
如今, 讓咱們開始!微信
可能有人和你說過: selection 就是 DOM 元素組成的數組. 但事實並非這樣, selection 是 array 的子類,這個子類提供了一些操做選中元素的方法 (好比設置屬性: selection.attr, 設置樣式: selection:style
). selection 一樣繼承了 array 的一些方法, 好比 array.forEach
, array.map
. 然而, 你並不會常用這些從 array 繼承來的方法, 由於 D3 提供了一些方便的替代方法(好比 selection.each
). 而且, 有一些 array 的方法爲了符合 selection 的邏輯而被 overridden, 好比 selection.filter
和 selection.sort
.app
另外一個 selection 不是 DOM 元素數組的緣由是: selection 是 group 的數組, 而 group 纔是 DOM 元素的數組. 舉個例子, d3.select
返回的 selection 包含了一個 group, 而這個 group 包含了選中的 body 元素:
var selection = d3.select('body')
在 JavaScript 控制檯, 嘗試運行下面的命令並查看 selection[0] ==> group 和 元素 selectio[0][0]
. 雖然 D3 支持這種經過數組下標訪問元素的方式, 可是你很快就會意識到用 selection.node
會更好.
類似的, d3.selectAll 也會返回一個 group, 這個 group 中會有若干個元素:
d3.selectAll('h2')
d3.select 和 d3.selectAll 都是返回的一個 group. 惟一得到包含多個 group 的 selection 的方法是 selection.selectAll . 好比, 若是你選中全部的 table row, 接着再選中這些 row 的 cell:
d3.selectAll('tr').selectAll('td')
當運行上面代碼的第二個 selectAll 時, 前面 d3.selectAll('tr')
獲得的 selection 中, 每個元素都將變成新 selection 中的一個 group; 每一個 group 都會包含老的元素中符合條件的全部子元素. 因此, 若是 table 中每一個 td 都包含有一個 span 的話, 咱們調用下面的代碼, 會獲得:
d3.selectAll('tr') .selectAll('td') .selectAll('span')
每個 group 都有一個 parentNode 屬性, 這個屬性存儲了 group 中全部元素的父節點. 父節點屬性會在 group 被建立時就被賦值. 所以, 若是你調用 d3.selectAll("tr").selectAll("td")
, 返回的 group 數組, 他們的父節點就是 tr. 而 d3.select 和 d3.selectAll 返回的 group, 他們的父節點就是 html.
一般來講, 你徹底不用在乎 selection 是由 group 組成的這個事實. 當你對 selection 調用 selection.attr 或者 selection.style 的時候, selection 中的全部 group 的全部子元素都會被調用. 而 group 存在的惟一影響是: 你在 selection.attr('attrName', function(data, i))
時, 傳遞的 function(data, i) 中, 第二個參數 i 是元素在 group 中的索引而不是在整個 selection 中的索引.
只有 selectAll
會涉及到 group 元素, select
會保留當前已有的 group. select 方法之因此不一樣, 是由於在老的 selection 中的每一個元素都只會在新的 selection 中對應一個新的元素. 所以 select 操做會直接把數據從父元素傳遞給子元素 (所以也根本沒有 data-join 的過程)
爲了方便使用, append 方法和 insert 方法都被掛載到了 selection 上, 這兩個方法都會自動維護 group 的結構, 而且自動傳遞數據. 好比咱們如今有一個有四個 section 節點的頁面:
d3.selectAll('section')
若是你調用下面的方法, 會爲每個 section 添加一個 p 元素, 你會獲得一個有四個 p 元素的 group:
d3.selectAll('section').append('p')
須要注意的是, 如今這個 selection 的父節點仍然是 html. 由於 selection.selectAll 尚未被調用, 因此父節點沒有發生變化.
group 中能夠保存 Null 元素, 用來聲明元素的缺失. Null 會被大部分的操做所忽略, 好比: D3 會在 selection.attr 和 selection.style 的時候自動忽略 Null 元素.
Null 元素會在 selection.select 沒法找到符合要求的子元素時被建立. 由於 select 方法會維護 group 的結構, 因此它會在缺失元素的地方填上 Null. 好比下面這個例子, 四個 section 中只有兩個有 aside 元素:
d3.selectAll('section').select('aside')
雖然在大部分狀況下, 你徹底能夠忽略 group 中的 Null 元素, 可是記住 Null 元素是確實存在於 group 的結構當中的, 而且他們會在計算 index 時被考慮進來.
data 並非保存在 selection 中的一個屬性, 這一點可能會讓你感到驚訝, 但確實如此. data 並非 selection 的一個屬性, 而是被保存爲 DOM 元素的一個屬性. 這就意味着, 當你使用 selection.data 綁定數據時, 其實數據是被綁定到了 DOM 元素上. data 會被賦值給 DOM 元素的 __data__
屬性. 若是一個 DOM 元素沒有 __data__
屬性, 就代表它沒有被綁定數據. 因此 selection 是臨時性的, 但數據是被持久化在 DOM 裏的, 你能夠從新建立 selection, 而你的 selection 中的 DOM 元素仍會保有它以前被綁定的數據.
數據的綁定能夠經過如下幾種方式實現, 接下來咱們會分別講解這三種方式:
- 給每個單獨的 DOM 元素調用
selection.datum
由於有 selection.datum 方法的存在, 你不須要手動的去給 __data__
屬性賦值, 雖然 selection.datum 內部就是這樣實現的:
document.body.__data__ = 42
使用 D3 的方式來達到一樣的效果:
d3.select('body').datum(42)
- 從父節點中繼承來數據, 好比: append, insert, select
若是咱們如今向 body 中 插入一個 h1 元素, h1 元素就會自動繼承 body 的數據:
d3.select('body') .datum(42) .append('h1')
- 調用 selection.data
最後咱們來看 selection.data , 講解這個方法會引入 d3 中很是重要的 data-join 思想. 但在咱們講解這個思想以前, 咱們須要首先回答一個更加基本的問題: 什麼是數據 ?
在 D3 中, 數據能夠是裝有基礎數據類型數據的數組, 好比下面這個:
var numbers = [4, 5, 18, 23, 42]
或者是對象數組:
var letters = [ { name: 'A', frequency: 0.08167 }, { name: 'B', frequency: 0.01492 }, { name: 'C', frequency: 0.0278 }, { name: 'D', frequency: 0.04253 }, { name: 'E', frequency: 0.12702 } ]
甚至是矩陣(由數組組成的數組):
var matrix = [[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11], [12, 13, 14, 15]]
你能夠經過 selection 來描述數據和可視化圖形之間的關係. 下面咱們來具體講解. 咱們先建立一個有 5 個數字的數組:
就像 selection.style 能夠傳入一個普通的 string (例: "red") 或者傳入一個返回 string 的 function (例: function(d) => d.color
) 同樣, selection.data 也能夠接受這兩種參數.
然而, 和其餘 selection 的方法不一樣, selection.data 是爲每個 group 定義了數據, 而不是爲每個 DOM 元素定義數據: 對於 group 來講, 數據應該是一個數組或者是一個返回數組的 function. 所以, 一個有多個 group 的 selection 其對應的數據也應該是一個包含多個子數組的數組.
上圖中, 藍色的線條表示 data() 方法返回的是多個數組. 你傳入 selection.data() 的 function 會有兩個參數: parentNode 和 groupIndex. 而後咱們根據這兩個參數, 返回對應的數據. 所以,這裏傳入的 function 至關因而持有父級的數據, 而後根據 parentNode 和 groupIndex 將父級數據拆分爲每一個 group 的子級數據.
selection.data(function(parentNode, groupIndex) { return data[groupIndex] })
對於只有一個 group 的 selection, 你能夠直接傳入 group 對應的數組數據便可. 只有當你遇到須要處理多個 group 的狀況時, 你才須要一個 function 來爲不一樣的 group 返回不一樣的數組數據.
如今, 咱們終於能夠開始討論 d3-selection 的核心思想了.
爲了綁定 data 到 DOM 元素, 咱們必須知道哪個數據是對應的哪個 DOM 元素. 這在D3中是經過比較 key 值來實現的. 一個 key 其實就是一個簡單的字符串, 就好比一個名字. 當一個數據和一個 DOM 節點的 key 值相同時, 咱們就認爲這個數據和這個 DOM 元素是綁定的.
最簡單的指定 key 值的方法是使用索引: 第一個數據和第一個 DOM 元素會被賦予 key 值 "0", 第二個會被賦予 "1", 以此類推. 將一個數字數組和一個 key 值匹配的 DOM 元素數組進行 join 操做, 效果如圖所示:
下面的代碼獲得的綁定好數據的 selection:
d3.selectAll('div').data(numbers)
若是你的數據和 DOM 元素的順序剛好相同(或者對順序並不在乎)時, 經過下標索引做爲 key 值是很是方便的. 可是, 一旦數據的順序發生變化, 經過下表索引做爲 key值就變得不可行了. 這時, 你須要手動設置一個 key functon, 將這個 function 做爲第二個參數傳入 selection.data(data, keyFunction)
. 這個 keyFunction 須要根據當前的數據, 返回一個對應的 key 值. 好比, 你有一個對象數組做爲數據. 每一個數據有一個 name 屬性, 你的 key function 就能夠返回數據的 name 屬性, 就像這樣:
var letters = [ { name: 'A', frequency: 0.08167 }, { name: 'B', frequency: 0.01492 }, { name: 'C', frequency: 0.0278 }, { name: 'D', frequency: 0.04253 }, { name: 'E', frequency: 0.12702 } ] function name(d) { return d.name } selection.data(data, name)
一樣的, 如今 DOM 元素和數據完成了綁定.
d3.selectAll('div').data(letters, name)
當有多個 group 時, 上面的狀況會變得更加複雜. 可是不用擔憂, 由於每個 group 會獨立的進行 join 操做. 所以, 你只須要關心如何在一個 group 中保持 key 值的惟一性便可.
上面的例子假設數據和 DOM 元素的數量是剛好 1:1. 那麼當 DOM 元素和數據的數量不相同時呢? 好比有一個 DOM元素 沒有對應 key 的數據, 或者有一個數據沒有對應 key 的 DOM 元素?
當咱們用 key 值來匹配 DOM 元素和數據時, 有三種可能的狀況會出現:
想對應的, selection 也會返回三種狀態的選擇集: selection.data, selection.enter, selection.exit. 假設咱們如今有一個柱狀圖, 柱狀圖有 5 列, 分別對應的 ABCDE 這五個字母. 如今你想將柱狀圖對應的數據從 ABCDE 切換成 YEAOI. 你能夠經過設置一個 key function 來爲此這五個字母和五列柱狀圖之間的關係, 數據轉換的過程如圖: ABCDE ==> YEAOI
其中 A 和 E 是一直都存在的. 因此他們被劃入了 Update 選擇集, 而且順序會切換爲新數據集中的順序, 如圖:
var div = d3.selectAll('div').data(vowels, name)
剩下的 B, C, D 由於在新的數據(YEAOI)中沒有對應的數據, 因此被劃入了 Exit 選擇集. 注意, Exit 選擇集中數據的順序保持原有數據集中的順序, 這個順序會在咱們須要加入移除動畫時頗有幫助.
div.exit()
最後, 新加入的三個字母: Y, O, I 由於沒有對應的 DOM 元素, 因此被劃分到了 Enter 選擇集:
div.enter()
在這三種狀態的選擇集中, Update 和 Exit 都是常規的選擇集, 他們都是 selection 的子類. 而 Enter 不一樣, 由於 Enter 選擇集中的 DOM 元素在 Enter 選擇集建立時還並不存在. Enter 選擇集包含的是 DOM 元素的佔位符而不是真正的 DOM 元素. 這個佔位符其實並無什麼特別的地方, 它就是一個有 __data__
屬性的 普通 JavaScript 對象而已. 當對 Enter 選擇集調用 selection.append 方法時, d3 會進行特殊的處理, 讓新插入的元素插入到 group 的父節點中去, 而且用新插入的元素取代佔位符.
這也就是爲何咱們須要先調用 selection.selectAll 再調用 selection.data : 由於咱們要爲 Enter 選擇集的 group 指定好用於插入新元素的父節點.
一般咱們使用 D3 都會分別的處理:
可是, 對於 Enter 選擇集和 Update 選擇集的操做, 常常會有重複的部分, 好比更新 DOM 元素的座標, 更新 DOM 元素的 style 樣式.
爲了減小這部分冗餘的代碼, selection 提供了 merge 方法, 使用方法以下:
var updateSelection = div div .enter() .append('text') .text(d => d) .merge(updateSelection) .attr('x', function(d, i) { return i * 10 }) .attr('y', 10)
之因此 Enter 選擇集和 Update 選擇集能夠 merge 是由於, div.enter().append('text')後, Enter 中的佔位符已經被真實的 DOM 元素取代, 於是能夠和 Update 選擇集合並操做.
感謝: Anna Powell-Smith, Scott Murray, Nelson Minar, Tom Carden, Shan Carter, Jason Davies, Tom MacWright, John Firebaugh. 感謝大家的審閱和建議幫助本文變的更好.
若是想進一步的學習 d3-selection, 閱讀源代碼是一個不錯的方式. 這裏也列出有一些其餘人的演講和文章, 方便進一步閱讀:
這裏是個人 D3.js 、 數據可視化 的github 地址, 歡迎 start & fork :tada:
郵箱: ssthouse@163.com
微信: