Giter Site home page Giter Site logo

blog's People

Contributors

wanqihua avatar wanqihua-12 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

blog's Issues

【前端词典】一文读懂单页应用和多页应用的区别

前言

最近看到一些人在问单页面和多页面应用的区别。因为最近在整理 Vue 相关的内容,所以也就输出这一篇短文希望可以给你一个整体的认识。

这里也会大体介绍单页应用实现的核心 —— 前端路由。

单页应用 VS 多页应用

直观对比图

单页应用( SinglePage Application,SPA )

指只有一个主页面的应用,一开始只需加载一次 js, css 等相关资源。所有的内容都包含在主页面,对每一个功能模块组件化。单页应用跳转,就是切换相关组件,仅刷新局部资源。

多页应用( MultiPage Application,MPA )

指有多个独立的页面的应用,每个页面必须重复加载 js, css 等相关资源。多页应用跳转,需要整页资源刷新。

两者对比表格:

对比项 \ 模式 SPA MPA
结构 一个主页面 + 许多模块的组件 许多完整的页面
体验 页面切换快,体验佳;当初次加载文件过多时,需要做相关的调优。 页面切换慢,网速慢的时候,体验尤其不好
资源文件 组件公用的资源只需要加载一次 每个页面都要自己加载公用的资源
适用场景 对体验度和流畅度有较高要求的应用,不利于 SEO(可借助 SSR 优化 SEO) 适用于对 SEO 要求较高的应用
过渡动画 Vue 提供了 transition 的封装组件,容易实现 很难实现
内容更新 相关组件的切换,即局部更新 整体 HTML 的切换,费钱(重复 HTTP 请求)
路由模式 可以使用 hash ,也可以使用 history 普通链接跳转
数据传递 因为单页面,使用全局变量就好(Vuex) cookie 、localStorage 等缓存方案,URL 参数,调用接口保存等
相关成本 前期开发成本较高,后期维护较为容易 前期开发成本低,后期维护就比较麻烦,因为可能一个功能需要改很多地方

单页应用实现 —— 前端路由

前端路由的核心:改变视图的同时不会向后端发出请求。

这里我讲讲 vue-router 路由的两种模式:hash & history

hash 模式

hash 模式背后的原理是 onhashchange 事件。

window.addEventListener('hashchange',function(e) { 
    console.log(e.oldURL);  
    console.log(e.newURL) 
},false);

通过 window.location.hash 属性获取和设置 hash 值。

由于 hash 发生变化的 url 都会被浏览器记录下来,所以浏览器的前进后退可以使用,尽管浏览器没有请求服务器,但是页面状态和 url 关联起来。后来人们称其为前端路由,成为单页应用标配。

hash 模式的特点在于 hash 出现在 url 中,但是不会被包括在 HTTP 请求中,对后端没有影响,不会重新加载页面。

history 模式

利用了 HTML5 History Interface 中新增的 pushState()replaceState(),它们提供了对历史记录进行修改的功能。

相关的 API:

history.pushState()

history.pushState(stateObj, title, url);
  1. state:一个与指定网址相关的状态对象,popstate 事件触发时,该对象会传入回调函数。如果不需要可填 null
  2. title:新页面的标题,但是所有浏览器目前都忽略这个值,可填 null
  3. url:新的网址,必须与当前页面处在同一个域。浏览器的地址栏将显示这个网址。

例如:history.pushState('new', 'new', 'new.html');

添加上面这个新记录后,浏览器地址栏立刻显示 ~/new.html,但并不会跳转到 new.html,它只是成为 history 中的最新记录。pushState 方法不会触发页面刷新,只是 history 对象变化,地址栏会变。

history.replaceState()

history.replaceState(stateObj, title, url);

参数同 pushState() 一样。

调用该方法,会修改当前的 history 对象记录,history.length 的长度不会改变

history.state

当前 URL 下对应的状态信息。如果当前 URL 不是通过 pushState 或者 replaceState 产生的,那么 history.statenull
当需要 state 和 URL 同步时可以使用 replaceState() 使之同步。

popstate 事件

同一个文档的 history 对象出现变化时,就会触发 popstate 事件。

不同的浏览器在加载页面时处理 popstate 事件的形式存在差异。页面加载时 Chrome 和 Safari 通常会触发 popstate 事件,但 Firefox 则不会。

注意:

调用 history.pushState() 或者 history.replaceState() 不会触发 popstate 事件. popstate 事件只会在浏览器某些行为下触发, 比如点击后退、前进按钮(或者调用 history.back()、history.forward()、history.go()方法)。

最后

如果你想进【大前端交流群】,关注公众号点击“交流加群”添加机器人自动拉你入群。关注我第一时间接收最新干货。

[译]现代框架存在的根本原因

原文链接:https://medium.com/dailyjs/the-deepest-reason-why-modern-javascript-frameworks-exist-933b86ebc445

前言

我曾见过许多人盲目地使用像 ReactAngularVue 这样的现代框架。这些框架提供了许多有趣的东西,但通常人们会忽略它们存在的根本原因。

并不是我们所想的以下原因:

  1. 它们基于组件;
  2. 它们有强大的社区;
  3. 它们有很多第三方库来解决问题;
  4. 它们有很多第三方组件;
  5. 它们有浏览器扩展工具来帮助调试;
  6. 它们适合做单页应用。

最基本的、最根本的、最深刻的原因是:

UI 与状态同步非常困难

为什么

假设你在开发一个这样需求:

用户可以通过发送邮件来邀请其他用户。

UI 交互设计如下:

  1. 输入框有一个空状态(带有提示信息)
  2. 输入邮箱后展示相应的
    邮箱,每个地址的右侧都有一个删除按钮。

原型如下:

这个表单是一个包含电子邮件地址和唯一标识符的对象数组。最初它将是空的。输入邮件回车后,向该数组中添加一项并更新 UI。当用户点击删除时,删除对应的项并更新 UI。

感受到了吗?每次更改状态时,都需要更新 UI。

我听到你再说,那又怎样?OK,让我们看看如何在不用框架的情况下实现它。

原生实现相对复杂的 UI

// html
<html>
  <body>
    <div id="addressList">
      <form>
        <input>
        <p class="help">Type an email address and hit enter</p>
        <ul>
        </ul>
      </form>
    </div>
  </body>
</html>

// js
class AddressList {
  constructor(root) {
    // state variables
    this.state = []
    // UI variables
    this.root = root
    this.form = root.querySelector('form')
    this.input = this.form.querySelector('input')
    this.help = this.form.querySelector('.help')
    this.ul = root.querySelector('ul')
    this.items = {} // id -> li element
    // event handlers
    this.form.addEventListener('submit', e => {
      e.preventDefault()
      const address = this.input.value
      this.input.value = ''
      this.addAddress(address)
    })
    this.ul.addEventListener('click', e => {
      const id = e.target.getAttribute('data-delete-id')
      if (!id) return // user clicked in something else  
      this.removeAddress(id)
    })
  }
  addAddress(address) {
    // state logic
    const id = String(Date.now())
    this.state = this.state.concat({ address, id })
    // UI logic
    this.updateHelp()
    const li = document.createElement('li')
    const span = document.createElement('span')
    const del = document.createElement('a')
    span.innerText = address
    del.innerText = 'delete'
    del.setAttribute('data-delete-id', id)
    this.ul.appendChild(li)
    li.appendChild(del)
    li.appendChild(span)
    this.items[id] = li
  }
  removeAddress(id) {
    // state logic
    this.state = this.state.filter(item => item.id !== id)
    // UI logic
    this.updateHelp()
    const li = this.items[id]
    this.ul.removeChild(li)
  }
  // utility method
  updateHelp() {
    if (this.state.length > 0) {
      this.help.classList.add('hidden')
    } else {
      this.help.classList.remove('hidden')
    }
  }
}

const root = document.getElementById('addressList')
new AddressList(root);

codepen 地址:https://codepen.io/gimenete/embed/vRZLrq?default-tabs=js%2Cresult&embed-version=2&height=600&host=https%3A%2F%2Fcodepen.io&referrer=https%3A%2F%2Fmedium.com%2Fdailyjs%2Fthe-deepest-reason-why-modern-javascript-frameworks-exist-933b86ebc445&slug-hash=vRZLrq

以上代码很好地说明了使用原生 JavaScript 实现一个相对复杂的 UI 所需的工作量。

在这个例子中,HTML 负责创建静态页面,JavaScript 通过 document.createElement 改变 DOM 结构。

这引来了第一个问题:

构建 UI 相关的 JavaScript 代码比较复杂,而且 UI 构建分为了两部分。我们本可以用 innerHTML,虽然它有更高的可读性,但降低了页面的性能,同时可能存在 CSRF 漏洞。

我们也可以使用模板引擎,但如果是大面积地修改 DOM,会面临两个问题:效率不高与需要重新绑定事件处理器。

但这不是最大问题。最大的问题是每当状态发生改变时都要手动更新 UI。每次状态更新时,都需要很多代码来改变 UI。当添加电子邮件地址时,只需要两行代码来更新状态,但要十三行代码更新 UI。而且我们已经让 UI 尽可能简单了!

它不仅难以编写而且难以推理,更重要的是:它也非常脆弱。

假设我们我们需要实现将列表与服务器同步的功能,我们需要将数据同服务器返回的数据作对比。

我们需要写大量代码,使 DOM 更新更加高效。但如果有任何微小的错误,视图将与数据不再同步。

因此,为了保持视图与状态同步,我们需要写大量乏味且脆弱的代码。

响应式拯救一切

之所以使用框架不是因为社区,不是因为工具,不是因为生态,不是因为第三方库......

目前为止,框架最大的改进是保证 UI 和数据同步。

只要你清楚框架的使用规则,就可以很愉快的使用他们。

We define the UI in a single shot, not having to write particular UI code in every action, and we always get the same output due to a particular state: the framework automatically updates it after the state changes.

框架是如何工作的呢?

有两个基本的策略:

  1. 重新渲染整个组件,如 React。当组件中的状态发生改变时,在内存中计算出新的 DOM 结构后与已有的 DOM 结构进行对比。实际上,这是非常昂贵的。因而采取虚拟 DOM ,通过对比状态变化前后虚拟 DOM 的不同,计算出变化后再改变真实 DOM 结构。这个过程称为调和(reconciliation)。

  2. 通过观察者监测变化,如 Angular 和 Vue。应用中状态的属性会被监测,当它们发生变化时,相应的 DOM 元素会重新渲染。

Web components 怎么样

很多情况,人们会把 React、 Angular 和 Vue 与 Web components 进行对比。这些人显然不理解这些框架所提供的最大好处:保持 UI 与状态同步。

Web components 并不提供这种同步机制。它只是提供了一个<template> 标签。如果你在应用中使用 Web components 时,想保持 UI 与状态同步,则需要开发者手工完成,或者使用相关库。

自己开发一个框架?

如果热衷于了解底层原理,想知道虚拟 DOM 的具体实现。那,为何不试着在不使用框架的情况下,仅使用虚拟 DOM 来重写原生 UI呢?

这里是框架的核心,所有组件的基础类。

我喜欢学习事物的原理 —— 虚拟 DOM 实现。那么,为什么我们学习 Virtual DOM 的实现呢?

这是框架的核心,是任何组件的基类。

这里是重写后的 AddressList 组件(使用 babel 来支持 JSX )。

现在 UI 是声明式的,没有使用任何框架。我们添加新逻辑来改变状态的同时,不再需要编写额外的代码来保持 UI 同步。

结论

  1. 现代 JavaScript 框架解决的主要问题是保持 UI 与状态同步。
  2. 使用原生 JavaScript 编写复杂、高效而又易于维护的 UI 界面几乎是不可能的。
  3. Web components 并没有提供解决 UI 与状态同步的方案。
  4. 使用现有的虚拟 DOM 库去开发自己的框架并不困难,但不建议。

最后

如果你想进【大前端交流群】,关注公众号点击“交流加群”添加机器人自动拉你入群。关注我第一时间接收最新干货。

【前端词典】提高幸福感的 9 个 CSS 技巧

前言

在这篇文章我会介绍 9 个使你的 CSS 更加简洁优雅的使用技巧。这些技巧小生经常使用,觉得挺高效实用,所以也就有了这篇文章。

9 个 CSS 技巧

特此声明,这里说的 CSS 并不止包含 CSS,也包含 CSS 预处理器(Less Sass 等),愿各位看官不要纠结于此。

正文现在开始。

1. 建议使用 padding 代替 margin

我们在设计稿还原的时候,paddingmargin 两个是常用的属性,但我们知道属于同一个 BFC 的两个相邻 Box 的 margin 会发生重叠,所以如果 margin 使用的过于频繁的时候,Box 的垂直距离可能就会发生重叠。

还有一个问题就是第一个子元素的 margin-top 值会加在父元素上的 bug(最后一个子元素的 margin-bottom 也存在类似的问题)。这里是不是有人问为什么呢?

原因就在于:

the expression collapsing margins means that adjoining margins (no non-empty content, padding or border areas or clearance separate them) of two or more boxes (which may be next to one another or nested) combine to form a single margin.

翻译过来就是:

所有毗邻的两个或多个盒元素的 margin 将会合并为一个 margin 共享。 毗邻的定义为:同级或者嵌套的盒元素,并且它们之间没有非空内容、PaddingBorder 分隔。

至于为什么合并我个人觉得这和排队的安全距离有点类似,人与人之间的安全距离是 1m,如果安全距离不合并,那么我们在排队的时候是不是人与人的距离就变成 2m 了。当然很可能不是这个原因。

所以我们可以在首位元素使用 padding 来替代 margin。当然有的时候使用 padding 不能满足需求,这时你也可以在“非空内容”这个条件做文章。即在父元素添加一个伪元素。

所以我们在使用 margin 的时候一定要注意 collapsing margins 问题。

2. position:fixed 降级问题

不知道曾经的你是不是遇到吸顶效果,就是使用 position:fixed 这个属性。其实如果其父元素中有使用 transformfixed 的效果会降级为 absolute

解决方案:

既然会降级为 absolute 效果,我们该怎么解决这个问题呢?我们就改考虑什么情况下 fixedabsolute 的表现效果会是一样的。

即当使用 fixed 的直接父元素的高度和屏幕的高度相同时 fixedabsolute 的表现效果会是一样的。

如果这个直接父级内的元素存在滚动的情况,那就加上 overflow-y: auto

3. 合理使用 px | em | rem | % 等单位

在 CSS 中有许多距离单位,比如 px | em | rem | %,还有 CSS3 中的 vh | vw 等单位。

那么我们在项目中应该如何使用呢?我们在 pc 端不需要考虑的这么复杂,所以这里我们主要讲讲这些单位在移动端中的使用。

基础单位 px

px 是我们最早接触到的单位了,不过我们在移动端自适应的要求下,使用的频率不是很高;我总结了以下使用的情况:

比较小的图案

比如需要我们画一个 r 为 5px 的圆,如果我们使用 rem 作为单位,我们很快会发现在一些机型上的图案不圆,会呈现椭圆形。这是由于 rem 转 px 会存在精度丢失问题。

所以这个时候我们就需要使用 px 配合 dpr 来实现:

// less 
/*@size 建议取双数*/
.circle(@size, @backgroundColor) {  
    width: @size;
    height: @size;
    background-color: @backgroundColor;
    [data-dpr="1"] & {
        width: @size * 0.5;
        height: @size * 0.5;
    }
    [data-dpr="3"] & {
        width: @size * 1.5;
        height: @size * 1.5;
    }
}

1px 细线问题

这个问题下面我会单独做一小节讲,在这里就不累述。

字体大小(基本都是用 rem 作为单位)

一般情况字体的大小我也会使用 rem 作为单位,因为精度丢失我认为在可以接受的范围之内。

相对单位 rem

rem 是 CSS3 新增的一个相对单位(root em),即相对 HTML 根元素的字体大小的值。

rem 应该是自适应使用的最广泛的单位了。

相对单位 em

em 也是一个相对单位,却是相对于当前对象内文本的字体大小。

line-height

一般建议在 line-height 使用 em。因为在需要调整字体大小的时候,只需修改 font-size 的值,而 line-height 已经设置成了相对行高了。

首行缩进两个字符

在存在首行缩进的需求,我也会使用这个单位。

text-indent: 2em

视口单位 vw | vh

vw: 1vw = 视口宽度的 1%
vh: 1vh = 视口高度的 1%

我们知道以 rem 单位设计的弹性布局,是需要在头部加载一段脚本来进行监听分辨率的变化来动态改变根元素字体大小,使得 CSS 与 JS 耦合了在一起。

那么有没有方案解决这个耦合的问题呢?

答案就是视口单位 vw | vh。

以下就是前人给出的使用方案:

$vm_fontsize: 75;
@function rem($px) {
     @return ($px / $vm_fontsize ) * 1rem;
}
$vm_design: 750;
html {
    font-size: ($vm_fontsize / ($vm_design / 2)) * 100vw; 
    @media screen and (max-width: 320px) {
        font-size: 64px;
    }
    @media screen and (min-width: 540px) {
        font-size: 108px;
    }
}
// body 也增加最大最小宽度限制,避免默认100%宽度的 block 元素跟随 body 而过大过小
body {
    max-width: 540px;
    min-width: 320px;
}

4. 合理使用变量

一般设计稿中的某一类的文字(元素)都是用相同的字体大小、颜色、行高等样式属性,所以这些值我们不必每次都重复写,因为当 UI 更新设计方案,你需要改的地方就很多了。这些重复使用的值我们完全可以存放在变量里面。

Sass 和 Less 稍微有点区别:

// sass
$direction: left;
// less
@direction: left;

当然 CSS 原生也是存在变量的,使用规则如下:

变量定义的语法是: --*; // 为变量名称。
变量使用的语法是:var(
);

  1. 无论是变量的定义和使用只能在声明块 {} 里面
  2. CSS 变量字符限制为: [0-9]、[a-zA-Z]、_、-、中文和韩文等。
:root {
    --blue_color: #3388ff;
    --main_bgcolor: #fafafa;
    --font_size_12: 12px;
    --font_size_14: 14px;
    --color: 20px;
}
.div1{
    background-color: var(--main_bgcolor);
    font-size: var(--font_size_12);
}

5. 使用 Mixin 归类重复样式

和重复变量一样,重复的样式也可以归类。我觉得优秀的代码其中有一条肯定是代码的复用性强。

之前我们写 CSS 的时候,也会将一些重复使用的代码放在一个 class 中,这样的确达到了一定的复用性,不过最后的效果可能就是在一个元素里面放了很多 class,如下图:

这样下一个接手得人难免会有点迷糊,而且这样会造成样式越来越难修改。

这个时候,mixin( 可以理解成 class 中的 class )就能发挥它的作用了。

这是一个描述性文字的样式:

.font-description {
    .font-des-style(24px,#fff,1.5em);
    .line-camp(2);
}

// less
/* 多行显示 */
.line-camp( @clamp:2 ) {
    text-overflow: -o-ellipsis-lastline;
    overflow: hidden;
    text-overflow: ellipsis;
    display: -webkit-box;
    -webkit-line-clamp: @clamp;
    -webkit-box-orient: vertical; 
}

.font-des-style( @fontSize, @color, @lineHeight, @textAlign:left ) {
    font-size: @fontSize;
    color: @color;
    line-height: @lineHeight;
    text-align: @textAlign;
}

这只是一个简单的例子,我们可以把可复用的样式放在 mixin 中,这样接手项目的人只需要熟悉你写的 mixin.less 就可以开始迭代需求了。

6. 1px 方案

做过移动端需求的前端肯定是避免不了处理 1px 细线问题,这个问题的原因就是 UI 对页面美观度的要求越来越高(不要和我说这是 retina 屏的问题)。

据小生所知好像没有什么兼容性特别好的方案,这里我只是提供两种种相对较好的方案。

使用伪类 + transform

.border_bottom { 
    overflow: hidden; 
    position: relative; 
    border: none!important; 
}
.border_bottom:after { 
    content: "";
    display: block;
    position: absolute; 
    left: 0; 
    bottom: 0; 
    width: 100%; 
    height: 1px; 
    background-color: #d4d6d7; 
    -webkit-transform-origin: 0 0;  
    transform-origin: 0 0; 
    -webkit-transform: scaleY(0.5);
    transform: scaleY(0.5);
}

当然这个方案在一些版本较低的机型也是会出现粗细不均、细线消失断裂的兼容性问题。不过现在已经 2019 年了,版本较低的机型也淘汰的差不多了。

使用 box-shadow 模拟

.border_bottom {
  box-shadow: inset 0px -1px 1px -1px #d4d6d7;
}

这个方案基本可以满足所有场景,不过有个缺点也就是颜色会变浅。

多谢 D文斌 分享的另一种方案:
这种方案对 dpr 做了不同的处理,可谓更加精细。

.min-device-pixel-ratio(@scale2, @scale3) {
  @media screen and (min-device-pixel-ratio: 2), (-webkit-min-device-pixel-ratio: 2) {
    transform: @scale2;
  }
  @media screen and (min-device-pixel-ratio: 3), (-webkit-min-device-pixel-ratio: 3) {
    transform: @scale3;
  }
}

.border-1px(@color: #DDD, @radius: 2PX, @style: solid) {
  &::before {
    content: "";
    pointer-events: none;
    display: block;
    position: absolute;
    left: 0;
    top: 0;
    transform-origin: 0 0;
    border: 1PX @style @color;
    border-radius: @radius;
    box-sizing: border-box;
    width: 100%;
    height: 100%;
    @media screen and (min-device-pixel-ratio: 2), (-webkit-min-device-pixel-ratio: 2) {
      width: 200%;
      height: 200%;
      border-radius: @radius * 2;
      transform: scale(.5);
    }
    @media screen and (min-device-pixel-ratio: 3), (-webkit-min-device-pixel-ratio: 3) {
      width: 300%;
      height: 300%;
      border-radius: @radius * 3;
      transform: scale(.33);
    }
  }
}

.border-top-1px(@color: #DDD, @style: solid) {
  &::before {
    content: "";
    position: absolute;
    left: 0;
    top: 0;
    width: 100%;
    border-top: 1Px @style @color;
    transform-origin: 0 0;
    .min-device-pixel-ratio(scaleY(.5), scaleY(.33));
  }
}

7. 从 html 元素继承 box-sizing

在大多数情况下我们在设置元素的 borderpadding 并不希望改变元素的 width,height 值,这个时候我们就可以为该元素设置 box-sizing:border-box;

我不希望每次都重写一遍,而是希望他是继承而来的,那么我们可以使用如下代码:

html {
  box-sizing: border-box;
}
*, *:before, *:after {
  box-sizing: inherit;
}

这样的好处在于他不会覆盖其他组件的 box-sizing 值,又无需为每一个元素重复设置 box-sizing: border-box;

8. 内联首屏关键 CSS

性能优化中有一个重要的指标 —— 首次有效绘制(FMP),即指页面的首要内容(primary content)出现在屏幕上的时间。这一指标影响用户看到页面前所需等待的时间,而 内联首屏关键 CSS(即 Critical CSS,可以称之为首屏关键 CSS) 能给用户一个更好的心理预期。

如图:

我们知道内联 CSS 能够使浏览器开始页面渲染的时间提前,即在 HTML 下载完成之后就能渲染了。

既然是内联关键 CSS,也就说明我们只会将少部分的 CSS 代码直接写入 HTML 中。至于内联哪些 CSS 你可以使用 Critical。

9. 文字超出省略、文字两端对齐

需求中我们也经常遇到这样的需求,这里直接提供方案。

超出省略

.line-camp( @clamp:2 ) {
    text-overflow: -o-ellipsis-lastline;
    overflow: hidden;
    text-overflow: ellipsis;
    display: -webkit-box;
    -webkit-line-clamp: @clamp;
    -webkit-box-orient: vertical; 
}

所遇到的问题:

-webkit-box-orient: vertical 在使用 webpack 打包的时候这段代码会被删除掉,原因是 optimize-css-assets-webpack-plugin 这个插件的问题。

解决方案:

可以使用如下的写法:

.line-camp( @clamp:2 ) {
    text-overflow: -o-ellipsis-lastline;
    overflow: hidden;
    text-overflow: ellipsis;
    display: -webkit-box;
    -webkit-line-clamp: @clamp;
    /*! autoprefixer: off */
    -webkit-box-orient: vertical;
    /* autoprefixer: on */
}

两端对齐

// html
<div>姓名</div>
<div>手机号码</div>
<div>账号</div>
<div>密码</div>

// css
div {
    margin: 10px 0; 
    width: 100px;
    border: 1px solid red;
    text-align: justify;
    text-align-last:justify
}
div:after{
    content: '';
    display: inline-block;
    width: 100%;
}

效果如下:

最后

如果你想进【大前端交流群】,关注公众号点击“交流加群”添加机器人自动拉你入群。关注我第一时间接收最新干货。

【前端词典】继承(一) - 原型链你真的懂吗?

写在前言前面的话

虽说标题是继承,但继承这块的涉及的知识点不仅仅只是继承,所以这块我会分成两个部分来讲:

  • 第一部分主讲原型以及原型链
  • 第二部分主讲继承的几种方式

分两节讲有以下两个原因:

  1. 需要了解继承必须对原型和原型链有深刻的了解,分开讲好消化
  2. 最近孩子快出生,需要更多的时间翻阅资料来保证文章的质量

前言

继承于我们前端来说绝对是非常熟悉也必须熟悉的一个高频必懂知识点。熟悉到只要是面试一定会有关于继承的问题;而且源码中继承的使用也随处可见。

可依旧有很多前端对继承的实现和应用没有一个整体的把握。追其原因无非有二:

  1. ECMAScript 继承的实现方法区别于其他基于类的实现继承的面向对象(Object Oriented)语言。
  2. 工作中即使对如何实现继承一知半解,也一点都不耽误写逻辑代码。

无论由于哪一个原因,建议请尽快弄懂继承的实现和应用,否则你可能会如同你的表情包一样——流下了没有技术的泪水。

接下来我会尽我所能讲清楚继承这个概念,并结合相关经典图文做辅助解释。

在讲 ECMAScript 继承的概念之前,我先说下原型的概念。

类与原型

讲 ECMAScript 继承的概念之前,我先说下类的概念。(如果接触过 Java 或者是 C++ 的话,我们就知道 Java(C++)的继承都是基于类的继承)。

类: 是面向对象(Object Oriented)语言实现信息封装的基础,称为类类型。每个类包含数据说明和一组操作数据或传递消息的函数。类的实例称为对象

类: 是描述了一种代码的组织结构形式,一种在软件中对真实世界中问题领域的建模方法。

类的概念这里我就不再扩展,感兴趣的同学可以自行查阅书籍。接下来我们重点讲讲原型以及原型链

原型

JavaScript 这门语言没有类的概念,所以 JavaScript 并非是基于类的继承,而是基于原型的继承。(主要是借鉴 Self 语言原型(prototype)继承机制)。

注意:ES6 中的 class 关键字和 OO 语言中的类的概念是不同的,下面我会讲到。ES6 的 class 其内部同样是基于原型实现的继承。

JavaScript 摒弃转而使用原型作为实现继承的基础,是因为基于原型的继承相比基于的继承上在概念上更为简单。首先我们明确一点,存在的目的是为了实例化对象,而 JavaScript 可以直接通过对象字面量语法轻松的创建对象。

每一个函数,都有一个 prototype 属性。
所有通过函数 new 出来的对象,这个对象都有一个 __proto__ 指向这个函数的 prototype
当你想要使用一个对象(或者一个数组)的某个功能时:如果该对象本身具有这个功能,则直接使用;如果该对象本身没有这个功能,则去 __proto__ 中找。

1. prototype [显式原型]

prototype 是一个显式的原型属性,只有函数才拥有该属性。
每一个函数在创建之后都会拥有一个名为 prototype 的属性,这个属性指向函数的原型对象。( 通过 Function.prototype.bind 方法构造出来的函数是个例外,它没有 prototype 属性 )

prototype 是一个指针,指向的是一个对象。比如 Array.prototype 指向的就是 Array 这个函数的原型对象。

在控制台中打印 console.log(Array.prototype) 里面有很多方法。这些方法都以事先内置在 JavaScript 中,直接调用即可。上面我标红了两个特别的属性 constructor__proto__。这两个属性接下来我都会讲。

我们现在写一个 function noWork(){} 函数。

当我写了一个 noWork 这个方法的时候,它自动创建了一个 prototype 指针属性(指向原型对象)。而这个被指向的原型对象自动获得了一个 constructor (构造函数)。细心的同学一定发现了:constructor 指向的是 noWork

noWork.prototype.constructor === noWork     // true
 一个函数的原型对象的构造函数是这个函数本身

如图:

tips: 图中打印的 Array 的显式原型对象中的这些方法你都知道吗?要知道数组也是非常重要的一部分哦 ~  咳咳咳,这是考试重点。

2. __proto__[隐式原型]

prototype 理解起来不难,__proto__ 理解起来就会比 prototype 稍微复杂一点。不过当你理解的时候你会发现,这个过程真的很有趣。下面我们就讲讲 __proto__

其实这个属性指向了 `[[prototype]]`,但是 `[[prototype]]`  是内部属性,我们并不能访问到,所以使用 `__proto__` 来访问。

我先给个有点绕的定义:

__proto__ 指向了创建该对象的构造函数的显式原型。

我们现在还是使用 noWork 这个例子来说。我们发现 noWork 原型对象中还有另一个属性 __proto__

我们先打印这个属性:

我们发现这个 __proto__ 指向的是 Object.prototype

我听到有人在问为什么?

  1. 因为这个 __proto__.constructor 指向的是 Object
  2. 我们知道:一个函数的原型对象的构造函数是这个函数本身
  3. 所以这个 __proto__.constructor 指向的是 Object.prototype.constructor
  4. 进而 __proto__ 指向的是 Object.prototype

如图:


我们来验证一下:

至于为什么是指向 Object? 因为所有的引用类型默认都是继承 Object 。

作用

  1. 显式原型:用来实现基于原型的继承与属性的共享。
  2. 隐式原型:构成原型链,同样用于实现基于原型的继承。 举个例子,当我们使用 noWork 这个对象中的 toString() 属性时,在noWork 中找不到,就会沿着 __proto__ 依次查找。

3. new 操作符

当我们使用 new 操作符时,生成的实例对象拥有了 __proto__属性。即在 new 的过程中,新对象被添加了 __proto__ 并且链接到构造函数的原型上。

new 的过程

  1. 新生成了一个对象
  2. 链接到原型
  3. 绑定 this
  4. 返回新对象

Function.__proto__ === Function.prototype

难道这代表着 Function 自己产生了自己? 要说明这个问题我们先从 Object 说起。

我们知道所有对象都可以通过原型链最终找到 Object.prototype ,虽然 Object.prototype 也是一个对象,但是这个对象却不是 Object 创造的,而是引擎自己创建了 Object.prototype 所以可以这样说:

所有实例都是对象,但是对象不一定都是实例。

接下来我们来看 Function.prototype 这个特殊的对象:

打印这个对象,会发现这个对象其实是一个函数。我们知道函数都是通过 new Function() 生成的,难道 Function.prototype 也是通过 new Function() 产生的吗?这个函数也是引擎自己创建的。

首先引擎创建了 Object.prototype ,然后创建了 Function.prototype ,并且通过 __proto__ 将两者联系了起来。

这就是为什么 Function.prototype.bind() 没有 prototype 属性。因为 Function.prototype 是引擎创建出来的对象,引擎认为不需要给这个对象添加 prototype 属性。

对于为什么 Function.__proto__ 会等于 Function.prototype ?
我看到的一个解释是这样的:
其他所有的构造函数都可以通过原型链找到 Function.prototype ,并且 function Function() 本质也是一个函数,为了不产生混乱就将 function Function()__proto__ 联系到了 Function.prototype 上。

继承的几种方式

未完待续

参考

  1. 《JavaScript 高级程序设计》
  2. 《你不知道的 JavaScript - 上》
  3. 《JavaScript 语言精粹》
  4. ~zepto设计和源码分析

最后

如果你想进【大前端交流群】,关注公众号点击“交流加群”添加机器人自动拉你入群。关注我第一时间接收最新干货。

【前端词典】 Vue 响应式原理其实很好懂

前言

这是十篇 Vue 系列文章的第三篇,这篇文章我们讲讲 Vue 最核心的功能之一 —— 响应式原理。

如何理解响应式

可以这样理解:当一个状态改变之后,与这个状态相关的事务也立即随之改变,从前端来看就是数据状态改变后相关 DOM 也随之改变。数据模型仅仅是普通的 JavaScript 对象。而当你修改它们时,视图会进行更新。

抛个问题

我们先看看我们在 Vue 中常见的写法:

<div id="app" @click="changeNum">
  {{ num }}
</div>

var app = new Vue({
  el: '#app',
  data: {
    num: 1
  },
  methods: {
    changeNum() {
      this.num = 2
    }
  }
})

这种写法很常见,不过你考虑过当为什么执行 this.num = 2 后视图为什么会更新呢?通过这篇文章我力争把这个点讲清楚。

如果不使用 Vue,我们应该怎么实现?

我的第一想法是像下面这样实现:

let data = {
  num: 1
};
Object.defineProperty(data, 'num',{
  set: function( newVal ){
    document.getElementById('app').value = newVal;
  }
});
input.addEventListener('input', function(){
  data.num = 2;
});

这样可以粗略的实现点击元素,自动更新视图。

这里我们需要通过 Object.defineProperty 来操作对象的访问器属性。监听到数据变化的时候,操作相关 DOM。

而这里用到了一个常见模式 —— 发布/订阅模式。

我画了一个大概的流程图,用来说明观察者模式和发布/订阅模式。如下:

仔细的同学会发现,我这个粗略的过程和使用 Vue 的不同的地方就是需要我自己操作 DOM 重新渲染。

如果我们使用 Vue 的话,这一步就是 Vue 内部的代码来处理的。这也是我们为什么在使用 Vue 的时候无需手动操作 DOM 的原因。

关于 Object.defineProperty 我在上一篇文章已经提及,这里就不再复述。

Vue 是如何实现响应式的

我们知道对象可以通过 Object.defineProperty 操作其访问器属性,即对象拥有了 gettersetter 方法。这就是实现响应式的基石。

先看一张很直观的流程图:

initData 方法

在 Vue 的初始化的时候,其 _init() 方法会调用执行 initState(vm) 方法。initState 方法主要是对 propsmethodsdatacomputedwathcer 等属性做了初始化操作。

这里我们就对 data 初始化的过程做一个比较详细的分析。

function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  if (!isPlainObject(data)) {
    ......
  }
  // proxy data on instance
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    ...... // 省略部分兼容代码,但不影响理解
    if (props && hasOwn(props, key)) {
      ......
    } else if (!isReserved(key)) {
      proxy(vm, `_data`, key)
    }
  }
  // observe data
  observe(data, true /* asRootData */)
}

initData初始化 data 的主要过程也是做两件事:

  1. 通过 proxy 把每一个值 vm._data.[key] 都代理到 vm.[key] 上;
  2. 调用 observe 方法观测整个 data 的变化,把 data 也变成响应式(可观察),可以通过 vm._data.[key] 访问到定义 data 返回函数中对应的属性。

数据劫持 — Observe

通过这个方法将 data 下面的所有属性变成响应式(可观察)。

// 给对象的属性添加 getter 和 setter,用于依赖收集和发布更新
export class Observer {
  value: any;
  dep: Dep;  
  vmCount: number; 
  constructor (value: any) {
    this.value = value
    // 实例化 Dep 对象
    this.dep = new Dep()
    this.vmCount = 0
    // 把自身实例添加到数据对象 value 的 __ob__ 属性上
    def(value, '__ob__', this)
    // value 是否为数组的不同调用
    if (Array.isArray(value)) {
      const augment = hasProto ? protoAugment : copyAugment
      augment(value, arrayMethods, arrayKeys)
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }

  // 取出所有属性遍历
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

def 函数内封装了 Object.defineProperty ,所以你 console.log(data) ,会发现多了一个 __ob__ 的属性。

defineReactive 方法遍历所有属性

// 定义一个响应式对象的具体实现
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()
  ..... // 省略部分兼容代码,但不影响理解
  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        // 进行依赖收集
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      ..... // 省略部分兼容代码,但不影响理解
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      // 对新的值进行监听
      childOb = !shallow && observe(newVal)
      // 通知所有订阅者,内部调用 watcher 的 update 方法 
      dep.notify()
    }
  })
}

defineReactive 方法最开始初始化 Dep 对象的实例,然后通过对子对象递归调用observe 方法,使所有子属性也能变成响应式的对象。并且在 Object.definePropertygettersetter 方法中调用 dep 的相关方法。

即:

  1. getter 方法完成的工作就是依赖收集 —— dep.depend()
  2. setter 方法完成的工作就是发布更新 —— dep.notify()

我们发现这里都和 Dep 对象有着不可忽略的关系。接下来我们就看看 Dep 对象。这个 Dep

调度中心作用的 Dep

前文中我们提到发布/订阅模式,在发布者和订阅者之前有一个调度中心。这里的 Dep 扮演的角色就是调度中心,主要的作用就是:

  1. 收集订阅者 Watcher 并添加到观察者列表 subs
  2. 接收发布者的事件
  3. 通知订阅者目标更新,让订阅者执行自己的 update 方法

详细代码如下:

// Dep 构造函数
export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

  constructor () {
    this.id = uid++
    this.subs = []
  }
  // 向 dep 的观察者列表 subs 添加 Watcher
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }
  // 从 dep 的观察者列表 subs 移除 Watcher
  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }
  // 进行依赖收集
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }
  // 通知所有订阅者,内部调用 watcher 的 update 方法
  notify () {
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}
// Dep.target 是全局唯一的观察者,因为在任何时候只有一个观察者被处理。
Dep.target = null
// 待处理的观察者队列
const targetStack = []

export function pushTarget (_target: ?Watcher) {
  if (Dep.target) targetStack.push(Dep.target)
  Dep.target = _target
}

export function popTarget () {
  Dep.target = targetStack.pop()
}

Dep 可以理解成是对 Watcher 的一种管理,Dep 和 Watcher 是紧密相关的。所以我们必须看一看 Watcher 的实现。

订阅者 —— Watcher

Watcher 中定义了许多原型方法,这里我只粗略的讲 updateget 这三个方法。

  // 为了方便理解,部分兼容代码已被我省去
  get () {
    // 设置需要处理的观察者
    pushTarget(this)
    const vm = this.vm
    let value = this.getter.call(vm, vm)
    // deep 是否为 true 的处理逻辑
    if (this.deep) {
      traverse(value)
    }
    // 将 Dep.target 指向栈顶的观察者,并将他从待处理的观察者队列中移除
    popTarget()
    // 执行依赖清空动作
    this.cleanupDeps()
    return value
  }

  update () {
    if (this.computed) {
      ...
    } else if (this.sync) { 
      // 标记为同步
      this.run()
    } else {      
      // 一般都是走这里,即异步批量更新:nextTick
      queueWatcher(this)
    }
  }

Vue 的响应式过程大概就是这样了。感兴趣的可以看看源码。

最后我们在通过这个流程图来复习一遍:

Vue 相关文章输出计划

最近总有朋友问我 Vue 相关的问题,因此接下来我会输出 10 篇 Vue 相关的文章,希望对大家有一定的帮助。我会保持在 7 到 10 天更新一篇。

  1. 【前端词典】Vuex 注入 Vue 生命周期的过程(完成)
  2. 【前端词典】学习 Vue 源码的必要知识储备(完成)
  3. 【前端词典】浅析 Vue 响应式原理(完成)
  4. 【前端词典】新老 VNode 进行 patch 的过程
  5. 【前端词典】如何开发功能组件并上传 npm
  6. 【前端词典】从这几个方面优化你的 Vue 项目
  7. 【前端词典】从 Vue-Router 设计讲前端路由发展
  8. 【前端词典】在项目中如何正确的使用 Webpack
  9. 【前端词典】Vue 服务端渲染
  10. 【前端词典】Axios 与 Fetch 该如何选择

最后

如果你想进【大前端交流群】,关注公众号点击“交流加群”添加机器人自动拉你入群。关注我第一时间接收最新干货。

【前端词典】实现 Canvas 下雪背景引发的性能思考

前言

去年圣诞节产品提了一个活动需求,其中有一个下雪的背景动画。在做这个动画的过程中加深了对 canvas 动画的一些了解,在这里我仅是抛砖引玉的分享一下,欢迎各位大佬批评。

代码已上传至 github ,感兴趣的可以 clone 代码到本地运行。望给个 star 支持一下

入题

需求给出的 UI 样式如下:

UI 的需求是雪花下落的方向有点倾斜角度,每片雪花的下落速度不一样但要保持在一个范围内。

需求了解的差不多就开始实现这个效果(在看这篇文章之前你需要对 canvas 的一些基本 API 了解)。

drawImage

drawImage 可传入 9 个参数,上图中的 5 个参数是比较常用的,另外几个参数是拿来剪切图片的。

直接使用 drawImage 来剪切图片,其性能不会太好,建议先将需要使用的部分用一个离屏 canvas 保存起来,需要用到的时候直接使用即可。

requestAnimationFrame

requestAnimationFrame 相对于 setinterval 处理动画有以下几个优势:

  1. 经过浏览器优化,动画更流畅
  2. 窗口没激活时,动画将停止,省计算资源
  3. 更省电,尤其是对移动终端

这个 API 不需要传入动画间隔时间,这个方法会告诉浏览器以最佳的方式进行动画重绘。

由于兼容性问题,可以使用以下方法对 requestAnimationFrame 进行重写:

window.requestAnimationFrame = (function(){
        return  window.requestAnimationFrame       || 
                window.webkitRequestAnimationFrame || 
                window.mozRequestAnimationFrame    || 
                window.oRequestAnimationFrame      || 
                window.msRequestAnimationFrame     || 
                function (callback) {
                    window.setTimeout(callback, 1000 / 60); 
                };
    })();

对于其他 API 烦请查阅文档。

第一次尝试

有一个大概想法后就开心的开始写代码了,基本思路就是使用 requestAnimationFrame 来刷新 canvas 画板。

由于雪花不规则,所以雪花是 UI 提供的图片,既然是图片我们就需要先将图片预加载好,要不然在 转换图片的时候很可能影响性能。

使用的预加载方法如下:

function preloadImg(srcArr){
    if(srcArr instanceof Array){
        for(let i = 0; i < srcArr.length; i++){
            let oImg = new Image();
            oImg.src = srcArr[i];
        }
    }
}

前前后后写了一个下午,算是写好了,在手机上查看的效果发现很是卡顿。100 片雪花 FPS 尽然平均才 40 多。而且在某些机型会出现都懂得情况。

要是产品看到这个效果,恐怕是又要召集相关人员开相关会议了。这么卡顿肯定是写了些开销大的代码,于是乎需要第二次尝试。

晚上还是需要按时下班的。不过下班回家后也不能闲着,开始找相关的资料,以便第二天快速的完成。

第二次尝试前的准备

经过一个晚上的查找学习,大概知道了以下几个优化 canvas 性能的方法:

1. 使用多层画布绘制复杂场景

分层的目的是降低完全不必要的渲染性能开销。

即:将变化频率高、幅度大的部分和变化频率小、幅度小的部分分成两个或两个以上的 canvas 对象。也就是说生成多个 canvas 实例,把它们重叠放置,每个 Canvas 使用不同的 z-index 来定义堆叠的次序。

<canvas style="position: absolute; z-index: 0"></canvas>
<canvas style="position: absolute; z-index: 1"></canvas>
// js 代码

2. 使用 requestAnimationFrame 制作动画

上面有提到。

3. 清除画布尽量使用 clearRect

一般情况下的性能:clearRect > fillRect > canvas.width = canvas.width;

4. 使用离屏绘制进行预渲染

当时用 drawImage 绘制同样的一块区域:

  1. 若数据源(图片、canvas)和 canvas 画板的尺寸相仿,那么性能会比较好;
  2. 若数据源只是大图上的一部分,那么性能就会比较差;因为每一次绘制还包含了裁剪工作。

第二种情况我们就可以先把待绘制的区域裁剪好,保存在一个离屏的 canvas 对象中。在绘制每一帧的时候,在将这个对象绘制到 canvas 画板中。

drawImage 方法的第一个参数不仅可以接收 Image 对象,也可以接收另一个 Canvas 对象。而且,使用 Canvas 对象绘制的开销与使用 Image 对象的开销几乎完全一致。

当每一帧需要调用的对象需要多次调用 canvasAPI 时,我们也可以使用离屏绘制进行预渲染的方式来提高性能。

即:

let cacheCanvas = document.createElement("canvas");
let cacheCtx = this.cacheCanvas.getContext("2d");

cacheCtx.save();
cacheCtx.lineWidth = 1;
for(let i = 1;i < 40; i++){
    cacheCtx.beginPath();
    cacheCtx.strokeStyle = this.color[i];
    cacheCtx.arc(this.r , this.r , i , 0 , 2*Math.PI);
    cacheCtx.stroke();
}
this.cacheCtx.restore();

// 在绘制每一帧的时候,绘制这个图形
context.drawImage(cacheCtx, x, y);

cacheCtx 的宽高尽量设置成实际使用的宽高,否则过多空白区域也会造成性能的损耗。

下图显示了使用离屏绘制进行预渲染技术所带来的性能改善情况(来自于jsperf):

5. 尽量少调用 canvasAPI ,尽可能集中绘制

如下代码:

for (var i = 0; i < points.length - 1; i++) {
    var p1 = points[i];
    var p2 = points[i + 1];
    context.beginPath();
    context.moveTo(p1.x, p1.y);
    context.lineTo(p2.x, p2.y);
    context.stroke();
} 

可以改成:

context.beginPath();
for (var i = 0; i < points.length - 1; i++) {
    var p1 = points[i];
    var p2 = points[i + 1];
    context.moveTo(p1.x, p1.y);
    context.lineTo(p2.x, p2.y);
}
context.stroke();

tips: 写粒子效果时,可以使用方形替代圆形,因为粒子小,所以方和原看上去差不多。有人问为什么?很容易理解,画一个圆需要三个步骤:先 beginPath,然后用 arc 画弧,再用 fill。而画方只需要一个 fillRect。当粒子对象达一定数量时性能差距就会显示出来了。

6. 像素级别操作尽量避免浮点运算

进行 canvas 动画绘制时,若坐标是浮点数,可能会出现 CSS Sub-pixel 的问题.也就是会自动将浮点数值四舍五入转为整数,在动画的过程中就可能出现抖动的情况,同时也可能让元素的边缘出现抗锯齿失真情况。

虽然 javascript 提供了一些取整方法,像 Math.floorMath.ceilparseInt,但 parseInt 这个方法做了一些额外的工作(比如检测数据是不是有效的数值、先将参数转换成了字符串等),所以,直接用 parseInt 的话相对来说比较消耗性能。
可以直接用以下巧妙的方法进行取整:

function getInt(num){
    var rounded;
    rounded = (0.5 + num) | 0;
    return rounded;
}

另 for 循环的数组效率是最高的,具体感兴趣的可以自行实验。

第二次尝试

通过昨天晚上的查阅,对这个动画做了以下几点优化:

  1. 使用离屏绘制进行预渲染
  2. 减少部分 API 的使用
  3. 浮点数取证
  4. 缓存变量
  5. 使用 for 循环,替代 forEach
  6. 将整体代码使用原型链方式改写了一遍

方案写好了就开始愉快的写代码。上午下班前新的动画写好了。

200 片雪花的时候 FPS 基本稳定在 60,而且抖动的情况也没了;
增加到 1000 片的时候,FPS 还是基本稳定在 60;
增加到 1500 片的时候,稍微有点零星的几次卡帧;
增加到 2000 片的时候,开始卡顿。

这说明这个动画还是没有优化好,还有优化空间,请各位大佬不吝指教。

推荐使用 stats.js 插件,这个插件可以显示动画运行时的 FPS。

主要代码

let snowBox = function () {
    let canvasEl = document.getElementById("snowFall");
    let ctx = canvasEl.getContext('2d');
    canvasEl.width = window.innerWidth;
    canvasEl.height = window.innerHeight;
    let lineList = []; // 雪的容器
    let snow = function () {
        let _this = this;
        _this.cacheCanvas = document.createElement("canvas");
        _this.cacheCtx = _this.cacheCanvas.getContext("2d");
        _this.cacheCanvas.width = 10;
        _this.cacheCanvas.height = 10;
        _this.speed = [1, 1.5, 2][Math.floor(Math.random()*3)];                // 雪花下落的三种速度,便于取整
        _this.posx = Math.round(Math.random() * canvasEl.width);               // 雪花x坐标
        _this.posy = Math.round(Math.random() * canvasEl.height);              // 雪花y坐标
        _this.img = `./img/snow_(${Math.ceil(Math.random() * 9)}).png`;        // img
        _this.w = _this.getInt(5 + Math.random() * 6);
        _this.h = _this.getInt(5 + Math.random() * 6);
        _this.cacheSnow();
    };

    snow.prototype = {
        cacheSnow: function () {
            let _this = this;
            // _this.cacheCtx.save();
            let img = new Image();   // 创建img元素
            img.src = _this.img;
            _this.cacheCtx.drawImage(img, 0, 0, _this.w, _this.h);
            // _this.cacheCtx.restore();
        },
        fall: function () {
            let _this = this;
            if (_this.posy > canvasEl.height + 5) {
                _this.posy = _this.getInt(0 - _this.h);
                _this.posx = _this.getInt(canvasEl.width * Math.random());
            }
            if (_this.posx > canvasEl.width + 5) {
                _this.posx = _this.getInt(0 - _this.w);
                _this.posy = _this.getInt(canvasEl.height * Math.random());
            }
            // 如果雪花在可视区域
            if (_this.posy <= canvasEl.height || _this.posx <= canvasEl.width) {
                _this.posy = _this.posy + _this.speed;
                _this.posx = _this.posx + _this.speed * .5;
            }
            _this.paint();
        },
        paint: function () {
            ctx.drawImage(this.cacheCanvas, this.posx, this.posy)
        },
        getInt: function(num){
            let rounded;
            rounded = (0.5 + num) | 0;
            return rounded;
        }
    };

    let control;
    control = {
        start: function (num) {
            for (let i = 0; i < num; i++) {
                let s = new snow();
                lineList.push(s);
            }
            (function loop() {
                ctx.clearRect(0, 0, canvasEl.width, canvasEl.height);
                for (let i = 0; i < num; i++) {
                    lineList[i].fall();
                }
                requestAnimationFrame(loop)
            })();
        }
    };
    return control;
}();

window.onload = function(){
    snowBox.start(2000)
};

建议从 github clone 代码到本地运行。

后话

这篇文章虽然说是关于 canvas 动画的性能优化。一些大佬也已经看出,其他方面的性能优化方案和这个大抵相同,无非是:

  1. 减少 API 的使用
  2. 使用缓存(重点)
  3. 合并频繁使用的 API
  4. 避免使用高耗能的 API
  5. 用 webWorker 来处理一些比较耗时的计算
  6. ……

希望你通过阅读这篇文章,可以在性能优化方面给你作一个参考,多谢阅读。

最后

如果你想进【大前端交流群】,关注公众号点击“交流加群”添加机器人自动拉你入群。关注我第一时间接收最新干货。

【前端词典】F5 同 Ctrl+F5 的区别

前言

F5Ctrl+F5 使用的频率很高,可是在使用的时候有没有想过 F5Ctrl+F5 的区别是什么? 这篇文章会将 F5Ctrl+F5 刷新页面的原理讲清楚。通过这篇小文,即便是对浏览器缓存机制加深一点点的认知,也是有所裨益的。

入题

下图是我们第一次打开掘金的 Network 界面,由于是第一次打开,所以全部资源是从服务器请求的,Status 都是 200

接下来我们按一下 F5,看看效果;

发现静态资源的 Size 都是 from disk cache;说明此时的静态资源是从缓存中取的。具体为什么 Sizefrom disk cache 我先按下不表。我先来说说 size 选项的 4 种情况。

size 选项的 4 种情况

  1. 资源的大小
  2. from disk cache
  3. from memory cache
  4. from ServiceWorker

from memory cache

表示此资源是取自内存,不会请求服务器。已经加载过该资源且缓存在内存当中;关闭该页面此资源就被内存释放掉了,再次打开相同页面时不会出现 from memory cache 的情况。

from disk cache

表示此资源是取自磁盘,不会请求服务器。已经在之前的某个时间加载过该资源,但是此资源不会随着该页面的关闭而释放掉,因为是存在硬盘当中的,下次打开仍会 from disk cache

资源本身大小数值

http 状态为 200 是实实在在从浏览器获取的资源,当 http 状态为 304 时该数字是与服务端通信报文的大小,并不是该资源本身的大小,该资源是从本地获取的。

from ServiceWorker

表示此资源是取自 from ServiceWorker

现在我们再按下 Ctrl+F5,看看效果

发现 Size 显示的又是资源自身的大小,说明 Ctrl+F5 后的资源又是重新从服务器中请求得到的。

F5 同 Ctrl+F5 的区别

为什么 F5 后请求的是缓存,而 Ctrl+F5 就重新请求资源呢?答案就是这两种方式发送的请求头不一样(不同的浏览器发送的请求头也有一些区别)。

F5


chrome 浏览器中按 F5 后,看到资源的请求头中有 provisional headers are show 字样。这是为什么呢?

原因:未与服务端正确通信。该文件是从缓存中获取的并未进行通信,所以详细标头并不会显示。强缓存 from disk cache 或者 from memory cache ,都不会正确的显示请求头。

下面看看按 F5 后在 firefox 浏览器中的表现。

从图中可以看出返回的状态码是 304 Not Modified

这是因为按 F5 进行页面刷新时请求头会添加 If-Modified-Since 字段,如果资源未过期,命中缓存,服务器就直接返回 304 状态码,客户端直接使用本地的资源。

可以看出 chromefirefox 在按下 F5 后,其内部使用的缓存机制不同。firefox 使用的是协商缓存,而 chrome 使用的是强缓存。

Ctrl+F5

我们还是先看看在 chromeCtrl+F5 的表现。

我们发现在请求头中多了两个 Cache-Control:no-cache,Pragma:no-cache 参数,这两个参数什么意思呢?

在请求头中的 Cache-Control:no-cache 表示客户端不接受本地缓存的资源,需要到源服务器进行资源请求,其实可以使用缓存服务器的资源,不过需要到源服务器进行验证,验证通过就可以将缓存服务器的资源返回给客户端。

那么在 firefox 中的表现是怎样的呢?


请求头中同样多了两个 Cache-Control:no-cache,Pragma:no-cache 参数。

可以看出 chromefirefox 在按下 Ctrl+F5 后,都不会使用本地缓存,并且对缓存服务器的资源会再验证。

写到这里差不多就把 F5Ctrl+F5 的缓存原理讲的差不多了。不过每个浏览器它们在实现同一个动作的时候,总是会有差异,不过在业界内 chrome 的缓存优化机制是做的最好的。这也是为什么我们在使用 chrome 开发或者是浏览网站的时候体验都不错的原因。

读完 F5Ctrl+F5 刷新页面的原理,其实你也把强缓存和协商缓存的区别也复习了一遍。

补充

我们可以通过勾选 `Network` 面板中的 `Disable cache` 选项,这样当你按 `F5` 的时候,也是直接请求服务器资源的效果。

最后

如果你想进【大前端交流群】,关注公众号点击“交流加群”添加机器人自动拉你入群。关注我第一时间接收最新干货。

[译] 如何写出漂亮的 JavaScript 代码

原文:https://github.com/ryanmcdermott/clean-code-javascript
说明:本文翻译自 github 上的一个项目,非全文搬运,只取部分精华。

如何提高代码的可读性、复用性、扩展性。我们将从以下四个方面讨论:

  1. 变量
  2. 函数
  3. 异步

一、变量

用有意义且常用的单词命名

// Bad:
const yyyymmdstr = moment().format('YYYY/MM/DD');
// Good:
const currentDate = moment().format('YYYY/MM/DD');

保持统一

对同一类型的变量使用相同的命名保持统一:

// Bad:
getUserInfo();
getClientData();
getCustomerRecord();
// Good:
getUser()

每个常量(全大写)都该命名

可以用 ESLint 检测代码中未命名的常量。

// Bad:
// 其他人知道 86400000 的意思吗?
setTimeout( blastOff, 86400000 );
// Good:
const MILLISECOND_IN_A_DAY = 86400000;
setTimeout( blastOff, MILLISECOND_IN_A_DAY );

避免无意义的命名

既然创建了一个 car 对象,就没有必要把它的颜色命名为 carColor。

// Bad:
const car = {
    carMake: 'Honda',
    carModel: 'Accord',
    carColor: 'Blue'
};
function paintCar( car ) {
    car.carColor = 'Red';
}
// Good:
const car = {
    make: 'Honda',
    model: 'Accord',
    color: 'Blue'
};
function paintCar( car ) {
    car.color = 'Red';
}

传参使用默认值

// Bad:
function createMicrobrewery( name ) {
    const breweryName = name || 'Hipster Brew Co.';
    // ...
}
// Good:
function createMicrobrewery( name = 'Hipster Brew Co.' ) {
    // ...
}

二、函数

函数参数( 最好 2 个或更少 )

如果参数超过两个,建议使用 ES6 的解构语法,不用考虑参数的顺序。

// Bad:
function createMenu( title, body, buttonText, cancellable ) {
    // ...
}
// Good:
function createMenu( { title, body, buttonText, cancellable } ) {
    // ...
}
createMenu({
    title: 'Foo',
    body: 'Bar',
    buttonText: 'Baz',
    cancellable: true
});

一个方法只做一件事情

这是一条在软件工程领域流传久远的规则。严格遵守这条规则会让你的代码可读性更好,也更容易重构。如果违反这个规则,那么代码会很难被测试或者重用。

// Bad:
function emailClients( clients ) {
    clients.forEach( client => {
        const clientRecord = database.lookup( client );
        if ( clientRecord.isActive() ) {
            email( client );
        }
    });
}
// Good:
function emailActiveClients( clients ) {
    clients
        .filter( isActiveClient )
        .forEach( email );
}
function isActiveClient( client ) {
    const clientRecord = database.lookup( client );    
    return clientRecord.isActive();
}

函数名上体现它的作用

// Bad:
function addToDate( date, month ) {
    // ...
}
const date = new Date();
// 很难知道是把什么加到日期中
addToDate( date, 1 );
// Good:
function addMonthToDate( month, date ) {
    // ...
}
const date = new Date();
addMonthToDate( 1, date );

删除重复代码,合并相似函数

很多时候虽然是同一个功能,但由于一两个不同点,让你不得不写两个几乎相同的函数。

// Bad:
function showDeveloperList(developers) {
  developers.forEach((developer) => {
    const expectedSalary = developer.calculateExpectedSalary();
    const experience = developer.getExperience();
    const githubLink = developer.getGithubLink();
    const data = {
      expectedSalary,
      experience,
      githubLink
    };
    render(data);
  });
}
function showManagerList(managers) {
  managers.forEach((manager) => {
    const expectedSalary = manager.calculateExpectedSalary();
    const experience = manager.getExperience();
    const portfolio = manager.getMBAProjects();
    const data = {
      expectedSalary,
      experience,
      portfolio
    };
    render(data);
  });
}
// Good:
function showEmployeeList(employees) {
  employees.forEach(employee => {
    const expectedSalary = employee.calculateExpectedSalary();
    const experience = employee.getExperience();
    const data = {
      expectedSalary,
      experience,
    };
    switch(employee.type) {
      case 'develop':
        data.githubLink = employee.getGithubLink();
        break
      case 'manager':
        data.portfolio = employee.getMBAProjects();
        break
    }
    render(data);
  })
}

使用 Object.assign 设置默认属性

// Bad:
const menuConfig = {
  title: null,
  body: 'Bar',
  buttonText: null,
  cancellable: true
};
function createMenu(config) {
  config.title = config.title || 'Foo';
  config.body = config.body || 'Bar';
  config.buttonText = config.buttonText || 'Baz';
  config.cancellable = config.cancellable !== undefined ? config.cancellable : true;
}
createMenu(menuConfig);
// Good:
const menuConfig = {
  title: 'Order',
  // 不包含 body
  buttonText: 'Send',
  cancellable: true
};
function createMenu(config) {
  config = Object.assign({
    title: 'Foo',
    body: 'Bar',
    buttonText: 'Baz',
    cancellable: true
  }, config);
  // config : {title: "Order", body: "Bar", buttonText: "Send", cancellable: true}
  // ...
}
createMenu(menuConfig);

尽量不要写全局方法

在 JavaScript 中,永远不要污染全局,会在生产环境中产生难以预料的 bug。举个例子,比如你在 Array.prototype 上新增一个 diff 方法来判断两个数组的不同。而你同事也打算做类似的事情,不过他的 diff 方法是用来判断两个数组首位元素的不同。很明显你们方法会产生冲突,遇到这类问题我们可以用 ES2015/ES6 的语法来对 Array 进行扩展。

// Bad:
Array.prototype.diff = function diff(comparisonArray) {
  const hash = new Set(comparisonArray);
  return this.filter(elem => !hash.has(elem));
};
// Good:
class SuperArray extends Array {
  diff(comparisonArray) {
    const hash = new Set(comparisonArray);
    return this.filter(elem => !hash.has(elem));        
  }
}

尽量别用“非”条件句

// Bad:
function isDOMNodeNotPresent(node) {
  // ...
}
if (!isDOMNodeNotPresent(node)) {
  // ...
}
// Good:
function isDOMNodePresent(node) {
  // ...
}
if (isDOMNodePresent(node)) {
  // ...
}

不要过度优化

现代浏览器已经在底层做了很多优化,过去的很多优化方案都是无效的,会浪费你的时间。

// Bad:
// 现代浏览器已对此( 缓存 list.length )做了优化。
for (let i = 0, len = list.length; i < len; i++) {
  // ...
}
// Good:
for (let i = 0; i < list.length; i++) {
  // ...
}

删除弃用代码

这里没有实例代码,删除就对了

三、类

使用 ES6 的 class

在 ES6 之前,没有类的语法,只能用构造函数的方式模拟类,可读性非常差。

// Good:
// 动物
class Animal {
  constructor(age) {
    this.age = age
  };
  move() {};
}
// 哺乳动物
class Mammal extends Animal{
  constructor(age, furColor) {
    super(age);
    this.furColor = furColor;
  };
  liveBirth() {};
}
// 人类
class Human extends Mammal{
  constructor(age, furColor, languageSpoken) {
    super(age, furColor);
    this.languageSpoken = languageSpoken;
  };
  speak() {};
}

使用链式调用

这种模式相当有用,可以在很多库中都有使用。它让你的代码简洁优雅。

class Car {
  constructor(make, model, color) {
    this.make = make;
    this.model = model;
    this.color = color;
  }

  setMake(make) {
    this.make = make;
  }

  setModel(model) {
    this.model = model;
  }

  setColor(color) {
    this.color = color;
  }

  save() {
    console.log(this.make, this.model, this.color);
  }
}
// Bad:
const car = new Car('Ford','F-150','red');
car.setColor('pink');
car.save();

// Good: 
class Car {
  constructor(make, model, color) {
    this.make = make;
    this.model = model;
    this.color = color;
  }

  setMake(make) {
    this.make = make;
    // NOTE: Returning this for chaining
    return this;
  }

  setModel(model) {
    this.model = model;
    // NOTE: Returning this for chaining
    return this;
  }

  setColor(color) {
    this.color = color;
    // NOTE: Returning this for chaining
    return this;
  }

  save() {
    console.log(this.make, this.model, this.color);
    // NOTE: Returning this for chaining
    return this;
  }
}

const car = new Car("Ford", "F-150", "red").setColor("pink").save();

四、异步

使用 promise 或者 Async/Await 代替回调

// Bad:
get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', (requestErr, response) => {
  if (requestErr) {
    console.error(requestErr);
  } else {
    writeFile('article.html', response.body, (writeErr) => {
      if (writeErr) {
        console.error(writeErr);
      } else {
        console.log('File written');
      }
    });
  }
});
// Good:
get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin')
  .then((response) => {
    return writeFile('article.html', response);
  })
  .then(() => {
    console.log('File written');
  })
  .catch((err) => {
    console.error(err);
  });

// perfect:
async function getCleanCodeArticle() {
  try {
    const response = await get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin');
    await writeFile('article.html', response);
    console.log('File written');
  } catch(err) {
    console.error(err);
  }
}

最后

如果你想进【大前端交流群】,关注公众号点击“交流加群”添加机器人自动拉你入群。关注我第一时间接收最新干货。

聊一聊加班严重时要如何自我提升

前言

看《小欢喜》看到 45 岁的方圆被辞退的那个场景我有点难过。当然我不是因为他而难过,我是想到我 45 岁的时候会不会也被辞退?这种情绪持续了几天也无法挥散。(我似乎知道为什么有些 40 多岁的老员工突然离职了)

正文

现在很多公司加班都很严重,这种情况导致很多人将学习放到了一边。

有些人在这种情况越陷越深,导致自己知识架构和市场已经脱节。然后在出去找工作已经没有了议价能力,恶性循环,再然后到 40 岁的时候公司已经看不到你这颗满是锈迹的螺丝钉之价值所在,索性对你动了手。

工作繁忙只是充实的一个假象,所谓充实应该是每一天都有进步;忙到没法学习是对自己(未来的家庭)的极大不负责。

忙到没法学习可能是一个假命题

我们还是先来看一个场景:

早上 7:46 小方终于挤上了地铁,并找到了一个好的角落靠着;地铁开动了,他也开始看视频或者是斗地主。48 分钟过后,他到站了,出地铁口顺便买了一份 10 元的热干面。

在 8:57 的时候赶到了工位上,他打开了一些常摸鱼的网站,边逛变边吃着有点让他心疼的早餐。吃完早餐后,看了下钉钉群的消息,发现没有什么需要马上处理的。又开始水群了,转眼就到了中午。

13:32 的时候,小方不情愿的从桌子上爬了起来开始处理需求了,算是开始工作了;开始写 bug 解 bug,再带着开了两个会议;一转眼晚饭都没吃就 21:26 了,到了下班的时间。

23:04 小方终于洗好澡了,他觉得辛苦了一天;需要放松一下……

我觉得这样的程序员并不是个例,要从时间的占用来说,确实很忙;可是换个角度看,很多都是即时的快感。其实还是有很多时间可以拿来学习的。

我们现在回到最初的问题 —— 加班太严重,如何平衡工作和学习?

首先我们必须清楚学习的目的到底是什么?

  • 短期的升值加薪
  • 抵御年龄的增长所带来的竞争力下降

说到底学习是为了提升自己,而这是一个很漫长的过程。我们需要在学习的过程中找到使自己快乐的因素,我们才有可能坚持下去。

然后我们要弄清楚加班严重是自己的拖拉造成的还是公司有修福报的文化。

如果是自己的问题,那还没有到平衡工作和学习地步,需要从自身找问题;所以侧重点在后者。

1. 项目本身是否对你具有挑战

如果导致我们一直加班的项目是具有挑战的,那么项目本身就是一个很好地学习提高的途径。我们不用一味的想着该如何学习,更不要在工作的时候摸鱼来学习,这才是舍近求远的糊涂。

都说实践才是检验真理的唯一标准,既然有这么好的实践机会,为什么要白白浪费掉呢?

如果没有挑战,就是需要做无数重复的工作;那么我们是否可以写一个插件让自己从这个重复工作中解放出来。让自己可以做一些更有挑战的事用来提高自己。

如果真的没有任何挑战又没有时间学习,我们就需要适时的重新思考一下自己的职业规划。

我的第一份工作就是外派到银行做外包,真的没有任何技术含量;而且国企的甲方真的是大爷,做了一年完全没有任何成就感。外派的时候,只要有时间我就为自己跳槽做准备。

年轻人确实有时间,但这些时间最好用来提升自己。

学习和钱都是重要的,有些公司打着有好的学习环境而开很低的工资,最后很可能学不到知识也没有赚到钱。

2. 你是否利用好了周末和下班的时间

这里说的利用好,不是说你一定需要周末一直学习,而是说周末你需要有计划的学习。好些人一到周末就是:

一觉睡到十二点
醒了先来把吃鸡
三点吃个早晚饭
看个电影电视剧
睡前一看三点钟

这种状态其实就是不渴望学习新技能,或者说不想付出就想习得新技能。这是不可能的,有危机感就需要具备保持持续学习新知识的能力,要不然这种危机感也是徒劳。

平日里可以每天挤一些出来学习,巩固基础也好,学习框架也好、学习口语也好。只要是有计划的学习,都是有好处的。

周末的时间是很宝贵的,这也是少有的属于自己的大段可控时间。这种时间是最适合学习的。具体来说:3个小时我们能好好的梳理‘闭包’或者‘原型链’这样基础知识点;我们知道这样的基础知识点其实没有很多。坚持两三个月下来我们必定可以有一个全新的认识。

两三个月的周末时间,就能让我们重新认识基础,这笔买卖很是划算。

关于时间管理这块我自己也没有太多的经验,我自己的做法很简单:

  1. 拟定好自己的学习计划,按时复盘
  2. 学习的时候尽量避免手机的干扰

3. 你遇到问题会如何处理

当你遇到一个问题,你最先想到的是怎么解决呢(这不是选择题)?

  • 马上把问题抛到群里面问群友?
  • 问旁边的同事?
  • 打开搜索引擎查找答案?
  • 先处理 bug,然后有时间在好好研究,避免在犯。

可能有些人是第一种,可是这种方式很容易让人忘了最开始想干嘛。很容易就在群里面吹水,所以建议不要用这种方式。至于问同事的话,需要先掂量一下。如果是业务上的问题,可以质询一下;如果就是代码本身的问题,同事也是有需求任务在身的,这样可能反而不好。

我一般是会先找找其他人的处理方案,其实你遇到的问题,之前肯定是有人也遇到过得。你打开使用 Google(百度)可能几分钟就能找到问题所在。这种效率一般是最高的。

当然最好是可以自己记录下来,以免忘记。有时间的时候可以研究问什么这样写会有 bug。

你可以这样记录一些有意义的 bug:

【日期】:*********

【问题】:*********

【原因】:*********

【如何发现】:*********

【如何修复】:*********

【总结】:*********

这就像我们高考的错题本一样有用。

关于前端的学习路线和方法我会在下一篇文章中详细说明。这里就不展开了。

4. 十年后你的竞争力在哪里

这一点就不展开了,我觉得忧患意识是一个很好地品质,有忧患意识并付出行动同样也是。

对于如何确定自己是否还有竞争力,最直接的办法应该就是时常出去面试一下。看看市场需要,也检测自己。

5. 有效地评估开发时间

于江水大佬写的挺好的,我就直接搬运了。

如何能评估比较准的工期呢?一个很简单的公式送给大家:

需求非常明确而且经常这样做:自己评估时间 * 1.5
需求不够清晰,有可能变,但是代码和技术方案熟悉:自己评估的时间 * 2
需求不够清晰,代码和技术方案也是新的,需要探索:自己评估的时间 * 2.5 or 3

自己评估的时间一般会留点 buffer,自我感觉应该没问题,实际上开发过程可能会有各种会议、需求和技术方案变更或者突发事件。所以多留一点 buffer 会更好,因为这个时间点可能是下游运营活动上线时间点,评估后业务方觉得太长可以砍需求拆成两期或者调整上线预期,但一旦设置了时间点,不应该跳票。如果你比预期早完成上线,皆大欢喜,如果你一次次的告知业务方还需要延期一两天,效果正好相反。

结尾

想要提升自己的能力,我们必须付出很多时间。对应的娱乐的时候就需要减少。这个道理大家都是清楚的,但是能做到的却不多。

最后提几个建议:

  1. 注重长期的可能性,而不是短期的快感
  2. 无论如何一定要做好持续学习的计划
  3. 技术之外的能力也很重要,比如表达能力、思考方式
  4. 系统的学习基础知识很重要,不要急于求新

其实说到底就是需要有持续学习的能力和渴望,如果加班已经严重影响到学习,那么这些人肯定会适时的考虑自己的职业规划。

最后

你可以关注我的同名公众号【小生方勤】,这里我会分享优质的文章,我们一同进步。

如果你想进【大前端交流群】,(群满 100 了,有需要的关注公众号,点击加群交流)即刻加入。

【前端词典】5 种滚动吸顶实现方式的比较[性能升级版]

修改版预览

这篇文章是三天前写就的,有大佬给我提了一些修改意见,我觉得这个意见确实中肯。所以就有了这个升级的修改版本。代码同步更新到 GitHub 了。

修改内容如下:

  1. 添加了图文说明,直观的说明 getBoundingClientRect()集合含义
  2. 频繁 reflow 风险该如何规避(优化滚动监听)
  3. 监听滚动带来的性能问题(使用 IntersectionObserver, 新方案)

修改更新的内容在第 4 点和第 5 点,如果你看过本文,可以直接看修改更新的内容。或者你可以再看一遍。

前言

我入职第二家公司接到的第一个需求就是修复之前外包做的滚动吸顶效果。我当时很纳闷为何一个滚动吸顶会有 bug,后来我查看代码才发现直接用的是 offsetTop 这个属性,而且并没有做兼容性处理。

offsetTop

用于获得当前元素到定位父级( element.offsetParent )顶部的距离(偏移值)。

定位父级 offsetParent 的定义是:与当前元素最近的 position != static 的父级元素。

或许写这个代码的人没有注意到“定位父级”这个这个附属条件。

后来在项目中总会遇到滚动吸顶的效果需要实现,现在我将我知道的 4 种滚动吸顶实现方式做详细介绍。

目录

  1. 使用 position:sticky 实现
  2. 使用 JQuery 的 offset().top 实现
  3. 使用原生的 offsetTop 实现
  4. 使用 obj.getBoundingClientRect().top 实现
  5. 性能优化篇

以上这四种方式你都了解吗?相关代码已上传到 GitHub ,感兴趣的可以 clone 代码到本地运行。望给个 star 支持一下。

四种实现方式

我们先看一下效果图:

一、使用 position:sticky 实现

1、粘性定位是什么?

粘性定位 sticky 相当于相对定位 relative 和固定定位 fixed 的结合;在页面元素滚动过程中,某个元素距离其父元素的距离达到 sticky 粘性定位的要求时;元素的相对定位 relative 效果变成固定定位 fixed 的效果。

MDN 传送门

2、如何使用?

使用条件:

  1. 父元素不能 overflow:hidden 或者 overflow:auto 属性
  2. 必须指定 top、bottom、left、right 4 个值之一,否则只会处于相对定位
  3. 父元素的高度不能低于 sticky 元素的高度
  4. sticky 元素仅在其父元素内生效

在需要滚动吸顶的元素加上以下样式便可以实现这个效果:

.sticky {
    position: -webkit-sticky;
    position: sticky;
    top: 0;
}

3、这个属性好用吗?

我们先看下在 Can I use 中看看这个属性的兼容性:

可以看出这个属性的兼容性并不是很好,因为这个 API 还只是实验性的属性。不过这个 API 在 IOS 系统的兼容性还是比较好的。

所以我们在生产环境如果使用这个 API 的时候一般会和下面的几种方式结合使用。

二、使用 JQuery 的 offset().top 实现

我们知道 JQuery 中封装了操作 DOM 和读取 DOM 计算属性的 API,基于 offset().top 这个 API 和 scrollTop() 的结合,我们也可以实现滚动吸顶效果。

...
window.addEventListener('scroll', self.handleScrollOne);
...
handleScrollOne: function() {
    let self = this;
    let scrollTop = $('html').scrollTop();
    let offsetTop = $('.title_box').offset().top;
    self.titleFixed = scrollTop > offsetTop;
}
...

这样实现固然可以,不过由于 JQuery 慢慢的退出历史的舞台,我们在代码中尽量不使用 JQuery 的 API。我们可以基于 offset().top 的源码自己处理原生 offsetTop。于是乎就有了第三种方式。

scrolloTop() 有兼容性问题,在微信浏览器、IE、某些 firefox 版本中 $('html').scrollTop() 的值会为 0,于是乎也就有了第三种方案的兼容性写法。

三、使用原生的 offsetTop 实现

我们知道 offsetTop 是相对定位父级的偏移量,倘若需要滚动吸顶的元素出现定位父级元素,那么 offsetTop 获取的就不是元素距离页面顶部的距离。

我们可以自己对 offsetTop 做以下处理:

getOffset: function(obj,direction){
    let offsetL = 0;
    let offsetT = 0;
    while( obj!== window.document.body && obj !== null ){
        offsetL += obj.offsetLeft;
        offsetT += obj.offsetTop;
        obj = obj.offsetParent;
    }
    if(direction === 'left'){
        return offsetL;
    }else {
        return offsetT;
    }
}

使用:

...
window.addEventListener('scroll', self.handleScrollTwo);
...
handleScrollTwo: function() {
    let self = this;
    let scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop;
    let offsetTop = self.getOffset(self.$refs.pride_tab_fixed);
    self.titleFixed = scrollTop > offsetTop;
}
...

你是不是看出了以上两种方式的一些问题?

我们一定需要使用 scrollTop - offsetTop 的值来实现滚动吸顶的效果吗?答案是否定的。

我们一同看看第四种方案。

四、使用 obj.getBoundingClientRect().top 实现

定义:

这个 API 可以告诉你页面中某个元素相对浏览器视窗上下左右的距离。

使用:
tab 吸顶可以使用 obj.getBoundingClientRect().top 代替 scrollTop - offsetTop,代码如下:

// html
<div class="pride_tab_fixed" ref="pride_tab_fixed">
    <div class="pride_tab" :class="titleFixed == true ? 'isFixed' :''">
        // some code
    </div>
</div>

// vue
export default {
    data(){
      return{
        titleFixed: false
      }
    },
    activated(){
      this.titleFixed = false;
      window.addEventListener('scroll', this.handleScroll);
    },
    methods: {
      //滚动监听,头部固定
      handleScroll: function () {
        let offsetTop = this.$refs.pride_tab_fixed.getBoundingClientRect().top;
        this.titleFixed = offsetTop < 0;
        // some code
      }
    }
  }

offsetTop 和 getBoundingClientRect() 区别

1. getBoundingClientRect():

用于获得页面中某个元素的左,上,右和下分别相对浏览器视窗的位置。不包含文档卷起来的部分。

该函数返回一个 object 对象,有8个属性:
top, right, buttom, left, width, height, x, y

2. offsetTop:

用于获得当前元素到定位父级( element.offsetParent )顶部的距离(偏移值)。

定位父级 offsetParent 的定义是:与当前元素最近的 position != static 的父级元素。

offsetTopoffsetParent 方法相结合可以获得该元素到 body 上边距的距离。代码如下:

getOffset: function(obj,direction){
    let offsetL = 0;
    let offsetT = 0;
    while( obj!== window.document.body && obj !== null ){
        offsetL += obj.offsetLeft;
        offsetT += obj.offsetTop;
        obj = obj.offsetParent;
    }
    if(direction === 'left'){
        return offsetL;
    }else {
        return offsetT;
    }
}

延伸知识点

offsetWidth:

元素在水平方向上占用的空间大小:
offsetWidth = border-left + padding-left + width + padding-right + border-right

offsetHeight:

元素在垂直方向上占用的空间大小:
offsetHeight = border-top + padding-top + height + padding-bottom + border-bottom

注:如果存在垂直滚动条,offsetWidth 也包括垂直滚动条的宽度;如果存在水平滚动条,offsetHeight 也包括水平滚动条的高度;

offsetTop:

元素的上外边框至 offsetParent 元素的上内边框之间的像素距离;

offsetLeft:

元素的左外边框至 offsetParent 元素的左内边框之间的像素距离;

注意事项

  1. 所有偏移量属性都是只读的;
  2. 如果给元素设置了 display:none,则它的偏移量属性都为 0;
  3. 每次访问偏移量属性都需要重新计算(保存变量);
  4. 在使用的时候可能出现 DOM 没有初始化,就读取了该属性,这个时候会返回 0;对于这个问题我们需要等到 DOM 元素初始化完成后再执行。

遇到的两个问题

一、吸顶的那一刻伴随抖动

出现抖动的原因是因为:在吸顶元素 position 变为 fixed 的时候,该元素就脱离了文档流,下一个元素就进行了补位。就是这个补位操作造成了抖动。

解决方案

为这个吸顶元素添加一个等高的父元素,我们监听这个父元素的 getBoundingClientRect().top 值来实现吸顶效果,即:

<div class="title_box" ref="pride_tab_fixed">
    <div class="title" :class="titleFixed == true ? 'isFixed' :''">
    使用 `obj.getBoundingClientRect().top` 实现
    </div>
</div>

这个方案就可以解决抖动的 Bug 了。

二、吸顶效果不能及时响应

这个问题是我比较头痛,之前我没有在意过这个问题。直到有一天我用美团点外卖的时候,我才开始注意这个问题。

描述:

  1. 当页面往下滚动时,吸顶元素需要等页面滚动停止之后才会出现吸顶效果
  2. 当页面往上滚动时,滚动到吸顶元素恢复文档流位置时吸顶元素不恢复原样,而等页面停止滚动之后才会恢复原样

原因:
在 ios 系统上不能实时监听 scroll 滚动监听事件,在滚动停止时才触发其相关的事件。

解决方案:

还记得第一种方案中的 position:sticky 吗?这个属性在 IOS6 以上的系统中有良好的兼容性,所以我们可以区分 IOS 和 Android 设备做两种处理。

IOS 使用 position:sticky,Android 使用滚动监听 getBoundingClientRect().top 的值。

如果 IOS 版本过低呢?这里提供一种思路:window.requestAnimationFrame()

性能优化篇(新增)

到此 4 中滚动吸顶的方式介绍完了,可是这样就真的结束了吗?其实还是有优化的空间的。

我们从两个方向做性能优化(其实是一个方向):

  1. 避免过度的 reflow
  2. 优化滚动监听事件

问题定位过程

我们知道过度的 reflow 会使页面的性能下降。所以我们需要尽可能的降低 reflow 的次数,给用户更加流畅的感觉。

有的朋友或许会说这个我知道,可是这和滚动吸顶有什么关系呢?

不急,你是否还记得滚动吸顶使用了 offsetTop 或者 getBoundingClientRect().top 来获取响应的偏移量呢?

既然有读取元素的属性就自然会导致页面 reflow。

因此我们优化的方向就是从减少读取元素属性次数下手,查看代码发现一触发屏幕滚动事件就会调用相关方法读取元素的偏移量。

优化方案

解决这个问题有以下两个方案:

  1. 牺牲平滑度满足性能,使用节流控制相关方法的调用
  2. 使用 IntersectionObserver 和节流结合,也牺牲了平滑度。

第一种方案

这个方案很常见,不过其带来的副作用也很明显,就是在吸顶效果会有些延迟,如果产品可以接受那就不失为一种好方法。

这样可以控制在一定时间内只读取

这里节流函数就直接是用 lodash.js 封装好的 throttle 方法。

代码如下:

window.addEventListener('scroll', _.throttle(self.handleScrollThree, 50));

第二种方案

第二种方案相对来说容易接受一点,就是支持 IntersectionObserver 就用 IntersectionObserver,否则就用 throttle。

我们先讲讲 IntersectionObserver

IntersectionObserver 可以用来监听元素是否进入了设备的可视区域之内,而不需要频繁的计算来做这个判断。

通过这个属性我们就可以在元素不在可视范围内,不去读取元素的相对位置,已达到性能优化;当浏览器不支持这个属性的时候就使用 throttle 来处理。

我们看看这个属性的兼容性怎么样:

大概支持了 60% 以上,在项目中还是可以使用的(你需要做好兼容性处理)。

关于 IntersectionObserver 如何使用,请看 MDN 或者 阮一峰教程

使用 IntersectionObserver 和 throttle 优化的代码如下:

IntersectionObserverFun: function() {
    let self = this;
    let ele = self.$refs.pride_tab_fixed;
    if( !!IntersectionObserver ){
        let observer = new IntersectionObserver(function(){
            let offsetTop = ele.getBoundingClientRect().top;
            self.titleFixed = offsetTop < 0;
        }, {
            threshold: [1]
        });
        observer.observe(document.getElementsByClassName('title_box')[0]);
    } else {
        window.addEventListener('scroll', _.throttle(function(){
            let offsetTop = ele.getBoundingClientRect().top;
            self.titleFixed = offsetTop < 0;
        }, 50));
    }
}, 

注意

IntersectionObserver API 是异步的,不随着目标元素的滚动同步触发。

规格写明,IntersectionObserver的实现,应该采用 requestIdleCallback()。它不会立即执行回调,它会调用 window.requestIdleCallback() 来异步的执行我们指定的回调函数,而且还规定了最大的延迟时间是 100 毫秒。

总结:

这种 IntersectionObserver 和 throttle 结合的方案不失为一种可选择的方案,这种方案的优点就在于可以有效地减少页面 reflow 的风险,不过缺点也是有的,需要牺牲页面的平滑度。具体该如何取舍,就看业务的需要啦。

最后

如果你想进【大前端交流群】,关注公众号点击“交流加群”添加机器人自动拉你入群。关注我第一时间接收最新干货。

【前端词典】4 个实用有趣的 JS 特性

前言

最近在学习的过程中发现了我之前未曾了解过的一些特性,发现有些很有趣并且在处理一些问题的时候可以给我一个新的思路。

这里我将这些特性介绍给大家。

4 个有趣的 JS 特性

利用 a 标签解析 URL

有的时候我们需要从一个 URL 中提取域名,查询关键字,变量参数值等,一般我们会自己去解析 URL 来获取这些内容。可是你或许不知道还有更简单的方法。

即创建一个 a 标签将需要解析的 URL 赋值给 a 的 href 属性,然后我们就能很方便的拿到这些内容。代码如下:

function parseURL(url) {
    var a =  document.createElement('a');
    a.href = url;
    return {
        host: a.hostname,
        port: a.port,
        query: a.search,
        params: (function(){
            var ret = {},
                seg = a.search.replace(/^\?/,'').split('&'),
                len = seg.length, i = 0, s;
            for (;i<len;i++) {
                if (!seg[i]) { continue; }
                s = seg[i].split('=');
                ret[s[0]] = s[1];
            }
            return ret;
        })(),
        hash: a.hash.replace('#','')
    };
}

标记语句(label)

有的时候我们会写双重 for 循环做一些数据处理,我们有的时候希望满足条件的时候就直接跳出循环。以免浪费不必要资源。

这个时候我们就可以用 labelcontinue/break 配合使用:

firstLoop: 
for (let i = 0; i < 3; i++) { 
   for (let j = 0; j < 3; j++) {
      if (i === j) {
         continue firstLoop; // 继续 firstLoop 循环
         // break firstLoop; // 中止 firstLoop 循环
      }
      console.log(`i = ${i}, j = ${j}`);
   }
}
// 输出
i = 1, j = 0
i = 2, j = 0
i = 2, j = 1
 
for (let i = 0; i < 3; i++) { 
   for (let j = 0; j < 3; j++) {
      if (i === j) {
         continue 
      }
      console.log(`i = ${i}, j = ${j}`);
   }
}
// 输出
i = 0, j = 1
i = 0, j = 2
i = 1, j = 0
i = 1, j = 2
i = 2, j = 0
i = 2, j = 1

相信你从上面两段代码的输出可以对标记语句有一个了解。

void 运算符

void 运算符对给定的表达式进行求值,然后返回 undefined。

由于 void 会忽略操作数的值,因此在操作数具有副作用的时候使用 void 会更加合理。

使用 void 替换 undefined

由于 undefined 并不是 JavaScript 的关键字,所以我们在赋值某个变量为 undefined 时可能会有点意想不到的结果。

function t(){
    var undefined = 10;
    console.log(undefined);
}
console.log(t()); // 大多数浏览器下都是10

如上代码我们可能希望赋值为 undefined,但却得到了 10 这个莫名其妙的情况。所以我们可以使用使用 void 替换 undefined

这也是为什么我们在很多源码中都能看到使用 void 替换 undefined

IntersectionObserver 是什么?

IntersectionObserver 可以用来监听元素是否进入了设备的可视区域之内,而不需要频繁的计算来做这个判断。

所以我们可以用这个特性来处理曝光埋点,而不是用 getBoundingClientRect().top 这种更加损耗性能的方式来处理。

当然你也可以用这个 API 来优化滚动吸顶,代码如下:

IntersectionObserverFun: function() {
    let self = this;
    let ele = self.$refs.pride_tab_fixed;
    if( !!IntersectionObserver ){
        let observer = new IntersectionObserver(function(){
            let offsetTop = ele.getBoundingClientRect().top;
            self.titleFixed = offsetTop < 0;
        }, {
            threshold: [1]
        });
        observer.observe(document.getElementsByClassName('title_box')[0]);
    } else {
        window.addEventListener('scroll', _.throttle(function(){
            let offsetTop = ele.getBoundingClientRect().top;
            self.titleFixed = offsetTop < 0;
        }, 50));
    }
}, 

希望这 4 个特性可以对你有所帮助,然后点个赞在走呗。

最后

如果你想进【大前端交流群】,关注公众号点击“交流加群”添加机器人自动拉你入群。关注我第一时间接收最新干货。

【前端词典】如何向老板解释反向代理

前言

现在看到的这篇文章是修改后的第三个版本。

由于我家老板看过之后,对这篇文章的评价是:写的不错,语句是通顺的,排版是可以的,但反向代理是什么还是不清楚?所以我就想尝试着向非 IT 工作者解释“正向代理”和“反向代理”。

接下来我会先尝试面向大众,来解释“代理”的概念。在从专业的角度解释“正向代理”和“反向代理”。

概念实例化

在讲代理的概念之前我先讲个类比。也是我向我家老板解释的过程。


还好我反应机敏,要不然这个月的零花钱就要替我挡一刀了。可是我该怎么解释呢?还要让没有编程语言基础的人也听懂,伤脑筋啊!

在没有思绪的时候,她突然问我晚上吃了没有?这不就是很好的例子吗?








解释了这么久,不知道是真的懂了,还是因为太困了。不过我有钱吃肉了。

接下来我们就认真的看看“正向代理”和“反向代理”

概念

首先看看说明图,先有一个整体的理解。

正向代理( Forward Proxy ):

是指是一个位于客户端和原始服务器之间的服务器,为了从原始服务器取得内容, 客户端向代理发送一个请求并指定目标(原始服务器),然后代理向原始服务器转交请求并将获得的内容返回给客户端。客户端才能使用正向代理。

反向代理( Reverse Proxy ):

是指以代理服务器来接受 Internet 上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给 Internet 上请求连接的客户端,此时代理服务器对外就表现为一个反向代理服务器。

接下来我提炼一下各自的特点。

特点

正向代理

  1. 代理客户;
  2. 隐藏真实的客户,为客户端收发请求,使真实客户端对服务器不可见;
  3. 一个局域网内的所有用户可能被一台服务器做了正向代理,由该台服务器负责 HTTP 请求;
  4. 意味着同服务器做通信的是正向代理服务器;

反向代理

  1. 代理服务器;
  2. 隐藏了真实的服务器,为服务器收发请求,使真实服务器对客户端不可见;
  3. 负载均衡服务器,将用户的请求分发到空闲的服务器上;
  4. 意味着用户和负载均衡服务器直接通信,即用户解析服务器域名时得到的是负载均衡服务器的 IP ;

共同点

  1. 都是做为服务器和客户端的中间层
  2. 都可以加强内网的安全性,阻止 web 攻击
  3. 都可以做缓存机制,提高访问速度

区别

  1. 正向代理其实是客户端的代理,反向代理则是服务器的代理。
  2. 正向代理中,服务器并不知道真正的客户端到底是谁;而在反向代理中,客户端也不知道真正的服务器是谁。
  3. 作用不同。正向代理主要是用来解决访问限制问题;而反向代理则是提供负载均衡、安全防护等作用。

说了这么多,现在说说代理在工作中的时机应用场景吧。

实际应用

翻墙软件 —— 正向代理

我们知道在国内用访问 www.google.com 是无法访问的,因为正常情况下是会被 GFW 限制访问的。

可是你还是想使用 google 来科学上网的话,这个时候我们就需要一些代理(翻墙软件)来帮我们去请求 www.google.com,代理再把响应结果返回给你。

GFW 的作用主要是用于分析和过滤**境内外网络间的互相访问。也就是说,他不仅能限制国内网民访问境外的某些站点,也能限制国外用户访问国内的站点。 我们通常说的“被墙”,就是指访问被 GFW 所限制。而”翻墙“,顾名思义,则是突破 GFW 的限制。

Nginx 服务器 —— 反向代理

Nginx 服务器的功能有很多,诸如反向代理、负载均衡、静态资源服务器等。

客户端本来可以直接通过 HTTP 协议访问服务器,不过我们可以在中间加上一个 Nginx 服务器,客户端请求 Nginx 服务器,Nginx 服务器请求应用服务器,然后将结果返回给客户端,此时 Nginx 服务器就是反向代理服务器。

在虚拟主机的配置中配置反向代理

# 虚拟主机的配置
server {
    listen 8080;                         # 监听的端口
    server_name  192.168.1.1;            # 配置访问域名
    root  /data/toor;                    # 站点根目录
    error_page 502 404 /page/404.html;   # 错误页面
    location ^~ /api/  {                        # 使用 /api/ 代理 proxy_pass 的值
        proxy_pass http://192.168.20.1:8080;    # 被代理的应用服务器 HTTP 地址
    }
}

以上简单的配置就可以实现反向代理的功能。

当然反向代理也可以处理跨域问题。

对于使用 vue-cli 搭建的工程而言,我们知道 vue-cli 采用 http-proxy-middleware 插件来进行代理服务器等各项配置。

所以我们可以利用 proxyTable,设置地址映射表。即使用 proxyTable 这个属性进行相关的配置来解决跨域问题带来的烦恼。配置如下:

...
proxyTable: {
    '/weixin': {
        target: 'http://192.168.20.1:8080/', // 接口的域名
        secure: false,      // 如果是 https 接口,需要配置这个参数
        changeOrigin: true, // 如果接口跨域,需要进行这个参数配置
        pathRewrite: {
            '^/weixin': ''
        }
    },
},
...

负载均衡的配置

# upstream 表示负载服务器池,定义名字为 my
upstream my {
    server 192.168.2.1:8080 weight=1 max_fails=2 fail_timeout=30s;
    server 192.168.2.2:8080 weight=1 max_fails=2 fail_timeout=30s;
    server 192.168.2.3:8080 weight=1 max_fails=2 fail_timeout=30s;
    server 192.168.2.4:8080 weight=1 max_fails=2 fail_timeout=30s;
   # 即在 30s 内尝试 2 次失败即认为主机不可用
  }

负载均衡即将 请求/数据 轮询分摊到多个服务器上执行,负载均衡的关键在于 均匀

也可以通过 ip-hash 的方式,根据客户端 ip 地址的 hash 值将请求分配给固定的某一个服务器处理。

另外,服务器的硬件配置可能不同,配置好的服务器可以处理更多的请求,这时可以通过 weight 参数来控制。

最后

如果你想进【大前端交流群】,关注公众号点击“交流加群”添加机器人自动拉你入群。关注我第一时间接收最新干货。

【前端词典】从源码解读 Vuex 注入 Vue 生命周期的过程

前言

这篇文章是【前端词典】系列文章的第 13 篇文章,接下的 9 篇我会围绕着 Vue 展开,希望这 9 篇文章可以使大家加深对 Vue 的了解。当然这些文章的前提是默认你对 Vue 有一定的基础。如果一点基础都没有,建议先看官方文档。

第一篇文章我会结合 Vue 和 Vuex 的部分源码,来说明 Vuex 注入 Vue 生命周期的过程。

说到源码,其实没有想象的那么难。也和我们平时写业务代码差不多,都是方法的调用。但是源码的调用树会复杂很多。

为何使用 Vuex

使用 Vue 我们就不可避免的会遇到组件间共享的数据或状态。应用的业务代码逐渐复杂,props、事件、事件总线等通信的方式的弊端就会愈发明显。这个时候我们就需要 Vuex 。Vuex 是一个专门为 Vue 设计的状态管理工具。

状态管理是 Vue 组件解耦的重要手段。

它借鉴了 Flux、redux 的基本**,将状态抽离到全局,形成一个 Store。

Vuex 不限制你的代码结构,但需要遵守一些规则:

  1. 应用层级的状态应该集中到单个 store 对象中
  2. 提交 mutation 是更改状态的唯一方法,并且这个过程是同步的
  3. 异步逻辑都应该封装到 action 里面

Vuex 注入 Vue 生命周期的过程

我们在安装插件的时候,总会像下面一样用 Vue.use() 来载入插件,可是 Vue.use() 做了什么呢?

import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

Vue.use() 做了什么

安装 Vue.js 插件。如果插件是一个对象,必须提供 install 方法。如果插件是一个函数,它会被作为 install 方法。install 方法调用时,会将 Vue 作为参数传入。

以上是 官方文档 的解释。

接下来我们从源码部分来看看 Vue.use() 都做了什么。

Vue 源码在 initGlobalAPI 入口方法中调用了 initUse (Vue) 方法,这个方法定义了 Vue.use() 需要做的内容。

function initGlobalAPI (Vue) {
  ......
  initUse(Vue);
  initMixin$1(Vue); // 下面讲 Vue.mixin 会提到
  ......
}

function initUse (Vue) {
  Vue.use = function (plugin) {
    var installedPlugins = (this._installedPlugins || (this._installedPlugins = []));
    /* 判断过这个插件是否已经安装 */
    if (installedPlugins.indexOf(plugin) > -1) {
      return this
    }
    var args = toArray(arguments, 1);
    args.unshift(this);
    /* 判断插件是否有 install 方法 */
    if (typeof plugin.install === 'function') {
      plugin.install.apply(plugin, args);
    } else if (typeof plugin === 'function') {
      plugin.apply(null, args);
    }
    installedPlugins.push(plugin);
    return this
  };
}

这段代码主要做了两件事情:

  1. 一件是防止重复安装相同的 plugin
  2. 另一件是初始化 plugin

插件的 install 方法

看完以上源码,我们知道插件(Vuex)需要提供一个 install 方法。那么我们看看 Vuex 源码中是否有这个方法。结果当然是有的:

/* 暴露给外部的 install 方法 */
function install (_Vue) {
  /* 避免重复安装(Vue.use 内部也会检测一次是否重复安装同一个插件)*/
  if (Vue && _Vue === Vue) {
    {
      console.error(
        '[vuex] already installed. Vue.use(Vuex) should be called only once.'
      );
    }
    return
  }
  Vue = _Vue;
  /* 将 vuexInit 混淆进 Vue 的 beforeCreate(Vue2.0) 或 _init 方法(Vue1.0) */
  applyMixin(Vue);
}

这段代码主要做了两件事情:

  1. 一件是防止 Vuex 被重复安装
  2. 另一件是执行 applyMixin,目的是执行 vuexInit 方法初始化 Vuex

接下来 我们看看 applyMixin(Vue) 源码:

/* 将 vuexInit 混淆进 Vue 的 beforeCreate */
function applyMixin (Vue) {
  var version = Number(Vue.version.split('.')[0]);
  if (version >= 2) {
    Vue.mixin({ beforeCreate: vuexInit });
  } else {
    /* Vue1.0 的处理逻辑,此处省略 */
    ......
  }
  function vuexInit () {
    ......
  }
}

从上面的源码,可以看出 Vue.mixin 方法将 vuexInit 方法混淆进 beforeCreate 钩子中,也是因为这个操作,所以每一个 vm 实例都会调用 vuexInit 方法。那么 vuexInit 又做了什么呢?

vuexInit()

我们在使用 Vuex 的时候,需要将 store 传入到 Vue 实例中去。

new Vue({
  el: '#app',
  store
});

但是我们却在每一个 vm 中都可以访问该 store,这个就需要靠 vuexInit 了。

  function vuexInit () {
    const options = this.$options
    if (options.store) {
      /* 根节点存在 stroe 时 */
      this.$store = typeof options.store === 'function'
        ? options.store()
        : options.store
    } else if (options.parent && options.parent.$store) {
      /* 子组件直接从父组件中获取 $store,这样就保证了所有组件都公用了全局的同一份 store*/
      this.$store = options.parent.$store
    }
  }

根节点存在 stroe 时,则直接将 options.store 赋值给 this.$store。否则则说明不是根节点,从父节点的 $store 中获取。

通过这步的操作,我们就以在任意一个 vm 中通过 this.$store 来访问 Store 的实例。接下来我们反过来说说 Vue.mixin()。

Vue.mixin()

全局注册一个混入,影响注册之后所有创建的每个 Vue 实例。插件作者可以使用混入,向组件注入自定义的行为。不推荐在应用代码中使用。

在 vue 的 initGlobalAPI 入口方法中调用了 initMixin$1(Vue) 方法:

function initMixin$1 (Vue) {
  Vue.mixin = function (mixin) {
    this.options = mergeOptions(this.options, mixin);
    return this
  };
}

Vuex 注入 Vue 生命周期的过程大概就是这样,如果你感兴趣的话,你可以直接看看 Vuex 的源码,接下来我们说说 Store。

Store

上面我们讲到了 vuexInit 会从 options 中获取 Store。所以接下来会讲到 Store 是怎么来的呢?

我们使用 Vuex 的时候都会定义一个和下面类似的 Store 实例。

import Vue from 'vue'
import Vuex from 'vuex'
import mutations from './mutations'

Vue.use(Vuex)

const state = {
    showState: 0,                             
}

export default new Vuex.Store({
    strict: true,
	state,
	getters,
})

不要在发布环境下启用严格模式。严格模式会深度监测状态树来检测不合规的状态变更 —— 请确保在发布环境下关闭严格模式,以避免性能损失。

state 的响应式

你是否关心 state 是如何能够响应式呢?这个主要是通过 Store 的构造函数中调用的 resetStoreVM(this, state) 方法来实现的。

这个方法主要是重置一个私有的 _vm(一个 Vue 的实例) 对象。这个 _vm 对象会保留我们的 state 树,以及用计算属性的方式存储了 store 的 getters。现在具体看看它的实现过程。

/* 使用 Vue 内部的响应式注册 state */
function resetStoreVM (store, state, hot) {
  /* 存放之前的vm对象 */
  const oldVm = store._vm 

  store.getters = {}
  const wrappedGetters = store._wrappedGetters
  const computed = {}

  /* 通过 Object.defineProperty 方法为 store.getters 定义了 get 方法。当在组件中调用 this.$store.getters.xxx 这个方法的时候,会访问 store._vm[xxx]*/
  forEachValue(wrappedGetters, (fn, key) => {
    computed[key] = partial(fn, store)
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key],
      enumerable: true // for local getters
    })
  })

  const silent = Vue.config.silent
  /* 设置 silent 为 true 的目的是为了取消 _vm 的所有日志和警告 */
  Vue.config.silent = true
  /*  这里new了一个Vue对象,运用Vue内部的响应式实现注册state以及computed*/
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  })
  Vue.config.silent = silent

  /* 使能严格模式,Vuex 中对 state 的修改只能在 mutation 的回调函数里 */
  if (store.strict) {
    enableStrictMode(store)
  }

  if (oldVm) {
    /* 解除旧 vm 的 state 的引用,并销毁这个旧的 _vm 对象 */
    if (hot) {
      store._withCommit(() => {
        oldVm._data.$$state = null
      })
    }
    Vue.nextTick(() => oldVm.$destroy())
  }
}

state 的响应式大概就是这样实现的,也就是初始化 resetStoreVM 方法的过程。

看看 Store 的 commit 方法

我们知道 commit 方法是用来触发 mutation 的。

commit (_type, _payload, _options) {
  /* unifyObjectStyle 方法校参 */
  const {
    type,
    payload,
    options
  } = unifyObjectStyle(_type, _payload, _options)

  const mutation = { type, payload }
  /* 找到相应的 mutation 方法 */
  const entry = this._mutations[type]
  if (!entry) {
    if (process.env.NODE_ENV !== 'production') {
      console.error(`[vuex] unknown mutation type: ${type}`)
    }
    return
  }
  /* 执行 mutation 中的方法 */
  this._withCommit(() => {
    entry.forEach(function commitIterator (handler) {
      handler(payload)
    })
  })
  /* 通知所有订阅者,传入当前的 mutation 对象和当前的 state */
  this._subscribers.forEach(sub => sub(mutation, this.state))

  if (
    process.env.NODE_ENV !== 'production' &&
    options && options.silent
  ) {
    console.warn(
      `[vuex] mutation type: ${type}. Silent option has been removed. ` +
      'Use the filter functionality in the vue-devtools'
    )
  }
}

该方法先进行参数风格校验,然后利用 _withCommit 方法执行本次批量触发 mutation 处理函数。执行完成后,通知所有 _subscribers(订阅函数)本次操作的 mutation 对象以及当前的 state 状态。

Vue 相关文章输出计划

最近总有朋友问我 Vue 相关的问题,因此接下来我会输出 9 篇 Vue 相关的文章,希望对大家有一定的帮助。我会保持在 7 到 10 天更新一篇。

  1. 【前端词典】Vuex 注入 Vue 生命周期的过程
  2. 【前端词典】浅析 Vue 响应式原理
  3. 【前端词典】新老 VNode 进行 patch 的过程
  4. 【前端词典】如何开发功能组件并上传 npm
  5. 【前端词典】从这几个方面优化你的 Vue 项目
  6. 【前端词典】从 Vue-Router 设计讲前端路由发展
  7. 【前端词典】在项目中如何正确的使用 Webpack
  8. 【前端词典】Vue 服务端渲染
  9. 【前端词典】Axios 与 Fetch 该如何选择

最后

如果你想进【大前端交流群】,关注公众号点击“交流加群”添加机器人自动拉你入群。关注我第一时间接收最新干货。

[译]10 种 JavaScript 最常见的错误

原文:https://rollbar.com/blog/top-10-javascript-errors/
声明:对原文稍作了修改,便于阅读

前言

看了数千个项目后,发现了 10 个最常见的 JavaScript 错误。我们会告诉你什么原因导致了这些错误,以及如何防止这些错误发生。如果你能够避免落入这些 “陷阱”,你将会成为一个更好的开发者。

JavaScript 常见错误 Top 10:

为了便于阅读,我们将每个错误描述都缩短了。接下来,让我们深入到每一个错误,来确定什么会导致它,以及如何避免创建它。

1、Uncaught TypeError: Cannot read property

如果你是一个 JavaScript 开发人员,可能你看到这个错误的次数比你敢承认的要多。当你读取一个未定义的对象的属性或调用其方法时,这个错误会在 Chrome 中出现。 您可以很容易的在 Chrome 开发者控制台中进行测试。

发生这种情况的原因很多,但常见的一种是在渲染 UI 组件时对于状态的初始化操作不当。

我们来看一个在真实应用程序中发生的例子:我们选择 React,但该情况也同样适用于 Angular、Vue 或任何其他框架。

class Quiz extends Component {
  componentWillMount() {
    axios.get('/thedata').then(res => {
      this.setState({items: res.data});
    });
  }
  render() {
    return (
      <ul>
        {this.state.items.map(item =>
          <li key={item.id}>{item.name}</li>
        )}
      </ul>
    );
  }
}

两个重要的流程:

  1. 组件的状态(例如 this.state)开始于 undefined。
  2. 当异步获取数据时,不管它是在构造函数 componentWillMount 还是 componentDidMount 中获取的,组件在数据加载之前至少会呈现一次,当 Quiz 第一次呈现时,this.state.items 是 undefined。

这很容易解决。最简单的方法:在构造函数中初始化 state。

class Quiz extends Component {
  // Added this:
  constructor(props) {
    super(props);
    // Assign state itself, and a default value for items
    this.state = {
      items: []
    };
  }
  componentWillMount() {
    axios.get('/thedata').then(res => {
      this.setState({items: res.data});
    });
  }
  render() {
    return (
      <ul>
        {this.state.items.map(item =>
          <li key={item.id}>{item.name}</li>
        )}
      </ul>
    );
  }
}

在你的应用程序中的具体代码可能是不同的,但我们希望我们已经给你足够的线索,以解决或避免在你的应用程序中出现的这个问题。如果还没有,请继续阅读,因为我们将在下面覆盖更多相关错误的示例。

2、 TypeError: ‘undefined’ is not an object

这是在 Safari 中读取属性或调用未定义对象上的方法时发生的错误。您可以在 Safari Developer Console 中轻松测试。这与第一点中提到的 Chrome 的错误基本相同,但 Safari 使用了不同的错误消息提示语。

3、 TypeError: null is not an object

这是在 Safari 中读取属性或调用空对象上的方法时发生的错误。 您可以在 Safari Developer Console 中轻松测试。

有趣的是,在 JavaScript 中,nullundefined 是并不同,这就是为什么我们看到的是两个不同的错误信息。

undefined 通常是一个尚未分配的变量,而 null 表示该值为空。 要验证它们不相等,请尝试使用严格的相等运算符 ===

在我们工作中,这种错误可能发生的一种场景是:如果在加载元素之前尝试在 JavaScript 中使用元素。 因为 DOM API 对于空白的对象引用返回值为 null。

任何执行和处理 DOM 元素的 JS 代码都应该在创建 DOM 元素之后执行。

JS 代码按照 HTML 中的规定从上到下进行解释。 所以,如果 DOM 元素之前有一个标签,脚本标签内的 JS 代码将在浏览器解析 HTML 页面时执行。 如果在加载脚本之前尚未创建 DOM 元素,则会出现此错误。

在这个例子中,我们可以通过添加一个事件监听器来解决这个问题,这个监听器会在页面准备好的时候通知我们。 一旦 addEventListener 被触发,init() 方法就可以使用 DOM 元素。

<script>
  function init() {
    var myButton = document.getElementById("myButton");
    var myTextfield = document.getElementById("myTextfield");
    myButton.onclick = function() {
      var userName = myTextfield.value;
    }
  }
  document.addEventListener('readystatechange', function() {
    if (document.readyState === "complete") {
      init();
    }
  });
</script>
<form>
  <input type="text" id="myTextfield" placeholder="Type your name" />
  <input type="button" id="myButton" value="Go" />
</form>

4、 (unknown): Script error

当未捕获的 JavaScript 错误(通过 window.onerror 处理程序引发的错误,而不是捕获在 try-catch 中)被浏览器的跨域策略限制时,会产生这类的脚本错误。 例如,如果您将您的 JavaScript 代码托管在 CDN 上,则任何未被捕获的错误将被报告为“脚本错误” 而不是包含有用的堆栈信息。这是一种浏览器安全措施,旨在防止跨域传递数据,否则将不允许进行通信。

要获得真正的错误消息,请执行以下操作:

1. 设置 ‘Access-Control-Allow-Origin’ 头部

将 Access-Control-Allow-Origin 标头设置为 * 表示可以从任何域正确访问资源。

在 Nginx 中设置如下:

将 add_header 指令添加到提供 JavaScript 文件的位置块中:

location ~ ^/assets/ {
    add_header Access-Control-Allow-Origin *;
}

2. 在 <script> 中设置 crossorigin="anonymous"

在您的 HTML 代码中,对于您设置了 Access-Control-Allow-Origin 的每个脚本,在 script 标签上设置 crossorigin =“anonymous”。在脚本标记中添加 crossorigin 属性之前,请确保验证上述 header 正确发送。

在 Firefox 中,如果存在crossorigin属性,但Access-Control-Allow-Origin头不存在,则脚本将不会执行。

5、 TypeError: Object doesn’t support property

这是您在调用未定义的方法时发生在 IE 中的错误。 您可以在 IE 开发者控制台中进行测试。

这相当于 Chrome 中的 “TypeError:”undefined“ is not a function” 错误。

是的,对于相同的逻辑错误,不同的浏览器可能具有不同的错误消息。

对于使用 JavaScript 命名空间的 Web 应用程序,这是一个 IE 浏览器的常见的问题。 在这种情况下,99.9% 的原因是 IE 无法将当前名称空间内的方法绑定到 this 关键字。

例如:如果你 JS 中有一个命名空间 Rollbar 以及方法 isAwesome。 通常,如果您在 Rollbar 命名空间内,则可以使用以下语法调用 isAwesome 方法:

this.isAwesome();

Chrome,Firefox 和 Opera 会欣然接受这个语法。 但是 IE 却不会。 因此,使用 JS 命名空间时最安全的选择是始终以实际名称空间作为前缀。

Rollbar.isAwesome();

6、 TypeError: ‘undefined’ is not a function

当您调用未定义的函数时,这是 Chrome 中产生的错误。 您可以在 Chrome 开发人员控制台和 Mozilla Firefox 开发人员控制台中进行测试。

function clearBoard(){
  alert("Cleared");
}

document.addEventListener("click", function(){
  this.clearBoard(); // what is “this” ?
});

执行上面的代码会导致以下错误:

“Uncaught TypeError:this.clearBoard is not a function”。

原因应该是清楚的,即执行上下文不理解导致的指向错误。

7、 Uncaught RangeError

当你调用一个不终止的递归函数就会发生这种错误。您可以在 Chrome 开发者控制台中进行测试。

此外,如果您将值传递给超出范围的函数,也可能会发生这种情况。

许多函数只接受其输入值的特定范围的数字。 例如:

  1. toExponential(digits)toFixed(digits) 接受 0 到 100
  2. toPrecision(digits) 接受 1 到 100
var num = 2.555555;
console.log(num.toExponential(4));  //OK
console.log(num.toExponential(-2)); //range error!

console.log(num.toFixed(2));   //OK
console.log(num.toFixed(105));  //range error!

console.log(num.toPrecision(1));   //OK
console.log(num.toPrecision(0));  //range error!

8、 TypeError: Cannot read property ‘length’

这是 Chrome 中发生的错误,因为读取未定义变量的长度属性。 您可以在 Chrome 开发者控制台中进行测试。

您通常会在数组中找到定义的长度,但是如果数组未初始化或者变量在另一个上下文中,则可能会遇到此错误。让我们用下面的例子来理解这个错误。

var testArray = ["Test"];
function testFunction(testArray) {
    for (var i = 0; i < testArray.length; i++) {
        console.log(testArray[i]);
    }
}
testFunction();

执行以上代码会报错:

Cannot read property 'length' of undefined

有两种方法可以解决这个问题:

var testArray = ["Test"];
/* Precondition: defined testArray outside of a function */
function testFunction(/* No params */) {
    for (var i = 0; i < testArray.length; i++) {
      console.log(testArray[i]);
    }
}
testFunction();

var testArray = ["Test"];
function testFunction(testArray) {
   for (var i = 0; i < testArray.length; i++) {
      console.log(testArray[i]);
    }
}
testFunction(testArray);

9、 Uncaught TypeError: Cannot set property

当我们尝试访问一个未定义的变量时,它总是返回 undefined,我们不能获取或设置任何未定义的属性。 在这种情况下会将抛出 “Uncaught TypeError: Cannot set property”。

10. ReferenceError: event is not defined

当您尝试访问未定义的变量或超出当前作用域的变量时,会引发此错误。 您可以在 Chrome 浏览器中测试。

如果在使用 event 时遇到此错误,请确保使用传入的事件对象作为参数。像 IE 这样的旧浏览器提供了一个全局变量事件,但并不是所有浏览器都支持。

