slickgrid 中ajax,slickgrid.js 一种高性能web数据表格组件的探讨
本文將探討一種高性能web數(shù)據(jù)表格組件的實現(xiàn),首先簡單介紹slickgrid這個前人開發(fā)的組件,接著對該組件的設(shè)計和實現(xiàn)思路進(jìn)行討論,最后對該組件的思想進(jìn)行提煉,實現(xiàn)基于原始思想的新組件。
slickgird
slickgrid是一款高性能的web數(shù)據(jù)表格組件,由mleibman于2009年5月提交到github,倉庫地址https://github.com/mleibman/SlickGrid。該組件近幾年已經(jīng)停止開發(fā),最后一次提交是2016年5月,但是之前的提交已經(jīng)是2014年。雖然組件本身的開發(fā)已經(jīng)停止,但是留下了非常豐富的grid組件思想。slickgrid的主要特點(diǎn)如下:
高性能:slickgrid采用了局部渲染的機(jī)制,對于整個數(shù)據(jù)表格而言,真正對用戶有用的,是可視區(qū)域的表格,因此,slickgrid只渲染可視區(qū)域的表格,從而節(jié)省了大量DOM渲染操作,性能上幾乎是目前市面上知名grid組件里面最好的。
DataView:slickgrid里面提供了一個DataView的工具,它獨(dú)立于grid本身,雖然grid本身就可以完成數(shù)據(jù)的展示,但是為了數(shù)據(jù)的更多可操作性,DataView提供了更加豐富的操作空間。同時,這一思想把界面操作和數(shù)據(jù)操作分離開,雖然在現(xiàn)在看來已經(jīng)非常老套,而且代碼層面也很落后,但是在當(dāng)初那個時代,還是非常先進(jìn)的。
語言性:slickgrid并不是完整的UI組件,并沒有提供直接的接口一次性完成所有配置,它具有語言性,也就是提供基礎(chǔ)的語法,你可以用它的語法和素材,不斷按照自己的需要進(jìn)行組合,如果它內(nèi)部沒有實現(xiàn),你可以自己另外寫一套代碼,和它混淆以后使用。總之,它就像一堆積木的塊,構(gòu)建出什么樣的grid功能就看開發(fā)者如何利用它提供的塊組合起來。
當(dāng)然,這些特點(diǎn)并不全是優(yōu)點(diǎn),幾乎每一個點(diǎn)都有可詬病的地方。不過,這種思想是對技術(shù)的一種提煉,已經(jīng)非常難能可貴。
grid的構(gòu)成元素
現(xiàn)在,我們從頭思考,一個數(shù)據(jù)表格,它應(yīng)該由哪些元素構(gòu)成。數(shù)據(jù)表單最基本的元素是單元格,也就是cell,是單點(diǎn)數(shù)據(jù)的展現(xiàn)方式。一個數(shù)據(jù)節(jié)點(diǎn)被存放在一個cell中。cell的父級是行,也就是row。整個數(shù)據(jù)表格就是由很多row堆疊而成。row是數(shù)據(jù)展示的主要部分。但是除此之外,還有表頭,也就是指示列信息的columns信息,它指明了同一行中,相同索引的cell的共同特征。這些是grid中最為基礎(chǔ)的元素。
問題是,就像slickgrid中給人的想象一樣,我們并沒有必要把所有的cell渲染出來,而是只渲染當(dāng)前可視區(qū)域的數(shù)據(jù)。這種思想基于數(shù)據(jù)量很大的假設(shè)。當(dāng)然,即使是小數(shù)據(jù)量,這種設(shè)計也是應(yīng)該兼容的才可以。因此,我們需要設(shè)計一種對可視區(qū)域和渲染的抽象概念。這就是viewport和canvas。
viewport是用于提供給用戶的可視區(qū)域的視口,就像瀏覽器視口一樣,只提供這么大的窗口大小,至于內(nèi)部網(wǎng)頁的實際大小,是無法通過該視口直接觀察到的,必須通過滾動條來感知。而對于數(shù)據(jù)可展開的實際大小,就是canvas的大小。canvas的意思是畫布,也就是說是繪制cells的地方。canvas的實際大小應(yīng)該是假設(shè)所有數(shù)據(jù)一次性繪制完成時所占用的空間大小。當(dāng)然,這里說假設(shè),意思是我們實際上并不一次性繪制完所有數(shù)據(jù),而是只在canvas的viewport遮蓋區(qū)域內(nèi)進(jìn)行繪制。所以它們的關(guān)系是:
grid中viewport和canvas的關(guān)系示意圖
上圖中的陰影部分是無法用肉眼看到的,只有viewport內(nèi)部是可以被看到的。在web開發(fā)中,通過overflow來輕易的實現(xiàn)滾動條,從而可以通過拖動viewport的滾動條查看canvas的每一個位置。
canvas作為gird繪制視圖的對象,grid的cells和columns將會被繪制在canvas中,因此,從這個層面講,canvas雖然只是繪制的載體,但最終包含來grid的基礎(chǔ)元素,因此被視為一個整體。在開發(fā)時,將基礎(chǔ)元素和canvas打包成一個實體,不需要再去考慮內(nèi)部但row和cell。
另一個問題是,columns可能被固定在grid的頂部,滾動視圖時,不會被遮蓋。除了columns,在實際使用中還有一些需求,要求左側(cè)、右側(cè)、頂部、底部固定一些真實的數(shù)據(jù)信息,而不是columns。這樣的需求衍生出另外一個思考,就是需要將完整的grid視圖拆分為不同的區(qū)塊,這些區(qū)塊有的可能被固定在某個位置,但是所有這些區(qū)塊組合起來讓用戶可以看到符合邏輯的完整的grid數(shù)據(jù)信息。而這種拆分,我認(rèn)為用另外一個概念來表達(dá):layout。
layout意思是布局,表示一個grid按照一定的思想被切分,按照特定的規(guī)則把切分開的部分合理組合在一起。為了滿足實際的需求,我將grid layout切分為3x4的12個部分,如下圖:
grid layout示意圖
header部分區(qū)別于neck、body、footer,因為它只用于展示columns信息。它的結(jié)構(gòu)可能在一些細(xì)節(jié)上稍有不同。
為什么要這樣設(shè)計呢?它可以滿足如下需求:
橫向滾動時,left、right可以被fixed,從而實現(xiàn)左側(cè)、右側(cè)的數(shù)據(jù)列固定
縱向滾動時,可以固定header、neck、footer,實現(xiàn)特定的頭、行固定需求
當(dāng)只需要簡單結(jié)構(gòu),不需要固定時,隱藏對應(yīng)區(qū)域即可,例如不需要固定左側(cè),則left所有區(qū)塊hide,不需要footer,hide即可,最為簡單的grid可能僅顯示header-middle和body-middle。
每一個區(qū)塊擁有自己的viewport-canvas結(jié)構(gòu),區(qū)塊本身實際上僅對應(yīng)區(qū)塊本身的數(shù)據(jù)(包括columns)而不包含其它區(qū)塊的數(shù)據(jù)展示,因此實際上grid沒有一個完整的展示所有數(shù)據(jù)的機(jī)會,只有這些區(qū)塊全部正確展示數(shù)據(jù)時,才能正確把grid反應(yīng)給用戶閱讀。
這里提出的grid layout和很多其它的grid組件存在本質(zhì)的區(qū)別,很多組件采用的是fake概念,也就是將表格復(fù)制一份,左右展示。筆者曾經(jīng)使用過dhtmlx,是一個非常笨重的grid組件,它不僅代碼量大,而且無法清晰的解釋如何實現(xiàn)我上述提到的需求。它會告訴你,自己是如何實現(xiàn)左側(cè)固定列的,但是無法告訴你一種通用的固定策略。
最終,一個grid的組成可能是這樣的結(jié)構(gòu):layout->viewport->canvas->rows->cells
滾動事件
我們需要去研究,用戶滾動滾動條的目的,當(dāng)然很簡單,是為了看非可視區(qū)域的可預(yù)判性數(shù)據(jù)。但是當(dāng)我們把整個grid切分為12個區(qū)塊之后,滾動變得有些復(fù)雜,如何簡單有效的去設(shè)計表格的滾動呢?
實際上,header總是被固定的,當(dāng)用戶滾動滾動條時,其實并不希望表頭消失,否則他很難確定可視的數(shù)據(jù)的列信息。而left, right, neck, footer這幾個區(qū)域本身也是為固定設(shè)計的,因此,實際上在滾動時,這幾個區(qū)域根本也不會發(fā)生本質(zhì)的變動,最終,body-middle這個區(qū)域才是滾動條的作用區(qū)域,只有這個區(qū)域是任何滾動事件發(fā)生時都應(yīng)該發(fā)生變化的。而left、right這兩個區(qū)域在左右滾動時保持不變,header、neck、footer這三個區(qū)域在上下滾動時保存不變。通過觀察,我們發(fā)現(xiàn)header-left, neck-left, footer-left, header-right, neck-right, footer-right這些區(qū)域是永久固定不變的。
基于上面的這些觀察,實際上,我們要考慮如何安排滾動條的位置。最終,我將滾動條放在body-right的右側(cè),footer-middle的底部。
當(dāng)右側(cè)的縱向滾動條發(fā)生滾動時,整個body區(qū)域的視圖發(fā)生上下滾動;當(dāng)?shù)撞康臋M向滾動條發(fā)生滾動時,整個middle區(qū)域的視圖發(fā)生左右滾動。整個滾動事件就這么簡單。
在代碼層面,我們需要實現(xiàn)滾動的聯(lián)動效果,也就是滾動滾動條時,其它區(qū)塊的viewport發(fā)生相同的滾動效果,或者在非滾動條區(qū)域,比如在body-middle滾動鼠標(biāo)滾輪時,其它區(qū)域也同時發(fā)生聯(lián)動效果。
數(shù)據(jù)布局
前面所講都是視圖布局,數(shù)據(jù)如何按照視圖布局的需求,進(jìn)行拆分和處理呢?實際上,在web開發(fā)中,數(shù)據(jù)可以僅存在一份,相互之間可以引用。我們更關(guān)心的是,在全部數(shù)據(jù)中,layout里的viewport應(yīng)該獲得哪些數(shù)據(jù)用以渲染,這些數(shù)據(jù)最終決定來該viewport內(nèi)的canvas的大小。這里,我們引入一個新的概念:range。
range表達(dá)了一個數(shù)據(jù)區(qū)域,即從第幾行到第幾行、第幾列到第幾列的數(shù)據(jù)區(qū)域。用代碼表示為{ fromRow, toRow, fromColumn, toColumn }這樣一個對象。
我們需要在兩個地方有明確的range概念:1. 每一個layout區(qū)塊的range,2. canvas的可視區(qū)域的range。
layout的range用以構(gòu)建canvas,而canvas的可視區(qū)域range用以渲染。layout的range完全是用戶傳入的,比如用戶想固定前2列,那么left區(qū)域的range一定有{ fromRow: 0, toRow: 1 },而這兩列的寬度可以通過columns信息中得到,這樣left區(qū)域的寬度就可以確定了。同樣的道理,其它區(qū)域的尺寸也可以通過類似的方法確定,最終剩下的區(qū)域就用總的尺寸去減去已知的區(qū)域尺寸。這樣下來,layout基本就有了固定的尺寸,再將scrollbar考慮進(jìn)去,視圖布局就確定了。viewport尺寸也跟著確定。
canvas的尺寸和layout差不多,但是不同的是,它必須知道完整的layout的range信息,因為canvas是通過真實的數(shù)據(jù)得到尺寸的,所以在傳入的配置信息中,你必須告訴canvas rowHeight信息。如果沒有rowHeight信息,無法得到canvas的尺寸。一般columns里面包含了width信息,所以寬度信息基本是不用擔(dān)心的,如果不存在,那么也必須傳入一個columnWidth信息。
canvas的可視區(qū)域的range則需要viewport把當(dāng)前scroll信息傳遞給canvas,再由canvas利用自己的數(shù)據(jù)信息計算出具體應(yīng)該渲染出哪些行哪些列。而當(dāng)滾動事件發(fā)生時這個計算過程需要反復(fù)發(fā)生,渲染也會反復(fù)發(fā)生,這是不可避免的代價。不過,我們可以做很多緩存的工作。
數(shù)據(jù)驅(qū)動
我們摒棄slicgrid中DataView的概念,轉(zhuǎn)而使用DataDriver的概念。數(shù)據(jù)層和視圖層分離是注定的,對于數(shù)據(jù)的操作會引發(fā)視圖的聯(lián)動效應(yīng),這也是注定的。對于開發(fā)而言,開發(fā)者只需要關(guān)心數(shù)據(jù)如何變得即可。例如新增了一條數(shù)據(jù),修改了一條數(shù)據(jù),刪除了一條數(shù)據(jù)。這些操作僅僅需要在數(shù)據(jù)層完成,完成數(shù)據(jù)操作之后,視圖會自動更新。
這在當(dāng)代開發(fā)中非常用以實現(xiàn),react、vue、redux、mobx都是非常好的思路。問題在于,有沒有必要為了實現(xiàn)數(shù)據(jù)驅(qū)動而去加載其它的庫,增加代碼量?
另外一個話題是,如何設(shè)計數(shù)據(jù)結(jié)構(gòu)。從結(jié)構(gòu)上,我們還是遵循slickgrid的當(dāng)初的設(shè)計,colunms、rows。數(shù)據(jù)視圖采用扁平的rows結(jié)構(gòu),由于是局部渲染,我們必須采用position:absolute絕對定位,無法使用float等布局方法,因此無法用display等css屬性來刪除列之后又顯示出來。這種操作只能通過修改數(shù)據(jù)來做到。因此rows信息包含兩個層面,一個層面是原始數(shù)據(jù),另一個層面是視圖數(shù)據(jù)。grid真正要展示的是視圖數(shù)據(jù),但是原始數(shù)據(jù)是視圖數(shù)據(jù)的根,因此,如何在兩者之間管理好,是一個大問題。
最后還有一個問題,是數(shù)據(jù)節(jié)點(diǎn)的父子關(guān)系,一條數(shù)據(jù)是另外一條數(shù)據(jù)的子節(jié)點(diǎn),這種數(shù)據(jù)結(jié)構(gòu)怎么來表達(dá)?我們采用來扁平的rows結(jié)構(gòu),所以,只能通過增加_parent, _chidren屬性等屬性來表示引用。當(dāng)展開一個帶有children的row時,我們需要想辦法把這些children從原始數(shù)據(jù)中加載到視圖數(shù)據(jù)中。而收起一個parent的時候,也要實現(xiàn)從視圖數(shù)據(jù)中刪除。
總結(jié)
本文最大的創(chuàng)新,是在grid layout上,它非常優(yōu)雅的解決來凍結(jié)行列問題。slickgrid本身并沒有考慮到這個層面,凍結(jié)列是在slickgrid的一個fork中實現(xiàn)的,不是它的核心庫。然而在現(xiàn)代web數(shù)據(jù)表格中,凍結(jié)行列幾乎是必備需求。另外,本文沒有探討的一個問題是編輯問題,也就是用戶可以在線像編輯excel一樣編輯自己的數(shù)據(jù)表。我認(rèn)為,這個話題需要引申出另外一個話題,就是組件的插件/擴(kuò)展機(jī)制,如何在原有功能上實現(xiàn)簡便的擴(kuò)展。
2017-12-11
3943
總結(jié)
以上是生活随笔為你收集整理的slickgrid 中ajax,slickgrid.js 一种高性能web数据表格组件的探讨的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: datatables ajax 数组,d
- 下一篇: ibm z系列服务器 cpu,低调发布: