目录 §

  • 1. 浅析 MVC §
    • 1.1. MVC是什么 §
    • 1.2. 表驱动编程 §
    • 1.3. EventBus §
    • 1.4. 理解模块化 §
  • 2. 模块化文件导入(CSS jQuery) §
  • 3. 四个需求描述 §
  • 4. 实现模块 §
  • 5. 抽象思维1:最小知识原则 js单一模块化 §
  • 6. 抽象思维2: 以不变应万变 封装成mvc §
  • 7. 抽象思维3 用数据去渲染 view = render(data) §
  • 8. 抽象思维4 表驱动编程 自动绑定事件 §
  • 9. 抽象思维5 俯瞰全局 对象间通信 EventBus §
  • 10. 第二个模块的使用MVC §
  • 11. 抽象思维6 事不过三 类优化代码继承 §
  • 12. 合并vc §
  • 13. M V 继承EventBus §
  • 14. 过渡到 vue §
  • 15. 总结 §

1. 浅析 MVC §

1.1 MVC是什么 §

什么是重复

代码级别的重复

  • 三行代码写了两遍
  • 就需要重构
  • 函数封装
  • 组件封装
  • 库封装
  • 造轮子

页面级别

  • 类似的页面做了10遍
  • 需要一个万金油写法

MVC就是万金油

  • 所有页面都可以使用MVC优化代码结构
  • 避免意大利面条式代码

MVC 的定义并不明确 MVC三个对象 分别做什么

  • 每个模块都可以写成三个对象,分别是M、V、C
  • M - Model(数据模型)负责操作所有数据相关的任务
  • V - View(视图)负责所有UI(用户)界面
  • C -Controller(控制器)负责监听用户事件,然后调用 M 和 V 更新数据和视图
  • 给出伪代码示例
1
2
3
const m = {data}
const v = {el, html, render}
const c = {init}

MVC 乱弹

  • react MV* 单向绑定 函数式Hooks
  • vue3 函数式 TS 单文件模板
  • Angular2 JS Dart TS

1.2. 表驱动编程 §

表指的是hash表 hashTable

  • 提高代码可读性 易维护
  • 减少重复代码
  • 可扩展性
  • 可重用性
  • 主干清晰
  • 恒定复杂度
  • 隔离变化
  • 机制和策略的分离

相对于程序逻辑,处理数据更易懂

  • 将设计的复杂度从程序代码转移至数据

参考

1.3. EventBus API §

给出伪代码示例

1.4. 理解模块化 §


2.四个需求描述 §

  • 验证展示MVC
  • 模块
  • 四个需求:1. 加减乘除 2. Tab切换 3. 动画 4. 变色

3.模块化文件导入(CSS jQuery) §

模块化 脚本化加载CSS

  • 在项目目录下打开终端

yarn初始化项目

1
yarn init -y

npm/yarn install

1
2
3
yarn add jquery@3.5.1
# yarn add jquery
# npm i jquery
  • 注意包的名字都是小写,大写会导致安装错误或版本错误
  • npm info <name> versions命令可以查看包的所有历史版本号

修改 package.json

1
2
3
4
5
6
7
8
{
  "devDependencies": {
    "sass": "^1.26.9"
  },
  "scripts": {
    "build": "rm -rf dist && parcel build src/index.html --no-minify --public-url ./"
  }
}

发布

1
yarn build src/index.html

开发

1
parcel src/index.html

main.js

1
2
3
4
5
6
import './app1.css'
import './app2.css'
import './app1.js'
import './app2.js'
import $ from 'jquery'
// console.log($)

4.实现模块 §

实现前两个模块

  • 面条代码

index.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<body>
  <div class="page">
    <section id="app1" class="app1">
      <div class="output">
        <span id="number">100</span>
      </div>
      <div class="actions">
        <button id="add1">+1</button>
        <button id="minus1">-1</button>
        <button id="mul2">×2</button>
        <button id="divide2">÷2</button>
      </div>
    </section>
    <section id="app2" class="app2">
      <ol class="tab-bar">
        <li><span>111111</span></li>
        <li><span>222222</span></li>
      </ol>
      <ol class="tab-content">
        <li>内容1</li>
        <li>内容2</li>
      </ol>
    </section>
    <section id="app3" class="app3"></section>
    <section id="app4" class="app4"></section>
  </div>

  <script src="./main.js"></script>
</body>

添加数据保存功能

当用户刷新时保存数据

  • 获取localStorage.getItem
1
2
3
4
5
const n = localStorage.getItem('n');
$number.text(n || 100)

/* 设置 */
localStorage.setItem('n', n)

app1.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 导入文件
import $ from 'jquery'
// 需要的元素
const $button1 = $('#add1')
const $button2 = $('#minus1')
const $button3 = $('#mul2')
const $button4 = $('#divide2')
const $number = $('#number')
// 初始化数据
const n = localStorage.getItem('n');
// 将数据渲染到页面
$number.text(n || 100)
// 绑定鼠标事件
$button1.on('click', () => {
  let n = parseInt($number.text())
  n += 1
  localStorage.setItem('n', n)
  $number.text(n)
})
$button2.on('click', () => {
  let n = parseInt($number.text())
  n -= 1
  localStorage.setItem('n', n)
  $number.text(n)
})
$button3.on('click', () => {
  let n = parseInt($number.text())
  n *= 2
  localStorage.setItem('n', n)
  $number.text(n)
})
$button4.on('click', () => {
  let n = parseInt($number.text())
  n /= 2
  localStorage.setItem('n', n)
  $number.text(n)
})
$recovery.on('click', () => {
  $number.text(100)
  localStorage.setItem('n', n)
})

app1.scss

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
$whitesmoke: #f5f5f5;
$gold: #ffd700;
$orange: #ff4500;

