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