# Javascript

# VUE十个需要注意的细节(2.0)

TIP

关于为什么要使用VUE,很多已经熟悉并依赖 jquery 的前端小伙伴刚开始可能会不太能够理解,最开始我也是这么想的,主要原因还是开发的项目页面不够复杂,当一个页面无刷新交互以及组件足够复杂之后,你会发现用 jquery 将很难进行下去,更不用说 单页应用(spa)

这里我想要再次强调一个小细节,虽然在开发 VUE 的时候会被经常用到,当绝不一定只是在开发 VUE 才会运用到

当我们对某个对象或者数组 computed 或者 watch 时候,往往会遇到需要对此变量需要预处理的操作,这时候有可能涉及遍历,显然如果我们之间遍历原对象或者原数组明显不太合适,对于一维数组对象我们可以之间使用 Object.assign([], arr)Object.assign({}, obj) or [...arr]{...obj}来进行浅拷贝,但是对于一维以上的数组或者对象建议参考 深拷贝

关于 VUE 和 React 哪家强的问题,就目前全球市场而言 React 的占比确实第一,不过 VUE 作为一个 个人项目 并且出现时间 晚于 React 的情况下 github 上星数可以超越 React 这样 大厂出品 依旧是让人震惊

个人感觉 React 至今并没有什么神奇的地方时 VUE 所无法取代的,相反 VUE 的许多优点 React 却没有,就 中国的市场 个人觉得 VUE 和 React 应该算的上 平分秋色 ,因为国内 web 项目对 MVVM 的应用相比国外还是有一定差距的,所以再较晚的使用 JS 关于 MVVM 框架有了更多的选择招聘就可以看的出来,别的城市我不知道,但是就北京和南京来说,招聘信息上面 有需求 会 VUE 或者 React 的,甚至单招 VUE 熟手的,却很少看到单招 React 的

还有一个最关键的原因,vue api 丰富学习梯度比较缓和相比而言 react api 就比较少且对新手不友好,可以说熟练使用 vue 转 react 会轻而易举(毕竟 react 的api就那么几个且 vue 都有容易理解),反正则不一定 vue api 太丰富,想要巧妙合理的运用好每个 api 不太容易短期实现

这里我想要给大家推荐一个快速学习上手使用 VUE 不错的一个开源项目后台管理系统

vue-element-admin (opens new window)这个项目目前看来在github上关于VUE的项目星数最多的一个,作者更新也非常活跃几乎每天都有新的 commit ,提出 Issues 基本上能够得到很快的回应,也非常荣幸可以成为该项目contributors之一,虽然提交的代码可能并不算特别重要,但是第一次提交的代码被合并的感觉真的是超级开心

# 1. v-if && v-show (opens new window)

  • v-if 是“真正”的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建

  • v-if 也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块

相比之下,v-show 就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 进行切换

一般来说,v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销因此,如果需要非常频繁地切换,则使用 v-show 较好;如果在运行时条件很少改变,则使用 v-if 较好

TIP

template标签无法使用 v-show ,不信的可以试试

# 2. deep (opens new window)

对于watch一个对象时候,若需要深度 watcher 则一定需要设置 deep:true

对于watch一个对象时候,若需要深度 watcher 则一定需要设置 deep:true

对于watch一个对象时候,若需要深度 watcher 则一定需要设置 deep:true

TIP

注意监听数组的变动不需要这么做

# 3. v-bind (opens new window)

  • 简介: 只要 自己定义的 标签属性前面有 : 则后面引号里面写JS(变量、函数),否则写字符串

  • 作用: (:v-bind:语法糖 )

TIP

很多人学习是从官方教程开始看的,在看到 Class 与 Style 绑定 (opens new window) 这章节时候可能会有些误解,这里需要强调一点 v-bind 不仅仅用于style或者class属性一切属性皆可用,这才是关键

# 4. 子父组件通信

  • 父向子:props <my-component :name="name"></my-component> or <my-component name="马云海"></my-component>,这里需要注意一点在你提交的代码中,prop 的定义应该尽量详细, 至少需要指定其类型 ,当然如果你非要偷懒 props: ['status'] 这样定义,也没人可以咬你

  • 子向父: 子组件使用 API vm.$emit (opens new window) this.$emit('res', val) ,父组件使用 <my-component @res="fun"></my-component> ,然后在 methods 里面定义 fun(val){} 来获取值

如果是在看不懂的可以百度 "VUE子父"网上有很多,不过我依旧觉得我这里介绍的是最精辟的,哈哈

# 5. Vue全家桶成员

# Vue Router (opens new window)

  • 简介:

    • 使用起来很方便主要就是一个配置文件,里面主要配置了SPA页面URL命名以及关联文件位置
    • this.$router里面主要定义了路由的相关信息以及跳转等钩子方法
  • 作用: 管理SPA所有虚拟页面 配置、跳转、跳转历史

TIP

这里需要强调一下如果想用 history (opens new window) 模式务必需要后台配置支持,就是要在服务端配置 Rewrite 路由把所有路由都指向 index.html

# Vuex (opens new window)

  • 简介: Vuex特别像一个全局对象,他和全局对象的区别有以下两点
    • 响应式,数据变动更新组件
    • 不能直接修改,修改唯一途径mutation

TIP

mutation (opens new window) 必须是同步函数

action (opens new window) 类似于 mutation,不同在于:

  • action 提交的是 mutation,而不是直接变更状态
  • action 可以包含任意异步操作
  • 作用: 多页面(SPA虚拟页面)多组件数据及时同步、通信

# 6. key (opens new window)

官方文档上面的说明可能比较难懂,简单的说就是当一个 list 中的 li 上面的 key 没有变化,那么在数据变化渲染中这个 li 就不会被重复渲染

TIP

很多人喜欢用 index 作为 key, 不是不可以啊,但是当一个列表中间的 item 发生变化(增、删、改)时,渲染就会出现问题

# 7. nextTick && v-cloak (opens new window)

这两个点和 生命周期 mounted有点类似,当然功能以及用法上面肯定有一定的区别

  • nextTick

这个算是VUE里面比较少见,但是很重要的接口官方解释:在下次 DOM 更新循环结束之后执行延迟回调在修改数据之后立即使用这个方法,获取更新后的 DOM是不是感觉挺难理解的,所以这里我们直接上一个实例:

// 当我们使用element UI时候,这里的作用是当element 组件el-input渲染完成后获取焦点
this.$nextTick(_ => {
  this.$refs.input.focus()
})
1
2
3
4

这里如果我们直接使用 this.$refs.input.focus() 是没用的不信你可以尝试一下,这里的很细节,若你不会并没人咬你,但是如果你注意这些细节会让用户体验发生质变

  • v-cloak

这里呢类似上面那个问题,一样的就算你不做处理程序不是跑步起来,只是体验不好并且这里多半用于一些非 SPA 项目上面,有些MVC项目为了追逐潮流硬是在MVC基础上使用 VUE Element 等套件时候,会发现一个诡异的事情,就是网页在加载的最初会出现未被VUE渲染的代码比如 {{name}} ,直到VUE编译完成才会被编译成你想要它展示的变量比如:小王

官方说明:

这个指令保持在元素上直到关联实例结束编译和 CSS 规则如 [v-cloak] { display: none } 一起用时,这个指令可以隐藏未编译的 Mustache 标签直到实例准备完毕

<style>
[v-cloak] {
  display: none;
}
</style>
<div v-cloak>
  {{ message }}
</div>
1
2
3
4
5
6
7
8

不会显示,直到编译结束

# 8. v-text && v-html (opens new window)

这两个指令有一定的区别,第一个HTML标签不会被渲染,第二个不会被VUE渲染,用到的地方虽然不多但是真当你遇上不会的话还是很恼火的

  • v-text

这个和使用“Mustache”语法 (双大括号) 的文本插值是一样的,细心的小伙伴会发现如果试图通过 {{html}} 来给标签插入html代码是不会被解析的

  • v-html

官方说明:

更新元素的 innerHTML 注意:内容按普通 HTML 插入不会作为 Vue 模板进行编译 如果试图使用 v-html 组合模板,可以重新考虑是否通过使用组件来替代

本项目是基于 vuepress (opens new window) 所以当我在这个当前页面需要输入{{name}}的时候,必须用下面这样的代码,如果直接使用写的话会被VUE编译,并提示报错找不到 name 变量

<span v-html="'{{name}}'"></span>
1

# 9. set (opens new window)

官方说明:

向响应式对象中添加一个属性,并确保这个新属性同样是响应式的,且触发视图更新它必须用于向响应式对象上添加新属性,因为 Vue 无法探测普通的新增属性 (比如 this.myObject.newProperty = 'hi' )

this.$set(this.myObject,'newProperty','hi')
1

# 10. 自定义组件强化

只要弄清楚以上9点对于VUE应用开发基本上就没有太大问题了,关于自定义组件的封装其实只要熟练掌握 子父组件通信 也足以解决日常开发需求,下面介绍的只是更深层次一点,对于新手而言不懂也没有太大关系尤其是slot插槽这块 VUE 3.0 会对此API有大幅度变动,虽然会有兼容方案

我们可以 @clickdom 上面绑定原生事件,但是在普通组件上面却不起作用这里我们就需要 .native 修饰符官方文档说的很清楚,这里还有一种解决方案就是可以配合 v-on="$listeners" (opens new window) 将所有的原生事件监听器指向这个组件的某个特定的子元素,这样就可以用不加修饰符也能够直接监听的到

显而易见就是官方提供了一个 API 让你能够在自己封装组件上面也可以使用 v-model 双向绑定,这个细节多用于封装自己的表单类组件或者二次封装别人的表单类组件用,并且对 element 这样的UI 进行单组件的二次封装我觉得意义不是很大,感兴趣的可以研究一下,这里我就不做过多解释官网说明的很清楚点击标题可以快速跳转官方文档的相关位置,或者参考 sync-修饰符 (opens new window)