.app1 {
  background-color: $whitesmoke;

  .output {
    font-size: 20px;
  }

  .actions {
    font-size: 20px;
  }
}
  • 注意宽度为17px(14~19px)左右的scroll bar
  • 使用事件委托(jQuery):$.on('click', () => {})
  • 禁止使用逻辑与样式耦合的jQuery代码: $.css({//...}) $.show() $.hide()
  • 使用添加/去除 样式的API:addClass('active') removeClass('active')
  • 基于 样式和行为分离的 思想:JS只管功能不管具体样式,而CSS只管具体样式不管功能

app2.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// 导入文件
import $ from 'jquery'
// console.log($)
// 需要的元素
const $tabBar = $('.app2 .tab-bar')
const $tabContent = $('.app2 .tab-content')
// 绑定鼠标事件
$tabBar.on('click', 'li', (e) => {
  // console.log(e.target) // 点击时可能获取到目标元素的子元素 改成用 e.currentTarget
  // console.log(e.currentTarget) // DOM 元素难用 换成用jQuery元素 封装$li
  const $li = $(e.currentTarget)
  const index = $li.index()
  // console.log(index)

  /*
    // 逻辑与样式耦合的代码1
    $tabContent.children()
      .eq(index).css({ display: 'block' })
      .siblings().css({
        display: 'none'
      })

    // 逻辑与样式耦合的代码2
    $tabContent.children()
      .eq(index).show()
      .siblings().hide()
   */

  // 行为与样式分离1 // 背景色切换
  $li.addClass('selected')
    .siblings()
    .removeClass('selected')

  // 行为与样式分离2 // 内容的显示与隐藏
  $tabContent
    .children()
    .eq(index)
    .addClass('active')
    .siblings()
    .removeClass('active')

})

// 帮你点击 代替在HTML标签中设置样式属性 'selected' 'active'
$tabBar.children().eq(0).trigger('click')

app2.scss

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
$temp-color: #8a2be2;
$shit-yellow: rgb(253, 231, 85);
$dirty-snow: #f4f4f4;

.app2 {

  height: 50vh;
  width: 50vw;

  .tab-bar {
    display: flex;

    li {
      border: 1px solid $temp-color;
      width: 50%;

      &.selected {
        background-color: $shit-yellow;
        color: $dirty-snow;
      }
    }
  }

  .tab-content {
    li {
      display: none;

      &.active {
        display: block;
      }

      &.visibility {
        visibility: visible;
      }
    }
  }
}

简化导入代码 import

  • JavaScript modules 模块 MDN
  • 使用 export default x 将一个变量默认导出给外部使用
  • 使用 import from './xxx.js' 引用另一个模块导出的默认变量
  • 使用 import {x} from './xxx.js' 引用另一个模块导出的名为 x 的变量

在app各自的js代码中引入css代码

  • CSS的@import,性能低,一般不使用

实现后两个模块

  • 不要用JS操作样式

  • 面条代码

main.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<section id="app3" class="app3">
      <div class="square"></div>
</section>
<section id="app4" class="app4">
  <div class="circle">
    <div class="heart">
      <span class="heart-L"></span>
      <span class="heart-R"></span>
      <span class="heart square"></span>
    </div>
    <div class="shadow"></div>
  </div>
</section>

app3.scss

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
$dirty-snow: #f4f4f4;
$shit-yellow: rgb(253, 231, 85);
$orange: #ff4500;

.app3 {

  .square {
    background-color: $shit-yellow;
    border: 1px solid $orange;
    height: 10vw;
    margin-left: 10vw;
    margin-top: 10vw;
    transition: all 1s;
    width: 10vw;

    &.active {
      background-color: mix($shit-yellow, $orange, $weight: .5);
      border: 1px solid $dirty-snow;
      transform: translateX(15vw);
    }
  }
}

app3.js

1
2
3
4
5
6
7
8
9
// 导入文件
import $ from 'jquery'
import './app3.css'
// 需要的元素
const $square = $('#app3 .square')
// 绑定鼠标事件
$square.on('click', () => {
  $square.toggleClass('active')
})

app4.scss

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// ----- Base ----- //

$shit-yellow: rgb(253, 231, 85);
$bg-color: #fdfdfd;
$fg-color: #f23f5d;

// ----- Animation ----- //

$anim-duration: 2.5s;
$anim-timing-function: cubic-bezier(.75, 0, .5, 1);
$anim-iteration-count: infinite;
$anim-direction: normal;

// ----- @mixin ----- //

@mixin animation($name) {
  animation: $name $anim-duration $anim-timing-function $anim-iteration-count $anim-direction;
}

@mixin blue {
  background-color: $fg-color;
  height: 1em;
  width: 1em;
}

// ----- Style ----- //
#app4 {
  &.app4 {
    align-items: center;
    display: grid;
    justify-items: center;
    position: relative;
  }

  .circle {
    border: 1px solid $fg-color;
    border-radius: 50%;
    height: 20vw;
    position: relative;
    width: 20vw;

    &.active {
      animation: change 1s infinite both linear;
    }

    .heart {
      @include animation(heart);
      left: 50%;
      position: absolute;
      top: 50%;
    }
  }
}

// ----- Animations ----- //
@keyframes change {
  0% {
    background-color: $shit-yellow;
    transform: scale(.5);
  }

  100% {
    background-color: $fg-color;
    transform: scale(.8);
  }
}

app4.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 导入文件
import $ from 'jquery'
import './app4.css'
// 需要的元素
const $circle = $('#app4 .circle')
// 绑定鼠标事件
$circle.on('mouseenter', () => {
  $circle.addClass('active')
}).on('mouseleave', () => {
  $circle.removeClass('active')
})

添加app2 app3数据保存功能

当用户刷新时保存数据

  • 添加localStorage

app2.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// 导入文件
import $ from 'jquery'
import './app2.css'
// 需要的元素
const $tabBar = $('#app2 .tab-bar')
const $tabContent = $('#app2 .tab-content')
// 初始化数据 read localStorage tab status
const localKey = 'app2.index'
// 保底值
// let index = localStorage.getItem(localKey) || 0
let index = localStorage.getItem(localKey) ?? 0

// 绑定鼠标事件 事件委托
$tabBar.on('click', 'li', (e) => {
  // console.log(e.target) // 点击时可能获取到目标元素的子元素 改成用 e.currentTarget
  // console.log(e.currentTarget) // DOM 元素难用 换成用jQuery元素 封装$li
  const $li = $(e.currentTarget)
  const index = $li.index()
  // console.log(index)

  localStorage.setItem(localKey, index) // write localStorage tab status

  /*
    // 逻辑与样式耦合的代码1
    $tabContent.children()
      .eq(index).css({ display: 'block' })
      .siblings().css({
        display: 'none'
      })

    // 逻辑与样式耦合的代码2
    $tabContent.children()
      .eq(index).show()
      .siblings().hide()
   */

  // 行为与样式分离1 // 背景色切换
  $li.addClass('selected')
    .siblings()
    .removeClass('selected')

  // 行为与样式分离2 // 内容的显示与隐藏
  $tabContent
    .children()
    .eq(index)
    .addClass('active')
    .siblings()
    .removeClass('active')

})
// 帮你点击 代替在HTML标签中设置样式属性 'selected' 'active'
// $tabBar.children().eq(0).trigger('click')
$tabBar.children().eq(index).trigger('click')

app3.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 导入文件
import $ from 'jquery'
import './app3.css'
// 需要的元素
const $square = $('#app3 .square')
const localKey = 'app3.active'
// 初始化数据
// 布尔值 判断是否点击了  no undefined / yes
const active = localStorage.getItem(localKey) === 'yes'
/*

if(active) {
  $square.addClass('active')
} else {
  $square.removeClass('active')
}
*/
// 简化成
$square.toggleClass('active', active)
// 绑定鼠标事件
$square.on('click', () => {
  if ($square.hasClass('active')) {
    $square.removeClass('active')
    localStorage.setItem(localKey, 'no')
  } else {
    $square.addClass('active')
    localStorage.setItem(localKey, 'yes')
  }
  // $square.toggleClass('active')
})

webStorm报错

  • localStorage.setItem('app3.active', false) 参数类型是布尔值,
  • 不应赋值给string,而setItem的取值只支持字符串
  • 虽然会进行隐式转换类型为字符串
  • 使用getItem取出的也是字符串'false',不严谨
  • localStorage.setItem('app3.active', 'no')代替

5.抽象思维1 最小知识原则 js单一模块化 §

  • 引入一个模块需要引入html、css、js
  • 引入一个模块需要引入html、js
  • 引入一个模块需要引入js
  • 需要的越少越好

代价

  • 会使得页面一开始是空白的,没内容样式:白屏不是bug,而是由于浏览器的渲染机制
  • 解决方案有加菊花gif loading、加骨架(掘金)、加占位内容(文章标题)等
  • 或用SSR {{app1}} {{app2}} {{app3}} {{app4}},大炮打蚊子

main.js

1
2
3
4
5
6
import './reset.css'
import './global.css'
import './app1.js'
import './app2.js'
import './app3.js'
import './app4.js'
  • ES6字符串模板
  • 将不同模块的html标签变成字符串 放入js文件中
  • 原来的index.html里只保留空的<div class="page"></div>

app1.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// 初始化html并加到页面里
const html = `
  <section id="app1" class="app1">
    <div class="output">
      <span id="number">100</span>
    </div>
    <div class="actions">
      <button id="add1">+1</button>
      <button id="minus1">-1</button>
      <button id="mul2">×2</button>
      <button id="divide2">÷2</button>
      <button id="recovery">恢复</button>
    </div>
  </section>
`
// 用jQuery方法见字符串变为HTML标签
const element = $(html)
        .prependTo($('body>.page'))
    //  .prependTo(document.body) 相当于.prependTo($('body'))

app2.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 初始化html并加到页面里
const html = `
  <section id="app2" class="app2">
    <ol class="tab-bar">
      <li><span>111111</span></li>
      <li><span>222222</span></li>
    </ol>
    <ol class="tab-content">
      <li>内容1</li>
      <li>内容2</li>
    </ol>
  </section>
`
const $element = $(html)
                  .appendTo($('body>.page'))

app3.js

1
2
3
4
5
6
7
8
// 初始化html并加到页面里
const html = `
  <section id="app3" class="app3">
    <div class="square"></div>
  </section>
`
const $element = $(html)
                  .appendTo($('body>.page'))

app4.js

1
2
3
4
5
6
7
const html = `
  <section id="app4" class="app4">
    <div class="circle"></div>
  </section>
`
const $element = $(html)
                  .appendTo($('body>.page'))
  • 使用每个模块不用关心HTML是怎样的
  • 注意.prependTo($('body>.page'))webStorm中智能显示的语法高亮,完胜VSCode
  • 又有重复const $element = $(html).appendTo($('body>.page'))
  • 之后再重构插入节点

小结

  • index.html只有空的结构
  • 引入main.jsscript标签
  • main.js里依次引入四个模块,不具体操作各个模块内容
  • 面条代码 -> 模块代码
  • 代价是网速慢时,加载的页面空白(chrome模拟slow 3G网速)

6.抽象思维2 以不变应万变 封装成mvc三个对象 §

  • 每个模块都可以用 m + v + c 搞定
  • 每个模块不用再考虑类似的需求了?

代价

  • 有多余的代码
  • 特殊情况:无html的模块

每个模块都有重复的思路

1
2
3
4
5
6
// 导入文件
// 初始化html并加到页面里
// 需要的元素
// 初始化数据
// 将数据渲染到页面
// 绑定鼠标事件

  • 数据相关放到 m
  • 视图相关放到 v
  • 其他相关放到 c
1
2
3
4
5
6
const v = {
    html: ``,
    render() {},
}
// 第一次渲染html
v.render()

放在 v 还是放在 c

  • 按目的 -> 是否在页面上看得见
  • 比如既不是数据,也不是看得见的放到 c 里

~vim 重复操作快捷键 . 操作


app1.js封装成 mvc 三个对象

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
/* 有bug */
import $ from 'jquery'
import './app1.css'
/* 数据相关放到 m */
const m = {
  // 初始化数据
  data: {
    n: localStorage.getItem('n')
  }
}
/* 视图相关放到 v */
const v = {
// 初始化html
  html: `
  <section id="app1" class="app1">
    <div class="output">
      <span id="number">{{number}}</span>
    </div>
    <div class="actions">
      <button id="add1">+1</button>
      <button id="minus1">-1</button>
      <button id="mul2">×2</button>
      <button id="divide2">÷2</button>
      <button id="recovery">恢复</button>
    </div>
  </section>
  `,
  update() {
    // 将数据渲染到页面
    c.ui.number.text(m.data.n ?? 100) // c.ui.number.text(m.data.n  || 100)
  },
  render() {
    // 用jQuery方法见字符串变为HTML标签
    const element = $(v.html)
      .prependTo($('body>.page'))
  },
}
/* 其他相关放到 c */
console.log("$('#add1')")
debugger
console.log($('#add1')) // null 空对象
const c = {
  ui: {
  // 需要的元素
    button1: $('#add1'),
    button2: $('#minus1'),
    button3: $('#mul2'),
    button4: $('#divide2'),
    number: $('#number'),
    recovery: $('#recovery')
  },
  bindEvents() {
    // console.log('bindEvents 执行了')
    console.log(c.ui.button1) //
    c.ui.button1.on('click', () => {
      let n = parseInt(c.ui.number.text())
      n += 1
      localStorage.setItem('n', n.toString())
      c.ui.number.text(n)
    })
    c.ui.button2.on('click', () => {
      let n = parseInt(c.ui.number.text())
      n -= 1
      localStorage.setItem('n', n.toString())
      c.ui.number.text(n)
    })
    c.ui.button3.on('click', () => {
      let n = parseInt(c.ui.number.text())
      n *= 2
      localStorage.setItem('n', n.toString())
      c.ui.number.text(n)
    })
    c.ui.button4.on('click', () => {
      let n = parseInt(c.ui.number.text())
      n /= 2
      localStorage.setItem('n', n.toString())
      c.ui.number.text(n)
    })
    c.ui.recovery.on('click', () => {
      c.ui.number.text('100')
      let n = parseInt(c.ui.number.text())
      n = 100
      localStorage.setItem('n', n.toString())
    })
  }
}
// 第一次渲染html
v.render()
c.bindEvents()
  • 一个缺点是从页面视图里拿数据let n = parseInt(c.ui.number.text())
  • 而不是数据模块m.data.n

点击无效,未成功绑定事件

debugger

  • 在点击事件里打印查看是否运行到bindEvents()
  • 执行了再看是否存在c.ui.button1
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// ..
,
bindEvents() {
    console.log('bindEvents 执行了')
    debugger
    console.log(c.ui.button1)
    c.ui.button1.on('click', () => {
    // debugger
      // ...
    })
}

c.ui.button1是个空对象

  • 说明未定义c.ui.button1
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
console.log("$('#add1')")
debugger
console.log($('#add1')) // null
const c = {
    ui: {
        // 需要的元素
        button1: $('#add1'),
        button2: $('#minus1'),
        button3: $('#mul2'),
        button4: $('#divide2'),
        number: $('#number'),
        recovery: $('#recovery')
    },
    bindEvents() { /*..*/ }
 }
v.render()
c.bindEvents()
  • 之前改了代码顺序,代码运行至debugger断点处时,页面中没有button元素
  • button 初始化在 render() 之前
  • 在赋值button1的时候,就立即执行(调用)了$(#add1)
  • 而此时render()还未执行
  • 浏览器解析到函数调用,会立即调用
  • 绑定事件c.bindEvents()在渲染页面之后v.render()
  • 在定义c.ui里的元素前,页面里还没有渲染,所以并没有取到元素

在c中写一个初始化方法init(),当调用时init(),才取到``c.ui`的属性,即 button 初始化

  • 将绑定事件方法c.bindEvents()写进初始化方法里init()
  • 即先初始化c.ui,再初始化绑定事件c.bindEvents()
  • 这样就可以实现先渲染,再初始化(绑定事件)

app1.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
const c = {
  init() {
    c.ui = {
        // 需要的元素
        button1: $('#add1'),
        button2: $('#minus1'),
        button3: $('#mul2'),
        button4: $('#divide2'),
        number: $('#number'),
        recovery: $('#recovery')
    }
    c.bindEvents()
  },
  bindEvents() {/*...*/}
 }
c.render()
c.init()

刷新无效

  • 第一次渲染时将不变的html传入了,没有用到数据中的n
  • const element = $(v.html).prependTo($('body>.page'))
  • html中加上占位符{{number}}
  • 传的时候将字符串htmlreplace()

app1.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const v = {
// 初始化html
  html: `...
        <span id="number">{{number}}</span>
        ...
  `,
  update() {/* ... */},
  render() {
    // 用jQuery方法见字符串变为HTML标签
    const element = $(v.html.replace('{{number}}', m.data.n))
    .prependTo($('body>.page'))
  }
}
  • 之后更新update()方法代替原来操作DOM
  • 未改变m.data.n,因为没有触发点击事件
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
const v = {
// 初始化html
  html: `...
        <span id="number">{{number}}</span>
        ...
  `,
  update() {
    v.render()
  },
  render() {
    // 用jQuery方法见字符串变为HTML标签
    const element = $(v.html.replace('{{number}}', m.data.n))
    .prependTo($('body>.page'))
  }
}

再点击事件中,直接从m.data中获取数据,而不是在页面上读取字符串c.ui.number.text()

  • 直接在事件监听函数中操作数据
  • 然后在事件监听函数中重新调用渲染函数v.render()
  • 而不是之前直接操作DOMc.ui.number.text(n)
  • 不需要update()

app1.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
const c = {
  init() {
    c.ui = { /* ... */ }
    c.bindEvents()
  },
  bindEvents() {
    // console.log('bindEvents 执行了')
    // console.log(c.ui.button1)
    c.ui.button1.on('click', () => {
      // 直接从`m.data`中获取数据 来操作
      console.log('run here')
      m.data.n += 1
      console.log(m.data.n)
      v.render()
      localStorage.setItem('n', m.data.n.toString())
    })
    /* ... */
  }
}

渲染函数重复添加相同的节点(点击看效果)

1
2
const element = $(v.html.replace('{{number}}', m.data.n))
.prependTo($('body>.page'))
  • 每次渲染都添加,没有删除原来的,去掉overflow: hidden;可以看到

app1.js

  • m.data.n是从localStorage.getItem('n')中获取的一个字符串
1
2
3
4
5
6
7
/* 数据相关放到 m */
const m = {
  // 初始化数据
  data: {
    n: parseInt(localStorage.getItem('n'))
  }
}
  • 添加一个标记,render()中未使用变量element
  • 将视图渲染的结果放到v,初始值为null
  • 每次渲染判断一下el,有就替换,没有再添加

app1.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
const v = {
// 初始化html
  html: `...`,
  render() {
    if(v.el === null) {
      // 没渲染过
      v.el = $(v.html.replace('{{number}}', m.data.n))
        .prependTo($('body>.page'))
    } else {
      // 渲染过
      const newEl = $(v.html.replace('{{number}}', m.data.n))
      v.el.replaceWith(newEl)
      // 更新地址 注意每次刷新都是不同的引用
      v.el = newEl
    }
  }
}

目前做到了 点击一次

  • 先改数据
  • 再根据数据,刷新整个自己
  • bug: 点击只有一次有效

解决之前先看v.el = $(v.html.replace('{{number}}', m.data.n)).prependTo($('body>.page')),怎么知道页面上有$('body>.page'),最好是传入

  • 如果页面上的.page未知,可以传入
  • c.init()的参数去接收
  • 在初始化的方法c.init()中渲染v.render()一次,代替外部代码的先渲染,再初始化
  • 先渲染,再做初始化的其他步骤
  • 可以减少一次函数调用
  • 再次体现了最小知识原则

渲染顺序 初始化顺序

  • 初始化时接收一个参数,来记录渲染到页面的哪个位置
  • 参数传递init(container),可以是一个div标签
  • container传给视图中的v.render(container)
  • 不需要写死$('body>.page'),替换为$(container)
  • 可以传递任意标签

app1.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/* 视图相关放到 v */
const v = {
  el: null,
  html: `...`,
  render(container) {
    if(v.el === null) {
      // 没渲染过
      v.el = $(v.html.replace('{{number}}', m.data.n))
        .prependTo($(container))
        // .prependTo($('body>.page'))
    } else {
      // 渲染过
      const newEl = $(v.html.replace('{{number}}', m.data.n))
      v.el.replaceWith(newEl)
      // 更新地址 注意每次刷新都是不同的引用
      v.el = newEl
    }
  }
}
const c = {
  init(container) {
    // 初始化渲染html
    v.render(container)
    c.ui = { /* ... */ }
    c.bindEvents()
  },
  bindEvents() { /* ... */ }
}
c.init()

模块传参,导出变量

需要在初始化时,得到传入的container,需要在app1.js外暴露变量

  • app1怎么知道外部容器`
  • app1.js中无法直接得到,不可在在app1.js里调用init()
  • 暴露出变量c, 即导出变量export default c
1
2
3
// ...
// c.init()
export default c
  • app1.js需要别的js文件来传入一个参数
  • main.js中导入c,并初始化app1.js
  • 搭好骨架<section id="app1"></section>
  • app1main.js里初始化,并接受参数`import x from “./app1”

main.js

1
2
3
4
5
6
7
8
import './reset.css'
import './global.css'

import x from "./app1"
import './app1.js'
import './app2.js'
import './app3.js'
import './app4.js'
  • 写一个的容器作为骨架
  • x.init('#app1')接受 id 为 app1 的容器
  • 同时删除app1.jshtml变量里 id 为 app1 的节点

index.html

1
2
3
4
5
6
<body>
  <div class="page">
    <section id="app1"></section>
  </div>
  <script src="./main.js"></script>
</body>
  • 修改对应的html
  • x就是默认导出的c,引入的就是c的地址
  • 把页面中的#app1传入模块,之后初始化
  • <section id="app1"></section>是因为css里写死了section

main.js

1
2
3
4
5
 // ...
import x from "./app1"
import './app1.js'
// ...
x.init('#app1')
  • main.js中导入app1.jscimport x from "./app1"
  • x 为导出时的变量名
  • 使用c中的init()方法:x.init('#app1'),将页面上的节点传入
  • 即将页面中的#app1传给app1.js模块去初始化

再次运用了最小知识原则

  • 模块不关心外部的 选择器 标签

解决点击只有一次有效的bug

  • 事件监听没了,原因是刷新页面之后,页面上新的button1不是原来的引用
  • 更新地址v.el = newEl 注意每次刷新都是不同的引用
  • 而绑定的监听函数是绑在替换之前初始化的button1节点上
  • 操作 DOM 更新页面后 节点的引用改变(不是原来的节点) 绑定在节点上的事件也失效了
  • 在不会动态更新的元素上绑定函数,即在不变的容器上添加事件监听

查看点击节点是否被替换,开发转工具相应节点添加属性frank="yes"

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<section id="app1" class="app1" frank="yes">
    <div class="output">
        <span id="number">100</span>
    </div>
    <div class="actions">
        <button id="add1">+1</button>
        <button id="minus1">-1</button>
        <button id="mul2">×2</button>
        <button id="divide2">÷2</button>
        <button id="recovery">恢复</button>
    </div>
</section>
  • 如果改变了,就会消除frank="yes"
  • 确认该元素不会被更新
  • 原来的bindEvents()中访问不到container

事件委托

绑定动态元素的事件无效 使用事件委托

  • 将事件绑定到一个不会更新的标签上
  • 使用事件委托
  • bindEvent()如何获取到v.container
  • v.render()时,记下一个变量v.container
  • 目的是有一个地方可以存储v.container

app1.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
/* 视图相关放到 v */
const v = {
// 视图默认渲染结果
  el: null,
// 初始化html
  html: `...`,
  init(container) {
    // 用jQuery封装对象 container <- #app1
    v.container = $(container)
    v.render()
  },
  // render(container) 省去参数 里面的替换为 $(v.container)
  render( /* container */ ) {
    if(v.el === null) {
      // 没渲染过
      v.el = $(v.html.replace('{{number}}', m.data.n))
        .prependTo($(v.container))
      /*
      v.el = $(v.html.replace('{{number}}', m.data.n))
        .prependTo($(container)) */
    } else {
      // 渲染过
      const newEl = $(v.html.replace('{{number}}', m.data.n))
      v.el.replaceWith(newEl)
      // 更新地址 注意每次刷新都是不同的引用
      v.el = newEl
    }
  }
}
const c = {
  init(container) {
    // 初始化渲染html
    // v.render(container) 改成 v.init(container)
    // 将 `container`传给视图 v :在v里创建 init(container) {}
    v.init(container)
    c.ui = {
        button1: $('#add1'),
        button2: $('#minus1'),
        button3: $('#mul2'),
        button4: $('#divide2'),
        number: $('#number'),
        recovery: $('#recovery')
    }
    c.bindEvents()
  },
  bindEvents() {
    //事件委托
    v.container.on('click', '#add1', () => {
      m.data.n += 1
      v.render()
      localStorage.setItem('n', m.data.n.toString())
    })
    // console.log('bindEvents 执行了')
    // console.log(c.ui.button1)
    /*
    c.ui.button1.on('click', () => {
      // console.log('run here')
      m.data.n += 1
      // console.log(m.data.n)
      v.render()
      localStorage.setItem('n', m.data.n.toString())
    })
    */
    // ... app2 app3 app4
}
  • 视图的外部的容器监听里面的元素
  • 容器不会被替换,只替换容器里面的东西
  • 而监听函数就不会失效

改其他绑定事件

  • 并且c.ui选出的元素不需要了,直接在事件委托中选择

app1.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/* 其他相关放到 c */
const c = {
  init(container) {
    // 初始化渲染html
    // v.render(container)
    v.init(container)
    /*
    c.ui = {
        // 需要的元素
        button1: $('#add1'),
        button2: $('#minus1'),
        button3: $('#mul2'),
        button4: $('#divide2'),
        number: $('#number'),
        recovery: $('#recovery')
    }
    */
    c.bindEvents()
  },
  bindEvents() {
    //事件委托
    v.container.on('click', '#add1', () => {
      // 直接从`m.data`中获取数据 来操作
      m.data.n += 1
      v.render()
      localStorage.setItem('n', m.data.n.toString())
    })
    v.container.on('click', '#minus1',() => {
      m.data.n -= 1
      v.render()
      localStorage.setItem('n', m.data.n.toString())
    })
    v.container.on('click', '#mul2',() => {
      m.data.n *= 2
      v.render()
      localStorage.setItem('n', m.data.n.toString())
    })
    v.container.on('click', '#divide2',() => {
      m.data.n /= 2
      v.render()
      localStorage.setItem('n', m.data.n.toString())
    })
    v.container.on('click', '#recovery',() => {
      m.data.n = 100
      v.render()
      localStorage.setItem('n', m.data.n.toString())
    })
  }
}
export default c

思路

  • 每次刷新,section父容器(骨架)为不可变
  • 每次点击时,更新整个父容器里面的div,原来绑定在button上的事件不存在了
  • 但事件绑定在不变的父容器section上,并判断对应不同子元素,进行不同的操作,这就是事件委托
  • 里面的引用变了,但id没变,每次点击:刷新子元素,重新绑定事件

反思:MVC带来什么好处

  • 面向过程到面向对象的转变
  • 模块化 各司其职
  • 复杂度随着代码量平稳上升,而不是线性增长

折叠后的代码 app1.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import $ from 'jquery'
import './app1.css'
/* 数据相关放到 m */
const m = {
  // 初始化数据
  data: { /* ... */ }
}
/* 视图相关放到 v */
const v = {
  // 视图默认渲染结果 视图元素
  el: null,
  // 放入的容器元素
  container: null,
  // 初始化html
  html: `...`,
  // 接受外部参数(传入容器),视图初始化
  init(container) { /* ... */ },
  // 负责渲染页面
  render() { /* ... */ }
}
/* 其他相关放到 c */
const c = {
  init(container) { /* ... */ },
  bindEvents() { /* ... */ }
}
export default c

抽象思维2 以不变应万变 每个模块都可以用 m + v + c 搞定

  • 适用所有MVC,万金油代码
  • 每个页面都有数据data
  • 每个视图都有元素el
  • 每个视图都有容器container
  • 每个视图都有数据html
  • 每个视图都需要初始化init()
  • 每个视图都需要更新渲染render()
  • 每个控制器都需要初始化init()
  • 每个控制器都需要绑定事件bindEvents()
  • 没有逻辑缺失

合并视图元素el container

  • 元素elcontainer可以合并
  • container第一个元素就是el

app1.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 负责渲染页面
  render() {
    /*
    if(v.el === null) {
      // 没渲染过
      v.el = $(v.html.replace('{{number}}', m.data.n))
        .prependTo(v.container)
    } else {
      // 渲染过
      const newEl = $(v.html.replace('{{number}}', m.data.n))
      v.el.replaceWith(newEl)
      // 更新地址 注意每次刷新都是不同的引用
      v.el = newEl
    }
    */
    // `v.container.children.length === 0` 代替看 `v.el === null`判断视图元素是否为空
    if(v.container.children.length === 0) {
      // 没渲染过
      $(v.html.replace('{{number}}', m.data.n))
              .prependTo(v.container)
    } else {
      // 渲染过
      v.container.empty()
      $(v.html.replace('{{number}}', m.data.n))
              .prependTo(v.container)
    }
  }

简化代码 app1.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 负责渲染页面
  render() {
    if(v.container.children.length === 0) {
      // 没渲染过
    } else {
      // 渲染过
      v.container.empty()
    }
    $(v.html.replace('{{number}}', m.data.n))
            .prependTo(v.container)
  }

再简化

1
2
3
4
5
if (v.container.children.length !== 0) {
  v.container.empty()
}
$(v.html.replace('{{number}}', m.data.n))
  .prependTo(v.container)

v.container代替了v.el,但名字太长 改名v.el

  • v.el.children.length === 0 代替 v.el === null判断视图元素是否为空
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
/* 视图相关放到 v */
const v = {
  // 视图元素 容器元素
  el: null,
  // 初始化html
  html: `...`,
  // 接受外部参数(传入容器)视图初始化
  init(container) {
    // 用jQuery封装对象 container <- #app1
    v.el = $(container)
    v.render()
  },
  render() {
    if (v.el.children.length !== 0) {
      v.el.empty()
    }
    $(v.html.replace('{{number}}', m.data.n))
      .prependTo(v.el)
  }
}

7.抽象思维3 用数据去渲染 §

view = render(data)

  • 比起操作DOM对象,拙接render简单多了
  • 只要改变data,就可以得到对应的view
  • React的诞生

代价

  • render粗犷的渲染肯定比DOM操作耗费更多性能
  • 用虚拟DOM能让render只更新改更新的地方

数据单向流动

  • 同样是最小知识原则

none MVC

JS(#add1) 数据流向 DOM(页面)
n = span.text <--- 100
n += 1 100
span.text = n ---> 101

MVC(React)

JS(#add1) 数据流向 DOM(页面)
n = 100
render(n) ---> 100
n += 1 100
render(n) ---> 101

用数据去渲染改代码

  • 数据:m.data.n
  • 视图:v.render(n)
  • v 不用知道 m 的存在
  • 初始化v.init(container)v.render(m.data.n)分开

app1.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
/* 数据相关放到 m */
const m = {
  // 初始化数据
  data: {
    n: parseInt(localStorage.getItem('n')) || 100
  }
}
const v = {
  // 视图元素 容器元素
  el: null,
  // 初始化html
  html: `...`,
  // 接受外部参数(传入容器)视图初始化
  init(container) {
    // 用jQuery封装对象 container <- #app1
    v.el = $(container)
    v.render(n)
  },
  // 负责渲染页面
  render(n) {
    if (v.el.children.length !== 0) {
      v.el.empty()
    }
    $(v.html.replace('{{number}}', n))
      .prependTo(v.el)
  }
}
const c = {
  init(container) {
    // 初始化渲染html
    v.init(container)
    v.render(m.data.n) // 1st view = render(data)
    c.bindEvents()
  },
  bindEvents() {
      v.el.on('click', '#add1', () => {
      // console.log('run here')
      // 直接从`m.data`中获取数据 来操作
      m.data.n += 1
      // console.log(m.data.n)
      v.render(m.data.n) // 2nd view = render(data)
      localStorage.setItem('n', m.data.n.toString())
    })
    v.el.on('click', '#minus1',() => {
      m.data.n -= 1
      v.render(m.data.n)
      localStorage.setItem('n', m.data.n.toString())
    })
    v.el.on('click', '#mul2',() => {
      m.data.n *= 2
      v.render(m.data.n)
      localStorage.setItem('n', m.data.n.toString())
    })
    v.el.on('click', '#divide2',() => {
      m.data.n /= 2
      v.render(m.data.n)
      localStorage.setItem('n', m.data.n.toString())
    })
    v.el.on('click', '#recovery',() => {
      m.data.n = 100
      v.render()
      localStorage.setItem('n', m.data.n.toString())
    })
  }
  }
export default c

重复的代码太多

  • 一样的东西只写一遍

8.抽象思维4 表驱动编程-自动绑定事件 §

  • 当看到大批类似但不重复的代码
  • 眯起眼睛,看看到底哪些才是重要的数据
  • 将重要的数据做成哈希表,代码就简单明了
  • 这就是数据结构知识的红利
  • 可以减少重复代码,只将重要的信息放在表里,然后利用表来编程

代价

  • 没有代价

哈希表 描述在做什么

  • 'click #add1': 'add'click点击#add1的时候,调用add函数
  • 添加add1函数
  • 重复的v.render(m.data.n)不抄
  • 遍历哈希表 循环绑定事件

app1.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
const c = {
  init(container) {
    // 初始化渲染html
    v.init(container)
    v.render(m.data.n) // 1st view = render(data)
    // c.bindEvents()
    c.autoBindEvents()
  },
  // `'click #add1': 'add'`:`click`点击`#add1`的时候,调用`add`函数
  events: {
    'click #add1': 'add',
    'click #minus': 'minus',
    'click #mul2': 'mul',
    'click #divide2': 'div',
    'click #recovery': 'recover',
  },
  add() {
    m.data.n += 1
    v.render(m.data.n)
    localStorage.setItem('n', m.data.n.toString())
  },
  minus() {
    m.data.n -= 1
    v.render(m.data.n)
    localStorage.setItem('n', m.data.n.toString())
  },
  mul() {
    m.data.n *= 2
    v.render(m.data.n)
    localStorage.setItem('n', m.data.n.toString())
  },
  div() {
    m.data.n /= 2
    v.render(m.data.n)
    localStorage.setItem('n', m.data.n.toString())
  },
  recover() {
    m.data.n = 100
    v.render(m.data.n)
    localStorage.setItem('n', m.data.n.toString())
  },
  // 表驱动编程-自动绑定事件
  // `'click #add1': 'add'`:`click`点击`#add1`的时候,调用`add`函数
  // 遍历哈希表 循环绑定事件
  autoBindEvents() {
    for(let key in c.events) {
      // console.log(key)
      // app1.js:124 click #add1
      // app1.js:124 click #minus
      // app1.js:124 click #mul2
      // app1.js:124 click #divide2
      // app1.js:124 click #recovery
      const spaceIndex = key.indexOf(' ')
      const part1 = key.slice(0, spaceIndex)
      const part2 = key.slice(spaceIndex + 1)
      // console.log(part1, ',', part2)
      const valueMethod = c[c.events[key]]
      console.log(part1, part2, valueMethod)
      v.el.on(part1, part2, valueMethod)
    }
  },
  /*
  ,
  bindEvents() {
    //事件委托
    v.el.on('click', '#add1', () => {
      // 直接从`m.data`中获取数据 来操作
      // console.log('run here')
      m.data.n += 1
      // console.log(m.data.n)
      v.render(m.data.n) // 2nd view = render(data)
      localStorage.setItem('n', m.data.n.toString())
    })
    v.el.on('click', '#minus1',() => {
      m.data.n -= 1
      v.render(m.data.n)
      localStorage.setItem('n', m.data.n.toString())
    })
    v.el.on('click', '#mul2',() => {
      m.data.n *= 2
      v.render(m.data.n)
      localStorage.setItem('n', m.data.n.toString())
    })
    v.el.on('click', '#divide2',() => {
      m.data.n /= 2
      v.render(m.data.n)
      localStorage.setItem('n', m.data.n.toString())
    })
    v.el.on('click', '#recovery',() => {
      m.data.n = 100
      v.render()
      localStorage.setItem('n', m.data.n.toString())
    })
  }
  */
}

guid报错

9.抽象思维5 俯瞰全局 对象间通信 §

  • 把所有的对象看成点
  • 一个点和一个点怎么通信
  • 一个点和多个点怎么通信
  • 多个点和多个点怎么通信
  • 最终找出一个专用的点负责通信
  • 这个点就是event bus(事件总线)

eventBus主要功能

  • eventBus 主要用于对象间通信
  • eventBus 提供了 onofftriggerAPI
  • on 用于监听事件,trigger 用域触发事件
  • 使用 eventBus 可以满足最小知识原则,mv 互相不知道对方的细节,但是却可以调用对方的功能

实现eventBus对象间通信

  • v 知道 m 发生变化
  • 如何使得v.render(m.data.n)localStorage.setItem('n', m.data.n.toString())从代码里消失
  • 真实需求: data 的 n 一变,就自动 render 视图
  • 监听 n 变化,直接更新数据,不关心视图
  • 监听 n 变化的方法有:
    1. eventBus
    2. vue2Object.definedProperty
    3. vue3.0 proxy Vue双向绑定原理 从vue2的Object.defineProperty到vue3的proxy

使用eventBus

  • const eventBus = $({}); console.log(eventBus)
  • 获取 jQuery 空对象元素的API on监听事件 trigger触发事件
  • console.log(eventBus.on) console.log(eventBus.trigger)
  • 在一个地方监听事件,在另一个地方标记触发事件
  • 两个地方互相通信

m.update()数据更新

  • 将传入的参数data的所有可枚举的属性,覆盖到m.data
  • Object.assign(m.data, data)
  • 触发标记eventBus.trigger('m:updated')
  • 在一个地方监听标记,在另一个地方标记触发事件
  • 在数据 m 中更新数据,并标记触发事件eventBus.trigger('m:updated')

app1.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
/* 数据相关放到 m */
const m = {
  // 初始化数据
  data: {
    n: parseInt(localStorage.getItem('n')) || 100
  },
  create() {},
  delete() {},
  update(data) {
    // 同名属性覆盖 赋值
    // 监听 n 变化,直接更新数据,不关心视图
    Object.assign(m.data, data)
    eventBus.trigger('m:updated')
    localStorage.setItem('n', m.data.n.toString())
  },
  get() {}
}

在 c 中监听触发标记'm:updated'

  • 在一个地方监听标记,在另一个地方标记触发事件
  • 在数据 m 中更新数据,并标记触发事件eventBus.trigger('m:updated')
  • 在控制器 c.init 中,监听标记触发事件eventBus.on(),统一渲染页面v.render(m.data.n)

app1.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
const c = {
  init(container) {
    // 初始化渲染html
    v.init(container)
    v.render(m.data.n) // 1st view = render(data)
    // c.bindEvents()
    c.autoBindEvents()
    // 监听触发标记`'m:updated'`
    // 统一在`eventBus.on()`监听的回调函数中渲染`v.render(m.data.n)`
    eventBus.on('m:updated', () => {
      v.render(m.data.n)
    })
  },
  events: {
    'click #add1': 'add',
    'click #minus1': 'minus',
    'click #mul2': 'mul',
    'click #divide2': 'div',
    'click #recovery': 'recover',
  },
  add() {
    // m.data.n += 1
    // v.render(m.data.n)
    m.update({n: m.data.n += 1})
  },
  minus() {
    // m.data.n -= 1
    // v.render(m.data.n)
    m.update({n: m.data.n -= 1})
  },
  mul() {
    // m.data.n *= 2
    // v.render(m.data.n)
    m.update({n: m.data.n *= 2})
  },
  div() {
    // m.data.n /= 2
    // v.render(m.data.n)
    m.update({n: m.data.n /= 2})
  },
  recover() {
    // m.data.n = 100
    // v.render(m.data.n)
    m.update({n: m.data.n = 100})
  },
  autoBindEvents() {
    for(let key in c.events) {
      const spaceIndex = key.indexOf(' ')
      const part1 = key.slice(0, spaceIndex)
      const part2 = key.slice(spaceIndex + 1)
      const valueMethod = c[c.events[key]]
      v.el.on(part1, part2, valueMethod)
    }
  }
}

debugger 过程

  • eventBus.trigger('m update')中的事件名,不能有空格
  • 对于 m.trigger('x xx')有空格的debugger
  • 点击add没反应
  • 找到add() {}函数,里面写一句console.log("add")
  • 控制台里查看add()是否运行
  • 执行到了,会调用m.update(),找到update() {}函数
  • update() {}函数里写一句console.log("here")
  • 执行到了,触发eventBus.trigger('m:updated'),查看监听eventBus.on('m:updated',() => {})
  • 监听是在c.init里,在eventBus.on('m:updated',() => {})的回调函数中写一句console.log("here")
  • 发现没有成功监听
  • eventBus的事件没有调用,定位bug
  • 改一下事件名xxx,可以执行eventBus
  • eventBus.trigger('xxx')中的事件名xxx,不能有空格

小结2

app1.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
import $ from 'jquery'
import './app1.css'
// 获取 jQuery对象的 on 和 trigger 方法
const eventBus = $({})
/* 数据相关放到 m */
const m = {
  // 初始化数据
  data: {
    n: parseInt(localStorage.getItem('n')) || 100
  },
  update(data) {
    // 更新数据
    Object.assign(m.data, data)
    // 标记数据更新
    eventBus.trigger('m:updated')
    localStorage.setItem('n', m.data.n.toString())
  }
}
/* 视图相关放到 v */
const v = {
  // 容器
  el: null,
  // 视图
  html: `
    <div>
      <div class="output">
        <span id="number">{{number}}</span>
      </div>
      <div class="actions">
        <button id="add1">+1</button>
        <button id="minus1">-1</button>
        <button id="mul2">×2</button>
        <button id="divide2">÷2</button>
        <button id="recovery">恢复</button>
      </div>
    </div>
  `,
  // 接受外部参数(传入容器)视图初始化
  init(container) {
    // 用jQuery封装容器 container <- #app1节点
    v.el = $(container)
  },
  // 渲染
  render(n) {
    if (v.el.children.length !== 0) {
      v.el.empty()
    }
    $(v.html.replace('{{number}}', n))
      .prependTo(v.el)
  }
}
/* 其他相关放到 c */
const c = {
  init(container) {
    // 初始化渲染html
    v.init(container)
    v.render(m.data.n) // 1st view = render(data)
    c.autoBindEvents()
    // 监听触发标记`'m:updated'` 统一渲染
    eventBus.on('m:updated', () => {
      v.render(m.data.n)
    })
  },
  events: {
    'click #add1': 'add',
    'click #minus1': 'minus',
    'click #mul2': 'mul',
    'click #divide2': 'div',
    'click #recovery': 'recover',
  },
  add() {
    m.update({n: m.data.n += 1})
  },
  minus() {
    m.update({n: m.data.n -= 1})
  },
  mul() {
    m.update({n: m.data.n *= 2})
  },
  div() {
    m.update({n: m.data.n /= 2})
  },
  recover() {
    m.update({n: m.data.n = 100})
  },
  // 表驱动编程-自动绑定事件
  autoBindEvents() {
    for(let key in c.events) {
      const spaceIndex = key.indexOf(' ')
      const part1 = key.slice(0, spaceIndex)
      const part2 = key.slice(spaceIndex + 1)
      const valueMethod = c[c.events[key]]
      v.el.on(part1, part2, valueMethod)
    }
  }
}
export default c

10.第二个模块的使用MVC §

app2.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 错误代码
const m = {
  localKey: 'app2.index',
  data: {
  // 不能在声明m的时候,在对象的右边使用m,会变成undefined
    index: localStorage.getItem(m.localKey) || 0
  },
  update(data) {
    Object.assign(m.data, data)
    eventBus.trigger('m:updated')
    localStorage.setItem(localKey, m.data.index)
  }
}
  • 不能在声明一个对象的时候,在对象的右边使用这个对象,会变成undefined

main.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import './reset.css'
import './global.css'
// 导入 控制器 c
import x from "./app1.js"
import y from "./app2.js"

import './app1.js'
import './app2.js'
import './app3.js'
import './app4.js'
// 传入初始化需要填入的节点
x.init('#app1')
// 导出初始化方法init 在main.js 中获取el <- #app2
y.init('#app2')
  • app2.js中导出 c export default c

函数封装 字符模板替换

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
html: (index) => {
return `
  <div>
    <ol class="tab-bar">
      <li class="${index === 0? 'selected' : ''}"><span>111111</span></li>
      <li class="${index === 1? 'selected' : ''}"><span>222222</span></li>
    </ol>
    <ol class="tab-content">
      <li class="${index === 0 ? 'active' : '' }">内容1</li>
      <li class="${index === 1 ? 'active' : '' }">内容2</li>
    </ol>
  </div>
`
}

在render中使用html()调用

1
2
3
4
5
6
render(index) {
    if (v.el.children.length !== 0) {
      v.el.empty()
    }
    $(v.html(index)).appendTo(v.el)
}

视图 v 中定义了 提供控制器 c 使用的回调函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
const v = {
  el: null,
  html: (index) => {
    return `...`
  },
  init(container) {
    v.el = $(container)
  },
  render(index) {
    if (v.el.children.length !== 0) {
      v.el.empty()
    }
    $(v.html(index)).appendTo(v.el)
  }
}

绑定自定义属性data-index 用DOM做标记

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// v ...
html: (index) => {
    return `
  <div>
    <ol class="tab-bar">
      <li class="${index === 0? 'selected' : ''}" data-index="0"><span>111111</span></li>
      <li class="${index === 1? 'selected' : ''}" data-index="1"><span>222222</span></li>
    </ol>
    <ol class="tab-content">
      <li class="${index === 0 ? 'active' : '' }">内容1</li>
      <li class="${index === 1 ? 'active' : '' }">内容2</li>
    </ol>
  </div>
  `

获取标记

  • focus()获取当前点击处的index是第几个
  • 把 data-index 写到 li 上, 在focus()中取dataset.index
  • 更新数据 m.update({index: dataIndex})
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const c = {
  init(container) {},
  events: {
    'click .tab-bar li': 'focus'
  },
  focus(e) {
    // console.log(e.currentTarget.dataset.index)
    const index = parseInt(e.currentTarget.dataset.index)
    m.update({index: index})
    // console.log('x')
  },
  autoBindEvents() { }
}
  • webStormlocal history回顾代码,思考为什么使用MVC

none MVC

JS(#add2) 数据流向 DOM(页面)
$(html).appendTo ---> tab Content
DOM操作10行代码 tab0
$li.addClass ---> tab0
$tabContent.addClass ---> tab1

MVC

JS(#add2) 数据流向 DOM(页面)
index = 0
render(n) ---> tab0
index = 1 tab0
render(n) ---> tab1
  • MVC复杂度稳定很小,应对复杂需求
  • DOM操作越复杂,代码线性增长

11.抽象思维6 事不过三 使用类优化代码 继承 §

  • 同样的代码写三遍,就该抽成一个函数
  • 同样的属性写三遍,就该做成共用属性(原型或类)
  • 同样的原型写三遍,就该用继承

代价

  • 有时候会造成继承层级太深,无法一下看懂代码
  • 可以通过写文档,画类图解决

使用类优化代码

  • 引入类(代替重复对象)
  • 引入继承(代替重复功能)
  • 路由

app2.js 代码结构

1
2
3
4
5
6
7
8
import './app2.css'
import $ from 'jquery'
const localKey = 'app2.index'
const eventBus = $({})
const m = {...}
const v = {...}
const c = {...}
export default c

app1.jsapp2.js 重复的代码

  • m create, delete, update, get
  • v el, html, init, render
  • c init, events, autoBindEvents
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/* app1.js */
import $ from 'jquery'
import './app1.css'
// 获取 jQuery对象的 on 和 trigger 方法
const eventBus = $({})
/*
const m = {
  // 初始化数据
  data: {
    n: parseInt(localStorage.getItem('n')) || 100
  },
  update(data) {
    // 更新数据
    Object.assign(m.data, data)
    // 标记数据更新
    eventBus.trigger('m:updated')
    localStorage.setItem('n', m.data.n.toString())
  }
}
*/

/* app2.js */
/*
const m = {
  data: {
    index: parseInt(localStorage.getItem(localKey)) || 0
  },
  update(data) {
    Object.assign(m.data, data)
    eventBus.trigger('m:updated')
    localStorage.setItem(localKey, m.data.index.toString())
  }
}
*/

相同属性抽成公共属性 原型/类

  • 注意大写 Model.js
  • 实例化const m = new Model({...})

Model.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Model {
  // 属性传参 data
  constructor(options) {
    this.data = options.data // 传参复制到对象实例中
  }
  // 原型方法
  create() {
    // if(console && console.error) console.error('未实现 create')
    // console && console.error && console.error('未实现 create')
    // 可选链语法 `?.` 语法
    console?.error?.('未实现 create')
  }
  delete() {
    console?.error?.('未实现 delete')
  }
  update() {
    console?.error?.('未实现 update')
  }
  get() {
    console?.error?.('未实现 get')}
}

// 使用时 实例化: // const m = new Model() //m.create() //m.delete()

// 导出
export default Model

用类改写 app1.jsm

  • import Model from './base/Model'
  • m.update属性覆盖
  • Chrome 打出的console.dir(m)是构造函数 查看属于哪个类

app1.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
/* 数据相关放到 m */
const m = new Model({
  data: {
    n: parseInt(localStorage.getItem('n')) || 100
  }})
// 可以直接覆盖
m.update = (data) => {
  // 更新数据
  Object.assign(m.data, data)
  // 标记数据更新
  eventBus.trigger('m:updated')
  localStorage.setItem('n', m.data.n.toString())
}
// Chrome 打出的是构造函数 查看属于哪个类
console.dir(m)
m.create() // 未实现 create

可以将 m.update 写到 new Model 参数中

  • 先改 Model.js
  • 为了让实例化中的传参更明确 在类中的形参 传一个对象option
  • 对象option.data 遍历简化代码['data', 'update', 'create', 'delete', 'get']

Model.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Model {
  // 属性传参 data
  constructor(options) {
    // this.data = options.data
    // this.update = options.update
    // this.delete = options.delete
    // this.get = options.get
    // 事不过三 简化代码 遍历 // 传参复制到对象实例中
    ['data', 'update', 'create', 'delete', 'get'].forEach((key) => {
      if(key in options) {
        this[key] = options[key]
      }
    })
  }
  // 原型方法
  create() {
    // if(console && console.error) console.error('未实现 create')
    // console && console.error && console.error('未实现 create')
    // 可选链语法 `?.`
    console?.error?.('未实现 create')
  }
  delete() {
    console?.error?.('未实现 delete')
  }
  update() {
    console?.error?.('未实现 update')
  }
  get() {
    console?.error?.('未实现 get')}
}
// 使用时 实例化: // const m = new Model() //m.create() //m.delete()
// 导出
export default Model
  • 遍历['data', 'update', 'create', 'delete', 'get'].forEach((key) => {} 代码的复杂度是稳定的
  • 无论数据规模多大,稳定地简单
  • 可选链语法 ?.

app1.jsm

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import $ from 'jquery'
import './app1.css'
import Model from './base/Model'
// 获取 jQuery对象的 on 和 trigger 方法
const eventBus = $({})
/* 数据相关放到 m */
const m = new Model({
  data: {
    n: parseInt(localStorage.getItem('n')) || 100
  },
  update(data) {
    // 更新数据
    Object.assign(m.data, data)
    // 标记数据更新
    eventBus.trigger('m:updated')
    localStorage.setItem('n', m.data.n.toString())
  }
})
  • 注意update(data) {}可以写成update: function(data) {}
  • 在面向对象的构造函数中不建议写箭头函数,易错

app2.jsm

  • 可以点击Model自动引入import Model from './base/Model'
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import './app2.css'
import $ from 'jquery'
import Model from './base/Model'
const localKey = 'app2.index'
const eventBus = $({})

const m = new Model({
  data: {
    index: parseInt(localStorage.getItem(localKey)) || 0
  },
  update(data) {
    Object.assign(m.data, data)
    eventBus.trigger('m:updated')
    localStorage.setItem(localKey, m.data.index.toString())
  }
})


app1.js 用类简化代码 v

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/* 视图相关放到 v */
const v = {
  // 容器
  el: null,
  // 视图
  html: `...`,
  // 接受外部参数(传入容器)视图初始化
  /*
  init(container) {
    // 用jQuery封装容器 container <- #app1节点
    v.el = $(container)
  },
  */
  init(el) {
    v.el = $(el)
  },
  // 渲染
  render(n) {
    if (v.el.children.length !== 0) {
      v.el.empty()
    }
    $(v.html.replace('{{number}}', n))
      .prependTo(v.el)
  }
}
  • touch src/base/View.js
  • 大写View
  • 不同的属性写到构造函数中 el html
  • 相同的写到原型/类中 init
  • render通过虚拟DOM可以变成一样,之后再改
  • render目前,不放到共用属性里

View.js 解构赋值传参

1
2
3
4
5
6
7
8
9
import $ from 'jquery'
class View {
  constructor({el, html, render}) {
    this.el = $(el)
    this.html = html
    this.render = render
  }
}
export default View

app1.js

  • 将 v 的初始化放到 c
  • v 和 c 的联系紧密,共用为一个对象
  • 必须在 c 中拿到 el ;拿到el后,才能初始化v
  • cinit(container)中,用c.initV()代替v.init(container)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/* 视图相关放到 v */
/* 其他相关放到 c */
const c = {
  init(container) {
    const v = new View({
      // 容器
      el: container,
      // 视图
      html: `...`,
      render(n) {
        if (v.el.children.length !== 0) {
          v.el.empty()
        }
        $(v.html.replace('{{number}}', n))
          .prependTo(v.el)
      }
    })
    // 初始化渲染html
    v.init(container)
    v.render(m.data.n) // 1st view = render(data)
    c.autoBindEvents()
    // 监听触发标记`'m:updated'` 统一渲染
    eventBus.on('m:updated', () => {
      v.render(m.data.n)
    })
  },
  events: {/**/},
  add() {/**/},
  minus() {/**/},
  mul() {/**/},
  div() {/**/},
  recover() {/**/},
  autoBindEvents() {/**/}
}

12.合并vc §

  • v c 都有init,合并init方法
  • 对象合并重写 v 改成 c.v

app1.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
import $ from 'jquery'
import './app1.css'
import Model from './base/Model.js'
import View from './base/View'
const eventBus = $({})
/* 数据相关放到 m */
const m = new Model({
  data: {
    n: parseInt(localStorage.getItem('n')) || 100
  },
  update(data) {
    // 更新数据
    Object.assign(m.data, data)
    // 标记数据更新
    eventBus.trigger('m:updated')
    localStorage.setItem('n', m.data.n.toString())
  }
})
/* 合并重写c 中的 v 改成 c.v */
const c = {
  v: null,
  initV() {
    c.v = new View({
      // 容器
      el: c.container, // // c.container = container from c.init(container)
      // 视图
      html: `...`,
      // 渲染
      render(n) {
        if (c.v.el.children.length !== 0) {
          c.v.el.empty()
        }
        $(c.v.html.replace('{{number}}', n))
          .prependTo(c.v.el)
      }
    })
    c.v.render(m.data.n) // 1st view = render(data)
  },
  init(container) {
    c.container = container
    c.initV()
    c.autoBindEvents()
    // 监听触发标记`'m:updated'` 统一渲染
    eventBus.on('m:updated', () => {
      c.v.render(m.data.n)
    })
  },
  events: {...},
  add() {
    m.update({n: m.data.n += 1})
  },
  minus() {
    m.update({n: m.data.n -= 1})
  },
  mul() {
    m.update({n: m.data.n *= 2})
  },
  div() {
    m.update({n: m.data.n /= 2})
  },
  recover() {
    m.update({n: m.data.n = 100})
  },
  // 表驱动编程-自动绑定事件
  autoBindEvents() {
    for(let key in c.events) {
      const spaceIndex = key.indexOf(' ')
      const part1 = key.slice(0, spaceIndex)
      const part2 = key.slice(spaceIndex + 1)
      const valueMethod = c[c.events[key]]
      c.v.el.on(part1, part2, valueMethod)
    }
  }
}
export default c
  • 开始声明对象时就合并
  • 过度到vue
  • 重构Refactor 变量名c -> view
  • 确认重构 DO REFACTOR

app1.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import $ from 'jquery'
import './app1.css'
import Model from './base/Model.js'
// 获取 jQuery对象的 on 和 trigger 方法
const eventBus = $({})
/* 数据相关放到 m */
const m = new Model({
  data: {
    n: parseInt(localStorage.getItem('n')) || 100
  },
  update(data) {
    // 更新数据
    Object.assign(m.data, data)
    // 标记数据更新
    eventBus.trigger('m:updated')
    localStorage.setItem('n', m.data.n.toString())
  }
})
/* 合并 vc */
const view = {
  // 容器
  el: null,
  // 视图
  html: `...`,
  init(container) {
    view.el = $(container)
    view.render(m.data.n) // 1st view = render(data)
    view.autoBindEvents()
    // 监听触发标记`'m:updated'` 统一渲染
    eventBus.on('m:updated', () => {
      view.render(m.data.n)
    })
  },
  // 渲染
  render(n) {
    if (view.el.children.length !== 0) {
      view.el.empty()
    }
    $(view.html.replace('{{number}}', n))
      .prependTo(view.el)
  },
  events: {...},
  add() {
    m.update({n: m.data.n += 1})
  },
  minus() {
    m.update({n: m.data.n -= 1})
  },
  mul() {
    m.update({n: m.data.n *= 2})
  },
  div() {
    m.update({n: m.data.n /= 2})
  },
  recover() {
    m.update({n: m.data.n = 100})
  },
  autoBindEvents() {
    for(let key in view.events) {
      const spaceIndex = key.indexOf(' ')
      const part1 = key.slice(0, spaceIndex)
      const part2 = key.slice(spaceIndex + 1)
      const valueMethod = view[view.events[key]]
      view.el.on(part1, part2, valueMethod)
    }
  }
}
export default view

再次使用类 改 app2.js

修改前 app2.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import './app2.css'
import $ from 'jquery'
import Model from './base/Model'
const localKey = 'app2.index'
const eventBus = $({})

const m = new Model({
  data: {
    index: parseInt(localStorage.getItem(localKey)) || 0
  },
  update(data) {
    Object.assign(m.data, data)
    eventBus.trigger('m:updated')
    localStorage.setItem(localKey, m.data.index.toString())
  }
})
const view = {
  el: null,
  html: (index) => {
    return `
      <div>
        <ol class="tab-bar">
          <li class="${index === 0? 'selected' : ''}" data-index="0"><span>111111</span></li>
          <li class="${index === 1? 'selected' : ''}" data-index="1"><span>222222</span></li>
        </ol>
        <ol class="tab-content">
          <li class="${index === 0 ? 'active' : '' }">内容1</li>
          <li class="${index === 1 ? 'active' : '' }">内容2</li>
        </ol>
      </div>
    `
  },
  render(index) {
    if (view.el.children.length !== 0) {
      view.el.empty()
    }
    $(view.html(index)).appendTo(view.el)
  },
  init(container) {
    view.el = $(container)
    view.render(m.data.index)
    view.autoBindEvents()
    eventBus.on('m:updated', () => {
      view.render(m.data.index)
    })
  },
  events: {
    'click .tab-bar li': 'focus'
  },
  focus(e) {
    // console.log(e.currentTarget.dataset.index)
    const tabIndex = parseInt(e.currentTarget.dataset.index)
    m.update({index: tabIndex})
    // console.log('x')
  },
  autoBindEvents() {
    for(let key in view.events) {
      const spaceIndex = key.indexOf(' ')
      const part1 = key.slice(0, spaceIndex)
      const part2 = key.slice(spaceIndex + 1)
      const valueMethod = view[view.events[key]]
      view.el.on(part1, part2, valueMethod)
    }
  }
}
export default view
  • 合并v c
  • 重构对象名view

View.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import $ from 'jquery'
class View {
  // {el, html, render, data, eventBus, events, methods}
  constructor({el, html, render, data, eventBus, events, methods}) {
    this.el = $(el)
    this.html = html
    this.render = render
    this.data = data
    this.render(data)
    this.eventBus = eventBus
    this.events = events
    this.autoBindEvents()
    eventBus.on('m:updated', () => {
      this.render(data)
    })
  }
  autoBindEvents() {
    for(let key in this.events) {
      const spaceIndex = key.indexOf(' ')
      const part1 = key.slice(0, spaceIndex)
      const part2 = key.slice(spaceIndex + 1)
      const valueMethod = this[this.events[key]]
      this.el.on(part1, part2, valueMethod)
    }
  }
}
export default View
  • 重构 遍历
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import $ from 'jquery'
class View {
  constructor(options) {
    // 将 options 的所有属性 复制到 this 上
    Object.assign(this, options) // {el, html, render, data, eventBus, events, methods}
    this.el = $(this.el)
    // console.log(this.data)
    this.render(this.data)
    this.autoBindEvents()
    this.eventBus.on('m:updated', () => {
      this.render(this.data)
    })
  }
  autoBindEvents(){ ...}
}

类继承 app1.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import $ from 'jquery'
import './app1.css'
import Model from './base/Model.js'
import View from './base/View.js'
// 获取 jQuery对象的 on 和 trigger 方法
const eventBus = $({})
/* 数据相关放到 m */
const m = new Model({
  data: {
    n: parseFloat(localStorage.getItem('n')) || 100
  },
  update(data) {
    // 更新数据
    Object.assign(m.data, data)
    // 标记数据更新
    eventBus.trigger('m:updated')
    localStorage.setItem('n', m.data.n.toString())
  }
})
/* 合并 vc */
const init = (el) => {
  // const view =
  new View ({ // {el, html, render, data, eventBus, events, methods}
    // 容器
    el: el,
    data: m.data,
    eventBus: eventBus,
    // 视图
    html: `...`,
    // 渲染
    render(data) {
      const n = data.n
      if (this.el.children.length !== 0) {
        this.el.empty()
      }
      $(this.html.replace('{{number}}', n))
        .prependTo(this.el)
    },
    events: {...},
    add() {
      m.update({n: m.data.n += 1})
    },
    minus() {
      m.update({n: m.data.n -= 1})
    },
    mul() {
      m.update({n: m.data.n *= 2})
    },
    div() {
      m.update({n: m.data.n /= 2})
    },
    recover() {
      m.update({n: m.data.n = 100})
    }
  })
}
// 导出初始化方法init 在main.js 中获取el <- #app1
export default init

main.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import './reset.css'
import './global.css'
// 导入 控制器 c
import x from "./app1.js"
import y from "./app2.js"

import './app1.js'
import './app2.js'
import './app3.js'
import './app4.js'
// 传入初始化需要填入的节点
x('#app1')
y('#app2')
  • bug n: parseFloat(localStorage.getItem('n')) || 100
  • 搜索并去除 console.log, 右键.cache 选 Mark Directory as > Excluded

13.让 M 和 V 继承 eventBus §

  • 没有必要每次在实例化构造函数new View中声明eventBus
  • 所有eventBus都是同一个
  • 新建一个EventBus.js类的文件
  • 偷梁换柱地使用jQuery封装的EventBus

EventBus.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import $ from 'jquery'
import Model from './Model'
class EventBus {
  // 偷梁换柱地使用jQuery封装的EventBus
  constructor() {
    this._eventBus = $(window)
  }

  // 监听
  on(eventName, fn) {
    return this._eventBus.on(eventName, fn)
  }

  // 触发
  trigger(eventName, data) {
    return this._eventBus.trigger(eventName, data)
  }

  // 取消监听
  off(eventName, fn) {
    return this._eventBus.off(eventName, fn)
  }
}
export default EventBus

// 用法 const e = new EventBus({})
// e.on()
// e.trigger()
// e.off()

修改 app1.jsapp2.js

  • 导入import EventBus from './base/EventBus'
  • 替换const eventBus = $({})const eventBus = new EventBus()
  • 抽像成类的好处在于,可以随时改变EventBus的实现
  • 比如不依赖jQuery,用虚拟DOM

模块解耦

  • 建立中间层 M V E
  • 具体功能模块依赖中间模块
  • 中间模块再依赖jQuery
  • 修改依赖时,只需重构中间模块,具体功能模块无需变动
  • 中间模块成为胶水层 粘合底层和依赖
  • 是用来隔绝底层细节

能否使eventBus.trigger('m:updated')直接用m.trigger('m:updated')实现,来隐藏细节具体实现的method

  • ModelView 都继承 EventBus
  • 实现Model中的m.trigger('...'),View中的 this.on('...')
  • 用类继承类m.trigger v.on
  • 主流的库,以及JS原生的DOM都是这么做的

控制台

1
2
3
4
5
6
7
console.dir(document.body)
// 第一层属性 __proto__: HTMLBodyElement
// 第二层属性 __proto__: HTMLElement
// 第三层属性 __proto__: Element
// 第四层属性 __proto__: Node
// 第五层属性 __proto__: EventTarget
// 第六层属性 __proto__: Object
  • DOM 的 EventTarget 就是 所谓的EventBus
  • 这就是所有DOM元素都可以监听和触发事件的原因
  • 所有DOM元素都继承于EventTarget

M V继承 EventBus

Model.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import EventBus from './EventBus'
class Model extends EventBus {
  constructor(options) {
    super() // EventBus # constructor()
    ['data', 'update', 'create', 'delete', 'get'].forEach((key) => {
      if(key in options) {
        this[key] = options[key]
      }
    })
  }
  • 先引入后继承
  • 类继承,extends,必须在constructor初始化里调用以下父类的初始化this._eventBus = $(window),即用super()调用
  • 否则报错:Missed super class constructor invocation,且只有共有属性/方法

View.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import $ from 'jquery'
import EventBus from './EventBus.js'
class View extends EventBus {
  constructor(options) {
    super()
    Object.assign(this, options) // {el, html, render, data, eventBus, events, methods}
    this.el = $(this.el)
    this.render(this.data)
    this.autoBindEvents()
    this.eventBus.on('m:updated', () => {
      this.render(this.data)
    })
  }
  autoBindEvents(){...}
}
  • 之后就可以删除app1 2中的EventBus引入和声明,直接用EventBus的方法
  • 注意class View extends EventBus后不可加括号,不能直接调用类作为函数调用:Cannot call a class as a function
  • ['data', 'update', 'create', 'delete', 'get'].forEach()的BUG
  • JS代码不能以括号开头,JS解释器会合并以括号开头的代码
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
const a = [1, 2, 3]
a
[0] // 1
a
[0].forEach()
const fn = () => [1, 2, 3]
fn() //  [1, 2, 3]
fn()[0] // 1
fn
[0].forEach

// `['data', 'update', 'create', 'delete', 'get']`返回undefined
super()
`['data', 'update', 'create', 'delete', 'get'].forEach()`.forEach()
  • 写JS 要么每句话加分号;要么 不可以括号开头
  • 不加分号报错?我还是选择不加分号

Model.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import EventBus from './EventBus'
class Model extends EventBus {
  constructor(options) {
    super()
    const keys = ['data', 'update', 'create', 'delete', 'get']
    keys.forEach((key) => {
      if(key in options) {
        this[key] = options[key]
      }
    })
  }

目前的代码

src/base/EventBus.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import $ from 'jquery'
class EventBus {
  constructor() {
    this._eventBus = $(window)
  }

  // 监听
  on(eventName, fn) {
    return this._eventBus.on(eventName, fn)
  }

  // 触发
  trigger(eventName, data) {
    return this._eventBus.trigger(eventName, data)
  }

  // 取消监听
  off(eventName, fn) {
    return this._eventBus.off(eventName, fn)
  }
}
export default EventBus

src/base/Model.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import EventBus from './EventBus'
class Model extends EventBus {
  constructor(options) {
    super()
    const keys = ['data', 'update', 'create', 'delete', 'get']
    keys.forEach((key) => {
      if(key in options) {
        this[key] = options[key]
      }
    })
  }
  create() {
    console?.error?.('未实现 create')
  }
  delete() {
    console?.error?.('未实现 delete')
  }
  update() {
    console?.error?.('未实现 update')
  }
  get() {
    console?.error?.('未实现 get')}
}
export default Model

src/base/View.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import $ from 'jquery'
import EventBus from './EventBus.js'
class View extends EventBus {
  constructor(options) {
    super()
    /* options: {el, html, render, data,
    eventBus, events, methods} */
    Object.assign(this, options)
    this.el = $(this.el)
    this.render(this.data)
    this.autoBindEvents()
    this.on('m:updated', () => {
      this.render(this.data)
    })
  }
  autoBindEvents(){
    for(let key in this.events) {
      const value = this[this.events[key]]
      const spaceIndex = key.indexOf(' ')
      const part1 = key.slice(0, spaceIndex)
      const part2 = key.slice(spaceIndex + 1)
      const valueMethod = this[this.events[key]]
      this.el.on(part1, part2, valueMethod)
    }
  }
}
export default View

src/app1.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import $ from 'jquery'
import './app1.css'
import Model from './base/Model.js'
import View from './base/View.js'
/* 数据相关放到 m */
const m = new Model({
  data: {
    n: parseFloat(localStorage.getItem('n')) || 100
  },
  update(data) {
    // 更新数据
    Object.assign(m.data, data)
    // 标记数据更新
    m.trigger('m:updated')
    localStorage.setItem('n', m.data.n.toString())
  }
})
const init = (el) => {
  new View ({
  // {el, html, render, data, eventBus, events, methods}
    // 容器
    el: el,
    data: m.data,
    // eventBus: eventBus,
    // 视图
    html: `
      <div>
        <div class="output">
          <span id="number">{{number}}</span>
        </div>
        <div class="actions">
          <button id="add1">+1</button>
          <button id="minus1">-1</button>
          <button id="mul2">×2</button>
          <button id="divide2">÷2</button>
          <button id="recovery">恢复</button>
        </div>
      </div>
  `,
    // 渲染
    render(data) {
      const n = data.n
      if (this.el.children.length !== 0) {
        this.el.empty()
      }
      $(this.html.replace('{{number}}', n))
        .prependTo(this.el)
    },
    events: {
      'click #add1': 'add',
      'click #minus1': 'minus',
      'click #mul2': 'mul',
      'click #divide2': 'div',
      'click #recovery': 'recover',
    },
    add() {
      m.update({n: m.data.n += 1})
    },
    minus() {
      m.update({n: m.data.n -= 1})
    },
    mul() {
      m.update({n: m.data.n *= 2})
    },
    div() {
      m.update({n: m.data.n /= 2})
    },
    recover() {
      m.update({n: m.data.n = 100})
    }
  })
}
// 导出初始化方法init 在main.js 中获取el <- #app1
export default init

src/app2.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import './app2.css'
import $ from 'jquery'
import Model from './base/Model'
import View from './base/View'

const localKey = 'app2.index'
const m = new Model({
  data: {
    index: parseInt(localStorage.getItem(localKey)) || 0
  },
  update(data) {
    Object.assign(m.data, data)
    m.trigger('m:updated')
    localStorage.setItem(localKey, m.data.index.toString())
  }
})
const init = (el) => {
  // let view = new View () 直接 new 不需再赋值 不需变量名 所有this代指对象实例
  new View({
    el: el,
    data: m.data,
    html: (index) => {
      return `
      <div>
        <ol class="tab-bar">
          <li class="${index === 0 ? 'selected' : ''}" data-index="0"><span>111111</span></li>
          <li class="${index === 1 ? 'selected' : ''}" data-index="1"><span>222222</span></li>
        </ol>
        <ol class="tab-content">
          <li class="${index === 0 ? 'active' : ''}">内容1</li>
          <li class="${index === 1 ? 'active' : ''}">内容2</li>
        </ol>
      </div>
    `
    },
    render(data) {
      const tabIndex = data.index
      if (this.el.children.length !== 0) {
        this.el.empty()
      }
      $(this.html(tabIndex)).appendTo(this.el)
    },
    events: {
      'click .tab-bar li': 'focus'
    },
    focus(e) {
      const tabIndex = parseInt(e.currentTarget.dataset.index)
      m.update({index: tabIndex})
    },
  })
}
// 导出初始化方法init 在main.js 中获取el <- #app2
export default init

src/main.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import './reset.css'
import './global.css'
// 导入 控制器 c
import x from "./app1.js"
import y from "./app2.js"

import './app1.js'
import './app2.js'
import './app3.js'
import './app4.js'
// 传初始化需要填入的节点
x('#app1')
y('#app2')

14.MVC 过渡到 vue §

安装 vue

1
yarn add vue

导入

1
2
import Vue from 'vue'
console.log(vue)

报错 runtime-only built of vue

  • 编译版 package.json
1
2
3
4
5
6
7
{
    // ...
    ,
      "alias": {
        "vue": "./node_modules/vue/dist/vue.common.js"
      }
}
  • template 就是 html,默认用当前的替换原来的div
  • m 也可以并入 v data: { n: parseFloat(localStorage.getItem('n')) || 100 },
  • 自动更新
  • 方法 监听函数放入 methods
  • 监听数据变化 放入 watch

vue改写app1.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import Vue from 'vue'
import './app1.css'
const init = (el) => {
  new Vue({
    el: el,
    data: {
      num: parseFloat(localStorage.getItem('n')) || 100
    },
    methods: {
      add() {this.num += 1},
      minus() {this.num -= 1},
      mul() {this.num *= 2},
      div() {this.num /= 2},
      recover() {this.num = 100}
    },
    watch: {
      num() {
        localStorage.setItem('n', this.num.toString())
      }
      // num : function() {} // 不可用箭头函数 this 失效
    },
    template: `
      <section id="app1" class="app1">
        <div class="output">
          <span id="number">{{num}}</span>
        </div>
        <div class="actions">
          <button @click="add" id="add1">+1</button>
          <button @click="minus" id="minus1">-1</button>
          <button @click="mul" id="mul2">×2</button>
          <button @click="div" id="divide2">÷2</button>
          <button @click="recover" id="recovery">恢复</button>
        </div>
      </section>
    `,
  })
}
export default init
  • template预封装html(),样式的动态绑定JS :class

vue改写app2.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import './app2.css'
import Vue from 'vue'
const init = (el) => {
  new Vue({
    el: el,
    data: {
      index: parseInt(localStorage.getItem('app2.index')) || 0
    },
    watch: {
      index() {
        localStorage.setItem('app2.index', this.index.toString())
      }
    },
    template: `
      <section id="app2" class="app2">
          <ol class="tab-bar">
            <li :class="index === 0 ? 'selected':''"
                @click="index = 0"><span>111111</span></li>
            <li :class="index === 1 ? 'selected':''"
                @click="index = 1"><span>222222</span></li>
          </ol>
          <ol class="tab-content">
            <li :class="index === 0? 'active': ''">内容1</li>
            <li :class="index === 1? 'active': ''">内容2</li>
          </ol>
      </section>
    `,
  })
}
// 导出初始化方法init(el) 在main.js 中获取el <- #app2: y('#app2')
export default init

抽离数据 app1.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import Vue from 'vue'
import './app1.css'
const init = (el) => {
  const m = {
    get() {
      return parseFloat(localStorage.getItem('n')) || 100
    },
    set(num) {
      localStorage.setItem('n', num.toString())
    }
  }
  new Vue({
    el: el,
    data: {
      num: m.get()
    },
    methods: {
      add() {this.num += 1},
      minus() {this.num -= 1},
      mul() {this.num *= 2},
      div() {this.num /= 2},
      recover() {this.num = 100}
    },
    watch: {
      num() {
        m.set(this.num)
      }
    },
    template: `...`,
  })
}
export default init

app2.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import './app2.css'
import Vue from 'vue'
const init = (el) => {
  const m = {
    get() {
      return parseInt(localStorage.getItem('app2.index')) || 0
    },
    set(index) {
      localStorage.setItem('app2.index', index.toString())
    }
  }
  new Vue({
    el: el,
    data: {
      index: m.get()
    },
    watch: {
      index() {
        set(this.index)
      }
    },
    template: `...`,
  })
}
// 导出初始化方法init(el) 在main.js 中获取el <- #app2: y('#app2')
export default init

vue 也实现了 EventBus 的继承

1
2
3
4
5
6
7
8
const eventBus = new Vue()
eventBus.$on()
eventBus.$emit() // trigger
eventBus.$off()
console.dir(eventBus)
console.log(eventBus.$on)
console.log(eventBus.$emit)
console.log(eventBus.$off)

vue 没有体现抽象思维3 用数据去渲染 view = render(data)

  • 监听数据变化,自动局部渲染
  • 元素没有被移除页面<button id="xxx"></button>数据改变刷新页面,id="xxx"还在
  • react显式地调用view = render(data)

15.总结 §

  • 再也不用DOM操作了,不用jQuery,直接在封装的元素上写逻辑,不需要id属性,id属性只给CSS用
  • 6个抽象思维
    • 抽象思维1 最小知识原则 js模块 模块解耦
    • 抽象思维2 以不变应万变 m + v + c
    • 抽象思维3 用数据去渲染 view = render(data)
    • 抽象思维4 表驱动编程-自动绑定事件 将不同的部分作为字符串存入hashMap 取用
    • 抽象思维5 俯瞰全局 EventBus
    • 抽象思维6 事不过三 class继承
  • 刻意用到项目中
  • 写文章总结



参考文章

相关文章


  • 作者: Joel
  • 文章链接:
  • 版权声明
  • 非自由转载-非商用-非衍生-保持署名