打個(gè)小廣告:
如果你想獲取更多前端干貨、鵝廠工程師的前端面試指南,
歡迎關(guān)注我的個(gè)人微信公眾號(hào):
前端夜談
以 Join 的方式來(lái)思考D3.js
聲明
原文鏈接: 來(lái)自 D3作者 Mike Bostock https://bost.ocks.org/mike/join/
譯文地址: github repository
如果覺(jué)得還不錯(cuò), 不妨去github給個(gè)star ?
Content
打個(gè)比方, 你想用D3畫一個(gè) 散點(diǎn)圖 , 用每一個(gè)svg的circle元素來(lái)可視化你的數(shù)據(jù). 你會(huì)驚訝的發(fā)現(xiàn): D3居然沒(méi)有直接創(chuàng)建多個(gè)DOM元素的方法! 怎么回事?
當(dāng)然, D3有 append 方法, 你可以用來(lái)創(chuàng)建單個(gè)元素. 比如:
svg.append("circle")
.attr("cx", d.x)
.attr("cy", d.y)
.attr("r", 2.5);
但這只是一個(gè)圓, 如果你想要?jiǎng)?chuàng)建很多個(gè)圓(每一個(gè)圓代表一個(gè)數(shù)據(jù)點(diǎn)). 你可能會(huì)想到用一個(gè)for循環(huán)來(lái)實(shí)現(xiàn) ? 這是非常直觀的想法, 這個(gè)想法并沒(méi)有什么錯(cuò), 但是在這之前不妨看看D3中是如何實(shí)現(xiàn)創(chuàng)建多個(gè)元素的:
svg.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", 2.5);
上面這段代碼完美的實(shí)現(xiàn)了你想要的效果: 為每一個(gè)數(shù)據(jù)點(diǎn)創(chuàng)建了一個(gè) circle, 用數(shù)據(jù)點(diǎn)的 x 和 y 屬性作為circle的坐標(biāo). 但這段代碼里面的 selectAll("circle") 是什么意思? 我們?yōu)槭裁匆?select 我們知道當(dāng)前并不存在的 circle, 還用這個(gè)方法的返回值去創(chuàng)建新的元素?
這段代碼的思想是: 不要告訴D3如何去做, 而是告訴D3你想要的效果. 你想要circle元素和數(shù)據(jù)一一對(duì)應(yīng), 那么你就不應(yīng)該告訴D3去創(chuàng)建circle元素, 而是告訴D3: .selectAll("circle") 得到的circle集合應(yīng)該和 .data(data) 一一對(duì)應(yīng). 這個(gè)思想就叫做 Join.