TIP

无论 v-model (opens new window) 还是 sync-修饰符 (opens new window) 实际上是一个实现双向绑定的语法糖

<!-- 在原生表单元素中 -->
<input v-model="searchText">
<!-- 相当于 -->
<input
  v-bind:value="searchText"
  v-on:input="searchText = $event.target.value"
>

<!-- 在自定义组件中 -->
<my-component v-model="inputValue" />
<!-- 相当于 -->
<my-component
  v-bind:value="inputValue"
  v-on:input="inputValue = argument[0]"
/>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

最主要的作用就是可以和 HTML 元素一样,我们经常需要向一个组件传递内容。通常是给组件传递一些 HTML 代码。

<slot> 元素作为组件模板之中的内容分发插槽。<slot> 元素自身将被替换,也可以在封装组件时候预留一下默认值,多的不说直接上代码我觉得会比较直观一些。

<script src="https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js"></script>
<div id="app">
  <alert-box>
    <b>Something bad happened.</b>
  </alert-box>
</div>
<script>
  Vue.component('alert-box', {
    template: `
      <div class="demo-alert-box">
        <strong>Error!</strong>
        <slot><b>这里可以定义默认值</b></slot>
      </div>
    `
  })
  const vm = new Vue({
    el: '#app'
  })
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

上面的代码会被解析成

<div class="demo-alert-box">
  <strong>Error!</strong>
  <b>Something bad happened.</b>
</div>
1
2
3
4

TIP

值得一提的这个 <slot></slot> 里面依然可以插入子组件并被解析

我先说一下使用场景,假设我们封装一个 使用echart渲染的图标组件,当我们这个页面多次使用此插件时候,你会发现如果你使用 this.chart = echarts.init(document.getElementById('chart')) 会出现id重复的情况,VUE就提供了一个dom选择器的API vm.$el 用来访问实例挂载之后的元素。

// ... 此处代码省略
mounted() {
  this.chart = echarts.init(this.$el)
  this.setOption(option)
},
// ... 此处代码省略
1
2
3
4
5
6

自定义指令完全能够实现一个小组件的功能,在一些全局用到的小方法上面非常好用,比如按压效果、角标、曝光监听封装等

混入当封装类似组件的时候如果有部分代码重复,就可以使用 mixins,比如一些通用监听、埋点

TIP

值得一提这里我想要强调一下 mixins 的语法规范,否则很容易出现一些找不到在哪定义的方法或者重名问题

  • 数据对象在内部会进行递归合并,并在发生冲突时以组件数据优先

  • 混入对象的钩子将在组件自身钩子之前调用

  • 附带一个 命名空间 (opens new window) 以回避和其它作者的冲突 (比如 $_yourPluginName_)

  • 如果使用了上述方法还需要注意一点,以 _$ 开头的 property 不会被 Vue 实例代理,因为它们可能和 Vue 内置的 property、API 方法冲突。你可以使用例如 vm.$data._property 的方式访问这些 property,详细参考文档 data (opens new window)

# 优雅地巧妙使用运算符

首先简单介绍两个最重要的运算符 && ||,学过数字逻辑电路的小伙伴应该知道与或非门是组成逻辑电路最关键的核心

稍微学过点编程的应该都听说过 "同真且真,同假或假" 即便听起来很简单,但是我仍然不建议大家死记硬背,只要理解与或机制判断真假易如反掌

TIP

  • && 从左往右执行遇到 false或者末尾 返回当前,终止该行否则继续,比如
0 && 1 && 2 // 0
1 && 2 && 3 // 3
1 && 0 && 3 // 0
1
2
3
  • || 从左往右执行遇到 true或者末尾 返回当前,终止该行否则继续,比如
0 || 1 || 2 // 1
1 || 2 || 3 // 1
0 || false || 3 // 3
1
2
3
  • && 优先级高于 ||

关于三元运算符这里就不多介绍了(不懂就百度网上很多),下面直接上几个例子

# 运算符判断

const a = true
const b = 1
const c = 2


if (a) {
  console.log(b)
}
// && 代替 if 判断
a && console.log(b)


if (a) {
  console.log(b)
} else {
  console.log(c)
}
// 三元运算代替 if else
a ? console.log(b) : console.log(c)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 运算符switch

const b =2
switch (b) {
  case 1:
    console.log('走路')
    break
  case 2:
    console.log('坐汽车')
    break
  case 3:
    console.log('坐轮船')
    break
  default:
    console.log('走路')
    break
}
// 运算符实现的 switch 语句
(b === 1 && (console.log('走路'), !0))
|| (b === 2 && (console.log('坐汽车'), !0))
|| (b === 3 && (console.log('坐轮船'), !0))
|| console.log('走路')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

如果看到这里有的人可能就要说了"花里胡哨没觉着有多优雅",不急咱们来看个需求:

项目基于vue,实现一个 td 标签 里面值为 type 枚举值(1:未付款, 2:已发货, 3:交易成功)

<td v-if="type === 1">未付款</td>
<td v-if="type === 2">已发货</td>
<td v-if="type === 3">交易成功</td>

<!-- 运算符 -->
<td v-text="type === 1 && '未付款' || type === 2 && '已发货' || type === 3 && '交易成功'" />
1
2
3
4
5
6

是不是瞬间觉得下面的写法很简洁,你还可以巧妙运用在 :class:style上面,当然上面这个例子还不是最实用的,如果一个 class 或者 style 的值需要因多个值判断多个枚举值且有优先级考虑且只能展示一个,这时直接用 hasIcon1 && 'icon1' || hasIcon2 && 'icon2' || hasIcon3 && 'icon3' 搞定会显得格外清爽

# 运算符取整

通常我们取整会用到 parseInt() ,但是实际上连续两次取反即可达到同一目的


parseInt(45.95)
// 45
parseInt(45.05)
// 45
parseInt(4)
// 4
parseInt(-45.05)
// -45
parseInt(-45.95)
// -45

~~45.95
// 45
~~45.05
// 45
~~4
// 4
~~-45.05
// -45
~~-45.95
// -45
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# let、const、var区别

# let、const、var作用域

对于这个概念其实网上说的最多的就是 块作用域 函数作用域,这个算是一个新手比较难以理解的问题,下面看段代码理解一下

// 块作用域
{
    var a = 1;
    let b = 2;
    const c = 3;
    console.log(a) // 1
    console.log(b) // 2
    console.log(c) // 3
}
console.log(a) // 1
console.log(b) // 报错 b is not defined
console.log(c) // 报错 c is not defined

// 函数作用域
(function () {
    var d = 5
    let e = 6
    const f = 7
    console.log(d) // 5
    console.log(e) // 6
    console.log(f) // 7
})();
console.log(d) // 报错 d is not defined
console.log(e) // 报错 e is not defined
console.log(f) // 报错 c is not defined
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

由此可见

  • 除了 var 能够跨越块作用域 {},let、const不能跨越快作用域

  • let、const、var 都不能跨越函数作用域 (function(){})()

实际上 var 之所以能够跨越块作用域实际上,在非函数作用域中 var 赋值实际上就是对 window 赋值,下面看段代码就能很容易理解

window.print() // 浏览器会弹出打印

var print = () => {
    console.log('window.print()方法以及被改变啦')
}
window.print() // 浏览器不再弹出打印 而是控制台打印 'window.print()方法以及被改变啦'
1
2
3
4
5
6

let、const、var作用域区别应该是他们最重要也是最容易引起出错的区别,没看懂的可以多看看多写写多试试就好了

# let、const、var其他区别

  • const 的值不能改变,其他两个的赋值可以被改变,举个简单的例子
const a = 1
a = 2 // 报错 Assignment to constant variable
1
2

TIP

这里值得一提的是数组对象记录的是地址且不变所以在定义数组对象时候直接用 const

// 所以这样是没问题的
const a = []
a.push(1)
const b = {}
b.name = 'mayunhai'
1
2
3
4
5

这里关于地址这块也是新手非常容易模糊出错的地方,经常会莫名的发现连续同时调用同一个对象或者数组的时候,第二次值会覆盖掉第一次的值,遇到这类问题处理起来其实也很简单,使用浅拷贝([...arr] {...obj})或者深拷贝即可

  • 正因为 const 的值不能改变所以在初始化的时候必须赋值,其他则不需要,比如
let a
var b
console.log({a, b}) // {a: undefined, b: undefined}

const c // 报错 Missing initializer in const declaration
1
2
3
4
5
  • var存在变量提升,let、const不存在 "变量提升"乍一看有点难理解,其实只要记住下面一句话就很容易理解

let、const必须准守 "先准守,后使用" 的原则,来直接看代码

console.log(a) // undefined
var a = 1
console.log(b) // 报错 Cannot access 'b' before initialization
let b = 1
console.log(c) // 报错 Cannot access 'c' before initialization
const c = 1
1
2
3
4
5
6
  • var可以重复申明,let、const则不需
var a = 1
var a = 2
console.log(a)

let b = 1
let b = 2 // 报错 Identifier 'b' has already been declared

const c = 1
const c = 2 // 报错 Identifier 'c' has already been declared

var a = 1
let a = 2 // 报错 Identifier 'a' has already been declared
const a = 2 // 报错 Identifier 'a' has already been declared
1
2
3
4
5
6
7
8
9
10
11
12
13

# webpack 4 动态加载依赖

关于 webpack 4 动态异步 import 依赖有细微的调整,文档详见 dynamic-imports (opens new window)

webpack 被大部分前端吐槽其最根本原因是配置复杂且文档不友好(英文难懂,中文不全),所有就有了 Webpack 工程师, 前端打包工程师Webpack 配置官...

下面我们看个实际例子关于 dynamic-imports (opens new window)

  • 需求: 在国际化配置语言为中文的时候,菜单搜索支持拼音搜索

  • 需求分析: 这里我们最关键的就是要在语言变为中文的时候,菜单搜索数据得附加上拼音,我们这里可以用 pinyin 这个插件可以有效帮助我们把中文转化成汉语拼音

  • 关键代码:

// package.json 前省略
"pinyin": "2.9.0",
// package.json 后省略
1
2
3






 



















// HeaderSearch.vue 前省略
methods: {
  async searchPool(list) {
    // 如果语言是中文则支持拼音搜索
    if (this.lang === 'zh') {
      // 动态加载依赖 pinyin 【关键点】
      const { default: pinyin } = await import('pinyin')
      if (Array.isArray(list)) {
        list.forEach(element => {
          const title = element.title // 这里 title 设计为数组考虑多级菜单
          if (Array.isArray(title)) {
            title.forEach(v => {
              v = pinyin(v, {
                style: pinyin.STYLE_NORMAL // pinyin 配置入参:STYLE_NORMAL 输出拼音不带声调
              }).join('')
              element.pinyinTitle = v // 新增字段 pinyinTitle 作为 title 的拼音转化
            })
          }
        })
      }
    }
    this.initFuse(list) // 初始化模糊搜索
  }
}
// HeaderSearch.vue 后省略
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

TIP

由上面可见最大的变化就是 多了一个 默认值,官方的原文是这样的:

The reason we need default is that since webpack 4, when importing a CommonJS module, the import will no longer resolve to the value of module.exports

我们需要默认值的原因是,由于webpack 4在导入CommonJS模块时,导入将不再解析为module.exports的值

# setTimeout的理解

setTimeout 看起来实现的假装异步操作实则还是同步运行,其实每次执行的时机都是晚于理想状态的,下面看段代码就能很容易理解


 













setTimeout(() => {
  console.log('setTimeout')
}, 0)
const timestamp = new Date().getTime()
while (timestamp + 3000 > new Date().getTime()) {
  // 连续三秒钟死循环打印 'loop'
  console.log('loop')
}
console.log('done')
// loop
// ...(持续很久重复打印loop)
// loop
// done
// setTimeout
1
2
3
4
5
6
7
8
9
10
11
12
13
14

上面这段代码直接复制在 chrome 的 Console 打印会发现 'setTimeout' 并不能在0毫秒后打印出来,而是在三秒甚至三秒更多的时间后打印出来

由于我的电脑比较卡,外加死循环执行 20多秒之后才能够正常打印 'setTimeout',且用心的小伙伴会发现 'done' 永远在 'setTimeout' 之前打印

这样 JS 执行顺序原则是不是一目了然了

TIP

js事件处理机制:单线程+事件队列

事件队列中的任务执行的条件:

①主线程已经空闲

②任务满足触发条件:

  • 定时函数(延迟时间已经达到)

  • 事件函数(特定事件被触发。譬如:点击)

  • ajax回调函数(服务器端有数据响应)

也就是说JS所谓的异步不过是把需要的异步代码先存入一个事件队列,当主线程空闲后再依次执行而不是真正多线程并行异步,而且虽然事件队列执行时遇到报错不会阻塞JS,但是死循环依旧会阻塞。

虽然JS是单线程的,但是HTML5 提出 Web Worker 标准,允许 JavaScript 脚本创建多个线程,详见 JS 多线程编程

# JS 多线程编程

Javascript 因为单线程的原因导致只要一步阻塞,整段垮掉,而且对于目前基本上都是多核 CPU 的终端显得格格不入(单线程只能使用到单核 CPU 进行计算)

HTML5 提出 Web Worker 标准,允许 JavaScript 脚本创建多个线程,但是子线程完 全受主线程控制,且不得操作 DOM。

TIP

首先我先总结一下想要使用 Web Worker 的几个注意点:

  • 同源限制,分配给 Worker 线程运行的脚本文件,必须与主线程的脚本文件同源

  • DOM 限制, Worker 线程所在的全局对象,与主线程不一样,无法读取主线程所在网页的 DOM 对象,也无法使用document、window、parent这些对象。但是,Worker 线程可以navigator对象和location对象

  • 通信联系,Worker 线程和主线程不在同一个上下文环境,它们不能直接通信,必须通过消息完成(与iframe子父通信一样)

  • 脚本限制, Worker 线程不能执行alert()方法和confirm()方法,但可以使用 XMLHttpRequest 对象发出 AJAX 请求

  • 文件限制,Worker 线程无法读取本地文件,即不能打开本机的文件系统,它所加载的脚本,必须来自网络

# 用法一:引入两个 JS 使用Web Worker

// worker.js

/**
 * @description 死循环函数函数
 * @param {Number} time 死循环时间,单位毫秒
 */
const endlessLoop = (time) => {
  const timestamp = new Date().getTime()
  while (timestamp + time > new Date().getTime()) {}
}
endlessLoop(3000)
console.log('endlessLoop done') // 死循环三秒后打印 'endlessLoop done'
1
2
3
4
5
6
7
8
9
10
11
12
// index.js
const worker = new Worker('worker.js')
console.log('这里是 index.js 的打印')
1
2
3

# 用法二:引入单个 JS 使用Web Worker









 





// index.js
// 创建 woker
const workerBlob = new Blob([`const endlessLoop = (time) => {
  const timestamp = new Date().getTime()
  while (timestamp + time > new Date().getTime()) {}
}
endlessLoop(3000)
console.log('endlessLoop done')`], { type: 'application/javascript' })
const workerJs = URL.createObjectURL(workerBlob)

// 调用 worker
const worker = new Worker(workerJs)
console.log('这里是 index.js 的打印')
1
2
3
4
5
6
7
8
9
10
11
12
13

# 用法三:webpack 中使用 Web Worker

目前前端项目主流打包工具 webpack 中若想要使用 Web Worker,须安装 worker-loader (opens new window) 这个依赖,感兴趣的小伙伴直接点链接跳转官方看文档这里就不做复制粘贴了

TIP

最后总结一下 Web Worker 绝对和 异步编程 Promise 不是一个概念

Promise 虽然能够实现异步任务,但仍然是单线程(始终只用了单核cpu),包括定时器 setTimeout

# 15行简单实现 Promise

看了网上很多关于用 ES5 实现 Promise 原理的文章,我这里只是想要简单分析 Promise 原理帮助对其使用,所以本文还是坚持使用 ES6 去简单实现功能(ES5 语法恶心并不方便新手去理解

直接上代码,其实想要实现一个 Promise 对象,其中关键代码并不多

TIP

如果对 this.resolveFn && this.resolveFn(val) 不懂的小伙伴

建议先阅读优雅地巧妙使用运算符

class MyPromise {
  constructor(fn) {
    fn(this.resolve, this.reject)
  }
  resolve = (val) => {
    this.resolveFn && this.resolveFn(val)
  }
  reject = (val) => {
    this.resolveFn && this.rejectFn(val)
  }
  then(resolveFn, rejectFn) {
    this.resolveFn = resolveFn
    this.rejectFn = rejectFn
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

验证一下

const fn = function (resolve, reject) {
  console.log('begin to execute!')
  setTimeout(() => {
    resolve(1)
    reject(2)
  }, 1000)
}

// 控制台打印 'begin to execute!'
const p = new MyPromise(fn)

// 一秒后 控制台打印
// resolve:  1
// reject:  2
p.then(function (data) {
  console.log('resolve: ', data)
}, function (data) {
  console.log('reject: ', data)
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

细心的小伙伴可能会发现其实在 new 的时候, fn 就已经开始执行了所以会打印 begin to execute! , 对比一下原生的 Promise

var p1 = new Promise(fn)

// 一秒后 控制台打印
// resolve:  1
p1.then(function (data) {
  console.log('resolve: ', data)
}, function (data) {
  console.log('reject: ', data)
})
1
2
3
4
5
6
7
8
9

事实上在 Promise规范 当中,规定 Promise 只能从初始 pending 状态变到 resolved 或者 rejected 状态,是单向变化的,也就是说执行了 resolve 就不会再执行 reject ,反之亦然。实现起来也很简单加个判断即可

class MyPromise {
  constructor(fn) {
    this.status = 'pending'
    fn(this.resolve, this.reject)
  }

  resolve = (val) => {
    if (this.status === 'pending') {
      this.status = 'resolved'
      this.resolveFn && this.resolveFn(val)
    }
  }

  reject = (val) => {
    if (this.status === 'pending') {
      this.status = 'rejected'
      this.rejectFn && this.rejectFn(val)
    }
  }

  then(resolveFn, rejectFn) {
    this.resolveFn = resolveFn
    this.rejectFn = rejectFn
  }
}
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

上面的问题解决了,我们再来看另外一个问题,假设我们 .then 是在 pending 状态改变之后之后调用,则又会发现另外的问题如下

const fn = function (resolve, reject) {
  setTimeout(() => {
    resolve(1)
    reject(2)
  }, 1000)
}

const p = new MyPromise(fn)

setTimeout(() => {
  p.then(function (data) {
    console.log('resolve: ', data)
  }, function (data) {
    console.log('reject: ', data)
  })
}, 1500)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

这样既不会打印 resolve 也不会打印 resolve ,虽然这种情况在实际开发几乎见不到,但不得不说它是一个bug,不过解决方案也很简单

class MyPromise {
  constructor(fn) {
    this.status = 'pending'
    fn(this.resolve, this.reject)
  }

  resolve = (val) => {
    if (this.status === 'pending') {
      this.status = 'resolved'
      this.res = val
      this.resolveFn && this.resolveFn(val)
    }
  }

  reject = (val) => {
    if (this.status === 'pending') {
      this.status = 'rejected'
      this.res = val
      this.rejectFn && this.rejectFn(val)
    }
  }

  then(resolveFn, rejectFn) {
    this.resolveFn = resolveFn
    this.rejectFn = rejectFn
    if (this.status !== 'pending') {
      this.status === 'resolved' && resolveFn(this.res)
      this.status === 'rejected' && rejectFn(this.res)
    }
  }
}
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

只要在 then 里面对 status 进行一个判断即可。再看下面这个问题

var p1 = new MyPromise(fn)
// resolve2: 1
p1.then(function (data) {
  console.log('resolve1: ', data)
}, function (data) {
  console.log('reject1: ', data)
})
p1.then(function (data) {
  console.log('resolve2: ', data)
}, function (data) {
  console.log('reject2: ', data)
})
1
2
3
4
5
6
7
8
9
10
11
12

当我们一开始就连续调用两次 .then 的时候会发现,只会执行一次 resolveFn or rejectFn,且都是第二次 .then 的方法,因为第一次被覆盖了,对于这种问题数组就能很好的解决

class MyPromise {
  constructor(fn) {
    this.status = 'pending'
    this.resolveFnArr = []
    this.rejectFnArr = []
    fn(this.resolve, this.reject)
  }

  resolve = (val) => {
    if (this.status === 'pending') {
      this.status = 'resolved'
      this.res = val
      if (this.resolveFnArr.length > 0) {
        this.resolveFnArr.forEach(v => v(val))
      }
    }
  }

  reject = (val) => {
    if (this.status === 'pending') {
      this.status = 'rejected'
      this.res = val
      if (this.rejectFnArr.length > 0) {
        this.rejectFnArr.forEach(v => v(val))
      }
    }
  }

  then(resolveFn, rejectFn) {
    this.resolveFnArr.push(resolveFn)
    this.rejectFnArr.push(rejectFn)
    if (this.status !== 'pending') {
      this.status === 'resolved' && resolveFn(this.res)
      this.status === 'rejected' && rejectFn(this.res)
    }
  }
}
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

TIP

即便这样,相比原生的Promise还是存在很多问题,不过 Promise 大致原理已经一目了然,在以后的使用中自然胸有成竹,得心应手

如果想要了解更多,百度 ES5实现Promise 看更详细更全面的实现

# 17行手写 promiseAll

TIP

const p = Promise.all([p1, p2, p3])

  • 只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
  • 只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。
const promiseAll = (arr) => {
  return new Promise((resolve, reject) => {
    let times = 0
    const res = []
    for (const i in arr) {
      arr[i].then(r => {
        res[i] = r
        times++
        if (times === arr.length) {
          resolve(res)
        }
      }).catch(e => {
        reject(e)
      })
    }
  })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

使用方法如下:

const p1 = new Promise((resolve, reject) => {
  resolve('hello')
})

const p2 = new Promise((resolve, reject) => {
  resolve('asda')
})

const p3 = new Promise((resolve, reject) => {
  resolve(1231)
})

promiseAll([p1, p2, p3]).then(res => {
  console.log(res) // ['hello', 'asda', 1231]
})
Promise.all([p1, p2, p3]).then(res => {
  console.log(res) // ['hello', 'asda', 1231]
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const p1 = new Promise((resolve, reject) => {
  resolve('hello')
})

const p2 = new Promise((resolve, reject) => {
  resolve('asda')
})

const p3 = new Promise((resolve, reject) => {
  reject(1231)
})

promiseAll([p1, p2, p3]).catch(res => {
  console.log(res) // 1231
})
Promise.all([p1, p2, p3]).catch(res => {
  console.log(res) // 1231
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 20行简单实现全局事件 EventBus

首先先介绍一下关于 EventBus 应用场景,一句话跨组件通信,当然大型复杂项目还是推荐使用 Vuex 去进行状态管理跨组件响应式通信

熟悉 node.js 的小伙伴应该知道在 node events 模块里面有个 EventEmitter 类,通过 require("events") 即可访问该模块,这里我也对其使用方法不多做介绍详细 Node.js EventEmitter (opens new window) 点击链接自己看

但是前端开发并没有这样的类,参考 VUE 源码我发现其实实现起来并不困难,VUE 源码我就不贴出来了感兴趣的小伙伴自行下载源码 ctrl + p 输入 events.js 即可轻松找到

这里我用 20 行比较简单透明的 ES6 语法进行实现

class EventBus {
  constructor() {
    this.events = {}
  }
  on = (eventName, fn) => {
    this.events[eventName] = this.events[eventName] || []
    this.events[eventName].push(fn)
  }
  off = (eventName) => {
    this.events[eventName] && delete this.events[eventName]
  }
  emit = (eventName, ...params) => {
    if (this.events[eventName]) {
      this.events[eventName].forEach(v => {
        v.apply(this, params)
      })
    }
  }
}
export default new EventBus()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

使用方法如下:

// a.js
import EventBus from '@/utils/EventBus' // 根据对应路径引入
EventBus.on('pause-video', function(a, b, c) {
  console.log('it works')
  console.log({ a, b, c })
})
1
2
3
4
5
6
// b.js
import EventBus from '@/utils/EventBus' // 根据对应路径引入
EventBus.emit('pause-video', 1) // it works   {a: 1, b: undefined, c: undefined}

1
2
3
4

TIP

  • 这里我想说一下之所以 on 方法使用了数组 push 的原因很简单就是为了支持同名注册多个全局事件

  • once 方法我上面没写了,因为使用并不多,实现也非常简单,触发的同时删除 off 即可,只要在上面的类里面加上下面代码即可

  once = (eventName, fn) => {
    function on() {
      this.off(eventName)
      fn.apply(this, arguments)
    }
    this.on(eventName, on)
  }
1
2
3
4
5
6
7

# async函数容易忽视的问题

async 给处理 Promise 对象带来的极大的方便,详细用法参照 es6.ruanyifeng async (opens new window)

TIP

asyncGenerator 的语法糖,主要区别在用于他自带触发器

async 函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而 await 命令就是内部 then 命令的语法糖,我们来看两个例子

  • # 例一

(async() => {
  console.time('time')
  const res1 = await new Promise(function(resolve, reject){
    setTimeout(() => {
      resolve(1)
    }, 2000);
  })
  const res2 = await new Promise(function(resolve, reject){
    setTimeout(() => {
      resolve(2)
    }, 3000);
  })
  console.log({res1, res2}) // {res1: 1, res2: 2}
  console.timeEnd('time')   // time: 5001.3740234375ms
})()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  • # 例二

(async() => {
  console.time('time')
  p1 = new Promise(function(resolve, reject){
    setTimeout(() => {
      resolve(1)
    }, 2000);
  })
  p2 = new Promise(function(resolve, reject){
    setTimeout(() => {
      resolve(2)
    }, 3000);
  })

  const res1 = await p1
  const res2 = await p2
  console.log({res1, res2}) // {res1: 1, res2: 2}
  console.timeEnd('time')   // time: 3007.679931640625ms
})()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

例一应该是经常容易被写出来的一种情况,大部分情况我们都会想要让两个 Promise 对象并行执行,但是显然这样写会依次执行,那么怎么解决呢?很简单先定义再 await,有的人可能会疑惑这样的话,同一个 Promise 对象岂不是被执行了两次,其实不是的,Promise 对象在被定义的时候就会被执行,只会被执行一次且是单向的(要么 resolve,要么 reject),如果对此模糊的小伙伴建议阅读 15行简单实现 Promise

# 遍历同步异步

废话不多说三种情况,直接看代码结果自行领悟

初始条件

const arr = [1, 2, 3];
const sleep = function(i) {
  return new Promise((resolve) =>{
    setTimeout(() => {
      resolve(i)
    }, i*1000)
  })
}
1
2
3
4
5
6
7
8

# 情况一 sleep异步,console.log("Finished async")异步

arr.forEach(async (i) => {
	await sleep(5 - i)
	console.log(i)
})
console.log("Finished async")
// Finished async
// 3
// 2
// 1
1
2
3
4
5
6
7
8
9

# 情况二 sleep异步,console.log("Finished async")同步

Promise.all(arr.map(async (i) => {
	await sleep(5 - i)
	console.log(i)
}))
console.log("Finished async")
// 3
// 2
// 1
// Finished async
1
2
3
4
5
6
7
8
9

# 情况三 sleep同步,console.log("Finished async")同步

await arr.reduce(async (memo, i) => {
	await memo
	await sleep(5 - i)
	console.log(i)
}, undefined)
console.log("Finished async")
// 1
// 2
// 3
// Finished async
1
2
3
4
5
6
7
8
9
10

TIP

总结:如果您需要一个一个地运行它们,请使用reduce

# IntersectionObserver监听可视区域

Intersection Observer API (opens new window) 算是一个比较少见的 API,但是它对于一些可视区域监听,性能方面远高于传统的 onscroll + 去抖监听,应用场景也比较多比如懒加载、埋点上报、滚动特效,这里我写个懒加载的 在线demo (opens new window) 体验一下,核心代码如下

const callback = function(entries) {
  entries.forEach(entrie => {
    const el = entrie.target
    if (entrie.intersectionRatio) {
      el.src = el.dataset.src
      observer.unobserve(el)
    }
  })
}
const options = { rootMargin: "100px" } // 预加载高度(距离100px则开始懒加载)
const observer = new IntersectionObserver(callback, options)
document.querySelectorAll(".lazy").forEach(el => {
  observer.observe(el)
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14

TIP

值得提的一点,可以通过配置 IntersectionObserver.thresholds 来对进入可视区域不同比例时监听

未完待续...