乾貨 - 記錄一次有意義的頁面和代碼優化

爲什麼寫這個文章

不多有時間對於寫過的代碼重構 此次發現重構是真的頗有意思的事情 因此就記錄下來了javascript

modal的fifo應用

集卡屬於互動類型的遊戲,此頁面有9個彈窗,其中有同時出現的5個彈窗的狀況,且若是同時出現必須按照指定順序彈出。
遇到複雜的交互邏輯,數據結構能夠幫助理清思路,抽象邏輯,完成穩定可靠的代碼。在此次交互中,彈框要一個個按照順序彈出,能夠慮有序隊列。可是彈框的彈出和關閉屬於事件。在上一個彈框彈出關閉後,觸發下一個彈框彈出。能夠考慮事件的發佈訂閱。css

隊列圖解

隊列 是一種特殊的線性表,特殊之處在於它只容許在表的前端(front)進行刪除操做,而在表的後端(end)進行插入操做,和棧同樣,隊列是一種操做受限制的線性表。進行插入操做的端稱爲隊尾,進行刪除操做的端稱爲隊首。 隊列的數據元素又稱爲隊列元素。在隊列中插入一個隊列元素稱爲入隊,從隊列中刪除一個隊列元素稱爲出隊。由於隊列只容許在一端插入,在另外一端刪除,因此只有最先進入隊列的元素才能最早從隊列中刪除,故隊列的特性爲 先進先出 (First-In-First-Out,FIFO)

JavaScript 實現隊列

class Queue {
  constructor() {
    this.dataStore = []
  }

  enqueue(e) {
    this.dataStore.push(e)
  }

  dequeue() {
    this.dataStore.shift()
  }

  front() {
    return this.dataStore[0]
  }

  back() {
    return this.dataStore[this.dataStore.length - 1]
  }

  isEmpty() {
    if (this.dataStore.length === 0) {
      return true
    }
    return false
  }

  toString() {
    return this.dataStore.join(",")
  }
}
複製代碼

jest 寫些測試用例驗證前端

import Queue from './utils'
describe('Queue', () => {
  const q = new Queue()
  q.enqueue('string1')
  q.enqueue('test')
  q.enqueue(3)
  test('queue', () => {
    expect(q.toString()).toBe('string1,test,3')
    q.dequeue()
    expect(q.front()).toBe('test')
    expect(q.back()).toBe('3')
    expect(q.toString()).toBe('test,3')
    expect(q.isEmpty()).toBe(false)
    q.dequeue()
    q.dequeue()
    expect(q.isEmpty()).toBe(true)
  })
})
複製代碼

彈窗和隊列結合

隊列的彈窗展現有三個狀態java

  1. 同一時間段只有一個彈窗觸發
  2. 同一時間段有兩個或多個彈窗出發
  3. 一個彈窗在展現過程當中,另外一個彈窗要觸發

彈窗展現時加入隊列的狀態以下node

總體邏輯分析以下圖ajax

import { EventEmitter } from "events"
const emitter = new EventEmitter()
const queue = new Queue()


// 事件中心
export class EventCenter {
  // 添加綁定事件
  static on(type, cb) {
    this[`${type}`] = cb
    emitter.on(type, this[`${type}`])
  }

  // 執行綁定事件回掉
  static emit(type, data) {
    emitter.emit(type, data)
    // 每次回掉後 就接觸綁定
    if (type.indexOf("Close") > -1) {
      this.remove(type)
    }
  }

  // 解除綁定事件
  static remove(type) {
    emitter.removeListener(type, this[`${type}`])
  }

  static eventNames() {
    return emitter.eventNames()
  }
}

// 一個彈窗的隊列
class Queue {
  constructor() {
    this.dataStore = []
  }

  enqueue(e) {
    this.dataStore.push(e)
  }

  dequeue() {
    this.dataStore.shift()
  }

  front() {
    return this.dataStore[0]
  }

  back() {
    return this.dataStore[this.dataStore.length - 1]
  }

  isEmpty() {
    if (this.dataStore.length === 0) {
      return true
    }
    return false
  }

  toString() {
    return this.dataStore.join(",")
  }
}


/**
 * 將彈窗事件名推入隊列
 */
export const push = eventName => {
  if (queue.isEmpty()) {
    queue.enqueue(eventName)
    openDialog() // 啓動出隊邏輯
  } else {
    queue.enqueue(eventName) // 循環中依然能夠同時入隊新的元素
  }
}

/**
 * 打開彈窗,遞歸,循環出隊
 */
const openDialog = () => {
  // 打開彈窗
  EventCenter.emit(queue.front())
  // 監聽彈窗關閉
  EventCenter.on(`${queue.front()}Close`, () => {
    queue.dequeue() // 出隊
    if (!queue.isEmpty()) {
      // 隊列不爲空時,遞歸
      openDialog()
    }
  })
}
複製代碼

用法

import { push } from "./utils"
push(MODAL_TYPE.GIVE_CARD)
push(MODAL_TYPE.ASSIST_CARD)
push(MODAL_TYPE.BUY_CARD)
push(MODAL_TYPE.TASK_CARD)
setTimeOut(()=>{
    push(MODAL_TYPE.RECEIVE_CARD)
},1000)
setTimeOut(()=>{
    push(MODAL_TYPE.ASSIST_CARD)
},4000)
複製代碼

結果

思惟拓展

  1. 能夠加一個彈框的彈出方向。能夠先進先出——默認最早進入數組的彈窗優先級最高,先進後出——默認最後進入數組的彈窗優先級最高。
  2. 這方法還能夠用在toast上。能夠防止toast同一時間的屢次出現。
toast.show('我要第一個出現')
toast.show('我要第二個出現')
toast.show('我要第三個出現')
複製代碼

建立enum管理狀態

遊戲狀態多意味着常量多,好的代碼最好是不寫註釋也一目瞭然。若是能夠把註釋經過代碼表示出來那就太棒了,限制維護者強制書寫註釋那就更好了。後端

實現

// 建立enum 數據
/**  *
 * 數據類型
 * KEY:{
 *  value:string,
 *  desc:string
 * }
 *
 * enum[KEY] => value:string
 * enum.keys() => KEYS: Array<KEY>
 * enum.values() => values: Array<value>
 *
 */
export const createEnum = enumObj => {
  const keys = target => Object.keys(target) || []
  const values = target =>
    (Object.keys(target) || []).map(key => {
      if (
        typeof target[key] === "object" &&
        target[key].hasOwnProperty("value")
      ) {
        return target[key].value
      } else if (
        typeof target[key] === "object" &&
        !target[key].hasOwnProperty("value")
      ) {
        return key
      }
    })

  const handler = {
    get: function(target, key) {
      switch (key) {
        // keys 獲取key => Array<key:string>
        case "keys":
          return () => keys(target)
        // values Array<key:string> || Array<Object<key:string,value:string>>
        case "values":
          return () => values(target)
        // 獲取 詳細描述 descs TODO
        // [key,[value]] 鍵值對 entries TODO
        // 合併 assign TODO
        default:
          break
      }

      if (target[key]) {
        if (
          typeof target[key] === "object" &&
          target[key].hasOwnProperty("value")
        ) {
          return target[key].value
        } else if (
          typeof target[key] === "object" &&
          !target[key].hasOwnProperty("value")
        ) {
          return key
        }
        return target[key]
      }
      throw new Error(`No such enumerator: ${key}`)
    },
  }

  return new Proxy(enumObj, handler)
}
複製代碼

使用

export const MODAL_TYPE = createEnum({
  GIVE_CARD: {
    value: "giveCard",
    desc: "天降彈窗",
  },
  ASSIST_CARD: {
    value: "assistCard",
    desc: "助力彈窗",
  },
  BUY_CARD: {
    value: "buyCard",
    desc: "下單彈窗",
  },
  TASK_CARD: {
    value: "taskCard",
    desc: "任務彈窗",
  },
  RECEIVE_CARD: {
    value: "receiveCard",
    desc: "收卡彈窗",
  },
  SEND_CARD: {
    value: "sendCard",
    desc: "送卡彈窗",
  },
})
複製代碼

代碼抽離和模塊劃分

優化後的代碼index.js減小了500行左右的代碼
個人處理是

  1. 先進行模塊劃分,把this.renderXXX裏的代碼放入components裏讓總體邏輯分離更清晰,把只和子組建相關的邏輯所有移除到子組建中
  2. 首頁的ajax多個請求合併,把index.js和store內都會用到的請求都組合再一塊兒,對總體頁面init的邏輯整合
  3. 此時再整理首頁裏面的各類 狀態控制鎖。將狀態控制鎖放入constant裏。只須要根據不一樣狀態進入不一樣的組建便可,進行總體的邏輯整合。由於原來的邏輯是 user的狀態有4中,活動的狀態有4中。這樣的排列組合就有16中。須要把這麼多排列組合分別在首頁邏輯中判斷是很複雜的。這裏能夠考慮根據展現結果判斷。一樣的展現結果對應哪幾種user和activity狀態判斷組合在一塊兒。後期增長遊戲狀態時候,只須要再加一個狀態和對應的組建便可,也無懼user和活動狀態的多變性
  4. 分離出ui組建和容器組建
  • UI組件:只負責頁面的渲染
  • 容器組件:只負責業務邏輯和數據的處理,要保證組建功能的完整性,與父組建的交互只需給出回調的callback。與父組建無關的邏輯就封閉起來。
  1. 對以爲已經不知足後期維護需求,承載負荷太重的代碼進行重構。此時代碼已經解耦,重構起來會是一件快樂和容易的事情
  2. 對css,動畫等進行優化
  3. 刪除重複和多餘的代碼
  4. 分析頁面性能,進行具體調整

nav點擊位置記錄

思路:用戶點擊哪一個nav,記錄當前scrollTop。等待下次切換到對應邏輯時候就設置對應scrollTop 這裏面的變量只有 scrollTop 和nav的index,且與ui和數據無關。有本身單獨的數據存儲,對外界無依賴。因此能夠抽離出來。數組

// 判斷點擊是否須要吸頂  不設置爲單例 因此記得unmount的時候銷燬
// pravite
// 1. 緩存舊的 scrolltop ✅
// 2. 設置當前點擊 active 的 scrolltop ✅
// 3. 判斷是否吸頂
// 4. 設置列表的height min-height 爲 100vh

// public
// 1. getScrollTop
// 2. onChange
// 3. setMinScrollTop
export class CacheListScrollTop {
	constructor(...rest) {
		this._init(...rest)
	}
	_init(minScrollTop, activeIndex, node) {
		this.minScrollTop = minScrollTop
		this.shouldSetMinScrollTop = false
		this.activeIndex = activeIndex || 0 // 設置起始值
		this.node = node
		this.scrollTop = new Map()
		this.DIST = 1
	}

	// 保存scrolltop
	_cachePreviousScrollTop() {
		const prevoisActiveIndex = this.activeIndex
		const body = document.documentElement.scrollTop
			? document.documentElement
			: document.body
		const node = this.node || body
		const prevoisTop = Math.abs(node.getBoundingClientRect().top)
		const scrollTop = new Map(this.scrollTop)
		scrollTop.set(prevoisActiveIndex, prevoisTop)
		this.scrollTop = scrollTop
	}
	_setNextScrollTop(index) {
		this._cachePreviousScrollTop()
		this.prevoisIndex = this.activeIndex
		this.activeIndex = Number(index)
		const activeNavScrollTop = this.scrollTop.get(Number(index)) || 0

		// 設置最小值 scrollTop <= 最小高度 => 設置最小吸頂量
		if (
			activeNavScrollTop <= this.minScrollTop &&
			this.shouldSetMinScrollTop
		) {
			this._setScrollTop(this.minScrollTop + this.DIST)
			return false
		}

		// 設置最小值 scrollTop > 最小高度  => 設置 scrollTop
		if (
			activeNavScrollTop > this.minScrollTop &&
			this.shouldSetMinScrollTop
		) {
			this._setScrollTop(activeNavScrollTop)
			return false
		}
		// 不設置最小值  統一設置 scrollTop
		const body = document.documentElement.scrollTop
			? document.documentElement
			: document.body
		const node = this.node || body
		const prevoisTop = Math.abs(node.getBoundingClientRect().top)
		const keys = this.scrollTop.keys()
		const scrollTop = new Map()
		;([ ...keys ] || []).forEach((key) => {
			scrollTop.set(key, prevoisTop)
		})
		this.scrollTop = scrollTop
		this._setScrollTop(prevoisTop)
		return false
	}
	_setScrollTop(scrollTop) {
		if (this.node) {
			this.node.scrollTop = scrollTop
			return
		}
		document.documentElement.scrollTop = scrollTop
		document.body.scrollTop = scrollTop
	}

	// 獲取scrollTop 集合
	get getScrollTop() {
		return this.scrollTop
	}

	// scrollTop是否設置 最小值
	setMinScrollTop(shouldSetMinScrollTop ) {
		this.shouldSetMinScrollTop = shouldSetMinScrollTop 
	}

	onChange(index) {
		this._setNextScrollTop(index)
	}
}
複製代碼

優化結果對比

此結果對比都是本地跑服務後 使用election截屏工具後保存緩存

截屏數據對比

優化前 白屏數量 優化後 白屏圖片數量 優化前 資源加載完成 優化後 資源加載完成
8 6 10 8
7 6 9 8
5 5 9 7
11 6 13 8
10 7 12 9
6 7 8 9
7 6 9 8
9 9 11 11
7 9 9 11
8 6 10 8

數據取平均數對比

優化前 白屏平均數 優化後 白屏平均數 優化前 資源加載平均數 優化後 資源加載平均數
7.5 6.7 10 7.8

很明顯看到 資源加載速度改變 收屏加載速度也有減小bash

reference

  1. FIFO和LIFO自動管理modal控制器
  2. 隊列在前端彈窗中的應用

歡迎投簡歷:郵箱:577281375@qq.com

  1. 上海innotech 集團 旗下:趣鍵盤/趣頭條/萌推等
  2. 上海拼多多
相關文章
相關標籤/搜索