document.addEventListener("mousemove", function (event) {
  console.log(event);
})

总结

我们看到上面的 10 个最常见的错误,其实所涉及的知识点并不难。当你认真读过《你不知道的 JavaScript》上卷后,这些错误基本就不会再出现了。

归根结底是对 JavaScript 基础知识掌握的不扎实。

最后

如果你想进【大前端交流群】,关注公众号点击“交流加群”添加机器人自动拉你入群。关注我第一时间接收最新干货。

【前端词典】分享 8 个有趣且实用的 API

前言

在日常开发中总是和各种 API 打交道,我们名为前端工程师实为 API 调用工程师。这篇文章我就分享 8 个有趣的 API,你若通过阅读这篇文章对前端增加一点点的乐趣,对我来说也是一种鼓励。

这几个 API 使用得当的话,可以提高你应用的友好性。

这些 API 的示例代码我已放在 github 上,感兴趣的可以 clone 代码到本地运行。望给个 star 支持一下。

API 也可以如此有趣

先看效果图:

1. 监听屏幕旋转变化接口: orientationchange

定义:

这个 API 可以将你手机是否横屏的情况暴露给需要知道的人知道。

使用:

screenOrientation: function(){
    let self = this;
    let orientation = screen.orientation || screen.mozOrientation || screen.msOrientation;
    window.addEventListener("onorientationchange" in window ? "orientationchange" : "resize", function() {
        self.angle = orientation.angle;
    });
},
orientation.angle 值 屏幕方向
0 竖屏
90 向左横屏
-90/270 向右横屏
180 倒屏

通过这个 API 我们在横屏和竖屏的时候可以添加一些动作或者是样式的改变。

如果只是样式的改变也可以通过媒体查询来监听:

/* 竖屏 */
@media screen and (orientation: portrait) {
    // some css code
}
/* 横屏 */
@media screen and (orientation: landscape) {
    // some css code
}

2. 电池状态:navigator.getBattery()

定义:

这个 API 可以将你手机电池状态的情况暴露给需要知道的人知道。

这个 api 返回的是一个 promise 对象,会给出一个 BatteryManager 对象,对象中包含了以下信息:

key 描述 备注
charging 是否在充电 可读属性
chargingTime 若在充电,还需充电时间 可读属性
dischargingTime 剩余电量 可读属性
level 剩余电量百分数 可读属性
onchargingchange 监听充电状态的改变 可监听事件
onchargingtimechange 监听充电时间的改变 可监听事件
ondischargingtimechange 监听电池可用时间的改变 可监听事件
onlevelchange 监听剩余电量百分数的改变 可监听事件

使用:

getBatteryInfo: function(){
    let self = this;
    if(navigator.getBattery){
        navigator.getBattery().then(function(battery) {
            // 判断是否在充电
            self.batteryInfo = battery.charging ? `在充电 : 剩余 ${battery.level * 100}%` : `没充电 : 剩余 ${battery.level * 100}%`;
            // 电池充电状态改变事件
            battery.addEventListener('chargingchange', function(){
                self.batteryInfo = battery.charging ? `在充电 : 剩余 ${battery.level * 100}%` : `没充电 : 剩余 ${battery.level * 100}%`;
            });
        });
    }else{
        self.batteryInfo = '不支持电池状态接口';
    }
},

3. 让你的手机震动: window.navigator.vibrate(200)

定义:

这个 API 可以让你的手机按你的想法震动。

使用:
震动效果会在很多游戏使用。比如欢乐斗地主中,地主打完王炸后手机都会有震动的效果,以此来表达地主嘚瑟的心情也很是合理。

示例代码如下:

vibrateFun: function(){
    let self = this;
    if( navigator.vibrate ){
        navigator.vibrate([500, 500, 500, 500, 500, 500, 500, 500, 500, 500]);
    }else{
        self.vibrateInfo = "您的设备不支持震动";
    }
    <!--
    // 清除震动 
    navigator.vibrate(0);
    // 持续震动
    setInterval(function() {
        navigator.vibrate(200);
    }, 500);
    -->
},

4. 当前语言:navigator.language

定义:

这个 API 可以告诉你当前应该使用什么语言。

如果你需要和我一样做多语言适配,这个 API 或许可以给你提供一个不错的思路。

使用:


不同浏览器返回的值稍微有点差异。你可以通过以下封装好的方法来消除这种差异:

function getThisLang(){
    const langList = ['cn','hk','tw','en','fr'];
    const langListLen = langList.length;
    const thisLang = (navigator.language || navigator.browserLanguage).toLowerCase();
    for( let i = 0; i < langListLen; i++ ){
        let lang = langList[i];
        if(thisLang.includes(lang)){
            return lang
        }else {
          return 'en'
        }
    }
}

不同的语言就对应不同的语言文案就好。

5. 联网状态:navigator.onLine

定义:

这个 API 可以告诉让你知道你的设备的网络状态是否连接着。

使用:

比如我上午登陆了掘金在看一篇文章,可是没看完就到了吃饭的点,这么热爱学习的我肯定是选择吃完午饭回来继续看。

30 分钟过后……

吃晚饭回到公司,打开电脑继续把那篇文章看完,看完了打算点了赞,发现给了这个提示:

这个提示让我有点懵(就是没网络了)。

这个时候我们就可以使用这个 API,这样就可以准确的告诉用户“您的网络无法连接,请检查”。这样用户是不是可以有更好的体验呢?

代码如下:

mounted(){
    let self = this;
    window.addEventListener('online',  self.updateOnlineStatus, true);
    window.addEventListener('offline', self.updateOnlineStatus, true);
},
methods: {
    updateOnlineStatus: function(){
        var self = this;
        self.onLineInfo = navigator.onLine ? "online" : "offline";
    },
}

注意:navigator.onLine 只会在机器未连接到局域网或路由器时返回 false,其他情况下均返回 true
也就是说,机器连接上路由器后,即使这个路由器没联通网络,navigator.onLine 仍然返回 true

6. 页面可编辑:contentEditable

定义:

这个 API 可以使页面所有元素成为可编辑状态,使浏览器变成你的编辑器。

使用:

  1. 你可以在地址栏输入 data:text/html, <html contenteditable>, 这样浏览器就变成了编辑器。

使用场景:

需求 —— 页面需要一个文本输入框。

  1. 该文本输入框默认状态下有默认文本提示信息
  2. 文本框输入状态下其高度会随文本内容自动撑开

像这样的需求我们就可以使用 <div contentEditable='true'></div> 代替 <textarea> 标签。

不过 contentEditable='true' 是不会有 placeholder 的,那 placeholder 怎么办呢?

我一般会使用如下方案,输入内容后改变 class:

<div class='haveInput' contentEditable='true' placeholder='请输入'></div> 
// css 样式
.haveInput:before {
    content: attr(placeholder);
    display: block;
    color: #333;
}

7. 浏览器活跃窗口监听: window.onblur & window.onfocus

定义:

这两个 api 分别表示窗口失去焦点和窗口处于活跃状态。

浏览其他窗口、浏览器最小化、点击其他程序等, window.onblur 事件就会触发;
回到该窗口, window.onfocus 事件就会触发。

使用:

上面的截图是微信网页版的消息提示。

代码很简单:

mounted(){
    let self = this;
    window.addEventListener('blur',  self.doFlashTitle, true);
    window.addEventListener('focus', function () {
        clearInterval(self.timer);
        document.title = '微信网页版';
    }, true);
},
methods: {
    doFlashTitle: function(){
        var self = this;
        self.timer = setInterval( () => {
            if (!self.flashFlag) {
                document.title = "微信网页版";
            } else {
                document.title = `微信(${self.infoNum})`;
            }
            self.flashFlag = ! self.flashFlag
        }, 500)
    },
}

8. 全屏 API(Fullscreen API)

定义:

这个 API 可以使你所打开的页面全屏展示,没有其他页面外的内容展示在屏幕上。

使用:

Element.requestFullscreen() 方法用于发出异步请求使元素进入全屏模式。

调用此 API 并不能保证元素一定能够进入全屏模式。如果元素被允许进入全屏幕模式,document 对象会收到一个 fullscreenchange 事件,通知调用者当前元素已经进入全屏模式。如果全屏请求不被许可,则会收到一个 fullscreenerror 事件。

当进入/退出全屏模式时,会触发 fullscreenchange 事件。你可以在监听这个事件,做你想做的事。

fullScreenFun: function(){
    let self = this;
    var fullscreenEnabled = document.fullscreenEnabled       ||
                            document.mozFullScreenEnabled    ||
                            document.webkitFullscreenEnabled ||
                            document.msFullscreenEnabled;

    if (fullscreenEnabled) {
        let de = document.documentElement;
        if(self.fullScreenInfo === '打开全屏'){
            if( de.requestFullscreen ){
                de.requestFullscreen();
            }else if( de.mozRequestFullScreen ){
                de.mozRequestFullScreen();
            }else if( de.webkitRequestFullScreen ){
                de.webkitRequestFullScreen();
            }
            self.fullScreenInfo = '退出全屏'
        } else {
            if( document.exitFullscreen ){
                document.exitFullscreen();
            }else if( document.mozCancelFullScreen ){
                document.mozCancelFullScreen();
            }else if( document.webkitCancelFullScreen ){
                document.webkitCancelFullScreen();
            }
            self.fullScreenInfo = '打开全屏'
        }
    } else {
        self.fullScreenInfo = '浏览器当前不能全屏';
    }
}

相关:

  1. document.fullscreenElement: 当前处于全屏状态的元素 element
  2. document.fullscreenEnabled: 标记 fullscreen 当前是否可用
  3. document.exitFullscreen(): 退出全屏

后记

这篇文章很简单。我的目的是想介绍一下有趣的 API,来提高大家对前端的一点兴趣。

我认为这些 API 比较有趣,若你一直疲于业务,那么这些 API 或许可以给你不一样的感受。

最后

如果你想进【大前端交流群】,关注公众号点击“交流加群”添加机器人自动拉你入群。关注我第一时间接收最新干货。

【前端词典】滚动穿透问题的解决方案

背景

产品有三宝,弹窗,浮层加引导;
设计有三宝,透明,阴影加圆角;
运营有三宝,短信,推送加红包;
程序员有一宝,这个做不了。

随着移动端市场的份额越大,需求就越多样化。我们今天讨论的是移动端的滚动穿透问题。上面这段调侃的话可以看出需求中弹窗浮层还是挺常见的,那这个和滚动穿透有什么联系呢?

我先解释下什么是滚动穿透

页面滑出了一个弹窗,我们用手指触摸屏幕滑动时,会发现弹窗下面的内容还是在滚动。这个现象就是滚动穿透

接下就说下我对滚动穿透问题解决方案探索的过程,希望对大家有点启发。

需求

需求: 希望在点击图片的时候,从下方弹一个全屏的弹框来描述这张图片的详情。

方案

接到这个需求觉得没有难度,很快就提测了,然后就开始逛逛掘金。可刚看大佬们的文章看的开心的时候,测试就在微信我。心想来 bug 了?
这是一张聊天截图
突然意识到写弹窗的时候忘记处理滚动穿透的问题了。记得第一次遇到这个问题的时候也是找了很久的资料。

方案一:

找到的第一个方法就是当弹窗触发的时候,给 overflow: scroll: 的元素加上一个 class (一般都是 body 元素)。退出的时候去掉这个 class。下面为了方便,会直接用 body 元素来代指弹窗下方的元素。

// css 部分
modal_open {
    position: fixed;
    height: 100%;
}

// js 部分
document.body.classList.add('modal_open');
document.body.classList.remove('modal_open');

上面的这个方法可以解决滚动穿透问题,却也会带来新的问题。
即:

body 的滚动位置会丢失,也就是bodyscrollTop 属性值会变为 0。

这个新问题比起滚动穿透本身来说更加麻烦,所以这个方案是要进行优化的。

方案二:

既然添加 modal_open 这个 class 会使 body 的滚动位置会丢失,那么我们为什么不在滚动位置丢失之前先保存下来,等到退出弹窗的前在將这个保存下来的滚动位置在设置回去。然后就朝着这个方向开始 coding 。

// css 部分
.modal_open {
  position: fixed;
  height: 100%;
}

// js 部分
/**
 * ModalHelper helpers resolve the modal scrolling issue on mobile devices
 * https://github.com/twbs/bootstrap/issues/15852
 */
var ModalHelper = (function(bodyClass) {
    var scrollTop;
    return {
        afterOpen: function() {
            scrollTop = document.scrollingElement.scrollTop  ||
                        document.documentElement.scrollTop || 
                        document.body.scrollTop;
            document.body.classList.add(bodyClass);
            document.body.style.top = -scrollTop + 'px';
        },
        beforeClose: function() {
            document.body.classList.remove(bodyClass);
            document.scrollingElement.scrollTop = document.documentElement.scrollTop = document.body.scrollTop = scrollTop;
        }
    };
})('modal_open');

// method
modalSwitch: function(){
    let self = this;
    if( self.switchFlag === 'close' ){
        ModalHelper.afterOpen();
        self.switchFlag = 'open';
    }else{
        ModalHelper.beforeClose();
        self.switchFlag = 'close';
    }
}

方案二可以达到以下效果:

  1. 弹窗滚动的时候,下方的 body 是固定的无法滚动;
  2. body 的滚动位置不会丢失;
  3. body 有 scroll 事件;

方案二可以适应绝大多数的弹窗需求,提测后测试方也没有在提其他问题,这个问题算是完美的解决了。不过我在这个过程有一个疑问:

IOS 自有的橡皮筋效果会导致页面会出现短暂卡顿现象,暂时没有找到原因,请教各位。

其他方案:

使用 preventDefault 阻止浏览器默认事件:

var modal = document.getElementById('modalBox');
modal.addEventListener('touchmove', function(e) {
    e.preventDefault();
}, false);

这个方案只适用于这个弹窗本身的高度小于屏幕的高度,即不可滚动的时候。touchmovetouchstart 更加合适。因为 touchstart 会连点击事件都阻止。

使用插件:

对于插件我的态度是,除非是自己实现起来太复杂,否则还是自己花点时间去实现。原因有二:

  1. 使用插件就意味着需要引入的文件至少多了一个。
  2. 插件过多,担心日后项目升级维护成本加大。

以上。

参考

  1. https://developer.mozilla.org/en-US/docs/Web/API/document/scrollingElement
  2. https://uedsky.com/2016-06/mobile-modal-scroll/

最后

如果你想进【大前端交流群】,关注公众号点击“交流加群”添加机器人自动拉你入群。关注我第一时间接收最新干货。

【前端词典】关于 Babel 你必须知道的

前言

我第一次打开搜索引擎查询关于 Babel 的资料时,出现的竟然是关于 Babel 的传说。后来我花了小一天的时间去了解这个传说(来自《旧约圣经》)。

Babel Tower 是全人类联手建造的一个建筑,人们决心合力修建一座通天高塔。 因为人们心里少了对上帝的敬畏,多了为自己歌功颂德的功利。上帝不希望这个奇观建成,于是让人们分化成不同的语言,令其不能交流。之后,因为沟通不畅,工程被迫放弃,而且人类从此不再团结,因为语言不通而分化成不同部落,并由于沟通问题,经常发生战乱,因此再无力撼动上帝的权威。

了解完这个神话之后,我后面就好好的去了解了一些有关 Babel 的知识。

什么是 Babel

Babel 官方文档: https://babeljs.io/

我们知道各个浏览器对 JavaScript 版本的支持各不相同,有很多优秀的新语法都不能直接在浏览器中运行。为了解决这个“沟通不畅”的问题,所以就有了 Babel,Babel 的出现使得我们可以无须顾忌的去使用 ES6+ 的语法。

Babel is a JavaScript compiler.

这也是为何我们必须使用 ES6+ 语法的前提条件。

如果你现在还不清楚 ES6+ 语法的话,赶快学习去吧,要不然你就只能回家继承几十亿的家产啦。

Babel 如何编译

先看下面这张图:

你会发现 ES6 的语法确实被编译成浏览器可以识别的版本了,你是不是也在问这事怎么做到的呢?

babel 编译的阶段

babel 总共分为三个阶段:解析,转换,生成。

我们需要知道现在 babel 本身是不具备这种转化功能,提供这些转化功能的是一个个 plugin。所以我们没有配置任何 plugin 的时候,经过 Babel 输出的代码是没有改变的。

Plugin —— transform 的载体

Babel 自 6.0 起,就不再对代码进行转换。现在只负责图中的 parse 和 generate 流程,转换代码的 transform 过程全都交给插件去做。

例子:

// 模板字面量
const name = '小生方勤';
let hello = `hello ${name}`;

上面是一个简单的模板字面量的例子,我们清楚这个是 ES6 的新特性,在不支持 ES6 的运行平台这段代码是会报错的,所以我们需要 Babel 来将其编译成 ES5 的代码。

所以我们需要如下来配置 babel:

// .babelrc 文件
{ 
  "plugins": [
    "transform-es2015-template-literals"  // 转译模版字符串的 plugins
  ],
  "presets": ["env", "stage-2"]
}

preset(即一组预先设定的插件)

preset: babel 插件集合的预设,包含某些插件 plugin。显然像上面那样一个一个配置插件会非常的麻烦,为了方便,babel 为我们提供了一个配置项叫做 persets(预设)。

当前 babel 推荐使用 babel-preset-env 替代 babel-preset-es201X ,env 的支持范围更广,包含es201X 的所有语法编译,并且它可以根据项目运行平台的支持情况自行选择编译版本。

plugins 与 presets 同时存在的执行顺序

  1. 先执行 plugins 的配置项,再执行 Preset 的配置项;
  2. plugins 配置项,按照声明顺序执行;
  3. Preset 配置项,按照声明逆序执行。

列入以下代码的执行顺序为:

  1. transform-es2015-template-literals
  2. stage-2
  3. env
// .babelrc 文件
{ 
    "plugins": [
        "transform-es2015-template-literals",  // 转译模版字符串的 plugins
    ],
    "presets": [
        ["env", {
            // 是否自动引入 polyfill,开启此选项必须保证已经安装了 babel-polyfill
            // “usage” | “entry” | false, defaults to false.
            "useBuiltIns": "usage"
        }], "stage-2"]
}

这里讲一讲 useBuiltIns 配置

我们可能在全局引入 babel-polyfill,这样打包后的整个文件体积必然是会变大的。

但是通过设置 "useBuiltIns": "usage" 能够把 babel-polyfill 中你需要用到的部分提取出来,不需要的去除。

useBuiltIns 参数说明:

  1. false: 不对 polyfills 做任何操作
  2. entry: 根据 target 中浏览器版本的支持,将 polyfills 拆分引入,仅引入有浏览器不支持的 polyfill
  3. usage(新):检测代码中 ES6/7/8 等的使用情况,仅仅加载代码中用到的 polyfills

Babel 相关模块简要说明

了解过 Babel 的同学,是否也觉得的模块有点多呢?我开始学习的时候就有这种感觉。其实每个模块是各司其职的。

babel-core(核心)

这个模块是最能顾名思义的了,即 babel 的核心模块。babel 的核心 api 都在这个模块中。也就是这个模块会把我们写的 js 代码抽象成 AST 树;然后再将 plugins 转译好的内容解析为 js 代码。

具体怎么工作的这里就不详细说了,因为我也不知道。

babel-cli

babel-cli 官方文档:https://babeljs.io/docs/en/babel-cli/

babel-cli 是一个通过命令行对 js 文件进行转换的工具。

当然我们一般不会使用到这个模块,因为一般我们都不会手动去做这个工作,这个工作基本都集成到模块化管理工具中去了,比如 webpack、Rollup 等。

简单使用(需要先安装 babel-cli):

babel test.js -o compiled.js

babel-node

babel-node 是 babel-cli 的一部分,所以它在安装 babel-cli 的时候也同时安装了。

它使 ES6+ 可以直接运行在 node 环境中。

babel-polyfill(内部集成了 core-js 和 regenerator)

babel 对一些新的 API 是无法转换,比如 Generator、Set、Proxy、Promise 等全局对象,以及新增的一些方法:includes、Array.form 等。所以这个时候就需要一些工具来为浏览器做这个兼容。

官网的定义:babel-polyfill 是为了模拟一个完整的 ES6+ 环境,旨在用于应用程序而不是库/工具。

babel-polyfill 主要有两个缺点:

  1. 使用 babel-polyfill 会导致打出来的包非常大,很多其实没有用到,对资源来说是一种浪费。
  2. babel-polyfill 可能会污染全局变量,给很多类的原型链上都作了修改,这就有不可控的因素存在。

因为上面两个问题,所以在 Babel7 中增加了 babel-preset-env,我们设置 "useBuiltIns": "usage" 这个参数值就可以实现按需加载 babel-polyfill 啦。

babel-runtime & babel-plugin-transform-runtime

在使用 Babel6 的时候, .babelrc 文件中会使用 babel-plugin-transform-runtime,而 package.json 中的 dependencies 同时包含了 babel-runtime,因为在使用 babel-plugin-transform-runtime 的时候必须把 babel-runtime 当做依赖。

.babelrc 配置:

{
  "presets": [
    ["env"]
  ],
  "plugins": [
    ["transform-runtime", {
      "helpers": false, // defaults to true
      "polyfill": false, // defaults to true
      "regenerator": true, // defaults to true
      "moduleName": "babel-runtime" // defaults to "babel-runtime"
    }]
  ]
}

我们在启用插件 babel-plugin-transform-runtime 后,Babel 就会使用 babel-runtime 下的工具函数,将一些浏览器不能支持的特性重写,然后在项目中使用。

babel-runtime 内部也集成了 core-js、 regenerator、helpers 等

由于采用了沙盒机制,这种做法不会污染全局变量,也不会去修改内建类的原型,所以会有重复引用的问题。

现在最好的实践应该是在 babel-preset-env 设置 "useBuiltIns": "usage",按需引入 polyfill。

三种方案对比

方案 优点 缺点
@babel/runtime & @babel/plugin-transform-runtime 按需引入, 打包体积小 不能兼容实例方法
@babel/polyfill 完整模拟 ES2015+ 环境 打包体积过大, 污染全局对象和内置的对象原型
@babel/preset-env 按需引入, 可配置性高 小生不知 -_-

babel7 的一些变化

preset 的变更:

淘汰 es201x,删除 stage-x,推荐 env

如果你还在使用 es201x,官方建议使用 env 进行替换。淘汰并不是删除,只是不推荐使用

但 stage-x 是直接被删了,也就是说在 babel7 中使用 es201X 是会报错的。

包名称变化

babel 7 的一个重大变化,把所有 babel-* 重命名为 @babel/*,

例如:

  1. babel-cli —> @babel/cli。
  2. babel-preset-env —> @babel/preset-env

低版本 node 不再支持

babel 7.0 开始不再支持 nodejs 0.10, 0.12, 4, 5 这四个版本,相当于要求 nodejs >= 6。

还有一些包从其他包独立出来的变化等等

关于如何配置 Babel

接下来我会专门写一篇关于开发环境配置的问题,也就是自己完成脚手架的功能,所以这里就不提如何配置 Babel 啦。

最后

如果你想进【大前端交流群】,关注公众号点击“交流加群”添加机器人自动拉你入群。关注我第一时间接收最新干货。

【前端词典】学习 Vue 源码的必要知识储备

前言

我最近在写 Vue 进阶的内容。在这个过程中,有些人问我看 Vue 源码需要有哪些准备吗?所以也就有了这篇计划之外的文章。

当你想学习 Vue 源码的时候,需要有扎实的 JavaScript 基础,下面罗列的只是其中的一部分比较具有代表性的知识点。如果你还不具备 JavaScript 基础的话,建议不要急着看 Vue 源码,这样你会很容易放弃的。

我会从以下 7 点来展开:

  1. Flow 基本语法
  2. 发布/订阅模式
  3. Object.defineProperty
  4. ES6+ 语法
  5. 原型链、闭包
  6. 函数柯里化
  7. event loop

必要知识储备

需要注意的是这篇文章每个点不会讲的特别详细,我这里就是把一些知识点归纳一下。每个详细的点仍需自己花时间学习。

Flow 基本语法

相信看过 Vue、Vuex 等源码的人都知道它们使用了 Flow 静态类型检查工具。

我们知道 JavaScript 是弱类型的语言,所以我们在写代码的时候容易出现一些始料未及的问题。也正是因为这个问题,才出现了 Flow 这个静态类型检查工具。

这个工具可以改变 JavaScript 是弱类型的语言的情况,可以加入类型的限制,提高代码质量。

// 未使用 Flow 限制
function sum(a, b) {
  return a + b;
}

// 使用 Flow 限制  a b 都是 number 类型。
function sum(a: number, b:number) {
  return a + b;
}

基础检测类型

Flow 支持原始数据类型,有如下几种:

boolean
number
string
null
void( 对应 undefined )

在定义变量的同时在关键的地方声明类型,使用如下:

let str:string = 'str';
// 重新赋值
str = 3  // 报错

复杂类型检测

Flow 支持复杂类型检测,有如下几种:

Object
Array
Function
自定义的 Class

需要注意直接使用 flow.js,JavaScript 是无法在浏览器端运行的,必须借助 babel 插件,vue 源码中使用的是 babel-preset-flow-vue 这个插件,并且在 babelrc 进行配置。

详细的 Flow 语法可以看以下资料:

这里推荐两个资料

  1. 官方文档:https://flow.org/en/
  2. Flow 的使用入门:https://zhuanlan.zhihu.com/p/26204569

发布/订阅模式

我们知道 Vue 是内部是实现了双向绑定机制,使得我们不用再像从前那样还要自己操作 DOM 了。

其实 Vue 的双向绑定机制采用数据劫持结合发布/订阅模式实现的: 通过 Object.defineProperty() 来劫持各个属性的 setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。

我发现有的人把观察者模式和发布/订阅模式混淆一谈,其实订阅模式有一个调度中心,对订阅事件进行统一管理。而观察者模式可以随意注册事件,调用事件。

我画了一个大概的流程图,用来说明观察者模式和发布/订阅模式。如下:

这块我会在接下的文章中详细讲到,这里先给出一个概念,感兴趣的可以自己查找资料,也可等我的文章出炉。

其实我们对这种模式再熟悉不过了,但可能你自己也没发现:

let div = document.getElementById('#div');
div.addEventListener('click', () => {
    console.log("div 被点击了一下")
})

可以思考下上面的事件绑定执行的一个过程,你应该会有共鸣。

数据双向绑定基础:Object.defineProperty()

一、数据属性

数据属性包含一个数据值的位置。这个位置可以读取和写入值。数据属性有 4 个描述他行为的特性:

属性 描述
Configurable 能否用 delete 删除属性从而重新定义属性。默认为 true
Enumerable 能否通过 for-in 遍历,即是否可枚举。默认为 true
Writable 是否能修改属性的值。默认为 true
Value 包含这个属性的数据值,读写属性的时候其实就在这里读写。默认为 undefined

如果你想要修改上述 4 个默认的数据属性,就需要使用 ECMAScript 的 Object.defineProperty() 方法。

该方法包含3个参数:属性所在的对象,属性名,描述符对象。描述符对象的属性必须在上述 4 个属性中。

var person = {
  name: '',
};
// 不能修改属性的值
Object.defineProperty(person, "name",{
    writable: false,
    value: "小生方勤"
});
console.log(person.name);   // "小生方勤"
person.name = "方勤";
console.log(person.name);  // "小生方勤"

二、访问器属性

访问器属性不包含数据值,他们包含一对 gettersetter 函数(非必须)。在读写访问器属性的值的时候,会调用相应的 gettersetter 函数,而我们的 vue 就是在 gettersetter 函数中增加了我们需要的操作。

需要注意的是【value 或 writable】一定不能和【get 或 set】共存。

访问器属性有以下 4 个特性:

特性 描述
Configurable 能否用 delete 删除属性从而重新定义属性。默认为 true
Enumerable 能否通过 for-in 遍历,即是否可枚举。默认为 true
get 读取属性时调用的函数,默认 undefined
set 写入属性时调用的函数,默认 undefined

接下来给个例子:

var person = {
    _name : "小生方勤"
};
Object.defineProperty(person, "name", {
    //注意 person 多定义了一个 name 属性
    set: function(value){
        this._name = "来自 setter : " + value;  
    },
    get: function(){
        return "来自 getter : " + this._name; 
    }
});

console.log( person.name );   // 来自 getter : 小生方勤

person.name = "XSFQ";        
console.log( person._name );  // 来自 setter : XSFQ
console.log( person.name );   // 来自 getter : 来自 setter : XSFQ

如果之前都不清楚有 Object.defineProperty() 方法,建议你看《JavaScript 高级程序设计》的 139 - 144 页。

额外讲讲 Object.create(null)

我们在源码随处可以 this.set = Object.create(null) 这样的赋值。为什么这样做呢?这样写的好处就是不需要考虑原型链上的属性,可以真正的创建一个纯净的对象。

首先 Object.create 可以理解为继承一个对象,它是 ES5 的一个特性,对于旧版浏览器需要做兼容,基本代码如下:

if (!Object.create) {
    Object.create = function (o) {
        function F() {}     // 定义了一个隐式的构造函数
        F.prototype = o;
        return new F();     // 其实还是通过new来实现的
    };
}

ES6+ 语法

其实这点应该是默认你需要知道的,不过鉴于之前有人问过我一些相关的问题,我稍微讲一下。

export defaultexport 的区别

  1. 在一个文件或模块中 export 可以有多个,但 export default 仅有一个
  2. 通过 export 方式导出,在导入时要加 { },而 export default 则不需要
1.export
//a.js
export const str = "小生方勤";
//b.js
import { str } from 'a';   // 导入的时候需要花括号

2.export default
//a.js
const str = "小生方勤";
export default str;
//b.js
import str from 'a';      // 导入的时候无需花括号

export default const a = 1; 这样写是会报错的哟。

箭头函数

这个一笔带过:

  1. 箭头函数中的 this 指向是固定不变的,即是在定义函数时的指向
  2. 而普通函数中的 this 指向时变化的,即是在使用函数时的指向

class 继承

Class 可以通过 extends 关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。

class staff { 
  constructor(){
    this.company = "ABC";	
    this.test = [1,2,3];
  }
  companyName(){
    return this.company; 
  }
}
class employee extends staff {
  constructor(name,profession){
    super();
    this.employeeName = name;
    this.profession = profession;
  }
}

// 将父类原型指向子类
let instanceOne = new employee("Andy", "A");
let instanceTwo = new employee("Rose", "B");
instanceOne.test.push(4);
// 测试 
console.log(instanceTwo.test);    // [1,2,3]
console.log(instanceOne.companyName()); // ABC
// 通过 Object.getPrototypeOf() 方法可以用来从子类上获取父类
console.log(Object.getPrototypeOf(employee) === staff)
// 通过 hasOwnProperty() 方法来确定自身属性与其原型属性
console.log(instanceOne.hasOwnProperty('test'))          // true
// 通过 isPrototypeOf() 方法来确定原型和实例的关系
console.log(staff.prototype.isPrototypeOf(instanceOne));    // true

super 关键字,它在这里表示父类的构造函数,用来新建父类的 this 对象。

  1. 子类必须在 constructor 方法中调用 super 方法,否则新建实例时会报错。这是因为子类没有自己的 this 对象,而是继承父类的 this 对象,然后对其进行加工。
  2. 只有调用 super 之后,才可以使用 this 关键字,否则会报错。这是因为子类实例的构建,是基于对父类实例加工,只有 super 方法才能返回父类实例。
`super` 虽然代表了父类 `A` 的构造函数,但是返回的是子类 `B` 的实例,即` super` 内部的 `this ` 指的是 `B`,因此 `super()` 在这里相当于 A.prototype.constructor.call(this)

ES5 和 ES6 实现继承的区别

ES5 的继承,实质是先创造子类的实例对象 this,然后再将父类的方法添加到 this 上面(Parent.apply(this))。
ES6 的继承机制完全不同,实质是先创造父类的实例对象 this (所以必须先调用 super() 方法),然后再用子类的构造函数修改 this

proxy

对最新动态了解的人就会知道,在下一个版本的 Vue 中,会使用 proxy 代替 Object.defineProperty 完成数据劫持的工作。

尤大说,这个新的方案会使初始化速度加倍,于此同时内存占用减半。

proxy 对象的用法:

var proxy = new Proxy(target, handler);

new Proxy() 即生成一个 Proxy 实例。target 参数表示所要拦截的目标对象,handler 参数也是一个对象,用来定制拦截行为。

var proxy = new Proxy({}, {
    get: function(obj, prop) {
        console.log('get 操作')
        return obj[prop];
    },
    set: function(obj, prop, value) {
        console.log('set 操作')
        obj[prop] = value;
    }
});

proxy.num = 2; // 设置 set 操作

console.log(proxy.num); // 设置 get 操作 // 2

除了 get 和 set 之外,proxy 可以拦截多达 13 种操作。

注意,proxy 的最大问题在于浏览器支持度不够,IE 完全不兼容。

倘若你基本不了解 ES6, 推荐下面这个教程:

阮一峰 ECMAScript 6 入门:http://es6.ruanyifeng.com/

原型链、闭包

原型链

因为之前我特意写了一篇文章来解释原型链,所以这里就不在讲述了:

原型链:https://juejin.im/post/5c335940f265da610e804097

闭包

这里我先放一段 Vue 源码中的 once 函数。这就是闭包调用 —— 函数作为返回值:

/**
 * Ensure a function is called only once.
 */
export function once (fn: Function): Function {
  let called = false
  return function () {
    if (!called) {
      called = true
      fn.apply(this, arguments)
    }
  }
}

这个函数的作用就是确保函数只调用一次。

为什么只会调用一次呢? 因为函数调用完成之后,其执行上下文环境不会被销毁,所以 called 的值依然在那里。

闭包到底是什么呢。《JavaScript 高级程序设计》的解释是:

闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式,就是在一个函数内部创建另一个函数。

简单讲,闭包就是指有权访问另一个函数作用域中的变量的函数。

给两段代码,如果你知道他们的运行结果,那么说明你是了解闭包的:

// 第一段
var num = 20;
function fun(){
    var num = 10;
    return function con(){
        console.log( this.num )
    }
}
var funOne = fun();

funOne();  // 20


// 第二段
var num = 20;
function fun(){
    var num = 10;
    return function con(){
        console.log( num )
    }
}
var funOne = fun();

funOne(); // 10

函数柯里化

所谓"柯里化",就是把一个多参数的函数,转化为单参数函数。

先说说我之前遇到过得一个面试题:

如何使 add(2)(3)(4)(5)() 输出 14

在那次面试的时候,我还是不知道柯里化这个概念的,所以当时我没答上。后来我才知道这可以用函数柯里化来解,即:

function add(num){
    var sum=0;
    sum= sum+num;
    return function tempFun(numB){
        if(arguments.length===0){
            return sum;
        }else{
            sum= sum+ numB;
            return tempFun;
        }
    }
}

那这和 Vue 有什么关系呢?当然是有关系的:

我们是否经常这样写判断呢?

if( A ){
 // code
}else if( B ){
 // code
}

这个写法没什么问题,可是在重复的出现这种相同的判断的时候。这个就显得有点不那么智能了。这个时候函数柯里化就可以排上用场了。

因为 Vue 可以在不同平台运行,所以也会存在上面的那种判断。这里利用柯里化的特点,通过 createPatchFunction 方法把一些参数提前保存,以便复用。

// 这样不用每次调用 patch 的时候都传递 nodeOps 和 modules
export function createPatchFunction (backend) {
    // 省略好多代码
    return function patch (oldVnode, vnode, hydrating, removeOnly) {
        // 省略好多代码
    }
}

event loop

四个概念:

  1. 同步任务:即在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。
  2. 异步任务:指的是不进入主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
  3. macrotask:主要场景有:主代码块、setTimeout、setInterval等
  4. microtask:主要场景有:Promise、process.nextTick等。

这一点网上教程已经很多了,再因为篇幅的问题,这里就不详细说了。

推荐一篇文章,说的很细致:

JavaScript 执行机制:https://juejin.im/post/59e85eebf265da430d571f89#heading-4

总结

这篇文章讲到这里就结束了。不过有一点我需要在说一篇,这篇文章的定位并不是面面俱到的将所有知识都讲一遍,这不现实我也没这个能力。

我只是希望通过这篇文章告诉大家一个观点,要想看源码,一些必备的 JavaScript 基础知识必须要扎实,否则你会举步维艰。

愿你每天都有进步。

Vue 相关文章输出计划

最近总有朋友问我 Vue 相关的问题,因此接下来我会输出 10 篇 Vue 相关的文章,希望对大家有一定的帮助。我会保持在 7 到 10 天更新一篇。

  1. 【前端词典】Vuex 注入 Vue 生命周期的过程(完成)
  2. 【前端词典】学习 Vue 源码的必要知识储备(完成)
  3. 【前端词典】浅析 Vue 响应式原理
  4. 【前端词典】新老 VNode 进行 patch 的过程
  5. 【前端词典】如何开发功能组件并上传 npm
  6. 【前端词典】从这几个方面优化你的 Vue 项目
  7. 【前端词典】从 Vue-Router 设计讲前端路由发展
  8. 【前端词典】在项目中如何正确的使用 Webpack
  9. 【前端词典】Vue 服务端渲染
  10. 【前端词典】Axios 与 Fetch 该如何选择

最后

如果你想进【大前端交流群】,关注公众号点击“交流加群”添加机器人自动拉你入群。关注我第一时间接收最新干货。

【前端词典】从输入 URL 到展现涉及哪些缓存环节

前言

缓存是一项用来提高网站性能不可或缺的技术,利用这项技术可以很好地提高 web 的性能。 缓存可以很有效地降低网络的时延,同时也会减少大量请求对于服务器的压力。
大家在继续看下去之前可以先思考一下 “从输入 URL 到页面加载完成的过程中都发生了什么事情”。你现在所看到的其实就是这个热点问题的一个变种问题。

不过这个问题对缓存会有一个更详尽的解释。我相信你看完这篇文章后对缓存会有一个全新的认识,如果没有那就再看一遍。

入题

在这篇文章,我会详尽的描述从输入 URL 到展现涉及到的缓存环节,不过由于本人知识有限,很可能有某些隐藏的缓存机制在下遗漏了,还请大佬不吝赐教。

在讲“从输入 URL 到展现涉及到的缓存环节”之前,我们先了解下缓存的优点:

缓存的几个优点

  1. 减少冗余的数据传输,可节省流量
  2. 缓解带宽瓶颈问题,可更快加载页面
  3. 缓解瞬间拥塞,可缓解原始服务器的压力
  4. 降低距离延时,加快响应速度

目录

  1. 地址栏网址缓存
  2. 检查 HSTS 预加载列表
  3. DNS 缓存
  4. ARP(地址解析协议)缓存
  5. TCP 发送缓冲区 & 接收缓冲区
  6. HTTP 请求缓存( CDN 节点缓存、代理服务器缓存、浏览器缓存、后端动态计算结果缓存等 )

接下来我们进入正题(带着答案,口味更佳):

一、地址栏网址缓存

输入 url 后遇到的第一个缓存环节就是地址栏网址缓存。

但我们输入一个常用的网址时,经常会有这样的情况,我们只是输入了几个字母,浏览器就自动补全了该网址。如下图:我只输入 j,就自动给我补全了 juejin.im

这就是地址栏网址缓存

当我们使用这个自动补全的网址时,你会发现请求的相关的静态资源也是从缓存中取得的。

注意:不论什么时候,我们获取的主页面资源 timeline, 都应该是重新请求服务器而获得的,不可以使用本地浏览器的缓存。至于为什么?你看到静态资源文件名的 hash 值你就应该清楚了。

可以在 Chrome 的地址栏中输入 Chrome://cache 查看缓存的信息

转换非 ASCII 的 Unicode 字符

浏览器检查输入是否含有不是 a-z,A-Z,0-9, - 或者 . 的字符;如果有的话,浏览器会对主机名部分使用 Punycode 编码

二、 检查 HSTS 预加载列表

HSTS( HTTP Strict Transport Security )国际互联网工程组织 IETE 正在推行一种新的 Web 安全协议,作用是强制客户端(如浏览器)使用 HTTPS 与服务器创建连接。

采用 HSTS 后:支持这个协议的浏览器,在输入 URL 后会检查自带的 HSTS 预加载列表(这个列表里包含了那些请求浏览器只使用 HTTPS 进行连接的域名),若网站在这个列表里,浏览器会使用 HTTPS 协议并且返回码为 307。而不支持 HSTS 的浏览器访问我们的网站,则不会产生跳转,从而提高了兼容性。这个机制对于不支持 HTTPS 的搜索引擎来说是非常友好的!

如掘金输入 http://juejin.im/timeline 会跳转到 https://juejin.im/timeline:


查看 HSTS 预加载列表是否存在你想访问的域名你可以在输入 qqbrowser://net-internals/#hsts,若存在会返回信息:

三、DNS 缓存

但你输入 juejin.im 按下回车后,就开始对 juejin.im 进行域名解析。域名解析最少涉及了三个地方的缓存:

  1. 浏览器的 DNS 缓存
  2. 操作系统中的 DNS 缓存
  3. 索操作系统的 hosts 文件(可手动写入的缓存)

域名解析的具体过程

  1. 浏览器搜索自己的 DNS 缓存(浏览器维护一张域名与 IP 地址的对应表);如果没有命中,进入下一步;
  2. 搜索操作系统中的 DNS 缓存;如果没有命中,进入下一步;
  3. 搜索操作系统的 hosts 文件( Windows 环境下,维护一张域名与 IP 地址的对应表);如果没有命中,进入下一步;
  1. 操作系统将域名发送至 LDNS (本地区域名服务器),LDNS 查询自己的 DNS 缓存(一般命中率在 80% 左右),查找成功则返回结果,失败则发起一个迭代 DNS 解析请求:
  2. LDNS向 Root Name Server(根域名服务器,如com、net、im 等的顶级域名服务器的地址)发起请求,此处,Root Name Server 返回 im 域的顶级域名服务器的地址;
  3. LDNS 向 im 域的顶级域名服务器发起请求,返回 juejin.im 域名服务器地址;
  4. LDNS 向 juejin.im 域名服务器发起请求,得到 juejin.im 的 IP 地址;
  5. LDNS 将得到的 IP 地址返回给操作系统,同时自己也将 IP 地址缓存起来;操作系统将 IP 地址返回给浏览器,同时自己也将 IP 地址缓存起来。

DNS Prefetch

即 DNS 预获取,是前端优化的一部分。一般来说,在前端优化中与 DNS 有关的有两点:

  1. 减少 DNS 的请求次数
  2. 进行 DNS 预获取

典型的一次 DNS 解析需要耗费 20-120 毫秒,减少DNS解析时间和次数是个很好的优化方式。DNS Prefetching 是让具有此属性的域名不需要用户点击链接就在后台解析,而域名解析和内容载入是串行的网络操作,所以这个方式能减少用户的等待时间,提升用户体验。

你可以通过 chrome://net-internals/#dns 查找目前系统中的 DNS 缓存和 Chrome 中使用的情况。

提个问题

问:浏览器 DNS 缓存的时间一般不会太长,一分钟左右。为什么缓存不设置较长时间呢?

答:虽然 DNS 缓存可以提高获取 DNS 的速度,但缓存时间过长也会影响 DNS 在 IP 变更时不能及时解析到最新的 IP。

四、ARP(地址解析协议)缓存

ARP 是一种用以解释地址的协议,根据通信方的 IP 地址就可以反查出对应方的 MAC 地址。

ARP 缓存是个用来储存 IP 地址和 MAC 地址的缓冲区,其本质就是一个 IP 地址与 MAC 地址的对应表,表中每一个条目分别记录了其他主机的 IP 地址和对应的 MAC 地址。

当地址解析协议被询问一个已知 IP 地址节点的 MAC 地址时,先在 AR 缓存中查看,若存在,就直接返回与之对应的MAC地址;若不存在,才发送 ARP 请求查询。

具体的 ARP 请求查询感兴趣的同学可以自行研究。

五、TCP 发送缓冲区 & 接收缓冲区

建立 TCP 连接这一步也涉及到缓存 —— 用来临时存放双方通信的数据,保证通信数据不会丢包

每个 TCP 连接在内核中都有一个发送缓冲区和接收缓冲区,TCP 的全双工的工作模式以及 TCP 的流量(拥塞)控制便是依赖于这两个独立的 buffer 以及 buffer 的填充状态。

发送缓冲区

发送缓冲区存放的是 send() 方法从应用缓冲区拷贝过来的数据。

内核基本上是按照 MSS(Maximum Segment Size,最大报文段长度) 从缓冲区中取数据发送出去,当缓冲区中数据小于 MSS,则将剩余数据全部发送出去。TCP 的发送缓冲区必须为已发送的数据保留一个副本,直到它被对端确认为止,才能从缓冲区中删掉已确认的数据。

接收缓冲区

接收缓冲区被 TCP 用来保存接收到的数据,直到应用程序来读取。

接收缓冲区把数据缓存入内核,等待 recv() 方法读取,recv() 方法所做的工作,就是把内核缓冲区中的数据拷贝到应用层用户的 buffer 里面,拷贝后就删掉已确认的数据。

流控制(Flow Control)

A mechanism to prevent a TCP sender from overwhelming a TCP receiver.
TCP 流控制主要用于匹配发送端和接收端的速度,即根据接收端当前的接收能力来调整发送端的发送速度。

由于发送速度可能大于接收速度,接收端的应用程序未能及时从接收缓冲区读取数据,接收缓冲区不够大不能缓存所有接收到的报文等原因,TCP接收端的接收缓冲区很快就会被塞满;从而导致不能接收后续的数据,发送端此后发送数据是无效的,因此需要流控制。

TCP 的缓存就讲到这里,感兴趣的可以自己翻阅资料。

六、HTTP 请求缓存( CDN 节点缓存、代理服务器缓存、浏览器缓存、后端动态计算结果缓存等 )

在建立了 TCP 连接之后,就开始 HTTP 请求了;而 HTTP 缓存是优化性能不可忽视的一部分,这一部分我会着重讲解。

再讲具体过程之前,我再讲一遍强缓存和协商缓存。

强缓存 ( Cache-Control 和 Expires )

强缓存主要是采用响应头中的 Cache-ControlExpires 两个字段进行控制的。

其中 ExpiresHTTP 1.0 中定义的,它指定了一个绝对的过期时期。而 Cache-ControlHTTP 1.1 时出现的缓存控制字段。 由于 ExpiresHTTP1.0 时代的产物,因此设计之初就存在着一些缺陷,如果本地时间和服务器时间相差太大,就会导致缓存错乱。

这两个字段同时使用的时候 Cache-Control 的优先级会更高一点。

这两个字段的效果是类似的,客户端都会通过对比本地时间和服务器返回的生存时间来检测缓存是否可用。如果缓存没有超出它的生存时间,客户端就会直接采用本地的缓存。如果生存日期已经过了,这个缓存也就宣告失效。接着客户端将再次与服务器进行通信来验证这个缓存是否需要更新

请求头中使用 Cache-Control 时,它可选的值有:

指令 说明
no-cache 使用代理服务器的缓存之前提交原始服务器验证,验证通过才能使用
no-store 在客户端或是代理服务器都不缓存请求或响应的任何内容
max-age=[秒] 告知服务器客户端可接受资源的存在最大时间
max-stale(=[秒]) 可接受(代理服务器缓存的)过期资源,参数可省略
min-fresh=[秒] 可接受(代理服务器缓存的)资源更新时间小于指定时间
no-transform 代理服务器不可以更改媒体类型
only-if-cached 客户端只接受已缓存的响应,若缓存不命中,则返回 504 错误
cache-extension 自定义扩展值,若服务器不知别该指令,就直接忽略

响应头中使用 Cache-Control 时,它可选的值有:

指令 说明
public 表明该资源可以给多个用户使用
private(= name) 该资源是私有资源,指定的用户可以使用的缓存
no-cache 强制每次请求直接发送给源服务器,而不经过本地缓存版本的校验。
no-store 在客户端或是代理服务器都不缓存请求或响应的任何内容
no-transform 代理服务器不可以更改媒体类型
must-revalidate 可缓存但必须再向源服务器进行请求确认
proxy-revalidate 要求缓存服务器返回缓存的时候向源服务器进行请求确认
max-age=[秒] 告知客户端该资源在规定时间内是新鲜的,无需向服务器确认
s-maxage=[秒] 告知缓存服务该资源在规定时间内是新鲜的,无需向服务器确认
cache-extension 自定义扩展值,若服务器不识别该指令,就直接忽略

可缓存性

  1. public:响应可以被任何对象(客户端、代理服务器等)缓存
  2. private:只能被单个用户缓存,不能作为共享缓存
  3. no-cache:使用缓存副本之前,需要将请求提交给原始服务器进行验证,验证通过才可以使用
  4. only-if-cached:客户端只接受已缓存的响应,并且不向原始服务器检查是否有更新的拷贝

到期

  1. max-age=<seconds>:缓存存储的最大周期,超过这个时间缓存被认为过期(单位秒)。与 Expires 相反,时间是相对于请求的时间
  2. s-maxage=<seconds>:覆盖 max-age 或者 Expires 头,但是仅适用于共享缓存(比如各个代理),并且私有缓存中它被忽略
  3. max-stale[=<seconds>]:表明客户端愿意接收一个已经过期的资源。可选的设置一个时间(单位秒),表示响应不能超过的过时时间
  4. min-fresh=<seconds>:表示客户端希望在指定的时间内获取最新的响应

重新验证和重新加载

  1. must-revalidate:缓存必须在使用之前验证旧资源的状态,并且不可使用过期资源。
  2. proxy-revalidate:与 must-revalidate 作用相同,但它仅适用于共享缓存(例如代理),并被私有缓存忽略

其他

  1. no-store:彻底得禁用缓冲,本地和代理服务器都不缓冲,每次都从服务器获取
  2. no-transform:不得对资源进行转换或转变。Content-Encoding, Content-Range, Content-TypeHTTP 头不能由代理修改。

协商缓存 ( Last-Modified 和 Etag )

协商缓存机制下,浏览器需要向服务器去询问缓存的相关信息,进而判断是重新发起请求还是从本地获取缓存的资源。如果服务端提示缓存资源未改动( Not Modified ),资源会被重定向到浏览器缓存,这种情况下网络请求对应的状态码是 304

**Last-Modified 和 If-Modified-Since **

基于资源在服务器修改时间而验证缓存的过期机制

当客户端再次请求该资源的时候,会在其请求头上附带上 If-Modified-Since 字段(值就是第一次获取请求资源时响应头中返回的 Last-Modified 值)。如果修改时间未改变则表明资源未过期,命中缓存,服务器就直接返回 304 状态码,客户端直接使用本地的资源。否则,服务器重新发送响应资源,从而保证资源的有效性。

Etag 和 If-None-Match

基于资源校验码(一般为md5值)而验证缓存的过期机制

当客户端再次请求该资源的时候,会在其请求头上附带上 If-None-Match 字段(值就是第一次获取请求资源时响应头中返回的 Etag 值),其值与服务器端资源文件的验证码进行对比,如果匹配成功直接返回 304 状态码,从浏览器本地缓存取资源文件。如果不匹配,服务器会把新的验证码放在请求头的 Etag 字段中,并且以 200 状态码返回资源。

需要注意的是当响应头中同时存在 EtagLast-Modified 的时候,会先对 Etag 进行比对,随后才是 Last-Modified

Etag 的问题
相同的资源,在两台服务器产生的 Etag 是不是相同的,所以对于使用服务器集群来处理请求的网站来说,Etag 的匹配概率会大幅降低。所在在这种情况下,使用 Etag 来处理缓存,反而会有更大的开销。

静态资源和动态资源的请求过程解析

静态资源
第一次请求肯定是从服务器请求过来的资源,这个没有什么疑问,我们先看看第一次请求的响应头的内容:

我们发现第一次的响应头中包含可强缓存的相关字段 cache-control ,同时也包含了协商缓存的相关字段 etaglast-modified;

当强缓存和协商缓存字段同时存在时会进行以下步骤来请求资源:

  1. 强缓存和协商缓存同时存在,如果强缓存还在有效期内则直接使用缓存;如果强缓存不在有效期,协商缓存生效。
    即:强缓存优先级 > 协商缓存优先级

  2. 强缓存的 expirescache-control 同时存在时,cache-control 会覆盖 expires 的效果,expires 无论有没有过期,都无效。
    即:cache-control 优先级 > expires 优先级。

  3. 协商缓存的 EtagLast-Modified 同时存在时,会先对 Etag 进行比对,随后才是 Last-Modified
    即:ETag 优先级 > Last-Modified 优先级。

第二次请求该资源的时候,就直接是从缓存中读取的:

其实我们第一次获取的资源极有可能是从 CDN 节点的缓存中获取的,也很有可能是从中间代理服务器(nginx,node 等)的缓存中读取的;其中的好处不言而喻。

动态资源

由于动态资源的返回结果不一致,所以这个我们肯定不会在浏览器(中间代理服务器)缓存动态的结果。

不过这里我们可以在后端缓存一些重复率比较高的相关的计算结果。

如:这里有 60 只股票,用户可以选择其中几只股票作为自己的股票投资池。用户选择完股票后提交,会通过相关的算法计算其预期收益效果等指标。我们知道每次计算的时间可能会比较久,所以在这步我们可以在后端将可能的组合结果先计算好缓存起来,当我们请求的时候就后端就可以直接返回已经计算好的结果给前端。至于计算结果的缓存时间也就完全由服务器控制了。

关于动态资源一般前端是不做缓存的。

后端缓存主要通过保留数据库连接,存储处理结果等方式缩短处理时间,尽快响应客户端请求。

成功获得资源后就开始客户端渲染,接下来的相关解析过程这里我就不讲了,有兴趣的同学可以留言交流。

最后

如果你想进【大前端交流群】,关注公众号点击“交流加群”添加机器人自动拉你入群。关注我第一时间接收最新干货。

【前端词典】你或许可以这样优化 if else 结构

前言

最近部门在对以往的代码做一些优化,我在代码中看到一连串的 if(){}else if(){} 的逻辑判断。这明显是有优化空间的。

由于内部代码不适合分享,这里我就用 <输出今天为星期几> 来讲讲逻辑判断优化的一些方案。

这里先声明,免有人疑惑:

我们在项目中使用的很可能会有多层的嵌套,不像我的例子只有一层。不过其优化的**是大致相同的。由于 returnWeekday() 方法异常之简单,所以提前声明。

需求

写一个 returnWeekday() 方法返回"今天是星期*"。

接下来我们就一步一步来。

优化过程

这里我简单的分为 “初学 —> 入门 —> 中级” 这几个阶段。

初学

当我们开始拿到需求的时候,看到一系列的逻辑判断,首先想到的应该就是 if 语句了。

代码如下:

function returnWeekday(){
    let string = "今天是星期";
    let date = new Date().getDay();
    if (date === 0) {
        string += "日";
    } else if (date === 1) {
        string += "一";
    } else if (date === 2) {
        string += "二";
    } else if (date === 3) {
        string += "三";
    } else if (date === 4) {
        string += "四";
    } else if (date === 5) {
        string += "五";
    } else if (date === 6) {
        string += "六";
    }
    return string
}
console.log(returnWeekday())

当我们写完了这样的代码,第一感觉就是 else if 块是不是太多了。

可是当我们还在思考如何优化的时候,产品就在钉钉发消息给你问这个需求完成的怎么样了?还带了一个坏笑的表情包。这个时候你告诉自己,这个优化后面再说吧。可是久而久之,需求加之。这个再也没有优化过了,直到下一个接手。

上面的代码确实 else if 块太多了,看着就不舒服。

我们在看《JavaScript 高级程序设计》的时候,看到这样一句话:

switch 语句与 if 语句的关系最为密切,而且也是在其他语言中普遍使用的一种流控制语句。

所以我们是不是可以考虑使用 switch 语句来优化一下呢?

入门

这里我们使用 switch 语句优化一遍代码。

注意: switch 语句在比较值的时候使用的是全等操作符,不会有类型转换的情况。

代码如下:

function returnWeekday(){
    let string = "今天是星期";
    let date = new Date().getDay();
    switch (date) {
        case 0 :
            string += "日";
            break;
        case 1 :
            string += "一";
            break;
        case 2 :
            string += "二";
            break;
        case 3 :
            string += "三";
            break;
        case 4 :
            string += "四";
            break;
        case 5 :
            string += "五";
            break;
        case 6 :
            string += "六";
            break;
    }
    return string
}
console.log(returnWeekday())

我们在 case 里面拼接字符,以达到输出预期结果的目的。这里的结构看起来确实比 if 语句清晰了一点。可是还是有点疑惑?

假设哪一天,相关组织发现,星象有变。每周需要变成八天(产品的思维,你无法想象)。我们这个 returnWeekday() 方法就需要多加一层判断了。

我们的希望是已经封装好的方法,不要频繁的修改。可是需求的变动是你无法控制的。

所以我们继续思考该怎么优化。

中级

我们看到这里的 case 是数字,和数组的下标是一致的。

即:['天','一','二','三','四','五','六'] 的下标。

所以我们可以考虑使用数组来优化。

代码如下:

function returnWeekday(){
    let string = "今天是星期";
    let date = new Date().getDay();
    // 使用数组
    let dateArr = ['天','一','二','三','四','五','六'];
    return string + dateArr[date]
}
console.log(returnWeekday())

以上代码是不是比 switch 语句和 if 语句清晰多了。而且就算一周变为八天,只需要修改 dateArr 数组就好了。

倘若我们的每个 case 是不规律的字符串呢?那我们使用对象,每个 case 为一个 key

代码如下:

function returnWeekday(){
    let string = "今天是星期";
    let date = new Date().getDay();
    // 使用对象
    dateObj = { 
        0: '天', 
        1: "一", 
        2: "二", 
        3: "三", 
        4: "四", 
        5: "五", 
        6: "六", 
    };
    return string + dateObj[date]
}
console.log(returnWeekday())

以上使用数组或者对象的写法,提高代码的可读性的同时,维护起来也变得简单了。

使用 charAt 字符方法

字符串有个和使用数组下标类似的方法:

// charAt 定位方法
function returnWeekday(){
    return "今天是星期" + "日一二三四五六".charAt(new Date().getDay());
}
console.log(returnWeekday())

需求变动

这个时候,有人希望 returnWeekday() 方法不仅返回今天是周几,还需要区分工作日和休息日,调用相关方法。

如果是使用 switchif数组 维护起来就有点麻烦,需要改写的地方还挺多的。

function returnWeekday(){
    let string = "今天是星期";
    let date = new Date().getDay();
    // 使用对象
    dateObj = { 
        0: ['天','休'], 
        1: ["一",'工'], 
        2: ["二",'工'], 
        3: ["三",'工'], 
        4: ["四",'工'], 
        5: ["五",'工'], 
        6: ["六",'休'], 
    }
    // 类型,这里也可以对应相关方法
    dayType = {
        '休': function(){
            // some code
            console.log('为休息日')
        },
        '工': function(){
            // some code
            console.log('为工作日')
        }
    }
    let returnData = {
        'string' : string + dateObj[date][0],
        'method' : dayType[dateObj[date][1]]
    }
    return returnData
};
console.log(returnWeekday().method.call(this))

其他常见优化手段

这里在给出一些常见的优化手段。

三元运算

适合简单的 if(){}else{} 情况。

//滚动监听,头部固定
handleScroll: function () {
    let offsetTop = this.$refs.pride_tab_fixed.getBoundingClientRect().top;
    if( offsetTop < 0 ){
        this.titleFixed = true
    } else {
        this.titleFixed = false
    }
    
    // 改成三元
    (offsetTop < 0) ? this.titleFixed = true : this.titleFixed = false;
    
    // 我们发现条件块里面的赋值情况是布尔值,所以可以更简单
    this.titleFixed = offsetTop < 0;
}

这样在精简代码的时候,可读性也没有降低。

逻辑与运算符

有些时候我们可以使用逻辑与运算符来简化代码

if( falg ){
    someMethod()
}

可以改成:

falg && someMethod();

使用 includes 处理多重条件

if( code === '202' || code === '203' || code === '204' ){
    someMethod()
}

可以改成

if( ['202','203','204'].includes(code) ){
    someMethod()
}

最后

如果你想进【大前端交流群】,关注公众号点击“交流加群”添加机器人自动拉你入群。关注我第一时间接收最新干货。

【前端词典】进阶必备的网络基础

前言

在不那么遥远的一些年以前,一个在江湖中行走的前端,只需要了解“前端三剑客”就足以找到一份工作。很多前端只限于 CSS,HTML、JS,网络基础,数据结构之类的都不甚了解。不过这个时期的前端也是最受鄙视的时期,这个时期前端的大量工作依赖于后端,且不需要动画效果和交互效果。

现如今前端圈已经发生翻天覆地的变化,Vue,React,ES6,HTML5,CSS3,Webpack, PostCss 等技术层出不穷。作为一个有格局的前端,对网络基础定是要了然于心的。

如果你对网络基础还不太了解,以下的内容可以给你提供一个思路;如果你对此已经了然于心,以下的内容烦请批评指正。

入题

任何事物的诞生,最初都是服务于极少数人的。渐渐地被这极少数人推而广之,我们大众就开始接触了解它,互联网是如此,麻将亦是如此。不管是互联网还是麻将,它们都增强了人与人之间的交流。
接下来我会讲以下内容:

  1. 五层因特网协议栈
  2. HTTP 与 HTTPS 的区别
  3. TCP/IP 协议
  4. 五类 IP 地址
  5. DNS 域名解析
  6. 三次握手和四次挥手
  7. 跨域的原因及处理方式
  8. 正向代理和反向代理
  9. CDN 带来的性能优化
  10. HTTP 强缓存&协商缓存

五层因特网协议栈 TOP

一、应用层

应用层( application-layer )的任务是通过应用进程间的交互来完成特定网络应用。应用层协议定义的是应用进程(进程:主机中正在运行的程序)间的通信和交互的规则。对于不同的网络应用需要不同的应用层协议。在互联网中应用层协议很多,如域名系统 DNS,支持万维网应用的 HTTP 协议,支持电子邮件的 SMTP 协议等等。

我们把应用层交互的数据单元称为报文

域名系统

域名系统( Domain Name System )是因特网的一项核心服务,它作为可以将域名和 IP 地址相互映射的一个分布式数据库,能够使人更方便的访问互联网,而不用去记住能够被机器直接读取的 IP 数串。

http 协议

超文本传输协议( HyperText Transfer Protocol )是互联网上应用最为广泛的一种网络协议。所有的 WWW(万维网) 文件都必须遵守这个标准。

二、传输层

传输层(transport layer)的主要任务就是负责向两台主机进程之间的通信提供通用的数据传输服务。应用进程利用该服务传送应用层报文。

传输层常用的两种协议

  1. 传输控制协议-TCP:提供面向连接的,可靠的数据传输服务。
  2. 用户数据协议-UDP:提供无连接的,尽最大努力的数据传输服务(不保证数据传输的可靠性)。

TCP(Transmisson Control Protocol)

  1. TCP 是面向连接的(需要先建立连接);
  2. 每一条 TCP 连接只能有两个端点,每一条 TCP 连接只能是一对一;
  3. TCP提供可靠交付的服务。通过TCP连接传送的数据,无差错、不丢失、不重复、并且按序到达;
  4. TCP 提供全双工通信。TCP 允许通信双方的应用进程在任何时候都能发送数据。TCP 连接的两端都设有发送缓存和接收缓存,用来临时存放双方通信的数据;
  5. 面向字节流。TCP 中的“流”(Stream)指的是流入进程或从进程流出的字节序列。

UDP(User Datagram Protocol)

  1. UDP 是无连接的;
  2. UDP 使用尽最大努力交付,即不保证可靠交付,因此主机不需要维持复杂的链接状态;
  3. UDP 是面向报文的;
  4. UDP 没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如直播,实时视频会议等);
  5. UDP 支持一对一、一对多、多对一和多对多的交互通信;
  6. UDP 的首部开销小,只有 8 个字节,比 TCP 的 20 个字节的首部要短。
  1. 单工数据传输只支持数据在一个方向上传输
  2. 半双工数据传输允许数据在两个方向上传输,但是,在某一时刻,只允许数据在一个方向上传输,它实际上是一种切换方向的单工通信;
  3. 全双工数据通信允许数据同时在两个方向上传输,因此,全双工通信是两个单工通信方式的结合,它要求发送设备和接收设备都有独立的接收和发送能力。

三、网络层

网络层的任务就是选择合适的网间路由和交换结点,确保计算机通信的数据及时传送。在发送数据时,网络层把运输层产生的报文段或用户数据报封装成分组和包进行传送。在 TCP/IP 体系结构中,由于网络层使用 IP 协议,因此分组也叫 IP 数据报 ,简称数据报。

互联网是由大量的异构(heterogeneous)网络通过路由器(router)相互连接起来的。互联网使用的网络层协议是无连接的网际协议(Intert Prococol)和许多路由选择协议,因此互联网的网络层也叫做网际层或 IP 层。

四、数据链路层

数据链路层(data link layer)通常简称为链路层。两台主机之间的数据传输,总是在一段一段的链路上传送的,这就需要使用专门的链路层的协议。
在两个相邻节点之间传送数据时,数据链路层将网络层接下来的 IP 数据报组装成帧,在两个相邻节点间的链路上传送帧。每一帧包括数据和必要的控制信息(如同步信息,地址信息,差错控制等)。

在接收数据时,控制信息使接收端能够知道一个帧从哪个比特开始和到哪个比特结束。

五、物理层

在物理层上所传送的数据单位是比特。 物理层(physical layer)的作用是实现相邻计算机节点之间比特流的透明传送,尽可能屏蔽掉具体传输介质和物理设备的差异。使其上面的数据链路层不必考虑网络的具体传输介质是什么。“透明传送比特流”表示经实际电路传送后的比特流没有发生变化,对传送的比特流来说,这个电路好像是看不见的。
在互联网使用的各种协中最重要和最著名的就是 TCP/IP 两个协议。

和 OSI 七层协议模型、TCP/IP 四层模型的区别

  1. OSI 七层模型
    OSI七层协议模型主要是:
    应用层(Application)、表示层(Presentation)、会话层(Session)、传输层(Transport)、网络层(Network)、数据链路层(Data Link)、物理层(Physical)。

  2. TCP/IP四层模型
    TCP/IP是一个四层的体系结构,主要包括:
    应用层、传输层、网络层和链路层。

各层对应

以下一张图很好的说明了这三种协议的区别

HTTP 与 HTTPS 的区别 TOP

区别 HTTP HTTPS
协议 运行在 TCP 之上,明文传输,客户端与服务器端都无法验证对方的身份 身披 SSL( Secure Socket Layer )外壳的 HTTP,运行于 SSL 上,SSL 运行于 TCP 之上, 是添加了加密和认证机制的 HTTP
端口 80 443
资源消耗 较少 由于加解密处理,会消耗更多的 CPU 和内存资源
开销 无需证书 需要证书,而证书一般需要向认证机构购买
加密机制 共享密钥加密和公开密钥加密并用的混合加密机制
安全性 由于加密机制,安全性强

对称加密与非对称加密

对称密钥加密是指加密和解密使用同一个密钥的方式,这种方式存在的最大问题就是密钥发送问题,即如何安全地将密钥发给对方;
而非对称加密是指使用一对非对称密钥,即公钥和私钥,公钥可以随意发布,但私钥只有自己知道。发送密文的一方使用对方的公钥进行加密处理,对方接收到加密信息后,使用自己的私钥进行解密。
由于非对称加密的方式不需要发送用来解密的私钥,所以可以保证安全性;但是和对称加密比起来,非常的慢.

综上:我们还是用对称加密来传送消息,但对称加密所使用的密钥我们可以通过非对称加密的方式发送出去。

TCP/IP 协议 TOP

负责传输的 IP 协议

按层次分,IP(Internet Protocol)网际协议位于网络层,IP 协议的作用是把各种数据包传送给对方。而要保证确实传送到对方那里,则需要满足各类条件,其中两个重要的条件是 IP 地址和 MAC 地址(Media Access Control Address)。

IP 地址和 MAC 地址: 指明了节点被分配到的地址,MAC 地址是指网卡所属的固定地址,IP 地址可以和 MAC 地址进行配对。IP 地址可变换,但 MAC 地址基本上不会更改。

使用 ARP 协议凭借 MAC 地址进行通信

  1. IP 间的通信依赖 MAC 地址。
  2. ARP 是一种用以解释地址的协议,根据通信方的 IP 地址就可以反查出对应方的 MAC 地址。

TCP 协议如何保持传输的可靠性

TCP提供一种面向连接的、可靠的字节流服务。
1. 面向连接
意味着两个使用 TCP 的应用(通常是一个客户和一个服务器)在彼此交换数据之前必须先建立一个 TCP 连接。在一个 TCP 连接中,仅有两方进行彼此通信;
2. 字节流服务
意味着两个应用程序通过 TCP 链接交换 8bit 字节构成的字节流,TCP 不在字节流中插入记录标识符。

TCP 之所以可靠,大体上由于以下原因:

  1. 数据包校验:目的是检测数据在传输过程中的任何变化,若校验出包有错,则丢弃报文段并且不给出响应,这时 TCP 发送数据端超时后会重发数据;
  2. 对失序数据包重排序:既然 TCP 报文段作为 IP 数据报来传输,而 IP 数据报的到达可能会失序,因此 TCP 报文段的到达也可能会失序。TCP 将对失序数据进行重新排序,然后才交给应用层;
  3. 丢弃重复数据:对于重复数据,能够丢弃重复数据;
  4. 应答机制:当 TCP 收到发自 TCP 连接另一端的数据,它将发送一个确认。这个确认不是立即发送,通常将推迟几分之一秒;
  5. 超时重发:当 TCP 发出一个段后,它启动一个定时器,等待目的端确认收到这个报文段。如果不能及时收到一个确认,将重发这个报文段;
  6. 流量控制:TCP 连接的每一方都有固定大小的缓冲空间。TCP 的接收端只允许另一端发送接收端缓冲区所能接纳的数据,这可以防止较快主机致使较慢主机的缓冲区溢出,这就是流量控制。

TCP/IP 通信传输流

借用图解 HTTP 一书中的图文:

发送端在层与层之间传输数据时,每经过一层必定会加上一个该层的首部信息。反之,接收端在层与层之间传输数据时,每经过一层会把相关的首部信息去掉。

五类 IP 地址 TOP

网络地址:用于识别主机所在的网络;
主机地址:用于识别该网络中的主机。

IP地址分为五类:

  1. A 类保留给政府机构
  2. B 类分配给中等规模的公司
  3. C 类分配给任何需要的人
  4. D 类用于组播
  5. E 类用于实验

各类可容纳的地址数目不同。其中A类、B类、和C类这三类地址用于TCP/IP节点,其它两类D类和E类被用于特殊用途。

一. A类地址

第一个八位段为网络地址,其它为主机地址,第一个八位段首位一定为0;
范围:1.0.0.1—126.155.255.254;
私有地址和保留地址:
10.X.X.X是私有地址(所谓的私有地址就是在互联网上不使用,而被用在局域网络中的地址)。
127.X.X.X是保留地址,用做循环测试用的。

二. B类地址

第一个八位段和第二个八位段为网络地址,其它为主机地址,第一个八位段首位一定为10;
范围:128.0.0.1—191.255.255.254。
私有地址和保留地址:
172.16.0.0—172.31.255.255是私有地址
169.254.X.X是保留地址。如果你的IP地址是自动获取IP地址,而你在网络上又没有找到可用的DHCP服务器。就会得到其中一个IP。

三. C类地址

前三个八位段为网络地址,第4个个字节为主机地址,第一个八位段首位一定为110。
范围:192.0.0.1—223.255.255.254。
私有地址:
192.168.X.X是私有地址。

四. D类地址

不分网络地址和主机地址,第一个八位段首位一定为1110。
范围:224.0.0.1—239.255.255.254

五. E类地址

不分网络地址和主机地址,第一个八位段首位一定为11110。
范围:240.0.0.1—255.255.255.254

DNS 域名解析 TOP

当你在浏览器的地址栏输入 https://juejin.im 后会发生什么,大家在心中肯定是有一个大概的,这里我将 DNS 域名解析 这个步骤详细的讲一遍。在讲概念之前我先放上一张经典的图文供大家思考一分钟。

查找域名对应的 IP 地址的聚体过程

  1. 浏览器搜索自己的 DNS 缓存(浏览器维护一张域名与 IP 地址的对应表);如果没有命中,进入下一步;
  2. 搜索操作系统中的 DNS 缓存;如果没有命中,进入下一步;
  3. 搜索操作系统的 hosts 文件( Windows 环境下,维护一张域名与 IP 地址的对应表);如果没有命中,进入下一步;
  1. 操作系统将域名发送至 LDNS (本地区域名服务器),LDNS 查询自己的 DNS 缓存(一般命中率在 80% 左右),查找成功则返回结果,失败则发起一个迭代 DNS 解析请求:
  2. LDNS向 Root Name Server(根域名服务器,如com、net、im 等的顶级域名服务器的地址)发起请求,此处,Root Name Server 返回 im 域的顶级域名服务器的地址;
  3. LDNS 向 im 域的顶级域名服务器发起请求,返回 juejin.im 域名服务器地址;
  4. LDNS 向 juejin.im 域名服务器发起请求,得到 juejin.im 的 IP 地址;
  5. LDNS 将得到的 IP 地址返回给操作系统,同时自己也将 IP 地址缓存起来;操作系统将 IP 地址返回给浏览器,同时自己也将 IP 地址缓存起来。

DNS Prefetch

即 DNS 预获取,是前端优化的一部分。一般来说,在前端优化中与 DNS 有关的有两点:

  1. 减少 DNS 的请求次数
  2. 进行 DNS 预获取

典型的一次 DNS 解析需要耗费 20-120 毫秒,减少DNS解析时间和次数是个很好的优化方式。DNS Prefetching 是让具有此属性的域名不需要用户点击链接就在后台解析,而域名解析和内容载入是串行的网络操作,所以这个方式能减少用户的等待时间,提升用户体验。

TCP 三次握手和四次挥手 TOP

TCP(Transmission Control Protocol 传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议,由 IETF 的 RFC793 定义。

三次握手


第一次握手:
建立连接时,向服务器发出连接请求报文,这是报文首部中的同部位 SYN = 1,同时选择一个初始序列号 seq = x ,客户端进程进入了 SYN-SENT (同步已发送状态)状态,等待服务器确认;
第二次握手:
服务器收到 syn 包后,如果同意连接,则发出确认报文; 确认报文 ACK = 1,SYN = 1,确认号是 ack = x + 1,同时也要为自己初始化一个序列号 seq = y,此时服务器进程进入了 SYN-RCVD(同步收到)状态;
第三次握手:
客户端收到服务器的 SYN+ACK 包,要向服务器给出确认。确认报文的 ACK = 1,ack = y + 1,自己的序列号 seq = x + 1,此时,TCP 连接建立,客户端进入 ESTABLISHED (已建立连接)状态。

完成三次握手,客户端与服务器开始传送数据。

注:
seq:"sequance" 序列号;
ack:"acknowledge" 确认号;
SYN:"synchronize" 请求同步标志;
ACK:"acknowledge" 确认标志;
FIN:"Finally" 结束标志。

未连接队列
在三次握手协议中,服务器维护一个未连接队列,该队列为每个客户端的SYN包(syn=j)开设一个条目,该条目表明服务器已收到SYN包,并向客户发出确认,正在等待客户的确认包。这些条目所标识的连接在服务器处于 Syn_RECV状态,当服务器收到客户的确认包时,删除该条目,服务器进入ESTABLISHED状态。

建立一个连接需要三次握手,而终止一个连接要经过四次握手,这是由 TCP 的半关闭(half-close)造成的。

四次挥手


第一次挥手:
客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部 FIN=1,其序列号为 seq = u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入 FIN-WAIT-1(终止等待1)状态。
第二次挥手:
服务器收到连接释放报文,发出确认报文,ACK = 1,ack = u + 1,并且带上自己的序列号 seq = v,此时,服务端就进入了 CLOSE-WAIT(关闭等待)状态。

TCP 服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个 CLOSE-WAIT 状态持续的时间。
客户端收到服务器的确认请求后,此时,客户端就进入 FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。

第三次挥手:
服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN = 1,ack = u + 1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为 seq = w,此时,服务器就进入了 LAST-ACK(最后确认)状态,等待客户端的确认。
第四次挥手:
客户端收到服务器的连接释放报文后,必须发出确认,ACK = 1,ack = w + 1,而自己的序列号是 seq = u + 1,此时,客户端就进入了 TIME-WAIT(时间等待)状态。

注意此时 TCP 连接还没有释放,必须经过 2MSL(最长报文段寿命)的时间后,当客户端撤销相应的 TCB 后,才进入 CLOSED 状态。

服务器只要收到了客户端发出的确认,立即进入 CLOSED 状态。同样,撤销 TCB 后,就结束了这次的 TCP 连接。

可以看到,服务器结束 TCP 连接的时间要比客户端早一些。

四次的原因

这是因为服务端的 LISTEN 状态下的 SOCKET 当收到 SYN 报文的建连请求后,它可以把 ACK 和 SYN(ACK 起应答作用,而 SYN 起同步作用)放在一个报文里来发送。 但关闭连接时,当收到对方的 FIN 报文通知时,它仅仅表示对方没有数据发送给你了;但未必你所有的数据都全部发送给对方了,所以你未必会马上会关闭 SOCKET ,也即你可能还需要发送一些数据给对方之后,再发送 FIN 报文给对方来表示你同意现在可以关闭连接了,所以它这里的 ACK 报文和 FIN 报文多数情况下都是分开发送的.

由于 TCP 连接是全双工的,因此每个方向都必须单独进行关闭。这个原则是当一方完成它的数据发送任务后就能发送一个 FIN 来终止这个方向的连接。收到一个 FIN 只意味着这一方向上没有数据流动,一个 TCP 连接在收到一个 FIN 后仍能发送数据。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。

跨域的原因及处理方式 TOP

出现跨域的原因是由于浏览器的同源策略 所决定的。

同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。

这个说法一如既往的很官方,犹如女神的一句 呵呵,让人不知所以然。接下来就从 Dom 查询和接口请求来说明同源策略的必要性。

接口请求

我们来看场景:
1.你准备去清空你的购物车,于是打开了买买买网站 www.maimaimai.com,然后登录成功,一看,购物车东西这么少,不行,还得买多点。
2.你在看有什么东西买的过程中,你的好基友发给你一个链 接 www.nidongde.com,一脸坏笑地跟你说:“你懂的”,你毫不犹豫打开了。
3.你饶有兴致地浏览着 www.nidongde.com,谁知这个网站暗地里做了些不可描述的事情!由于没有同源策略的限制,它向 www.maimaimai.com 发起了请求!暗地里为所欲为的做一些为所欲为的事情。

Dom 查询

1.有一天你刚睡醒,收到一封邮件,说是你的银行账号有风险,赶紧点进 www.yinghang.com 改密码。你吓尿了,赶紧点进去,还是熟悉的银行登录界面,你果断输入你的账号密码,登录进去看看钱有没有少了。
2.睡眼朦胧的你没看清楚,平时访问的银行网站是 www.yinhang.com,而现在访问的是 www.yinghang.com,这个钓鱼网站做了什么呢?

// HTML
<iframe name="yinhang" src="www.yinhang.com"></iframe>
// JS
// 由于没有同源策略的限制,钓鱼网站可以直接拿到别的网站的Dom
const iframe = window.frames['yinhang']
const node = iframe.document.getElementById('你输入账号密码的Input')
console.log(`拿到了这个${node},我还拿不到你刚刚输入的账号密码吗`)

由此我们知道,同源策略确实能规避一些危险,不是说有了同源策略就安全,只是说同源策略是一种浏览器最基本的安全机制,毕竟能提高一点攻击的成本。其实没有刺不穿的盾,只是攻击的成本和攻击成功后获得的利益成不成正比。

跨域解决方案

  1. 通过jsonp跨域
  2. document.domain + iframe跨域
  3. location.hash + iframe
  4. window.name + iframe跨域
  5. postMessage跨域
  6. 跨域资源共享(CORS)
  7. nginx代理跨域
  8. nodejs中间件代理跨域
  9. WebSocket协议跨域

具体的方案请看前端常见跨域解决方案(全)

正向代理和反向代理 TOP

正向代理

  1. 代理客户;
  2. 隐藏真实的客户,为客户端收发请求,使真实客户端对服务器不可见;
  3. 一个局域网内的所有用户可能被一台服务器做了正向代理,由该台服务器负责 HTTP 请求;
  4. 意味着同服务器做通信的是正向代理服务器;

反向代理

  1. 代理服务器;
  2. 隐藏了真实的服务器,为服务器收发请求,使真实服务器对客户端不可见;
  3. 负载均衡服务器,将用户的请求分发到空闲的服务器上;
  4. 意味着用户和负载均衡服务器直接通信,即用户解析服务器域名时得到的是负载均衡服务器的 IP ;

共同点

  1. 都是做为服务器和客户端的中间层
  2. 都可以加强内网的安全性,阻止 web 攻击
  3. 都可以做缓存机制

具体的应用可以看我写的这一篇文章 【前端词典】和媳妇讲代理后的意外收获

CDN 带来的性能优化 TOP

HTTP 强缓存&协商缓存 TOP

缓存是一种保存资源副本并在下次请求时直接使用该副本的技术。当 web 缓存发现请求的资源已经被存储,它会拦截请求,返回该资源的拷贝,而不会去源服务器重新下载。
这样带来的好处是缓解服务器端压力,提升性能(获取资源的耗时更短了)。对于网站来说,缓存是达到高性能的重要组成部分。
缓存大致可归为两类:私有缓存与共享缓存
共享缓存能够被多个用户使用;
私有缓存只能用于单独用户;

HTTP 协议主要是通过请求头当中的一些字段来和服务器进行通信,从而采用不同的缓存策略。
HTTP 通过缓存将服务器资源的副本保留一段时间,这段时间称为新鲜度限值。这在一段时间内请求相同资源不会再通过服务器。HTTP 协议中 Cache-ControlExpires 可以用来设置新鲜度的限值。

强缓存 ( Cache-Control 和 Expires )

强缓存主要是采用响应头中的 Cache-ControlExpires 两个字段进行控制的。

其中 ExpiresHTTP 1.0 中定义的,它指定了一个绝对的过期时期。而 Cache-ControlHTTP 1.1 时出现的缓存控制字段。

Cache-Control:max-age 定义了一个最大使用期。 就是从第一次生成文档到缓存不再生效的合法生存日期。由于Expires是HTTP1.0时代的产物,因此设计之初就存在着一些缺陷,如果本地时间和服务器时间相差太大,就会导致缓存错乱。

这两个字段同时使用的时候 Cache-Control 的优先级给更高一点。

这两个字段的效果是类似的,客户端都会通过对比本地时间和服务器生存时间来检测缓存是否可用。如果缓存没有超出它的生存时间内,客户端就会直接采用本地的缓存。如果生存日期已经过了,这个缓存也就宣告失效。接着客户端将再次与服务器进行通信来验证这个缓存是否需要更新

Cache-Control 通用消息头字段被用于在 http 请求和响应中通过指定指令来实现缓存机制。

可缓存性

  1. public:响应可以被任何对象(客户端、代理服务器等)缓存
  2. private:只能被单个用户缓存,不能作为共享缓存
  3. no-cache:使用缓存副本之前,需要将请求提交给原始服务器进行验证,验证通过才可以使用
  4. only-if-cached:客户端只接受已缓存的响应,并且不向原始服务器检查是否有更新的拷贝

到期

  1. max-age=<seconds>:缓存存储的最大周期,超过这个时间缓存被认为过期(单位秒)。与 Expires 相反,时间是相对于请求的时间
  2. s-maxage=<seconds>:覆盖 max-age 或者 Expires 头,但是仅适用于共享缓存(比如各个代理),并且私有缓存中它被忽略
  3. max-stale[=<seconds>]:表明客户端愿意接收一个已经过期的资源。可选的设置一个时间(单位秒),表示响应不能超过的过时时间
  4. min-fresh=<seconds>:表示客户端希望在指定的时间内获取最新的响应
重新验证和重新加载
  1. must-revalidate:缓存必须在使用之前验证旧资源的状态,并且不可使用过期资源。
  2. proxy-revalidate:与 must-revalidate 作用相同,但它仅适用于共享缓存(例如代理),并被私有缓存忽略

其他

  1. no-store:彻底得禁用缓冲,本地和代理服务器都不缓冲,每次都从服务器获取
  2. no-transform:不得对资源进行转换或转变。Content-Encoding, Content-Range, Content-TypeHTTP 头不能由代理修改。

示例:

// 禁止缓存:  
Cache-Control: no-cache, no-store, must-revalidate
// 缓存静态资源文件:  
Cache-Control:public, max-age=31536000

协商缓存 ( Last-Modified 和 Etag )

协商缓存机制下,浏览器需要向服务器去询问缓存的相关信息,进而判断是重新发起请求、下载完整的响应,还是从本地获取缓存的资源。
如果服务端提示缓存资源未改动(Not Modified),资源会被重定向到浏览器缓存,这种情况下网络请求对应的状态码是 304

Last-Modified 和 If-Modified-Since

基于资源在服务器修改时间的验证缓存过期机制

当客户端再次请求该资源的时候,会在其请求头上附带上 If-Modified-Since 字段,值就是第一次获取请求资源时响应头中返回的 Last-Modified 值。如果资源未过期,命中缓存,服务器就直接返回 304 状态码,客户端直接使用本地的资源。否则,服务器重新发送响应资源。从而保证资源的有效性。

Etag 和 If-None-Match

基于服务资源校验码的验证缓存过期机制

服务器返回的报文响应头的 Etag 字段标示服务器资源的校验码(例如文件的 hash 值),发送到客户端浏览器,浏览器收到后把资源文件缓存起来并且缓存 Etag 值,当浏览器再次请求此资源文件时,会在请求头 If-None-Match 字段带上缓存的 Etag 值。
服务器收到请求后,把请求头中 If-None-Match 字段值与服务器端资源文件的验证码进行对比,如果匹配成功直接返回 304 状态码,从浏览器本地缓存取资源文件。如果不匹配,服务器会把新的验证码放在请求头的 etag 字段中,并且以 200 状态码返回资源。

需要注意的是当响应头中同时存在 Etag 和 Last-Modified 的时候,会先对 Etag 进行比对,随后才是 Last-Modified

参考

  1. 《图解 HTTP》
  2. 《计算机网络基础》
  3. https://blog.csdn.net/qzcsu/article/details/72861891
  4. https://segmentfault.com/a/1190000011145364
  5. https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Caching_FAQ

最后

如果你想进【大前端交流群】,关注公众号点击“交流加群”添加机器人自动拉你入群。关注我第一时间接收最新干货。

这一年我有了妻儿却落了技术

前言

公元二零一八年,戊戌年,正是戊戌变法的那个戊戌年。

120 年前戊戌变法的发起人,同腐败堕落的晚清政府而斗争。虽说变法失败,可他们的**影响世人至今。伟人之所以伟大,应该是因为后人在想到他们的时候依旧肃然起敬。当下我的心情亦是如此。

戊戌年之回顾

今年于我意义特别

说意义特别,是因为今年多了两个需要我保护的可爱灵魂,一个是我可爱的妻子,一个是我即将来到这个世界的孩子。她们于我来说是一种不可比拟的温暖,我于她们来说同样也是依靠。

从此有了需要精心呵护的家庭,多了两种无可替代的思念,和一份充满期待的责任,我很满足。可是我的迷茫也随之而来,在往后的时光里我该怎样做好一个丈夫、如何去成为一个父亲、可以给她们一个怎样的生活……

我对自己说,未来可期。

今年是我毕业的第三年,虽说有两年多的工作经验。可我却非常的清楚在技术上我依旧是个菜鸟,情况好的话可能已经入门了。我非常感谢我的第一家公司。说来幸运吧,我是在加紧学了半个月前端后,面试的第一家公司就被录用了(建议多面试几家公司,手上拿几个 offer ,再做选择),我就没有继续面试了。也正是因为第一家公司我才从事前端的,然后就开始了我为期两年的菜鸟之旅,至今还没有结束。

这年我先后就职于两家公司

第一家主要用 Vue 全家桶系列做开发。由于业务相对简单,自我感觉没有什么进步。会的技术只是一些皮毛,放不上台面。所以在业余时间自己开始学习 Node, 开始有目的学习 Vue 的技术点。

第二家,也就是我现在就职的公司(九月份加入)。所用的技术主要是 Node + Vue。由于是电商网站,涉及的内容很多:首屏时间问题、性能问题、网络安全问题、数据采集等问题对我来说都是有挑战的,而且我对这份工作充满期待。

虽说我已经工作两年多了,但自己真的是一个刚刚入门的菜鸟这也是个不争的事实。现在想起八月份的几次面试的表现都心有余悸,很多简单的问题没有答好。

有一位面试官直接说:这些吃饭的家伙都不会,怎么行?

进入新公司后,察觉到了自己的差距在哪里,也在努力磨平自己当初欠下的技术债。今年在技术上没有什么进步,虽然说了解了一些知识,可习的东西却非常浅显。

年初定的计划年底复盘的时候又是没有完成一半,年年复年年,结果何其相似。

年初定的计划其中有如下几条

  1. 读 50 本书(最后读了 29 本)
  2. 一年爬10座山(最后爬了 4 座)
  3. 早睡早起(睡得早起得晚)
  4. 深入了解 Webpack (只会简单的使用)

…………

前后列举了 20 个目标。最终只有 9 项完成;4项完成 50%左右;7项基本没有完成。 年底复盘的时候自己都感到羞愧。

推荐大家读一下《万历十五年》《围城》《教父》《人生》《活着》《苏东坡 传》。在每一本书中你都可以体验一种你不曾体会的人生。相信我,你会爱上这种感觉。

新年的盼望

每个人在新年总是有盼望的,有期待的。同时信心满满的立一个年度 Flag 。结合当下的自身情况以及自我的期望,从以下五个维度列举期望:

一、学习

  1. 《前端词典》系列文章输出五十篇左右
  2. 了解小程序,并完成记账小程序的开发
  3. React Native开发一个记录时光的应用
  4. 学会使用思维导图并读不少于二十本书

二、生活

  1. 电脑桌前,养一盆金鱼,植几株多肉,观其春夏秋冬
  2. 早睡早起,起床于辰时,休息于亥时,愿其日日如此
  3. 轻重缓急,事物有轻重,缓急需分清,慢慢来比较快

三、健康

  1. 登山、跑步、少吃辣(10座山、500KM)
  2. 早九、晚六、少加班
  3. 霸王、枸杞、防脱发

四、情感

  1. 一起看场演唱会、走过一座城、感受春夏秋冬

五、工作

  1. 提前一刻赶到公司,准备好当天该做的事
  2. 认真的对待工作中的每一个人,每一件事

说个题外话

昨天在群里分享了年度总结,让大佬们批评来着。可是好些人都是在惊讶我怎么就结婚了,而且孩子都快出生了。如下:

其中有个问题让我觉得可以提一下。其实结婚之后,还是有个人安排时间,学习工作和家庭是不冲突。前提是你不能因为自己的工作学习把所有的家务丢给妻子。妻子嫁给你可不是只有给你洗衣做饭扫地洗碗的。学习是没有问题,家务同样也是要做。

希望 2019 大家共同进步,心想事成

以上

最后

如果你想进【大前端交流群】,关注公众号点击“交流加群”添加机器人自动拉你入群。关注我第一时间接收最新干货。

【前端词典】这些功能其实不需要 JS,CSS 就能搞定

前言

今天我们大家介绍一些你可能乍一眼以为一定需要 JavaScript 才能完成的功能,其实 CSS 就能完成,甚至更加简单。

内容已经发布在 gitHub 了,欢迎围观 Star,更多文章都在 gitHub。

直接入题

1. 每个单词的首字母大写

其实我第一次看到这个功能的时候就是使用 JS 去实现这个功能,想都没想 CSS 可以完成这个功能。马上就屁颠屁颠的写了一个方法:

function capitalizeFirst( str ) {
    let result = '';
    result = str.toLowerCase().replace(/( |^)[a-z]/g, (L) => L.toUpperCase());
    return result
}  

写完这个方法后,还有点小得意,也就没想其他方案。直到有一天看到 CSS 也能做这个功能的时候,我才反应过来明明一句 CSS 就能解决的问题,我却使用了更复杂的方案。

CSS 方案如下:

.capitalizeFirst-css {
    text-transform: capitalize;
}

是不是特别简单(代码在上面的 blog 仓库,访问 cssDo 路由便可,下面的案例都是这个路由下):

text-transform 简单介绍

这是 CSS2 中的属性,参数有 capitalize | uppercase | lowercase | none

参数介绍:

  1. none: 默认。定义带有小写字母和大写字母的标准的文本。
  2. capitalize: 文本中的每个单词以大写字母开头。
  3. uppercase: 定义仅有大写字母。
  4. lowercase: 定义无大写字母,仅有小写字母。

从这个属性我们可以知道全部大写(小写)的需求这个属性也能轻易实现。

2. 单选高亮

可能你看到“单选高亮”没反应过来,直接来张图片你就马上清楚了:

不知道你是否第一次看到这种单选高亮的需求时,是怎么处理的。我第一次直接是用 JS 控制的。后来我发现这个需求用 CSS 更方便处理。

主要代码就是一段 CSS 代码:

// 省略部分代码,详细代码看仓库
.input:checked + .colors {
  border-color: #e63838;
  color: #e63838;
}

<div class="single-check">
    <input class="input" type="radio" name="colors" value="1">
    <div class="colors">天空之境</div> 
</div>

看效果:

两个选择器的区别

~ 选择器:查找某个元素后面的所有兄弟元素

+ 选择器:查找某个元素后面紧邻的兄弟元素

扩展

其实这个技巧也完全可以使用在导航栏的交互效果,个人觉得可以简化一部分工作。

3、多列等高问题

之前做 pc 端的客户画像需求时,遇到需要左右两边等到的需求(左边块的高度会随着内容变化)。

最初我使用的 JS 计算高度再赋值,可是这样会有页面闪动的效果。所以找到了两种 CSS 的处理方案:

  1. 每列设置一个很大的 padding,再设置一个很大的负的 margin
  2. 使用 display: table;

第一种有明显的缺陷:

  1. border-bottom 看不到了
  2. 设置的下方的两个圆角也不见了

所以我使用了 display: table; 的方式来实现等高,可以说非常的方便。

建议不要一味的抵触 table,有的场景还是可以使用的。

4、表单验证

先声明:这里没有用到 JS,不过用到了 HTML5 关于 <input> 的新属性 —— pattern( 检查控件值的正则表达式 )。
还有一点:其实我在实际项目中没这么用过。

代码如下:

input[type="text"]:invalid ~ input[type="submit"] {
    display: none
}

<div class="form-css">
    <input type="text" name="tel" placeholder="输入手机号码" pattern="^1[3456789]\d{9}$" required><br>
    <input type="text" name="smscode" placeholder="输入验证码" pattern="\d{4}" required><br>
    <input type="submit" ></input>
</div>

效果如下(样式不好看的问题请忽略):

invalid 伪类和 vaild 伪类

  • valid 伪类,匹配通过 pattern 验证的元素
  • invalid 伪类,匹配未通过 pattern 验证的元素

后记

还有一些大家比较常用的这里就不介绍了,周三愉快。

最后

你可以关注我的同名公众号【小生方勤】,这里我会分享优质的文章,我们一同进步。

如果你想进【大前端交流群】,点击加群交流即刻加入。

【前端词典】几个有益的 CSS 小知识

前言

今天偷个懒,不长篇大论,分享几个你可能不知道的 CSS 小知识。

样式的顺序

CSS 代码:

.red {
    color: red;
}
.blue {
    color: blue;
}

HTML 代码:

<div class="red blue">这是什么颜色</div>
<div class="blue red">这是什么颜色</div>

记得之前这是一道比较火的 CSS 考题,当时好像是有不少的人答错(30% 以上)

答案你们应该是知道的。

可以这样提升 CSS 性能

后代选择器

样式选择器中间的空格是什么?它的名字是 —— 后代选择器

div p {
    color: #3388ff;
    font-size: 14px;
}

后代选择器或许会很耗性能

耗能与否取决于项目的体积,但不建议使用没有意义的后代选择器。例如:

.div p {
    // ...
}

为什么会更消耗性能呢?

因为浏览器首先会找到所有 p 标签,然后再向上查找包含 classdiv 标签。这样一来如果代码中有很多 p 标签,无疑是会做很多重复工作的。

所以可以减少使用 HTML 标签来定义 CSS 的方式,换成使用具体的 class

浏览器会从右到左解析 CSS 选择器

.content_box div p a {
    // ...
}

浏览器会对上面的例子做如下的步骤处理:

  1. 首先找到页面所有的 <a> 元素
  2. 然后向上找到被 <p> 元素包裹的 <a> 元素
  3. 再向上查找到一直到 .content_box 的元素

从上面的步骤我们可以看出,越靠右的选择器越具有唯一性,浏览器解析 CSS 属性的效率就越高。

所以一定换成使用具体的 class 编写 CSS 代码。

避免 reflow 风险

我们知道修改某些 CSS 属性会导致整个页面布局的重绘( repaint )/重排( reflow )。

repaint 的速度远快于 reflow,所以避免 reflow 更重要

导致 repaint 和 reflow 的原因

  1. DOM 元素的添加、修改、删除(repaint、reflow)
  2. 仅仅修改 DOM 元素的字体颜色(repaint,不需要调整布局)
  3. 应用新的样式或者修改任何影响元素外观的属性(repaint、reflow)
  4. resize,页面滚动(repaint、reflow)
  5. 读取元素的某些属性(offsetTop/Left/Width/Height、getComputedStyle、scrollTop/Left/Width/Height、clientTop/Left/Width/Height等)(repaint、reflow)

如果在大量的元素上更改这些属性,那么计算和更新他们的位置/大小需要花费很长的时间。

更加消耗性能的 CSS 属性

有一些 CSS 属性会比其他属性消耗能多的性能,即浏览器解析这些属性需要花费更多的时间。

如:border-radiusbox-shadowfilter:nth-child

当然这些属性我们经常使用,有些无法避免。要做出适当的取舍。

希望这几个 CSS 小知识可以对你有所帮助,然后点个赞在走呗。

最后

如果你想进【大前端交流群】,关注公众号点击“交流加群”添加机器人自动拉你入群。关注我第一时间接收最新干货。

【前端词典】CDN 带来这些性能优化

前言

CDN的全称是 Content Delivery Network,即内容分发网络。CDN 是构建在网络之上的内容分发网络,依靠部署在各地的边缘服务器,通过中心平台的负载均衡、内容分发、调度等功能模块,使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。CDN 的关键技术主要有内容存储和分发技术。—— 科学百科

CDN 存在的意义:

为了不让网络拥塞成为互联网发展的障碍。

举个例子

在讲 CDN 之前我们先看个生活实例:

在淘宝购物

我们在淘宝购物,大部分个人卖家只是在一个地方发货,江浙沪以外的地方好像收货都比较慢。

在京东购物

而我们在京东上买自营产品的话,它会根据我们的收货地点,在全国范围内找离我们最近、送达最快的仓库,不管我们在江浙沪,还是**西藏内蒙古,我们的收货时间都会大大减少。京东的物流体系就类似于 CDN。

从上面这个例子我们可以大致了解 CDN 的基本原理,即将相关静态资源放在各地的 CDN 服务器。

CDN 的优势

  1. CDN 节点解决了跨运营商和跨地域访问的问题,访问延时大大降低;
  2. 大部分请求在 CDN 边缘节点完成,CDN 起到了分流作用,减轻了源站的负载;
  3. 降低“广播风暴”的影响,提高网络访问的稳定性;节省骨干网带宽,减少带宽需求量。

访问速度快是电商网站取胜的必要法宝之一。

CDN 的核心点有两个: 一个是缓存,一个是回源。

缓存

将从根服务器请求来的资源按要求缓存。

回源

当有用户访问某个资源的时候,如果被解析到的那个 CDN 节点没有缓存响应的内容,或者是缓存已经到期,就会回源站去获取。没有人访问,CDN 节点不会主动去源站请求资源。

关键技术

  1. 内容发布:它借助于建立索引、缓存、流分裂、组播(Multicast)等技术,将内容发布或投递到距离用户最近的远程服务点(POP)处;
  2. 内容路由:它是整体性的网络负载均衡技术,通过内容路由器中的重定向(DNS)机制,在多个远程 POP 上均衡用户的请求,以使用户请求得到最近内容源的响应;
  3. 内容交换:它根据内容的可用性、服务器的可用性以及用户的背景,在POP的缓存服务器上,利用应用层交换、流分裂、重定向(ICP、WCCP)等技术,智能地平衡负载流量;
  4. 性能管理:它通过内部和外部监控系统,获取网络部件的状况信息,测量内容发布的端到端性能(如包丢失、延时、平均带宽、启动时间、帧速率等),保证网络处于最佳的运行状态。

前端往往认为 CDN 是不需要了解的知识。可是我们前端工程首先是软件工程师。多了解一些东西肯定是有益的。

CDN & 静态资源

静态资源本身具有访问频率高、承接流量大的特点,因此静态资源加载速度始终是前端性能的一个非常关键的指标。CDN 是静态资源提速的重要手段。

淘宝

京东

掘金

我们随手打开一个网站点开一个静态资源,可以看到它都是从 CDN 服务器上请求来的。可以看出 "静态资源走 CDN" 是最佳实践。

CDN & Cookie

我们知道 Cookie 和域名是紧密联系的。即同一个域名下的所有请求,都会携带一个相同的 Cookie(设置不当就会非常大)。

静态资源往往并不需要 Cookie

携带的认证信息等,静态资源一般是不需要的。把静态资源和主页面置于不同的域名下,就可以完美地避免请求中携带不必要的 Cookie。

这也是 CDN 带来的另一个好处。

最后给大家放一张直观图,给大家对比下 CDN 服务部署前后的区别:

最后

如果你想进【大前端交流群】,关注公众号点击“交流加群”添加机器人自动拉你入群。关注我第一时间接收最新干货。

【前端词典】继承(二) - 回的八种写法

前言

上一篇我讲了下继承的基础知识-原型和原型链。看到有人读完我的技术分享后而有所得,我很开心;看到有人提意见我也虚心接受。

在讲继承的几种方式前我打算先说一下——《孔乙己》。

《孔乙己》一文中我印象最深的是孔己乙的一个动作和一句对白一个提问。

一个动作:排出九文大钱
一句对白:窃书不能算偷……读书人的事,能算偷么
一个提问:回香豆的回字,怎样写的

孔乙己这种深受科举教育毒害的读书人,常会注意一些没有用的字,而且把这看成学问和本领。会‘回’的几种写法就是有本领吗?

我正思考这个问题时。好像有一个面试官在回答: 会‘回’的几种写法是不是本领我不清楚,不过我想知道你会几种继承的写法。

So 正月初七开工大吉,了解继承的几种方式,不失为一种有趣的迎新方式。

入题

JavaScript继承是非常重要的一个概念。我们有必要去了解,请大家多指教。

目的:简化代码逻辑和结构,实现代码重用

接下来我们一起学习下 8 种 JavaScript 实现继承的方法。

继承的实现

推荐组合继承(四)、寄生组合式继承(七)、ES6 继承(八)

一、原型链法(使用原型)

基本**是利用原型让一个引用类型继承另一个引用类型的方法和实例。

代码如下

function staff(){ 
  this.company = 'ABC';
}
staff.prototype.companyName = function(){
  return this.company; 
}
function employee(name,profession){
  this.employeeName = name;
  this.profession = profession;
}
// 继承 staff
employee.prototype = new staff();
// 将这个对象的 constructor 手动改成 employee,否则还会是 staff
employee.prototype.constructor = employee;
// 不使用对象字面量方式创建原型方法,会重写原型链
employee.prototype.showInfo = function(){
  return this.employeeName + "'s profession is " + this.profession;
}
let instance = new employee('Andy','front-end');

// 测试 
console.log(instance.companyName()); // ABC
console.log(instance.showInfo());    // "Andy's profession is front-end"
// 通过 hasOwnProperty() 方法来确定自身属性与其原型属性
console.log(instance.hasOwnProperty('employeeName'))     // true
console.log(instance.hasOwnProperty('company'))          // false
// 通过 isPrototypeOf() 方法来确定原型和实例的关系
console.log(employee.prototype.isPrototypeOf(instance)); // true
console.log(staff.prototype.isPrototypeOf(instance));    // true
console.log(Object.prototype.isPrototypeOf(instance));   // true

存在的问题

原型链实现继承最大的问题是:

当原型中存在引用类型值时,实例可以修改其值。

function staff(){ 
  this.test = [1,2,3,4];
}
function employee(name,profession){
  this.employeeName = name;
  this.profession = profession;
}
employee.prototype = new staff();
let instanceOne = new employee();
let instanceTwo = new employee();
instanceOne.test.push(5);
console.log(instanceTwo.test); // [1, 2, 3, 4, 5]

鉴于此问题:所以我们在实践中会少单独使用原型链实现继承。

小结

  1. 基于构造函数和原型链
  2. 通过 hasOwnProperty() 方法来确定自身属性与其原型属性
  3. 通过 isPrototypeOf() 方法来确定原型和实例的关系
  4. 在实例中可以修改原型中引用类型的值

二.仅继承父构造函数的原型对象

此方法和方法一区别就是将:

employee.prototype = new staff();

改成:

Employee.prototype = Person.prototype;

优点

  1. 构建继承关系时不需要新建对象实例
  2. 由于公用一个原型对象,所以在访问对象的时候不需要遍历原型链,效率自然就高

缺点

  1. 和方法一相同,子对象的修改会影响父对象。

小结

  1. 基于构造函数,没有使用原型链
  2. 子对象和父对象公用一个原型对象

三、借用构造函数法

此方法可以解决原型中引用类型值被修改的问题

function staff(){ 
  this.test = [1,2,3];
}
staff.prototype.companyName = function(){
  return this.company; 
}
function employee(name,profession){
  staff.call(this);	
  this.employeeName = name;
  this.profession = profession;
}
// 不使用对象字面量方式创建原型方法,会重写原型链
employee.prototype.showInfo = function(){
  return this.employeeName + "'s profession is " + this.profession;
}
let instanceOne = new employee('Andy','front-end');
let instanceTwo = new employee('Mick','after-end');
instanceOne.test.push(4);
// 测试 
console.log(instanceTwo.test);    // [1,2,3]
// console.log(instanceOne.companyName()); // 报错
// 通过 hasOwnProperty() 方法来确定自身属性与其原型属性
console.log(instanceOne.hasOwnProperty('test'))          // true
// 通过 isPrototypeOf() 方法来确定原型和实例的关系
console.log(staff.prototype.isPrototypeOf(instanceOne));    // false

从上面的结果可以看出:

  1. 借用构造函数法可以解决原型中引用类型值被修改的问题
  2. 可是 instanceOnestaff 已经没有原型链的关系了

缺点

  1. 只能继承父对象的实例属性和方法,不能继承父对象原型属性和方法
  2. 无法实现函数复用,每个子对象都有父对象实例的副本,性能欠优

四、组合继承(推荐)

指的是将原型链技术和借用构造函数技术结合起来,二者皆取其长处的一种经典继承方式。

function staff(){ 
  this.company = "ABC";	
  this.test = [1,2,3];
}
staff.prototype.companyName = function(){
  return this.company; 
}
function employee(name,profession){
  // 继承属性
  staff.call(this);	
  this.employeeName = name;
  this.profession = profession;
}
// 继承方法
employee.prototype = new staff();
employee.prototype.constructor = employee;
employee.prototype.showInfo = function(){
  return this.employeeName + "'s profession is " + this.profession;
}

let instanceOne = new employee('Andy','front-end');
let instanceTwo = new employee('Mick','after-end');
instanceOne.test.push(4);
// 测试 
console.log(instanceTwo.test);    // [1,2,3]
console.log(instanceOne.companyName()); // ABC
// 通过 hasOwnProperty() 方法来确定自身属性与其原型属性
console.log(instanceOne.hasOwnProperty('test'))          // true
// 通过 isPrototypeOf() 方法来确定原型和实例的关系
console.log(staff.prototype.isPrototypeOf(instanceOne));    // true

优点

  1. 可以复用原型上定义的方法
  2. 可以保证每个函数有自己的属性,可以解决原型中引用类型值被修改的问题

缺点

  1. staff 会被调用 2 次:第 1 次是employee.prototype = new staff();,第 2 次是调用 staff.call(this)

五、原型式继承 - Object.create()

利用一个临时性的构造函数(空对象)作为中介,将某个对象直接赋值给构造函数的原型。

function object(obj){
  function F(){}
  F.prototype = obj;
  return new F();
}

本质上 object() 对传入其中的对象执行了一次浅复制,将构造函数 F 的原型直接指向传入的对象。

var employee = {
  test: [1,2,3]
}

let instanceOne = object(employee);
let instanceTwo = object(employee);
// 测试 
instanceOne.test.push(4);
console.log(instanceTwo.test); // [1, 2, 3, 4]

缺点

  1. 原型中引用类型值会被修改
  2. 无法传递参数

另,ES5 中存在 Object.create() 的方法规范化了原型式继承,能够代替 object 方法。

六、寄生式继承

要点:在原型式继承的基础上,通过封装继承过程的函数增强对象,返回对象

function createAnother(original){
  var clone = object(original); // 通过调用 object() 函数创建一个新对象
  clone.sayHi = function(){  // 以某种方式来增强对象
    alert("hi");
  };
  return clone; // 返回这个对象
}

createAnother 函数的主要作用是为构造函数新增属性和方法,以增强函数。

缺点(同原型式继承):

  1. 原型中引用类型值会被修改
  2. 无法传递参数

七、寄生组合式继承(推荐)

该方法主要是解决组合继承调用两次超类构造函数的问题。

function inheritPrototype(sub, super){
  var prototype = Object.create(super.prototype); // 创建对象,父原型的副本
  prototype.constructor = sub;                    // 增强对象
  sub.prototype = prototype;                      // 指定对象,赋给子的原型
}

function staff(){ 
  this.company = "ABC";	
  this.test = [1,2,3];
}
staff.prototype.companyName = function(){
  return this.company; 
}
function employee(name,profession){
  staff.call(this, name);
  this.employeeName = name;
  this.profession = profession;
}

// 将父类原型指向子类
inheritPrototype(employee,staff)
let instanceOne = new employee("Andy", "A");
let instanceTwo = new employee("Rose", "B");
instanceOne.test.push(4);
// 测试 
console.log(instanceTwo.test);            // [1,2,3]
console.log(instanceOne.companyName());   // ABC
// 通过 hasOwnProperty() 方法来确定自身属性与其原型属性
console.log(instanceOne.hasOwnProperty('test'))           // true
// 通过 isPrototypeOf() 方法来确定原型和实例的关系
console.log(staff.prototype.isPrototypeOf(instanceOne));  // true
开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式,

八、Class 的继承(推荐)

Class 可以通过 extends 关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。

class staff { 
  constructor(){
    this.company = "ABC";	
    this.test = [1,2,3];
  }
  companyName(){
    return this.company; 
  }
}
class employee extends staff {
  constructor(name,profession){
    super();
    this.employeeName = name;
    this.profession = profession;
  }
}

// 将父类原型指向子类
let instanceOne = new employee("Andy", "A");
let instanceTwo = new employee("Rose", "B");
instanceOne.test.push(4);
// 测试 
console.log(instanceTwo.test);    // [1,2,3]
console.log(instanceOne.companyName()); // ABC
// 通过 Object.getPrototypeOf() 方法可以用来从子类上获取父类
console.log(Object.getPrototypeOf(employee) === staff)
// 通过 hasOwnProperty() 方法来确定自身属性与其原型属性
console.log(instanceOne.hasOwnProperty('test'))          // true
// 通过 isPrototypeOf() 方法来确定原型和实例的关系
console.log(staff.prototype.isPrototypeOf(instanceOne));    // true

super 关键字,它在这里表示父类的构造函数,用来新建父类的 this 对象。

  1. 子类必须在 constructor 方法中调用 super 方法,否则新建实例时会报错。这是因为子类没有自己的 this 对象,而是继承父类的 this 对象,然后对其进行加工。
  2. 只有调用 super 之后,才可以使用 this 关键字,否则会报错。这是因为子类实例的构建,是基于对父类实例加工,只有 super 方法才能返回父类实例。
`super` 虽然代表了父类 `A` 的构造函数,但是返回的是子类 `B` 的实例,即` super` 内部的 `this ` 指的是 `B`,因此 `super()` 在这里相当于

A.prototype.constructor.call(this)

ES5 和 ES6 实现继承的区别

ES5 的继承,实质是先创造子类的实例对象 this,然后再将父类的方法添加到 this 上面(Parent.apply(this))。
ES6 的继承机制完全不同,实质是先创造父类的实例对象 this (所以必须先调用 super() 方法),然后再用子类的构造函数修改 this

extends 继承核心代码(寄生组合式继承)

function _inherits(subType, superType) {
  subType.prototype = Object.create(superType && superType.prototype, {
    constructor: {
      value: subType,
      enumerable: false,
      writable: true,
      configurable: true
    }
  });
  if (superType) {
    Object.setPrototypeOf 
    ? Object.setPrototypeOf(subType, superType) 
    : subType.__proto__ = superType;
  }
}

由此可以看出:

  1. 子类的 __proto__ 属性,表示构造函数的继承,总是指向父类。
  2. 子类 prototype 属性的 __proto__ 属性,表示方法的继承,总是指向父类的 prototype 属性。
另:ES6 可以自定义原生数据结构(比如Array、String等)的子类,这是 ES5 无法做到的。

以上八种继承方式是比较常见的继承方式,倘若了解了这些方式的机制,在以后的面试中原型链与继承的问题也就不在话下了。

参考

  1. 《JavaScript 高级程序设计》
  2. http://es6.ruanyifeng.com/#docs/class-extends

后记

前后写了两个多星期,最主要的原因宝宝刚进入我的生活,无休的照顾宝宝,换尿布、喂奶、换衣之类花费了大量精力和时间。这篇文章也是在宝宝睡觉的间隙写成的,文章的内容如果觉得简陋,也请大家多包涵,提出宝贵的意见,日后有时间一定修改。

新年伊始,不忘初心

最后

如果你想进【大前端交流群】,关注公众号点击“交流加群”添加机器人自动拉你入群。关注我第一时间接收最新干货。

全网最优惠的极客教程

为了回馈读者特意向极客时间申请了 200 份 199 元优惠礼包。

优惠券领取链接:http://gk.link/a/104VQ

优惠礼包券有:

满199减54 * 1
满129减30 * 1
满99减25 * 2
满68减10 * 2
极客时间每日一课的免费月卡一张(原价 45 元)

有需要的自取哟!

推荐一个 TypeScript 视频课:

61455015-03c56480-a995-11e9-9c3d-2a005ff4715a

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.