一段代码引发的思考

96 阅读4分钟

1. 前导

下面的内容,围绕下面这段代码展开,实现一个简单的计算器

calculation.gif

import $ from 'jquery' // 模块化
const eventBus = $({}) // 事件总线
// mvc
const m = { 
    data: {
        num: parseFloat(localStorage.getItem('app1.num') || 100)
    },
    update(data) {
        Object.assign(m.data, data)
        eventBus.trigger('m:updated')
        localStorage.setItem('app1.num', m.data.num)
    }
}
const v = {
    el: null,
    html: `
    <div>
        <div><span class="number">{num}</span></div>
        <button class="add">+1</button>
        <button class="min">-1</button>
        <button class="mul">*2</button>
        <button class="div">÷2</button>
    </div>
    `,
    init(container) {
        v.el = $(container)
    },
    render() {
        if (v.el.children().length > 0) {
            v.el.empty()
        }
        $(v.html.replace('{num}', m.data.num)).appendTo(v.el)
    }
}
const c = {
    init(container) {
        v.init(container)
        v.render(m.data.num)
        c.toBindEvent()
        eventBus.on('m:updated', () => {
            v.render(m.data.num)
        })
    },
	eventsList: { // 表驱动编程
        'click .add': 'add',
        'click .min': 'min',
        'click .mul': 'mul',
        'click .div': 'div'
    },
    add() {
        m.update({ num: m.data.num + 1 })
    },
    min() {
        m.update({ num: m.data.num - 1 })
    },
    mul() {
        m.update({ num: m.data.num * 2 })
    },
    div() {
        m.update({ num: m.data.num / 2 })
    },
    toBindEvent() {
        for (let event in c.eventsList) {
            let value = c.eventsList[event];
            let spaceIndex = event.indexOf(' ')
            let part1 = event.slice(0, spaceIndex)
            let part2 = event.slice(spaceIndex)
            $(v.el).on(part1, part2, c[value])
        }
    }
}

export default c

2. MVC

MVC 是一种设计模式,实现了视图与功能模块的分离;提高了程序的可维护性、可移植性、可扩展性与可重用性;降低了程序的开发难度

Model --- 数据模型,负责操作数据

View --- 和视图相关的,也就是UI界面展示相关的,描绘的是model的当前状态

Controller --- 剩下的其他逻辑

上述例子,最初的实现如下

import $ from 'jquery'

let num = parseFloat(localStorage.getItem('app1.num') || 100);
$('.number').text(num)
let add = document.selector('.add')
let min = document.selector('.min')
let mul = document.selector('.mul')
let div = document.selector('.div')
add.onclick = () => {
    num += 1;
    $('.number').text(num)
    localStorage.setItem('app1.num', num)
}
min.onclick = () => {
    num -= 1;
    $('.number').text(num)
    localStorage.setItem('app1.num', num)
}
mul.onclick = () => {
    num *= 2;
    $('.number').text(num)
    localStorage.setItem('app1.num', num)
}
div.onclick = () => {
    num /= 2;
    $('.number').text(num)
    localStorage.setItem('app1.num', num)
}

似乎这看起来更简单易懂一些,但是里面出现了很多重复的代码,采用MVC模式后,视图和数据分离,代码耦合性低,当要重新写一个功能模块时,可以以这个为模板去填充。

从上述例子也可以看出,MVC缺点也很明显,对于简单的小型项目而言,不一定需要采用该模式,因为你需要思考,如何划分这三块,以及实现模块间的通信

3. eventBus

eventBus 本质就是发布-订阅模式,达成任意组件间相互通信的作用。在一个地方触发(发布)事件,然后通过事件中心通知所有订阅者(订阅)

eventBus.png

eventBus 提供了 on、off 和 trigger 等 API,on 用于监听事件,trigger 用于触发事件,off移除事件

3.1 eventBus的优缺点

**优点:**简化组件之间的通信方式,实现解耦让业务代码更加简洁

**缺点:**每个事件都必须自定义一个事件类,造成事件类太多,无形中加大了维护成本,大量使用 eventBus 会造成项目难以维护、问题难以定位

可以通过jQuery实现eventBus,因为jQuery自带on、off、trigger等API

const eventBus = $({})

vue实例中提供了onon、emit等方法所以只需要创建一个空的vue实例,在组件中通过on()注册事件,在另外一个组件中通过on()注册事件,在另外一个组件中通过emit()去执行对应的事件并且可以传参来达到组件之间的通讯。

3.2 简单实现eventBus

class EventBus {
    constructor() {
        this.eventsList = {}
    }
    on(key, fn) {
        if (!this.eventsList[key]) {
            this.eventsList[key] = []
        }
        this.eventsList[key].push(fn)
    }
    emit(key, ...args) {
        let arr = this.eventsList[key]
        if (arr) {
            arr.forEach(fn => {
                fn(...args)
            });
        }

    }
    off(key, fn) {
        let arr = this.eventsList[key]
        if (arr) {
            let index = arr.findIndex(f => f === fn)
            if (index >= 0) {
                arr.splice(index, 1)
            }
        }
    }
}

PS:需要给事件分类别,要不然,emit触发事件时,会将事件列表的所有事件都会执行一遍

4. 表驱动编程

表驱动法就是一种编程模式,从表里面查找信息而不使用逻辑语句。事实上,凡是能通过逻辑语句来选择的事物,都可以通过查表来选择。对简单的情况而言,使用逻辑语句更为容易和直白。但随着逻辑链的越来越复杂,查表法也就愈发显得更具吸引力

表驱动只是为了优化复杂的逻辑判断,使其变得更灵活、易扩展。

比如,下面例子中页面需要根据后端返回的结果显示婚姻关系

let marryInfo = [
    { code: '10', value: '未婚' },
    { code: '20', value: '已婚' },
    { code: '30', value: '离异' },
    { code: '40', value: '再婚' },
    { code: '50', value: '丧偶' },
    { code: '60', value: '其他' }
]
let info = marryInfo.find((item) => {
    return item.code === code
})
console.log(info)
document.querySelector('.showInfo').innerText = info.value || '其他'
 

上述就是简单的表驱动编程,将所有条件列出来,通过其中一个条件,找到对应的信息。相对于使用if...else或者switch,代码更简洁点,并且,修改条件时,只需要在marryInfo中修改对应的配置,只修改数据,不修改逻辑,灵活且风险低

5. es6 模块化

在创建JavaScript模块时,export 语句用于从模块中导出实时绑定的函数、对象或原始值,以便其他程序可以通过 import 语句使用它们。

静态的import 语句用于导入模块。无论是否声明了 strict mode ,导入的模块都运行在严格模式下。在浏览器中

  1. 命名导出(每个模块包含任意数量)
export {a,b,c} // 导出该文件中的a,b,c三个对象

import {a,b,c} from 'test.js' // 引入test.js的a,b,c三个对象
  1. 默认导出(没有大括号,因为每个模块只允许有一个默认导出)
export default aa //默认导出aa
import {default as aa} from 'test.js' // 完整写法
import aa from 'test.js' // 简写
  1. 重命名导入导出,在 importexport 语句的大括号中,可以使用 as 关键字跟一个新的名字,来改变在顶级模块中将要使用的功能的标识名字
export {
	userName as uName,
    password as pwd
}
import { uName, pwd } from 'test.js'

export{ userName, password }

import {
    userName as uName,
    password as pwd } from 'test.js'
  1. 整个模块仅为副作用而导入,而不导入模块中的任何内容(接口)

    这将运行模块中的全局代码, 但实际上不导入任何值。

import './app1.js'
  1. 创建模块对象

    如果觉得一个个导入有点麻烦,可以试试将每一个模块功能导入到一个模块功能对象上

import * as Module from '/module.mjs'

这将获取 module.mjs 中所有可用的导出,并使它们可以作为对象模块的成员使用,从而有效地为其提供自己的命名空间

Module.function1()
  1. 动态加载模块

import() 作为函数调用,将其作为参数传递给模块的路径。它返回一个 promise,它用一个模块对象来实现,可以访问该对象的导出,例如

import('/module.js')
.then((module)=>{
    module.function1()
    ...
})