從上圖中可以看到:
-
數(shù)據(jù)集合和DOM元素集合相交產(chǎn)生了中間的update集合 - 沒(méi)有DOM元素與之對(duì)應(yīng)的Data產(chǎn)生了左邊的
enter集合 (也就是缺失DOM元素) - 同樣的, 所有沒(méi)有數(shù)據(jù)與之對(duì)應(yīng)的DOM元素產(chǎn)生了右邊的
exit集合 (也就意味著這些DOM元素將被移除)
現(xiàn)在我們可以再來(lái)看看上面那段使用 enter-append 模型的代碼了:
- 首先,
svg.selectAll("circle")返回的是一個(gè)空的集合, 因?yàn)楫?dāng)前 svg 容器還是空的. 這里的 svg 是所有后續(xù)創(chuàng)建的 circle元素的父節(jié)點(diǎn). -
svg.selectAll("circle")返回的集合接下來(lái)和data進(jìn)行 Join 操作, 得到的就是我們上面提到的三個(gè)集合:update集合 ,enter集合 ,exit集合. 因?yàn)槌跏紩r(shí) Elements集合(也就是circle集合)是空的, 所以update和exit集合為空, 而enter集合會(huì)自動(dòng)為每一個(gè)新的data元素生成一個(gè)占位符. - 默認(rèn)
.data(data)返回的是update集合, 因?yàn)?update集合為空, 所以我們不對(duì)其進(jìn)行操作, 這里我們調(diào)用.enter()得到enter集合. - 接下來(lái), 對(duì)于
enter集合中的每一個(gè)元素, 我們使用selection.append('circle')(值得注意的是, 對(duì)集合的操作會(huì)被應(yīng)用到集合中的每一個(gè)元素上去). 這樣就為每一個(gè)數(shù)據(jù)點(diǎn)創(chuàng)建了一個(gè)circle(這些circle都在他們的父節(jié)點(diǎn) svg 中)
用 Join 的方式來(lái)思考意味著, 我們要做的事情僅僅是聲明 DOM集合(比如這里的 circle 集合) 和數(shù)據(jù)集合之間的關(guān)系, 并且通過(guò)處理三個(gè)不同狀態(tài)的集合 enter, update , exit 來(lái)描述這種關(guān)系.
你也許會(huì)問(wèn), 為什么要用這種方式來(lái)進(jìn)行我的數(shù)據(jù)可視化工作呢? 好處在哪? 為什么我不直接用for循環(huán)創(chuàng)建所有我想要的元素? 答案是這個(gè)思想確實(shí)是非常有好處的, 它的優(yōu)美之處在于它的概括性. 現(xiàn)在我們的代碼還只是處理了 enter 的部分, 這部分對(duì)于展示靜態(tài)的數(shù)據(jù)已經(jīng)足夠了, 但如果你想進(jìn)行動(dòng)態(tài)的數(shù)據(jù)展示, 這種 Join 的方式將大大簡(jiǎn)化你的工作, 你只需要對(duì) update 和 exit 進(jìn)行很少的操作就能得到你想要的效果. 這也意味著你可以輕松的展示實(shí)時(shí)數(shù)據(jù), 能夠?yàn)橛脩籼砑觿?dòng)態(tài)的交互, 能平滑的切換不同的展示數(shù)據(jù)集.
下面這段代碼展示了對(duì)于 exit 和 update 集合的處理:
var circle = svg.selectAll("circle")
.data(data);
circle.exit().remove();
circle.enter().append("circle")
.attr("r", 2.5)
.merge(circle)
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
無(wú)論什么時(shí)候上面的這段代碼被執(zhí)行, 它都將重新計(jì)算 Join 并且維護(hù)好 DOM元素集合 和 數(shù)據(jù)集合 之間的對(duì)應(yīng)關(guān)系. 如果你的新數(shù)據(jù)集比之前老的數(shù)據(jù)集要小, 多余的DOM元素就會(huì)進(jìn)入 exit 集合, 然后被 remove掉. 如果新的數(shù)據(jù)集比老的大, 那么新的數(shù)據(jù)就將進(jìn)入 enter 集合, 并創(chuàng)建出新的DOM元素. 如果新的數(shù)據(jù)集和老的數(shù)目相同, 那么只有 update 集合會(huì)被更新坐標(biāo).
使用 Join 的思想能讓我們的代碼更加直觀. 你只需要處理好這三種狀態(tài)的集合, 而不需要 if 和 for 來(lái)進(jìn)行復(fù)雜的邏輯判斷. 你只需要描述好你的數(shù)據(jù)集合和DOM集合想要有怎樣的對(duì)應(yīng)關(guān)系.
Join 還讓你可以對(duì)不同狀態(tài)的DOM元素進(jìn)行不同的操作. 比如, 你可以只對(duì) enter 集合進(jìn)行操作, 這樣就不會(huì)每次都對(duì)所有的 DOM元素進(jìn)行更新, 這能顯著的提升你的數(shù)據(jù)可視化作品的渲染效率.
同樣的, 你也可以給指定集合的元素添加動(dòng)畫效果, 比如給 enter 的元素添加放大進(jìn)入的效果:
circle.enter().append("circle")
.attr("r", 0)
.transition()
.attr("r", 2.5);
或者給 exit 的集合添加 縮小隱藏 的效果:
circle.exit().transition()
.attr("r", 0)
.remove();
譯者注:
這里有一個(gè)非常好的實(shí)踐 Join 思想的例子(同樣來(lái)自D3作者), 不妨看看:

這里是我對(duì)這個(gè)例子的實(shí)現(xiàn)(也包括一些其他的案例):
想繼續(xù)了解 D3.js ?
這里是我的 D3.js 、 數(shù)據(jù)可視化 的github 地址, 歡迎 start & fork :tada: