干货 - 记录一次有意义的页面和代码优化

为什么写这个文章

不多有时间对于写过的代码重构 此次发现重构是真的颇有意思的事情 因此就记录下来了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. 上海拼多多
相关文章
相关标签/搜索