原生 js 實現面對對象版瀑布流

1、一些閒話

  • 做爲一個寫靜態的切圖仔,其實平常工做中根本用不上瀑布流這種小清新,畢竟營銷頁面都是要求 搶眼__、__吸睛__、 __高大上 (文案爸爸說啥都對)。
  • 昨上午閒着沒事看到別人寫的瀑布流的帖子,以爲很好玩的樣子,而後決定上午就寫一個試試。。。因此,今天下午,就來整理下這過程當中的一些思路。

2、需求整理及最終效果

寫代碼以前大概列了一下需求,而後中間又加上了一些其餘功能,最終的需求以下:html

2.1 需求列表

  • 一、但願是用原生 js 代碼,jq 寫多了怕忘了原生;
  • 二、面對對象方式封裝,根據圖片數據渲染頁面;
  • 三、瀑布流部分能夠添加至任意容器元素內,畢竟頁面還會有其餘內容;
  • 四、圖片寬度,行列間距可定義;
  • 五、圖片外容器能夠自定義邊框、陰影等屬性;
  • 六、圖片數據增長後能夠調用方法渲染新增部分,原始部分保存不變;

2.2 最終效果

完整代碼
頁面預覽git

  • 3秒後新增3張圖片

3、代碼實現

3.1 基礎框架

起一個自執行函數,只須要暴露一個 falls 變量,該變量指向一個包含 init 方法的對象;
init 方法有2個參數:
- el:瀑布流的容器的選擇符
- options:其餘參數

ps: 這裏還用到了一個自定義的 extend 方法,用於合併默認屬性以及自定義屬性對象,怕被說兼容性很差就沒有用 ES6 語法,參照 Object.assign 方法,完整代碼裏也有,此處很少介紹;github

var falls = (function () {
    var defaults = {

    };

    var Falls = function (el, options) {

    };

    var prototype = Falls.prototype;

    var init = function (el, options) {
        options = extend([], defaults, options);
        return new Falls(el, options).init();
    };

    return {
        init: init
    }
})();

3.2 基礎屬性

接下來肯定一些能夠定義的屬性:
- width: 圖片(圖片外容器)的寬度
- colSpace: 列間距
- rowSpace: 行間距
- itemClass: 圖片外容器類名,方便修改邊框、陰影等樣式

根據這些屬性,隨手肯定了各項默認值數組

var defaults = {
    width: 220,
    colSpace: 10,
    rowSpace: 10,
    itemClass: '_list-item'
};

3.3 構造函數

定義 Falls 構造函數,最終該構造函數有如下初始屬性(屬性在後面用到再作解釋):
var Falls = function (el, options) {
    this.el = document.querySelector(el);
    this.imgList = options.imgList;
    this.colSpace = options.colSpace;
    this.rowSpace = options.rowSpace;
    this.width = options.width;
    this.itemClass = options.itemClass;
    this.first = true;
    this.startIndex = 0;
    this.callback = [];
    this.loadAll = false;
};

3.4 添加原型方法

在添加原型方法以前:
var prototype = Falls.prototype;

這樣能夠少寫好多字母呢,真棒!!!app

3.4.1 初始化 initialize 方法

prototype.initialize = function () {
    var rootEl = document.createElement('div');

    rootEl.style.margin = '0 auto';
    rootEl.style.position = 'relative';

    this.rootEl = rootEl;
    this.el.appendChild(this.rootEl);
};

這裏定義了根元素 rootEl ,並給它添加了相對定位及 margin 屬性,這樣整個根元素會在容器中水平居中。框架

3.4.2 瀑布流加載 loadFalls 方法

這裏有一些前置屬性及初始邏輯
prototype.loadFalls = function () {
    var wrapWidth = this.el.clientWidth;    // 獲取容器的寬度
    this.colWidth = this.width + this.colSpace;     // 單個圖片加上列間隙須要的寬度
    this.col = Math.floor((wrapWidth + this.colSpace) / this.colWidth); // 獲取圖片列數
    this.rootEl.style.width = this.col * this.colWidth - this.colSpace + 'px';  // 根元素的寬度

    if (this.first) {   // 若是初次渲染,直接執行
        this.storageTop();
        this.addItem();
        this.first = false;
        this.lastCol = this.col;
    } else {    // 非初次渲染,判斷列數是否變化
        if (this.lastCol !== this.col) {
            this.startIndex = 0;    // 列數變化時,所有從新渲染
            this.rootEl.innerHTML = '';     // 清空根元素
            this.storageTop();
            this.addItem();
            this.lastCol = this.col;
        }
    }
};

以代碼空行來拆分:dom

  • 第一部分:__屬性定義__,見註釋。
  • 第二部分:__邏輯部分__函數

    • 若是是第一次渲染,就依次初始化,執行 addItem 添加圖片列表,而後標記 this.first 爲 false,而且記錄當前的列數 this.lastCol
    • 非第一次渲染,只有列數改變時才從新渲染瀑布流,this.startIndex 是圖片數組的標記位,後面會講到。

3.4.3 生成高度列表 storageHeight 方法

prototype.storageTop = function () {
    var topArr = [];
    for (var i = 0; i < this.col; i++) {
        topArr.push({
            left: this.colWidth * i,
            top: 0
        })
    }
    this.topArr = topArr;
};

根據列數生成一個存儲每列下一張圖片 top 及 left 值的數組,top 初始都爲 0 ,left 爲每列的寬度 * 列數;佈局

3.4.4 添加圖片隊列 addItem 方法

prototype.addItem = function () {
    var _this = this,
        maxHeight = 0,
        topArr = this.topArr,
        imgList = this.imgList.slice(_this.startIndex),
        len = topArr.length;

    (function addImg() {
        var current = imgList.shift(),
            top = topArr[0].top,
            index = 0;
        for (var j = 1; j < len; j++) { // 遍歷求出當前最小 top 值,及對應的列數 index
            if (topArr[j].top < top) {
                top = topArr[j].top;
                index = j;
            }
        }

        var item = document.createElement('div');   // 建立圖片包裹元素
        item.style.position = 'absolute';
        item.style.top = top + 'px';
        item.style.left = topArr[index].left + 'px';
        item.style.width = _this.width + 'px';
        item.style.boxSizing = 'border-box';
        item.classList.add(_this.itemClass);

        var img = document.createElement('img');    // 建立圖片元素
        img.style.width = '100%';
        img.src = current.src;
        img.alt = current.alt;

        item.appendChild(img);
        _this.rootEl.appendChild(item);

        img.onload = function () {
            topArr[index].top += item.offsetHeight + _this.rowSpace;  // 新增圖片後更新高度數組
            maxHeight = maxHeight < topArr[index].top ? topArr[index].top : maxHeight;
            _this.rootEl.style.height = maxHeight + 'px';   // 更新容器的高度

            if (imgList.length) {
                addImg();
            } else {
                _this.startIndex = _this.imgList.length;
                if (!_this.callback.length) {
                    _this.loadAll = true;
                } else {
                    _this.callback.shift()();
                }
            }
        };
    })();
};

這一塊有點長,由於有兩處爲 dom 對象添加屬性,主要邏輯以下:優化

  • 一、變量聲明,保存 this ,複製圖片數組至 imgList(由於後面會對數組進行更改);
  • 二、建立 addImg 方法添加單個圖片進根元素,在圖片的 onload 事件裏遞歸 addImg 添加下一張圖片;
只有在圖片加載完成後才能獲取圖片高度,進行 topArr 的更新
  • 三、在 addImg 函數內,首先遍歷出當前最小 top 值,及對應的列數 index;
  • 四、生成圖片容器元素 item ,並添加屬性及暴露的類名 itemClass;
  • 五、生成圖片元素 img ,並添加屬性,圖片寬度100%;
  • 六、依次添加圖片及圖片容器至根元素,注意前後順序;
  • 七、進入 onload 事件內,首先須要更新 topArr 對應序號的 top 值,由於該位置新增了一張圖片
  • 八、求出總高度更新根元素高度(防止根元素後面其餘頁面元素佈局混亂);
  • 九、若是 imgList 內還有數據,遞歸完成圖片添加
  • 十、若是 imgList 無數據:
  • 記錄圖片索引至 startIndex ,新增圖片數據後只需從 startIndex 位置開始添加
  • 檢查回調隊列 callback 內是否有回調事件,若是有,取出第一條進行處理(回調隊列後面解釋)

3.4.5 監聽寬度變化 bindEvent 方法

prototype.bindEvent = function () {     // 經過 resize 事件監聽容器寬度變化
    window.addEventListener('resize', this.loadFalls.bind(this));
};

爲 window 對象的 resize 事件添加監聽,觸發 loadFalls 函數,這裏經過bind修正了方法內部的 this 指向;

3.4.6 生成一個瀑布流實例 init 方法

prototype.init = function () {  //  生成一個瀑布流實例
    this.initialize();
    this.loadFalls();
    this.bindEvent();
    return this;    // 返回實例對象
};

就是依次調用 初始化 添加圖片 綁定事件 三個方法,這裏返回了 this ,用於保存當前實例,下一步會用到;

3.4.7 添加圖片後從新繪製 addImgReload 方法

prototype.addImgReload = function (arr) {
    var _this = this;
    if (this.loadAll) {
        this.imgList = arr;
        this.addItem();
    } else {
        this.callback.push(function () {
            _this.imgList = arr;
            _this.addItem();
        })
    }
};

上面爲了這一步作了不少鋪墊

  • this.loadAll 保存當前圖片隊列是否所有加載完成
  • 若是當前圖片隊列已經加載完成,那就跟新圖片隊列 this.imgList ,繼續加載 this.addItem(),由於已經存儲了 startIndex ,因此會重新增的圖片繼續加載
  • 若是當前圖片隊列還沒加載完成,將更新圖片的任務推動回調隊列 callback ,當前圖片隊列加載完成後會檢測回調隊列,取出更新圖片任務完成,就算有多個圖片更新事件也沒關係, callback 保持先進先出執行順序;

4、總結

  • 彷佛沒有提懶加載
經過 addImgReload 方法分次跟新圖片屬性能夠實現懶加載
  • 圖片數據能夠根據實際擴充,此處只添加了 src 及 alt ,包括超連接能夠修改外層容器 item 或者再套一層;
  • 代碼寫完沒有作優化,有幾段比較長,有空再優化吧,畢竟快下班了
  • 最後,我的能力有限,歡迎大佬補充,謝謝!!!
  • 編輯文章的時候發現了一個坑,圖片加載失敗會阻塞後續圖片,明天改
相關文章
相關標籤/搜索