GithubHelp home page GithubHelp logo

ljianshu / blog Goto Github PK

View Code? Open in Web Editor NEW
7.8K 442.0 1.2K 3.98 MB

关注基础知识,打造优质前端博客,公众号[前端工匠]的作者

Vue 7.43% JavaScript 71.11% HTML 19.21% CSS 2.25%
vue javascript css ajax

blog's Introduction

博客目录

努力打造一系列适合初中级工程师能够看得懂的优质文章,今年博客侧重于框架、TS和构建工具等底层原理分析,如果想第一时间获取文章,欢迎关注我的公众号:前端工匠,接下去的路我们一起走! 欢迎添加我的个人微信frontJS,获取大厂面试题及其答案

版权声明:本文为博主原创文章,未经博主允许不得转载。

| 浏览器相关

| Javascript

| ES6+

| CSS

| Http协议与数据请求

| 页面性能优化与安全

| Vue全家桶

| 工具

| 面试

| 其他

| 备战大厂面试

  • 大厂面试题及其答案(四份PDF囊括上千经典题)
  • vue/react/webpack源码视频深度讲解
  • JavaScript/vue 常见面试题视频讲解
  • JavaScript版数据结构与算法视频资料
  • 简历模版等等

感兴趣的扫描下方微信二维码(frontJS)免费找我领取,备注:git,希望对你们有些许帮助!有时候图片显示不出来,直接加frontJS

image.png

blog's People

Contributors

ljianshu 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  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

blog's Issues

vue组件间通信六种方式(完整版)

前言

组件是 vue.js最强大的功能之一,而组件实例的作用域是相互独立的,这就意味着不同组件之间的数据无法相互引用。一般来说,组件可以有以下几种关系:
image

如上图所示,A 和 B、B 和 C、B 和 D 都是父子关系,C 和 D 是兄弟关系,A 和 C 是隔代关系(可能隔多代)。

针对不同的使用场景,如何选择行之有效的通信方式?这是我们所要探讨的主题。本文总结了vue组件间通信的几种方式,如props、$emit/$on、vuex、$parent / $children$attrs/$listeners和provide/inject,以通俗易懂的实例讲述这其中的差别及使用场景,希望对小伙伴有些许帮助。

本文的代码请猛戳github博客,纸上得来终觉浅,大家动手多敲敲代码!

方法一、props/$emit

父组件A通过props的方式向子组件B传递,B to A 通过在 B 组件中 $emit, A 组件中 v-on 的方式实现。

1.父组件向子组件传值

接下来我们通过一个例子,说明父组件如何向子组件传递值:在子组件Users.vue中如何获取父组件App.vue中的数据 users:["Henry","Bucky","Emily"]

//App.vue父组件
<template>
  <div id="app">
    <users v-bind:users="users"></users>//前者自定义名称便于子组件调用,后者要传递数据名
  </div>
</template>
<script>
import Users from "./components/Users"
export default {
  name: 'App',
  data(){
    return{
      users:["Henry","Bucky","Emily"]
    }
  },
  components:{
    "users":Users
  }
}
//users子组件
<template>
  <div class="hello">
    <ul>
      <li v-for="user in users">{{user}}</li>//遍历传递过来的值,然后呈现到页面
    </ul>
  </div>
</template>
<script>
export default {
  name: 'HelloWorld',
  props:{
    users:{           //这个就是父组件中子标签自定义名字
      type:Array,
      required:true
    }
  }
}
</script>

总结:父组件通过props向下传递数据给子组件。注:组件中的数据共有三种形式:data、props、computed

2.子组件向父组件传值(通过事件形式)

接下来我们通过一个例子,说明子组件如何向父组件传递值:当我们点击“Vue.js Demo”后,子组件向父组件传递值,文字由原来的“传递的是一个值”变成“子向父组件传值”,实现子组件向父组件值的传递。

子组件向父组件传值之前

// 子组件
<template>
  <header>
    <h1 @click="changeTitle">{{title}}</h1>//绑定一个点击事件
  </header>
</template>
<script>
export default {
  name: 'app-header',
  data() {
    return {
      title:"Vue.js Demo"
    }
  },
  methods:{
    changeTitle() {
      this.$emit("titleChanged","子向父组件传值");//自定义事件  传递值“子向父组件传值”
    }
  }
}
</script>
// 父组件
<template>
  <div id="app">
    <app-header v-on:titleChanged="updateTitle" ></app-header>//与子组件titleChanged自定义事件保持一致
   // updateTitle($event)接受传递过来的文字
    <h2>{{title}}</h2>
  </div>
</template>
<script>
import Header from "./components/Header"
export default {
  name: 'App',
  data(){
    return{
      title:"传递的是一个值"
    }
  },
  methods:{
    updateTitle(e){   //声明这个函数
      this.title = e;
    }
  },
  components:{
   "app-header":Header,
  }
}
</script>

总结:子组件通过events给父组件发送消息,实际上就是子组件把自己的数据发送到父组件。

方法二、$emit/$on

这种方法通过一个空的Vue实例作为**事件总线(事件中心),用它来触发事件和监听事件,巧妙而轻量地实现了任何组件间的通信,包括父子、兄弟、跨级。当我们的项目比较大时,可以选择更好的状态管理解决方案vuex。

1.具体实现方式:

    var Event=new Vue();
    Event.$emit(事件名,数据);
    Event.$on(事件名,data => {});

2.举个例子

假设兄弟组件有三个,分别是A、B、C组件,C组件如何获取A或者B组件的数据

<div id="itany">
	<my-a></my-a>
	<my-b></my-b>
	<my-c></my-c>
</div>
<template id="a">
  <div>
    <h3>A组件:{{name}}</h3>
    <button @click="send">将数据发送给C组件</button>
  </div>
</template>
<template id="b">
  <div>
    <h3>B组件:{{age}}</h3>
    <button @click="send">将数组发送给C组件</button>
  </div>
</template>
<template id="c">
  <div>
    <h3>C组件:{{name}},{{age}}</h3>
  </div>
</template>
<script>
var Event = new Vue();//定义一个空的Vue实例
var A = {
	template: '#a',
	data() {
	  return {
	    name: 'tom'
	  }
	},
	methods: {
	  send() {
	    Event.$emit('data-a', this.name);
	  }
	}
}
var B = {
	template: '#b',
	data() {
	  return {
	    age: 20
	  }
	},
	methods: {
	  send() {
	    Event.$emit('data-b', this.age);
	  }
	}
}
var C = {
	template: '#c',
	data() {
	  return {
	    name: '',
	    age: ""
	  }
	},
	mounted() {//在模板编译完成后执行
	 Event.$on('data-a',name => {
	     this.name = name;//箭头函数内部不会产生新的this,这边如果不用=>,this指代Event
	 })
	 Event.$on('data-b',age => {
	     this.age = age;
	 })
	}
}
var vm = new Vue({
	el: '#itany',
	components: {
	  'my-a': A,
	  'my-b': B,
	  'my-c': C
	}
});	
</script>

image
$on 监听了自定义事件 data-a和data-b,因为有时不确定何时会触发事件,一般会在 mounted 或 created 钩子中来监听。

方法三、vuex

image

1.简要介绍Vuex原理

Vuex实现了一个单向数据流,在全局拥有一个State存放数据,当组件要更改State中的数据时,必须通过Mutation进行,Mutation同时提供了订阅者模式供外部插件调用获取State数据的更新。而当所有异步操作(常见于调用后端接口异步获取更新数据)或批量的同步操作需要走Action,但Action也是无法直接修改State的,还是需要通过Mutation来修改State的数据。最后,根据State的变化,渲染到视图上。

2.简要介绍各模块在流程中的功能:

  • Vue Components:Vue组件。HTML页面上,负责接收用户操作等交互行为,执行dispatch方法触发对应action进行回应。
  • dispatch:操作行为触发方法,是唯一能执行action的方法。
  • actions:操作行为处理模块,由组件中的$store.dispatch('action 名称', data1)来触发。然后由commit()来触发mutation的调用 , 间接更新 state。负责处理Vue Components接收到的所有交互行为。包含同步/异步操作,支持多个同名方法,按照注册的顺序依次触发。向后台API请求的操作就在这个模块中进行,包括触发其他action以及提交mutation的操作。该模块提供了Promise的封装,以支持action的链式触发。
  • commit:状态改变提交操作方法。对mutation进行提交,是唯一能执行mutation的方法。
  • mutations:状态改变操作方法,由actions中的commit('mutation 名称')来触发。是Vuex修改state的唯一推荐方法。该方法只能进行同步操作,且方法名只能全局唯一。操作之中会有一些hook暴露出来,以进行state的监控等。
  • state:页面状态管理容器对象。集中存储Vue components中data对象的零散数据,全局唯一,以进行统一的状态管理。页面显示所需的数据从该对象中进行读取,利用Vue的细粒度数据响应机制来进行高效的状态更新。
  • getters:state对象读取方法。图中没有单独列出该模块,应该被包含在了render中,Vue Components通过该方法读取全局state对象。

3.Vuex与localStorage

vuex 是 vue 的状态管理器,存储的数据是响应式的。但是并不会保存起来,刷新之后就回到了初始状态,具体做法应该在vuex里数据改变的时候把数据拷贝一份保存到localStorage里面,刷新之后,如果localStorage里有保存的数据,取出来再替换store里的state。

let defaultCity = "上海"
try {   // 用户关闭了本地存储功能,此时在外层加个try...catch
  if (!defaultCity){
    defaultCity = JSON.parse(window.localStorage.getItem('defaultCity'))
  }
}catch(e){}
export default new Vuex.Store({
  state: {
    city: defaultCity
  },
  mutations: {
    changeCity(state, city) {
      state.city = city
      try {
      window.localStorage.setItem('defaultCity', JSON.stringify(state.city));
      // 数据改变的时候把数据拷贝一份保存到localStorage里面
      } catch (e) {}
    }
  }
})

这里需要注意的是:由于vuex里,我们保存的状态,都是数组,而localStorage只支持字符串,所以需要用JSON转换:

JSON.stringify(state.subscribeList);   // array -> string
JSON.parse(window.localStorage.getItem("subscribeList"));    // string -> array 

方法四、$attrs/$listeners

1.简介

多级组件嵌套需要传递数据时,通常使用的方法是通过vuex。但如果仅仅是传递数据,而不做中间处理,使用 vuex 处理,未免有点大材小用。为此Vue2.4 版本提供了另一种方法----$attrs/$listeners

  • $attrs:包含了父作用域中不被 prop 所识别 (且获取) 的特性绑定 (class 和 style 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过 v-bind="$attrs" 传入内部组件。通常配合 inheritAttrs 选项一起使用。

  • $listeners:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件

接下来我们看个跨级通信的例子:

// index.vue
<template>
  <div>
    <h2>浪里行舟</h2>
    <child-com1
      :foo="foo"
      :boo="boo"
      :coo="coo"
      :doo="doo"
      title="前端工匠"
    ></child-com1>
  </div>
</template>
<script>
const childCom1 = () => import("./childCom1.vue");
export default {
  components: { childCom1 },
  data() {
    return {
      foo: "Javascript",
      boo: "Html",
      coo: "CSS",
      doo: "Vue"
    };
  }
};
</script>
// childCom1.vue
<template class="border">
  <div>
    <p>foo: {{ foo }}</p>
    <p>childCom1的$attrs: {{ $attrs }}</p>
    <child-com2 v-bind="$attrs"></child-com2>
  </div>
</template>
<script>
const childCom2 = () => import("./childCom2.vue");
export default {
  components: {
    childCom2
  },
  inheritAttrs: false, // 可以关闭自动挂载到组件根元素上的没有在props声明的属性
  props: {
    foo: String // foo作为props属性绑定
  },
  created() {
    console.log(this.$attrs); // { "boo": "Html", "coo": "CSS", "doo": "Vue", "title": "前端工匠" }
  }
};
</script>
// childCom2.vue
<template>
  <div class="border">
    <p>boo: {{ boo }}</p>
    <p>childCom2: {{ $attrs }}</p>
    <child-com3 v-bind="$attrs"></child-com3>
  </div>
</template>
<script>
const childCom3 = () => import("./childCom3.vue");
export default {
  components: {
    childCom3
  },
  inheritAttrs: false,
  props: {
    boo: String
  },
  created() {
    console.log(this.$attrs); // { "coo": "CSS", "doo": "Vue", "title": "前端工匠" }
  }
};
</script>
// childCom3.vue
<template>
  <div class="border">
    <p>childCom3: {{ $attrs }}</p>
  </div>
</template>
<script>
export default {
  props: {
    coo: String,
    title: String
  }
};
</script>

image
如上图所示$attrs表示没有继承数据的对象,格式为{属性名:属性值}。Vue2.4提供了$attrs , $listeners 来传递数据与事件,跨级组件之间的通讯变得更简单。

简单来说:$attrs$listeners 是两个对象,$attrs 里存放的是父组件中绑定的非 Props 属性,$listeners里存放的是父组件中绑定的非原生事件。

方法五、provide/inject

1.简介

Vue2.2.0新增API,这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。一言而蔽之:祖先组件中通过provider来提供变量,然后在子孙组件中通过inject来注入变量。
provide / inject API 主要解决了跨级组件间的通信问题,不过它的使用场景,主要是子组件获取上级组件的状态,跨级组件间建立了一种主动提供与依赖注入的关系

2.举个例子

假设有两个组件: A.vue 和 B.vue,B 是 A 的子组件

// A.vue
export default {
  provide: {
    name: '浪里行舟'
  }
}
// B.vue
export default {
  inject: ['name'],
  mounted () {
    console.log(this.name);  // 浪里行舟
  }
}

可以看到,在 A.vue 里,我们设置了一个 provide: name,值为 浪里行舟,它的作用就是将 name 这个变量提供给它的所有子组件。而在 B.vue 中,通过 inject 注入了从 A 组件中提供的 name 变量,那么在组件 B 中,就可以直接通过 this.name 访问这个变量了,它的值也是 浪里行舟。这就是 provide / inject API 最核心的用法。

需要注意的是:provide 和 inject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的----vue官方文档
所以,上面 A.vue 的 name 如果改变了,B.vue 的 this.name 是不会改变的,仍然是 浪里行舟。

3.provide与inject 怎么实现数据响应式

一般来说,有两种办法:

  • provide祖先组件的实例,然后在子孙组件中注入依赖,这样就可以在子孙组件中直接修改祖先组件的实例的属性,不过这种方法有个缺点就是这个实例上挂载很多没有必要的东西比如props,methods
  • 使用2.6最新API Vue.observable 优化响应式 provide(推荐)

我们来看个例子:孙组件D、E和F获取A组件传递过来的color值,并能实现数据响应式变化,即A组件的color变化后,组件D、E、F会跟着变(核心代码如下:)

image

// A 组件 
<div>
      <h1>A 组件</h1>
      <button @click="() => changeColor()">改变color</button>
      <ChildrenB />
      <ChildrenC />
</div>
......
  data() {
    return {
      color: "blue"
    };
  },
  // provide() {
  //   return {
  //     theme: {
  //       color: this.color //这种方式绑定的数据并不是可响应的
  //     } // 即A组件的color变化后,组件D、E、F不会跟着变
  //   };
  // },
  provide() {
    return {
      theme: this//方法一:提供祖先组件的实例
    };
  },
  methods: {
    changeColor(color) {
      if (color) {
        this.color = color;
      } else {
        this.color = this.color === "blue" ? "red" : "blue";
      }
    }
  }
  // 方法二:使用2.6最新API Vue.observable 优化响应式 provide
  // provide() {
  //   this.theme = Vue.observable({
  //     color: "blue"
  //   });
  //   return {
  //     theme: this.theme
  //   };
  // },
  // methods: {
  //   changeColor(color) {
  //     if (color) {
  //       this.theme.color = color;
  //     } else {
  //       this.theme.color = this.theme.color === "blue" ? "red" : "blue";
  //     }
  //   }
  // }
// F 组件 
<template functional>
  <div class="border2">
    <h3 :style="{ color: injections.theme.color }">F 组件</h3>
  </div>
</template>
<script>
export default {
  inject: {
    theme: {
      //函数式组件取值不一样
      default: () => ({})
    }
  }
};
</script>

虽说provide 和 inject 主要为高阶插件/组件库提供用例,但如果你能在业务中熟练运用,可以达到事半功倍的效果!

方法六、$parent / $childrenref

  • ref:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例
  • $parent / $children:访问父 / 子实例

需要注意的是:这两种都是直接得到组件实例,使用后可以直接调用组件的方法或访问数据。我们先来看个用 ref来访问组件的例子:

// component-a 子组件
export default {
  data () {
    return {
      title: 'Vue.js'
    }
  },
  methods: {
    sayHello () {
      window.alert('Hello');
    }
  }
}
// 父组件
<template>
  <component-a ref="comA"></component-a>
</template>
<script>
  export default {
    mounted () {
      const comA = this.$refs.comA;
      console.log(comA.title);  // Vue.js
      comA.sayHello();  // 弹窗
    }
  }
</script>

不过,这两种方法的弊端是,无法在跨级或兄弟间通信

// parent.vue
<component-a></component-a>
<component-b></component-b>
<component-b></component-b>

我们想在 component-a 中,访问到引用它的页面中(这里就是 parent.vue)的两个 component-b 组件,那这种情况下,就得配置额外的插件或工具了,比如 Vuex 和 Bus 的解决方案。

总结

常见使用场景可以分为三类:

  • 父子通信:
    父向子传递数据是通过 props,子向父是通过 events($emit);通过父链 / 子链也可以通信($parent / $children);ref 也可以访问组件实例;provide / inject API;$attrs/$listeners
  • 兄弟通信:
    Bus;Vuex
  • 跨级通信:
    Bus;Vuex;provide / inject API、$attrs/$listeners

参考文章

关于响应式布局,你必须要知道的

一、前言

响应式 Web 设计可以让一个网站同时适配多种设备和多个屏幕,可以让网站的布局和功能随用户的使用环境(屏幕大小、输入方式、设备/浏览器能力)而变化。本文主要介绍一些响应式布局容易忽略但又很重要的知识点。

二、视口

移动前端中常说的 viewport (视口)就是浏览器中用于呈现网页的区域。视口通常并不等于屏幕大小,特别是可以缩放浏览器窗口的情况下。手机端与 PC 端视口存在差异,电脑端的视口宽度等于分辨率,而移动端的视口宽度跟分辨率没有关系,宽度默认值是设备厂家指定的。iOS, Android 基本都将这个视口分辨率设置为 980px。

1.为什么手机端视口要设为 980px?

当年乔布斯设想:苹果手机如果在市场上火爆了,但是各个网站还没有来得及制作手机端网页,那么用户不得不用手机访问电脑版的网页,如何用小屏幕访问大屏幕的页面也同样可读呢?乔帮主就想着为手机固定一个视口宽度,让手机的视口宽度等于世界上绝大多数 PC 网页的版心宽度,就是 980px。这样,用手机访问电脑版网页的时候,旁边刚好没有留白。不过页面缩放后文字会变得非常小,用户需要手动放大缩小才能看清楚,体验非常差。
PC端页面在手机上显示效果

2.约束视口

为了解决前面的问题,可以在网页的中添加下面这行代码:

<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" />
width=device-width   视口为设备宽度(就是人设置的一个宽度)//不设置的话默认为980px
initial-scale=1.0    初始化的视口大小是1.0倍
maximum-scale=1.0    最大的倍数是1.0倍
user-scalable=0      不允许缩放视口

这个视口的标签告诉浏览器怎么渲染网页。在这里,标签想表达的意思是:按照设备的宽度(device-width)来渲染网页内容。事实上,在支持这个标签的设备上给你看一看效果,你就明白了。

约束视口后

不错呀!用户体验大大改善!!!
此时如果用 document.documentElement.clientWidth 来测试浏览器屏幕宽度,你会发现当前视口宽度等于手机屏幕的宽度,约数后的视口宽度都是在 320~480 之间(手机竖直使用的时候)。
这个视口的尺寸,是手机厂商设置的,能够保证我们的文字比如 16px,在自己的这个视口下清晰、大小刚刚合适。所以大屏幕的手机的约束视口 > 小屏幕手机的约束视口。这就能够保证我们的网页可以用 px 写字号、写行高
需要注意的是:约束之后的视口宽度,不是自己的分辨率!!每个手机的分辨率,都要比自己的视口宽度大得多得多!

最最重要的一句话:前端开发工程师,丝毫不关心手机的分辨率,我们只关心视口。

三、图片

人们常说说“一图胜千言”,确实如此。我们网页中关于松饼的文字介绍再多,也没有图片有吸引力。下面我们就在页面上方添加一张松饼的图片(2000 像素宽),效果类似引诱用户往下看的大题图。

添加图片后

哇,真是好大一张图,它让整个网页看起来都失衡了,水平方向上图片溢出了。不行,必须解决这个问题。可以用 CSS 给图片指定固定宽度,但问题是我们想让它能在不同大小的屏幕中自动缩放。比如,我们例子中的 iPhone 屏幕宽度为 320 像素,如果我们把图片设置成 320 像素宽,那么 iPhone 屏幕旋转后又怎么办呢?这时候 320 像素变成了 480 像素。
解决方案很简单,只要一行 CSS 代码就可以让图片随容器宽度自动缩放:

img {
 max-width: 100%;
}

回到手机上,刷新页面,结果比较符合预期了。
这里声明 max-width 规则,就是要保证所有图片最大显示为其自身的 100%(即最大只可以显示为自身那么大)。此时,如果包含图片的元素(比如包含图片的 body 或 div)比图片固有宽度小,图片会缩放占满最大可用空间

为什么不用 width:100%?

要实现图片的自动缩放,也可以使用更通用的 width 属性,比如 width:100%。然而,这条规则在这里并不适用。因为这条规则会导致它显示得跟它的容器一样宽。在容器比图片宽得多的情况下,图片会被无谓地拉伸。

四、手机浏览器内核

在移动端,仅有四个独立的浏览器内核,分别为微软的 Trident、火狐的 Gecko、开源内核 Webkit、Opera 的 Presto。
目前微软的 Trident 在移动终端上主要为 WP7、8 系统内置浏览器。Opera 的 Presto 内核主要为 Opera Mobile、OperaMini、欧朋浏览器以及欧朋 HD Beta 版。Webkit 内核的适用范围则较为广泛,Android 原生浏览器、苹果的 Safari、谷歌 Chrome(Android4.0 使用)都是基于 Webkit 开源内核开发的。

兼容的前缀:
1	-ms-
2	-moz-
3	-o-
4	-webkit-

**用户的浏览器市场份额:

近一年**用户的浏览器市场份额

UC、Android 内置、Chrome、Safari、QQ Browser 都是 webkit 内核,从图上看占了绝大部分的市场份额。
所以一定要伺候好-webkit-。 有的公司干脆只兼容-webkit-,别的兼容比如-ms-都不写。

五、流式布局

百分比布局也叫作流式布局、弹性盒布局。手机网页没有版心,都左右撑满。

百分比能够设置的属性是 width、height、padding、margin。其他属性比如 border、font-size 不能用百分比设置的。

  • 如果用百分比写 width,那么指的是父元素 width 的百分之多少。
  • 如果用百分比写 height,那么指的是父元素 height 的百分之多少。
  • 如果用百分比写 padding,那么指的是父元素 width 的百分之多少,无论是水平的 padding 还是竖直的 padding。
  • 如果用百分比写 margin,那么指的是父元素 width 的百分之多少,无论是水平的 margin 还是竖直的 margin。
  • 不能用百分比写 border 的宽度

接下来我们看一个例子:

	div{
		width:200px;
		height:300px;
		padding:10px;
	}
	div p{
		width:50%;
		height:50%;
		padding:10%;
	}
    此时p的真实宽度是多少?

p的盒模型图

此时 p 的真实宽度是 140px*190px

六、媒体查询

1.为什么响应式 Web 设计需要媒体查询

CSS3 媒体查询可以让我们针对特定的设备能力或条件为网页应用特定的 CSS 样式。如果没有媒体查询,光用 CSS 是无法大大修改网页外观的。这个模块让我们可以提前编写出适应很多不可预测因素的 CSS 规则,比如屏幕方向水平或垂直、视口或大或小等等。弹性布局虽然可以让设计适应较多场景,也包括某些尺寸的屏幕,但有时候确实不够用,因为我们还需要对布局进行更细致的调整。媒体查询让这一切成为可能,它就相当于 CSS 中基本的条件逻辑。

2.媒体查询语法

我们在媒体查询外面写的第一条规则,是“基本的”样式,它适用于任何设备。在此基础上,我们再为不同视口、不同能力的设备,渐进增加不同的视觉效果和功能。

body {
    background-color: grey;
 }
@media screen and (min-width:1200px){
    body{
        background-color: pink;
	}
}
 @media screen and (min-width:700px) and (max-width:1200px){
    body{
	background-color: blue;
	}
}
@media screen and (max-width:700px){
    body{
	background-color: orange;
        }
}

其中@media 就表示媒体查询,查询现在看这个网页的设备是什么,以及它的宽度是多少。screen 表示看这个网页的设备是显示器,而不是残疾人听力设备、也不是打印机。后面用 and 符号罗列所有的可能性。
值得注意:媒体查询只能包裹选择器,不能包裹 k:v 对儿。

IE6、7、8 不支持媒体查询,也为了防止手机端的某些浏览器不支持媒体查询,所以不要把所有的选择器都放在媒体查询里面。

七、rem 响应式布局

rem 响应式布局**

  1. 一般不要给元素设置具体的宽度,但是对于一些小图标可以设定具体宽度值
  2. 高度值可以设置固定值,设计稿有多大,我们就严格写多大
  3. 所有设置的固定值都用 REM 做单位(首先在 HTML 中设置一个基准值:PX 和 REM 的对应比例,然后在效果图上获取 PX 值,布局的时候转化为 REM 值)
  4. JS 获取真实屏幕的宽度,让其除以设计稿的宽度,算出比例,把之前的基准值按照比例进行重新的设定,这样项目就可以在移动端自适应了

什么是 rem,它与 em 有何区别

rem:当前页面中元素的 REM 单位的样式值都是针对于 HTML 元素的 font-size 的值进行动态计算的

em:表示父元素的字号的倍数。(特例:在 text-indent 属性中,表示文字宽度)

  body             →font-size:20px;
   <div class="box1">   → font-size:2em;
	box1
	  <div class="box2">  → font-size:2em;
		box2
	    <div class="box3">  → font-size:2em;
	    	box3
	    </div>
	 </div>
  </div>

得到结果

em 为单位的时候,font-size 属性是计算后继承,box1 计算出来是 40px。那么里面的 box2继承的是 40px。em 单位不仅仅可以用来设置字号,还可以设置任何盒模型的属性,比如 width、height、padding、margin、border

rem 有一点优势就是可以和媒体查询配合,实现响应式布局:

@media screen and (min-width: 320px) {
    html {font-size: 14px;}
}
@media screen and (min-width: 360px) {
    html {font-size: 16px;}
}
@media screen and (min-width: 400px) {
    html {font-size: 18px;}
}

运用场景

如果我们做的 H5 页面只在移动端访问,这是因为 REM 不兼容低版本的浏览器。而如果移动端和 PC 端公用一套代码,建议使用流式布局。

如何做个 REM 响应式布局

1、从 UI 设计师拿到 PSD 设计稿,然后在样式中给 HTML 设定一个 font-size 的值,我们一般都设置一个方便后面计算的值,例如:100px

html{
font-size:100px;//1rem=100px
}

2、写页面,写样式
首先按照设计稿的尺寸来写样式,然后在写样式值的时候,需要把得到的像素值除以 100 计算出对应的 REM 的值。
值得注意的是:真实项目中外层盒子的宽度我们一般还是不写固定值,沿用流式布局法的**,我们用百分比的方式布局

margin:0  0.2rem;
height:3rem;

3、根据当前屏幕的宽度和设计稿的宽度来计算我们 HTML 的 font-size 的值
例如:设计稿宽度为 640px,其中有一个部分是轮播图,它的尺寸是 600*300,在样式中给 HTML 设定一个 font-size 的值为 100px,则轮播图大小应该为 6rem×3rem,那如果手机屏幕宽度为 375px,其 font-size 应该设置为多少。

375/640*100->fontsize=58.59375//此时轮播图能自适应手机屏幕大小

根据当前屏幕宽度和设计稿宽度的比例,动态计算一下当前宽度下的 fontsize 值应该是多少,如果 fontsize 的值改变了,之前设定的所有 REM 单位的值自动会跟着放大或者缩小。可以通过以下这段代码实现:

<script>
~function(){
var desW=640,
winW=document.documentElement.clientwidth,
ratio=winW/desW;
document.documentElement.style.fontSize=ratio*100+"px";
}();
</script>

但如果当前屏幕宽度大于设计稿宽度,图片会被拉长而失真,所以以上代码需要稍微做些修改:

//html部分
<section id="main">
<div class="box"></div>
</section>
//js部分
<script>
~function(){
var desW=640,
winW=document.documentElement.clientwidth,
ratio=winW/desW;
var oMain=document.getElementById(main");
if(winW>desW){
oMain.style.width=desW+"px";
oMain.style.margin="0 auto";
return;
}
document.documentElement.style.fontSize=ratio*100+"px";
}();
</script>

八、参考

响应式 Web 设计:HTML5 和 CSS3 实战/(英)本·弗莱恩(Ben Frain)著;奇舞团译

fetch 如何请求数据

一 序言

在 传统Ajax 时代,进行 API 等网络请求都是通过XMLHttpRequest或者封装后的框架进行网络请求,然而配置和调用方式非常混乱,对于刚入门的新手并不友好。今天我们介绍的Fetch提供了一个更好的替代方法,它不仅提供了一种简单,合乎逻辑的方式来跨网络异步获取资源,而且可以很容易地被其他技术使用,例如 Service Workers。

本文框架图

二 与Ajax对比

使用Ajax请求一个 JSON 数据一般是这样:

var xhr = new XMLHttpRequest();
xhr.open('GET', url/file,true);
xhr.onreadystatechange = function() {
   if(xhr.readyState==4){
        if(xhr.status==200){
            var data=xhr.responseText;
             console.log(data);
   }
};
xhr.onerror = function() {
  console.log("Oh, error");
};
xhr.send();

同样我们使用fetch请求JSON数据:

fetch(url).then(response => response.json())//解析为可读数据
  .then(data => console.log(data))//执行结果是 resolve就调用then方法
  .catch(err => console.log("Oh, error", err))//执行结果是 reject就调用catch方法

从两者对比来看,fetch代码精简许多,业务逻辑更清晰明了,使得代码易于维护,可读性更高。
总而言之,Fetch 优点主要有:

1. 语法简洁,更加语义化,业务逻辑更清晰

2. 基于标准 Promise 实现,支持 async/await

3. 同构方便,使用isomorphic-fetch

三 Promise简介

由于 Fetch API 是基于 Promise 设计,接下来我们简单介绍下Promise工作流程,方便大家更好理解Fetch。
Promise工作原理

fetch方法返回一个Promise对象, 根据 Promise Api 的特性, fetch可以方便地使用then方法将各个处理逻辑串起来, 使用 Promise.resolve() 或 Promise.reject() 方法将分别返会肯定结果的Promise或否定结果的Promise, 从而调用下一个then 或者 catch。一旦then中的语句出现错误, 也将跳到catch中。

四 请求常见数据格式

接下来将介绍如何使用fetch请求本地文本数据,请求本地JSON数据以及请求网络接口。其实操作相比与Ajax,简单很多!

//HTML部分
  <div class="container">
    <h1>Fetch Api sandbox</h1>
    <button id="button1">请求本地文本数据</button>
    <button id="button2">请求本地json数据</button>
    <button id="button3">请求网络接口</button>
    <br><br>
    <div id="output"></div>
  </div>
  <script src="app.js"></script>

1.fetch请求本地文本数据

本地有一个test.txt文档,通过以下代码就可以获取其中的数据,并且显示在页面上。

document.getElementById('button1').addEventListener('click',getText);
function getText(){
  fetch("test.txt")
      .then((res) => res.text())//注意:此处是res.text()
      .then(data => {
        console.log(data);
        document.getElementById('output').innerHTML = data;
      })
      .catch(err => console.log(err));
}

2.fetch请求本地JSON数据

本地有个posts.json数据,与请求本地文本不同的是,得到数据后还要用forEach遍历,最后呈现在页面上。

document.getElementById('button2').addEventListener('click',getJson);
function getJson(){
  fetch("posts.json")
      .then((res) => res.json())
      .then(data => {
        console.log(data);
        let output = '';
        data.forEach((post) => {
          output += `<li>${post.title}</li>`;
        })
        document.getElementById('output').innerHTML = output;
      })
      .catch(err => console.log(err));
}

3.fetch请求网络接口

获取https://api.github.com/users中的数据,做法与获取本地JSON的方法类似,得到数据后,同样要经过处理

document.getElementById('button3').addEventListener('click',getExternal);
function getExternal(){
  // https://api.github.com/users
  fetch("https://api.github.com/users")
      .then((res) => res.json())
      .then(data => {
        console.log(data);
        let output = '';
        data.forEach((user) => {
          output += `<li>${user.login}</li>`;
        })
        document.getElementById('output').innerHTML = output;
      })
      .catch(err => console.log(err));
}

懒加载和预加载

一、懒加载

1.什么是懒加载

懒加载也叫延迟加载,指的是在长网页中延迟加载图像,是一种很好优化网页性能的方式。用户滚动到它们之前,可视区域外的图像不会加载。这与图像预加载相反,在长网页上使用延迟加载将使网页加载更快。在某些情况下,它还可以帮助减少服务器负载。常适用图片很多,页面很长的电商网站场景中。

2.为什么要用懒加载

  • 能提升用户的体验,不妨设想下,用户打开像手机淘宝长页面的时候,如果页面上所有的图片都需要加载,由于图片数目较大,等待时间很长,用户难免会心生抱怨,这就严重影响用户体验。
  • 减少无效资源的加载,这样能明显减少了服务器的压力和流量,也能够减小浏览器的负担。
  • 防止并发加载的资源过多会阻塞js的加载,影响网站的正常使用。

3.懒加载的原理

首先将页面上的图片的 src 属性设为空字符串,而图片的真实路径则设置在data-original属性中,
当页面滚动的时候需要去监听scroll事件,在scroll事件的回调中,判断我们的懒加载的图片是否进入可视区域,如果图片在可视区内将图片的 src 属性设置为data-original 的值,这样就可以实现延迟加载。

4.懒加载实现步骤

<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Lazyload</title>
    <style>
      .image-item {
	    display: block;
	    margin-bottom: 50px;
	    height: 200px;//一定记得设置图片高度
	}
    </style>
</head>
<body>
<img src="" class="image-item" lazyload="true"  data-original="images/1.png"/>
<img src="" class="image-item" lazyload="true"  data-original="images/2.png"/>
<img src="" class="image-item" lazyload="true"  data-original="images/3.png"/>
<img src="" class="image-item" lazyload="true"  data-original="images/4.png"/>
<img src="" class="image-item" lazyload="true"  data-original="images/5.png"/>
<img src="" class="image-item" lazyload="true"  data-original="images/6.png"/>
<img src="" class="image-item" lazyload="true"  data-original="images/7.png"/>
<img src="" class="image-item" lazyload="true"  data-original="images/8.png"/>
<img src="" class="image-item" lazyload="true"  data-original="images/9.png"/>
<img src="" class="image-item" lazyload="true"  data-original="images/10.png"/>
<img src="" class="image-item" lazyload="true"  data-original="images/11.png"/>
<img src="" class="image-item" lazyload="true"  data-original="images/12.png"/>
<script>
var viewHeight =document.documentElement.clientHeight//获取可视区高度
function lazyload(){
var eles=document.querySelectorAll('img[data-original][lazyload]')
Array.prototype.forEach.call(eles,function(item,index){
var rect
if(item.dataset.original==="")
   return
rect=item.getBoundingClientRect()// 用于获得页面中某个元素的左,上,右和下分别相对浏览器视窗的位置
if(rect.bottom>=0 && rect.top < viewHeight){
!function(){
  var img=new Image()
  img.src=item.dataset.original
  img.onload=function(){
    item.src=img.src
    }
item.removeAttribute("data-original")//移除属性,下次不再遍历
item.removeAttribute("lazyload")
   }()
  }
 })
}
lazyload()//刚开始还没滚动屏幕时,要先触发一次函数,初始化首页的页面图片
document.addEventListener("scroll",lazyload)
</script>
</body>
</html>

二、预加载

1.什么是预加载

资源预加载是另一个性能优化技术,我们可以使用该技术来预先告知浏览器某些资源可能在将来会被使用到。预加载简单来说就是将所有所需的资源提前请求加载到本地,这样后面在需要用到时就直接从缓存取资源

2.为什么要用预加载

在网页全部加载之前,对一些主要内容进行加载,以提供给用户更好的体验,减少等待的时间。否则,如果一个页面的内容过于庞大,没有使用预加载技术的页面就会长时间的展现为一片空白,直到所有内容加载完毕。

3.实现预加载的几种办法

  • 使用HTML标签

<img src="http://pic26.nipic.com/20121213/6168183 0044449030002.jpg" style="display:none"/>

  • 使用Image对象

<script src="./myPreload.js"></script>

//myPreload.js文件
var image= new Image()
image.src="http://pic26.nipic.com/20121213/6168183 004444903000 2.jpg"
  • 使用XMLHttpRequest对象,虽然存在跨域问题,但会精细控制预加载过程
var xmlhttprequest=new XMLHttpRequest();
xmlhttprequest.onreadystatechange=callback;
xmlhttprequest.onprogress=progressCallback;
xmlhttprequest.open("GET","http://image.baidu.com/mouse,jpg",true);
xmlhttprequest.send();
function callback(){
  if(xmlhttprequest.readyState==4&& xmlhttprequest.status==200){
    var responseText=xmlhttprequest.responseText;
  }else{
     console.log("Request was unsuccessful:"+xmlhttprequest.status);
  }
}
function progressCallback(e){
e=e || event;
if(e.lengthComputable){
console.log("Received"+e.loaded+"of"+e.total+"bytes")
}
}

PreloadJS提供了一种预加载内容的一致方式,以便在HTML应用程序中使用。预加载可以使用HTML标签以及XHR来完成。默认情况下,PreloadJS会尝试使用XHR加载内容,因为它提供了对进度和完成事件的更好支持,但是由于跨域问题,使用基于标记的加载可能更好。

//使用preload.js
var queue=new createjs.LoadQueue();//默认是xhr对象,如果是new createjs.LoadQueue(false)是指使用HTML标签,可以跨域
queue.on("complete",handleComplete,this);
queue.loadManifest([
{id:"myImage",src:"http://pic26.nipic.com/20121213/6168183  0044449030002.jpg"},
{id:"myImage2",src:"http://pic9.nipic.com/20100814/2839526  1931471581702.jpg"}
]);
function handleComplete(){
  var image=queue.getResuLt("myImage");
  document.body.appendChild(image);
}

三、懒加载和预加载的对比

两者都是提高页面性能有效的办法,两者主要区别是一个是提前加载,一个是迟缓甚至不加载。懒加载对服务器前端有一定的缓解压力作用,预加载则会增加服务器前端压力

如果想了解更多页面性能优化的办法,请猛戳页面性能优化办法有哪些

四、参考文章

详解懒加载和预加载(js)

懒加载和预加载

浏览器与Node的事件循环(Event Loop)有何区别?

前言

本文我们将会介绍 JS 实现异步的原理,并且了解了在浏览器和 Node 中 Event Loop 其实是不相同的。

想阅读更多优质原创文章请猛戳GitHub博客

一、线程与进程

1.概念

我们经常说JS 是单线程执行的,指的是一个进程里只有一个主线程,那到底什么是线程?什么是进程?

官方的说法是:进程是 CPU资源分配的最小单位;线程是 CPU调度的最小单位。这两句话并不好理解,我们先来看张图:

  • 进程好比图中的工厂,有单独的专属自己的工厂资源。
  • 线程好比图中的工人,多个工人在一个工厂中协作工作,工厂与工人是 1:n的关系。也就是说一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线
  • 工厂的空间是工人们共享的,这象征一个进程的内存空间是共享的,每个线程都可用这些共享内存
  • 多个工厂之间独立存在。

2.多进程与多线程

  • 多进程:在同一个时间里,同一个计算机系统中如果允许两个或两个以上的进程处于运行状态。多进程带来的好处是明显的,比如你可以听歌的同时,打开编辑器敲代码,编辑器和听歌软件的进程之间丝毫不会相互干扰。
  • 多线程:程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务,也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。

以Chrome浏览器中为例,当你打开一个 Tab 页时,其实就是创建了一个进程,一个进程中可以有多个线程(下文会详细介绍),比如渲染线程、JS 引擎线程、HTTP 请求线程等等。当你发起一个请求时,其实就是创建了一个线程,当请求结束后,该线程可能就会被销毁。

二、浏览器内核

简单来说浏览器内核是通过取得页面内容、整理信息(应用CSS)、计算和组合最终输出可视化的图像结果,通常也被称为渲染引擎。

浏览器内核是多线程,在内核控制下各线程相互配合以保持同步,一个浏览器通常由以下常驻线程组成:

  • GUI 渲染线程
  • JavaScript引擎线程
  • 定时触发器线程
  • 事件触发线程
  • 异步http请求线程

1.GUI渲染线程

  • 主要负责页面的渲染,解析HTML、CSS,构建DOM树,布局和绘制等。
  • 当界面需要重绘或者由于某种操作引发回流时,将执行该线程。
  • 该线程与JS引擎线程互斥,当执行JS引擎线程时,GUI渲染会被挂起,当任务队列空闲时,主线程才会去执行GUI渲染。

2.JS引擎线程

  • 该线程当然是主要负责处理 JavaScript脚本,执行代码。
  • 也是主要负责执行准备好待执行的事件,即定时器计数结束,或者异步请求成功并正确返回时,将依次进入任务队列,等待 JS引擎线程的执行。
  • 当然,该线程与 GUI渲染线程互斥,当 JS引擎线程执行 JavaScript脚本时间过长,将导致页面渲染的阻塞。

3.定时器触发线程

  • 负责执行异步定时器一类的函数的线程,如: setTimeout,setInterval。
  • 主线程依次执行代码时,遇到定时器,会将定时器交给该线程处理,当计数完毕后,事件触发线程会将计数完毕后的事件加入到任务队列的尾部,等待JS引擎线程执行。

4.事件触发线程

  • 主要负责将准备好的事件交给 JS引擎线程执行。

比如 setTimeout定时器计数结束, ajax等异步请求成功并触发回调函数,或者用户触发点击事件时,该线程会将整装待发的事件依次加入到任务队列的队尾,等待 JS引擎线程的执行。

5.异步http请求线程

  • 负责执行异步请求一类的函数的线程,如: Promise,axios,ajax等。
  • 主线程依次执行代码时,遇到异步请求,会将函数交给该线程处理,当监听到状态码变更,如果有回调函数,事件触发线程会将回调函数加入到任务队列的尾部,等待JS引擎线程执行。

三、浏览器中的 Event Loop

1.Micro-Task 与 Macro-Task

浏览器端事件循环中的异步队列有两种:macro(宏任务)队列和 micro(微任务)队列。宏任务队列可以有多个,微任务队列只有一个

  • 常见的 macro-task 比如:setTimeout、setInterval、script(整体代码)、 I/O 操作、UI 渲染等。
  • 常见的 micro-task 比如: new Promise().then(回调)、MutationObserver(html5新特性) 等。

2.Event Loop 过程解析

一个完整的 Event Loop 过程,可以概括为以下阶段:

  • 一开始执行栈空,我们可以把执行栈认为是一个存储函数调用的栈结构,遵循先进后出的原则。micro 队列空,macro 队列里有且只有一个 script 脚本(整体代码)。

  • 全局上下文(script 标签)被推入执行栈,同步代码执行。在执行的过程中,会判断是同步任务还是异步任务,通过对一些接口的调用,可以产生新的 macro-task 与 micro-task,它们会分别被推入各自的任务队列里。同步代码执行完了,script 脚本会被移出 macro 队列,这个过程本质上是队列的 macro-task 的执行和出队的过程。

  • 上一步我们出队的是一个 macro-task,这一步我们处理的是 micro-task。但需要注意的是:当 macro-task 出队时,任务是一个一个执行的;而 micro-task 出队时,任务是一队一队执行的。因此,我们处理 micro 队列这一步,会逐个执行队列中的任务并把它出队,直到队列被清空。

  • 执行渲染操作,更新界面

  • 检查是否存在 Web worker 任务,如果有,则对其进行处理

  • 上述过程循环往复,直到两个队列都清空

我们总结一下,每一次循环都是一个这样的过程:

当某个宏任务执行完后,会查看是否有微任务队列。如果有,先执行微任务队列中的所有任务,如果没有,会读取宏任务队列中排在最前的任务,执行宏任务的过程中,遇到微任务,依次加入微任务队列。栈空后,再次读取微任务队列里的任务,依次类推。

接下来我们看道例子来介绍上面流程:

Promise.resolve().then(()=>{
  console.log('Promise1')  
  setTimeout(()=>{
    console.log('setTimeout2')
  },0)
})
setTimeout(()=>{
  console.log('setTimeout1')
  Promise.resolve().then(()=>{
    console.log('Promise2')    
  })
},0)

最后输出结果是Promise1,setTimeout1,Promise2,setTimeout2

  • 一开始执行栈的同步任务(这属于宏任务)执行完毕,会去查看是否有微任务队列,上题中存在(有且只有一个),然后执行微任务队列中的所有任务输出Promise1,同时会生成一个宏任务 setTimeout2
  • 然后去查看宏任务队列,宏任务 setTimeout1 在 setTimeout2 之前,先执行宏任务 setTimeout1,输出 setTimeout1
  • 在执行宏任务setTimeout1时会生成微任务Promise2 ,放入微任务队列中,接着先去清空微任务队列中的所有任务,输出 Promise2
  • 清空完微任务队列中的所有任务后,就又会去宏任务队列取一个,这回执行的是 setTimeout2

四、Node 中的 Event Loop

1.Node简介

Node 中的 Event Loop 和浏览器中的是完全不相同的东西。Node.js采用V8作为js的解析引擎,而I/O处理方面使用了自己设计的libuv,libuv是一个基于事件驱动的跨平台抽象层,封装了不同操作系统一些底层特性,对外提供统一的API,事件循环机制也是它里面的实现(下文会详细介绍)。


Node.js的运行机制如下:

  • V8引擎解析JavaScript脚本。
  • 解析后的代码,调用Node API。
  • libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎。
  • V8引擎再将结果返回给用户。

2.六个阶段

其中libuv引擎中的事件循环分为 6 个阶段,它们会按照顺序反复运行。每当进入某一个阶段的时候,都会从对应的回调队列中取出函数去执行。当队列为空或者执行的回调函数数量到达系统设定的阈值,就会进入下一阶段。

从上图中,大致看出node中的事件循环的顺序:

外部输入数据-->轮询阶段(poll)-->检查阶段(check)-->关闭事件回调阶段(close callback)-->定时器检测阶段(timer)-->I/O事件回调阶段(I/O callbacks)-->闲置阶段(idle, prepare)-->轮询阶段(按照该顺序反复运行)...

  • timers 阶段:这个阶段执行timer(setTimeout、setInterval)的回调
  • I/O callbacks 阶段:处理一些上一轮循环中的少数未执行的 I/O 回调
  • idle, prepare 阶段:仅node内部使用
  • poll 阶段:获取新的I/O事件, 适当的条件下node将阻塞在这里
  • check 阶段:执行 setImmediate() 的回调
  • close callbacks 阶段:执行 socket 的 close 事件回调

注意:上面六个阶段都不包括 process.nextTick()(下文会介绍)

接下去我们详细介绍timerspollcheck这3个阶段,因为日常开发中的绝大部分异步任务都是在这3个阶段处理的。

(1) timer

timers 阶段会执行 setTimeout 和 setInterval 回调,并且是由 poll 阶段控制的。
同样,在 Node 中定时器指定的时间也不是准确时间,只能是尽快执行

(2) poll

poll 是一个至关重要的阶段,这一阶段中,系统会做两件事情

1.回到 timer 阶段执行回调

2.执行 I/O 回调

并且在进入该阶段时如果没有设定了 timer 的话,会发生以下两件事情

  • 如果 poll 队列不为空,会遍历回调队列并同步执行,直到队列为空或者达到系统限制
  • 如果 poll 队列为空时,会有两件事发生
    • 如果有 setImmediate 回调需要执行,poll 阶段会停止并且进入到 check 阶段执行回调
    • 如果没有 setImmediate 回调需要执行,会等待回调被加入到队列中并立即执行回调,这里同样会有个超时时间设置防止一直等待下去

当然设定了 timer 的话且 poll 队列为空,则会判断是否有 timer 超时,如果有的话会回到 timer 阶段执行回调。

(3) check阶段

setImmediate()的回调会被加入check队列中,从event loop的阶段图可以知道,check阶段的执行顺序在poll阶段之后。
我们先来看个例子:

console.log('start')
setTimeout(() => {
  console.log('timer1')
  Promise.resolve().then(function() {
    console.log('promise1')
  })
}, 0)
setTimeout(() => {
  console.log('timer2')
  Promise.resolve().then(function() {
    console.log('promise2')
  })
}, 0)
Promise.resolve().then(function() {
  console.log('promise3')
})
console.log('end')
//start=>end=>promise3=>timer1=>timer2=>promise1=>promise2
  • 一开始执行栈的同步任务(这属于宏任务)执行完毕后(依次打印出start end,并将2个timer依次放入timer队列),会先去执行微任务(这点跟浏览器端的一样),所以打印出promise3
  • 然后进入timers阶段,执行timer1的回调函数,打印timer1,并将promise.then回调放入microtask队列,同样的步骤执行timer2,打印timer2;这点跟浏览器端相差比较大,timers阶段有几个setTimeout/setInterval都会依次执行,并不像浏览器端,每执行一个宏任务后就去执行一个微任务(关于Node与浏览器的 Event Loop 差异,下文还会详细介绍)。

3.Micro-Task 与 Macro-Task

Node端事件循环中的异步队列也是这两种:macro(宏任务)队列和 micro(微任务)队列。

  • 常见的 macro-task 比如:setTimeout、setInterval、 setImmediate、script(整体代码)、 I/O 操作等。
  • 常见的 micro-task 比如: process.nextTick、new Promise().then(回调)等。

4.注意点

(1) setTimeout 和 setImmediate

二者非常相似,区别主要在于调用时机不同。

  • setImmediate 设计在poll阶段完成时执行,即check阶段;
  • setTimeout 设计在poll阶段为空闲时,且设定时间到达后执行,但它在timer阶段执行
setTimeout(function timeout () {
  console.log('timeout');
},0);
setImmediate(function immediate () {
  console.log('immediate');
});
  • 对于以上代码来说,setTimeout 可能执行在前,也可能执行在后。
  • 首先 setTimeout(fn, 0) === setTimeout(fn, 1),这是由源码决定的
    进入事件循环也是需要成本的,如果在准备时候花费了大于 1ms 的时间,那么在 timer 阶段就会直接执行 setTimeout 回调
  • 如果准备时间花费小于 1ms,那么就是 setImmediate 回调先执行了

但当二者在异步i/o callback内部调用时,总是先执行setImmediate,再执行setTimeout

const fs = require('fs')
fs.readFile(__filename, () => {
    setTimeout(() => {
        console.log('timeout');
    }, 0)
    setImmediate(() => {
        console.log('immediate')
    })
})
// immediate
// timeout

在上述代码中,setImmediate 永远先执行。因为两个代码写在 IO 回调中,IO 回调是在 poll 阶段执行,当回调执行完毕后队列为空,发现存在 setImmediate 回调,所以就直接跳转到 check 阶段去执行回调了。

(2) process.nextTick

这个函数其实是独立于 Event Loop 之外的,它有一个自己的队列,当每个阶段完成后,如果存在 nextTick 队列,就会清空队列中的所有回调函数,并且优先于其他 microtask 执行。

setTimeout(() => {
 console.log('timer1')
 Promise.resolve().then(function() {
   console.log('promise1')
 })
}, 0)
process.nextTick(() => {
 console.log('nextTick')
 process.nextTick(() => {
   console.log('nextTick')
   process.nextTick(() => {
     console.log('nextTick')
     process.nextTick(() => {
       console.log('nextTick')
     })
   })
 })
})
// nextTick=>nextTick=>nextTick=>nextTick=>timer1=>promise1

五、Node与浏览器的 Event Loop 差异

浏览器环境下,microtask的任务队列是每个macrotask执行完之后执行。而在Node.js中,microtask会在事件循环的各个阶段之间执行,也就是一个阶段执行完毕,就会去执行microtask队列的任务

接下我们通过一个例子来说明两者区别:

setTimeout(()=>{
    console.log('timer1')
    Promise.resolve().then(function() {
        console.log('promise1')
    })
}, 0)
setTimeout(()=>{
    console.log('timer2')
    Promise.resolve().then(function() {
        console.log('promise2')
    })
}, 0)

浏览器端运行结果:timer1=>promise1=>timer2=>promise2

浏览器端的处理过程如下:

Node端运行结果分两种情况:

  • 如果是node11版本一旦执行一个阶段里的一个宏任务(setTimeout,setInterval和setImmediate)就立刻执行微任务队列,这就跟浏览器端运行一致,最后的结果为timer1=>promise1=>timer2=>promise2
  • 如果是node10及其之前版本:要看第一个定时器执行完,第二个定时器是否在完成队列中。
    • 如果是第二个定时器还未在完成队列中,最后的结果为timer1=>promise1=>timer2=>promise2
    • 如果是第二个定时器已经在完成队列中,则最后的结果为timer1=>timer2=>promise1=>promise2(下文过程解释基于这种情况下)

1.全局脚本(main())执行,将2个timer依次放入timer队列,main()执行完毕,调用栈空闲,任务队列开始执行;

2.首先进入timers阶段,执行timer1的回调函数,打印timer1,并将promise1.then回调放入microtask队列,同样的步骤执行timer2,打印timer2;

3.至此,timer阶段执行结束,event loop进入下一个阶段之前,执行microtask队列的所有任务,依次打印promise1、promise2

Node端的处理过程如下:

六、总结

浏览器和Node 环境下,microtask 任务队列的执行时机不同

  • Node端,microtask 在事件循环的各个阶段之间执行
  • 浏览器端,microtask 在事件循环的 macrotask 执行完之后执行

后记

文章于2019.1.16晚,对最后一个例子在node运行结果,重新修改!再次特别感谢zy445566BuptStEve的精彩点评,由于node版本更新到11,Event Loop运行原理发生了变化,一旦执行一个阶段里的一个宏任务(setTimeout,setInterval和setImmediate)就立刻执行微任务队列,这点就跟浏览器端一致

参考文章

深入理解HTTPS工作原理

前言

近几年,互联网发生着翻天覆地的变化,尤其是我们一直习以为常的HTTP协议,在逐渐的被HTTPS协议所取代,在浏览器、搜索引擎、CA机构、大型互联网企业的共同促进下,互联网迎来了“HTTPS加密时代”,HTTPS将在未来的几年内全面取代HTTP成为传输协议的主流。

读完本文,希望你能明白:

  • HTTP通信存在什么问题
  • HTTPS如何改进HTTP存在那些问题
  • HTTPS工作原理是什么

想阅读更多优质文章请猛戳GitHub博客,一年五十篇优质文章等着你!

一、什么是HTTPS

HTTPS是在HTTP上建立SSL加密层,并对传输数据进行加密,是HTTP协议的安全版。现在它被广泛用于万维网上安全敏感的通讯,例如交易支付方面。

HTTPS主要作用是:

(1)对数据进行加密,并建立一个信息安全通道,来保证传输过程中的数据安全;

(2)对网站服务器进行真实身份认证。

我们经常会在Web的登录页面和购物结算界面等使用HTTPS通信。使用HTTPS通信时,不再用http://,而是改用https://。另外,当浏览器访问HTTPS通信有效的Web网站时,浏览器的地址栏内会出现一个带锁的标记。对HTTPS的显示方式会因浏览器的不同而有所改变。

二、为什么需要HTTPS

在HTTP协议中有可能存在信息窃取或身份伪装等安全问题。使用HTTPS通信机制可以有效地防止这些问题,接下来,我们先来了解下
HTTP协议存在的哪些问题:

  • 通信使用明文(不加密),内容可能被窃听

由于HTTP本身不具备加密的功能,所以也无法做到对通信整体(使用HTTP协议通信的请求和响应的内容)进行加密。即,HTTP报文使用明文(指未经过加密的报文)方式发送

HTTP明文协议的缺陷是导致数据泄露、数据篡改、流量劫持、钓鱼攻击等安全问题的重要原因。HTTP协议无法加密数据,所有通信数据都在网络中明文“裸奔”。通过网络的嗅探设备及一些技术手段,就可还原HTTP报文内容。

  • 无法证明报文的完整性,所以可能遭篡改

所谓完整性是指信息的准确度。若无法证明其完整性,通常也就意味着无法判断信息是否准确。由于HTTP协议无法证明通信的报文完整性,因此,在请求或响应送出之后直到对方接收之前的这段时间内,即使请求或响应的内容遭到篡改,也没有办法获悉。
换句话说,没有任何办法确认,发出的请求/响应和接收到的请求/响应是前后相同的

  • 不验证通信方的身份,因此有可能遭遇伪装

HTTP协议中的请求和响应不会对通信方进行确认。在HTTP协议通信时,由于不存在确认通信方的处理步骤,任何人都可以发起请求。另外,服务器只要接收到请求,不管对方是谁都会返回一个响应(但也仅限于发送端的IP地址和端口号没有被Web服务器设定限制访问的前提下)

HTTP协议无法验证通信方身份,任何人都可以伪造虚假服务器欺骗用户,实现“钓鱼欺诈”,用户无法察觉。

反观HTTPS协议,它比HTTP协议相比多了以下优势(下文会详细介绍):

  • 数据隐私性:内容经过对称加密,每个连接生成一个唯一的加密密钥
  • 数据完整性:内容传输经过完整性校验
  • 身份认证:第三方无法伪造服务端(客户端)身份

三、HTTPS如何解决HTTP上述问题?

HTTPS并非是应用层的一种新协议。只是HTTP通信接口部分用SSL和TLS协议代替而已。

通常,HTTP直接和TCP通信。当使用SSL时,则演变成先和SSL通信,再由SSL和TCP通信了。简言之,所谓HTTPS,其实就是身披SSL协议这层外壳的HTTP

在采用SSL后,HTTP就拥有了HTTPS的加密、证书和完整性保护这些功能。也就是说HTTP加上加密处理和认证以及完整性保护后即是HTTPS

HTTPS 协议的主要功能基本都依赖于 TLS/SSL 协议,TLS/SSL 的功能实现主要依赖于三类基本算法:散列函数 、对称加密和非对称加密,其利用非对称加密实现身份认证和密钥协商,对称加密算法采用协商的密钥对数据加密,基于散列函数验证信息的完整性

1.解决内容可能被窃听的问题——加密

方法1.对称加密

这种方式加密和解密同用一个密钥。加密和解密都会用到密钥。没有密钥就无法对密码解密,反过来说,任何人只要持有密钥就能解密了

以对称加密方式加密时必须将密钥也发给对方。可究竟怎样才能安全地转交?在互联网上转发密钥时,如果通信被监听那么密钥就可会落人攻击者之手,同时也就失去了加密的意义。另外还得设法安全地保管接收到的密钥。

方法2.非对称加密

公开密钥加密使用一对非对称的密钥。一把叫做私有密钥,另一把叫做公开密钥。顾名思义,私有密钥不能让其他任何人知道,而公开密钥则可以随意发布,任何人都可以获得

使用公开密钥加密方式,发送密文的一方使用对方的公开密钥进行加密处理,对方收到被加密的信息后,再使用自己的私有密钥进行解密。利用这种方式,不需要发送用来解密的私有密钥,也不必担心密钥被攻击者窃听而盗走。

非对称加密的特点是信息传输一对多,服务器只需要维持一个私钥就能够和多个客户端进行加密通信。

这种方式有以下缺点:

  • 公钥是公开的,所以针对私钥加密的信息,黑客截获后可以使用公钥进行解密,获取其中的内容;
  • 公钥并不包含服务器的信息,使用非对称加密算法无法确保服务器身份的合法性,存在中间人攻击的风险,服务器发送给客户端的公钥可能在传送过程中被中间人截获并篡改;
  • 使用非对称加密在数据加密解密过程需要消耗一定时间,降低了数据传输效率;

方法3.对称加密+非对称加密(HTTPS采用这种方式)

使用对称密钥的好处是解密的效率比较快,使用非对称密钥的好处是可以使得传输的内容不能被破解,因为就算你拦截到了数据,但是没有对应的私钥,也是不能破解内容的。就比如说你抢到了一个保险柜,但是没有保险柜的钥匙也不能打开保险柜。那我们就将对称加密与非对称加密结合起来,充分利用两者各自的优势,在交换密钥环节使用非对称加密方式,之后的建立通信交换报文阶段则使用对称加密方式

具体做法是:发送密文的一方使用对方的公钥进行加密处理“对称的密钥”,然后对方用自己的私钥解密拿到“对称的密钥”,这样可以确保交换的密钥是安全的前提下,使用对称加密方式进行通信。所以,HTTPS采用对称加密和非对称加密两者并用的混合加密机制。

2.解决报文可能遭篡改问题——数字签名

网络传输过程中需要经过很多中间节点,虽然数据无法被解密,但可能被篡改,那如何校验数据的完整性呢?----校验数字签名。

数字签名有两种功效

  • 能确定消息确实是由发送方签名并发出来的,因为别人假冒不了发送方的签名。
  • 数字签名能确定消息的完整性,证明数据是否未被篡改过。

数字签名如何生成:

将一段文本先用Hash函数生成消息摘要,然后用发送者的私钥加密生成数字签名,与原文文一起传送给接收者。接下来就是接收者校验数字签名的流程了。

校验数字签名流程

接收者只有用发送者的公钥才能解密被加密的摘要信息,然后用HASH函数对收到的原文产生一个摘要信息,与上一步得到的摘要信息对比。如果相同,则说明收到的信息是完整的,在传输过程中没有被修改,否则说明信息被修改过,因此数字签名能够验证信息的完整性。

假设消息传递在Kobe,James两人之间发生。James将消息连同数字签名一起发送给Kobe,Kobe接收到消息后,通过校验数字签名,就可以验证接收到的消息就是James发送的。当然,这个过程的前提是Kobe知道James的公钥。问题的关键的是,和消息本身一样,公钥不能在不安全的网络中直接发送给Kobe,或者说拿到的公钥如何证明是James的。

此时就需要引入了证书颁发机构(Certificate Authority,简称CA),CA数量并不多,Kobe客户端内置了所有受信任CA的证书。CA对James的公钥(和其他信息)数字签名后生成证书。

3.解决通信方身份可能被伪装的问题——数字证书

数字证书认证机构处于客户端与服务器双方都可信赖的第三方机构的立场上。

我们来介绍一下数字证书认证机构的业务流程:

  • 服务器的运营人员向第三方机构CA提交公钥、组织信息、个人信息(域名)等信息并申请认证;
  • CA通过线上、线下等多种手段验证申请者提供信息的真实性,如组织是否存在、企业是否合法,是否拥有域名的所有权等;
  • 如信息审核通过,CA会向申请者签发认证文件-证书。证书包含以下信息:申请者公钥、申请者的组织信息和个人信息、签发机构 CA的信息、有效时间、证书序列号等信息的明文,同时包含一个签名。 其中签名的产生算法:首先,使用散列函数计算公开的明文信息的信息摘要,然后,采用 CA的私钥对信息摘要进行加密,密文即签名;
  • 客户端 Client 向服务器 Server 发出请求时,Server 返回证书文件;
  • 客户端 Client 读取证书中的相关的明文信息,采用相同的散列函数计算得到信息摘要,然后,利用对应 CA的公钥解密签名数据,对比证书的信息摘要,如果一致,则可以确认证书的合法性,即服务器的公开密钥是值得信赖的。
  • 客户端还会验证证书相关的域名信息、有效时间等信息; 客户端会内置信任CA的证书信息(包含公钥),如果CA不被信任,则找不到对应 CA的证书,证书也会被判定非法。

四、 HTTPS工作流程

1.Client发起一个HTTPS(比如https://juejin.im/user/5a9a9cdcf265da238b7d771c)的请求,根据RFC2818的规定,Client知道需要连接Server的443(默认)端口。

2.Server把事先配置好的公钥证书(public key certificate)返回给客户端。

3.Client验证公钥证书:比如是否在有效期内,证书的用途是不是匹配Client请求的站点,是不是在CRL吊销列表里面,它的上一级证书是否有效,这是一个递归的过程,直到验证到根证书(操作系统内置的Root证书或者Client内置的Root证书)。如果验证通过则继续,不通过则显示警告信息。

4.Client使用伪随机数生成器生成加密所使用的对称密钥,然后用证书的公钥加密这个对称密钥,发给Server。

5.Server使用自己的私钥(private key)解密这个消息,得到对称密钥。至此,Client和Server双方都持有了相同的对称密钥。

6.Server使用对称密钥加密“明文内容A”,发送给Client。

7.Client使用对称密钥解密响应的密文,得到“明文内容A”。

8.Client再次发起HTTPS的请求,使用对称密钥加密请求的“明文内容B”,然后Server使用对称密钥解密密文,得到“明文内容B”。

五、HTTP 与 HTTPS 的区别

  • HTTP 是明文传输协议,HTTPS 协议是由 SSL+HTTP 协议构建的可进行加密传输、身份认证的网络协议,比 HTTP 协议安全。


关于安全性,用最简单的比喻形容两者的关系就是卡车运货,HTTP下的运货车是敞篷的,货物都是暴露的。而https则是封闭集装箱车,安全性自然提升不少。

  • HTTPS比HTTP更加安全,对搜索引擎更友好,利于SEO,谷歌、百度优先索引HTTPS网页;
  • HTTPS需要用到SSL证书,而HTTP不用;
  • HTTPS标准端口443,HTTP标准端口80;
  • HTTPS基于传输层,HTTP基于应用层;
  • HTTPS在浏览器显示绿色安全锁,HTTP没有显示;

六、为何不所有的网站都使用HTTPS

既然HTTPS那么安全可靠,那为何不所有的Web网站都使用HTTPS?

首先,很多人还是会觉得HTTPS实施有门槛,这个门槛在于需要权威CA颁发的SSL证书。从证书的选择、购买到部署,传统的模式下都会比较耗时耗力。

其次,HTTPS普遍认为性能消耗要大于HTTP,因为与纯文本通信相比,加密通信会消耗更多的CPU及内存资源。如果每次通信都加密,会消耗相当多的资源,平摊到一台计算机上时,能够处理的请求数量必定也会随之减少。但事实并非如此,用户可以通过性能优化、把证书部署在SLB或CDN,来解决此问题。举个实际的例子,“双十一”期间,全站HTTPS的淘宝、天猫依然保证了网站和移动端的访问、浏览、交易等操作的顺畅、平滑。通过测试发现,经过优化后的许多页面性能与HTTP持平甚至还有小幅提升,因此HTTPS经过优化之后其实并不慢。

除此之外,想要节约购买证书的开销也是原因之一。要进行HTTPS通信,证书是必不可少的。而使用的证书必须向认证机构(CA)购买。

最后是安全意识。相比国内,国外互联网行业的安全意识和技术应用相对成熟,HTTPS部署趋势是由社会、企业、政府共同去推动的。

欢迎关注公众号:前端工匠,你的成长我们一起见证!

参考文章与书籍

JS 异步编程六种方案

前言

我们知道Javascript语言的执行环境是"单线程"。也就是指一次只能完成一件任务。如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务。

这种模式虽然实现起来比较简单,执行环境相对单纯,但是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。常见的浏览器无响应(假死),往往就是因为某一段Javascript代码长时间运行(比如死循环),导致整个页面卡在这个地方,其他任务无法执行。

为了解决这个问题,Javascript语言将任务的执行模式分成两种:同步和异步。本文主要介绍异步编程几种办法,并通过比较,得到最佳异步编程的解决方案!

一、同步与异步

我们可以通俗理解为异步就是一个任务分成两段,先执行第一段,然后转而执行其他任务,等做好了准备,再回过头执行第二段。排在异步任务后面的代码,不用等待异步任务结束会马上运行,也就是说,异步任务不具有”堵塞“效应。比如,有一个任务是读取文件进行处理,异步的执行过程就是下面这样

这种不连续的执行,就叫做异步。相应地,连续的执行,就叫做同步

"异步模式"非常重要。在浏览器端,耗时很长的操作都应该异步执行,避免浏览器失去响应,最好的例子就是Ajax操作。在服务器端,"异步模式"甚至是唯一的模式,因为执行环境是单线程的,如果允许同步执行所有http请求,服务器性能会急剧下降,很快就会失去响应。接下来介绍下异步编程六种方法。

二、回调函数(Callback)

回调函数是异步操作最基本的方法。以下代码就是一个回调函数的例子:

ajax(url, () => {
    // 处理逻辑
})

但是回调函数有一个致命的弱点,就是容易写出回调地狱(Callback hell)。假设多个请求存在依赖性,你可能就会写出如下代码:

ajax(url, () => {
    // 处理逻辑
    ajax(url1, () => {
        // 处理逻辑
        ajax(url2, () => {
            // 处理逻辑
        })
    })
})

回调函数的优点是简单、容易理解和实现,缺点是不利于代码的阅读和维护,各个部分之间高度耦合,使得程序结构混乱、流程难以追踪(尤其是多个回调函数嵌套的情况),而且每个任务只能指定一个回调函数。此外它不能使用 try catch 捕获错误,不能直接 return。

三、事件监听

这种方式下,异步任务的执行不取决于代码的顺序,而取决于某个事件是否发生

下面是两个函数f1和f2,编程的意图是f2必须等到f1执行完成,才能执行。首先,为f1绑定一个事件(这里采用的jQuery的写法)

f1.on('done', f2);

上面这行代码的意思是,当f1发生done事件,就执行f2。然后,对f1进行改写:

function f1() {
  setTimeout(function () {
    // ...
    f1.trigger('done');
  }, 1000);
}

上面代码中,f1.trigger('done')表示,执行完成后,立即触发done事件,从而开始执行f2。

这种方法的优点是比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以"去耦合",有利于实现模块化。缺点是整个程序都要变成事件驱动型,运行流程会变得很不清晰。阅读代码的时候,很难看出主流程。

四、发布订阅

我们假定,存在一个"信号中心",某个任务执行完成,就向信号中心"发布"(publish)一个信号,其他任务可以向信号中心"订阅"(subscribe)这个信号,从而知道什么时候自己可以开始执行。这就叫做"发布/订阅模式"(publish-subscribe pattern),又称"观察者模式"(observer pattern)。

首先,f2向信号中心jQuery订阅done信号。

jQuery.subscribe('done', f2);

然后,f1进行如下改写:

function f1() {
  setTimeout(function () {
    // ...
    jQuery.publish('done');
  }, 1000);
}

上面代码中,jQuery.publish('done')的意思是,f1执行完成后,向信号中心jQuery发布done信号,从而引发f2的执行。
f2完成执行后,可以取消订阅(unsubscribe)

jQuery.unsubscribe('done', f2);

这种方法的性质与“事件监听”类似,但是明显优于后者。因为可以通过查看“消息中心”,了解存在多少信号、每个信号有多少订阅者,从而监控程序的运行。

五、Promise/A+

Promise本意是承诺,在程序中的意思就是承诺我过一段时间后会给你一个结果。 什么时候会用到过一段时间?答案是异步操作,异步是指可能比较长时间才有结果的才做,例如网络请求、读取本地文件等

1.Promise的三种状态

  • Pending----Promise对象实例创建时候的初始状态
  • Fulfilled----可以理解为成功的状态
  • Rejected----可以理解为失败的状态

这个承诺一旦从等待状态变成为其他状态就永远不能更改状态了,比如说一旦状态变为 resolved 后,就不能再次改变为Fulfilled

let p = new Promise((resolve, reject) => {
  reject('reject')
  resolve('success')//无效代码不会执行
})
p.then(
  value => {
    console.log(value)
  },
  reason => {
    console.log(reason)//reject
  }
)

当我们在构造 Promise 的时候,构造函数内部的代码是立即执行的

new Promise((resolve, reject) => {
  console.log('new Promise')
  resolve('success')
})
console.log('end')
// new Promise => end

2.promise的链式调用

  • 每次调用返回的都是一个新的Promise实例(这就是then可用链式调用的原因)
  • 如果then中返回的是一个结果的话会把这个结果传递下一次then中的成功回调
  • 如果then中出现异常,会走下一个then的失败回调
  • 在 then中使用了return,那么 return 的值会被Promise.resolve() 包装(见例1,2)
  • then中可以不传递参数,如果不传递会透到下一个then中(见例3)
  • catch 会捕获到没有捕获的异常

接下来我们看几个例子:

  // 例1
  Promise.resolve(1)
  .then(res => {
    console.log(res)
    return 2 //包装成 Promise.resolve(2)
  })
  .catch(err => 3)
  .then(res => console.log(res))
// 例2
Promise.resolve(1)
  .then(x => x + 1)
  .then(x => {
    throw new Error('My Error')
  })
  .catch(() => 1)
  .then(x => x + 1)
  .then(x => console.log(x)) //2
  .catch(console.error)
// 例3
let fs = require('fs')
function read(url) {
  return new Promise((resolve, reject) => {
    fs.readFile(url, 'utf8', (err, data) => {
      if (err) reject(err)
      resolve(data)
    })
  })
}
read('./name.txt')
  .then(function(data) {
    throw new Error() //then中出现异常,会走下一个then的失败回调
  }) //由于下一个then没有失败回调,就会继续往下找,如果都没有,就会被catch捕获到
  .then(function(data) {
    console.log('data')
  })
  .then()
  .then(null, function(err) {
    console.log('then', err)// then error
  })
  .catch(function(err) {
    console.log('error')
  })

Promise不仅能够捕获错误,而且也很好地解决了回调地狱的问题,可以把之前的回调地狱例子改写为如下代码:

ajax(url)
  .then(res => {
      console.log(res)
      return ajax(url1)
  }).then(res => {
      console.log(res)
      return ajax(url2)
  }).then(res => console.log(res))

它也是存在一些缺点的,比如无法取消 Promise,错误需要通过回调函数捕获。

六、生成器Generators/ yield

Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同,Generator 最大的特点就是可以控制函数的执行。

  • 语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。
  • Generator 函数除了状态机,还是一个遍历器对象生成函数
  • 可暂停函数, yield可暂停,next方法可启动,每次返回的是yield后的表达式结果
  • yield表达式本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值

我们先来看个例子:

function *foo(x) {
  let y = 2 * (yield (x + 1))
  let z = yield (y / 3)
  return (x + y + z)
}
let it = foo(5)
console.log(it.next())   // => {value: 6, done: false}
console.log(it.next(12)) // => {value: 8, done: false}
console.log(it.next(13)) // => {value: 42, done: true}

可能结果跟你想象不一致,接下来我们逐行代码分析:

  • 首先 Generator 函数调用和普通函数不同,它会返回一个迭代器
  • 当执行第一次 next 时,传参会被忽略,并且函数暂停在 yield (x + 1) 处,所以返回 5 + 1 = 6
  • 当执行第二次 next 时,传入的参数12就会被当作上一个yield表达式的返回值,如果你不传参,yield 永远返回 undefined。此时 let y = 2 * 12,所以第二个 yield 等于 2 * 12 / 3 = 8
  • 当执行第三次 next 时,传入的参数13就会被当作上一个yield表达式的返回值,所以 z = 13, x = 5, y = 24,相加等于 42

我们再来看个例子:有三个本地文件,分别1.txt,2.txt和3.txt,内容都只有一句话,下一个请求依赖上一个请求的结果,想通过Generator函数依次调用三个文件

//1.txt文件
2.txt
//2.txt文件
3.txt
//3.txt文件
结束
let fs = require('fs')
function read(file) {
  return new Promise(function(resolve, reject) {
    fs.readFile(file, 'utf8', function(err, data) {
      if (err) reject(err)
      resolve(data)
    })
  })
}
function* r() {
  let r1 = yield read('./1.txt')
  let r2 = yield read(r1)
  let r3 = yield read(r2)
  console.log(r1)
  console.log(r2)
  console.log(r3)
}
let it = r()
let { value, done } = it.next()
value.then(function(data) { // value是个promise
  console.log(data) //data=>2.txt
  let { value, done } = it.next(data)
  value.then(function(data) {
    console.log(data) //data=>3.txt
    let { value, done } = it.next(data)
    value.then(function(data) {
      console.log(data) //data=>结束
    })
  })
})
// 2.txt=>3.txt=>结束

从上例中我们看出手动迭代Generator 函数很麻烦,实现逻辑有点绕,而实际开发一般会配合 co 库去使用。co是一个为Node.js和浏览器打造的基于生成器的流程控制工具,借助于Promise,你可以使用更加优雅的方式编写非阻塞代码

安装co库只需:npm install co

上面例子只需两句话就可以轻松实现

function* r() {
  let r1 = yield read('./1.txt')
  let r2 = yield read(r1)
  let r3 = yield read(r2)
  console.log(r1)
  console.log(r2)
  console.log(r3)
}
let co = require('co')
co(r()).then(function(data) {
  console.log(data)
})
// 2.txt=>3.txt=>结束=>undefined

我们可以通过 Generator 函数解决回调地狱的问题,可以把之前的回调地狱例子改写为如下代码:

function *fetch() {
    yield ajax(url, () => {})
    yield ajax(url1, () => {})
    yield ajax(url2, () => {})
}
let it = fetch()
let result1 = it.next()
let result2 = it.next()
let result3 = it.next()

七、async/await

1.Async/Await简介

使用async/await,你可以轻松地达成之前使用生成器和co函数所做到的工作,它有如下特点:

  • async/await是基于Promise实现的,它不能用于普通的回调函数。
  • async/await与Promise一样,是非阻塞的。
  • async/await使得异步代码看起来像同步代码,这正是它的魔力所在。

一个函数如果加上 async ,那么该函数就会返回一个 Promise

async function async1() {
  return "1"
}
console.log(async1()) // -> Promise {<resolved>: "1"}

Generator函数依次调用三个文件那个例子用async/await写法,只需几句话便可实现

let fs = require('fs')
function read(file) {
  return new Promise(function(resolve, reject) {
    fs.readFile(file, 'utf8', function(err, data) {
      if (err) reject(err)
      resolve(data)
    })
  })
}
async function readResult(params) {
  try {
    let p1 = await read(params, 'utf8')//await后面跟的是一个Promise实例
    let p2 = await read(p1, 'utf8')
    let p3 = await read(p2, 'utf8')
    console.log('p1', p1)
    console.log('p2', p2)
    console.log('p3', p3)
    return p3
  } catch (error) {
    console.log(error)
  }
}
readResult('1.txt').then( // async函数返回的也是个promise
  data => {
    console.log(data)
  },
  err => console.log(err)
)
// p1 2.txt
// p2 3.txt
// p3 结束
// 结束

2.Async/Await并发请求

如果请求两个文件,毫无关系,可以通过并发请求

let fs = require('fs')
function read(file) {
  return new Promise(function(resolve, reject) {
    fs.readFile(file, 'utf8', function(err, data) {
      if (err) reject(err)
      resolve(data)
    })
  })
}
function readAll() {
  read1()
  read2()//这个函数同步执行
}
async function read1() {
  let r = await read('1.txt','utf8')
  console.log(r)
}
async function read2() {
  let r = await read('2.txt','utf8')
  console.log(r)
}
readAll() // 2.txt 3.txt

八、总结

1.JS 异步编程进化史:callback -> promise -> generator -> async + await

2.async/await 函数的实现,就是将 Generator 函数和自动执行器,包装在一个函数里。

3.async/await可以说是异步终极解决方案了。

(1) async/await函数相对于Promise,优势体现在:

  • 处理 then 的调用链,能够更清晰准确的写出代码
  • 并且也能优雅地解决回调地狱问题。

当然async/await函数也存在一些缺点,因为 await 将异步代码改造成了同步代码,如果多个异步代码没有依赖性却使用了 await 会导致性能上的降低,代码没有依赖性的话,完全可以使用 Promise.all 的方式。
(2) async/await函数对 Generator 函数的改进,体现在以下三点:

  • 内置执行器。

Generator 函数的执行必须靠执行器,所以才有了 co 函数库,而 async 函数自带执行器。也就是说,async 函数的执行,与普通函数一模一样,只要一行

  • 更广的适用性。

co 函数库约定,yield 命令后面只能是 Thunk 函数或 Promise 对象,而 async 函数的 await 命令后面,可以跟 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)

  • 更好的语义。

async 和 await,比起星号和 yield,语义更清楚了。async 表示函数里有异步操作,await 表示紧跟在后面的表达式需要等待结果。

参考文章

Ajax请求后台数据

本文框架图

一、前期准备

安装好XAMPP软件,并运行起来。本文代码是基于XAMPP开发环境,XAMPP是完全免费且易于安装的Apache发行版,其中包含MariaDB、PHP和Perl。XAMPP开放源码包的设置让安装和使用出奇容易。

二、前后台数据交互

前台部分
其中“process.php?name=Herry”,向后台传递name数据

 document.getElementById("button").addEventListener("click",function () {
        var xhr = new XMLHttpRequest();
        xhr.open("GET","process.php?name=Herry",true);
        xhr.onreadystatechange=function () {
            if(xhr.readyState==4&&xhr.status==200) {
                var data = xhr.responseText;
                console.log(data)
            }
        };
        xhr.send();
    })

后台PHP部分
后台接收了name数值,并向前台返回了"GET: 你的名字是". $_GET['name']

<?php 
   	if (isset($_GET['name'])) {
   		echo "GET: 你的名字是". $_GET['name'];
   	}
?>

于是最后前台console里面得到:GET: 你的名字是Herry

三、正常表单提交到PHP与Ajax方式提交

正常表单GET提交数据到PHP

前台部分

<form action="process.php" method="GET">
    <input type="text" name="name">
    <input type="submit" value="提交">
</form>

后台PHP部分

<?php
   	if (isset($_GET['name'])) {
   		echo "GET: 你的名字是". $_GET['name'];
   	}
?>

表单输入名字Bucky,然后点击提交后,浏览器将数据打包后,传递给后台,最后后台返回我们想要的数据----GET: 你的名字是Bucky。整个过程中页面有刷新,数据点击提交后,页面跳转到这个网址http://localhost/ajax/process.php?name=Bucky

Ajax请求后台数据GET

Ajax异步请求数据,无需刷新页面。可以提高用户体验,较少网络数据的传输量。click事件改成submit事件(表单应该用submit事件),然后取消默认事件。

前台部分

//Html部分
<form id="getForm">
    <input type="text"name="name" id="name1">
    <input type="submit"value="提交">
</form>
//Javascript部分
 document.getElementById("getForm").addEventListener("submit",function(e){
        e.preventDefault();//阻止默认跳转事件
        var name=document.getElementById("name1").value;//获取输入的值
        var xhr = new XMLHttpRequest();
        xhr.open("GET","process.php?name="+name,true);
        xhr.onreadystatechange=function () {
            if (  xhr.status == 200&&xhr.readyState == 4) {
                var data = xhr.responseText;
                console.log(data);
            }
        }
            xhr.send();
    })

后台PHP部分

<?php
   	if (isset($_GET['name'])) {
   		echo "GET: 你的名字是". $_GET['name'];
   	}
?>

在表单输入Bucky,点击提交,最后在console显示:GET: 你的名字是Bucky。整个过程页面无刷新,有效提高页面性能。

正常表单POST提交数据到PHP

与GET提交数据差不多
前台部分

<form action="process.php" method="POST">
    <input type="text" name="name">
    <input type="submit" value="提交">
</form>

后台PHP部分

<?php
   	if (isset($_POST['name'])) {
       	echo "POST: 你的名字是". $_POST['name'];
       	}
?>

表单输入名字Bucky,然后点击提交后,浏览器将数据打包后,传递给后台,最后后台返回我们想要的数据----POST: 你的名字是Bucky。整个过程中页面有刷新,数据点击提交后,页面跳转到这个网址http://localhost/ajax/process.php。与GET方式提交不同的是,POST方法数据并没有内嵌在url中,这样安全性比较高。

Ajax请求后台数据POST

POST请求与GET主要有两点不同:

①post请求一定要设置请求头的格式内容:

xhr.setRequestHeader("Content-type","application/x-www-form-urlencoded");  

②post请求参数放在send里面,即请求体

xhr.send("name="+name" );  

前台部分

//HTML部分
<form id="postForm">
    <input type="text"name="name" id="name2">
    <input type="submit"value="提交">
</form>
//Javascript部分
  document.getElementById("postForm").addEventListener("submit", function (e) {
        e.preventDefault();
        var name=document.getElementById("name2").value;
        var params = "name="+name;
        var xhr = new XMLHttpRequest();
        xhr.open("POST","process.php",true);
        xhr.setRequestHeader("Content-type","application/x-www-form-urlencoded");
        xhr.onreadystatechange=function () {
            if(xhr.readyState==4&&xhr.status==200) {
                var data = xhr.responseText;
                console.log(data);
            }
        };
        xhr.send(params);
    })

后台PHP部分

<?php
     if (isset($_POST['name'])) {
        echo "POST: 你的名字是". $_POST['name'];
        }
?>

表单输入名字Bucky,然后点击提交后,最后在console显示:POST: 你的名字是Bucky。整个过程页面无刷新。

四、总结

1.在不需要重新刷新页面的情况下,Ajax 通过异步请求加载后台数据,提高用户体验和页面性能。
2.GET参数通过URL传递,POST放在Request body中,后者安全性比较高。

页面性能优化办法有哪些?

引子

互联网有一项著名的8秒原则。用户在访问Web网页时,如果时间超过8秒就会感到不耐烦,如果加载需要太长时间,他们就会放弃访问。大部分用户希望网页能在2秒之内就完成加载。事实上,加载时间每多1秒,你就会流失7%的用户。8秒并不是准确的8秒钟,只是向网站开发者表明了加载时间的重要性。那我们如何优化页面性能,提高页面加载速度呢?这是本文主要要探讨的问题,然而性能优化是个综合性问题,没有标准答案,想要面面俱到罗列出来,并非易事。

本文只关注一些核心要点,以下是我结合慕课网课程《Web前端性能优化》,总结性能优化常见的办法:

一、资源压缩与合并

主要包括这些方面:html压缩、css 压缩、js的压缩和混乱和文件合并
资源压缩可以从文件中去掉多余的字符,比如回车、空格。你在编辑器中写代码的时候,会使用缩进和注释,这些方法无疑会让你的代码简洁而且易读,但它们也会在文档中添加多余的字节。

1.html压缩

html代码压缩就是压缩这些在文本文件中有意义,但是在HTML中不显示的字符,包括空格,制表符,换行符等,还有一些其他意义的字符,如HTML注释也可以被压缩。

如何进行html压缩:

  1. 使用在线网站进行压缩(开发过程中一般不用)
  2. nodejs 提供了html-minifier工具
  3. 后端模板引擎渲染压缩

2.css代码压缩:

css代码压缩简单来说就是无效代码删除和css语义合并

如何进行css压缩:

  1. 使用在线网站进行压缩(开发过程中一般不用)
  2. 使用html-minifier工具
  3. 使用clean-css对css压缩

css 压缩

3.js的压缩和混乱

js的压缩和混乱主要包括以下这几部分:

  1. 无效字符的删除
  2. 剔除注释
  3. 代码语义的缩减和优化
  4. 代码保护(代码逻辑变得混乱,降低代码的可读性,这点很重要)

如何进行js的压缩和混乱

  1. 使用在线网站进行压缩(开发过程中一般不用)
  2. 使用html-minifier工具
  3. 使用uglifyjs2对js进行压缩

其实css压缩与js的压缩和混乱比html压缩收益要大得多,同时css代码和js代码比html代码多得多,通过css压缩和js压缩带来流量的减少,会非常明显。所以对大公司来说,html压缩可有可无,但css压缩与js的压缩和混乱必须要有!

4.文件合并


从上图可以看出不合并请求有以下缺点:

  • 文件与文件之间有插入的上行请求,增加了N-1个网络延迟
  • 受丢包问题影响更严重
  • keep-alive方式可能会出现状况,经过代理服务器时可能会被断开,也就是说不能一直保持keep-alive的状态

压缩合并css和js可以减少网站http请求的次数,但合并文件可能会带来问题:首屏渲染和缓存失效问题。那该如何处理这问题呢?----公共库合并和不同页面的合并。

如何进行文件合并

  1. 使用在线网站进行文件合并
  2. 使用nodejs实现文件合并(gulp、fis3)

二、非核心代码异步加载异步加载的方式

1、异步加载的方式

异步加载的三种方式——async和defer、动态脚本创建

① async方式

  • async属性是HTML5新增属性,需要Chrome、FireFox、IE9+浏览器支持
  • async属性规定一旦脚本可用,则会异步执行
  • async属性仅适用于外部脚本
  • 如果是多个脚本,该方法不能保证脚本按顺序执行
<script type="text/javascript" src="xxx.js" async="async"></script>

② defer方式

  • 兼容所有浏览器
  • defer属性规定是否对脚本执行进行延迟,直到页面加载为止
  • 如果是多个脚本,该方法可以确保所有设置了defer属性的脚本按顺序执行
  • 如果脚本不会改变文档的内容,可将defer属性加入到script标签中,以便加快处理文档的速度

③动态创建script标签
在还没定义defer和async前,异步加载的方式是动态创建script,通过window.onload方法确保页面加载完毕再将script标签插入到DOM中,具体代码如下:

function addScriptTag(src){  
    var script = document.createElement('script');  
    script.setAttribute("type","text/javascript");  
    script.src = src;  
    document.body.appendChild(script);  
}  
window.onload = function(){  
    addScriptTag("js/index.js");  
}  

2、异步加载的区别

1)defer是在HTML解析完之后才会执行,如果是多个,按照加载的顺序依次执行

2)async是在加载完之后立即执行,如果是多个,执行顺序和加载顺序无关
async和defer

其中蓝色线代表网络读取,红色线代表执行时间,这俩都是针对脚本的;绿色线代表 HTML 解析。

三、利用浏览器缓存

对于web应用来说,缓存是提升页面性能同时减少服务器压力的利器。

浏览器缓存类型

1.强缓存:不会向服务器发送请求,直接从缓存中读取资源,在chrome控制台的network选项中可以看到该请求返回200的状态码,并且size显示from disk cache或from memory cache;

相关的header:

Expires :response header里的过期时间,浏览器再次加载资源时,如果在这个过期时间内,则命中强缓存。它的值为一个绝对时间的GMT格式的时间字符串, 比如Expires:Thu,21 Jan 2018 23:39:02 GMT

Cache-Control :这是一个相对时间,在配置缓存的时候,以秒为单位,用数值表示。当值设为max-age=300时,则代表在这个请求正确返回时间(浏览器也会记录下来)的5分钟内再次加载资源,就会命中强缓存。比如Cache-Control:max-age=300,

简单概括:其实这两者差别不大,区别就在于 Expires 是http1.0的产物,Cache-Control是http1.1的产物,两者同时存在的话,Cache-Control优先级高于Expires;在某些不支持HTTP1.1的环境下,Expires就会发挥用处。所以Expires其实是过时的产物,现阶段它的存在只是一种兼容性的写法。强缓存判断是否缓存的依据来自于是否超出某个时间或者某个时间段,而不关心服务器端文件是否已经更新,这可能会导致加载文件不是服务器端最新的内容,那我们如何获知服务器端内容较客户端是否已经发生了更新呢?此时我们需要协商缓存策略。

2.协商缓存:向服务器发送请求,服务器会根据这个请求的request header的一些参数来判断是否命中协商缓存,如果命中,则返回304状态码并带上新的response header通知浏览器从缓存中读取资源;另外协商缓存需要与cache-control共同使用。

相关的header:

①Last-Modified和If-Modified-Since:当第一次请求资源时,服务器将资源传递给客户端时,会将资源最后更改的时间以“Last-Modified: GMT”的形式加在实体首部上一起返回给客户端。

Last-Modified: Fri, 22 Jul 2016 01:47:00 GMT

客户端会为资源标记上该信息,下次再次请求时,会把该信息附带在请求报文中一并带给服务器去做检查,若传递的时间值与服务器上该资源最终修改时间是一致的,则说明该资源没有被修改过,直接返回304状态码,内容为空,这样就节省了传输数据量 。如果两个时间不一致,则服务器会发回该资源并返回200状态码,和第一次请求时类似。这样保证不向客户端重复发出资源,也保证当服务器有变化时,客户端能够得到最新的资源。一个304响应比一个静态资源通常小得多,这样就节省了网络带宽。
Last-Modified和If-Modified-Since

但last-modified 存在一些缺点:

Ⅰ.某些服务端不能获取精确的修改时间

Ⅱ.文件修改时间改了,但文件内容却没有变

既然根据文件修改时间来决定是否缓存尚有不足,能否可以直接根据文件内容是否修改来决定缓存策略?----ETag和If-None-Match

②ETag和If-None-Match:Etag是上一次加载资源时,服务器返回的response header,是对该资源的一种唯一标识,只要资源有变化,Etag就会重新生成。浏览器在下一次加载资源向服务器发送请求时,会将上一次返回的Etag值放到request header里的If-None-Match里,服务器只需要比较客户端传来的If-None-Match跟自己服务器上该资源的ETag是否一致,就能很好地判断资源相对客户端而言是否被修改过了。如果服务器发现ETag匹配不上,那么直接以常规GET 200回包形式将新的资源(当然也包括了新的ETag)发给客户端;如果ETag是一致的,则直接返回304知会客户端直接使用本地缓存即可。

ETag和If-None-Match

两者之间对比
首先在精确度上,Etag要优于Last-Modified。Last-Modified的时间单位是秒,如果某个文件在1秒内改变了多次,那么他们的Last-Modified其实并没有体现出来修改,但是Etag每次都会改变确保了精度;如果是负载均衡的服务器,各个服务器生成的Last-Modified也有可能不一致。
第二在性能上,Etag要逊于Last-Modified,毕竟Last-Modified只需要记录时间,而Etag需要服务器通过算法来计算出一个hash值。
第三在优先级上,服务器校验优先考虑Etag

缓存的机制

强制缓存优先于协商缓存进行,若强制缓存(Expires和Cache-Control)生效则直接使用缓存,若不生效则进行协商缓存(Last-Modified / If-Modified-Since和Etag / If-None-Match),协商缓存由服务器决定是否使用缓存,若协商缓存失效,那么代表该请求的缓存失效,重新获取请求结果,再存入浏览器缓存中;生效则返回304,继续使用缓存。主要过程如下:

缓存的机制

用户行为对浏览器缓存的影响

1.地址栏访问,链接跳转是正常用户行为,将会触发浏览器缓存机制;

2.F5刷新,浏览器会设置max-age=0,跳过强缓存判断,会进行协商缓存判断;

3.ctrl+F5刷新,跳过强缓存和协商缓存,直接从服务器拉取资源。

如果想了解更多缓存机制,请猛戳 深入理解浏览器的缓存机制

四、使用CDN

大型Web应用对速度的追求并没有止步于仅仅利用浏览器缓存,因为浏览器缓存始终只是为了提升二次访问的速度,对于首次访问的加速,我们需要从网络层面进行优化,最常见的手段就是CDN(Content Delivery Network,内容分发网络)加速。通过将静态资源(例如javascript,css,图片等等)缓存到离用户很近的相同网络运营商的CDN节点上,不但能提升用户的访问速度,还能节省服务器的带宽消耗,降低负载。

遍布全国的CDN节点和内容源示意图

CDN是怎么做到加速的呢?

其实这是CDN服务商在全国各个省份部署计算节点,CDN加速将网站的内容缓存在网络边缘,不同地区的用户就会访问到离自己最近的相同网络线路上的CDN节点,当请求达到CDN节点后,节点会判断自己的内容缓存是否有效,如果有效,则立即响应缓存内容给用户,从而加快响应速度。如果CDN节点的缓存失效,它会根据服务配置去我们的内容源服务器获取最新的资源响应给用户,并将内容缓存下来以便响应给后续访问的用户。因此,一个地区内只要有一个用户先加载资源,在CDN中建立了缓存,该地区的其他后续用户都能因此而受益

五、预解析DNS

资源预加载是另一个性能优化技术,我们可以使用该技术来预先告知浏览器某些资源可能在将来会被使用到。
通过 DNS 预解析来告诉浏览器未来我们可能从某个特定的 URL 获取资源,当浏览器真正使用到该域中的某个资源时就可以尽快地完成 DNS 解析。例如,我们将来可从 example.com 获取图片或音频资源,那么可以在文档顶部的 标签中加入以下内容:

<link rel="dns-prefetch" href="//example.com">

当我们从该 URL 请求一个资源时,就不再需要等待 DNS 的解析过程。该技术对使用第三方资源特别有用。通过简单的一行代码就可以告知那些兼容的浏览器进行 DNS 预解析,这意味着当浏览器真正请求该域中的某个资源时,DNS 的解析就已经完成了,从而节省了宝贵的时间。
另外需要注意的是,浏览器会对a标签的href自动启用DNS Prefetching,所以a标签里包含的域名不需要在head中手动设置link。但是在HTTPS下不起作用,需要meta来强制开启功能。这个限制的原因是防止窃听者根据DNS Prefetching推断显示在HTTPS页面中超链接的主机名。下面这句话作用是强制打开a标签域名解析

<meta http-equiv="x-dns-prefetch-control" content="on">

参考

浏览器存储

前言

随着移动网络的发展与演化,我们手机上现在除了有原生 App,还能跑“WebApp”——它即开即用,用完即走。一个优秀的 WebApp 甚至可以拥有和原生 App 媲美的功能和体验。WebApp 优异的性能表现,有一部分原因要归功于浏览器存储技术的提升。cookie存储数据的功能已经很难满足开发所需,逐渐被Web Storage、IndexedDB所取代,本文将介绍这几种存储方式的差异和优缺点。

一、cookie

1.cookie的来源

cookie 的本职工作并非本地存储,而是“维持状态”。因为HTTP协议是无状态的,HTTP协议自身不对请求和响应之间的通信状态进行保存,通俗来说,服务器不知道用户上一次做了什么,这严重阻碍了交互式Web应用程序的实现。在典型的网上购物场景中,用户浏览了几个页面,买了一盒饼干和两瓶饮料。最后结帐时,由于HTTP的无状态性,不通过额外的手段,服务器并不知道用户到底买了什么,于是就诞生了cookie。它就是用来绕开HTTP的无状态性的“额外手段”之一。服务器可以设置或读取cookie中包含信息,借此维护用户跟服务器会话中的状态。

在刚才的购物场景中,当用户选购了第一项商品,服务器在向用户发送网页的同时,还发送了一段cookie,记录着那项商品的信息。当用户访问另一个页面,浏览器会把cookie发送给服务器,于是服务器知道他之前选购了什么。用户继续选购饮料,服务器就在原来那段Cookie里追加新的商品信息。结帐时,服务器读取发送来的cookie就行了。

2.什么是cookie

cookie指某些网站为了辨别用户身份而储存在用户本地终端上的数据(通常经过加密)。 cookie是服务端生成,客户端进行维护和存储,存储在内存或者磁盘中。通过cookie,可以让服务器知道请求是来源哪个客户端,就可以进行客户端状态的维护,比如登陆后刷新,请求头就会携带登陆时response header中的Set-Cookie,Web服务器接到请求时也能读出cookie的值,根据cookie值的内容就可以判断和恢复一些用户的信息状态。

简而言之,cookie 使基于无状态的HTTP协议记录稳定的状态信息成为了可能。

cookie 主要用于以下三个方面:

  • 会话状态管理(如用户登录状态、购物车、游戏分数或其它需要记录的信息)
  • 个性化设置(如用户自定义设置、主题等)
  • 浏览器行为跟踪(如跟踪分析用户行为等)

3.cookie的原理及其构成

简单的HTTP请求过程

第一次访问网站的时候,浏览器发出请求,服务器端生成 cookie在响应中通过Set-Cookie头部告知客户端(允许多个Set-Cookie头部传递多个值),客户端得到 cookie后,后续请求都会自动将 cookie头部携带至请求中发送给服务器(见下面例子),另外,cookie的过期时间、域、路径、有效期、适用站点都可以根据需要来指定。

// 一个HTTP响应:
HTTP/1.1 200 OK
Content-type: text/html
Set-Cookie: name=value
Other-header: other-header-value

这个HTTP响应会设置一个名为"name",值为"value"的cookie。名和值在发送时都会经过URL编码。浏览器会存储这些会话信息,并在之后的每个请求中都会通过HTTP头部cookie再将它们发回服务器,比如:

GET /index.jsl HTTP/1.1
Cookie: name=value
Other-header: other-header-value

cookie在浏览器中是由以下参数构成的:

  • name:唯一标识cookie的名称。cookie名不区分大小写,因此myCookie和MyCookie是同一个名称。不过,实践中最好将cookie名当成区分大小写来对待,因为一些服务器软件可能这样对待它们。cookie名必须经过URL编码
  • value:存储在cookie里的字符串值。这个值必须经过URL编码
  • Domain:cookie有效的域。发送到这个域的所有请求都会包含对应的cookie。如果不指定,默认为文档来源(由协议、域名和端口共同定义),不包含子域名。如果指定了Domain,则一般包含子域名。因此,指定 Domain 比省略它的限制要少。但是,当子域需要共享有关用户的信息时,这可能会有所帮助。例如,如果设置 Domain=mozilla.org,则 Cookie 也包含在子域名中(如developer.mozilla.org)。
  • Path:请求URL中包含这个路径才会把cookie发送到服务器。
// 例如,设置 Path=/docs,则以下地址都会匹配:
/docs
/docs/Web/
/docs/Web/HTTP
  • Expires/Max-Age:设置cookie过期时间(Expires)或有效期(Max-Age)(即什么时间之后就不发送到服务器了)。简单名/值对形式的cookie只在当前会话期间存在,用户关闭浏览器就会丢失。如果想让cookie的生命周期超过单个浏览对话,那就指定Expires/Max-Age,max-age优先级高于expires。
  • Secure:设置之后,只在使用SSL安全连接的情况下才会把cookie发送到服务器。例如,请求https://www.wrox.com会发送cookie,而请求http://www.wrox.com则不会。
  • HttpOnly:设置了 HttpOnly 属性的 cookie 不能使用 JavaScript 经由 Document.cookie 属性、XMLHttpRequest 和 Request APIs 进行访问,以防范跨站脚本攻击(XSS)。
HTTP/1.1 200 OK
Content-type: text/html
Set-Cookie: name=value; domain=.wrox.com; path=/; secure
Other-header: other-header-value

这里创建的cookie对所有wrox.com的子域及该域中的所有页面有效(通过path=/指定)。不过,这个cookie只能在SSL连接上发送,因为设置了secure标志。

要知道,域、路径、过期时间和secure标志用于告诉浏览器什么情况下应该在请求中包含cookie。这些参数并不会随请求发送给服务器,实际发送的只有cookie的名/值对

4.Javascript 中的cookie

一般说来,cookie的生成方式主要有两种,一种是上文提到的在响应中通过Set-Cookie头部告知客户端;另外一种就是在JavaScript中可以通过document.cookie可以读写cookie,如下:

//读取浏览器中的cookie
console.log(document.cookie);
//写入cookie
document.cookie='myname=langlixingzhou;path=/;domain=.baidu.com';

在JavaScript中处理cookie比较麻烦,因为接口过于简单,只有BOM的document.cookie属性。在设置值时,可以通过document.cookie属性设置新的cookie字符串。这个字符串在被解析后会添加到原有cookie中。设置document.cookie不会覆盖之前存在的任何cookie,除非设置了已有的cookie。要为创建的cookie指定额外的信息,只要像Set-Cookie头部一样直接在后面追加相同格式的字符串即可:

document.cookie = encodeURIComponent("name") + "=" +
                  encodeURIComponent("Nicholas") + "; domain=.wrox.com; path=/";
// 使用encodeURIComponent()对名称和值进行编码

5.cookie的缺陷

  • cookie 不够大

每个cookie的大小为4KB(名字和值都包含在这4KB之内),对于复杂的存储需求来说是不够用的。当 cookie 超过 4KB 时,它将面临被裁切的命运。这样看来,cookie 只能用来存取少量的信息。此外很多浏览器对一个站点的cookie个数也是有限制的(一般来说不超过300个cookie)。

  • 过多的 cookie 会带来巨大的性能浪费

cookie是与特定域绑定的。同一个域名下的所有请求,都会携带 cookie。大家试想,如果我们此刻仅仅是请求一张图片或者一个 CSS 文件,我们也要携带一个 cookie 跑来跑去(关键是 cookie 里存储的信息并不需要),这是一件多么劳民伤财的事情。cookie 虽然小,但随着请求的叠加,这样的不必要的 cookie 带来的开销将是无法想象的。

cookie是用来维护用户信息的,而域名(domain)下所有请求都会携带cookie,但对于静态文件的请求,携带cookie信息根本没有用,此时可以通过CDN(存储静态文件的)的域名和主站的域名分开来解决。

  • 由于在HTTP请求中的cookie是明文传递的,所以安全性成问题,除非用HTTPS。

6.cookie与安全

有两种方法可以确保 cookie 被安全发送,并且不会被意外的参与者或脚本访问:Secure 属性和HttpOnly 属性。

标记为 Secure 的 cookie 只应通过被 HTTPS 协议加密过的请求发送给服务端,因此可以预防中间人攻击。但即便设置了 Secure 标记,敏感信息也不应该通过 cookie 传输,因为 cookie 有其固有的不安全性,Secure 标记也无法提供确实的安全保障, 例如,可以访问客户端硬盘的人可以读取它。

从 Chrome 52 和 Firefox 52 开始,不安全的站点(http:)无法使用cookie的 Secure 标记。

JavaScript  Document.cookie API 无法访问带有 HttpOnly 属性的cookie;此类 cookie 仅作用于服务器。例如,持久化服务器端会话的 cookie 不需要对 JavaScript 可用,而应具有 HttpOnly 属性。此预防措施有助于缓解跨站点脚本(XSS)攻击。

Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2019 07:28:00 GMT; Secure; HttpOnly

对cookie的限制及其特性决定了cookie并不是存储大量数据的理想方式,让“专业的人做专业的事情”,Web Storage 出现了。

HTML5中新增了本地存储的解决方案----Web Storage,这样有了Web Storage后,cookie能只做它应该做的事情了—— 作为客户端与服务器交互的通道,保持客户端状态。

二、Web Storage

Web Storage的目的是解决通过客户端存储不需要频繁发送回服务器的数据时使用cookie的问题。Web Storage API包含了两个对象:localStorage和sessionStorage,本质上是映射字符串键和值的对象化。localStorage是永久存储机制,sessionStorage是跨会话的存储机制。这两种浏览器存储API提供了在浏览器中不受页面刷新影响而存储数据的两种方式。

1、Storage 对象

Window 对象的localStorage 和 sessionStorage 属性引用的是 Storage对象。Storage对象用于保存名/值对数据,直至存储空间上限(由浏览器决定)。一般来说,客户端数据的大小限制是按照每个源(协议、域和端口)来设置的,因此每个源有固定大小的数据存储空间。不同浏览器给localStorage和sessionStorage设置了不同的空间限制,但大多数会限制为每个源5MB。

Storage对象定义了如下方法:

  • clear():删除所有值;不在Firefox中实现。
  • getItem(name):取得给定name的值。
  • key(index):取得给定数值位置的名称。
  • removeItem(name):删除给定name的名/值对。
  • setItem(name, value):设置给定name的值。

Storage 对象中的键值对总是以字符串的形式存储,这意味着数值类型会自动转化为字符串类型。

2、sessionStorage

sessionStorage对象只存储会话数据,这意味着数据只会存储到浏览器关闭。这跟浏览器关闭时会消失的会话cookie类似。存储在sessionStorage中的数据不受页面刷新影响,可以在浏览器崩溃并重启后恢复(取决于浏览器,Firefox和WebKit支持,IE不支持)。

sessionStorage 特别应该注意一点就是,即便是相同域名下的两个页面,只要它们不在同一个浏览器窗口中打开,那么它们的 sessionStorage 数据便无法共享。

localStorage 与 sessionStorage 在 API 方面无异,这里我们以 sessionStorage 为例:

  • 存储数据:setItem()
sessionStorage.setItem('user_name', 'juejin')
  • 读取数据: getItem()
sessionStorage.getItem('user_name')
  • 删除某一键名对应的数据: removeItem()
sessionStorage.removeItem('user_name')
  • 清空数据记录:clear()
sessionStorage.clear()

虽然Web Storage存储数据会带来诸多便利,但实际开发中�使用它也有不便之处:

  • sessionStorage本身有API,但是只是简单的 key/value形式
  • sessionStorage只存储字符串,需要转换成json对象

基于上面两点,开发过程中会对它进行封装后再调用:

 // 碍于文章篇幅,并未将完整代码展示出来
 // 想要获取完整的代码,可以加wx:qqlcx55
 // 将属性存储在某一模块下
  setItem(key,value,module_name){
    if (module_name){
      let val = this.getItem(module_name);
      val[key] = value;
      this.setItem(module_name, val);
    }else{
      let val = this.getStorage();
      val[key] = value;
      window.sessionStorage.setItem(STORAGE_KEY, JSON.stringify(val));
    }
  },
  // 获取某一个模块下面的属性
  getItem(key,module_name){
    if (module_name){
      let val = this.getItem(module_name);
      if(val) return val[key];
    }
    return this.getStorage()[key];
  },
  getStorage(){
    return JSON.parse(window.sessionStorage.getItem(STORAGE_KEY) || '{}');
  }

3、localStorage

localStorage 类似 sessionStorage,但其区别在于:存储在 localStorage 的数据可以长期保留;而当页面会话结束(即当页面被关闭时),存储在 sessionStorage 的数据会被清除 。要访问同一个localStorage对象,页面必须来自同一个域(子域不可以)、在相同的端口上使用相同的协议。

考虑到 localStorage 的特点之一是持久,有时我们更倾向于用它来存储一些内容稳定的资源。比如图片内容丰富的电商网站会用它来存储 Base64 格式的图片字符串:

Base64 格式的图片字符串

有的网站还会用它存储一些不经常更新的 CSS、JS 等静态资源。

4、Web Storage与cookie 之间的区别

我们先来说说两者的共同点,然后再细说下哪些地方有区别:

  • 共同点:都是保存在浏览器端,且都遵循同源策略。
  • 不同点:在于生命周期与作用域的不同

作用域:localStorage只要在相同的协议、相同的主机名、相同的端口下,就能读取/修改到同一份localStorage数据。不过sessionStorage比localStorage更严苛一点,除了协议、主机名、端口外,还要求在同一窗口(也就是浏览器的标签页)下


生命周期:localStorage 是持久化的本地存储,存储在其中的数据是永远不会过期的,使其消失的唯一办法是手动删除;而 sessionStorage 是临时性的本地存储,它是会话级别的存储,当会话结束(页面被关闭)时,存储内容也随之被释放。

说到底,Web Storage 是对 Cookie 的拓展,它只能用于存储少量的简单数据。当遇到大规模的、结构复杂的数据时,Web Storage 也爱莫能助了。这时候我们就要清楚我们的终极大 boss——IndexedDB!

四、IndexedDB

Indexed Database API简称IndexedDB,是浏览器中存储结构化数据的一个方案。IndexedDB背后的**是创造一套API,方便JavaScript对象的存储和获取,同时也支持查询和搜索。

IndexedDB是类似于MySQL或Web SQL Database的数据库。与传统数据库最大的区别在于,IndexedDB使用对象存储而不是表格保存数据。IndexedDB数据库就是在一个公共命名空间下的一组对象存储,类似于NoSQL风格的实现。既然是数据库了,那就不是 5M、10M 这样小打小闹级别了。理论上来说,IndexedDB 是没有存储上限的(一般来说不会小于 250M)。

1.IndexedDB的特点

  • 键值对储存。

IndexedDB 内部采用对象仓库(object store)存放数据。所有类型的数据都可以直接存入,包括 JavaScript 对象。对象仓库中,数据以"键值对"的形式保存,每一个数据记录都有对应的主键,主键是独一无二的,不能有重复,否则会抛出一个错误。

  • 异步

IndexedDB的设计几乎完全是异步的。为此,大多数操作以请求的形式执行,这些请求会异步执行,产生成功的结果或错误。绝大多数IndexedDB操作要求添加onerror和onsuccess事件处理程序来确定输出。IndexedDB 操作时不会锁死浏览器,用户依然可以进行其他操作,这与 localStorage 形成对比,后者的操作是同步的。异步设计是为了防止大量数据的读写,拖慢网页的表现。

  • 支持事务。

IndexedDB 支持事务(transaction),这意味着一系列操作步骤之中,只要有一步失败,整个事务就都取消,数据库回滚到事务发生之前的状态,不存在只改写一部分数据的情况。

  • 同源限制

IndexedDB 受到同源限制,每一个数据库对应创建它的域名。网页只能访问自身域名下的数据库,而不能访问跨域的数据库。

  • 储存空间大

IndexedDB 的储存空间比 localStorage 大得多,一般来说不少于 250MB,甚至没有上限。

  • 支持二进制储存。

IndexedDB 不仅可以储存字符串,还可以储存二进制数据(ArrayBuffer 对象和 Blob 对象)。

2.IndexedDB 使用流程

在IndexedDB大部分操作并不是我们常用的调用方法,返回结果的模式,而是请求——响应的模式。

接下来,通过一个基本的 IndexedDB 使用流程,旨在对 IndexedDB 形成一个感性的认知:

  • 打开/创建一个 IndexedDB 数据库(当该数据库不存在时,open 方法会直接创建一个名为 admin 新数据库)
  // 后面的回调中,我们可以通过event.target.result拿到数据库实例
  let db
  // 参数1位数据库名,参数2为版本号
  const request = window.indexedDB.open("admin", 1)
  // 使用IndexedDB失败时的监听函数
  request.onerror = function(event) {
     console.log('无法使用IndexedDB')
   }
  // 成功
  request.onsuccess  = function(event){
    // 此处就可以获取到db实例
    db = event.target.result
    console.log("你打开了IndexedDB")
  }
  • 创建一个 object store(object store 对标到数据库中的“表”单位)
// onupgradeneeded事件会在初始化数据库/版本发生更新时被调用,我们在它的监听函数中创建object store
request.onupgradeneeded = function(event){
  let objectStore
  // 如果同名表未被创建过,则新建test表
  if (!db.objectStoreNames.contains('test')) {
    objectStore = db.createObjectStore('test', { keyPath: 'id' })
  }
}  
  • 构建一个事务来执行一些数据库操作,像增加或提取数据等
  // 创建事务,指定表格名称和读写权限
  const transaction = db.transaction(["test"],"readwrite")
  // 拿到Object Store对象
  const objectStore = transaction.objectStore("test")
  // 向表格写入数据
  objectStore.add({id: 1, name: 'juejin'})
  • 通过监听正确类型的事件以等待操作完成。
  // 操作成功时的监听函数
  transaction.oncomplete = function(event) {
    console.log("操作成功")
  }
  // 操作失败时的监听函数
  transaction.onerror = function(event) {
    console.log("这里有一个Error")
  }

3.Web Storage、cookie 和 IndexedDB之间的区别

有了这些存储手段,就可以在客户端通过使用JavaScript存储可观的数据。因为这些数据没有加密,所以要注意不能使用它们存储敏感信息。

总结

正是浏览器存储、缓存技术的出现和发展,为我们的前端应用带来了无限的转机。近年来基于存储、缓存技术的第三方库层出不绝,此外还衍生出了 PWA 这样优秀的 Web 应用模型。总结下本文几个核心观点:

  • Cookie 的本职工作并非本地存储,而是“维持状态”。
  • Web Storage定义了两个对象用于存储数据:sessionStorage和localStorage。前者用于严格保存浏览器一次会话期间的数据,因为数据会在浏览器关闭时被删除。后者用于会话之外持久保存数据。
  • IndexedDB是类似于SQL数据库的结构化数据存储机制。不同的是,IndexedDB存储的是对象,而不是数据表。

本文于2021年3.28日修改后重修发布!

参考文章

深入理解JavaScript执行上下文和执行栈

前言

如果你是一名 JavaScript 开发者,或者想要成为一名 JavaScript 开发者,那么你必须知道 JavaScript 程序内部的执行机制。执行上下文和执行栈是JavaScript中关键概念之一,是JavaScript难点之一。 理解执行上下文和执行栈同样有助于理解其他的 JavaScript 概念如提升机制、作用域和闭包等。本文尽可能用通俗易懂的方式来介绍这些概念。

一、执行上下文(Execution Context)

1.什么是执行上下文

简而言之,执行上下文就是当前 JavaScript 代码被解析和执行时所在环境的抽象概念, JavaScript 中运行任何的代码都是在执行上下文中运行

2.执行上下文的类型

执行上下文总共有三种类型:

  • 全局执行上下文: 这是默认的、最基础的执行上下文。不在任何函数中的代码都位于全局执行上下文中。它做了两件事:1. 创建一个全局对象,在浏览器中这个全局对象就是 window 对象。2. 将 this 指针指向这个全局对象。一个程序中只能存在一个全局执行上下文。
  • 函数执行上下文: 每次调用函数时,都会为该函数创建一个新的执行上下文。每个函数都拥有自己的执行上下文,但是只有在函数被调用的时候才会被创建。一个程序中可以存在任意数量的函数执行上下文。每当一个新的执行上下文被创建,它都会按照特定的顺序执行一系列步骤,具体过程将在本文后面讨论。
  • Eval 函数执行上下文: 运行在 eval 函数中的代码也获得了自己的执行上下文,但由于 Javascript 开发人员不常用 eval 函数,所以在这里不再讨论。

二、执行上下文的生命周期

执行上下文的生命周期包括三个阶段:创建阶段→执行阶段→回收阶段,本文重点介绍创建阶段。

1.创建阶段

当函数被调用,但未执行任何其内部代码之前,会做以下三件事:

  • 创建变量对象:首先初始化函数的参数arguments,提升函数声明和变量声明。下文会详细说明。
  • 创建作用域链(Scope Chain):在执行期上下文的创建阶段,作用域链是在变量对象之后创建的。作用域链本身包含变量对象。作用域链用于解析变量。当被要求解析变量时,JavaScript 始终从代码嵌套的最内层开始,如果最内层没有找到变量,就会跳转到上一层父作用域中查找,直到找到该变量。
  • 确定this指向:包括多种情况,下文会详细说明

在一段 JS 脚本执行之前,要先解析代码(所以说 JS 是解释执行的脚本语言),解析的时候会先创建一个全局执行上下文环境,先把代码中即将执行的变量、函数声明都拿出来。变量先暂时赋值为undefined,函数则先声明好可使用。这一步做完了,然后再开始正式执行程序。

另外,一个函数在执行之前,也会创建一个函数执行上下文环境,跟全局上下文差不多,不过 函数执行上下文中会多出this arguments和函数的参数。

2.执行阶段

执行变量赋值、代码执行

3.回收阶段

执行上下文出栈等待虚拟机回收执行上下文

三、变量提升和this指向的细节

1.变量声明提升

大部分编程语言都是先声明变量再使用,但在JS中,事情有些不一样:

console.log(a)// undefined
var a = 10

上述代码正常输出undefined而不是报错Uncaught ReferenceError: a is not defined,这是因为声明提升(hoisting),相当于如下代码:

var a; //声明 默认值是undefined “准备工作”
console.log(a);
a=10; //赋值

2.函数声明提升

我们都知道,创建一个函数的方法有两种,一种是通过函数声明function foo(){}
另一种是通过函数表达式var foo = function(){} ,那这两种在函数提升有什么区别呢?

console.log(f1) // function f1(){}
function f1() {} // 函数声明
console.log(f2) // undefined
var f2 = function() {} // 函数表达式

接下来我们通过一个例子来说明这个问题:

function test() {
    foo(); // Uncaught TypeError "foo is not a function"
    bar(); // "this will run!"
    var foo = function () { // function expression assigned to local variable 'foo'
        alert("this won't run!");
    }
    function bar() { // function declaration, given the name 'bar'
        alert("this will run!");
    }
}
test();

在上面的例子中,foo()调用的时候报错了,而bar能够正常调用。

我们前面说过变量和函数都会上升,遇到函数表达式 var foo = function(){}时,首先会将var foo上升到函数体顶部,然而此时的foo的值为undefined,所以执行foo()报错。

而对于函数bar(), 则是提升了整个函数,所以bar()才能够顺利执行。

有个细节必须注意:当遇到函数和变量同名且都会被提升的情况,函数声明优先级比较高,因此变量声明会被函数声明所覆盖,但是可以重新赋值。

alert(a);//输出:function a(){ alert('我是函数') }
function a(){ alert('我是函数') }//
var a = '我是变量';
alert(a);   //输出:'我是变量'

function声明的优先级比var声明高,也就意味着当两个同名变量同时被function和var声明时,function声明会覆盖var声明

这代码等效于:

function a(){alert('我是函数')} 
var a;    //hoisting
alert(a);    //输出:function a(){ alert('我是函数') }
a = '我是变量';//赋值
alert(a);   //输出:'我是变量'

最后我们看个复杂点的例子:

function test(arg){
    // 1. 形参 arg 是 "hi"
    // 2. 因为函数声明比变量声明优先级高,所以此时 arg 是 function
    console.log(arg);  
    var arg = 'hello'; // 3.var arg 变量声明被忽略, arg = 'hello'被执行
    function arg(){
	console.log('hello world') 
    }
    console.log(arg);  
}
test('hi');
/* 输出:
function arg(){
    console.log('hello world') 
    }
hello 
*/

这是因为当函数执行的时候,首先会形成一个新的私有的作用域,然后依次按照如下的步骤执行:

  • 如果有形参,先给形参赋值
  • 进行私有作用域中的预解释,函数声明优先级比变量声明高,最后后者会被前者所覆盖,但是可以重新赋值
  • 私有作用域中的代码从上到下执行

3.确定this的指向

先搞明白一个很重要的概念 —— this的值是在执行的时候才能确认,定义的时候不能确认! 为什么呢 —— 因为this是执行上下文环境的一部分,而执行上下文需要在代码执行之前确定,而不是定义的时候。看如下例子:

// 情况1
function foo() {
  console.log(this.a) //1
}
var a = 1
foo()

// 情况2
function fn(){
  console.log(this);
}
var obj={fn:fn};
obj.fn(); //this->obj

// 情况3
function CreateJsPerson(name,age){
//this是当前类的一个实例p1
this.name=name; //=>p1.name=name
this.age=age; //=>p1.age=age
}
var p1=new CreateJsPerson("尹华芝",48);

// 情况4
function add(c, d){
  return this.a + this.b + c + d;
}
var o = {a:1, b:3};
add.call(o, 5, 7); // 1 + 3 + 5 + 7 = 16
add.apply(o, [10, 20]); // 1 + 3 + 10 + 20 = 34

// 情况5
<button id="btn1">箭头函数this</button>
<script type="text/javascript">   
    let btn1 = document.getElementById('btn1');
    let obj = {
        name: 'kobe',
        age: 39,
        getName: function () {
            btn1.onclick = () => {
                console.log(this);//obj
            };
        }
    };
    obj.getName();
</script>

接下来我们逐一解释上面几种情况

  • 对于直接调用 foo 来说,不管 foo 函数被放在了什么地方,this 一定是 window
  • 对于 obj.foo() 来说,我们只需要记住,谁调用了函数,谁就是 this,所以在这个场景下 foo 函数中的 this 就是 obj 对象
  • 在构造函数模式中,类中(函数体中)出现的this.xxx=xxx中的this是当前类的一个实例
  • call、apply和bind:this 是第一个参数
  • 箭头函数this指向:箭头函数没有自己的this,看其外层的是否有函数,如果有,外层函数的this就是内部箭头函数的this,如果没有,则this是window。

四、执行上下文栈(Execution Context Stack)

函数多了,就有多个函数执行上下文,每次调用函数创建一个新的执行上下文,那如何管理创建的那么多执行上下文呢?

JavaScript 引擎创建了执行上下文栈来管理执行上下文。可以把执行上下文栈认为是一个存储函数调用的栈结构,遵循先进后出的原则

从上面的流程图,我们需要记住几个关键点:

  • JavaScript执行在单线程上,所有的代码都是排队执行。
  • 一开始浏览器执行全局的代码时,首先创建全局的执行上下文,压入执行栈的顶部。
  • 每当进入一个函数的执行就会创建函数的执行上下文,并且把它压入执行栈的顶部。当前函数执行完成后,当前函数的执行上下文出栈,并等待垃圾回收。
  • 浏览器的JS执行引擎总是访问栈顶的执行上下文。
  • 全局上下文只有唯一的一个,它在浏览器关闭时出栈。

我们再来看个例子:

var color = 'blue';
function changeColor() {
    var anotherColor = 'red';
    function swapColors() {
        var tempColor = anotherColor;
        anotherColor = color;
        color = tempColor;
    }
    swapColors();
}
changeColor();

上述代码运行按照如下步骤:

  • 当上述代码在浏览器中加载时,JavaScript 引擎会创建一个全局执行上下文并且将它推入当前的执行栈
  • 调用 changeColor函数时,此时changeColor函数内部代码还未执行,js执行引擎立即创建一个changeColor的执行上下文(简称EC),然后把这执行上下文压入到执行栈(简称ECStack)中。
  • 执行changeColor函数过程中,调用swapColors函数,同样地,swapColors函数执行之前也创建了一个swapColors的执行上下文,并压入到执行栈中。
  • swapColors函数执行完成,swapColors函数的执行上下文出栈,并且被销毁。
  • changeColor函数执行完成,changeColor函数的执行上下文出栈,并且被销毁。

参考文章

九种跨域方式实现原理(完整版)

前言

前后端数据交互经常会碰到请求跨域,什么是跨域,以及有哪几种跨域方式,这是本文要探讨的内容。

本文完整的源代码请猛戳github博客

一、什么是跨域?

1.什么是同源策略及其限制内容?

同源策略是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSS、CSRF等攻击。所谓同源是指"协议+域名+端口"三者相同,即便两个不同的域名指向同一个ip地址,也非同源。
url的组成
同源策略限制内容有:

  • Cookie、LocalStorage、IndexedDB 等存储性内容
  • DOM 节点
  • AJAX 请求发送后,结果被浏览器拦截了

但是有三个标签是允许跨域加载资源:

  • <img src=XXX>
  • <link href=XXX>
  • <script src=XXX>

2.常见跨域场景

当协议、子域名、主域名、端口号中任意一个不相同时,都算作不同域。不同域之间相互请求资源,就算作“跨域”。常见跨域场景如下图所示:

特别说明两点:

第一:如果是协议和端口造成的跨域问题“前台”是无能为力的。

第二:在跨域问题上,仅仅是通过“URL的首部”来识别而不会根据域名对应的IP地址是否相同来判断。“URL的首部”可以理解为“协议, 域名和端口必须匹配”

这里你或许有个疑问:请求跨域了,那么请求到底发出去没有?

跨域并不是请求发不出去,请求能发出去,服务端能收到请求并正常返回结果,只是结果被浏览器拦截了。你可能会疑问明明通过表单的方式可以发起跨域请求,为什么 Ajax 就不会?因为归根结底,跨域是为了阻止用户读取到另一个域名下的内容,Ajax 可以获取响应,浏览器认为这不安全,所以拦截了响应。但是表单并不会获取新的内容,所以可以发起跨域请求。同时也说明了跨域并不能完全阻止 CSRF,因为请求毕竟是发出去了。

二、跨域解决方案

1.jsonp

1) JSONP原理

利用 <script> 标签没有跨域限制的漏洞,网页可以得到从其他来源动态产生的 JSON 数据。JSONP请求一定需要对方的服务器做支持才可以。

2) JSONP和AJAX对比

JSONP和AJAX相同,都是客户端向服务器端发送请求,从服务器端获取数据的方式。但AJAX属于同源策略,JSONP属于非同源策略(跨域请求)

3) JSONP优缺点

JSONP优点是简单兼容性好,可用于解决主流浏览器的跨域数据访问的问题。缺点是仅支持get方法具有局限性,不安全可能会遭受XSS攻击。

4) JSONP的实现流程

  • 声明一个回调函数,其函数名(如show)当做参数值,要传递给跨域请求数据的服务器,函数形参为要获取目标数据(服务器返回的data)。
  • 创建一个<script>标签,把那个跨域的API数据接口地址,赋值给script的src,还要在这个地址中向服务器传递该函数名(可以通过问号传参:?callback=show)。
  • 服务器接收到请求后,需要进行特殊的处理:把传递进来的函数名和它需要给你的数据拼接成一个字符串,例如:传递进去的函数名是show,它准备好的数据是show('我不爱你')
  • 最后服务器把准备的数据通过HTTP协议返回给客户端,客户端再调用执行之前声明的回调函数(show),对返回的数据进行操作。

在开发中可能会遇到多个 JSONP 请求的回调函数名是相同的,这时候就需要自己封装一个 JSONP函数。

// index.html
function jsonp({ url, params, callback }) {
  return new Promise((resolve, reject) => {
    let script = document.createElement('script')
    window[callback] = function(data) {
      resolve(data)
      document.body.removeChild(script)
    }
    params = { ...params, callback } // wd=b&callback=show
    let arrs = []
    for (let key in params) {
      arrs.push(`${key}=${params[key]}`)
    }
    script.src = `${url}?${arrs.join('&')}`
    document.body.appendChild(script)
  })
}
jsonp({
  url: 'http://localhost:3000/say',
  params: { wd: 'Iloveyou' },
  callback: 'show'
}).then(data => {
  console.log(data)
})

上面这段代码相当于向http://localhost:3000/say?wd=Iloveyou&callback=show这个地址请求数据,然后后台返回show('我不爱你'),最后会运行show()这个函数,打印出'我不爱你'

// server.js
let express = require('express')
let app = express()
app.get('/say', function(req, res) {
  let { wd, callback } = req.query
  console.log(wd) // Iloveyou
  console.log(callback) // show
  res.end(`${callback}('我不爱你')`)
})
app.listen(3000)

5) jQuery的jsonp形式

JSONP都是GET和异步请求的,不存在其他的请求方式和同步请求,且jQuery默认就会给JSONP的请求清除缓存。

$.ajax({
url:"http://crossdomain.com/jsonServerResponse",
dataType:"jsonp",
type:"get",//可以省略
jsonpCallback:"show",//->自定义传递给服务器的函数名,而不是使用jQuery自动生成的,可省略
jsonp:"callback",//->把传递函数名的那个形参callback,可省略
success:function (data){
console.log(data);}
});

2.cors

CORS 需要浏览器和后端同时支持。IE 8 和 9 需要通过 XDomainRequest 来实现

浏览器会自动进行 CORS 通信,实现 CORS 通信的关键是后端。只要后端实现了 CORS,就实现了跨域。

服务端设置 Access-Control-Allow-Origin 就可以开启 CORS。 该属性表示哪些域名可以访问资源,如果设置通配符则表示所有网站都可以访问资源。

虽然设置 CORS 和前端没什么关系,但是通过这种方式解决跨域问题的话,会在发送请求时出现两种情况,分别为简单请求复杂请求

1) 简单请求

只要同时满足以下两大条件,就属于简单请求

条件1:使用下列方法之一:

  • GET
  • HEAD
  • POST

条件2:Content-Type 的值仅限于下列三者之一:

  • text/plain
  • multipart/form-data
  • application/x-www-form-urlencoded

请求中的任意 XMLHttpRequestUpload 对象均没有注册任何事件监听器; XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问。

2) 复杂请求

不符合以上条件的请求就肯定是复杂请求了。
复杂请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求,该请求是 option 方法的,通过该请求来知道服务端是否允许跨域请求。

我们用PUT向后台请求时,属于复杂请求,后台需做如下配置:

// 允许哪个方法访问我
res.setHeader('Access-Control-Allow-Methods', 'PUT')
// 预检的存活时间
res.setHeader('Access-Control-Max-Age', 6)
// OPTIONS请求不做任何处理
if (req.method === 'OPTIONS') {
  res.end() 
}
// 定义后台返回的内容
app.put('/getData', function(req, res) {
  console.log(req.headers)
  res.end('我不爱你')
})

接下来我们看下一个完整复杂请求的例子,并且介绍下CORS请求相关的字段

// index.html
let xhr = new XMLHttpRequest()
document.cookie = 'name=xiamen' // cookie不能跨域
xhr.withCredentials = true // 前端设置是否带cookie
xhr.open('PUT', 'http://localhost:4000/getData', true)
xhr.setRequestHeader('name', 'xiamen')
xhr.onreadystatechange = function() {
  if (xhr.readyState === 4) {
    if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
      console.log(xhr.response)
      //得到响应头,后台需设置Access-Control-Expose-Headers
      console.log(xhr.getResponseHeader('name'))
    }
  }
}
xhr.send()
//server1.js
let express = require('express');
let app = express();
app.use(express.static(__dirname));
app.listen(3000);
//server2.js
let express = require('express')
let app = express()
let whitList = ['http://localhost:3000'] //设置白名单
app.use(function(req, res, next) {
  let origin = req.headers.origin
  if (whitList.includes(origin)) {
    // 设置哪个源可以访问我
    res.setHeader('Access-Control-Allow-Origin', origin)
    // 允许携带哪个头访问我
    res.setHeader('Access-Control-Allow-Headers', 'name')
    // 允许哪个方法访问我
    res.setHeader('Access-Control-Allow-Methods', 'PUT')
    // 允许携带cookie
    res.setHeader('Access-Control-Allow-Credentials', true)
    // 预检的存活时间
    res.setHeader('Access-Control-Max-Age', 6)
    // 允许返回的头
    res.setHeader('Access-Control-Expose-Headers', 'name')
    if (req.method === 'OPTIONS') {
      res.end() // OPTIONS请求不做任何处理
    }
  }
  next()
})
app.put('/getData', function(req, res) {
  console.log(req.headers)
  res.setHeader('name', 'jw') //返回一个响应头,后台需设置
  res.end('我不爱你')
})
app.get('/getData', function(req, res) {
  console.log(req.headers)
  res.end('我不爱你')
})
app.use(express.static(__dirname))
app.listen(4000)

上述代码由http://localhost:3000/index.htmlhttp://localhost:4000/跨域请求,正如我们上面所说的,后端是实现 CORS 通信的关键。

3.postMessage

postMessage是HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window属性之一,它可用于解决以下方面的问题:

  • 页面和其打开的新窗口的数据传递
  • 多窗口之间消息传递
  • 页面与嵌套的iframe消息传递
  • 上面三个场景的跨域数据传递

postMessage()方法允许来自不同源的脚本采用异步方式进行有限的通信,可以实现跨文本档、多窗口、跨域消息传递

otherWindow.postMessage(message, targetOrigin, [transfer]);

  • message: 将要发送到其他 window的数据。
  • targetOrigin:通过窗口的origin属性来指定哪些窗口能接收到消息事件,其值可以是字符串"*"(表示无限制)或者一个URI。在发送消息的时候,如果目标窗口的协议、主机地址或端口这三者的任意一项不匹配targetOrigin提供的值,那么消息就不会被发送;只有三者完全匹配,消息才会被发送。
  • transfer(可选):是一串和message 同时传递的 Transferable 对象. 这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。

接下来我们看个例子: http://localhost:3000/a.html页面向http://localhost:4000/b.html传递“我爱你”,然后后者传回"我不爱你"。

// a.html
  <iframe src="http://localhost:4000/b.html" frameborder="0" id="frame" onload="load()"></iframe> //等它加载完触发一个事件
  //内嵌在http://localhost:3000/a.html
    <script>
      function load() {
        let frame = document.getElementById('frame')
        frame.contentWindow.postMessage('我爱你', 'http://localhost:4000') //发送数据
        window.onmessage = function(e) { //接受返回数据
          console.log(e.data) //我不爱你
        }
      }
    </script>
// b.html
  window.onmessage = function(e) {
    console.log(e.data) //我爱你
    e.source.postMessage('我不爱你', e.origin)
 }

4.websocket

Websocket是HTML5的一个持久化的协议,它实现了浏览器与服务器的全双工通信,同时也是跨域的一种解决方案。WebSocket和HTTP都是应用层协议,都基于 TCP 协议。但是 WebSocket 是一种双向通信协议,在建立连接之后,WebSocket 的 server 与 client 都能主动向对方发送或接收数据。同时,WebSocket 在建立连接时需要借助 HTTP 协议,连接建立好了之后 client 与 server 之间的双向通信就与 HTTP 无关了。

原生WebSocket API使用起来不太方便,我们使用Socket.io,它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。

我们先来看个例子:本地文件socket.html向localhost:3000发生数据和接受数据

// socket.html
<script>
    let socket = new WebSocket('ws://localhost:3000');
    socket.onopen = function () {
      socket.send('我爱你');//向服务器发送数据
    }
    socket.onmessage = function (e) {
      console.log(e.data);//接收服务器返回的数据
    }
</script>
// server.js
let express = require('express');
let app = express();
let WebSocket = require('ws');//记得安装ws
let wss = new WebSocket.Server({port:3000});
wss.on('connection',function(ws) {
  ws.on('message', function (data) {
    console.log(data);
    ws.send('我不爱你')
  });
})

5. Node中间件代理(两次跨域)

实现原理:同源策略是浏览器需要遵循的标准,而如果是服务器向服务器请求就无需遵循同源策略。
代理服务器,需要做以下几个步骤:

  • 接受客户端请求 。
  • 将请求 转发给服务器。
  • 拿到服务器 响应 数据。
  • 将 响应 转发给客户端。

我们先来看个例子:本地文件index.html文件,通过代理服务器http://localhost:3000向目标服务器http://localhost:4000请求数据。

// index.html(http://127.0.0.1:5500)
 <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
    <script>
      $.ajax({
        url: 'http://localhost:3000',
        type: 'post',
        data: { name: 'xiamen', password: '123456' },
        contentType: 'application/json;charset=utf-8',
        success: function(result) {
          console.log(result) // {"title":"fontend","password":"123456"}
        },
        error: function(msg) {
          console.log(msg)
        }
      })
     </script>
// server1.js 代理服务器(http://localhost:3000)
const http = require('http')
// 第一步:接受客户端请求
const server = http.createServer((request, response) => {
  // 代理服务器,直接和浏览器直接交互,需要设置CORS 的首部字段
  response.writeHead(200, {
    'Access-Control-Allow-Origin': '*',
    'Access-Control-Allow-Methods': '*',
    'Access-Control-Allow-Headers': 'Content-Type'
  })
  // 第二步:将请求转发给服务器
  const proxyRequest = http
    .request(
      {
        host: '127.0.0.1',
        port: 4000,
        url: '/',
        method: request.method,
        headers: request.headers
      },
      serverResponse => {
        // 第三步:收到服务器的响应
        var body = ''
        serverResponse.on('data', chunk => {
          body += chunk
        })
        serverResponse.on('end', () => {
          console.log('The data is ' + body)
          // 第四步:将响应结果转发给浏览器
          response.end(body)
        })
      }
    )
    .end()
})
server.listen(3000, () => {
  console.log('The proxyServer is running at http://localhost:3000')
})
// server2.js(http://localhost:4000)
const http = require('http')
const data = { title: 'fontend', password: '123456' }
const server = http.createServer((request, response) => {
  if (request.url === '/') {
    response.end(JSON.stringify(data))
  }
})
server.listen(4000, () => {
  console.log('The server is running at http://localhost:4000')
})

上述代码经过两次跨域,值得注意的是浏览器向代理服务器发送请求,也遵循同源策略,最后在index.html文件打印出{"title":"fontend","password":"123456"}

6.nginx反向代理

实现原理类似于Node中间件代理,需要你搭建一个中转nginx服务器,用于转发请求。

使用nginx反向代理实现跨域,是最简单的跨域方式。只需要修改nginx的配置即可解决跨域问题,支持所有浏览器,支持session,不需要修改任何代码,并且不会影响服务器性能。

实现思路:通过nginx配置一个代理服务器(域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域登录。

先下载nginx,然后将nginx目录下的nginx.conf修改如下:

// proxy服务器
server {
    listen       80;
    server_name  www.domain1.com;
    location / {
        proxy_pass   http://www.domain2.com:8080;  #反向代理
        proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名
        index  index.html index.htm;

        # 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用
        add_header Access-Control-Allow-Origin http://www.domain1.com;  #当前端只跨域不带cookie时,可为*
        add_header Access-Control-Allow-Credentials true;
    }
}

最后通过命令行nginx -s reload启动nginx

// index.html
var xhr = new XMLHttpRequest();
// 前端开关:浏览器是否读写cookie
xhr.withCredentials = true;
// 访问nginx中的代理服务器
xhr.open('get', 'http://www.domain1.com:81/?user=admin', true);
xhr.send();
// server.js
var http = require('http');
var server = http.createServer();
var qs = require('querystring');
server.on('request', function(req, res) {
    var params = qs.parse(req.url.substring(2));
    // 向前台写cookie
    res.writeHead(200, {
        'Set-Cookie': 'l=a123456;Path=/;Domain=www.domain2.com;HttpOnly'   // HttpOnly:脚本无法读取
    });
    res.write(JSON.stringify(params));
    res.end();
});
server.listen('8080');
console.log('Server is running at port 8080...');

7.window.name + iframe

window.name属性的独特之处:name值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)。

其中a.html和b.html是同域的,都是http://localhost:3000;而c.html是http://localhost:4000

 // a.html(http://localhost:3000/b.html)
  <iframe src="http://localhost:4000/c.html" frameborder="0" onload="load()" id="iframe"></iframe>
  <script>
    let first = true
    // onload事件会触发2次,第1次加载跨域页,并留存数据于window.name
    function load() {
      if(first){
      // 第1次onload(跨域页)成功后,切换到同域代理页面
        let iframe = document.getElementById('iframe');
        iframe.src = 'http://localhost:3000/b.html';
        first = false;
      }else{
      // 第2次onload(同域b.html页)成功后,读取同域window.name中数据
        console.log(iframe.contentWindow.name);
      }
    }
  </script>

b.html为中间代理页,与a.html同域,内容为空。

 // c.html(http://localhost:4000/c.html)
  <script>
    window.name = '我不爱你'  
  </script>

总结:通过iframe的src属性由外域转向本地域,跨域数据即由iframe的window.name从外域传递到本地域。这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作。

8.location.hash + iframe

实现原理: a.html欲与c.html跨域相互通信,通过中间页b.html来实现。 三个页面,不同域之间利用iframe的location.hash传值,相同域之间直接js访问来通信。

具体实现步骤:一开始a.html给c.html传一个hash值,然后c.html收到hash值后,再把hash值传递给b.html,最后b.html将结果放到a.html的hash值中。
同样的,a.html和b.html是同域的,都是http://localhost:3000;而c.html是http://localhost:4000

 // a.html
  <iframe src="http://localhost:4000/c.html#iloveyou"></iframe>
  <script>
    window.onhashchange = function () { //检测hash的变化
      console.log(location.hash);
    }
  </script>
 // b.html
  <script>
    window.parent.parent.location.hash = location.hash 
    //b.html将结果放到a.html的hash值中,b.html可通过parent.parent访问a.html页面
  </script>
 // c.html
 console.log(location.hash);
  let iframe = document.createElement('iframe');
  iframe.src = 'http://localhost:3000/b.html#idontloveyou';
  document.body.appendChild(iframe);

9.document.domain + iframe

该方式只能用于二级域名相同的情况下,比如 a.test.comb.test.com 适用于该方式
只需要给页面添加 document.domain ='test.com' 表示二级域名都相同就可以实现跨域。

实现原理:两个页面都通过js强制设置document.domain为基础主域,就实现了同域。

我们看个例子:页面a.zf1.cn:3000/a.html获取页面b.zf1.cn:3000/b.html中a的值

// a.html
<body>
 helloa
  <iframe src="http://b.zf1.cn:3000/b.html" frameborder="0" onload="load()" id="frame"></iframe>
  <script>
    document.domain = 'zf1.cn'
    function load() {
      console.log(frame.contentWindow.a);
    }
  </script>
</body>
// b.html
<body>
   hellob
   <script>
     document.domain = 'zf1.cn'
     var a = 100;
   </script>
</body>

三、总结

  • CORS支持所有类型的HTTP请求,是跨域HTTP请求的根本解决方案
  • JSONP只支持GET请求,JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。
  • 不管是Node中间件代理还是nginx反向代理,主要是通过同源策略对服务器不加限制。
  • 日常工作中,用得比较多的跨域方案是cors和nginx反向代理

参考文章

你还没搞懂this?

一、前言

this关键字是JavaScript中最复杂的机制之一。它是一个很特别的关键字,被自动定义在所有函数的作用域中。对于那些没有投入时间学习this机制的JavaScript开发者来说,this的绑定一直是一件非常令人困惑的事。

二、了解this

学习this的第一步是明白this既不指向函数自身也不指向函数的词法作用域,你也许被这样的解释误导过,但其实它们都是错误的。随着函数使用场合的不同,this的值会发生变化。但总有一条原则就是JS中的this代表的是当前行为执行的主体,在JS中主要研究的都是函数中的this,但并不是说只有在函数里才有this,this实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。如何的区分this呢?

三、this到底是谁

这要分情况讨论,常见有五种情况:

1、函数执行时首先看函数名前面是否有".",有的话,"."前面是谁,this就是谁;没有的话this就是window

function fn(){
  console.log(this);
}
var obj={fn:fn};
fn();//this->window
obj.fn();//this->obj
function sum(){
     fn();//this->window
}
sum();
var oo={
 sum:function(){
 console.log(this);//this->oo
       fn();//this->window
  }
};
oo.sum();

2、自执行函数中的this永远是window

  (function(){ //this->window })();
  ~function(){ //this->window }();

3、给元素的某一个事件绑定方法,当事件触发的时候,执行对应的方法,方法中的this是当前的元素,除了IE6~8下使用attachEvent(IE一个著名的bug)

  • DOM零级事件绑定
  oDiv.onclick=function(){
     //this->oDiv
  };
  • DOM二级事件绑定
  oDiv.addEventListener("click",function(){
     //this->oDiv
  },false);
  • 在IE6~8下使用attachEvent,默认的this就是指的window对象
  oDiv.attachEvent("click",function(){
       //this->window
  });

我们大多数时候,遇到事件绑定,如下面例子这种,对于IE6~8下使用attachEvent不必太较真

function fn(){
  console.log(this);
}
document.getElementById("div1").onclick=fn;//fn中的this就是#divl
document.getElementById("div1").onclick=function(){
console.log(this);//this->#div1
fn();//this->window
};

4、在构造函数模式中,类中(函数体中)出现的this.xxx=xxx中的this是当前类的一个实例

function CreateJsPerson(name,age){
//浏览器默认创建的对象就是我们的实例p1->this
this.name=name;//->p1.name=name
this.age=age;
this.writeJs=function(){
console.log("my name is"+this.name +",i can write Js");
   };
//浏览器再把创建的实例默认的进行返回
}
var p1=new CreateJsPerson("尹华芝",48);

必须要注意一点:类中某一个属性值(方法),方法中的this需要看方法执行的时候,前面是否有".",才能知道this是谁。大家不妨看下接下来的这个例子,就可明白是啥意思。

function Fn(){
this.x=100;//this->f1
this.getX=function(){
console.log(this.x);//this->需要看getX执行的时候才知道
   }
}
var f1=new Fn;
f1.getX();//->方法中的this是f1,所以f1.x=100
var ss=f1.getX;
ss();//->方法中的this是window ->undefined

5.call、apply和bind

我们先来看一个问题,想在下面的例子中this绑定obj,怎么实现?

var obj={name:"浪里行舟"};
function fn(){
console.log(this);//this=>window
}
fn();
obj.fn();//->Uncaught TypeError:obj.fn is not a function

如果直接绑定obj.fn(),程序就会报错。这里我们应该用fn.call(obj)就可以实现this绑定obj,接下来我们详细介绍下call方法:

  • call方法的作用:

①首先我们让原型上的call方法执行,在执行call方法的时候,我们让fn方法中的this变为第一个参数值obj;然后再把fn这个函数执行。

②call还可以传值,在严格模式下和非严格模式下,得到值不一样。

//在非严格模式下
var obj={name:"浪里行舟 "};
function fn(num1,num2){
console.log(num1+num2);
console.log(this);
}
fn.call(100,200);//this->100 num1=200 num2=undefined
fn.call(obj,100,200);//this->obj num1=100 num2=200
fn.call();//this->window
fn.call(null);//this->window
fn.call(undefined);//this->window
//严格模式下 
fn.call();//在严格模式下this->undefined
fn.call(null);// 在严格模式 下this->null
fn.call(undefined);//在严格模式下this->undefined
  • apply和call方法的作用是一模一样的,都是用来改变方法的this关键字并且把方法
    执行,而且在严格模式下和非严格模式下对于第一个参数是null/undefined这种情况的规
    律也是一样的。

两者唯一的区别:call在给fn传递参数的时候,是一个个的传递值的,而apply不是一个个传,而是把要给fn传递的参数值统一的放在一个数组中进行操作。但是也相当子一个个的给fn的形参赋值。总结一句话:call第二个参数开始接受一个参数列表,apply第二个参数开始接受一个参数数组

fn.call(obj,100,200);
fn.apply(obj,[100,200]);
  • bind:这个方法在IE6~8下不兼容,和call/apply类似都是用来改变this关键字的,但是和这两者有明显区别:
fn.call(obj,1,2);//->改变this和执行fn函数是一起都完成了
fn.bind(obj,1,2);//->只是改变了fn中的this为obj,并且给fn传递了两个参数值1、2,
                     但是此时并没有把fn这个函数执行
var tempFn=fn.bind(obj,1,2);
tempFn(); //这样才把fn这个函数执行

bind体现了预处理**:事先把fn的this改变为我们想要的结果,并且把对应的参数值也准备好,以后要用到了,直接的执行即可。

call和apply直接执行函数,而bind需要再一次调用。

  var a ={
        name : "Cherry",
        fn : function (a,b) {
            console.log( a + b)
        }
    }
  var b = a.fn;
  b.bind(a,1,2)

上述代码没有执行,bind返回改变了上下文的一个函数,我们必须要手动去调用:

 b.bind(a,1,2)() //3

必须要声明一点:遇到第五种情况(call apply和bind),前面四种全部让步。

四、箭头函数this指向

箭头函数正如名称所示那样使用一个“箭头”(=>)来定义函数的新语法,但它优于传统的函数,主要体现两点:更简短的函数并且不绑定this

var obj = {
    birth: 1990,
    getAge: function () {
        var b = this.birth; // 1990
        var fn = function () {
            return new Date().getFullYear() - this.birth; // this指向window或undefined
        };
        return fn();
    }
};

现在,箭头函数完全修复了this的指向,箭头函数没有自己的this,箭头函数的this不是调用的时候决定的,而是在定义的时候处在的对象就是它的this

换句话说,箭头函数的this看外层的是否有函数,如果有,外层函数的this就是内部箭头函数的this,如果没有,则this是window

    <button id="btn1">测试箭头函数this_1</button>
    <button id="btn2">测试箭头函数this_2</button>
    <script type="text/javascript">   
        let btn1 = document.getElementById('btn1');
        let obj = {
            name: 'kobe',
            age: 39,
            getName: function () {
                btn1.onclick = () => {
                    console.log(this);//obj
                };
            }
        };
        obj.getName();
    </script>

上例中,由于箭头函数不会创建自己的this,它只会从自己的作用域链的上一层继承this。其实可以简化为如下代码:

   let btn1 = document.getElementById('btn1');
        let obj = {
            name: 'kobe',
            age: 39,
            getName: function () {
                console.log(this)
            }
        };
   obj.getName();

那假如上一层并不存在函数,this指向又是谁?

    <button id="btn1">测试箭头函数this_1</button>
    <button id="btn2">测试箭头函数this_2</button>
    <script type="text/javascript">   
        let btn2 = document.getElementById('btn2');
        let obj = {
            name: 'kobe',
            age: 39,
            getName: () => {
                btn2.onclick = () => {
                    console.log(this);//window
                };
            }
        };
        obj.getName();
    </script>

上例中,虽然存在两个箭头函数,其实this取决于最外层的箭头函数,由于obj是个对象而非函数,所以this指向为Window对象

由于this在箭头函数中已经按照词法作用域绑定了,所以,用call()或者apply()调用箭头函数时,无法对this进行绑定,即传入的第一个参数被忽略

var obj = {
    birth: 1990,
    getAge: function (year) {
        var b = this.birth; // 1990
        var fn = (y) => y - this.birth; // this.birth仍是1990
        return fn.call({birth:2000}, year);
    }
};
obj.getAge(2018); // 28

参考文章

浅拷贝与深拷贝

前言

在 javascript 中有不同的方法来复制对象,如果你还不熟悉这门语言的话,复制对象时就会很容易掉进陷阱里,那么我们怎样才能正确地复制一个对象呢?

读完本文,希望你能明白:

  • 什么是深/浅拷贝,他们跟赋值有何区别?
  • 深/浅拷贝的实现方式有几种?

浅拷贝与深拷贝

  • 浅拷贝是创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象

  • 深拷贝是将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象

var a1 = {b: {c: {}};

var a2 = shallowClone(a1); // 浅拷贝方法
a2.b.c === a1.b.c // true 新旧对象还是共享同一块内存

var a3 = deepClone(a3); // 深拷贝方法
a3.b.c === a1.b.c // false 新对象跟原对象不共享内存

借助ConardLi大佬以下两张图片,帮我们更好的理解两者的含义:

总而言之,浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。

赋值和深/浅拷贝的区别

这三者的区别如下,不过比较的前提都是针对引用类型

  • 当我们把一个对象赋值给一个新的变量时,赋的其实是该对象的在栈中的地址,而不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,因此,两个对象是联动的。

  • 浅拷贝:重新在堆中创建内存,拷贝前后对象的基本数据类型互不影响,但拷贝前后对象的引用类型因共享同一块内存,会相互影响。

  • 深拷贝:从堆内存中开辟一个新的区域存放新对象,对对象中的子对象进行递归拷贝,拷贝前后的两个对象互不影响。

我们先来看下面的例子,对比赋值与深/浅拷贝得到的对象修改后对原始对象的影响:

// 对象赋值
let obj1 = {
    name : '浪里行舟',
    arr : [1,[2,3],4],
};
let obj2 = obj1;
obj2.name = "阿浪";
obj2.arr[1] =[5,6,7] ;
console.log('obj1',obj1) // obj1 { name: '阿浪', arr: [ 1, [ 5, 6, 7 ], 4 ] }
console.log('obj2',obj2) // obj2 { name: '阿浪', arr: [ 1, [ 5, 6, 7 ], 4 ] }
// 浅拷贝
let obj1 = {
    name : '浪里行舟',
    arr : [1,[2,3],4],
};
let obj3=shallowClone(obj1)
obj3.name = "阿浪";
obj3.arr[1] = [5,6,7] ; // 新旧对象还是共享同一块内存
// 这是个浅拷贝的方法
function shallowClone(source) {
    var target = {};
    for(var i in source) {
        if (source.hasOwnProperty(i)) {
            target[i] = source[i];
        }
    }
    return target;
}
console.log('obj1',obj1) // obj1 { name: '浪里行舟', arr: [ 1, [ 5, 6, 7 ], 4 ] }
console.log('obj3',obj3) // obj3 { name: '阿浪', arr: [ 1, [ 5, 6, 7 ], 4 ] }
// 深拷贝
let obj1 = {
    name : '浪里行舟',
    arr : [1,[2,3],4],
};
let obj4=deepClone(obj1)
obj4.name = "阿浪";
obj4.arr[1] = [5,6,7] ; // 新对象跟原对象不共享内存
// 这是个深拷贝的方法
function deepClone(obj) {
    if (obj === null) return obj; 
    if (obj instanceof Date) return new Date(obj);
    if (obj instanceof RegExp) return new RegExp(obj);
    if (typeof obj !== "object") return obj;
    let cloneObj = new obj.constructor();
    for (let key in obj) {
      if (obj.hasOwnProperty(key)) {
        // 实现一个递归拷贝
        cloneObj[key] = deepClone(obj[key]);
      }
    }
    return cloneObj;
}
console.log('obj1',obj1) // obj1 { name: '浪里行舟', arr: [ 1, [ 2, 3 ], 4 ] }
console.log('obj4',obj4) // obj4 { name: '阿浪', arr: [ 1, [ 5, 6, 7 ], 4 ] }

上面例子中,obj1是原始对象,obj2是赋值操作得到的对象,obj3浅拷贝得到的对象,obj4深拷贝得到的对象,通过下面的表格,我们可以很清晰看到他们对原始数据的影响:

浅拷贝的实现方式

1.Object.assign()

Object.assign() 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。

let obj1 = { person: {name: "kobe", age: 41},sports:'basketball' };
let obj2 = Object.assign({}, obj1);
obj2.person.name = "wade";
obj2.sports = 'football'
console.log(obj1); // { person: { name: 'wade', age: 41 }, sports: 'basketball' }

2.函数库lodash的_.clone方法

该函数库也有提供_.clone用来做 Shallow Copy,后面我们会再介绍利用这个库实现深拷贝。

var _ = require('lodash');
var obj1 = {
    a: 1,
    b: { f: { g: 1 } },
    c: [1, 2, 3]
};
var obj2 = _.clone(obj1);
console.log(obj1.b.f === obj2.b.f);// true

3.展开运算符...

展开运算符是一个 es6 / es2015特性,它提供了一种非常方便的方式来执行浅拷贝,这与 Object.assign ()的功能相同。

let obj1 = { name: 'Kobe', address:{x:100,y:100}}
let obj2= {... obj1}
obj1.address.x = 200;
obj1.name = 'wade'
console.log('obj2',obj2) // obj2 { name: 'Kobe', address: { x: 200, y: 100 } }

4.Array.prototype.concat()

let arr = [1, 3, {
    username: 'kobe'
    }];
let arr2 = arr.concat();    
arr2[2].username = 'wade';
console.log(arr); //[ 1, 3, { username: 'wade' } ]

5.Array.prototype.slice()

let arr = [1, 3, {
    username: ' kobe'
    }];
let arr3 = arr.slice();
arr3[2].username = 'wade'
console.log(arr); // [ 1, 3, { username: 'wade' } ]

深拷贝的实现方式

1.JSON.parse(JSON.stringify())

let arr = [1, 3, {
    username: ' kobe'
}];
let arr4 = JSON.parse(JSON.stringify(arr));
arr4[2].username = 'duncan'; 
console.log(arr, arr4)


这也是利用JSON.stringify将对象转成JSON字符串,再用JSON.parse把字符串解析成对象,一去一来,新的对象产生了,而且对象会开辟新的栈,实现深拷贝。

这种方法虽然可以实现数组或对象深拷贝,但不能处理函数和正则,因为这两者基于JSON.stringify和JSON.parse处理后,得到的正则就不再是正则(变为空对象),得到的函数就不再是函数(变为null)了。

比如下面的例子:

let arr = [1, 3, {
    username: ' kobe'
},function(){}];
let arr4 = JSON.parse(JSON.stringify(arr));
arr4[2].username = 'duncan'; 
console.log(arr, arr4)

2.函数库lodash的_.cloneDeep方法

该函数库也有提供_.cloneDeep用来做 Deep Copy

var _ = require('lodash');
var obj1 = {
    a: 1,
    b: { f: { g: 1 } },
    c: [1, 2, 3]
};
var obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f);// false

3.jQuery.extend()方法

jquery 有提供一個$.extend可以用来做 Deep Copy

$.extend(deepCopy, target, object1, [objectN])//第一个参数为true,就是深拷贝
var $ = require('jquery');
var obj1 = {
    a: 1,
    b: { f: { g: 1 } },
    c: [1, 2, 3]
};
var obj2 = $.extend(true, {}, obj1);
console.log(obj1.b.f === obj2.b.f); // false

4.手写递归方法

递归方法实现深度克隆原理:遍历对象、数组直到里边都是基本数据类型,然后再去复制,就是深度拷贝

有种特殊情况需注意就是对象存在循环引用的情况,即对象的属性直接的引用了自身的情况,解决循环引用问题,我们可以额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象,如果有的话直接返回,如果没有的话继续拷贝,这样就巧妙化解的循环引用的问题。关于这块如有疑惑,请仔细阅读ConardLi大佬如何写出一个惊艳面试官的深拷贝?这篇文章。

function deepClone(obj, hash = new WeakMap()) {
  if (obj === null) return obj; // 如果是null或者undefined我就不进行拷贝操作
  if (obj instanceof Date) return new Date(obj);
  if (obj instanceof RegExp) return new RegExp(obj);
  // 可能是对象或者普通的值  如果是函数的话是不需要深拷贝
  if (typeof obj !== "object") return obj;
  // 是对象的话就要进行深拷贝
  if (hash.get(obj)) return hash.get(obj);
  let cloneObj = new obj.constructor();
  // 找到的是所属类原型上的constructor,而原型上的 constructor指向的是当前类本身
  hash.set(obj, cloneObj);
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      // 实现一个递归拷贝
      cloneObj[key] = deepClone(obj[key], hash);
    }
  }
  return cloneObj;
}
let obj = { name: 1, address: { x: 100 } };
obj.o = obj; // 对象存在循环引用的情况
let d = deepClone(obj);
obj.address.x = 200;
console.log(d);

欢迎关注公众号:前端工匠,你的成长我们一起见证!

参考文章

关于Http协议,你必须要知道的

Http协议请求--响应

引言

HTTP协议是Hyper Text Transfer Protocol(超文本传输协议)的缩写,是用于从万维网服务器传输超文本到本地浏览器的传送协议。HTTP 是基于 TCP/IP 协议通信协议来传递数据(HTML 文件, 图片文件, 查询结果等)。它不涉及数据包(packet)传输,主要规定了客户端和服务器之间的通信格式,默认使用80端口。

本文框架图

一、Http的特点

1.简单快速:客户向服务器请求服务时,只需传送请求方法和路径。请求方法常用的有GET、HEAD、PUT、DELETE、POST。每种方法规定了客户与服务器联系的类型不同。由于HTTP协议简单,使得HTTP服务器的程序规模小,因而通信速度很快。

2.灵活:HTTP允许传输任意类型的数据对象。

3.无连接:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。

4.无状态:**HTTP协议是无状态的,HTTP 协议自身不对请求和响应之间的通信状态进行保存。任何两次请求之间都没有依赖关系。**直观地说,就是每个请求都是独立的,与前面的请求和后面的请求都是没有直接联系的。协议本身并不保留之前一切的请求或 响应报文的信息。这是为了更快地处理大量事务,确保协议的可伸缩性,而特意把 HTTP 协议设计成如此简单的。

HTTP协议是无状态的

二、Http报文

Http报文包括请求报文和响应报文两大部分,其中请求报文由请求行(request line)、请求头(header)、空行和请求体四个部分组成。而响应报文由状态行、响应头部、空行和响应体四个部分组成。接下来我们详细介绍下请求报文的各个部分及其作用。

请求报文

1.请求行,用来说明请求类型,要访问的资源以及所使用的HTTP版本。

POST  /chapter17/user.html HTTP/1.1

以上代码中“POST ”代表请求方法,“/chapter17/user.html”表示URI,“HTTP/1.1”代表协议和协议的版本。现在比较流行的是Http1.1版本

2.请求头由关键字/值对组成,每行一对,关键字和值用英文冒号“:”分隔。

请求头部通知服务器有关于客户端请求的信息。它包含许多有关的客户端环境和请求正文的有用信息。其中比如:
Host,表示主机名,虚拟主机;
Connection,HTTP/1.1增加的,使用keepalive,即持久连接,一个连接可以发多个请求;
User-Agent,请求发出者,兼容性以及定制化需求。

3.最后一个请求头之后是一个空行,这个行非常重要,它表示请求头已经结束,接下来的是请求正文。

4.请求体,可以承载多个请求参数的数据

name=tom&password=1234&realName=tomson

上面代码,承载着name、password、realName三个请求参数。

三、HTTP请求方法

  • GET 请求指定的页面信息,并返回实体主体。
  • HEAD 类似于get请求,只不过返回的响应中没有具体的内容,用于获取报头
  • POST 向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。
  • PUT 从客户端向服务器传送的数据取代指定的文档的内容。
  • DELETE 请求服务器删除指定的页面。

四、GET与POST区别

  • GET在浏览器回退时是无害的,而POST会再次提交请求
  • GET请求会被浏览器主动缓存,而POST不会,除非手动设置
  • GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留
  • GET请求在URL中传送的参数是有长度限制的,而POST没有限制
  • GET参数通过URL传递,POST放在Request body中

五、Http状态码

状态代码有三位数字组成,第一个数字定义了响应的类别,共分五种类别:

  • 1xx:指示信息--表示请求已接收,继续处理
  • 2xx:成功--表示请求已被成功接收、理解、接受
  • 3xx:重定向--要完成请求必须进行更进一步的操作
  • 4xx:客户端错误--请求有语法错误或请求无法实现
  • 5xx:服务器端错误--服务器未能实现合法的请求

比如我们平时常见两种出错的状态码:

403 Forbidden                 //对被请求页面的访问被禁止
404 Not Found                 //请求资源不存在,比如:输入了错误的URL

六、持久连接

1.为什么需要持久连接

HTTP协议的初始版本中,每进行一次HTTP通信就要断开一次TCP连接。以当年的通信情况来说,因为都是些容量很小的文本传输,所以即使这样也没有多大问题。可随着 HTTP 的 普及,文档中包含大量图片的情况多了起来。比如,使用浏览器浏览一个包含多张图片的 HTML 页面时,在发送请求访问 HTML 页面资源的同时,也会请 求该 HTML 页面里包含的其他资源。因此,每次的请求都会造成无谓的 TCP 连接建立和断开,增加通信量的 开销。

2.持久连接的特点

为解决上述 TCP 连接的问题,HTTP/1.1 和一部分的 HTTP/1.0 想出了持久连接(HTTP Persistent Connections,也称为 HTTP keep-alive 或 HTTP connection reuse)的方法。持久连接的特点是,只要任意一端没有明确提出断开连接,则保持TCP连接状态。

建立 1 次 次 TCP 连接后进行多次请求和响应的交互

持久连接的好处在于减少了 TCP 连接的重复建立和断开所造成的额外开销,减轻了服务器端的负载。另外, 减少开销的那部分时间,使 HTTP 请求和响应能够更早地结束,这样 Web 页面的显示速度也就相应提高了。
在 HTTP/1.1 中,所有的连接默认都是持久连接,但在 HTTP/1.0 内并未标准化。虽然有一部分服务器通过非 标准的手段实现了持久连接,但服务器端不一定能够支持持久连接。毫无疑问,除了服务器端,客户端也需 要支持持久连接。

七、管线化

持久连接使得多数请求以管线化(pipelining)方式发送成为可能。从前发送请求后需等待并收到响应,才能 发送下一个请求。管线化技术出现后,不用等待响应亦可直接发送下一个请求。
这样就能够做到同时并行发送多个请求,而不需要一个接一个地等待响应了。通俗地讲,请求打包一次传输过去,响应打包一次传递回来。管线化的前提是在持久连接下。

不等待响应,直接发送下一个请求

假如当请求一个包含 10 张图片的 HTML Web 页面,与挨个连接相比,用持久连接可以让请求更快结束。 而管线化技术则比持久连接还要快。请求数越多,时间差就越明显。客户端需要请求这十个资源。以前的做法是,在同一个TCP连接里面,先发送A请求,然后等待服务器做出回应,收到后再发出B请求,以此类推,而管道机制则是允许浏览器同时发出这十个请求,但是服务器还是按照顺序,先回应A请求,完成后再回应B请求。
于是在使用持久连接的情况下,某个连接上消息的传递类似于

请求1->响应1->请求2->响应2->请求3->响应3

管线化方式发送变成了类似这样:

请求1->请求2->请求3->响应1->响应2->响应3

参考

《图解HTTP》[日] 上野宣 著

JavaScript数据类型转换

前言

JavaScript是一门动态语言,所谓的动态语言可以暂时理解为在语言中的一切内容都是不确定的。比如一个变量,这一时刻是个整型,下一时刻可能会变成字符串了。虽然变量的数据类型是不确定的,但是各种运算符对数据类型是有要求的。如果运算符发现,运算子的类型与预期不符,就会自动转换类型。

本文主要介绍数据类型强制转换和自动转换,自动转换是基于强制转换之上。强制转换主要指使用Number、String和Boolean三个函数,手动将各种类型的值,分布转换成数字、字符串或者布尔值

一、强制转换

1、其他的数据类型转换为String

方式一:toString()方法

  • 调用被转换数据类型的toString()方法,该方法不会影响到原变量,它会将转换的结果返回,但是注意:null和undefined这两个值没有toString,如果调用他们的方法,会报错
var a = 123
a.toString()//"123"
var b = null;
b.toString()//"报错"
var c = undefined
c.toString()//"报错"
  • 采用 Number 类型的 toString() 方法的基模式,可以用不同的基输出数字,例如二进制的基是 2,八进制的基是 8,十六进制的基是 16
var iNum = 10;
alert(iNum.toString(2));        //输出 "1010"
alert(iNum.toString(8));        //输出 "12"
alert(iNum.toString(16));       //输出 "A"

方式二:String()函数

  • 使用String()函数做强制类型转换时,对于Number和Boolean实际上就是调用的toString()方法,
    但是对于null和undefined,就不会调用toString()方法,它会将null直接转换为"null",将undefined 直接转换为"undefined"
var a = null
String(a)//"null"
var b = undefined
String(b)//"undefined"
  • String方法的参数如果是对象,返回一个类型字符串;如果是数组,返回该数组的字符串形式。
String({a: 1}) // "[object Object]"
String([1, 2, 3]) // "1,2,3"

2、其他的数据类型转换为Number

方式一:使用Number()函数

下面分成两种情况讨论,一种是参数是原始类型的值,另一种是参数是对象

(1)原始类型值

①字符串转数字

Ⅰ 如果是纯数字的字符串,则直接将其转换为数字

Ⅱ 如果字符串中有非数字的内容,则转换为NaN

Ⅲ 如果字符串是一个空串或者是一个全是空格的字符串,则转换为0

Number('324') // 324
Number('324abc') // NaN
Number('') // 0

②布尔值转数字:true转成1,false转成0

Number(true) // 1
Number(false) // 0

③undefined转数字:转成NaN

Number(undefined) // NaN

④null转数字:转成0

Number(null) // 0

⑤Number() 接受数值作为参数,此时它既能识别负的十六进制,也能识别0开头的八进制,返回值永远是十进制值

Number(3.15);    //3.15
Number(023);     //19
Number(0x12);    //18
Number(-0x12);   //-18

(2)对象

简单的规则是,Number方法的参数是对象时,将返回NaN,除非是包含单个数值的数组。

Number({a: 1}) // NaN
Number([1, 2, 3]) // NaN
Number([5]) // 5

方式二:parseInt() & parseFloat()

这种方式专门用来对付字符串,parseInt()一个字符串转换为一个整数,可以将一个字符串中的有效的整数内容取出来,然后转换为Number。parseFloat()把一个字符串转换为一个浮点数。parseFloat()作用和parseInt()类似,不同的是它可以获得有效的小数。

console.log(parseInt('.21'));        //NaN
console.log(parseInt("10.3"));        //10
console.log(parseFloat('.21'));      //0.21
console.log(parseFloat('.d1'));       //NaN
console.log(parseFloat("10.11.33"));  //10.11
console.log(parseFloat("4.3years"));  //4.3
console.log(parseFloat("He40.3"));    //NaN

parseInt()在没有第二个参数时默认以十进制转换数值,有第二个参数时,以第二个参数为基数转换数值,如果基数有误返回NaN

console.log(parseInt("13"));          //13
console.log(parseInt("11",2));        //3
console.log(parseInt("17",8));        //15
console.log(parseInt("1f",16));       //31

两者的区别:Number函数将字符串转为数值,要比parseInt函数严格很多。基本上,只要有一个字符无法转成数值,整个字符串就会被转为NaN。

parseInt('42 cats') // 42
Number('42 cats') // NaN

上面代码中,parseInt逐个解析字符,而Number函数整体转换字符串的类型。
另外,对空字符串的处理也不一样

Number("   ");     //0    
parseInt("   ");   //NaN

3、其他的数据类型转换为Boolean

它的转换规则相对简单:只有空字符串("")、null、undefined、+0、-0 和 NaN 转为布尔型是 false,其他的都是 true,空数组、空对象转换为布尔类型也是 true,甚至连false对应的布尔对象new Boolean(false)也是true

Boolean(undefined) // false
Boolean(null) // false
Boolean(0) // false
Boolean(NaN) // false
Boolean('') // false
Boolean({}) // true
Boolean([]) // true
Boolean(new Boolean(false)) // true

二、自动转换

遇到以下三种情况时,JavaScript 会自动转换数据类型,即转换是自动完成的,用户不可见。

1.自动转换为布尔值

JavaScript 遇到预期为布尔值的地方(比如if语句的条件部分),就会将非布尔值的参数自动转换为布尔值。系统内部会自动调用Boolean函数。

if ('abc') {
  console.log('hello')
}  // "hello"

2.自动转换为数值

算数运算符(+ - * /)跟非Number类型的值进行运算时,会将这些值转换为Number,然后在运算,除了字符串的加法运算

true + 1 // 2
2 + null // 2
undefined + 1 // NaN
2 + NaN // NaN 任何值和NaN做运算都得NaN
'5' - '2' // 3
'5' * '2' // 10
true - 1  // 0
'1' - 1   // 0
'5' * []    // 0
false / '5' // 0
'abc' - 1   // NaN

一元运算符也会把运算子转成数值。

+'abc' // NaN
-'abc' // NaN
+true // 1
-false // 0

3.自动转换为字符串

字符串的自动转换,主要发生在字符串的加法运算时。当一个值为字符串,另一个值为非字符串,则后者转为字符串。

'5' + 1 // '51'
'5' + true // "5true"
'5' + false // "5false"
'5' + {} // "5[object Object]"
'5' + [] // "5"
'5' + function (){} // "5function (){}"
'5' + undefined // "5undefined"
'5' + null // "5null"

三、总结

1. 强制转换的各种情况

2. 自动转换的的各种情况

  • 只有空字符串("")、null、undefined、+0、-0 和 NaN 转为布尔型是 false,其他的都是 true
  • 除了加法运算符(+)有可能把运算子转为字符串,其他运算符都会把运算子自动转成数值。一元运算符也会把运算子转成数值。
  • 字符串的自动转换,主要发生在字符串的加法运算时。

参考文章

深入浅出Javascript闭包

一、引子

闭包(closure)是 Javascript 语言的一个难点,面试时常被问及,也是它的特色,很多高级应用都要依靠闭包实现。本文尽可能用简单易懂的话,讲清楚闭包的概念、形成条件及其常见的面试题。

我们先来看一个例子:

var n = 999;
function f1() {
console.log(n);
}
f1() // 999

上面代码中,函数f1可以读取全局变量n。但是,函数外部无法读取函数内部声明的变量。

function f1() {
var n = 999;
}
console.log(n)
// Uncaught ReferenceError: n is not defined

上面代码中,函数f1内部声明的变量n,函数外是无法读取的。

如果有时需要得到函数内的局部变量。正常情况下,这是办不到的,只有通过变通方法才能实现。那就是在函数的内部,再定义一个函数。

function f1() {
var n = 999;
function f2() {
  console.log(n); // 999
 }
}

上面代码中,函数f2就在函数f1内部,这时f1内部的所有局部变量,对f2都是可见的。既然f2可以读取f1的局部变量,那么只要把f2作为返回值,我们不就可以在f1外部读取它的内部变量了吗!

二、闭包是什么

我们可以对上面代码进行如下修改:

   function f1(){
   var a = 999;
   function f2(){
    console.log(a);
   }
   return f2; // f1返回了f2的引用
   }
   var result = f1(); // result就是f2函数了
   result();  // 执行result,全局作用域下没有a的定义,
         //但是函数闭包,能够把定义函数的时候的作用域一起记住,输出999			

上面代码中,函数f1的返回值就是函数f2,由于f2可以读取f1的内部变量,所以就可以在外部获得f1的内部变量了。

闭包就是函数f2,即能够读取其他函数内部变量的函数。由于在JavaScript语言中,只有函数内部的子函数才能读取内部变量,因此可以把闭包简单理解成“定义在一个函数内部的函数”。闭包最大的特点,就是它可以“记住”诞生的环境,比如f2记住了它诞生的环境f1,所以从f2可以得到f1的内部变量。在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。

那到底什么是闭包呢?

当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这就产生了闭包。 ----《你不知道的Javascript上卷》

我个人理解,闭包就是函数中的函数(其他语言不能函数再套函数),里面的函数可以访问外面函数的变量,外面的变量的是这个内部函数的一部分。

闭包形成的条件

  • 函数嵌套
  • 内部函数引用外部函数的局部变量

三、闭包的特性

每个函数都是闭包,每个函数天生都能够记忆自己定义时所处的作用域环境。把一个函数从它定义的那个作用域,挪走,运行。这个函数居然能够记忆住定义时的那个作用域。不管函数走到哪里,定义时的作用域就带到了哪里。接下来我们用两个例子来说明这个问题:

//例题1
var inner;
function outer(){
var a=250;
inner=function(){
alert(a);//这个函数虽然在外面执行,但能够记忆住定义时的那个作用域,a是250
  }
}
outer();
var a=300;
inner();//一个函数在执行的时候,找闭包里面的变量,不会理会当前作用域。
//例题2
function outer(x){
  function inner(y){
  console.log(x+y);
  }
return inner;
}
var inn=outer(3);//数字3传入outer函数后,inner函数中x便会记住这个值
inn(5);//当inner函数再传入5的时候,只会对y赋值,所以最后弹出8

四、闭包的内存泄漏

栈内存提供一个执行环境,即作用域,包括全局作用域和私有作用域,那他们什么时候释放内存的?

  • 全局作用域----只有当页面关闭的时候全局作用域才会销毁
  • 私有的作用域----只有函数执行才会产生

一般情况下,函数执行会形成一个新的私有的作用域,当私有作用域中的代码执行完成后,我们当前作用域都会主动的进行释放和销毁。但当遇到函数执行返回了一个引用数据类型的值,并且在函数的外面被一个其他的东西给接收了,这种情况下一般形成的私有作用域都不会销毁

如下面这种情况:

function fn(){
var num=100;
return function(){
  }
}
var f=fn();//fn执行形成的这个私有的作用域就不能再销毁了

也就是像上面这段代码,fn函数内部的私有作用域会被一直占用的,发生了内存泄漏。所谓内存泄漏指任何对象在您不再拥有或需要它之后仍然存在。闭包不能滥用,否则会导致内存泄露,影响网页的性能。闭包使用完了后,要立即释放资源,将引用变量指向null

接下来我们看下有关于内存泄漏的一道经典面试题:

  function outer(){
  var num=0;//内部变量
  return function add(){//通过return返回add函数,就可以在outer函数外访问了
  num++;//内部函数有引用,作为add函数的一部分了
  console.log(num);
  };
 }
  var func1=outer();
  func1();//实际上是调用add函数, 输出1
  func1();//输出2 因为outer函数内部的私有作用域会一直被占用
  var func2=outer();
  func2();// 输出1  每次重新引用函数的时候,闭包是全新的。
  func2();// 输出2  

五、闭包的作用

1.可以读取函数内部的变量

2.可以使变量的值长期保存在内存中,生命周期比较长。因此不能滥用闭包,否则会造成网页的性能问题

3.可以用来实现JS模块

JS模块:具有特定功能的js文件,将所有的数据和功能都封装在一个函数内部(私有的),只向外暴露一个包信n个方法的对象或函数,模块的使用者,只需要通过模块暴露的对象调用方法来实现对应的功能

具体请看下面的例子:

//index.html文件
<script type="text/javascript" src="myModule.js"></script>
<script type="text/javascript">
  myModule2.doSomething()
  myModule2.doOtherthing()
</script>
//myModule.js文件
(function () {
  var msg = 'Beijing'//私有数据
  //操作数据的函数
  function doSomething() {
    console.log('doSomething() '+msg.toUpperCase())
  }
  function doOtherthing () {
    console.log('doOtherthing() '+msg.toLowerCase())
  }
  //向外暴露对象(给外部使用的两个方法)
  window.myModule2 = {
    doSomething: doSomething,
    doOtherthing: doOtherthing
  }
})()

六、闭包的运用

我们要实现这样的一个需求: 点击某个按钮, 提示"点击的是第n个按钮",此处我们先不用事件代理:

.....
<button>测试1</button>
<button>测试2</button>
<button>测试3</button>
<script type="text/javascript">
   var btns = document.getElementsByTagName('button')
    for (var i = 0; i < btns.length; i++) {
      btns[i].onclick = function () {
        console.log('第' + (i + 1) + '个')
      }
    }
</script>  

万万没想到,点击任意一个按钮,后台都是弹出“第四个”,这是因为i是全局变量,执行到点击事件时,此时i的值为3。那该如何修改,最简单的是用let声明i

 for (let i = 0; i < btns.length; i++) {
      btns[i].onclick = function () {
        console.log('第' + (i + 1) + '个')
      }
    }

另外我们可以通过闭包的方式来修改:

   for (var i = 0; i < btns.length; i++) {
      (function (j) {
        btns[j].onclick = function () {
          console.log('第' + (j + 1) + '个')
        }
      })(i)
    }

如果觉得文章对你有些许帮助,欢迎在我的GitHub博客点赞和关注,感激不尽!

ps:文章于2018.11.16重新修改,希望对你们有所收获!

参考文章

Javascript教程

你不知道的Javascript上卷

深入理解浏览器的缓存机制

一、前言

缓存可以说是性能优化中简单高效的一种优化方式了。一个优秀的缓存策略可以缩短网页请求资源的距离,减少延迟,并且由于缓存文件可以重复利用,还可以减少带宽,降低网络负荷。

对于一个数据请求来说,可以分为发起网络请求、后端处理、浏览器响应三个步骤。浏览器缓存可以帮助我们在第一和第三步骤中优化性能。比如说直接使用缓存而不发起请求,或者发起了请求但后端存储的数据和前端一致,那么就没有必要再将数据回传回来,这样就减少了响应数据。

接下来的内容中我们将通过缓存位置、缓存策略以及实际场景应用缓存策略来探讨浏览器缓存机制。

如需获取思维导图或想阅读更多优质文章请猛戳GitHub博客

二、缓存位置

从缓存位置上来说分为四种,并且各自有优先级,当依次查找缓存且都没有命中的时候,才会去请求网络。

  • Service Worker
  • Memory Cache
  • Disk Cache
  • Push Cache

1.Service Worker

Service Worker 是运行在浏览器背后的独立线程,一般可以用来实现缓存功能。使用 Service Worker的话,传输协议必须为 HTTPS。因为 Service Worker 中涉及到请求拦截,所以必须使用 HTTPS 协议来保障安全。Service Worker 的缓存与浏览器其他内建的缓存机制不同,它可以让我们自由控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的

Service Worker 实现缓存功能一般分为三个步骤:首先需要先注册 Service Worker,然后监听到 install 事件以后就可以缓存需要的文件,那么在下次用户访问的时候就可以通过拦截请求的方式查询是否存在缓存,存在缓存的话就可以直接读取缓存文件,否则就去请求数据。

当 Service Worker 没有命中缓存的时候,我们需要去调用 fetch 函数获取数据。也就是说,如果我们没有在 Service Worker 命中缓存的话,会根据缓存查找优先级去查找数据。但是不管我们是从 Memory Cache 中还是从网络请求中获取的数据,浏览器都会显示我们是从 Service Worker 中获取的内容。

2.Memory Cache

Memory Cache 也就是内存中的缓存,主要包含的是当前中页面中已经抓取到的资源,例如页面上已经下载的样式、脚本、图片等。读取内存中的数据肯定比磁盘快,内存缓存虽然读取高效,可是缓存持续性很短,会随着进程的释放而释放。 一旦我们关闭 Tab 页面,内存中的缓存也就被释放了

那么既然内存缓存这么高效,我们是不是能让数据都存放在内存中呢?
这是不可能的。计算机中的内存一定比硬盘容量小得多,操作系统需要精打细算内存的使用,所以能让我们使用的内存必然不多。

当我们访问过页面以后,再次刷新页面,可以发现很多数据都来自于内存缓存

内存缓存中有一块重要的缓存资源是preloader相关指令(例如<link rel="prefetch">)下载的资源。总所周知preloader的相关指令已经是页面优化的常见手段之一,它可以一边解析js/css文件,一边网络请求下一个资源。

需要注意的事情是,内存缓存在缓存资源时并不关心返回资源的HTTP缓存头Cache-Control是什么值,同时资源的匹配也并非仅仅是对URL做匹配,还可能会对Content-Type,CORS等其他特征做校验

3.Disk Cache

Disk Cache 也就是存储在硬盘中的缓存,读取速度慢点,但是什么都能存储到磁盘中,比之 Memory Cache 胜在容量和存储时效性上

在所有浏览器缓存中,Disk Cache 覆盖面基本是最大的。它会根据 HTTP Herder 中的字段判断哪些资源需要缓存,哪些资源可以不请求直接使用,哪些资源已经过期需要重新请求。并且即使在跨站点的情况下,相同地址的资源一旦被硬盘缓存下来,就不会再次去请求数据。绝大部分的缓存都来自 Disk Cache,关于 HTTP 的协议头中的缓存字段,我们会在下文进行详细介绍。

浏览器会把哪些文件丢进内存中?哪些丢进硬盘中?
关于这点,网上说法不一,不过以下观点比较靠得住:

  • 对于大文件来说,大概率是不存储在内存中的,反之优先
  • 当前系统内存使用率高的话,文件优先存储进硬盘

4.Push Cache

Push Cache(推送缓存)是 HTTP/2 中的内容,当以上三种缓存都没有命中时,它才会被使用。它只在会话(Session)中存在,一旦会话结束就被释放,并且缓存时间也很短暂,在Chrome浏览器中只有5分钟左右,同时它也并非严格执行HTTP头中的缓存指令。

Push Cache 在国内能够查到的资料很少,也是因为 HTTP/2 在国内不够普及。这里推荐阅读Jake ArchibaldHTTP/2 push is tougher than I thought 这篇文章,文章中的几个结论:

  • 所有的资源都能被推送,并且能够被缓存,但是 Edge 和 Safari 浏览器支持相对比较差
  • 可以推送 no-cache 和 no-store 的资源
  • 一旦连接被关闭,Push Cache 就被释放
  • 多个页面可以使用同一个HTTP/2的连接,也就可以使用同一个Push Cache。这主要还是依赖浏览器的实现而定,出于对性能的考虑,有的浏览器会对相同域名但不同的tab标签使用同一个HTTP连接。
  • Push Cache 中的缓存只能被使用一次
  • 浏览器可以拒绝接受已经存在的资源推送
  • 你可以给其他域名推送资源

如果以上四种缓存都没有命中的话,那么只能发起请求来获取资源了。

那么为了性能上的考虑,大部分的接口都应该选择好缓存策略,通常浏览器缓存策略分为两种:强缓存和协商缓存,并且缓存策略都是通过设置 HTTP Header 来实现的

三、缓存过程分析

浏览器与服务器通信的方式为应答模式,即是:浏览器发起HTTP请求 – 服务器响应该请求,那么浏览器怎么确定一个资源该不该缓存,如何去缓存呢?浏览器第一次向服务器发起该请求后拿到请求结果后,将请求结果和缓存标识存入浏览器缓存,浏览器对于缓存的处理是根据第一次请求资源时返回的响应头来确定的。具体过程如下图:

第一次发起HTTP请求

由上图我们可以知道:

  • 浏览器每次发起请求,都会先在浏览器缓存中查找该请求的结果以及缓存标识

  • 浏览器每次拿到返回的请求结果都会将该结果和缓存标识存入浏览器缓存中

以上两点结论就是浏览器缓存机制的关键,它确保了每个请求的缓存存入与读取,只要我们再理解浏览器缓存的使用规则,那么所有的问题就迎刃而解了,本文也将围绕着这点进行详细分析。为了方便大家理解,这里我们根据是否需要向服务器重新发起HTTP请求将缓存过程分为两个部分,分别是强缓存和协商缓存。

四、强缓存

强缓存:不会向服务器发送请求,直接从缓存中读取资源,在chrome控制台的Network选项中可以看到该请求返回200的状态码,并且Size显示from disk cache或from memory cache。强缓存可以通过设置两种 HTTP Header 实现:Expires 和 Cache-Control。

1.Expires

缓存过期时间,用来指定资源到期的时间,是服务器端的具体的时间点。也就是说,Expires=max-age + 请求时间,需要和Last-modified结合使用。Expires是Web服务器响应消息头字段,在响应http请求时告诉浏览器在过期时间前浏览器可以直接从浏览器缓存取数据,而无需再次请求。

Expires 是 HTTP/1 的产物,受限于本地时间,如果修改了本地时间,可能会造成缓存失效Expires: Wed, 22 Oct 2018 08:41:00 GMT表示资源会在 Wed, 22 Oct 2018 08:41:00 GMT 后过期,需要再次请求。

2.Cache-Control

在HTTP/1.1中,Cache-Control是最重要的规则,主要用于控制网页缓存。比如当Cache-Control:max-age=300时,则代表在这个请求正确返回时间(浏览器也会记录下来)的5分钟内再次加载资源,就会命中强缓存。

Cache-Control 可以在请求头或者响应头中设置,并且可以组合使用多种指令:

public所有内容都将被缓存(客户端和代理服务器都可缓存)。具体来说响应可被任何中间节点缓存,如 Browser <-- proxy1 <-- proxy2 <-- Server,中间的proxy可以缓存资源,比如下次再请求同一资源proxy1直接把自己缓存的东西给 Browser 而不再向proxy2要。

private所有内容只有客户端可以缓存,Cache-Control的默认取值。具体来说,表示中间节点不允许缓存,对于Browser <-- proxy1 <-- proxy2 <-- Server,proxy 会老老实实把Server 返回的数据发送给proxy1,自己不缓存任何数据。当下次Browser再次请求时proxy会做好请求转发而不是自作主张给自己缓存的数据。

no-cache:客户端缓存内容,是否使用缓存则需要经过协商缓存来验证决定。表示不使用 Cache-Control的缓存控制方式做前置验证,而是使用 Etag 或者Last-Modified字段来控制缓存。需要注意的是,no-cache这个名字有一点误导。设置了no-cache之后,并不是说浏览器就不再缓存数据,只是浏览器在使用缓存数据时,需要先确认一下数据是否还跟服务器保持一致。

no-store:所有内容都不会被缓存,即不使用强制缓存,也不使用协商缓存

max-age:max-age=xxx (xxx is numeric)表示缓存内容将在xxx秒后失效

s-maxage(单位为s):同max-age作用一样,只在代理服务器中生效(比如CDN缓存)。比如当s-maxage=60时,在这60秒中,即使更新了CDN的内容,浏览器也不会进行请求。max-age用于普通缓存,而s-maxage用于代理缓存。s-maxage的优先级高于max-age。如果存在s-maxage,则会覆盖掉max-age和Expires header。

max-stale:能容忍的最大过期时间。max-stale指令标示了客户端愿意接收一个已经过期了的响应。如果指定了max-stale的值,则最大容忍时间为对应的秒数。如果没有指定,那么说明浏览器愿意接收任何age的响应(age表示响应由源站生成或确认的时间与当前时间的差值)。

min-fresh:能够容忍的最小新鲜度。min-fresh标示了客户端不愿意接受新鲜度不多于当前的age加上min-fresh设定的时间之和的响应。
cache-control

从图中我们可以看到,我们可以将多个指令配合起来一起使用,达到多个目的。比如说我们希望资源能被缓存下来,并且是客户端和代理服务器都能缓存,还能设置缓存失效时间等等。

3.Expires和Cache-Control两者对比

其实这两者差别不大,区别就在于 Expires 是http1.0的产物,Cache-Control是http1.1的产物,两者同时存在的话,Cache-Control优先级高于Expires;在某些不支持HTTP1.1的环境下,Expires就会发挥用处。所以Expires其实是过时的产物,现阶段它的存在只是一种兼容性的写法。
强缓存判断是否缓存的依据来自于是否超出某个时间或者某个时间段,而不关心服务器端文件是否已经更新,这可能会导致加载文件不是服务器端最新的内容,那我们如何获知服务器端内容是否已经发生了更新呢?此时我们需要用到协商缓存策略。

五、协商缓存

协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程,主要有以下两种情况

  • 协商缓存生效,返回304和Not Modified
    协商缓存生效

  • 协商缓存失效,返回200和请求结果

协商缓存失效
协商缓存可以通过设置两种 HTTP Header 实现:Last-Modified 和 ETag 。

1.Last-Modified和If-Modified-Since

浏览器在第一次访问资源时,服务器返回资源的同时,在response header中添加 Last-Modified的header,值是这个资源在服务器上的最后修改时间,浏览器接收后缓存文件和header;

Last-Modified: Fri, 22 Jul 2016 01:47:00 GMT

浏览器下一次请求这个资源,浏览器检测到有 Last-Modified这个header,于是添加If-Modified-Since这个header,值就是Last-Modified中的值;服务器再次收到这个资源请求,会根据 If-Modified-Since 中的值与服务器中这个资源的最后修改时间对比,如果没有变化,返回304和空的响应体,直接从缓存读取,如果If-Modified-Since的时间小于服务器中这个资源的最后修改时间,说明文件有更新,于是返回新的资源文件和200

但是 Last-Modified 存在一些弊端:

  • 如果本地打开缓存文件,即使没有对文件进行修改,但还是会造成 Last-Modified 被修改,服务端不能命中缓存导致发送相同的资源
  • 因为 Last-Modified 只能以秒计时,如果在不可感知的时间内修改完成文件,那么服务端会认为资源还是命中了,不会返回正确的资源

既然根据文件修改时间来决定是否缓存尚有不足,能否可以直接根据文件内容是否修改来决定缓存策略?所以在 HTTP / 1.1 出现了 ETagIf-None-Match

2.ETag和If-None-Match

Etag是服务器响应请求时,返回当前资源文件的一个唯一标识(由服务器生成),只要资源有变化,Etag就会重新生成。浏览器在下一次加载资源向服务器发送请求时,会将上一次返回的Etag值放到request header里的If-None-Match里,服务器只需要比较客户端传来的If-None-Match跟自己服务器上该资源的ETag是否一致,就能很好地判断资源相对客户端而言是否被修改过了。如果服务器发现ETag匹配不上,那么直接以常规GET 200回包形式将新的资源(当然也包括了新的ETag)发给客户端;如果ETag是一致的,则直接返回304知会客户端直接使用本地缓存即可。

ETag和If-None-Match

3.两者之间对比:

  • 首先在精确度上,Etag要优于Last-Modified。

Last-Modified的时间单位是秒,如果某个文件在1秒内改变了多次,那么他们的Last-Modified其实并没有体现出来修改,但是Etag每次都会改变确保了精度;如果是负载均衡的服务器,各个服务器生成的Last-Modified也有可能不一致。

  • 第二在性能上,Etag要逊于Last-Modified,毕竟Last-Modified只需要记录时间,而Etag需要服务器通过算法来计算出一个hash值。
  • 第三在优先级上,服务器校验优先考虑Etag

六、缓存机制

强制缓存优先于协商缓存进行,若强制缓存(Expires和Cache-Control)生效则直接使用缓存,若不生效则进行协商缓存(Last-Modified / If-Modified-Since和Etag / If-None-Match),协商缓存由服务器决定是否使用缓存,若协商缓存失效,那么代表该请求的缓存失效,返回200,重新返回资源和缓存标识,再存入浏览器缓存中;生效则返回304,继续使用缓存。具体流程图如下:

缓存的机制

看到这里,不知道你是否存在这样一个疑问:如果什么缓存策略都没设置,那么浏览器会怎么处理?

对于这种情况,浏览器会采用一个启发式的算法,通常会取响应头中的 Date 减去 Last-Modified 值的 10% 作为缓存时间。

七、实际场景应用缓存策略

1.频繁变动的资源

Cache-Control: no-cache

对于频繁变动的资源,首先需要使用Cache-Control: no-cache 使浏览器每次都请求服务器,然后配合 ETag 或者 Last-Modified 来验证资源是否有效。这样的做法虽然不能节省请求数量,但是能显著减少响应数据大小。

2.不常变化的资源

Cache-Control: max-age=31536000

通常在处理这类资源时,给它们的 Cache-Control 配置一个很大的 max-age=31536000 (一年),这样浏览器之后请求相同的 URL 会命中强制缓存。而为了解决更新的问题,就需要在文件名(或者路径)中添加 hash, 版本号等动态字符,之后更改动态字符,从而达到更改引用 URL 的目的,让之前的强制缓存失效 (其实并未立即失效,只是不再使用了而已)。
在线提供的类库 (如 jquery-3.3.1.min.js, lodash.min.js 等) 均采用这个模式。

八、用户行为对浏览器缓存的影响

所谓用户行为对浏览器缓存的影响,指的就是用户在浏览器如何操作时,会触发怎样的缓存策略。主要有 3 种:

  • 打开网页,地址栏输入地址: 查找 disk cache 中是否有匹配。如有则使用;如没有则发送网络请求。
  • 普通刷新 (F5):因为 TAB 并没有关闭,因此 memory cache 是可用的,会被优先使用(如果匹配的话)。其次才是 disk cache。
  • 强制刷新 (Ctrl + F5):浏览器不使用缓存,因此发送的请求头部均带有 Cache-control: no-cache(为了兼容,还带了 Pragma: no-cache),服务器直接返回 200 和最新内容。

参考文章

如何清除浮动

本文概述

本文的框架图如下:

一、浮动到底是什么?

W3school中给出的浮动定义为浮动的框可以向左或向右移动,直到它的外边缘碰到包含框或另一个浮动框的边框为止。由于浮动框脱离文档的普通流中,所以文档的普通流中的块框表现得就像浮动框不存在一样。
css浮动

二、浮动有什么特点?

浮动的特点,可以用八个字总结:脱标、贴边、字围和收缩。

为了更好说明,请看下图:
当框 1 向左浮动时,它脱离文档流(脱标)并且向左移动(贴边),直到它的左边缘碰到包含框的左边缘。因为它不再处于文档流中,所以它不占据空间,实际上覆盖住了框 2,使框 2 从视图中消失。如果框2中有文字,就会围着框1排开(字围)。

如果把所有三个框都向左浮动,那么框 1 向左浮动直到碰到包含框,另外两个框向左浮动直到碰到前一个浮动框。
CSS 浮动实例 - 向左浮动的元素
下面着重讲解下第四个特点--收缩

一个浮动的内联元素(比如span img标签)不需要设置display:block就可以设置宽度。

<style>
        div{
            float: left;
            background-color: greenyellow;
        }
    </style>
</head>
<body>
<div>
    这是一段文字
</div>
</body>

得到以下的效果:

我们都知道div标签是块级元素,会独占一行,然而上面的例子中将div设置为左浮后,其宽度不再是占满一行,而是收紧为内部元素的宽度,这就是浮动第四个特征的含义。

三、浮动有什么缺点?

先看下面这段代码:

  <style>
      .parent{
          border: solid 5px;
          width:300px;
      }
      .child:nth-child(1){
          height: 100px;
          width: 100px;
          background-color: yellow;
          float: left;
      }
      .child:nth-child(2){
          height: 100px;
          width: 100px;
          background-color: red;
          float: left;
      }
      .child:nth-child(3){
          height: 100px;
          width: 100px;
          background-color: greenyellow;
          float: left;
      }
    </style>
</head>
<body>
<div class="parent">
    <div class="child"></div>
    <div class="child"></div>
    <div class="child"></div>
</div>
</body>

我们想让父容器包裹着三个浮动元素,然而事与愿违,得到却是这样的结果:
父容器高度塌陷
这就是浮动带来副作用----父容器高度塌陷,于是清理浮动就显着至关重要。

四、如何清理浮动?

清除浮动不是不用浮动,清除浮动产生的父容器高度塌陷

套路1:给浮动元素的父元素添加高度(扩展性不好)

如果一个元素要浮动,那么它的父元素一定要有高度。高度的盒子,才能关住浮动。可以通过直接给父元素设置height,实际应用中我们不大可能给所有的盒子加高度,不仅麻烦,并且不能适应页面的快速变化;另外一种,父容器的高度可以通过内容撑开(比如img图片),实际当中此方法用的比较多。

套路2:clear:both;

在最后一个子元素新添加最后一个冗余元素,然后将其设置clear:both,这样就可以清除浮动。这里强调一点,即在父级元素末尾添加的元素必须是一个块级元素,否则无法撑起父级元素高度

	<div id="wrap">
	    <div id="inner"></div>
	    <div style="clear: both;"></div>
	</div>
	#wrap{
	      border: 1px solid;
	}
	#inner{
	      float: left;
	      width: 200px;
	      height: 200px;
	      background: pink;
	}

套路3:伪元素清除浮动

上面那种办法固然可以清除浮动,但是我们不想在页面中添加这些没有意义的冗余元素,此时如何清除浮动吗?
结合 :after 伪元素和 IEhack ,可以完美兼容当前主流的各大浏览器,这里的 IEhack 指的是触发 hasLayout

<div id="wrap" class="clearfix">
    <div id="inner"></div>
</div>
      #wrap {
        border: 1px solid;
      }
      #inner {
        float: left;
        width: 200px;
        height: 200px;
        background: pink;
      }
      /*开启haslayout*/
      .clearfix {
        *zoom: 1;
      }
      /*ie6 7 不支持伪元素*/
      .clearfix:after {
        content: '';
        display: block;
        clear: both;
        height:0;
        line-height:0;
        visibility:hidden;//允许浏览器渲染它,但是不显示出来
      }

给浮动元素的父容器添加一个clearfix的class,然后给这个class添加一个:after伪元素,实现元素末尾添加一个看不见的块元素来清理浮动。这是通用的清理浮动方案,推荐使用

套路4:给父元素使用overflow:hidden;

这种方案让父容器形成了BFC(块级格式上下文),而BFC可以包含浮动,通常用来解决浮动父元素高度坍塌的问题。

BFC的触发方式

我们可以给父元素添加以下属性来触发BFC:

  • float 为 left | right

  • overflow 为 hidden | auto | scorll

  • display 为 table-cell | table-caption | inline-block

  • position 为 absolute | fixed

这里可以给父元素设置overflow:auto,但是为了兼容IE最好使用overflow:hidden。

但这种办法有个缺陷:如果有内容出了盒子,用这种方法就会把多的部分裁切掉,所以这时候不能使用。

BFC的主要特征:

  • BFC容器是一个隔离的容器,和其他元素互不干扰;所以我们可以用触发两个元素的BFC来解决垂直边距折叠问题。

  • BFC不会重叠浮动元素

  • BFC可以包含浮动,这可以清除浮动。

套路5:br标签清浮动

br标签存在一个属性:clear。这个属性就是能够清除浮动的利器,在br标签中设置属性clear,并赋值all。即能清除掉浮动

    <div id="wrap">
      <div id="inner"></div>
      <br clear="all" />
    </div>
      #wrap {
        border: 1px solid;
      }
      #inner {
        float: left;
        width: 200px;
        height: 200px;
        background: pink;
      }

参考文章

如何居中一个元素(终结版)

前言

本文主要介绍水平居中,垂直居中,还有水平垂直居中各种办法,思维导图如下:
image

一、水平居中

1.行内元素水平居中

利用 text-align: center 可以实现在块级元素内部的行内元素水平居中。此方法对inline、inline-block、inline-table和inline-flex元素水平居中都有效。

   .parent{//在父容器设置
        text-align:center;
    }

此外,如果块级元素内部包着也是一个块级元素,我们可以先将其由块级元素改变为行内块元素,再通过设置行内块元素居中以达到水平居中

<div class="parent">
  <div class="child">Demo</div>
</div>
<style>
  .parent{
    text-align:center;  
  }
  .child {
    display: inline-block;
  }
</style>

2.块级元素的水平居中

这种情形可以有多种实现方式,下面我们详细介绍:

①将该块级元素左右外边距margin-left和margin-right设置为auto

.child{
    width: 100px;//确保该块级元素定宽
    margin:0 auto;
}

②使用table+margin

先将子元素设置为块级表格来显示(类似),再将其设置水平居中

display:table在表现上类似block元素,但是宽度为内容宽。

<div class="parent">
  <div class="child">Demo</div>
</div>
<style>
  .child {
    display: table;
    margin: 0 auto;
  }
</style>

③使用absolute+transform

先将父元素设置为相对定位,再将子元素设置为绝对定位,向右移动子元素,移动距离为父容器的一半,最后通过向左移动子元素的一半宽度以达到水平居中。

<div class="parent">
  <div class="child">Demo</div>
</div>
<style>
  .child {
    position:absolute;
    left:50%;
    transform:translateX(-50%);
  }
  .parent {
    position:relative;
  }
</style>

不过transform属于css3内容,兼容性存在一定问题,高版本浏览器需要添加一些前缀

④使用flex+justify-content

通过CSS3中的布局利器flex中的justify-content属性来达到水平居中。

<div class="parent">
  <div class="child">Demo</div>
</div>
<style>
  .parent {
    display: flex;
    justify-content:center;
  }
</style>

⑤使用flex+margin

通过flex将父容器设置为为Flex布局,再设置子元素居中。

<div class="parent">
  <div class="child">Demo</div>
</div>
<style>
  .parent {
    display: flex;
  }
  .child {
    margin:0 auto;
  }
</style>

3.多块级元素水平居中

①利用flex布局

利用弹性布局(flex),实现水平居中,其中justify-content 用于设置弹性盒子元素在主轴(默认横轴)方向上的对齐方式,本例中设置子元素水平居中显示。

 #container {
    display: flex;
    justify-content: center;
}

源代码演示

②利用inline-block

将要水平排列的块状元素设为display:inline-block,然后在父级元素上设置text-align:center,达到与上面的行内元素的水平居中一样的效果

.container {
text-align: center;
}
.inline-block {
display: inline-block;
}

源代码演示

4.浮动元素水平居中

  • 对于定宽的浮动元素,通过子元素设置relative + 负margin
  • 对于不定宽的浮动元素,父子容器都用相对定位
  • 通用方法(不管是定宽还是不定宽):flex布局

①定宽的非浮动元素

通过子元素设置relative + 负margin,原理见下图:


注意:样式设置在浮动元素本身

.child {
   position:relative;
   left:50%;
   margin-left:-250px;
}
<div class="parent">
  <span class="child" style="float: left;width: 500px;">我是要居中的浮动元素</span>
</div>

②不定宽的浮动元素

通过父子容器都相对定位,偏移位移见下图:

注意:要清除浮动,给外部元素加上float。这里的父元素就是外部元素

<div class="box">
    <p>我是浮动的</p>
    <p>我也是居中的</p>
</div>
.box{
    float:left;
    position:relative;
    left:50%;
}
p{
    float:left;
    position:relative;
    right:50%;
}

③通用办法flex布局(不管是定宽还是不定宽)

利用弹性布局(flex)的justify-content属性,实现水平居中

.parent {
    display:flex;
    justify-content:center;
}
.chlid{
    float: left;
    width: 200px;//有无宽度不影响居中
}
<div class="parent">
  <span class="chlid">我是要居中的浮动元素</span>
</div>

5.绝对定位元素水平居中

这种方式非常独特,通过子元素绝对定位,外加margin: 0 auto来实现

<div class="parent">
    <div class="child">让绝对定位的元素水平居中对齐。</div>
</div>
  .parent{
        position:relative;
    }
   .child{
         position: absolute; /*绝对定位*/
         width: 200px;
         height:100px;
         background: yellow;
         margin: 0 auto; /*水平居中*/
         left: 0; /*此处不能省略,且为0*/
         right: 0;/*此处不能省略,且为0*/
    }

二、垂直居中

1.单行内联元素垂直居中

<div id="box">
     <span>单行内联元素垂直居中。</span>。
</div>
<style>
 #box {
    height: 120px;
    line-height: 120px;
    border: 2px dashed #f69c55;
    }
</style>

2.多行内联元素垂直居中

①利用flex布局(flex)

利用flex布局实现垂直居中,其中flex-direction: column定义主轴方向为纵向。这种方式在较老的浏览器存在兼容性问题。

<div class="parent">
    <p>Dance like nobody is watching, code like everybody is.    
    Dance like nobody is watching, code like everybody is.    
    Dance like nobody is watching, code like everybody is.</p>
</div>
<style>
    .parent { 
        height: 140px;
        display: flex;
        flex-direction: column;
        justify-content: center;
        border: 2px dashed #f69c55;
    }
</style>

②利用表布局(table)

利用表布局的vertical-align: middle可以实现子元素的垂直居中

<div class="parent">
    <p class="child">The more technology you learn, the more you realize how little you know.
    The more technology you learn, the more you realize how little you know.
    The more technology you learn, the more you realize how little you know.</p>
</div>
 <style>
    .parent {
        display: table;
        height: 140px;
        border: 2px dashed #f69c55;
    }
    .child {
        display: table-cell;
        vertical-align: middle;
    }
</style>

3 块级元素垂直居中

①使用absolute+负margin(已知高度宽度)

通过绝对定位元素距离顶部50%,并设置margin-top向上偏移元素高度的一半,就可以实现了

<div class="parent">
    <div class="child">固定高度的块级元素垂直居中。</div>
</div>
.parent {
position: relative;
}
.child {
position: absolute;
top: 50%;
height: 100px;
margin-top: -50px;
}

②使用absolute+transform

当垂直居中的元素的高度和宽度未知时,可以借助CSS3中的transform属性向Y轴反向偏移50%的方法实现垂直居中。但是部分浏览器存在兼容性的问题。

<div class="parent">
    <div class="child">未知高度的块级元素垂直居中。</div>
</div>
.parent {
position: relative;
}
.child {
position: absolute;
top: 50%;
transform: translateY(-50%);
}

③使用flex+align-items

通过设置flex布局中的属性align-items,使子元素垂直居中

<div class="parent">
    <div class="child">未知高度的块级元素垂直居中。</div>
</div>
.parent {
    display:flex;
    align-items:center;
}

④使用table-cell+vertical-align

通过将父元素转化为一个表格单元格显示(类似 <td><th>),再通过设置 vertical-align属性,使表格单元格内容垂直居中。

<div class="parent">
  <div class="child">Demo</div>
</div>
<style>
  .parent {
    display: table-cell;
    vertical-align: middle;
  }
</style>

三、水平垂直居中

这种情形也是有多种实现方式,接下去我们娓娓道来:

方法1:绝对定位与负边距实现(已知高度宽度)

这种方式需要知道被垂直居中元素的高和宽,才能计算出margin值,兼容所有浏览器

// css部分
 #container {
      position: relative;
    }
 #center {
      position: absolute;
      top: 50%;
      left: 50%;
      margin: -50px 0 0 -50px;
    }
// html部分(这部分不做变化,下面例子直接共用)
<body>
  <div id='container'>
    <div id='center' style="width: 100px;height: 100px;background-color: #666">center</div>
  </div>
</body>

方法2:绝对定位与margin:auto(已知高度宽度)

这种方式无需知道被垂直居中元素的高和宽,但不能兼容低版本的IE浏览器。

 #container {
      position: relative;
      height:100px;//必须有个高度
    }
 #center {
      position: absolute;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      margin: auto;//注意此处的写法
    }

方法3:绝对定位+CSS3(未知元素的高宽)

利用Css3的transform,可以轻松的在未知元素的高宽的情况下实现元素的垂直居中
CSS3的transform固然好用,但在项目的实际运用中必须考虑兼容问题,大量的hack代码可能会导致得不偿失。

  #container {
      position: relative;
    }
  #center {
      position: absolute;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
    }

方法4:flex布局

利用flex布局,其中justify-content 用于设置或检索弹性盒子元素在主轴(横轴)方向上的对齐方式;而align-items属性定义flex子项在flex容器的当前行的侧轴(纵轴)方向上的对齐方式。不能兼容低版本的IE浏览器。

   #container {//直接在父容器设置即可
      height: 100vh;//必须有高度
      display: flex;
      justify-content: center;
      align-items: center;
    }

方法5:flex/grid与margin:auto(最简单写法)

容器元素设为 flex 布局或是grid布局,子元素只要写 margin: auto 即可,不能兼容低版本的IE浏览器。

  #container {
      height: 100vh;//必须有高度
      display: grid;
    }
  #center {
      margin: auto;
    }


如果觉得文章对你有些许帮助,欢迎在我的GitHub博客点赞和关注,感激不尽!

后记

写博客是件挺费精力的事,特别是你有想写出一篇好博客的初衷,本文前后花了不止五六个小时,但我却常常乐此不疲!一方面是看了很多博客,自己不手动敲一遍整理一遍,就感觉没掌握一样;另一方面,分享学习心得是件很快乐的事,愿与诸君共同成长进步!如果觉得本文还不错,记得点赞关注喔!因为往往是普通人才最需要别人的鼓励和支持!

参考文章

常见六大Web安全攻防解析

前言

在互联网时代,数据安全与个人隐私受到了前所未有的挑战,各种新奇的攻击技术层出不穷。如何才能更好地保护我们的数据?本文主要侧重于分析几种常见的攻击的类型以及防御的方法。

一、XSS

XSS (Cross-Site Scripting),跨站脚本攻击,因为缩写和 CSS重叠,所以只能叫 XSS。跨站脚本攻击是指通过存在安全漏洞的Web网站注册用户的浏览器内运行非法的HTML标签或JavaScript进行的一种攻击。

跨站脚本攻击有可能造成以下影响:

  • 利用虚假输入表单骗取用户个人信息。
  • 利用脚本窃取用户的Cookie值,被害者在不知情的情况下,帮助攻击者发送恶意请求。
  • 显示伪造的文章或图片。

XSS 的原理是恶意攻击者往 Web 页面里插入恶意可执行网页脚本代码,当用户浏览该页之时,嵌入其中 Web 里面的脚本代码会被执行,从而可以达到攻击者盗取用户信息或其他侵犯用户安全隐私的目的

XSS 的攻击方式千变万化,但还是可以大致细分为几种类型。

1.非持久型 XSS(反射型 XSS )

非持久型 XSS 漏洞,一般是通过给别人发送带有恶意脚本代码参数的 URL,当 URL 地址被打开时,特有的恶意代码参数被 HTML 解析、执行。


举一个例子,比如页面中包含有以下代码:

<select>
    <script>
        document.write(''
            + '<option value=1>'
            +     location.href.substring(location.href.indexOf('default=') + 8)
            + '</option>'
        );
        document.write('<option value=2>English</option>');
    </script>
</select>

攻击者可以直接通过 URL (类似:https://xxx.com/xxx?default=<script>alert(document.cookie)</script>) 注入可执行的脚本代码。不过一些浏览器如Chrome其内置了一些XSS过滤器,可以防止大部分反射型XSS攻击。

非持久型 XSS 漏洞攻击有以下几点特征:

  • 即时性,不经过服务器存储,直接通过 HTTP 的 GET 和 POST 请求就能完成一次攻击,拿到用户隐私数据。
  • 攻击者需要诱骗点击,必须要通过用户点击链接才能发起
  • 反馈率低,所以较难发现和响应修复
  • 盗取用户敏感保密信息

为了防止出现非持久型 XSS 漏洞,需要确保这么几件事情:

  • Web 页面渲染的所有内容或者渲染的数据都必须来自于服务端。
  • 尽量不要从 URLdocument.referrerdocument.forms 等这种 DOM API 中获取数据直接渲染。
  • 尽量不要使用 eval, new Function()document.write()document.writeln()window.setInterval()window.setTimeout()innerHTMLdocument.createElement() 等可执行字符串的方法。
  • 如果做不到以上几点,也必须对涉及 DOM 渲染的方法传入的字符串参数做 escape 转义。
  • 前端渲染的时候对任何的字段都需要做 escape 转义编码。

2.持久型 XSS(存储型 XSS)

持久型 XSS 漏洞,一般存在于 Form 表单提交等交互功能,如文章留言,提交文本信息等,黑客利用的 XSS 漏洞,将内容经正常功能提交进入数据库持久保存,当前端页面获得后端从数据库中读出的注入代码时,恰好将其渲染执行。

举个例子,对于评论功能来说,就得防范持久型 XSS 攻击,因为我可以在评论中输入以下内容

主要注入页面方式和非持久型 XSS 漏洞类似,只不过持久型的不是来源于 URL,referer,forms 等,而是来源于后端从数据库中读出来的数据 。持久型 XSS 攻击不需要诱骗点击,黑客只需要在提交表单的地方完成注入即可,但是这种 XSS 攻击的成本相对还是很高。

攻击成功需要同时满足以下几个条件:

  • POST 请求提交表单后端没做转义直接入库。
  • 后端从数据库中取出数据没做转义直接输出给前端。
  • 前端拿到后端数据没做转义直接渲染成 DOM。

持久型 XSS 有以下几个特点:

  • 持久性,植入在数据库中
  • 盗取用户敏感私密信息
  • 危害面广

3.如何防御

对于 XSS 攻击来说,通常有两种方式可以用来防御。

1) CSP

CSP 本质上就是建立白名单,开发者明确告诉浏览器哪些外部资源可以加载和执行。我们只需要配置规则,如何拦截是由浏览器自己实现的。我们可以通过这种方式来尽量减少 XSS 攻击。

通常可以通过两种方式来开启 CSP:

  • 设置 HTTP Header 中的 Content-Security-Policy
  • 设置 meta 标签的方式

这里以设置 HTTP Header 来举例:

  • 只允许加载本站资源
Content-Security-Policy: default-src 'self'
  • 只允许加载 HTTPS 协议图片
Content-Security-Policy: img-src https://*
  • 允许加载任何来源框架
Content-Security-Policy: child-src 'none'

如需了解更多属性,请查看Content-Security-Policy文档

对于这种方式来说,只要开发者配置了正确的规则,那么即使网站存在漏洞,攻击者也不能执行它的攻击代码,并且 CSP 的兼容性也不错。

2) 转义字符

用户的输入永远不可信任的,最普遍的做法就是转义输入输出的内容,对于引号、尖括号、斜杠进行转义

function escape(str) {
  str = str.replace(/&/g, '&amp;')
  str = str.replace(/</g, '&lt;')
  str = str.replace(/>/g, '&gt;')
  str = str.replace(/"/g, '&quto;')
  str = str.replace(/'/g, '&#39;')
  str = str.replace(/`/g, '&#96;')
  str = str.replace(/\//g, '&#x2F;')
  return str
}

但是对于显示富文本来说,显然不能通过上面的办法来转义所有字符,因为这样会把需要的格式也过滤掉。对于这种情况,通常采用白名单过滤的办法,当然也可以通过黑名单过滤,但是考虑到需要过滤的标签和标签属性实在太多,更加推荐使用白名单的方式。

const xss = require('xss')
let html = xss('<h1 id="title">XSS Demo</h1><script>alert("xss");</script>')
// -> <h1>XSS Demo</h1>&lt;script&gt;alert("xss");&lt;/script&gt;
console.log(html)

以上示例使用了 js-xss 来实现,可以看到在输出中保留了 h1 标签且过滤了 script 标签。

3) HttpOnly Cookie。

这是预防XSS攻击窃取用户cookie最有效的防御手段。Web应用程序在设置cookie时,将其属性设为HttpOnly,就可以避免该网页的cookie被客户端恶意JavaScript窃取,保护用户cookie信息。

二、CSRF

CSRF(Cross Site Request Forgery),即跨站请求伪造,是一种常见的Web攻击,它利用用户已登录的身份,在用户毫不知情的情况下,以用户的名义完成非法操作。

1.CSRF攻击的原理

下面先介绍一下CSRF攻击的原理:

完成 CSRF 攻击必须要有三个条件:

  • 用户已经登录了站点 A,并在本地记录了 cookie
  • 在用户没有登出站点 A 的情况下(也就是 cookie 生效的情况下),访问了恶意攻击者提供的引诱危险站点 B (B 站点要求访问站点A)。
  • 站点 A 没有做任何 CSRF 防御

我们来看一个例子: 当我们登入转账页面后,突然眼前一亮惊现"XXX隐私照片,不看后悔一辈子"的链接,耐不住内心躁动,立马点击了该危险的网站(页面代码如下图所示),但当这页面一加载,便会执行submitForm这个方法来提交转账请求,从而将10块转给黑客。

2.如何防御

防范 CSRF 攻击可以遵循以下几种规则:

  • Get 请求不对数据进行修改
  • 不让第三方网站访问到用户 Cookie
  • 阻止第三方网站请求接口
  • 请求时附带验证信息,比如验证码或者 Token

1) SameSite

可以对 Cookie 设置 SameSite 属性。该属性表示 Cookie 不随着跨域请求发送,可以很大程度减少 CSRF 的攻击,但是该属性目前并不是所有浏览器都兼容。

2) Referer Check

HTTP Referer是header的一部分,当浏览器向web服务器发送请求时,一般会带上Referer信息告诉服务器是从哪个页面链接过来的,服务器籍此可以获得一些信息用于处理。可以通过检查请求的来源来防御CSRF攻击。正常请求的referer具有一定规律,如在提交表单的referer必定是在该页面发起的请求。所以通过检查http包头referer的值是不是这个页面,来判断是不是CSRF攻击

但在某些情况下如从https跳转到http,浏览器处于安全考虑,不会发送referer,服务器就无法进行check了。若与该网站同域的其他网站有XSS漏洞,那么攻击者可以在其他网站注入恶意脚本,受害者进入了此类同域的网址,也会遭受攻击。出于以上原因,无法完全依赖Referer Check作为防御CSRF的主要手段。但是可以通过Referer Check来监控CSRF攻击的发生。

3) Anti CSRF Token

目前比较完善的解决方案是加入Anti-CSRF-Token。即发送请求时在HTTP 请求中以参数的形式加入一个随机产生的token,并在服务器建立一个拦截器来验证这个token。服务器读取浏览器当前域cookie中这个token值,会进行校验该请求当中的token和cookie当中的token值是否都存在且相等,才认为这是合法的请求。否则认为这次请求是违法的,拒绝该次服务。

这种方法相比Referer检查要安全很多,token可以在用户登陆后产生并放于session或cookie中,然后在每次请求时服务器把token从session或cookie中拿出,与本次请求中的token 进行比对。由于token的存在,攻击者无法再构造出一个完整的URL实施CSRF攻击。但在处理多个页面共存问题时,当某个页面消耗掉token后,其他页面的表单保存的还是被消耗掉的那个token,其他页面的表单提交时会出现token错误。

4) 验证码

应用程序和用户进行交互过程中,特别是账户交易这种核心步骤,强制用户输入验证码,才能完成最终请求。在通常情况下,验证码够很好地遏制CSRF攻击。但增加验证码降低了用户的体验,网站不能给所有的操作都加上验证码。所以只能将验证码作为一种辅助手段,在关键业务点设置验证码。

三、点击劫持

点击劫持是一种视觉欺骗的攻击手段。攻击者将需要攻击的网站通过 iframe 嵌套的方式嵌入自己的网页中,并将 iframe 设置为透明,在页面中透出一个按钮诱导用户点击。

1. 特点

  • 隐蔽性较高,骗取用户操作
  • "UI-覆盖攻击"
  • 利用iframe或者其它标签的属性

2. 点击劫持的原理

用户在登陆 A 网站的系统后,被攻击者诱惑打开第三方网站,而第三方网站通过 iframe 引入了 A 网站的页面内容,用户在第三方网站中点击某个按钮(被装饰的按钮),实际上是点击了 A 网站的按钮。
接下来我们举个例子:我在优酷发布了很多视频,想让更多的人关注它,就可以通过点击劫持来实现

iframe {
width: 1440px;
height: 900px;
position: absolute;
top: -0px;
left: -0px;
z-index: 2;
-moz-opacity: 0;
opacity: 0;
filter: alpha(opacity=0);
}
button {
position: absolute;
top: 270px;
left: 1150px;
z-index: 1;
width: 90px;
height:40px;
}
</style>
......
<button>点击脱衣</button>
<img src="http://pic1.win4000.com/wallpaper/2018-03-19/5aaf2bf0122d2.jpg">
<iframe src="http://i.youku.com/u/UMjA0NTg4Njcy" scrolling="no"></iframe>


从上图可知,攻击者通过图片作为页面背景,隐藏了用户操作的真实界面,当你按耐不住好奇点击按钮以后,真正的点击的其实是隐藏的那个页面的订阅按钮,然后就会在你不知情的情况下订阅了。

3. 如何防御

1)X-FRAME-OPTIONS

X-FRAME-OPTIONS是一个 HTTP 响应头,在现代浏览器有一个很好的支持。这个 HTTP 响应头 就是为了防御用 iframe 嵌套的点击劫持攻击。

该响应头有三个值可选,分别是

  • DENY,表示页面不允许通过 iframe 的方式展示
  • SAMEORIGIN,表示页面可以在相同域名下通过 iframe 的方式展示
  • ALLOW-FROM,表示页面可以在指定来源的 iframe 中展示

2)JavaScript 防御

对于某些远古浏览器来说,并不能支持上面的这种方式,那我们只有通过 JS 的方式来防御点击劫持了。

<head>
  <style id="click-jack">
    html {
      display: none !important;
    }
  </style>
</head>
<body>
  <script>
    if (self == top) {
      var style = document.getElementById('click-jack')
      document.body.removeChild(style)
    } else {
      top.location = self.location
    }
  </script>
</body>

以上代码的作用就是当通过 iframe 的方式加载页面时,攻击者的网页直接不显示所有内容了。

四、URL跳转漏洞

定义:借助未验证的URL跳转,将应用程序引导到不安全的第三方区域,从而导致的安全问题。

1.URL跳转漏洞原理

黑客利用URL跳转漏洞来诱导安全意识低的用户点击,导致用户信息泄露或者资金的流失。其原理是黑客构建恶意链接(链接需要进行伪装,尽可能迷惑),发在QQ群或者是浏览量多的贴吧/论坛中。
安全意识低的用户点击后,经过服务器或者浏览器解析后,跳到恶意的网站中。

恶意链接需要进行伪装,经常的做法是熟悉的链接后面加上一个恶意的网址,这样才迷惑用户。

诸如伪装成像如下的网址,你是否能够识别出来是恶意网址呢?

http://gate.baidu.com/index?act=go&url=http://t.cn/RVTatrd
http://qt.qq.com/safecheck.html?flag=1&url=http://t.cn/RVTatrd
http://tieba.baidu.com/f/user/passport?jumpUrl=http://t.cn/RVTatrd

2.实现方式:

  • Header头跳转
  • Javascript跳转
  • META标签跳转

这里我们举个Header头跳转实现方式:

<?php
$url=$_GET['jumpto'];
header("Location: $url");
?>
http://www.wooyun.org/login.php?jumpto=http://www.evil.com

这里用户会认为www.wooyun.org都是可信的,但是点击上述链接将导致用户最终访问www.evil.com这个恶意网址。

3.如何防御

1)referer的限制

如果确定传递URL参数进入的来源,我们可以通过该方式实现安全限制,保证该URL的有效性,避免恶意用户自己生成跳转链接

2)加入有效性验证Token

我们保证所有生成的链接都是来自于我们可信域的,通过在生成的链接里加入用户不可控的Token对生成的链接进行校验,可以避免用户生成自己的恶意链接从而被利用,但是如果功能本身要求比较开放,可能导致有一定的限制。

五、SQL注入

SQL注入是一种常见的Web安全漏洞,攻击者利用这个漏洞,可以访问或修改数据,或者利用潜在的数据库漏洞进行攻击。

1.SQL注入的原理

我们先举一个万能钥匙的例子来说明其原理:

<form action="/login" method="POST">
    <p>Username: <input type="text" name="username" /></p>
    <p>Password: <input type="password" name="password" /></p>
    <p><input type="submit" value="登陆" /></p>
</form>

后端的 SQL 语句可能是如下这样的:

let querySQL = `
    SELECT *
    FROM user
    WHERE username='${username}'
    AND psw='${password}'
`;
// 接下来就是执行 sql 语句...

这是我们经常见到的登录页面,但如果有一个恶意攻击者输入的用户名是 admin' --,密码随意输入,就可以直接登入系统了。why! ----这就是SQL注入

我们之前预想的SQL 语句是:

SELECT * FROM user WHERE username='admin' AND psw='password'

但是恶意攻击者用奇怪用户名将你的 SQL 语句变成了如下形式:

SELECT * FROM user WHERE username='admin' --' AND psw='xxxx'

在 SQL 中,' --是闭合和注释的意思,-- 是注释后面的内容的意思,所以查询语句就变成了:

SELECT * FROM user WHERE username='admin'

所谓的万能密码,本质上就是SQL注入的一种利用方式。

一次SQL注入的过程包括以下几个过程:

  • 获取用户请求参数
  • 拼接到代码当中
  • SQL语句按照我们构造参数的语义执行成功

SQL注入的必备条件:
1.可以控制输入的数据
2.服务器要执行的代码拼接了控制的数据


我们会发现SQL注入流程中与正常请求服务器类似,只是黑客控制了数据,构造了SQL查询,而正常的请求不会SQL查询这一步,SQL注入的本质:数据和代码未分离,即数据当做了代码来执行。

2.危害

  • 获取数据库信息
    • 管理员后台用户名和密码
    • 获取其他数据库敏感信息:用户名、密码、手机号码、身份证、银行卡信息……
    • 整个数据库:脱裤
  • 获取服务器权限
  • 植入Webshell,获取服务器后门
  • 读取服务器敏感文件

3.如何防御

  • 严格限制Web应用的数据库的操作权限,给此用户提供仅仅能够满足其工作的最低权限,从而最大限度的减少注入攻击对数据库的危害

  • 后端代码检查输入的数据是否符合预期,严格限制变量的类型,例如使用正则表达式进行一些匹配处理。

  • 对进入数据库的特殊字符(',",\,<,>,&,*,; 等)进行转义处理,或编码转换。基本上所有的后端语言都有对字符串进行转义处理的方法,比如 lodash 的 lodash._escapehtmlchar 库。

  • 所有的查询语句建议使用数据库提供的参数化查询接口,参数化的语句使用参数而不是将用户输入变量嵌入到 SQL 语句中,即不要直接拼接 SQL 语句。例如 Node.js 中的 mysqljs 库的 query 方法中的 ? 占位参数。

六、OS命令注入攻击

OS命令注入和SQL注入差不多,只不过SQL注入是针对数据库的,而OS命令注入是针对操作系统的。OS命令注入攻击指通过Web应用,执行非法的操作系统命令达到攻击的目的。只要在能调用Shell函数的地方就有存在被攻击的风险。倘若调用Shell时存在疏漏,就可以执行插入的非法命令。

命令注入攻击可以向Shell发送命令,让Windows或Linux操作系统的命令行启动程序。也就是说,通过命令注入攻击可执行操作系统上安装着的各种程序。

1.原理


黑客构造命令提交给web应用程序,web应用程序提取黑客构造的命令,拼接到被执行的命令中,因黑客注入的命令打破了原有命令结构,导致web应用执行了额外的命令,最后web应用程序将执行的结果输出到响应页面中。

我们通过一个例子来说明其原理,假如需要实现一个需求:用户提交一些内容到服务器,然后在服务器执行一些系统命令去返回一个结果给用户

// 以 Node.js 为例,假如在接口中需要从 github 下载用户指定的 repo
const exec = require('mz/child_process').exec;
let params = {/* 用户输入的参数 */};
exec(`git clone ${params.repo} /some/path`);

如果 params.repo 传入的是 https://github.com/admin/admin.github.io.git 确实能从指定的 git repo 上下载到想要的代码。
但是如果 params.repo 传入的是 https://github.com/xx/xx.git && rm -rf /* && 恰好你的服务是用 root 权限起的就糟糕了。

2.如何防御

  • 后端对前端提交内容进行规则限制(比如正则表达式)。
  • 在调用系统命令前对所有传入参数进行命令行参数转义过滤。
  • 不要直接拼接命令语句,借助一些工具做拼接、转义预处理,例如 Node.js 的 shell-escape npm

参考资料

DOM事件机制

前言

本文主要介绍DOM事件级别、DOM事件模型、事件流、事件代理和Event对象常见的应用,希望对你们有些帮助和启发!

一、DOM事件级别

DOM级别一共可以分为四个级别:DOM0级、DOM1级、DOM2级和DOM3级。而DOM事件分为3个级别:DOM 0级事件处理,DOM 2级事件处理和DOM 3级事件处理。由于DOM 1级中没有事件的相关内容,所以没有DOM 1级事件。

1.DOM 0级事件

el.onclick=function(){}

// 例1
var btn = document.getElementById('btn');
 btn.onclick = function(){
     alert(this.innerHTML);
 }

当希望为同一个元素/标签绑定多个同类型事件的时候(如给上面的这个btn元素绑定3个点击事件),是不被允许的。DOM0事件绑定,给元素的事件行为绑定方法,这些方法都是在当前元素事件行为的冒泡阶段(或者目标阶段)执行的

2.DOM 2级事件

el.addEventListener(event-name, callback, useCapture)

  • event-name: 事件名称,可以是标准的DOM事件
  • callback: 回调函数,当事件触发时,函数会被注入一个参数为当前的事件对象 event
  • useCapture: 默认是false,代表事件句柄在冒泡阶段执行
// 例2
var btn = document.getElementById('btn');
btn.addEventListener("click", test, false);
function test(e){
	e = e || window.event;
    alert((e.target || e.srcElement).innerHTML);
    btn.removeEventListener("click", test)
}
//IE9-:attachEvent()与detachEvent()。
//IE9+/chrom/FF:addEventListener()和removeEventListener()

IE9以下的IE浏览器不支持 addEventListener()和removeEventListener(),使用 attachEvent()与detachEvent() 代替,因为IE9以下是不支持事件捕获的,所以也没有第三个参数,第一个事件名称前要加on。

3.DOM 3级事件

在DOM 2级事件的基础上添加了更多的事件类型。

  • UI事件,当用户与页面上的元素交互时触发,如:load、scroll
  • 焦点事件,当元素获得或失去焦点时触发,如:blur、focus
  • 鼠标事件,当用户通过鼠标在页面执行操作时触发如:dblclick、mouseup
  • 滚轮事件,当使用鼠标滚轮或类似设备时触发,如:mousewheel
  • 文本事件,当在文档中输入文本时触发,如:textInput
  • 键盘事件,当用户通过键盘在页面上执行操作时触发,如:keydown、keypress
  • 合成事件,当为IME(输入法编辑器)输入字符时触发,如:compositionstart
  • 变动事件,当底层DOM结构发生变化时触发,如:DOMsubtreeModified
  • 同时DOM3级事件也允许使用者自定义一些事件。

二、DOM事件模型和事件流

DOM事件模型分为捕获和冒泡。一个事件发生后,会在子元素和父元素之间传播(propagation)。这种传播分成三个阶段。

(1)捕获阶段:事件从window对象自上而下向目标节点传播的阶段;

(2)目标阶段:真正的目标节点正在处理事件的阶段;

(3)冒泡阶段:事件从目标节点自下而上向window对象传播的阶段。

DOM事件捕获的具体流程

捕获是从上到下,事件先从window对象,然后再到document(对象),然后是html标签(通过document.documentElement获取html标签),然后是body标签(通过document.body获取body标签),然后按照普通的html结构一层一层往下传,最后到达目标元素。

而事件冒泡的流程刚好是事件捕获的逆过程。
接下来我们看个事件冒泡的例子:

// 例3
<div id="outer">
    <div id="inner"></div>
</div>
......
window.onclick = function() {
    console.log('window');
};
document.onclick = function() {
    console.log('document');
};
document.documentElement.onclick = function() {
    console.log('html');
};
document.body.onclick = function() {
    console.log('body');
}
outer.onclick = function(ev) {
    console.log('outer');
};
inner.onclick = function(ev) {
    console.log('inner');
};

正如我们上面提到的onclick给元素的事件行为绑定方法都是在当前元素事件行为的冒泡阶段(或者目标阶段)执行的。

三、事件代理(事件委托)

由于事件会在冒泡阶段向上传播到父节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件。这种方法叫做事件的代理(delegation)。

1.优点

  • 减少内存消耗,提高性能

假设有一个列表,列表之中有大量的列表项,我们需要在点击每个列表项的时候响应一个事件

// 例4
<ul id="list">
  <li>item 1</li>
  <li>item 2</li>
  <li>item 3</li>
  ......
  <li>item n</li>
</ul>

如果给每个列表项一一都绑定一个函数,那对于内存消耗是非常大的,效率上需要消耗很多性能。借助事件代理,我们只需要给父容器ul绑定方法即可,这样不管点击的是哪一个后代元素,都会根据冒泡传播的传递机制,把容器的click行为触发,然后把对应的方法执行,根据事件源,我们可以知道点击的是谁,从而完成不同的事。

  • 动态绑定事件

在很多时候,我们需要通过用户操作动态的增删列表项元素,如果一开始给每个子元素绑定事件,那么在列表发生变化时,就需要重新给新增的元素绑定事件,给即将删去的元素解绑事件,如果用事件代理就会省去很多这样麻烦。

2.如何实现

接下来我们来实现上例中父层元素 #list 下的 li 元素的事件委托到它的父层元素上:

// 给父层元素绑定事件
document.getElementById('list').addEventListener('click', function (e) {
  // 兼容性处理
  var event = e || window.event;
  var target = event.target || event.srcElement;
  // 判断是否匹配目标元素
  if (target.nodeName.toLocaleLowerCase === 'li') {
    console.log('the content is: ', target.innerHTML);
  }
});

四、Event对象常见的应用

  • event. preventDefault()

如果调用这个方法,默认事件行为将不再触发。什么是默认事件呢?例如表单一点击提交按钮(submit)跳转页面、a标签默认页面跳转或是锚点定位等。

很多时候我们使用a标签仅仅是想当做一个普通的按钮,点击实现一个功能,不想页面跳转,也不想锚点定位。

//方法一:
<a href="javascript:;">链接</a>

也可以通过JS方法来阻止,给其click事件绑定方法,当我们点击A标签的时候,先触发click事件,其次才会执行自己的默认行为

//方法二:
<a id="test" href="http://www.cnblogs.com">链接</a>
<script>
test.onclick = function(e){
    e = e || window.event;
    return false;
}
</script>
//方法三:
<a id="test" href="http://www.cnblogs.com">链接</a>
<script>
test.onclick = function(e){
    e = e || window.event;
    e.preventDefault();
}
</script>

接下来我们看个例子:输入框最多只能输入六个字符,如何实现?

// 例5
 <input type="text" id='tempInp'>
 <script>
    tempInp.onkeydown = function(ev) {
        ev = ev || window.event;
        let val = this.value.trim() //trim去除字符串首位空格(不兼容)
        // this.value=this.value.replace(/^ +| +$/g,'') 兼容写法
        let len = val.length
        if (len >= 6) {
            this.value = val.substr(0, 6);
            //阻止默认行为去除特殊按键(DELETE\BACK-SPACE\方向键...)
            let code = ev.which || ev.keyCode;
            if (!/^(46|8|37|38|39|40)$/.test(code)) {
                ev.preventDefault()
            }
        }
    }
 </script>
  • event.stopPropagation() & event.stopImmediatePropagation()

event.stopPropagation() 方法阻止事件冒泡到父元素,阻止任何父事件处理程序被执行。上面提到事件冒泡阶段是指事件从目标节点自下而上向window对象传播的阶段。
我们在例3的inner元素click事件上,添加event.stopPropagation()这句话后,就阻止了父事件的执行,最后只打印了'inner'。

 inner.onclick = function(ev) {
    console.log('inner');
    ev.stopPropagation();
};

stopImmediatePropagation 既能阻止事件向父元素冒泡,也能阻止元素同事件类型的其它监听器被触发。而 stopPropagation 只能实现前者的效果。我们来看个例子:

<body>
  <button id="btn">click me to stop propagation</button>
</body>
......
const btn = document.querySelector('#btn');
btn.addEventListener('click', event => {
  console.log('btn click 1');
  event.stopImmediatePropagation();
});
btn.addEventListener('click', event => {
  console.log('btn click 2');
});
document.body.addEventListener('click', () => {
  console.log('body click');
});
// btn click 1

如上所示,使用 stopImmediatePropagation后,点击按钮时,不仅body绑定事件不会触发,与此同时按钮的另一个点击事件也不触发。

  • event.target & event.currentTarget

老实说这两者的区别,并不好用文字描述,我们先来看个例子:

<div id="a">
  <div id="b">
    <div id="c"><div id="d"></div></div>
  </div>
</div>
<script>
  document.getElementById('a').addEventListener('click', function(e) {
    console.log(
      'target:' + e.target.id + '&currentTarget:' + e.currentTarget.id
    )
  })
  document.getElementById('b').addEventListener('click', function(e) {
    console.log(
      'target:' + e.target.id + '&currentTarget:' + e.currentTarget.id
    )
  })
  document.getElementById('c').addEventListener('click', function(e) {
    console.log(
      'target:' + e.target.id + '&currentTarget:' + e.currentTarget.id
    )
  })
  document.getElementById('d').addEventListener('click', function(e) {
    console.log(
      'target:' + e.target.id + '&currentTarget:' + e.currentTarget.id
    )
  })
</script>

当我们点击最里层的元素d的时候,会依次输出:

target:d&currentTarget:d
target:d&currentTarget:c
target:d&currentTarget:b
target:d&currentTarget:a

从输出中我们可以看到,event.target指向引起触发事件的元素,而event.currentTarget则是事件绑定的元素,只有被点击的那个目标元素的event.target才会等于event.currentTarget也就是说,event.currentTarget始终是监听事件者,而event.target是事件的真正发出者

五、参考文章

LESS即学即用

前言

我们大家都知道HTML和CSS不属于编程语言而是属于标记语言,所以很难像JS一样定义变量、编写方法、实现模块化开发等。而目前的CSS编写模式中,都是定义一些公共的样式类名,哪一块的HTML需要这个样式,就去增加对应的样式类名,所以我们经常看到一个标签上存在很多样式类名,在这种模式中我们要时常关注CSS的优先级,避免样式的重叠覆盖...

为了解决CSS的这一困境,CSS预处理预编译的**脱颖而出,比较具有代表性的有LESS、SASS、Stylus。它们在传统的CSS基础上增加了大量的新的语法,编写方式,常用的函数等,可以让我们的CSS像JS一样成为一门编程语言。本文主要介绍LESS的语法和使用。

一、LESS的编译

编写完成的LESS代码是不能直接在浏览器中运行的,需要编译成正常的CSS代码。那么我们首先就来学习一下常用的LESS编译方式。

1.在浏览器中调用LESS.JS

LESS只支持在现代浏览器中运行(最新版本的Chrome, Firefox, Safari 和 IE)。我们不建议在生产环境中使用LESS客户端,因为在将LESS编译成CSS的时候,用户会看到加载延迟的现象,即便在浏览器中有不足一秒的加载延迟,但也会降低性能。

首先引入我们设置样式的LESS文件,注意:这里的rel='stylesheet/less'

<link type="text/css" rel="stylesheet/less" href="1.less"/>

然后引入less.js文件

<script src="//cdnjs.cloudflare.com/ajax/libs/less.js/3.8.1/less.min.js" ></script>

浏览器中调用有个好处:可以开启监视模式,只要我们的LESS改变,在一定时间内,浏览器就会重新的编译,我们可以看到想要的效果。具体操作如下:

    <script charset="utf-8" type="text/javascript">
    //->在引入LESS之前设置一个全局的变量less,配置一些参数值(根据情况自行选择需要配置的项)
      var less = {
        //->env:设置运行的环境(生产模式还是开发模式)
        //production:编译后的CSS缓存到本地localStorage中
        //development:没有把编译后的CSS缓存到本地,在URL不是标准的格式下(例如:file://...),自动设置为development
        env: "development",
        //->poll:在监视模式下,每两次请求之间的时间间隔(ms)
        poll:500
        }
    </script>     
    <script src="//cdnjs.cloudflare.com/ajax/libs/less.js/3.8.1/less.min.js" ></script>
    <script charset="utf-8" type="text/javascript">
      //->启用监视模式(env必须要设置成development)
      less.watch();
    </script>   


需要特别注意的是,由于浏览器端使用时是使用ajax来拉取.less文件,因此直接在本机文件系统打开(即地址是file://开头)或者是有跨域的情况下会拉取不到.less文件,导致样式无法生效。所以一般less做css预处理,很少采用引用less的方式,一般是把less编译成css之后,引用生成的css文件。

// index.html文件
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>less</title>
    <link type="text/css" rel="stylesheet/less" href="css/styles.less">
</head>
<body>
    <header>
        <p>Hello World!</p>
    </header>
<script src="./less.min.js" type="text/javascript"></script>   
</body>
</html>
// styles.less文件
@blue:#5B83AD;
@size:50px;
header {
    color: @blue;
    font-size: @size;
}

如果直接引入less文件,便会出现如下错误:


可以尝试用一个简单的web server启动网站就能解决,譬如lite-server

npm install -g lite-server
lite-server //to run

2、使用NODE命令编译LESS(单个文件)

这种方式是目前项目中最常用的方式,适用于项目的生产环境,它是把我们的LESS文件编译成CSS文件,我们项目中直接的引入CSS文件即可,基本步骤:安装->编译/压缩编译->或者使用NODE代码实现批量编译等

把LESS模块安装到全局NODE环境中

npm install less -g

使用命令进行编译

    //->把styles.less文件编译成styles.css文件(如果没有这个CSS文件自己会创建)
    lessc styles.less styles.css

    //->编译完成的CSS文件是经过压缩的
    lessc styles.less styles.min.css -x或者--compress

经过node命令编译代码前后对比:

// styles.less文件(编译前)
@blue:#5B83AD;
@size:50px;
header {
    color: @blue;
    font-size: @size;
}
// styles.css文件(编译后)
header {
  color: #5B83AD;
  font-size: 50px;
}

3、在NODE环境中编写批量编译的代码

我们在上述用NODE命令编译的时候,一次只能编译一个文件,这样,如果页面中有多个LESS,每一次编译都是比较耗费时间的,所以我们结合NODE的FS文件读写操作,可以写一套批量编译的代码。

    //->定义编译文件目录和目标导出目录
    var dirPath = "./less/", tarPath = "./css/";
    //->导入NODE中需要使用的模块
    var fs = require("fs"),
        less = require("less");
    //->读取dirPath中的所有文件,检查文件的类型,只有LESS文件我们才进行存储
    var ary = [],
        files = fs.readdirSync(dirPath);
    files.forEach(function (file, index) {
        /\.(LESS)/i.test(file) ? ary.push(file) : null;
    });
    //->把目录下的所有文件进行编译,把编译完成的结果保存在指定的目录中
    ary.forEach(function (file) {
        var newFile = file.replace(".less", ".css"),
            conFile = fs.readFileSync(dirPath + file, "utf-8");
        less.render(conFile, {compress: true}, function (error, output) {
            fs.writeFileSync(tarPath + newFile, output.css, "utf-8");
        });
    });

4、使用工具编译LESS

目前常用的编译工具有:Koala(据说目前最流行的)、在线编译(http://tool.oschina.net/less)、SimpLESS等。本篇文章不做过多的说明。

二、LESS的语法

LESS的基础语法基本上分为以下几个方面:变量、混合(Mixins)、嵌套规则、运算、函数、作用域等。

1.变量

和JS中的变量一样,只是LESS的变量定义不是使用VAR而是使用@

  //->LESS代码
    @link-color: #428bca;
    @link-color-hover: darken(@link-color, 10%);
    a {
        color: @link-color;
        &:hover {
            color: @link-color-hover;
        }
    }
  //->编译为CSS的结果
    a {
        color: #428bca;
    }
    a:hover {
        color: #3071a9;
    }

除了上述用变量存储公用的属性值,我们还可以用变量存储公用的URL、选择器等等

    //->LESS代码
    .@{selector} {
        width: 100px;
        height: 100px;
        @{property}: #000;
        background: url("@{bgImg}/test.png");
    }
    @selector: box;
    @bgImg: "../img";
    @property: color;

2.混合(Mixins)

① 基本使用

混合可以将一个定义好的class A轻松的引入到另一个class B中,从而简单实现class B继承class A中的所有属性。我们还可以带参数地调用,就像使用函数一样。

    //->LESS代码
    .public {
        width: 100px;
        height: 100px;
    }
    nav ul {
        .public;
        list-style: none;
    }
    //->编译为CSS的结果
    .public {
        width: 100px;
        height: 100px;
    }
    nav ul {
        width: 100px;
        height: 100px;
        list-style: none;
    }

上述的代码,nav ul是把public中设定的样式属性值copy了一份到自己的样式中。如果你想在编译完成的结果中不输出public这个样式的结果,只需要按照下述的代码编写即可:

    //->LESS代码
    .public() {//->在选择器后面加上()就可以不编译这个样式了
        width: 100px;
        height: 100px;
    }
    nav ul {
        .public;//如果public有子孙元素的样式,同样也会被复制过来
        list-style: none;
    }
    //->编译为CSS的结果
    nav ul {
        width: 100px;
        height: 100px;
        list-style: none;
    }

② extend

虽然在上述的案例中,nav ul把public中的样式继承了过来,但是原理却是把代码copy一份过来,这样编译后的CSS中依然会存留大量的冗余CSS代码,为了避免这一点,我们可以使用extend伪类来实现样式的继承使用。和原来的选择器共用一套样式,但要保证原来的选择器不加括号。

    //->LESS代码
    .public {
        width: 100px;
        height: 100px;
    }
    nav ul {
        &:extend(.public);
        list-style: none;
    }
    //->编译为CSS的结果
    .public, nav ul {
        width: 100px;
        height: 100px;
    }
    nav ul {
        list-style: none;
    }

3.嵌套规则

我们可以在一个选择器中嵌套另一个选择器来实现继承,这样很大程度减少了代码量,并且代码看起来更加的清晰。

//->LESS代码
#header {
  color: black;
  .navigation {
    font-size: 12px;
  }
  .logo {
    width: 300px;
    &:hover { text-decoration: none }
  }
}
//->编译为CSS的结果
#header { color: black; }
#header .navigation {
  font-size: 12px;
}
#header .logo { 
  width: 300px; 
}
#header .logo:hover {
  text-decoration: none;
}

4.函数 & 运算

运算提供了加,减,乘,除操作;我们可以做属性值和颜色的运算,这样就可以实现属性值之间的复杂关系。LESS中的函数一一映射了JavaScript代码,如果你愿意的话可以操作属性值。

任何数字、颜色或者变量都可以参与运算. 来看一组例子:

@base: 5%;
@filler: @base * 2;
@other: @base + @filler;
color: #888 / 4;
background-color: @base-color + #111;
height: 100% / 2 + @filler;

LESS 提供了一系列的颜色运算函数. 颜色会先被转化成 HSL 色彩空间, 然后在通道级别操作:

lighten(@color, 10%);     // return a color which is 10% *lighter* than @color
darken(@color, 10%);      // return a color which is 10% *darker* than @color

LESS提供了一组方便的数学函数,你可以使用它们处理一些数字类型的值:

round(1.67); // returns `2`
ceil(2.4);   // returns `3`
floor(2.6);  // returns `2`

5.命名空间和作用域

LESS 中的作用域跟其他编程语言非常类似,首先会从本地查找变量或者混合模块,如果没找到的话会去父级作用域中查找,直到找到为止。

@var: red;
#page {
  @var: white;
  #header {
    color: @var; // white
  }
}
#footer {
  color: @var; // red  
}

如果觉得文章对你有些许帮助,欢迎在我的GitHub博客点赞和关注,感激不尽!

参考文章

LESS学习:了解LESS和编译LESS

LESS学习:LESS的基础语法

LESS一种动态样式语言

Ajax原理一篇就够了

前言

AJAX即“Asynchronous Javascript And XML”,是指一种创建交互式网页应用的网页开发技术。AJAX 是一种用于创建快速动态网页的技术。它可以令开发者只向服务器获取数据(而不是图片,HTML文档等资源),互联网资源的传输变得前所未有的轻量级和纯粹,这激发了广大开发者的创造力,使各式各样功能强大的网络站点,和互联网应用如雨后春笋一般冒出,不断带给人惊喜。

一、什么是Ajax

Ajax是一种异步请求数据的web开发技术,对于改善用户的体验和页面性能很有帮助。简单地说,在不需要重新刷新页面的情况下,Ajax 通过异步请求加载后台数据,并在网页上呈现出来。常见运用场景有表单验证是否登入成功、百度搜索下拉框提示和快递单号查询等等。Ajax的目的是提高用户体验,较少网络数据的传输量。同时,由于AJAX请求获取的是数据而不是HTML文档,因此它也节省了网络带宽,让互联网用户的网络冲浪体验变得更加顺畅。

二、Ajax原理是什么

在解释Ajax原理之前,我们不妨先举个“领导想找小李汇报一下工作”例子,领导想找小李问点事,就委托秘书去叫小李,自己就接着做其他事情,直到秘书告诉他小李已经到了,最后小李跟领导汇报工作。

Ajax请求数据流程与“领导想找小李汇报一下工作”类似。其中最核心的依赖是浏览器提供的XMLHttpRequest对象,它扮演的角色相当于秘书,使得浏览器可以发出HTTP请求与接收HTTP响应。浏览器接着做其他事情,等收到XHR返回来的数据再渲染页面。

理解了Ajax的工作原理后,接下来我们探讨下如何使用Ajax。

三、Ajax的使用

1.创建Ajax核心对象XMLHttpRequest(记得考虑兼容性)

	1. var xhr=null;  
	2. if (window.XMLHttpRequest)  
	3.   {// 兼容 IE7+, Firefox, Chrome, Opera, Safari  
	4.   xhr=new XMLHttpRequest();  
	5.   } else{// 兼容 IE6, IE5 
	6.     xhr=new ActiveXObject("Microsoft.XMLHTTP");  
	7.   } 

2.向服务器发送请求

    1. xhr.open(method,url,async);  
    2. send(string);//post请求时才使用字符串参数,否则不用带参数。
  • method:请求的类型;GET 或 POST

  • url:文件在服务器上的位置

  • async:true(异步)或 false(同步)
    注意:post请求一定要设置请求头的格式内容

xhr.open("POST","test.html",true);  
xhr.setRequestHeader("Content-type","application/x-www-form-urlencoded");  
xhr.send("fname=Henry&lname=Ford");  //post请求参数放在send里面,即请求体

3.服务器响应处理(区分同步跟异步两种情况)

responseText 获得字符串形式的响应数据。

responseXML 获得XML 形式的响应数据。

①同步处理

	1. xhr.open("GET","info.txt",false);  
	2. xhr.send();  
	3. document.getElementById("myDiv").innerHTML=xhr.responseText; //获取数据直接显示在页面上

②异步处理

相对来说比较复杂,要在请求状态改变事件中处理。

	1. xhr.onreadystatechange=function()  { 
	2.    if (xhr.readyState==4 &&xhr.status==200)  { 
	3.       document.getElementById("myDiv").innerHTML=xhr.responseText;  
	4.      }
	5.    } 

什么是readyState?

readyState是XMLHttpRequest对象的一个属性,用来标识当前XMLHttpRequest对象处于什么状态。
readyState总共有5个状态值,分别为0~4,每个值代表了不同的含义

  • 0:未初始化 -- 尚未调用.open()方法;
  • 1:启动 -- 已经调用.open()方法,但尚未调用.send()方法;
  • 2:发送 -- 已经调用.send()方法,但尚未接收到响应;
  • 3:接收 -- 已经接收到部分响应数据;
  • 4:完成 -- 已经接收到全部响应数据,而且已经可以在客户端使用了;

什么是status?

HTTP状态码(status)由三个十进制数字组成,第一个十进制数字定义了状态码的类型,后两个数字没有分类的作用。HTTP状态码共分为5种类型:

常见的状态码

仅记录在 RFC2616 上的 HTTP 状态码就达 40 种,若再加上 WebDAV(RFC4918、5842)和附加 HTTP 状态码 (RFC6585)等扩展,数量就达 60 余种。接下来,我们就介绍一下这些具有代表性的一些状态码。

  • 200 表示从客户端发来的请求在服务器端被正常处理了。
  • 204 表示请求处理成功,但没有资源返回。
  • 301 表示永久性重定向。该状态码表示请求的资源已被分配了新的URI,以后应使用资源现在所指的URI。
  • 302 表示临时性重定向。
  • 304 表示客户端发送附带条件的请求时(指采用GET方法的请求报文中包含if-matched,if-modified-since,if-none-match,if-range,if-unmodified-since任一个首部)服务器端允许请求访问资源,但因发生请求未满足条件的情况后,直接返回304Modified(服务器端资源未改变,可直接使用客户端未过期的缓存)
  • 400 表示请求报文中存在语法错误。当错误发生时,需修改请求的内容后再次发送请求。
  • 401 表示未授权(Unauthorized),当前请求需要用户验证
  • 403 表示对请求资源的访问被服务器拒绝了
  • 404 表示服务器上无法找到请求的资源。除此之外,也可以在服务器端拒绝请求且不想说明理由时使用。
  • 500 表示服务器端在执行请求时发生了错误。也有可能是Web应用存在的bug或某些临时的故障。
  • 503 表示服务器暂时处于超负载或正在进行停机维护,现在无法处理请求。

③GET和POST请求数据区别

  • 使用Get请求时,参数在URL中显示,而使用Post方式,则放在send里面
  • 使用Get请求发送数据量小,Post请求发送数据量大
  • 使用Get请求安全性低,会被缓存,而Post请求反之
    关于第一点区别,详情见下面两张图:

四、结束语

其实通过 XMLHttpRequest或者封装后的框架进行网络请求,这种方式已经有点老旧了,配置和调用方式非常混乱,近几年刚刚出来的Fetch提供了一个更好的替代方法,它不仅提供了一种简单,合乎逻辑的方式来跨网络异步获取资源,而且可以很容易地被其他技术使用。

想了解如何利用Fetch请求数据,请猛戳
fetch 如何请求常见数据格式

想了解Ajax如何请求后台数据,请猛戳
Ajax请求后台数据

参考文章

异步解决方案----Promise与Await

前言

异步编程模式在前端开发过程中,显得越来越重要。从最开始的XHR到封装后的Ajax都在试图解决异步编程过程中的问题。随着ES6新标准的到来,处理异步数据流又有了新的方案。我们都知道,在传统的ajax请求中,当异步请求之间的数据存在依赖关系的时候,就可能产生很难看的多层回调,俗称'回调地狱'(callback hell),这却让人望而生畏,Promise的出现让我们告别回调函数,写出更优雅的异步代码。在实践过程中,却发现Promise并不完美,Async/Await是近年来JavaScript添加的最革命性的的特性之一,Async/Await提供了一种使得异步代码看起来像同步代码的替代方法。接下来我们介绍这两种处理异步编程的方案。

一、Promise的原理与基本语法

1.Promise的原理

Promise 是一种对异步操作的封装,可以通过独立的接口添加在异步操作执行成功、失败时执行的方法。主流的规范是 Promises/A+。

Promise中有几个状态:

  • pending: 初始状态, 非 fulfilled 或 rejected;
  • fulfilled: 成功的操作,为表述方便,fulfilled 使用 resolved 代替;
  • rejected: 失败的操作。


pending可以转化为fulfilled或rejected并且只能转化一次,也就是说如果pending转化到fulfilled状态,那么就不能再转化到rejected。并且fulfilled和rejected状态只能由pending转化而来,两者之间不能互相转换。

2.Promise的基本语法

  • Promise实例必须实现then这个方法
  • then()必须可以接收两个函数作为参数
  • then()返回的必须是一个Promise实例
<script src="https://cdn.bootcss.com/bluebird/3.5.1/bluebird.min.js"></script>//如果低版本浏览器不支持Promise,通过cdn这种方式
<script>
function loadImg(src) {
  var promise = new Promise(function(resolve, reject) {
    var img = document.createElement('img')
    img.onload = function() {
      resolve(img)
    }
    img.onerror = function() {
      reject('图片加载失败')
    }
    img.src = src
  })
  return promise
}
var src = 'https://www.imooc.com/static/img/index/logo_new.png'
var result = loadImg(src)
result.then(
    function(img) {
      console.log(1, img.width)
      return img
    },
    function() {
      console.log('error 1')
    }
  ).then(function(img) {
    console.log(2, img.height)
  })
</script>

二、Promise多个串联操作

Promise还可以做更多的事情,比如,有若干个异步任务,需要先做任务1,如果成功后再做任务2,任何任务失败则不再继续并执行错误处理函数。要串行执行这样的异步任务,不用Promise需要写一层一层的嵌套代码。

有了Promise,我们只需要简单地写job1.then(job2).then(job3).catch(handleError);
其中job1、job2和job3都是Promise对象。

比如我们想实现第一个图片加载完成后,再加载第二个图片,如果其中有一个执行失败,就执行错误函数:

var src1 = 'https://www.imooc.com/static/img/index/logo_new.png'
var result1 = loadImg(src1) //result1是Promise对象
var src2 = 'https://img1.mukewang.com/545862fe00017c2602200220-100-100.jpg'
var result2 = loadImg(src2) //result2是Promise对象
result1.then(function (img1) {
  console.log('第一个图片加载完成', img1.width)
  return result2  // 链式操作
}).then(function (img2) {
  console.log('第二个图片加载完成', img2.width)
}).catch(function (ex) {
  console.log(ex)
})     

这里需注意的是:then 方法可以被同一个 promise 调用多次,then 方法必须返回一个 promise 对象。上例中result1.then如果没有明文返回Promise实例,就默认为本身Promise实例即result1,result1.then返回了result2实例,后面再执行.then实际上执行的是result2.then

三、Promise常用方法

除了串行执行若干异步任务外,Promise还可以并行执行异步任务

试想一个页面聊天系统,我们需要从两个不同的URL分别获得用户的个人信息和好友列表,这两个任务是可以并行执行的,用Promise.all()实现如下:

var p1 = new Promise(function (resolve, reject) {
    setTimeout(resolve, 500, 'P1');
});
var p2 = new Promise(function (resolve, reject) {
    setTimeout(resolve, 600, 'P2');
});
// 同时执行p1和p2,并在它们都完成后执行then:
Promise.all([p1, p2]).then(function (results) {
    console.log(results); // 获得一个Array: ['P1', 'P2']
});

有些时候,多个异步任务是为了容错。比如,同时向两个URL读取用户的个人信息,只需要获得先返回的结果即可。这种情况下,用Promise.race()实现:

var p1 = new Promise(function (resolve, reject) {
    setTimeout(resolve, 500, 'P1');
});
var p2 = new Promise(function (resolve, reject) {
    setTimeout(resolve, 600, 'P2');
});
Promise.race([p1, p2]).then(function (result) {
    console.log(result); // 'P1'
});

由于p1执行较快,Promise的then()将获得结果'P1'。p2仍在继续执行,但执行结果将被丢弃。

总结:Promise.all接受一个promise对象的数组,待全部完成之后,统一执行success;

Promise.race接受一个包含多个promise对象的数组,只要有一个完成,就执行success

接下来我们对上面的例子做下修改,加深对这两者的理解:

var src1 = 'https://www.imooc.com/static/img/index/logo_new.png'
var result1 = loadImg(src1)
var src2 = 'https://img1.mukewang.com/545862fe00017c2602200220-100-100.jpg'
var result2 = loadImg(src2)
Promise.all([result1, result2]).then(function(datas) {
  console.log('all', datas[0]) //<img src="https://www.imooc.com/static/img/index/logo_new.png">
  console.log('all', datas[1]) //<img src="https://img1.mukewang.com/545862fe00017c2602200220-100-100.jpg">
})
Promise.race([result1, result2]).then(function(data) {
  console.log('race', data) //<img src="https://img1.mukewang.com/545862fe00017c2602200220-100-100.jpg">
})  

如果我们组合使用Promise,就可以把很多异步任务以并行和串行的方式组合起来执行

四、Async/Await简介与用法

异步操作是 JavaScript 编程的麻烦事,很多人认为async函数是异步操作的终极解决方案。

1、Async/Await简介

  • async/await是写异步代码的新方式,优于回调函数和Promise。
  • async/await是基于Promise实现的,它不能用于普通的回调函数。
  • async/await与Promise一样,是非阻塞的。
  • async/await使得异步代码看起来像同步代码,再也没有回调函数。但是改变不了JS单线程、异步的本质。

2、Async/Await的用法

  • 使用await,函数必须用async标识
  • await后面跟的是一个Promise实例
  • 需要安装babel-polyfill,安装后记得引入 //npm i --save-dev babel-polyfill
function loadImg(src) {
  const promise = new Promise(function(resolve, reject) {
    const img = document.createElement('img')
    img.onload = function() {
      resolve(img)
    }
    img.onerror = function() {
      reject('图片加载失败')
    }
    img.src = src
  })
  return promise
}
const src1 = 'https://www.imooc.com/static/img/index/logo_new.png'
const src2 = 'https://img1.mukewang.com/545862fe00017c2602200220-100-100.jpg'
const load = async function() {
  const result1 = await loadImg(src1)
  console.log(result1)
  const result2 = await loadImg(src2)
  console.log(result2)
}
load()

当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再接着执行函数体内后面的语句。

五、Async/Await错误处理

await 命令后面的 Promise 对象,运行结果可能是 rejected,所以最好把 await 命令放在 try...catch 代码块中。try..catch错误处理也比较符合我们平常编写同步代码时候处理的逻辑

async function myFunction() {
  try {
    await somethingThatReturnsAPromise();
  } catch (err) {
    console.log(err);
  }
}

六、为什么Async/Await更好?

Async/Await较Promise有诸多好处,以下介绍其中三种优势:

1. 简洁

使用Async/Await明显节约了不少代码。我们不需要写.then,不需要写匿名函数处理Promise的resolve值,也不需要定义多余的data变量,还避免了嵌套代码。

2. 中间值

你很可能遇到过这样的场景,调用promise1,使用promise1返回的结果去调用promise2,然后使用两者的结果去调用promise3。你的代码很可能是这样的:

const makeRequest = () => {
  return promise1()
    .then(value1 => {
      return promise2(value1)
        .then(value2 => {        
          return promise3(value1, value2)
        })
    })
}

使用async/await的话,代码会变得异常简单和直观

const makeRequest = async () => {
  const value1 = await promise1()
  const value2 = await promise2(value1)
  return promise3(value1, value2)
}

3.条件语句

下面示例中,需要获取数据,然后根据返回数据决定是直接返回,还是继续获取更多的数据。

const makeRequest = () => {
  return getJSON()
    .then(data => {
      if (data.needsAnotherRequest) {
        return makeAnotherRequest(data)
          .then(moreData => {
            console.log(moreData)
            return moreData
          })
      } else {
        console.log(data)
        return data
      }
    })
}

代码嵌套(6层)可读性较差,它们传达的意思只是需要将最终结果传递到最外层的Promise。使用async/await编写可以大大地提高可读性:

const makeRequest = async () => {
  const data = await getJSON()
  if (data.needsAnotherRequest) {
    const moreData = await makeAnotherRequest(data);
    console.log(moreData)
    return moreData
  } else {
    console.log(data)
    return data    
  }
}

参考文章

Web 实时推送技术的总结

前言

随着 Web 的发展,用户对于 Web 的实时推送要求也越来越高 ,比如,工业运行监控、Web 在线通讯、即时报价系统、在线游戏等,都需要将后台发生的变化主动地、实时地传送到浏览器端,而不需要用户手动地刷新页面。本文对过去和现在流行的 Web 实时推送技术进行了比较与总结。

本文完整的源代码请猛戳Github博客,纸上得来终觉浅,建议大家动手敲敲代码。

一、双向通信

HTTP 协议有一个缺陷:通信只能由客户端发起。举例来说,我们想了解今天的天气,只能是客户端向服务器发出请求,服务器返回查询结果。HTTP 协议做不到服务器主动向客户端推送信息。这种单向请求的特点,注定了如果服务器有连续的状态变化,客户端要获知就非常麻烦。在WebSocket协议之前,有三种实现双向通信的方式:轮询(polling)、长轮询(long-polling)和iframe流(streaming)

1.轮询(polling)

image
轮询是客户端和服务器之间会一直进行连接,每隔一段时间就询问一次。其缺点也很明显:连接数会很多,一个接受,一个发送。而且每次发送请求都会有Http的Header,会很耗流量,也会消耗CPU的利用率

  • 优点:实现简单,无需做过多的更改
  • 缺点:轮询的间隔过长,会导致用户不能及时接收到更新的数据;轮询的间隔过短,会导致查询请求过多,增加服务器端的负担
// 1.html
<div id="clock"></div>
<script>
    let clockDiv = document.getElementById('clock');
    setInterval(function(){
        let xhr = new XMLHttpRequest;
        xhr.open('GET','/clock',true);
        xhr.onreadystatechange = function(){
            if(xhr.readyState == 4 && xhr.status == 200){
                console.log(xhr.responseText);
                clockDiv.innerHTML = xhr.responseText;
            }
        }
        xhr.send();
    },1000);
</script>
//轮询  服务端
let express = require('express');
let app = express();
app.use(express.static(__dirname));
app.get('/clock',function(req,res){
  res.end(new Date().toLocaleString());
});
app.listen(8080);

启动本地服务,打开http://localhost:8080/1.html,得到如下结果:

image

2.长轮询(long-polling)

image
长轮询是对轮询的改进版,客户端发送HTTP给服务器之后,看有没有新消息,如果没有新消息,就一直等待。当有新消息的时候,才会返回给客户端。在某种程度上减小了网络带宽和CPU利用率等问题。由于http数据包的头部数据量往往很大(通常有400多个字节),但是真正被服务器需要的数据却很少(有时只有10个字节左右),这样的数据包在网络上周期性的传输,难免对网络带宽是一种浪费

  • 优点:比 Polling 做了优化,有较好的时效性
  • 缺点:保持连接会消耗资源; 服务器没有返回有效数据,程序超时。
// 2.html  服务端代码同上
<div id="clock"></div>
<script>
let clockDiv = document.getElementById('clock')
function send() {
  let xhr = new XMLHttpRequest()
  xhr.open('GET', '/clock', true)
  xhr.timeout = 2000 // 超时时间,单位是毫秒
  xhr.onreadystatechange = function() {
    if (xhr.readyState == 4) {
      if (xhr.status == 200) {
        //如果返回成功了,则显示结果
        clockDiv.innerHTML = xhr.responseText
      }
      send() //不管成功还是失败都会发下一次请求
    }
  }
  xhr.ontimeout = function() {
    send()
  }
  xhr.send()
}
send()
</script>

3.iframe流(streaming)

image
iframe流方式是在页面中插入一个隐藏的iframe,利用其src属性在服务器和客户端之间创建一条长连接,服务器向iframe传输数据(通常是HTML,内有负责插入信息的javascript),来实时更新页面。

  • 优点:消息能够实时到达;浏览器兼容好
  • 缺点:服务器维护一个长连接会增加开销;IE、chrome、Firefox会显示加载没有完成,图标会不停旋转。
// 3.html
<body>
    <div id="clock"></div>
    <iframe src="/clock" style="display:none"></iframe>
</body>
//iframe流 
let express = require('express')
let app = express()
app.use(express.static(__dirname))
app.get('/clock', function(req, res) {
  setInterval(function() {
    let date = new Date().toLocaleString()
    res.write(`
       <script type="text/javascript">
         parent.document.getElementById('clock').innerHTML = "${date}";//改变父窗口dom元素
       </script>
     `)
  }, 1000)
})
app.listen(8080)

启动本地服务,打开http://localhost:8080/3.html,得到如下结果:
image

上述代码中,客户端只请求一次,然而服务端却是源源不断向客户端发送数据,这样服务器维护一个长连接会增加开销。

以上我们介绍了三种实时推送技术,然而各自的缺点很明显,使用起来并不理想,接下来我们着重介绍另一种技术--websocket,它是比较理想的双向通信技术。

二、WebSocket

1.什么是websocket

WebSocket是一种全新的协议,随着HTML5草案的不断完善,越来越多的现代浏览器开始全面支持WebSocket技术了,它将TCP的Socket(套接字)应用在了webpage上,从而使通信双方建立起一个保持在活动状态连接通道。

一旦Web服务器与客户端之间建立起WebSocket协议的通信连接,之后所有的通信都依靠这个专用协议进行。通信过程中可互相发送JSON、XML、HTML或图片等任意格式的数据。由于是建立在HTTP基础上的协议,因此连接的发起方仍是客户端,而一旦确立WebSocket通信连接,不论服务器还是客户端,任意一方都可直接向对方发送报文

初次接触 WebSocket 的人,都会问同样的问题:我们已经有了 HTTP 协议,为什么还需要另一个协议?

2.HTTP的局限性

  • HTTP是半双工协议,也就是说,在同一时刻数据只能单向流动,客户端向服务器发送请求(单向的),然后服务器响应请求(单向的)。
  • 服务器不能主动推送数据给浏览器。这就会导致一些高级功能难以实现,诸如聊天室场景就没法实现。

3.WebSocket的特点

  • 支持双向通信,实时性更强
  • 可以发送文本,也可以发送二进制数据
  • 减少通信量:只要建立起WebSocket连接,就希望一直保持连接状态。和HTTP相比,不但每次连接时的总开销减少,而且由于WebSocket的首部信息很小,通信量也相应减少了

image

相对于传统的HTTP每次请求-应答都需要客户端与服务端建立连接的模式,WebSocket是类似Socket的TCP长连接的通讯模式,一旦WebSocket连接建立后,后续数据都以帧序列的形式传输。在客户端断开WebSocket连接或Server端断掉连接前,不需要客户端和服务端重新发起连接请求。在海量并发和客户端与服务器交互负载流量大的情况下,极大的节省了网络带宽资源的消耗,有明显的性能优势,且客户端发送和接受消息是在同一个持久连接上发起,实时性优势明显

接下来我看下websocket如何实现客户端与服务端双向通信:

// websocket.html
<div id="clock"></div>
<script>
let clockDiv = document.getElementById('clock')
let socket = new WebSocket('ws://localhost:9999')
//当连接成功之后就会执行回调函数
socket.onopen = function() {
  console.log('客户端连接成功')
  //再向服务 器发送一个消息
  socket.send('hello') //客户端发的消息内容 为hello
}
//绑定事件是用加属性的方式
socket.onmessage = function(event) {
  clockDiv.innerHTML = event.data
  console.log('收到服务器端的响应', event.data)
}
</script>
// websocket.js
let express = require('express')
let app = express()
app.use(express.static(__dirname))
//http服务器
app.listen(3000)
let WebSocketServer = require('ws').Server
//用ws模块启动一个websocket服务器,监听了9999端口
let wsServer = new WebSocketServer({ port: 9999 })
//监听客户端的连接请求  当客户端连接服务器的时候,就会触发connection事件
//socket代表一个客户端,不是所有客户端共享的,而是每个客户端都有一个socket
wsServer.on('connection', function(socket) {
  //每一个socket都有一个唯一的ID属性
  console.log(socket)
  console.log('客户端连接成功')
  //监听对方发过来的消息
  socket.on('message', function(message) {
    console.log('接收到客户端的消息', message)
    socket.send('服务器回应:' + message)
  })
})

启动本地服务,打开http://localhost:3000/websocket.html,得到如下结果:
image

三、Web 实时推送技术的比较

方式 类型 技术实现 优点 缺点 适用场景
轮询Polling client→server 客户端循环请求 1、实现简单 2、 支持跨域 1、浪费带宽和服务器资源 2、 一次请求信息大半是无用(完整http头信息) 3、有延迟 4、大部分无效请求 适于小型应用
长轮询Long-Polling client→server 服务器hold住连接,一直到有数据或者超时才返回,减少重复请求次数 1、实现简单 2、不会频繁发请求 3、节省流量 4、延迟低 1、服务器hold住连接,会消耗资源 2、一次请求信息大半是无用 WebQQ、Hi网页版、Facebook IM
长连接iframe client→server 在页面里嵌入一个隐蔵iframe,将这个 iframe 的 src 属性设为对一个长连接的请求,服务器端就能源源不断地往客户端输入数据。 1、数据实时送达 2、不发无用请求,一次链接,多次“推送” 1、服务器增加开销 2、无法准确知道连接状态 3、IE、chrome等一直会处于loading状态 Gmail聊天
WebSocket server⇌client new WebSocket() 1、支持双向通信,实时性更强 2、可发送二进制文件3、减少通信量 1、浏览器支持程度不一致 2、不支持断开重连 网络游戏、银行交互和支付

综上所述:Websocket协议不仅解决了HTTP协议中服务端的被动性,即通信只能由客户端发起,也解决了数据同步有延迟的问题,同时还带来了明显的性能优势,所以websocket
是Web 实时推送技术的比较理想的方案,但如果要兼容低版本浏览器,可以考虑用轮询来实现。

欢迎关注公众号:前端工匠,你的成长我们一起见证!如果你感觉有收获,欢迎给我打赏,以激励我更多输出优质开源内容image

参考文章

深入浅出浏览器渲染原理

前言

浏览器的内核是指支持浏览器运行的最核心的程序,分为两个部分的,一是渲染引擎,另一个是JS引擎。渲染引擎在不同的浏览器中也不是都相同的。目前市面上常见的浏览器内核可以分为这四种:Trident(IE)、Gecko(火狐)、Blink(Chrome、Opera)、Webkit(Safari)。这里面大家最耳熟能详的可能就是 Webkit 内核了,Webkit 内核是当下浏览器世界真正的霸主。
本文我们就以 Webkit 为例,对现代浏览器的渲染过程进行一个深度的剖析。

想阅读更多优质文章请猛戳GitHub博客,一年五十篇优质文章等着你!

页面加载过程

在介绍浏览器渲染过程之前,我们简明扼要介绍下页面的加载过程,有助于更好理解后续渲染过程。

要点如下:

  • 浏览器根据 DNS 服务器得到域名的 IP 地址
  • 向这个 IP 的机器发送 HTTP 请求
  • 服务器收到、处理并返回 HTTP 请求
  • 浏览器得到返回内容

例如在浏览器输入https://juejin.im/timeline,然后经过 DNS 解析,juejin.im对应的 IP 是36.248.217.149(不同时间、地点对应的 IP 可能会不同)。然后浏览器向该 IP 发送 HTTP 请求。

服务端接收到 HTTP 请求,然后经过计算(向不同的用户推送不同的内容),返回 HTTP 请求,返回的内容如下:

其实就是一堆 HMTL 格式的字符串,因为只有 HTML 格式浏览器才能正确解析,这是 W3C 标准的要求。接下来就是浏览器的渲染过程。

浏览器渲染过程

浏览器渲染过程大体分为如下三部分:

1)浏览器会解析三个东西:

  • 一是HTML/SVG/XHTML,HTML字符串描述了一个页面的结构,浏览器会把HTML结构字符串解析转换DOM树形结构。

  • 二是CSS,解析CSS会产生CSS规则树,它和DOM结构比较像。

  • 三是Javascript脚本,等到Javascript 脚本文件加载后, 通过 DOM API 和 CSSOM API 来操作 DOM Tree 和 CSS Rule Tree。

2)解析完成后,浏览器引擎会通过DOM Tree 和 CSS Rule Tree 来构造 Rendering Tree。

  • Rendering Tree 渲染树并不等同于DOM树,渲染树只会包括需要显示的节点和这些节点的样式信息。
  • CSS 的 Rule Tree主要是为了完成匹配并把CSS Rule附加上Rendering Tree上的每个Element(也就是每个Frame)。
  • 然后,计算每个Frame 的位置,这又叫layout和reflow过程。

3)最后通过调用操作系统Native GUI的API绘制。

接下来我们针对这其中所经历的重要步骤详细阐述

构建DOM

浏览器会遵守一套步骤将HTML 文件转换为 DOM 树。宏观上,可以分为几个步骤:

构建DOM的具体步骤

  • 浏览器从磁盘或网络读取HTML的原始字节,并根据文件的指定编码(例如 UTF-8)将它们转换成字符串。

在网络中传输的内容其实都是 0 和 1 这些字节数据。当浏览器接收到这些字节数据以后,它会将这些字节数据转换为字符串,也就是我们写的代码。

  • 将字符串转换成Token,例如:<html><body>等。Token中会标识出当前Token是“开始标签”或是“结束标签”亦或是“文本”等信息

这时候你一定会有疑问,节点与节点之间的关系如何维护?

事实上,这就是Token要标识“起始标签”和“结束标签”等标识的作用。例如“title”Token的起始标签和结束标签之间的节点肯定是属于“head”的子节点。

上图给出了节点之间的关系,例如:“Hello”Token位于“title”开始标签与“title”结束标签之间,表明“Hello”Token是“title”Token的子节点。同理“title”Token是“head”Token的子节点。

  • 生成节点对象并构建DOM

事实上,构建DOM的过程中,不是等所有Token都转换完成后再去生成节点对象,而是一边生成Token一边消耗Token来生成节点对象。换句话说,每个Token被生成后,会立刻消耗这个Token创建出节点对象。注意:带有结束标签标识的Token不会创建节点对象。

接下来我们举个例子,假设有段HTML文本:

<html>
<head>
    <title>Web page parsing</title>
</head>
<body>
    <div>
        <h1>Web page parsing</h1>
        <p>This is an example Web page.</p>
    </div>
</body>
</html>

上面这段HTML会解析成这样:

构建CSSOM

DOM会捕获页面的内容,但浏览器还需要知道页面如何展示,所以需要构建CSSOM。

构建CSSOM的过程与构建DOM的过程非常相似,当浏览器接收到一段CSS,浏览器首先要做的是识别出Token,然后构建节点并生成CSSOM。

在这一过程中,浏览器会确定下每一个节点的样式到底是什么,并且这一过程其实是很消耗资源的。因为样式你可以自行设置给某个节点,也可以通过继承获得。在这一过程中,浏览器得递归 CSSOM 树,然后确定具体的元素到底是什么样式。

注意:CSS匹配HTML元素是一个相当复杂和有性能问题的事情。所以,DOM树要小,CSS尽量用id和class,千万不要过渡层叠下去

构建渲染树

当我们生成 DOM 树和 CSSOM 树以后,就需要将这两棵树组合为渲染树。

在这一过程中,不是简单的将两者合并就行了。渲染树只会包括需要显示的节点和这些节点的样式信息,如果某个节点是 display: none 的,那么就不会在渲染树中显示。

我们或许有个疑惑:浏览器如果渲染过程中遇到JS文件怎么处理

渲染过程中,如果遇到<script>就停止渲染,执行 JS 代码。因为浏览器有GUI渲染线程与JS引擎线程,为了防止渲染出现不可预期的结果,这两个线程是互斥的关系。JavaScript的加载、解析与执行会阻塞DOM的构建,也就是说,在构建DOM时,HTML解析器若遇到了JavaScript,那么它会暂停构建DOM,将控制权移交给JavaScript引擎,等JavaScript引擎运行完毕,浏览器再从中断的地方恢复DOM构建。

也就是说,如果你想首屏渲染的越快,就越不应该在首屏就加载 JS 文件,这也是都建议将 script 标签放在 body 标签底部的原因。当然在当下,并不是说 script 标签必须放在底部,因为你可以给 script 标签添加 defer 或者 async 属性(下文会介绍这两者的区别)。

JS文件不只是阻塞DOM的构建,它会导致CSSOM也阻塞DOM的构建

原本DOM和CSSOM的构建是互不影响,井水不犯河水,但是一旦引入了JavaScript,CSSOM也开始阻塞DOM的构建,只有CSSOM构建完毕后,DOM再恢复DOM构建。

这是什么情况?

这是因为JavaScript不只是可以改DOM,它还可以更改样式,也就是它可以更改CSSOM。因为不完整的CSSOM是无法使用的,如果JavaScript想访问CSSOM并更改它,那么在执行JavaScript时,必须要能拿到完整的CSSOM。所以就导致了一个现象,如果浏览器尚未完成CSSOM的下载和构建,而我们却想在此时运行脚本,那么浏览器将延迟脚本执行和DOM构建,直至其完成CSSOM的下载和构建。也就是说,在这种情况下,浏览器会先下载和构建CSSOM,然后再执行JavaScript,最后在继续构建DOM

布局与绘制

当浏览器生成渲染树以后,就会根据渲染树来进行布局(也可以叫做回流)。这一阶段浏览器要做的事情是要弄清楚各个节点在页面中的确切位置和大小。通常这一行为也被称为“自动重排”。

布局流程的输出是一个“盒模型”,它会精确地捕获每个元素在视口内的确切位置和尺寸,所有相对测量值都将转换为屏幕上的绝对像素。

布局完成后,浏览器会立即发出“Paint Setup”和“Paint”事件,将渲染树转换成屏幕上的像素。

以上我们详细介绍了浏览器工作流程中的重要步骤,接下来我们讨论几个相关的问题:

几点补充说明

1.async和defer的作用是什么?有什么区别?

接下来我们对比下 defer 和 async 属性的区别:

async和defer

其中蓝色线代表JavaScript加载;红色线代表JavaScript执行;绿色线代表 HTML 解析。

1)情况1<script src="script.js"></script>

没有 defer 或 async,浏览器会立即加载并执行指定的脚本,也就是说不等待后续载入的文档元素,读到就加载并执行。

2)情况2<script async src="script.js"></script> (异步下载)

async 属性表示异步执行引入的 JavaScript,与 defer 的区别在于,如果已经加载好,就会开始执行——无论此刻是 HTML 解析阶段还是 DOMContentLoaded 触发之后。需要注意的是,这种方式加载的 JavaScript 依然会阻塞 load 事件。换句话说,async-script 可能在 DOMContentLoaded 触发之前或之后执行,但一定在 load 触发之前执行。

3)情况3 <script defer src="script.js"></script>(延迟执行)

defer 属性表示延迟执行引入的 JavaScript,即这段 JavaScript 加载时 HTML 并未停止解析,这两个过程是并行的。整个 document 解析完毕且 defer-script 也加载完成之后(这两件事情的顺序无关),会执行所有由 defer-script 加载的 JavaScript 代码,然后触发 DOMContentLoaded 事件。

defer 与相比普通 script,有两点区别:载入 JavaScript 文件时不阻塞 HTML 的解析,执行阶段被放到 HTML 标签解析完成之后。
在加载多个JS脚本的时候,async是无顺序的加载,而defer是有顺序的加载。

2.为什么操作 DOM 慢

把 DOM 和 JavaScript 各自想象成一个岛屿,它们之间用收费桥梁连接。——《高性能 JavaScript》

JS 是很快的,在 JS 中修改 DOM 对象也是很快的。在JS的世界里,一切是简单的、迅速的。但 DOM 操作并非 JS 一个人的独舞,而是两个模块之间的协作。

因为 DOM 是属于渲染引擎中的东西,而 JS 又是 JS 引擎中的东西。当我们用 JS 去操作 DOM 时,本质上是 JS 引擎和渲染引擎之间进行了“跨界交流”。这个“跨界交流”的实现并不简单,它依赖了桥接接口作为“桥梁”(如下图)。

过“桥”要收费——这个开销本身就是不可忽略的。我们每操作一次 DOM(不管是为了修改还是仅仅为了访问其值),都要过一次“桥”。过“桥”的次数一多,就会产生比较明显的性能问题。因此“减少 DOM 操作”的建议,并非空穴来风。

3.你真的了解回流和重绘吗

渲染的流程基本上是这样(如下图黄色的四个步骤):1.计算CSS样式 2.构建Render Tree 3.Layout – 定位坐标和大小 4.正式开画

注意:上图流程中有很多连接线,这表示了Javascript动态修改了DOM属性或是CSS属性会导致重新Layout,但有些改变不会重新Layout,就是上图中那些指到天上的箭头,比如修改后的CSS rule没有被匹配到元素。

这里重要要说两个概念,一个是Reflow,另一个是Repaint

  • 重绘:当我们对 DOM 的修改导致了样式的变化、却并未影响其几何属性(比如修改了颜色或背景色)时,浏览器不需重新计算元素的几何属性、直接为该元素绘制新的样式(跳过了上图所示的回流环节)。
  • 回流:当我们对 DOM 的修改引发了 DOM 几何尺寸的变化(比如修改元素的宽、高或隐藏元素等)时,浏览器需要重新计算元素的几何属性(其他元素的几何属性和位置也会因此受到影响),然后再将计算的结果绘制出来。这个过程就是回流(也叫重排)

我们知道,当网页生成的时候,至少会渲染一次。在用户访问的过程中,还会不断重新渲染。重新渲染会重复回流+重绘或者只有重绘。
回流必定会发生重绘,重绘不一定会引发回流。重绘和回流会在我们设置节点样式时频繁出现,同时也会很大程度上影响性能。回流所需的成本比重绘高的多,改变父节点里的子节点很可能会导致父节点的一系列回流。

1)常见引起回流属性和方法

任何会改变元素几何信息(元素的位置和尺寸大小)的操作,都会触发回流,

  • 添加或者删除可见的DOM元素;
  • 元素尺寸改变——边距、填充、边框、宽度和高度
  • 内容变化,比如用户在input框中输入文字
  • 浏览器窗口尺寸改变——resize事件发生时
  • 计算 offsetWidth 和 offsetHeight 属性
  • 设置 style 属性的值

2)常见引起重绘属性和方法

3)如何减少回流、重绘

  • 使用 transform 替代 top
  • 使用 visibility 替换 display: none ,因为前者只会引起重绘,后者会引发回流(改变了布局)
  • 不要把节点的属性值放在一个循环里当成循环里的变量。
for(let i = 0; i < 1000; i++) {
    // 获取 offsetTop 会导致回流,因为需要去获取正确的值
    console.log(document.querySelector('.test').style.offsetTop)
}
  • 不要使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局
  • 动画实现的速度的选择,动画速度越快,回流次数越多,也可以选择使用 requestAnimationFrame
  • CSS 选择符从右往左匹配查找,避免节点层级过多
  • 将频繁重绘或者回流的节点设置为图层,图层能够阻止该节点的渲染行为影响别的节点。比如对于 video 标签来说,浏览器会自动将该节点变为图层。

性能优化策略

基于上面介绍的浏览器渲染原理,DOM 和 CSSOM 结构构建顺序,初始化可以对页面渲染做些优化,提升页面性能。

  • JS优化: <script> 标签加上 defer属性 和 async属性 用于在不阻塞页面文档解析的前提下,控制脚本的下载和执行。
    • defer属性: 用于开启新的线程下载脚本文件,并使脚本在文档解析完成后执行。
    • async属性: HTML5新增属性,用于异步下载脚本文件,下载完毕立即解释执行代码。
  • CSS优化: <link> 标签的 rel属性 中的属性值设置为 preload 能够让你在你的HTML页面中可以指明哪些资源是在页面加载完成后即刻需要的,最优的配置加载顺序,提高渲染性能

总结

综上所述,我们得出这样的结论:

  • 浏览器工作流程:构建DOM -> 构建CSSOM -> 构建渲染树 -> 布局 -> 绘制。
  • CSSOM会阻塞渲染,只有当CSSOM构建完毕后才会进入下一个阶段构建渲染树。
  • 通常情况下DOM和CSSOM是并行构建的,但是当浏览器遇到一个不带defer或async属性的script标签时,DOM构建将暂停,如果此时又恰巧浏览器尚未完成CSSOM的下载和构建,由于JavaScript可以修改CSSOM,所以需要等CSSOM构建完毕后再执行JS,最后才重新DOM构建。

欢迎关注公众号:前端工匠,你的成长我们一起见证!

参考文章

实现三栏布局的几种方法

前言

三栏布局,顾名思义就是两边固定,中间自适应。三栏布局在实际的开发十分常见,比如淘宝网的首页,就是个典型的三栏布局:即左边商品导航和右边导航固定宽度,中间的主要内容随浏览器宽度自适应。

我们不妨假定这样一个布局:高度已知,其中左栏、右栏宽度各为300px,中间自适应,可以通过几种方法来实现?以及各自的优缺点是什么?

本文源代码请猛戳三栏布局源码,欢迎star和fork

一、浮动布局

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Layout</title>
    <style media="screen">
        html * {
            padding: 0;
            margin: 0;
        }
        .layout article div {
            min-height: 150px;
        }
    </style>
</head>
<body>
    <!--浮动布局  -->
    <section class="layout float">
        <style media="screen">
            .layout.float .left {
                float: left;
                width: 300px;
                background: red;
            }
            .layout.float .center {
                background: yellow;
            }
            .layout.float .right {
                float: right;
                width: 300px;
                background: blue;
            }
        </style>
        <h1>三栏布局</h1>
        <article class="left-right-center">
            <div class="left"></div>
            <div class="right"></div> // 右栏部分要写在中间内容之前
            <div class="center">
                <h2>浮动解决方案</h2>
                1.这是三栏布局的浮动解决方案; 2.这是三栏布局的浮动解决方案; 3.这是三栏布局的浮动解决方案; 4.这是三栏布局的浮动解决方案; 5.这是三栏布局的浮动解决方案; 6.这是三栏布局的浮动解决方案;
            </div>
        </article>
    </section>
</body>
</html>


这种布局方式,dom结构必须是先写浮动部分,然后再中间块,否则右浮动块会掉到下一行。
浮动布局的优点就是比较简单,兼容性也比较好。但浮动布局是有局限性的,浮动元素脱离文档流,要做清除浮动,这个处理不好的话,会带来很多问题,比如父容器高度塌陷等

二、绝对布局

   <!--绝对布局  -->
    <section class="layout absolute">
        <style>
            .layout.absolute .left-center-right>div{
                position: absolute;//三块都是绝对定位
            }
            .layout.absolute .left {
                left:0;
                width: 300px;
                background: red;
            }
            .layout.absolute .center {
                right: 300px;
                left: 300px;//离左右各三百
                background: yellow;
            }
            .layout.absolute .right {
                right: 0;
                width: 300px;
                background: blue;
            }
        </style>
        <h1>三栏布局</h1>
        <article class="left-center-right">
            <div class="left"></div>
            <div class="center">
                <h2>绝对定位解决方案</h2>
                1.这是三栏布局的浮动解决方案; 2.这是三栏布局的浮动解决方案; 3.这是三栏布局的浮动解决方案; 4.这是三栏布局的浮动解决方案; 5.这是三栏布局的浮动解决方案; 6.这是三栏布局的浮动解决方案;
            </div>
            <div class="right"></div>
        </article>
    </section>

绝对定位布局优点就是快捷,设置很方便,而且也不容易出问题。缺点就是,容器脱离了文档流,后代元素也脱离了文档流,高度未知的时候,会有问题,这就导致了这种方法的有效性和可使用性是比较差的。

三、flexbox布局

    <!--flexbox布局-->
    <section class="layout flexbox">
        <style>
            .layout.flexbox .left-center-right{
                display: flex;
            }
            .layout.flexbox .left {
                width: 300px;
                background: red;
            }
            .layout.flexbox .center {
                background: yellow;
                flex: 1;
            }
            .layout.flexbox .right {
                width: 300px;
                background: blue;
            }
        </style>
        <h1>三栏布局</h1>
        <article class="left-center-right">
            <div class="left"></div>
            <div class="center">
                <h2>flexbox解决方案</h2>
                1.这是三栏布局的浮动解决方案; 2.这是三栏布局的浮动解决方案; 3.这是三栏布局的浮动解决方案; 4.这是三栏布局的浮动解决方案; 5.这是三栏布局的浮动解决方案; 6.这是三栏布局的浮动解决方案;
            </div>
            <div class="right"></div>
        </article>
    </section>

flexbox布局是css3里新出的一个,它就是为了解决上述两种方式的不足出现的,是比较完美的一个。目前移动端的布局也都是用flexbox。 flexbox的缺点就是IE10开始支持,但是IE10的是-ms形式的。

四、表格布局

<!--表格布局-->
    <section class="layout table">
        <style>
            .layout.table .left-center-right {
                display: table;
                height: 150px;
                width: 100%;
            }
            .layout.table .left-center-right>div {
                display: table-cell;
            }
            .layout.table .left {
                width: 300px;
                background: red;
            }
            .layout.table .center {
                background: yellow;
            }
            .layout.table .right {
                width: 300px;
                background: blue;
            }
        </style>
        <h1>三栏布局</h1>
        <article class="left-center-right">
            <div class="left"></div>
            <div class="center">
                <h2>表格布局解决方案</h2>
                1.这是三栏布局的浮动解决方案; 2.这是三栏布局的浮动解决方案; 3.这是三栏布局的浮动解决方案; 4.这是三栏布局的浮动解决方案; 5.这是三栏布局的浮动解决方案; 6.这是三栏布局的浮动解决方案;
            </div>
            <div class="right"></div>
        </article>
    </section>

表格布局的兼容性很好(见下图),在flex布局不兼容的时候,可以尝试表格布局。当内容溢出时会自动撑开父元素

表格布局也是有缺陷:①无法设置栏边距;②对seo不友好;③当其中一个单元格高度超出的时候,两侧的单元格也是会跟着一起变高的,然而有时候这并不是我们想要的效果。

五、网格布局

    <!--网格布局-->
    <section class="layout grid">
        <style>
            .layout.grid .left-center-right {
                display: grid;
                width: 100%;
                grid-template-columns: 300px auto 300px;
                grid-template-rows: 150px;//行高
            }
            .layout.grid .left {
                background: red;
            }
            .layout.grid .center {
                background: yellow;
            }
            .layout.grid .right {
                background: blue;
            }
        </style>
        <h1>三栏布局</h1>
        <article class="left-center-right">
            <div class="left"></div>
            <div class="center">
                <h2>网格布局解决方案</h2>
                1.这是三栏布局的浮动解决方案; 2.这是三栏布局的浮动解决方案; 3.这是三栏布局的浮动解决方案; 4.这是三栏布局的浮动解决方案; 5.这是三栏布局的浮动解决方案; 6.这是三栏布局的浮动解决方案;
            </div>
            <div class="right"></div>
        </article>
    </section>

CSS Grid是创建网格布局最强大和最简单的工具。就像表格一样,网格布局可以让Web设计师根据元素按列或行对齐排列,但他和表格不同,网格布局没有内容结构,从而使各种布局不可能与表格一样。例如,一个网格布局中的子元素都可以定位自己的位置,这样他们可以重叠和类似元素定位

但网格布局的兼容性不好。IE10+上支持,而且也仅支持部分属性

六、总结

通过上面详细介绍五种布局的优缺点,在实际开发中最优选择哪种布局?相信读者心中会有自己的答案。
我觉得flex和grid布局就可以搞定实际开发中的布局,假设浏览器都支持这两个模块,你将选择grid还是flexbox来给页面布局?flexbox是一维布局,他只能在一条直线上放置你的内容区块;而grid是一个二维布局。前面也简单说到,你可以根据你的设计需求,将内容区块放置到任何你想要放的地方。那么不用多说,你应该知道哪一种更适合你的布局。此外,如果要兼容低版本的IE(比如IE8+),可以考虑table布局。

最后问大家一个问题,如果中间部分被内容高度撑开,需要左右栏也撑开,这五种布局哪些布局还可以用?

答案:flex布局和table布局

从头开始学习vue-router

一、前言

要学习vue-router就要先知道这里的路由是什么?为什么我们不能像原来一样直接用标签编写链接哪?vue-router如何使用?常见路由操作有哪些?等等这些问题,就是本篇要探讨的主要问题。

二、vue-router是什么

这里的路由并不是指我们平时所说的硬件路由器,这里的路由就是SPA(单页应用)的路径管理器。再通俗的说,vue-router就是WebApp的链接路径管理系统。
vue-router是Vue.js官方的路由插件,它和vue.js是深度集成的,适合用于构建单页面应用。vue的单页面应用是基于路由和组件的,路由用于设定访问路径,并将路径和组件映射起来。传统的页面应用,是用一些超链接来实现页面切换和跳转的。在vue-router单页面应用中,则是路径之间的切换,也就是组件的切换。路由模块的本质 就是建立起url和页面之间的映射关系

至于我们为啥不能用a标签,这是因为用Vue做的都是单页应用(当你的项目准备打包时,运行npm run build时,就会生成dist文件夹,这里面只有静态资源和一个index.html页面),所以你写的标签是不起作用的,你必须使用vue-router来进行管理。

三、vue-router实现原理

SPA(single page application):单一页面应用程序,只有一个完整的页面;它在加载页面时,不会加载整个页面,而是只更新某个指定的容器中内容。单页面应用(SPA)的核心之一是: 更新视图而不重新请求页面;vue-router在实现单页面前端路由时,提供了两种方式:Hash模式和History模式;根据mode参数来决定采用哪一种方式。

1、Hash模式:

vue-router 默认 hash 模式 —— 使用 URL 的 hash 来模拟一个完整的 URL,于是当 URL 改变时,页面不会重新加载。 hash(#)是URL 的锚点,代表的是网页中的一个位置,单单改变#后的部分,浏览器只会滚动到相应位置,不会重新加载网页,也就是说hash 出现在 URL 中,但不会被包含在 http 请求中,对后端完全没有影响,因此改变 hash 不会重新加载页面;同时每一次改变#后的部分,都会在浏览器的访问历史中增加一个记录,使用”后退”按钮,就可以回到上一个位置;所以说Hash模式通过锚点值的改变,根据不同的值,渲染指定DOM位置的不同数据。hash 模式的原理是 onhashchange 事件(监测hash值变化),可以在 window 对象上监听这个事件

2、History模式:

由于hash模式会在url中自带#,如果不想要很丑的 hash,我们可以用路由的 history 模式,只需要在配置路由规则时,加入"mode: 'history'",这种模式充分利用了html5 history interface 中新增的 pushState() 和 replaceState() 方法。这两个方法应用于浏览器记录栈,在当前已有的 back、forward、go 基础之上,它们提供了对历史记录修改的功能。只是当它们执行修改时,虽然改变了当前的 URL ,但浏览器不会立即向后端发送请求

//main.js文件中
const router = new VueRouter({
  mode: 'history',
  routes: [...]
})

当你使用 history 模式时,URL 就像正常的 url,例如 http://yoursite.com/user/id,比较好看!
不过这种模式要玩好,还需要后台配置支持。因为我们的应用是个单页客户端应用,如果后台没有正确的配置,当用户在浏览器直接访问 http://oursite.com/user/id 就会返回 404,这就不好看了。
所以呢,你要在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面。

 export const routes = [ 
  {path: "/", name: "homeLink", component:Home}
  {path: "/register", name: "registerLink", component: Register},
  {path: "/login", name: "loginLink", component: Login},
  {path: "*", redirect: "/"}]

此处就设置如果URL输入错误或者是URL 匹配不到任何静态资源,就自动跳到到Home页面

3、使用路由模块来实现页面跳转的方式

  • 方式1:直接修改地址栏

  • 方式2:this.$router.push(‘路由地址’)

  • 方式3:<router-link to="路由地址"></router-link>

四、vue-router使用方式

1:下载 npm i vue-router -S
2:在main.js中引入 import VueRouter from 'vue-router';
3:安装插件Vue.use(VueRouter);
4:创建路由对象并配置路由规则 let router = new VueRouter({routes:[{path:'/home',component:Home}]});
5:将其路由对象传递给Vue的实例,options中加入 router:router
6:在app.vue中留坑 <router-view></router-view>

具体实现请看如下代码:

//main.js文件中引入
import Vue from 'vue';
import VueRouter from 'vue-router';
//主体
import App from './components/app.vue';
import Home from './components/home.vue'
//安装插件
Vue.use(VueRouter); //挂载属性
//创建路由对象并配置路由规则
let router = new VueRouter({
    routes: [
        //一个个对象
        { path: '/home', component: Home }
    ]
});
//new Vue 启动
new Vue({
    el: '#app',
    //让vue知道我们的路由规则
    router: router, //可以简写router
    render: c => c(App),
})

最后记得在在app.vue中“留坑”

//app.vue中
<template>
    <div>
        <!-- 留坑,非常重要 -->
        <router-view></router-view>
    </div>
</template>
<script>
    export default {
        data(){
            return {}
        }
    }
</script>

五、 vue-router参数传递

声明式的导航<router-link :to="...">和编程式的导航router.push(...)都可以传参,本文主要介绍前者的传参方法,同样的规则也适用于编程式的导航。

1.用name传递参数

在路由文件src/router/index.js里配置name属性

routes: [
    {
      path: '/',
      name: 'Hello',
      component: Hello
    }
]

模板里(src/App.vue)用$route.name来接收 比如:<p>{{ $route.name}}</p>

2 通过<router-link> 标签中的to传参

这种传参方法的基本语法:

<router-link :to="{name:xxx,params:{key:value}}">valueString</router-link>

比如先在src/App.vue文件中

<router-link :to="{name:'hi1',params:{username:'jspang',id:'555'}}">Hi页面1</router-link>

然后把src/router/index.js文件里给hi1配置的路由起个name,就叫hi1.

{path:'/hi1',name:'hi1',component:Hi1}

最后在模板里(src/components/Hi1.vue)用$route.params.username进行接收.

{{$route.params.username}}-{{$route.params.id}}

3 利用url传递参数----在配置文件里以冒号的形式设置参数。

我们在/src/router/index.js文件里配置路由

{
    path:'/params/:newsId/:newsTitle',
    component:Params
}

我们需要传递参数是新闻ID(newsId)和新闻标题(newsTitle).所以我们在路由配置文件里制定了这两个值。

在src/components目录下建立我们params.vue组件,也可以说是页面。我们在页面里输出了url传递的的新闻ID和新闻标题。

<template>
    <div>
        <h2>{{ msg }}</h2>
        <p>新闻ID:{{ $route.params.newsId}}</p>
        <p>新闻标题:{{ $route.params.newsTitle}}</p>
    </div>
</template>
<script>
export default {
  name: 'params',
  data () {
    return {
      msg: 'params page'
    }
  }
}
</script>

在App.vue文件里加入我们的<router-view>标签。这时候我们可以直接利用url传值了

<router-link to="/params/198/jspang website is very good">params</router-link>

4. 使用path来匹配路由,然后通过query来传递参数

<router-link :to="{ name:'Query',query: { queryId:  status }}" >
     router-link跳转Query
</router-link>

对应路由配置:

   {
     path: '/query',
     name: 'Query',
     component: Query
   }

于是我们可以获取参数:

this.$route.query.queryId

六、vue-router配置子路由(二级路由)

实际生活中的应用界面,通常由多层嵌套的组件组合而成。同样地,URL中各段动态路径也按某种结构对应嵌套的各层组件,例如:

如何实现下图效果(H1页面和H2页面嵌套在主页中)

1.首先用<router-link>标签增加了两个新的导航链接

<router-link :to="{name:'HelloWorld'}">主页</router-link>
<router-link :to="{name:'H1'}">H1页面</router-link>
<router-link :to="{name:'H2'}">H2页面</router-link>

2.在HelloWorld.vue加入<router-view>标签,给子模板提供插入位置

 <template>
  <div class="hello">
    <h1>{{ msg }}</h1>
    <router-view></router-view>
  </div>
</template>

3.在components目录下新建两个组件模板 H1.vue 和 H2.vue
两者内容类似,以下是H1.vue页面内容:

 <template>
  <div class="hello">
    <h1>{{ msg }}</h1>
  </div>
</template>
<script>
  export default {
    data() {
      return {
        msg: 'I am H1 page,Welcome to H1'
      }
    }
  }
</script>
  1. 修改router/index.js代码,子路由的写法是在原有的路由配置下加入children字段。
   routes: [
    {
      path: '/',
      name: 'HelloWorld',
      component: HelloWorld,
      children: [{path: '/h1', name: 'H1', component: H1},//子路由的<router-view>必须在HelloWorld.vue中出现
        {path: '/h2', name: 'H2', component: H2}
      ]
    }
  ]

七、单页面多路由区域操作

在一个页面里我们有两个以上<router-view>区域,我们通过配置路由的js文件,来操作这些区域的内容

1.App.vue文件,在<router-view>下面新写了两行<router-view>标签,并加入了些CSS样式

<template>
  <div id="app">
    <img src="./assets/logo.png">
       <router-link :to="{name:'HelloWorld'}"><h1>H1</h1></router-link>
       <router-link :to="{name:'H1'}"><h1>H2</h1></router-link>
    <router-view></router-view>
    <router-view name="left" style="float:left;width:50%;background-color:#ccc;height:300px;"/>
    <router-view name="right" style="float:right;width:50%;background-color:yellowgreen;height:300px;"/>
  </div>
</template>

2.需要在路由里配置这三个区域,配置主要是在components字段里进行

export default new Router({
    routes: [
      {
        path: '/',
        name: 'HelloWorld',
        components: {default: HelloWorld,
          left:H1,//显示H1组件内容'I am H1 page,Welcome to H1'
          right:H2//显示H2组件内容'I am H2 page,Welcome to H2'
        }
      },
      {
        path: '/h1',
        name: 'H1',
        components: {default: HelloWorld,
          left:H2,//显示H2组件内容
          right:H1//显示H1组件内容
        }
      }
    ]
  })

上边的代码我们编写了两个路径,一个是默认的‘/’,另一个是‘/Hi’.在两个路径下的components里面,我们对三个区域都定义了显示内容。最后页面展示如下图:

八.$route$router 的区别

我们先将这两者console.log打印出来:

$route 是“路由信息对象”,包括 path,params,hash,query,fullPath,matched,name 等路由信息参数。

$route.path
字符串,对应当前路由的路径,总是解析为绝对路径,如 "/order"。

$route.params
一个 key/value 对象,包含了 动态片段 和 全匹配片段,
如果没有路由参数,就是一个空对象。

$route.query
一个 key/value 对象,表示 URL 查询参数。
例如,对于路径 /foo?user=1,则有 $route.query.user为1,
如果没有查询参数,则是个空对象。

$route.hash
当前路由的 hash 值 (不带 #) ,如果没有 hash 值,则为空字符串。

$route.fullPath
完成解析后的 URL,包含查询参数和 hash 的完整路径。

$route.matched
数组,包含当前匹配的路径中所包含的所有片段所对应的配置参数对象。

$route.name 当前路径名字

$router 是“路由实例”对象,即使用 new VueRouter创建的实例,包括了路由的跳转方法,钩子函数等。

$router常见跳转方法:

<button @click="goToMenu" class="btn btn-success">Let's order!</button>
.....
<script>
  export default{
    methods:{
      goToMenu(){
        this.$router.go(-1)//跳转到上一次浏览的页面
        this.$router.replace('/menu')//指定跳转的地址
        this.$router.replace({name:'menuLink'})//指定跳转路由的名字下
        this.$router.push('/menu')//通过push进行跳转
        this.$router.push({name:'menuLink'})//通过push进行跳转路由的名字下
      }
    }
  }
</script>

$router.push$router.replace的区别

  • 使用push方法的跳转会向 history 栈添加一个新的记录,当我们点击浏览器的返回按钮时可以看到之前的页面。
  • 使用replace方法不会向 history 添加新记录,而是替换掉当前的 history 记录,即当replace跳转到的网页后,‘后退’按钮不能查看之前的页面。

九、 如何设置404页面

用户会经常输错页面,当用户输错页面时,我们希望给他一个友好的提示页面,这个页面就是我们常说的404页面。vue-router也为我们提供了这样的机制。

  1. 设置我们的路由配置文件(/src/router/index.js)
{
   path:'*',
   component:Error
}

这里的path:'*'就是输入地址不匹配时,自动显示出Error.vue的文件内容

  1. 在/src/components/文件夹下新建一个Error.vue的文件。简单输入一些有关错误页面的内容。
<template>
    <div>
        <h2>{{ msg }}</h2>
    </div>
</template>
<script>
export default {
  data () {
    return {
      msg: 'Error:404'
    }
  }
}
</script>

此时我们随意输入一个错误的地址时,便会自动跳转到404页面

参考文章

几种常见的CSS布局

本文概要

本文将介绍如下几种常见的布局:

其中实现三栏布局有多种方式,本文着重介绍圣杯布局和双飞翼布局。另外几种可以猛戳实现三栏布局的几种方法

一、单列布局

常见的单列布局有两种:

  • header,content和footer等宽的单列布局
  • header与footer等宽,content略窄的单列布局

1.如何实现

对于第一种,先通过对header,content,footer统一设置width:1000px;或者max-width:1000px(这两者的区别是当屏幕小于1000px时,前者会出现滚动条,后者则不会,显示出实际宽度);然后设置margin:auto实现居中即可得到。

<div class="header"></div>
<div class="content"></div>
<div class="footer"></div>
.header{
    margin:0 auto; 
    max-width: 960px;
    height:100px;
    background-color: blue;
}
.content{
    margin: 0 auto;
    max-width: 960px;
    height: 400px;
    background-color: aquamarine;
}
.footer{
    margin: 0 auto;
    max-width: 960px;
    height: 100px;
    background-color: aqua;
}

对于第二种,header、footer的内容宽度不设置,块级元素充满整个屏幕,但header、content和footer的内容区设置同一个width,并通过margin:auto实现居中。

<div class="header">
    <div class="nav"></div>
</div>
<div class="content"></div>
<div class="footer"></div>
.header{
    margin:0 auto;
    max-width: 960px;
    height:100px;
    background-color: blue;
}
.nav{
    margin: 0 auto;
    max-width: 800px;
    background-color: darkgray;
    height: 50px;
}
.content{
    margin: 0 auto;
    max-width: 800px;
    height: 400px;
    background-color: aquamarine;
}
.footer{
    margin: 0 auto;
    max-width: 960px;
    height: 100px;
    background-color: aqua;
}

二、两列自适应布局

两列自适应布局是指一列由内容撑开,另一列撑满剩余宽度的布局方式

1.float+overflow:hidden

如果是普通的两列布局,浮动+普通元素的margin便可以实现,但如果是自适应的两列布局,利用float+overflow:hidden便可以实现,这种办法主要通过overflow触发BFC,而BFC不会重叠浮动元素。由于设置overflow:hidden并不会触发IE6-浏览器的haslayout属性,所以需要设置zoom:1来兼容IE6-浏览器。具体代码如下:

<div class="parent" style="background-color: lightgrey;">
    <div class="left" style="background-color: lightblue;">
        <p>left</p>
    </div>
    <div class="right"  style="background-color: lightgreen;">
        <p>right</p>
        <p>right</p>
    </div>        
</div>
.parent {
  overflow: hidden;
  zoom: 1;
}
.left {
  float: left;
  margin-right: 20px;
}
.right {
  overflow: hidden;
  zoom: 1;
}

注意点:如果侧边栏在右边时,注意渲染顺序。即在HTML中,先写侧边栏后写主内容

2.Flex布局

Flex布局,也叫弹性盒子布局,区区简单几行代码就可以实现各种页面的的布局。

//html部分同上
.parent {
  display:flex;
}  
.right {
  margin-left:20px; 
  flex:1;
}

3.grid布局

Grid布局,是一个基于网格的二维布局系统,目的是用来优化用户界面设计。

//html部分同上
.parent {
  display:grid;
  grid-template-columns:auto 1fr;
  grid-gap:20px
} 

三、三栏布局

特征:中间列自适应宽度,旁边两侧固定宽度

1.圣杯布局

① 特点

比较特殊的三栏布局,同样也是两边固定宽度,中间自适应,唯一区别是dom结构必须是先写中间列部分,这样实现中间列可以优先加载

  .container {
    padding-left: 220px;//为左右栏腾出空间
    padding-right: 220px;
  }
  .left {
    float: left;
    width: 200px;
    height: 400px;
    background: red;
    margin-left: -100%;
    position: relative;
    left: -220px;
  }
  .center {
    float: left;
    width: 100%;
    height: 500px;
    background: yellow;
  }
  .right {
    float: left;
    width: 200px;
    height: 400px;
    background: blue;
    margin-left: -200px;
    position: relative;
    right: -220px;
  }
  <article class="container">
    <div class="center">
      <h2>圣杯布局</h2>
    </div>
    <div class="left"></div>
    <div class="right"></div>
  </article>

② 实现步骤

  • 三个部分都设定为左浮动,否则左右两边内容上不去,就不可能与中间列同一行。然后设置center的宽度为100%(实现中间列内容自适应),此时,left和right部分会跳到下一行

  • 通过设置margin-left为负值让left和right部分回到与center部分同一行

  • 通过设置父容器的padding-left和padding-right,让左右两边留出间隙。

  • 通过设置相对定位,让left和right部分移动到两边。

③ 缺点

  • center部分的最小宽度不能小于left部分的宽度,否则会left部分掉到下一行
  • 如果其中一列内容高度拉长(如下图),其他两列的背景并不会自动填充。(借助等高布局正padding+负margin可解决,下文会介绍)

2.双飞翼布局

① 特点

同样也是三栏布局,在圣杯布局基础上进一步优化,解决了圣杯布局错乱问题,实现了内容与布局的分离。而且任何一栏都可以是最高栏,不会出问题

    .container {
        min-width: 600px;//确保中间内容可以显示出来,两倍left宽+right宽
    }
    .left {
        float: left;
        width: 200px;
        height: 400px;
        background: red;
        margin-left: -100%;
    }
    .center {
        float: left;
        width: 100%;
        height: 500px;
        background: yellow;
    }
    .center .inner {
        margin: 0 200px; //新增部分
    }
    .right {
        float: left;
        width: 200px;
        height: 400px;
        background: blue;
        margin-left: -200px;
    }
    <article class="container">
        <div class="center">
            <div class="inner">双飞翼布局</div>
        </div>
        <div class="left"></div>
        <div class="right"></div>
    </article>

② 实现步骤(前两步与圣杯布局一样)

  • 三个部分都设定为左浮动,然后设置center的宽度为100%,此时,left和right部分会跳到下一行;
  • 通过设置margin-left为负值让left和right部分回到与center部分同一行;
  • center部分增加一个内层div,并设margin: 0 200px;

③ 缺点

多加一层 dom 树节点,增加渲染树生成的计算量

3.两种布局实现方式对比:

  • 两种布局方式都是把主列放在文档流最前面,使主列优先加载。
  • 两种布局方式在实现上也有相同之处,都是让三列浮动,然后通过负外边距形成三列布局。
  • 两种布局方式的不同之处在于如何处理中间主列的位置:
    圣杯布局是利用父容器的左、右内边距+两个从列相对定位
    双飞翼布局是把主列嵌套在一个新的父级块中利用主列的左、右外边距进行布局调整

四、等高布局

等高布局是指子元素在父元素中高度相等的布局方式。接下来我们介绍常见几种实现方式:

1.利用正padding+负margin

我们通过等布局便可解决圣杯布局的第二点缺点,因为背景是在 padding 区域显示的,设置一个大数值的 padding-bottom,再设置相同数值的负的 margin-bottom,并在所有列外面加上一个容器,并设置 overflow:hidden 把溢出背景切掉。这种可能实现多列等高布局,并且也能实现列与列之间分隔线效果,结构简单,兼容所有浏览器。新增代码如下:

      .center,
      .left,
      .right {
        padding-bottom: 10000px;
        margin-bottom: -10000px;
      }
      .container {
        padding-left: 220px;
        padding-right: 220px;
        overflow: hidden;//把溢出背景切掉
      }

image

2.利用背景图片

这种方法是我们实现等高列最早使用的一种方法,就是使用背景图片,在列的父元素上使用这个背景图进行Y轴的铺放,从而实现一种等高列的假象。实现方法简单,兼容性强,不需要太多的css样式就可以轻松实现,但此方法不适合流体布局等高列的布局。

在制作样式之前需要一张类似下面的背景图:

<div class=”container clearfix”>
    <div class=”left”></div>
    <div  class=”content”></div>
    <div class=”right”></div>
</div>
.container {
  background: url("column.png") repeat-y;
  width: 960px;
  margin: 0 auto;
}
.left {
  float: left;
  width: 220px;
}
.content {
  float: left;
  width: 480px;
}
.right {
  float: left;
  width: 220px;
}

3.模仿表格布局

这是一种非常简单,易于实现的方法。不过兼容性不好,在ie6-7无法正常运行。

   <div class="container table">
      <div class="containerInner tableRow">
        <div class="column tableCell cell1">
          <div class="left aside">
            ....
          </div>
        </div>
        <div class="column tableCell cell2">
          <div class="content section">
            ...
          </div>
        </div>
        <div class="column tableCell cell3">
          <div class="right aside">
            ...
          </div>
        </div>
      </div>
    </div>
.table {
  width: auto;
  min-width: 1000px;
  margin: 0 auto;
  padding: 0;
  display: table;
}
.tableRow {
  display: table-row;
}
.tableCell {
  display: table-cell;
  width: 33%;
}
.cell1 {
  background: #f00;
  height: 800px;
}
.cell2 {
  background: #0f0;
}
.cell3 {
  background: #00f;
}

4.使用边框和定位

这种方法是使用边框和绝对定位来实现一个假的高度相等列的效果。结构简单,兼容各浏览器,容易掌握。假设你需要实现一个两列等高布局,侧栏高度要和主内容高度相等。

#wrapper {
  width: 960px;
  margin: 0 auto;
}
#mainContent {
  border-right: 220px solid #dfdfdf;
  position: absolute;
  width: 740px;
  height: 800px;  
  background: green;
}
#sidebar {
  background: #dfdfdf;
  margin-left: 740px;
  position: absolute;
  height: 800px;
  width: 220px;
}
<div id="wrapper">
    <div id="mainContent">...</div>
    <div id="sidebar">...</div>
</div>

五、粘连布局

1.特点

  • 有一块内容<main>,当<main>的高康足够长的时候,紧跟在<main>后面的元素<footer>会跟在<main>元素的后面。
  • <main>元素比较短的时候(比如小于屏幕的高度),我们期望这个<footer>元素能够“粘连”在屏幕的底部

具体代码如下:

    <div id="wrap">
      <div class="main">
        main <br />
        main <br />
        main <br />
      </div>
    </div>
    <div id="footer">footer</div>
   * {
        margin: 0;
        padding: 0;
      }
      html,
      body {
        height: 100%;//高度一层层继承下来
      }
      #wrap {
        min-height: 100%;
        background: pink;
        text-align: center;
        overflow: hidden;
      }
      #wrap .main {
        padding-bottom: 50px;
      }
      #footer {
        height: 50px;
        line-height: 50px;
        background: deeppink;
        text-align: center;
        margin-top: -50px;
      }

2.实现步骤

(1)footer必须是一个独立的结构,与wrap没有任何嵌套关系

(2)wrap区域的高度通过设置min-height,变为视口高度

(3)footer要使用margin为负来确定自己的位置

(4)在main区域需要设置 padding-bottom。这也是为了防止负 margin 导致 footer 覆盖任何实际内容。

于2019.1.2重新修改,如果觉得文章对你有些许帮助,欢迎在我的GitHub博客点赞和关注,感激不尽!

参考文章

写技术博客那点事

前言

写文章是一个短期收益少,长期收益很大的一件事情,人们总是高估短期收益,低估长期收益。往往是很多人坚持不下来,特别是写文章的初期,刚写完文章没有人阅读会有一种挫败感,影响了后期创作。

从某种意义上说,博客是我最好的学习笔记和个人名片。在IT行业内,技术博客是了解一个开发者最好的方式之一,特别是当你没有一张足够分量的文凭或者一段出彩的工作经历时,你就应该沉下心来好好打磨自己技术,打造自己的博客。往者不可谏,来者犹可追。从现在开始,养成写技术博客的习惯,或许可以在你的职业生涯发挥着不可忽略的作用。

作为一个一年多工作经验的前端小白,本文并不是想炫耀自己在写博客这件事上,有多么的出色,至少我从来不觉得!相反我想以我的切身体会来分享一些诸如如何去写技术博客,如何推广自己文章,以及如何打造个人品牌的一些事。

一、写技术博客的收获

仔细算来,我坚持写博客已经有15个月了,目前在掘金社区上,发表文章50篇,阅读量11w+,点赞数6000+,在segmentfault社区上发表文章39篇,阅读量5w+,点赞数3800+,Github博客上发表文章50篇,800+star,150+watch,文章也曾被奇舞周刊、前端大全、前端之巅等知名公众号转载过,这里并不是为炫耀这点微不足道的成绩,只是想勉励和我一样默默坚持付出的朋友们----功不唐捐。

刚开始写文章和大部分人一样,最主要的目的有两个:第一,记录自己阶段性的学习成果;第二,想多结识一些志同道合的人。不管是以什么样的目的开始的,只要坚持写下去,就会渐渐感受到这件事所给你带来的好处:

1.检验自己学习成果,加深对技术的理解

如果一件事情不能用通俗、简单的语言说出来或者写出来,证明你并没有全面地掌握或者掌握的深度不够。只有通过知识共享才能更好地发现自己在知识理解和掌握上的不足。

2.提高文字的表达能力

随着我的阅读量的增多,我对自己的文章的要求就越来越高,无论是文章质量还是给读者的阅读体验上我都会花费心思去琢磨,经常在写作过程反复问自己,诸如“如何可以让读者更容易吸收我所写的东西?”“如何表达更让人印象深刻?”等等,此外我会借助思维导图工具xmind和生成动态图工具ScreenToGif,因为图像比文字记忆牢固,视觉化记忆可以充分发挥优势。

3.结交更多优秀的人,获得更多内推机会

乐莫乐兮新相知,通过文章结交新朋友何尝不是快乐的事!况且分享知识的同时还能帮助到别人,也会给人带来成就感!
回复读者留言的时候,可以通过这种方式和五湖四海的技术人在一起交流。素未谋面,但神交已久这是件很酷的事!

4.打造个人品牌,提高业内影响力

自媒体时代,信息高度流通,是个段子手都可能收获一份事业。通过知识共享可以帮助自己找到更有价值的朋友和合作伙伴,从而协助你打造个人品牌。“让别人知道你知道”,建立你的个人品牌,让别人更愿意跟你合作——传递和共享自己的知识是最简单的方法,通过知识共享,可以促进人们对你的了解和信任,为个人发展开拓新的天地。当你有了足够的名气时,你会收到类似网站广告,(付费)演讲邀请,录制视频,出书等邀请和机会。

5.得到报酬(稿费/打赏/广告费等)

坚持写优质的文章会带来稿费或者是打赏,虽说可能都是些小钱,但这也是正向反馈一种形式,会刺激你持续输出。等到博客帮你塑造了个人品牌后,这个东西会成为和你日常所做的工作同样有价值,甚至超过你的本职工作。

二、如何写出一篇受欢迎的技术文章?

1.全力找一个好标题

标题可以说是一个门户的作用,是让读者了解到文章的内容主旨的一个标识。如果标题起得不好,就很难引起别人的兴趣,更不必说高阅读量了,即使是文章的内容质量很好。

标题确定下来后,要多查找些资料,因为不同的人可能对同一个事物有不同的看法,所以通过阅读和整理多个人对同一事件的描述可以让自己更深刻,更多角度地理解这个问题,然后再结合自己的理解,就会理解的很快,也比较透彻。

2.明确目标和受众

确定好标题后就需要确定你的目标读者群体。了解你的读者群体是极其重要的一步,他们是怎么样的人,有什么背景知识(或者你希望你的读者群体需要什么知识),这些人需要什么思维方式。这是极其重要的一步,最好是能够把你对读者的要求写在文章的前面,这样对你和读者都有好处。了解目标群体就像产品经理了解目标用户需求,切身体会到了才能做出好的产品。

3.有一个好的开头结尾并且内容翔实

俗话说,巧妇难为无米之炊,事实上要想写出一篇内容翔实的文章,高质量参考文章必不可少!以下罗列一些个人觉得不错前端资料:

  • JavaScript Weekly,这是一份相当有名气的英文周报,整理的文章质量都很高,如果你只想订阅一份周报,那就是它了。

  • 33 Concepts Every JavaScript Developer Should Know,这份资料讲解了 33 个前端开发必须知道的 JS 概念,内容是英文的,网上有中文版。

  • 前端精读周刊,这是一份前端好文集合,每周都会更新,目前已经更新了 84 篇文章。

  • 前端性能清单,这是一份前端性能清单,如果你需要优化一个项目的话,可以根据这份清单一个个来检查优化项。

  • 30-seconds-of-code,30 秒系列,很短的代码片段让你了解一个知识点。

  • 30-seconds-of-css,30 秒系列,很短的代码片段让你了解一个CSS知识点

  • must-watch-javascript,这份资料包含了很多高质量的前端相关视频,值得一看。

  • css-protips,通过这份资料你可以学习到很多 tips 来提高你的 CSS 技能。

  • 奇舞周刊,每周都会整理一份不错的中文文章合集。

如果想了解更多优秀的前端资料,建议收藏下前端英文网站汇总这个网站,收录了国外一些优质的博客及其视频资料。

4.获得反馈并进行迭代

我有一个毫无根据的理论,并且对此深信不疑:好文章都是慢慢打磨出来的。当有一定阅读量后,跟读者互动交流后,就会发现文章不足之处,此时就应该及时修改过来。等过了段时间,写过的文章也应该经常拿出来温习,此时可能能力较之前有所进步,看问题更深刻,觉得不满意的旧文章也应该进行迭代!这样,你的写作能力才能迅速提高!

5.添加最后润色:包装,发布和宣传

三、关于写博客的几个问题

1.什么时候开始

每个人的心里,有多么长的一个清单,这些清单里写着多少美好的事,可是,它们总是被推迟,被搁置,在时间的阁楼上腐烂。

万事开头难,写技术文章也是,很多人会在起步这个阶段逗留很长的时间,你或许有这样的疑问:开发人员技术到什么程度才有资格去开通自己的博客?种一棵树的最好时机是十年前,其次是现在,开通技术博客同样如此。其实我一年前开始写那些文章,现在再回头看,便觉得当时写得很差劲,对于那些不满意的文章,可以等你能力提高了,再删掉重新写。所以非要等到是技术牛人,才有资格写文章,然而恰恰相反,你可以通过写文章来记录自己成长轨迹,分享文章的同时,打造自己个人品牌,何乐而不为?

2.如何开始写?

  • 从模仿和重写开始

对于一个新手想要一开始就写出一篇高质量文章,往往会觉得无从下手。而我的建议是从模仿和重写开始,虽然你可能还没开始写文章,但你一定阅读过那些优秀的博客文章,不妨先模仿那些优秀文章,学习如何给文章起标题的,如何写开场白,如何阐述他们的观点,以及如何总结的。通过模仿和重写,你会渐渐培养起写作的感觉,并且越写越好的。

  • 建议先确定主题,然后再根据主题查找相关资料

根据主题来查阅资料,而不是根据资料来查阅主题。按照主题来阅读,你会发现读的时候不再是老老实实地一本书看完看另一本,而是非常频繁 地从一本书跳到另一本书,从一处资料跳到另一处资料, 从而来获得多个不同的人对同一个主题是如何讲解的。通过对比不同资料,取其精华,去除糟粕,总结成文。

  • 养成思考习惯

刘未鹏《暗时间》中提到:“你走路、买菜、洗脸洗手、坐公交、逛街、出游、吃饭、睡觉,所有这些时间都可以称为‘暗时间’。 我们可以充分利用这些时间进行思考、反刍和消化平时看和读的东西, 让我们的认识脱离照本宣科的层面。”

当思考和总结成为习惯之后,诉诸文字以及借助书写来进一步思考就变成了一件自然而然的事情, 就变成了一件“因为你在思考和总结从而必须书写下来”的事情,博客就变成了副产品。

3.写博客挺花时间,如何平衡工作和学习?

写一篇高质量的文章确实很花功夫,为了可以让读者有所收获,需要从内容,措辞,配图,排版等多个方面下功夫,而这些地方都是需要花时间的。

其实我的大部分的博客的初稿就是我的学习笔记,这主要包括看视频学习或者看书做的笔记,有了这样的初稿,写博客已经成功了一半。接下来你要做的事,就是如何将你所掌握的知识进行深度和广度的延申,最后通俗易懂地讲给别人听。所以如果你不坚持学习,很难持续高产。

程序员可能是加班相对严重的一种职业。对于有工作的上班族来说,写博客就要善于利用小块时间,也就是《奇特的一生》中所说的“时间下脚料”,比如利用中午休息时间,根据主题搜索一些资料,做好前期准备。我一般一个礼拜时间才写一篇文章,从来不是花个几个小时一气呵成,而是有时间就写一部分,写博客对我来说是挺享受一个过程。

4.在哪个平台写

现在可以写文章的地方太多了,所以选择会有很多,反而让人迷茫,哪里才是技术人员真正的聚集地。我发现很多爱写文章的技术人,首先会在一个平台去写文章,慢慢的在这个平台积累了一些名气之后,就会被各种编辑邀请或者是出于推广的目的,会在各个热门的平台下去同步自己的文章,然后引流到自己的独立博客,最后开通自己的公众号。

如果是写作新手,我建议先重点选择一个平台,了解这个平台的规则和技术氛围,如果各方面都和自己比较贴切,那就选择它写作发表文章,平台积累了一些名气之后,再选择开通自己的博客。至于选择什么样的平台,个人觉得最好选择支持 Markdown语法的平台,这样你就不必花心思调整文章的格式,在IT技术平台中,个人最喜欢掘金和SegmentFault社区,两者的技术氛围都很不错,只是前者比后者浏览量来的多!

  • 掘金
  • SegmentFault
  • Github issues

5.如何推广自己的文章?

好酒也怕巷子深,如今写技术文章而且写的不错的人太多了,如果不太注重推广的话,文章很快淹没在知识的海洋中,阅读量很小,继而影响写文章的动力和激情。就我个人而言,写文章最大的乐趣就是和读者互动交流以及他们带来的反馈,如果阅读量少了,我就会质疑自己文章是否对别人到底有没有帮助,文章质量是不是很差?

到底如何推广呢,第一需要研究清楚平台的规则,熟悉规则,才能玩转规则!接下来以SegmentFault社区为例,你应该仔细研究我们进一步改善了首页的推送这篇文章,里面详细介绍了如何上首页的一些条件:

当满足上述一个条件的时候,这篇文章就会被推送到所有订阅了相关标签用户的时间线上了。这又引出了另一个问题,那就是在撰写文章时对标签的使用,我们建议为了扩大你潜在的文章阅读人群,第一个标签最好选择一个比较广泛的技术标签。

6.为什么应该采用博客的形式而不是云笔记

上文我们提到,博客源于自己的笔记,这里可能会有不少人有疑惑为啥不直接在印象笔记来记录笔记,而还要花心思来写博客?我认为采用云笔记的局限性如下:

  • 记录在笔记,更多的是为自己看,无法进行深入理解和研究
  • 无法分享给广大同行,不利于发现自己看待问题的不足和更有的见解
  • 无法获得类似知识变现,名气提升等益处,更不用说打造个人品牌了

四、2018年文章列表(50篇)

过去一年,坚持写博客,陆陆续续写了五十多篇,不乏有些不错的文章,被一些前端知名公众号转载过,得到过别人的认可和赞誉,这让我在写博客这条路一发不可收拾,越陷越深!现将所有文章归纳如下,希望对你们有些许帮助。

Vue全家桶

1.从头开始学习Vuex

2.从头开始学习vue-router

3.Vue2.0父子组件间通信

4.Vue2.0自定义指令与实例的属性和方法

5.Vue2.0数据通信详解

6.Vue2.0组件的继承与扩展

页面性能优化

1.页面性能优化办法有哪些

2.懒加载和预加载

3.函数节流和防抖

Javascript

1.深入浅出JavaScript运行机制

2.JavaScript预解释是一种毫无节操的机制

3.JavaScript的数据类型及其检测

4.JavaScript创建对象几种形式

5.细说数组常用遍历的方法

6.浅拷贝与深拷贝

7.深入浅出Javascript闭包

8.你还没搞懂this?

9.原型与原型链详解

10.九种跨域方式实现原理(完整版)

11.Dom事件机制

12.JavaScript常见的六种继承方式

13.JavaScript数据类型转换

14.JavaScript线程机制与事件机制

15.前端模块化详解(完整版)

16.浏览器与Node的事件循环(Event Loop)有何区别?

ES6

1.ES6核心特性

2.ES6数组的扩展--Array.from()和Array.of()

3.异步解决方案--Promise与Await

4.JS 异步编程六种方案

5.ES6迭代器和生成器

CSS

1.实现三栏布局的几种方法

2.深入理解BFC

3.如何清除浮动

4.LESS即学即用

5.如何居中一个元素(终结版)

6.几种常见的CSS布局

7.关于响应式布局,你必须要知道的

Http协议与数据请求

1.关于Http协议,你必须要知道的

2.关于Http协议一些重要概念

3.深入浅出HTTPS工作原理

4.Ajax原理一篇就够了

5.Ajax请求后台数据

6.fetch 如何请求数据

浏览器相关

1.从URL输入到页面展现到底发生什么?

2.浏览器存储

3.深入浅出浏览器渲染原理

4.深入理解浏览器的缓存机制

web安全

1.常见六大Web安全攻防解析

工具

1.如何在线预览 GitHub 项目

实战

1.vue全家桶制作一个精致的美团项目

五、2019年待写文章计划(暂定)

这部分写作计划参考木易杨大神的进阶系列目录,目前暂定以下主题,个人觉得这些都是前端重难点,有必要深入了解。

  • vue深入系列
  • react入门系列
  • node入门系列
  • webpack配置与原理系列
  • 调用堆栈
  • 原型与原型链
  • bind、call、apply的实现原理
  • 高阶函数
  • JavaScript设计模式
  • Promise原理及手写Promise
  • 计算机网络概述
  • TCP/UDP协议
  • http2协议
  • websocket
  • 前端监控
  • 性能优化
  • 骨架屏实现
  • Vue3.0新特性
  • vue与react对比
  • MVVM框架实现原理
  • Diff算法
  • Virtual Dom原理
  • 前端数据结构与算法

六、后记

事实上,理解一件事并不难,难的是如何将一件事通俗分享给别人,并让别人也有所收获。我有个习惯,经常会翻看自己之前写的文章,觉得不满意,便会重新修改,这是我一直秉持的态度:文章的质量要比数量重要得多,每一篇文章,都是像是我的孩子,要认真对待!

上文中提到写文章诸多好处,这里友情提醒大家,千万不要为了写博客而写博客,作为程序员,我个人觉得最重要的还是敲代码的能力,以及工作中解决问题的能力,千万不要本末倒置,捡了芝麻丢了西瓜。

最后打个小广告,如果你想了解前端的重难点、常考面试题和新知识等等,可以关注我的公众号「前端工匠」,一周两篇原创优质文章,旨在用通俗语言带你入门!

参考文章及书籍

ES6迭代器和生成器

一、迭代器

JavaScript 原有的表示“集合”的数据结构,主要是数组(Array)和对象(Object),ES6 又添加了Map和Set。这样就需要一种统一的接口机制,来处理所有不同的数据结构。遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)

1.Iterator的作用:

  • 为各种数据结构,提供一个统一的、简便的访问接口;
  • 使得数据结构的成员能够按某种次序排列
  • ES6创造了一种新的遍历命令for...of循环,Iterator接口主要供for...of消费。

2.原生具备iterator接口的数据(可用for of遍历)

  • Array
  • set容器
  • map容器
  • String
  • 函数的 arguments 对象
  • NodeList 对象
let arr3 = [1, 2, 'kobe', true];
for(let i of arr3){
   console.log(i); // 1 2 kobe true
}
let str = 'abcd';
for(let item of str){
   console.log(item); // a b c d
}   
function fun() {
    for (let i of arguments) {
       console.log(i) // 1 4 5
    }
}
fun(1, 4, 5)
var engines = new Set(["Gecko", "Trident", "Webkit", "Webkit"]);
for (var e of engines) {
  console.log(e);
}
// Gecko
// Trident
// Webkit    

3.迭代器的工作原理

  • 创建一个指针对象,指向数据结构的起始位置。
  • 第一次调用next方法,指针自动指向数据结构的第一个成员
  • 接下来不断调用next方法,指针会一直往后移动,直到指向最后一个成员
  • 每调用next方法返回的是一个包含value和done的对象,{value: 当前成员的值,done: 布尔值}
    • value表示当前成员的值,done对应的布尔值表示当前的数据的结构是否遍历结束。
    • 当遍历结束的时候返回的value值是undefined,done值为true

4.手写一个迭代器

    function myIterator(arr) {
        let nextIndex = 0
        return {
          next: function() {
            return nextIndex < arr.length
              ? { value: arr[nextIndex++], done: false }
              : { value: undefined, done: true }
          }
        }
      }
      let arr = [1, 4, 'ads']// 准备一个数据
      let iteratorObj = myIterator(arr)
      console.log(iteratorObj.next()) // 所有的迭代器对象都拥有next()方法,会返回一个结果对象
      console.log(iteratorObj.next())
      console.log(iteratorObj.next())
      console.log(iteratorObj.next())

5.注意点

① for of循环不支持遍历普通对象

var obj = { a: 2, b: 3 }
   for (let i of obj) {
     console.log(i) // Uncaught TypeError: obj is not iterable
}

对象的Symbol.iterator属性,指向该对象的默认遍历器方法。当使用for of去遍历某一个数据结构的时候,首先去找Symbol.iterator,找到了就去遍历,没有找到的话不能遍历,提示Uncaught TypeError: XXX is not iterable

② 当使用扩展运算符(...)或者对数组和 Set 结构进行解构赋值时,会默认调用Symbol.iterator方法

let arr1 = [1,3]
let arr2 = [2,3,4,5]
arr2 = [1,...arr2,6]
console.log(arr2) // [1, 2, 3, 4, 5, 6]

二、生成器

1.概念

  • Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同
  • 语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态
  • Generator 函数除了状态机,还是一个遍历器对象生成函数
  • 可暂停函数(惰性求值), yield可暂停,next方法可启动。每次返回的是yield后的表达式结果

2.特点

  • function关键字与函数名之间有一个星号;
  • 函数体内部使用yield表达式,定义不同的内部状态
 function* generatorExample(){
    console.log("开始执行")
    yield 'hello';  
    yield 'generator'; 
 }
// generatorExample() 
// 这种调用方法Generator 函数并不会执行
let MG = generatorExample() // 返回指针对象
MG.next() //开始执行  {value: "hello", done: false}

Generator 函数是分段执行的,调用next方法函数内部逻辑开始执行,遇到yield表达式停止,返回{value: yield后的表达式结果/undefined, done: false/true},再次调用next方法会从上一次停止时的yield处开始,直到最后。

function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}
var hw = helloWorldGenerator();
hw.next()// { value: 'hello', done: false }
hw.next()// { value: 'world', done: false }
hw.next()// { value: 'ending', done: true }
hw.next()// { value: undefined, done: true }

第一次调用,Generator 函数开始执行,直到遇到第一个yield表达式为止。next方法返回一个对象,它的value属性就是当前yield表达式的值hello,done属性的值false,表示遍历还没有结束。

第二次调用,Generator 函数从上次yield表达式停下的地方,一直执行到下一个yield表达式。next方法返回的对象的value属性就是当前yield表达式的值world,done属性的值false,表示遍历还没有结束。

第三次调用,Generator 函数从上次yield表达式停下的地方,一直执行到return语句(如果没有return语句,就执行到函数结束)。next方法返回的对象的value属性,就是紧跟在return语句后面的表达式的值(如果没有return语句,则value属性的值为undefined),done属性的值true,表示遍历已经结束。

第四次调用,此时 Generator 函数已经运行完毕,next方法返回对象的value属性为undefined,done属性为true。以后再调用next方法,返回的都是这个值。

3.next传递参数

yield表达式本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。

function* generatorExample () {
  console.log('开始执行')
  let result = yield 'hello'
  console.log(result)
  yield 'generator'
}
let MG = generatorExample()
MG.next()
MG.next()
// 开始执行
// undefined
// {value: "generator", done: false}

没有传值时result默认是undefined,接下来我们向第二个next传递一个参数,看下输出结果是啥?

function* generatorExample () {
  console.log('开始执行')
  let result = yield 'hello'
  console.log(result)
  yield 'generator'
}
let MG = generatorExample()
MG.next()
MG.next(11)
// 开始执行
// 11
// {value: "generator", done: false}

4.与 Iterator 接口的关系

我们上文中提到对象没有iterator接口,用for...of遍历时便会报错。

let obj = { username: 'kobe', age: 39 }
for (let i of obj) {
  console.log(i) //  Uncaught TypeError: obj is not iterable
}

由于 Generator 函数就是遍历器生成函数,因此可以把 Generator 赋值给对象的Symbol.iterator属性,从而使得该对象具有 Iterator 接口

let obj = { username: 'kobe', age: 39 }
obj[Symbol.iterator] = function* myTest() {
  yield 1;
  yield 2;
  yield 3;
};
for (let i of obj) {
  console.log(i) // 1 2 3
}

上面代码中,Generator函数赋值给Symbol.iterator属性,从而使得obj对象具有了 Iterator 接口,可以被for of遍历了。

5.Generator的异步的应用

业务需求:

  • 发送ajax请求获取新闻内容
  • 新闻内容获取成功后再次发送请求,获取对应的新闻评论内容
  • 新闻内容获取失败则不需要再次发送请求。

如何实现(前端核心代码如下):

    function* sendXml() {
      // url为next传参进来的数据
     let url = yield getNews('http://localhost:3000/news?newsId=2');//获取新闻内容
      yield getNews(url);//获取对应的新闻评论内容,只有先获取新闻的数据拼凑成url,才能向后台请求
    }
    function getNews(url) {
      $.get(url, function (data) {
        console.log(data);
        let commentsUrl = data.commentsUrl;
        let url = 'http://localhost:3000' + commentsUrl;
        // 当获取新闻内容成功,发送请求获取对应的评论内容
        // 调用next传参会作为上次暂停是yield的返回值
        sx.next(url);
      })
    }
    let sx = sendXml();// 发送请求获取新闻内容
    sx.next();

如果觉得文章对你有些许帮助,欢迎在我的GitHub博客点赞和关注,感激不尽!

参考文章

ECMAScript 6 入门

进阶 Javascript 生成器

ES6生成器函数generator

从URL输入到页面展现到底发生什么?

前言

打开浏览器从输入网址到网页呈现在大家面前,背后到底发生了什么?经历怎么样的一个过程?先给大家来张总体流程图,具体步骤请看下文分解!
从URL输入到页面展现

总体来说分为以下几个过程:

  • DNS 解析:将域名解析成 IP 地址
  • TCP 连接:TCP 三次握手
  • 发送 HTTP 请求
  • 服务器处理请求并返回 HTTP 报文
  • 浏览器解析渲染页面
  • 断开连接:TCP 四次挥手

一、URL 到底是啥

URL(Uniform Resource Locator),统一资源定位符,用于定位互联网上资源,俗称网址。
比如 http://www.w3school.com.cn/html/index.asp,遵守以下的语法规则:

scheme://host.domain:port/path/filename
各部分解释如下:
scheme - 定义因特网服务的类型。常见的协议有 http、https、ftp、file,其中最常见的类型是 http,而 https 则是进行加密的网络传输。
host - 定义域主机(http 的默认主机是 www)
domain - 定义因特网域名,比如 w3school.com.cn
port - 定义主机上的端口号(http 的默认端口号是 80)
path - 定义服务器上的路径(如果省略,则文档必须位于网站的根目录中)。
filename - 定义文档/资源的名称

二、域名解析(DNS)

在浏览器输入网址后,首先要经过域名解析,因为浏览器并不能直接通过域名找到对应的服务器,而是要通过 IP 地址。大家这里或许会有个疑问----计算机既可以被赋予 IP 地址,也可以被赋予主机名和域名。比如 www.hackr.jp。那怎么不一开始就赋予个 IP 地址?这样就可以省去解析麻烦。我们先来了解下什么是 IP 地址

1.IP 地址

IP 地址是指互联网协议地址,是 IP Address 的缩写。IP 地址是 IP 协议提供的一种统一的地址格式,它为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址的差异。IP 地址是一个 32 位的二进制数,比如 127.0.0.1 为本机 IP。
域名就相当于 IP 地址乔装打扮的伪装者,带着一副面具。它的作用就是便于记忆和沟通的一组服务器的地址。用户通常使用主机名或域名来访问对方的计算机,而不是直接通过 IP 地址访问。因为与 IP 地址的一组纯数字相比,用字母配合数字的表示形式来指定计算机名更符合人类的记忆习惯。但要让计算机去理解名称,相对而言就变得困难了。因为计算机更擅长处理一长串数字。为了解决上述的问题,DNS 服务应运而生。

2.什么是域名解析

DNS 协议提供通过域名查找 IP 地址,或逆向从 IP 地址反查域名的服务。DNS 是一个网络服务器,我们的域名解析简单来说就是在 DNS 上记录一条信息记录

例如 baidu.com  220.114.23.56(服务器外网IP地址)80(服务器端口号)

3. 浏览器如何通过域名去查询 URL 对应的 IP 呢

  • 浏览器缓存:浏览器会按照一定的频率缓存 DNS 记录。
  • 操作系统缓存:如果浏览器缓存中找不到需要的 DNS 记录,那就去操作系统中找。
  • 路由缓存:路由器也有 DNS 缓存。
  • ISP 的 DNS 服务器:ISP 是互联网服务提供商(Internet Service Provider)的简称,ISP 有专门的 DNS 服务器应对 DNS 查询请求。
  • 根服务器:ISP 的 DNS 服务器还找不到的话,它就会向根服务器发出请求,进行递归查询(DNS 服务器先问根域名服务器.com 域名服务器的 IP 地址,然后再问.baidu 域名服务器,依次类推)

DNS解析流程

4. 小结

浏览器通过向 DNS 服务器发送域名,DNS 服务器查询到与域名相对应的 IP 地址,然后返回给浏览器,浏览器再将 IP 地址打在协议上,同时请求参数也会在协议搭载,然后一并发送给对应的服务器。接下来介绍向服务器发送 HTTP 请求阶段,HTTP 请求分为三个部分:TCP 三次握手、http 请求响应信息、关闭 TCP 连接。

三、TCP 三次握手

在客户端发送数据之前会发起 TCP 三次握手用以同步客户端和服务端的序列号和确认号,并交换 TCP 窗口大小信息

1.TCP 三次握手的过程如下:

  • 客户端发送一个带 SYN=1,Seq=X 的数据包到服务器端口(第一次握手,由浏览器发起,告诉服务器我要发送请求了)

  • 服务器发回一个带 SYN=1, ACK=X+1, Seq=Y 的响应包以示传达确认信息(第二次握手,由服务器发起,告诉浏览器我准备接受了,你赶紧发送吧)

  • 客户端再回传一个带 ACK=Y+1, Seq=Z 的数据包,代表“握手结束”(第三次握手,由浏览器发送,告诉服务器,我马上就发了,准备接受吧)

2.为啥需要三次握手

谢希仁著《计算机网络》中讲“三次握手”的目的是“为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误”。

四、发送 HTTP 请求

TCP 三次握手结束后,开始发送 HTTP 请求报文
请求报文由请求行(request line)、请求头(header)、请求体三个部分组成,如下图所示:

1.请求行包含请求方法、URL、协议版本

  • 请求方法包含 8 种:GET、POST、PUT、DELETE、PATCH、HEAD、OPTIONS、TRACE。
  • URL 即请求地址,由 <协议>://<主机>:<端口>/<路径>?<参数> 组成
  • 协议版本即 http 版本号
POST  /chapter17/user.html HTTP/1.1

以上代码中“POST”代表请求方法,“/chapter17/user.html”表示 URL,“HTTP/1.1”代表协议和协议的版本。现在比较流行的是 Http1.1 版本

2.请求头包含请求的附加信息,由关键字/值对组成,每行一对,关键字和值用英文冒号“:”分隔。

请求头部通知服务器有关于客户端请求的信息。它包含许多有关的客户端环境和请求正文的有用信息。其中比如:Host,表示主机名,虚拟主机;Connection,HTTP/1.1 增加的,使用 keepalive,即持久连接,一个连接可以发多个请求;User-Agent,请求发出者,兼容性以及定制化需求。

3.请求体,可以承载多个请求参数的数据,包含回车符、换行符和请求数据,并不是所有请求都具有请求数据。

name=tom&password=1234&realName=tomson

上面代码,承载着 name、password、realName 三个请求参数。

五、服务器处理请求并返回 HTTP 报文

1. 服务器

服务器是网络环境中的高性能计算机,它侦听网络上的其他计算机(客户机)提交的服务请求,并提供相应的服务,比如网页服务、文件下载服务、邮件服务、视频服务。而客户端主要的功能是浏览网页、看视频、听音乐等等,两者截然不同。 每台服务器上都会安装处理请求的应用——web server。常见的 web server 产品有 apache、nginx、IIS 或 Lighttpd 等。
web server 担任管控的角色,对于不同用户发送的请求,会结合配置文件,把不同请求委托给服务器上处理相应请求的程序进行处理(例如 CGI 脚本,JSP 脚本,servlets,ASP 脚本,服务器端 JavaScript,或者一些其它的服务器端技术等),然后返回后台程序处理产生的结果作为响应。
服务器和客户端区别.png

2.MVC 后台处理阶段

后台开发现在有很多框架,但大部分都还是按照 MVC 设计模式进行搭建的。
MVC 是一个设计模式,将应用程序分成三个核心部件:模型(model)-- 视图(view)--控制器(controller),它们各自处理自己的任务,实现输入、处理和输出的分离。
MVC架构

1、视图(view)

它是提供给用户的操作界面,是程序的外壳。

2、模型(model)

**模型主要负责数据交互。**在 MVC 的三个部件中,模型拥有最多的处理任务。一个模型能为多个视图提供数据。

3、控制器(controller)

它负责根据用户从"视图层"输入的指令,选取"模型层"中的数据,然后对其进行相应的操作,产生最终结果。控制器属于管理者角色,从视图接收请求并决定调用哪个模型构件去处理请求,然后再确定用哪个视图来显示模型处理返回的数据。
这三层是紧密联系在一起的,但又是互相独立的,每一层内部的变化不影响其他层。每一层都对外提供接口(Interface),供上面一层调用。
至于这一阶段发生什么?简而言之,首先浏览器发送过来的请求先经过控制器,控制器进行逻辑处理和请求分发,接着会调用模型,这一阶段模型会获取 redis db 以及 MySQL 的数据,获取数据后将渲染好的页面,响应信息会以响应报文的形式返回给客户端,最后浏览器通过渲染引擎将网页呈现在用户面前。

3.http 响应报文

响应报文由响应行(request line)、响应头部(header)、响应主体三个部分组成。如下图所示:

(1) 响应行包含:协议版本,状态码,状态码描述

状态码规则如下:
1xx:指示信息--表示请求已接收,继续处理。
2xx:成功--表示请求已被成功接收、理解、接受。
3xx:重定向--要完成请求必须进行更进一步的操作。
4xx:客户端错误--请求有语法错误或请求无法实现。
5xx:服务器端错误--服务器未能实现合法的请求。

(2) 响应头部包含响应报文的附加信息,由 名/值 对组成

(3) 响应主体包含回车符、换行符和响应返回数据,并不是所有响应报文都有响应数据

六、浏览器解析渲染页面

浏览器拿到响应文本 HTML 后,接下来介绍下浏览器渲染机制

浏览器解析渲染页面分为一下五个步骤:

  • 根据 HTML 解析出 DOM 树
  • 根据 CSS 解析生成 CSS 规则树
  • 结合 DOM 树和 CSS 规则树,生成渲染树
  • 根据渲染树计算每一个节点的信息
  • 根据计算好的信息绘制页面

1.根据 HTML 解析 DOM 树

  • 根据 HTML 的内容,将标签按照结构解析成为 DOM 树,DOM 树解析的过程是一个深度优先遍历。即先构建当前节点的所有子节点,再构建下一个兄弟节点。
  • 在读取 HTML 文档,构建 DOM 树的过程中,若遇到 script 标签,则 DOM 树的构建会暂停,直至脚本执行完毕。

2.根据 CSS 解析生成 CSS 规则树

  • 解析 CSS 规则树时 js 执行将暂停,直至 CSS 规则树就绪。
  • 浏览器在 CSS 规则树生成之前不会进行渲染。

3.结合 DOM 树和 CSS 规则树,生成渲染树

  • DOM 树和 CSS 规则树全部准备好了以后,浏览器才会开始构建渲染树。
  • 精简 CSS 并可以加快 CSS 规则树的构建,从而加快页面相应速度。

4.根据渲染树计算每一个节点的信息(布局)

  • 布局:通过渲染树中渲染对象的信息,计算出每一个渲染对象的位置和尺寸
  • 回流:在布局完成后,发现了某个部分发生了变化影响了布局,那就需要倒回去重新渲染。

5.根据计算好的信息绘制页面

  • 绘制阶段,系统会遍历呈现树,并调用呈现器的“paint”方法,将呈现器的内容显示在屏幕上。
  • 重绘:某个元素的背景颜色,文字颜色等,不影响元素周围或内部布局的属性,将只会引起浏览器的重绘。
  • 回流:某个元素的尺寸发生了变化,则需重新计算渲染树,重新渲染。

七、断开连接

当数据传送完毕,需要断开 tcp 连接,此时发起 tcp 四次挥手

  • 发起方向被动方发送报文,Fin、Ack、Seq,表示已经没有数据传输了。并进入 FIN_WAIT_1 状态。(第一次挥手:由浏览器发起的,发送给服务器,我请求报文发送完了,你准备关闭吧)
  • 被动方发送报文,Ack、Seq,表示同意关闭请求。此时主机发起方进入 FIN_WAIT_2 状态。(第二次挥手:由服务器发起的,告诉浏览器,我请求报文接受完了,我准备关闭了,你也准备吧)
  • 被动方向发起方发送报文段,Fin、Ack、Seq,请求关闭连接。并进入 LAST_ACK 状态。(第三次挥手:由服务器发起,告诉浏览器,我响应报文发送完了,你准备关闭吧)
  • 发起方向被动方发送报文段,Ack、Seq。然后进入等待 TIME_WAIT 状态。被动方收到发起方的报文段以后关闭连接。发起方等待一定时间未收到回复,则正常关闭。(第四次挥手:由浏览器发起,告诉服务器,我响应报文接受完了,我准备关闭了,你也准备吧)

参考文章

原型与原型链详解

前言

与大部分面向对象语言不同,ES6之前并没有引入类(class)的概念,JavaScript并非通过类而是直接通过构造函数来创建实例。在介绍原型和原型链之前,我们有必要先复习一下构造函数的知识。

一、构造函数

构造函数模式的目的就是为了创建一个自定义类,并且创建这个类的实例。构造函数模式中拥有了类和实例的概念,并且实例和实例之间是相互独立的,即实例识别。

构造函数就是一个普通的函数,创建方式和普通函数没有区别,不同的是构造函数习惯上首字母大写。另外就是调用方式的不同,普通函数是直接调用,而构造函数需要使用new关键字来调用

	function Person(name, age, gender) {
		this.name = name
		this.age = age
		this.gender = gender
		this.sayName = function () {
			alert(this.name);
		}
	}
	var per = new Person("孙悟空", 18, "男");
	function Dog(name, age, gender) {
		this.name = name
		this.age = age
		this.gender = gender
	}
	var dog = new Dog("旺财", 4, "雄")
	console.log(per);//当我们直接在页面中打印一个对象时,事件上是输出的对象的toString()方法的返回值
	console.log(dog);

每创建一个Person构造函数,在Person构造函数中,为每一个对象都添加了一个sayName方法,也就是说构造函数每执行一次就会创建一个新的sayName方法。这样就导致了构造函数执行一次就会创建一个新的方法,执行10000次就会创建10000个新的方法,而10000个方法都是一摸一样的,为什么不把这个方法单独放到一个地方,并让所有的实例都可以访问到呢?这就需要原型(prototype)

二、原型

在JavaScript中,每当定义一个函数数据类型(普通函数、类)时候,都会天生自带一个prototype属性,这个属性指向函数的原型对象,并且这个属性是一个对象数据类型的值。

让我们用一张图表示构造函数和实例原型之间的关系:


原型对象就相当于一个公共的区域,所有同一个类的实例都可以访问到这个原型对象,我们可以将对象**有的内容,统一设置到原型对象中。

三、原型链

1.__proto__constructor

每一个对象数据类型(普通的对象、实例、prototype......)也天生自带一个属性__proto__,属性值是当前实例所属类的原型(prototype)。原型对象中有一个属性constructor, 它指向函数对象。

    function Person() {}
    var person = new Person()
    console.log(person.__proto__ === Person.prototype)//true
    console.log(Person.prototype.constructor===Person)//true
    //顺便学习一个ES5的方法,可以获得对象的原型
    console.log(Object.getPrototypeOf(person) === Person.prototype) // true

2.何为原型链

在JavaScript中万物都是对象,对象和对象之间也有关系,并不是孤立存在的。对象之间的继承关系,在JavaScript中是通过prototype对象指向父类对象,直到指向Object对象为止,这样就形成了一个原型指向的链条,专业术语称之为原型链

举例说明:person → Person → Object ,普通人继承人类,人类继承对象类

当我们访问对象的一个属性或方法时,它会先在对象自身中寻找,如果有则直接使用,如果没有则会去原型对象中寻找,如果找到则直接使用。如果没有则去原型的原型中寻找,直到找到Object对象的原型,Object对象的原型没有原型,如果在Object原型中依然没有找到,则返回undefined。

我们可以使用对象的hasOwnProperty()来检查对象自身中是否含有该属性;使用in检查对象中是否含有某个属性时,如果对象中没有但是原型中有,也会返回true

	function Person() {}
	Person.prototype.a = 123;
	Person.prototype.sayHello = function () {
	  alert("hello");
	};
	var person = new Person()
	console.log(person.a)//123
	console.log(person.hasOwnProperty('a'));//false
	console.log('a'in person)//true

person实例中没有a这个属性,从 person 对象中找不到 a 属性就会从 person 的原型也就是 person.__proto__ ,也就是 Person.prototype中查找,很幸运地得到a的值为123。那假如 person.__proto__中也没有该属性,又该如何查找?

当读取实例的属性时,如果找不到,就会查找与对象关联的原型中的属性,如果还查不到,就去找原型的原型,一直找到最顶层Object为止。Object是JS中所有对象数据类型的基类(最顶层的类)在Object.prototype上没有__proto__这个属性。

console.log(Object.prototype.__proto__ === null) // true


如果觉得文章对你有些许帮助,欢迎在我的GitHub博客点赞和关注,感激不尽!

参考文章

最详尽的 JS 原型与原型链终极详解,没有「可能是」。(一)

JavaScript深入之从原型到原型链

深入理解BFC

一、什么是BFC

Formatting context 是 W3C CSS2.1 规范中的一个概念。它是页面中的一块渲染区域,并且有一套渲染规则,它决定了其子元素将如何定位,以及和其他元素的关系和相互作用。最常见的 Formatting context 有 Block fomatting context (简称BFC)和 Inline formatting context (简称IFC)。Block formatting context直译为"块级格式化上下文"。它是一个独立的渲染区域,只有Block-level box参与, 它规定了内部的Block-level Box如何布局,并且与这个区域外部毫不相干通俗地讲,BFC是一个容器,用于管理块级元素。

二、如何创建BFC

  • float为 left|right
  • overflow为 hidden|auto|scroll
  • display为 table-cell|table-caption|inline-block|inline-flex|flex
  • position为 absolute|fixed
  • 根元素

三、BFC布局规则:

  • 内部的Box会在垂直方向,一个接一个地放置(即块级元素独占一行)。
  • BFC的区域不会与float box重叠(利用这点可以实现自适应两栏布局)。
  • 内部的Box垂直方向的距离由margin决定。属于同一个BFC的两个相邻Box的margin会发生重叠(margin重叠三个条件:同属于一个BFC;相邻;块级元素)。
  • 计算BFC的高度时,浮动元素也参与计算。(清除浮动 haslayout)
  • BFC就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素。反之也如此。

四、 BFC有哪些特性

特性1:BFC会阻止垂直外边距折叠

按照BFC的定义,只有同属于一个BFC时,两个元素才有可能发生垂直margin的重叠,这个包括相邻元素或者嵌套元素,只要他们之间没有阻挡(比如边框、非空内容、padding等)就会发生margin重叠。

①相邻兄弟元素margin重叠问题

<style>
p{
        color: #fff;
        background: #888;
        width: 200px;
        line-height: 100px;
        text-align:center;
        margin: 100px;
  }
</style>
<body>
    <p>ABC</p>
    <p>abc</p>
</body>

相邻兄弟元素margin重叠问题
上面例中两个P元素之间距离本该为200px,然而实际上只有100px,发生了margin重叠。遇到这种情形,我们如何处理?
只需要在p外面包裹一层容器,并触发该容器生成一个BFC。那么两个P便不属于同一个BFC,就不会发生margin重叠了

<style>
p{
        color: #fff;
        background: #888;
        width: 200px;
        line-height: 100px;
        text-align:center;
        margin: 100px;
    }
.wrap{
  overflow:hidden;
}
</style>
<body>
   <p>ABC</p>
  <div class="wrap">
    <p>abc</p>
  </div>
</body>

bug修补后

②父子元素margin重叠问题

<style>
.box{
width:100px;
height:100px;
background:#ccc;
}
.wrap {
  background:yellow;
}
.wrap h1{
  background:pink;
  margin:40px;
}
</style>
<body>
<div class="box">box</div>
<div class="wrap">
  <h1>h1</h1>
</div>
</body>

父子元素margin重叠问题
上图wrap元素与h1元素之间l理论上本该有个40px的上下margin值,然而实际上父子元素并没有存在margin值,与此同时,两个div元素的间距为40px。遇到这种情形,我们如何处理?
处理方法其实有很多,**在wrap元素中添加:overflow:hidden;或者overflow:auto;使其父元素形成一个BFC;也可以在wrap元素中添加border:1px solid;或是padding:1px;**这些都可以有效解决父子元素margin重叠问题。

bug修补后

特性2:BFC不会重叠浮动元素

利用这个特性,我们可以创造自适应两栏布局

<style>
.box1{
  height: 100px;
  width: 100px;
  float: left;
  background: lightblue;
}
.box2{width: 200px;
  height: 200px;
  background: #eee;
}
</style>
<body>
<div class="box1">我是一个左浮动的元素</div>
<div class="box2">喂喂喂!大家不要生气嘛,生气会犯嗔戒的。悟空你也太调皮了,
我跟你说过叫你不要乱扔东西,你怎么又……你看,我还没说完你就把棍子给扔掉了!
月光宝盒是宝物,你把它扔掉会污染环境,要是砸到小朋友怎么办,就算砸不到小朋友,
砸到花花草草也是不对的。</div>
</body>

文字围绕浮动元素排列
上图中,文字围绕着浮动元素排列,不过在这里,这显然不是我们想要的。此时我们可以为.box2元素的样式加上overflow:hidden;使其建立一个BFC,让其内容消除对外界浮动元素的影响

自适应布局
这个方法可以用来实现两列自适应布局,效果不错,此时左边的宽度固定,右边的内容自适应宽度。如果我们改变文字的大小或者左边浮动元素的大小,两栏布局的结构依然没有改变!

特性3:BFC可以包含浮动----清除浮动

我们都知道浮动会脱离文档流,接下来我们看看下面的例子:

<style>
.box1{
  width:100px;
  height:100px;
  float:left;
  border: 1px solid #000;
}
.box2{
  width:100px;
  height:100px;
  float:left;
  border: 1px solid #000;
}
.box{
  background:yellow
}
</style>
<body>
<div class="box">
  <div class="box1"></div>
  <div class="box2"></div>
</div> 
</body>

出现bug

由于容器内两个div元素浮动,脱离了文档流,父容器内容宽度为零(即发生高度塌陷),未能将子元素包裹住。解决这个问题,只需要把把父元素变成一个BFC就行了。常用的办法是给父元素设置overflow:hidden。

修补后
本文于2018.10.14重新修改,希望文章对你有些许帮助,欢迎在我的GitHub博客点赞和关注,感激不尽!

参考文章

【CSS】深入理解BFC原理及应用

10 分钟理解 BFC 原理

ES6数组的扩展--Array.from()和Array.of()

一、 Array.from() : 将伪数组对象或可遍历对象转换为真数组

1.何为伪数组

如果一个对象的所有键名都是正整数或零,并且有length属性,那么这个对象就很像数组,语法上称为“类似数组的对象”(array-like object),即为伪数组。

var obj = {
0: 'a',
1: 'b',
2: 'c',
length: 3
};
obj[0] // 'a'
obj[1] // 'b'
obj.length // 3
obj.push('d') // TypeError: obj.push is not a function

上面代码中,对象obj就是一个类似数组的对象。但是“类似数组的对象”并不是数组,因为它们不具备数组特有的方法。对象obj没有数组的push方法,使用该方法就会报错。

2.有哪些是伪数组

典型的“类似数组的对象”是函数的arguments对象,以及大多数 DOM 元素集,还有字符串。

3.如何转化为真数组

①数组的slice方法可以将“类似数组的对象”变成真正的数组

function doSomething(){
    console.log(arguments)
    var args = Array.prototype.slice.call(arguments);
    args.push("hj")
    console.log(args)
    return args
}
doSomething(1,2,3)

或者你也可以写成:

function doSomething(){
    var args = [].slice.call(arguments);
    return args
}
doSomething(1,2,3)

尽管这种方法,也可以实现将类数组转变为数组的目的,但并不直观。ES6新增Array.from()方法来提供一种明确清晰的方式以解决这方面的需求,更推荐后者的办法。

②Array.from()

<button>测试1</button>
<br>
<button>测试2</button>
<br>
<button>测试3</button>
<br>
<script type="text/javascript">
let btns = document.getElementsByTagName("button")
console.log("btns",btns);//得到一个伪数组
//btns.forEach(item=>console.log(item)) Uncaught TypeError: btns.forEach is not a function
Array.from(btns).forEach(item=>console.log(item))将伪数组转换为数组
</script>


在ES6中,扩展运算符(...)也可以将某些数据结构转为数组。只不过它需要在背后调用遍历器接口Symbol.iterator。值得注意的是如果一个对象没有部署遍历器接口,使用扩展运算符是无法将类似数组对象转换成数组。

function doSomething (){ 
  return [...arguments] 
}
doSomething('a','b','c'); // ["a","b","c"]

4.Array.from()用法

Array.from接受三个参数,但只有input是必须的:

  • input: 你想要转换的类似数组对象和可遍历对象
  • map: 类似于数组的map方法,用来对每个元素进行处理,将处理后的值放入返回的数组
  • context: 绑定map中用到的this

只要是部署了iterator接口的数据结构,Array.from都能将其转为数组:

let arr = Array.from('juejin'); 
console.log(arr); //["j", "u", "e", "j", "i", "n"]

Array.from还可以接受第二个参数,作用类似于数组的map方法,用来对每个元素进行处理,处理后的值放入返回的数组。

Array.from([1, 2, 3], (x) => x * x)// [1, 4, 9]
// 等同于
Array.from([1,2,3].map(x => x * x))

如果map函数里面用到了this关键字,还可以传入Array.from的第三个参数,用来绑定this。

Array.from()可以将各种值转为真正的数组,并且还提供map功能。这实际上意味着,只要有一个原始的数据结构,你就可以先对它的值进行处理,然后转成规范的数组结构,进而就可以使用数量众多的数组方法。

Array.from({ length: 2 }, () => 'jack')// ['jack', 'jack']

二、Array.of(v1, v2, v3) : 将一系列值转换成数组

当调用 new Array( )构造器时,根据传入参数的类型与数量的不同,实际上会导致一些不同的结果, 例如:

let items = new Array(2) ;
console.log(items.length) ; // 2
console.log(items[0]) ; // undefined
console.log(items[1]) ;
let items = new Array(1, 2) ;
console.log(items.length) ; // 2
console.log(items[0]) ; // 1
console.log(items[1]) ; // 2

当使用单个数值参数来调用 Array 构造器时,数组的长度属性会被设置为该参数。 如果使用多个参数(无论是否为数值类型)来调用,这些参数也会成为目标数组的项。数组的这种行为既混乱又有风险,因为有时可能不会留意所传参数的类型。

ES6 引入了Array.of( )方法来解决这个问题。该方法的作用非常类似Array构造器,但在使用单个数值参数的时候并不会导致特殊结果。Array.of( )方法总会创建一个包含所有传入参数的数组,而不管参数的数量与类型

let items = Array.of(1, 2);
console.log(items.length); // 2
console.log(items[0]); // 1
console.log(items[1]); // 2
items = Array.of(2);
console.log(items.length); // 1
console.log(items[0]); // 2

Array.of基本上可以用来替代Array()或newArray(),并且不存在由于参数不同而导致的重载,而且他们的行为非常统一。

参考文章

深入理解ES6

Javascript教程

JavaScript学习笔记:ES6数组方法

JavaScript的数据类型及其检测

一、JavaScript有几种类型的值?

Javascript有两种数据类型,分别是基本数据类型和引用数据类型。其中基本数据类型包括Undefined、Null、Boolean、Number、String、Symbol (ES6新增,表示独一无二的值),而引用数据类型统称为Object对象,主要包括对象、数组和函数。接下来我们分别看下两者的特点。

二、基本数据类型

1.值是不可变的

var name = 'java';
name.toUpperCase(); // 输出 'JAVA'
console.log(name); // 输出  'java'

由此可得,基本数据类型的值是不可改变的

2.存放在栈区

原始数据类型直接存储在栈(stack)中的简单数据段,占据空间小、大小固定,属于被频繁使用数据,所以放入栈中存储。

3.值的比较

var a = 1;
var b = true;
console.log(a == b);    // true
console.log(a === b);   // false

== : 只进行值的比较,会进行数据类型的转换。
=== : 不仅进行值得比较,还要进行数据类型的比较。

三、引用数据类型

1.值是可变的

var a={age:20};
a.age=21;
console.log(a.age)//21

上面代码说明引用类型可以拥有属性和方法,并且是可以动态改变的。

2.同时保存在栈内存和堆内存

引用数据类型存储在堆(heap)中的对象,占据空间大、大小不固定,如果存储在栈中,将会影响程序运行的性能;引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。

3.比较是引用的比较

当从一个变量向另一个变量赋引用类型的值时,同样也会将存储在变量中的对象的值复制一份放到为新变量分配的空间中。

var a={age:20};
var b=a;
b.age=21;
console.log(a.age==b.age)//true

上面我们讲到基本类型和引用类型存储于内存的位置不同,引用类型存储在堆中的对象,与此同时,在栈中存储了指针,而这个指针指向正是堆中实体的起始位置。变量a初始化时,a指针指向对象{age:20}的地址,a赋值给b后,b又指向该对象{age:20}的地址,这两个变量指向了同一个对象。因此,改变其中任何一个变量,都会相互影响。

此时,如果取消某一个变量对于原对象的引用,不会影响到另一个变量。

var a={age:20};
var b=a;
a = 1;
b // {age:20}

上面代码中,a和b指向同一个对象,然后a的值变为1,这时不会对b产生影响,b还是指向原来的那个对象。

四、检验数据类型

1.typeof

typeof返回一个表示数据类型的字符串,返回结果包括:number、boolean、string、symbol、object、undefined、function等7种数据类型,但不能判断null、array等

typeof Symbol(); // symbol 有效
typeof ''; // string 有效
typeof 1; // number 有效
typeof true; //boolean 有效
typeof undefined; //undefined 有效
typeof new Function(); // function 有效
typeof null; //object 无效
typeof [] ; //object 无效
typeof new Date(); //object 无效
typeof new RegExp(); //object 无效

数组和对象返回的都是object,这时就需要使用instanceof来判断

2.instanceof

instanceof 是用来判断A是否为B的实例,表达式为:A instanceof B,如果A是B的实例,则返回true,否则返回false。instanceof 运算符用来测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性。

[] instanceof Array; //true
{} instanceof Object;//true
new Date() instanceof Date;//true
new RegExp() instanceof RegExp//true

关于数组的类型判断,还可以用ES6新增Array.isArray()

Array.isArray([]);   // true

instanceof 三大弊端

  • 对于基本数据类型来说,字面量方式创建出来的结果和实例方式创建的是有一定的区别的
console.log(1 instanceof Number)//false
console.log(new Number(1) instanceof Number)//true

从严格意义上来讲,只有实例创建出来的结果才是标准的对象数据类型值,也是标准的Number这个类的一个实例;对于字面量方式创建出来的结果是基本的数据类型值,不是严谨的实例,但是由于JS的松散特点,导致了可以使用Number.prototype上提供的方法。

  • 只要在当前实例的原型链上,我们用其检测出来的结果都是true。在类的原型继承中,我们最后检测出来的结果未必准确
var arr = [1, 2, 3];
console.log(arr instanceof Array) // true
console.log(arr instanceof Object);  // true
function fn(){}
console.log(fn instanceof Function)// true
console.log(fn instanceof Object)// true
  • 不能检测null 和 undefined

对于特殊的数据类型null和undefined,他们的所属类是Null和Undefined,但是浏览器把这两个类保护起来了,不允许我们在外面访问使用

3.constructor

constructor作用和instanceof非常相似。但constructor检测 Object与instanceof不一样,还可以处理基本数据类型的检测。

var aa=[1,2];
console.log(aa.constructor===Array);//true
console.log(aa.constructor===RegExp);//false
console.log((1).constructor===Number);//true
var reg=/^$/;
console.log(reg.constructor===RegExp);//true
console.log(reg.constructor===Object);//false 

constructor 两大弊端

  • null 和 undefined 是无效的对象,因此是不会有 constructor 存在的,这两种类型的数据需要通过其他方式来判断。

  • 函数的 constructor 是不稳定的,这个主要体现在把类的原型进行重写,在重写的过程中很有可能出现把之前的constructor给覆盖了,这样检测出来的结果就是不准确的

function Fn(){}
Fn.prototype = new Array()
var f = new Fn
console.log(f.constructor)//Array

4.Object.prototype.toString.call()

Object.prototype.toString.call() 最准确最常用的方式。首先获取Object原型上的toString方法,让方法执行,让toString方法中的this指向第一个参数的值。

关于toString重要补充说明

  • 本意是转换为字符串,但是某些toString方法不仅仅是转换为字符串
  • 对于Number、String,Boolean,Array,RegExp、Date、Function原型上的toString方法都是把当前的数据类型转换为字符串的类型(它们的作用仅仅是用来转换为字符串的)
  • Object上的toString并不是用来转换为字符串的。

Object上的toString它的作用是返回当前方法执行的主体(方法中的this)所属类的详细信息即"[object Object]",其中第一个object代表当前实例是对象数据类型的(这个是固定死的),第二个Object代表的是this所属的类是Object。

Object.prototype.toString.call('') ;   // [object String]
Object.prototype.toString.call(1) ;    // [object Number]
Object.prototype.toString.call(true) ; // [object Boolean]
Object.prototype.toString.call(undefined) ; // [object Undefined]
Object.prototype.toString.call(null) ; // [object Null]
Object.prototype.toString.call(new Function()) ; // [object Function]
Object.prototype.toString.call(new Date()) ; // [object Date]
Object.prototype.toString.call([]) ; // [object Array]
Object.prototype.toString.call(new RegExp()) ; // [object RegExp]
Object.prototype.toString.call(new Error()) ; // [object Error]
Object.prototype.toString.call(document) ; // [object HTMLDocument]
Object.prototype.toString.call(window) ; //[object global] window是全局对象global的引用

参考资料

从头开始学习Vuex

一、前言

当我们的应用遇到多个组件共享状态时,会需要多个组件依赖于同一状态抑或是来自不同视图的行为需要变更同一状态。以前的解决办法:

a.将数据以及操作数据的行为都定义在父组件;

b.将数据以及操作数据的行为传递给需要的各个子组件(有可能需要多级传递)

传参的方法对于多层嵌套的组件将会非常繁琐,并且对于兄弟组件间的状态传递无能为力。在搭建下面页面时,你可能会对 vue 组件之间的通信感到崩溃 ,特别是非父子组件之间通信。此时就应该使用vuex,轻松可以搞定组件间通信问题。

组件间通信

二、什么是Vuex

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。这里的关键在于集中式存储管理。简单来说,对 vue 应用中多个组件的共享状态进行集中式的管理(读/写)

三、Vuex的原理是什么

1.简要介绍Vuex原理

Vuex实现了一个单向数据流,在全局拥有一个State存放数据,当组件要更改State中的数据时,必须通过Mutation进行,Mutation同时提供了订阅者模式供外部插件调用获取State数据的更新。而当所有异步操作(常见于调用后端接口异步获取更新数据)或批量的同步操作需要走Action,但Action也是无法直接修改State的,还是需要通过Mutation来修改State的数据。最后,根据State的变化,渲染到视图上。

2.简要介绍各模块在流程中的主要功能:

  • Vue Components:Vue组件。HTML页面上,负责接收用户操作等交互行为,执行dispatch方法触发对应action进行回应。
  • dispatch:操作行为触发方法,是唯一能执行action的方法。
  • actions:操作行为处理模块,由组件中的$store.dispatch('action 名称', data1)来触发。然后由commit()来触发mutation的调用 , 间接更新 state。负责处理Vue Components接收到的所有交互行为。包含同步/异步操作,支持多个同名方法,按照注册的顺序依次触发。向后台API请求的操作就在这个模块中进行,包括触发其他action以及提交mutation的操作。该模块提供了Promise的封装,以支持action的链式触发。
  • commit:状态改变提交操作方法。对mutation进行提交,是唯一能执行mutation的方法。
  • mutations:状态改变操作方法,由actions中的commit('mutation 名称')来触发。是Vuex修改state的唯一推荐方法。该方法只能进行同步操作,且方法名只能全局唯一。操作之中会有一些hook暴露出来,以进行state的监控等。
  • state:页面状态管理容器对象。集中存储Vue components中data对象的零散数据,全局唯一,以进行统一的状态管理。页面显示所需的数据从该对象中进行读取,利用Vue的细粒度数据响应机制来进行高效的状态更新。
  • getters:state对象读取方法。图中没有单独列出该模块,应该被包含在了render中,Vue Components通过该方法读取全局state对象。

四、什么时候使用Vuex

虽然 Vuex 可以帮助我们管理共享状态,但也附带了更多的概念和框架。这需要对短期和长期效益进行权衡。
如果您的应用够简单,您最好不要使用 Vuex,因为使用 Vuex 可能是繁琐冗余的。一个简单的 global event bus 就足够您所需了。但是,如果您需要构建一个中大型单页应用,您很可能会考虑如何更好地在组件外部管理状态,Vuex 将会成为自然而然的选择。

五、Vuex安装(限定开发环境为 vue-cli)

首先要安装vue-cli脚手架,对于大陆用户,建议将npm的注册表源设置为国内的镜像(淘宝镜像),可以大幅提升安装速度。

npm config set registry https://[registry.npm.taobao.org](http://registry.npm.taobao.org/)
npm config get registry//配置后可通过下面方式来验证是否成功
npm install -g cnpm --registry=[https://registry](https://registry/).npm.taobao.org
//cnpm安装脚手架
cnpm install -g vue-cli
vue init webpack my-vue
cd my-vue
cnpm install
cnpm run dev

脚手架安装好后,再安装vuex

cnpm install vuex --save

六、如何使用Vuex

1.如何通过Vue来实现如下效果?

这个小demo很容易用vue实现,核心代码如下:

  <div class="hello">
    <p>click {{count}} times,count is {{evenOrOdd}}</p>
    <button @click="increment">+</button>
    <button @click="decrement">-</button>
    <button @click="incrementIfOdd">increment if odd</button>
    <button @click="incrementAsync">increment async</button>
  </div>
  ......
  export default {
  name: "HelloWorld",
  data() {
    return {
      count: 0
    };
  },
  computed: {
    evenOrOdd() {
      return this.count % 2 === 0 ? "偶数" : "奇数";
    }
  },
  methods: {
    increment() {
      this.count = this.count + 1;
    },
    decrement() {
      this.count = this.count - 1;
    },
    // 只有是奇数才加1
    incrementIfOdd() {
      if (this.count % 2 === 1) {
        this.count = this.count + 1;
      }
    },
    // 过两秒才加1
    incrementAsync() {
      setInterval(() => {
        this.count = this.count + 1;
      }, 2000);
    }
  }
}

2.如何通过Vuex来改造上面代码?

①创建一个store.js文件

import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
    state: {
        count: 0
    },
    mutations: {// 包含了多个直接更新state函数的对象
        INCREMENT(state) {
            state.count = state.count + 1;
        },
        DECREMENT(state) {
            state.count = state.count - 1;
        }
    },
    getters: {   // 当读取属性值时自动调用并返回属性值
        evenOrOdd(state) {
            return state.count % 2 === 0 ? "偶数" : "奇数";
        }
    },
    actions: { // 包含了多个对应事件回调函数的对象
        incrementIfOdd({ commit, state }) { // 带条件的action
            if (state.count % 2 === 1) {
                commit('INCREMENT')
            }
        },
        incrementAsync({ commit }) { //异步的action
            setInterval(() => {
                commit('INCREMENT')
            }, 2000);
        }

    }
})
export default store //用export default 封装代码,让外部可以引用

②在main.js文件中引入store.js文件

import store from './store'
new Vue({
  el: '#app',
  router,
  store,//注册上vuex的store: 所有组件对象都多一个属性$store
  components: { App },
  template: '<App/>'
})

③新建一个模板HelloWorld.vue

<template>
  <div class="hello">
    <p>click {{count}} times,count is {{evenOrOdd}}</p>
    <button @click="increment">+</button>
    <button @click="decrement">-</button>
    <button @click="incrementIfOdd">increment if odd</button>
    <button @click="incrementAsync">increment async</button>
  </div>
</template>
<script>
export default {
  name: "HelloWorld",
  computed: {
    count() {
      return this.$store.state.count;
    },
    evenOrOdd() {
      return this.$store.getters.evenOrOdd;
    }
  },
  methods: {
    increment() {
      this.$store.commit("INCREMENT");
    },
    decrement() {
      this.$store.commit("DECREMENT");
    },
    // 只有是奇数才加1
    incrementIfOdd() {
      this.$store.dispatch("incrementIfOdd"); //触发store中对应的action调用
    },
    // 过两秒才加1
    incrementAsync() {
      this.$store.dispatch("incrementAsync");
    }
  }
};
</script>

由于 store 中的状态是响应式的,当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。在组件中调用 store 中的状态简单到仅需要在计算属性中返回即可。改变store 中的状态的唯一途径就是显式地提交 (commit) mutations。

3.如何通mapState等辅助函数优化上面代码?

import { mapActions, mapGetters, mapState, mapMutations } from "vuex";
...
 computed: {
    ...mapState(["count"]),
    ...mapGetters(["evenOrOdd"])
    }
  methods: {
    ...mapActions(["incrementIfOdd", "incrementAsync"]),
    ...mapMutations(["increment", "decrement"])
    }

有点必须要注意:HelloWorld.vue文件中increment函数名称要跟store.js文件mutations中一致,才可以写成 ...mapMutations(["increment", "decrement"]),同样的道理,incrementIfOdd和incrementAsync也要和store.js文件actions保持一致。

七、使用Vuex的注意点

1.如何在Mutations里传递参数

先store.js文件里给add方法加上一个参数n

  mutations: {
    INCREMENT(state,n) {
      state.count+=n;
    },
    DECREMENT(state){
        state.count--;
    }
  }

然后在HelloWorld.vue里修改按钮的commit( )方法传递的参数

 increment() {
      return this.$store.commit("INCREMENT",2);
    },
 decrement() {
      return this.$store.commit("DECREMENT");
    }

2.如何理解getters

getters从表面是获得的意思,可以把他看作在获取数据之前进行的一种再编辑,相当于对数据的一个过滤和加工。getters就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。

例如:要对store.js文件中的count进行操作,在它输出前,给它加上100。

首先要在store.js里Vuex.Store()里引入getters

getters:{
     count:state=>state.count+=100
  }

然后在HelloWorld.vue中对computed进行配置,在vue 的构造器里边只能有一个computed属性,如果你写多个,只有最后一个computed属性可用,所以要用展开运算符”…”对上节写的computed属性进行一个改造。

 computed: {
   ...mapGetters(["count"])
}

3.actions和mutations区别

actions和上面的Mutations功能基本一样,不同点是,actions是异步的改变state状态,而Mutations是同步改变状态

同步的意义在于这样每一个 mutation 执行完成后都可以对应到一个新的状态(和 reducer 一样),这样 devtools 就可以打个 snapshot 存下来,然后就可以随便 time-travel 了。如果你开着 devtool 调用一个异步的 action,你可以清楚地看到它所调用的 mutation 是何时被记录下来的,并且可以立刻查看它们对应的状态----尤雨溪

ps:如果想访问源代码,请猛戳git地址

如果觉得本文对您有用,请给本文的github加个star,万分感谢

参考文章

vuex官方文档

Vuex 2.0 源码分析

Vuex框架原理与源码分析

TCP和UDP比较

引言

网络协议是每个前端工程师都必须要掌握的知识,TCP/IP 中有两个具有代表性的传输层协议,分别是 TCP 和 UDP,本文将介绍下这两者以及它们之间的区别。

一、TCP/IP 网络模型

计算机与网络设备要相互通信,双方就必须基于相同的方法。比如,如何探测到通信目标、由哪一边先发起通信、使用哪种语言进行通信、怎样结束通信等规则都需要事先确定。不同的硬件、操作系统之间的通信,所有的这一切都需要一种规则。而我们就把这种规则称为协议(protocol)。

TCP/IP 是互联网相关的各类协议族的总称,比如:TCP,UDP,IP,FTP,HTTP,ICMP,SMTP 等都属于 TCP/IP 族内的协议。

TCP/IP 模型是互联网的基础,它是一系列网络协议的总称。这些协议可以划分为四层,分别为链路层、网络层、传输层和应用层。

  • 链路层:负责封装和解封装 IP 报文,发送和接受 ARP/RARP 报文等。
  • 网络层:负责路由以及把分组报文发送给目标网络或主机。
  • 传输层:负责对报文进行分组和重组,并以 TCP 或 UDP 协议格式封装报文。
  • 应用层:负责向用户提供应用程序,比如 HTTP、FTP、Telnet、DNS、SMTP 等。


在网络体系结构中网络通信的建立必须是在通信双方的对等层进行,不能交错。 在整个数据传输过程中,数据在发送端时经过各层时都要附加上相应层的协议头和协议尾(仅数据链路层需要封装协议尾)部分,也就是要对数据进行协议封装,以标识对应层所用的通信协议。接下去介绍 TCP/IP 中有两个具有代表性的传输层协议----TCP 和 UDP。

二、UDP

UDP 协议全称是用户数据报协议,在网络中它与 TCP 协议一样用于处理数据包,是一种无连接的协议。在 OSI 模型中,在第四层——传输层,处于 IP 协议的上一层。UDP 有不提供数据包分组、组装和不能对数据包进行排序的缺点,也就是说,当报文发送之后,是无法得知其是否安全完整到达的。

它有以下几个特点:

1.面向无连接

首先 UDP 是不需要和 TCP 一样在发送数据前进行三次握手建立连接的,想发数据就可以开始发送了。并且也只是数据报文的搬运工,不会对数据报文进行任何拆分和拼接操作。

具体来说就是:

  • 在发送端,应用层将数据传递给传输层的 UDP 协议,UDP 只会给数据增加一个 UDP 头标识下是 UDP 协议,然后就传递给网络层了
  • 在接收端,网络层将数据传递给传输层,UDP 只去除 IP 报文头就传递给应用层,不会任何拼接操作

2.有单播,多播,广播的功能

UDP 不止支持一对一的传输方式,同样支持一对多,多对多,多对一的方式,也就是说 UDP 提供了单播,多播,广播的功能。

3.UDP 是面向报文的

发送方的 UDP 对应用程序交下来的报文,在添加首部后就向下交付 IP 层。UDP 对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界。因此,应用程序必须选择合适大小的报文

4.不可靠性

首先不可靠性体现在无连接上,通信都不需要建立连接,想发就发,这样的情况肯定不可靠。

并且收到什么数据就传递什么数据,并且也不会备份数据,发送数据也不会关心对方是否已经正确接收到数据了。

再者网络环境时好时坏,但是 UDP 因为没有拥塞控制,一直会以恒定的速度发送数据。即使网络条件不好,也不会对发送速率进行调整。这样实现的弊端就是在网络条件不好的情况下可能会导致丢包,但是优点也很明显,在某些实时性要求高的场景(比如电话会议)就需要使用 UDP 而不是 TCP。

从上面的动态图可以得知,UDP 只会把想发的数据报文一股脑的丢给对方,并不在意数据有无安全完整到达。

5.头部开销小,传输数据报文时是很高效的。


UDP 头部包含了以下几个数据:

  • 两个十六位的端口号,分别为源端口(可选字段)和目标端口
  • 整个数据报文的长度
  • 整个数据报文的检验和(IPv4 可选 字段),该字段用于发现头部信息和数据中的错误

因此 UDP 的头部开销小,只有八字节,相比 TCP 的至少二十字节要少得多,在传输数据报文时是很高效的

三、TCP

当一台计算机想要与另一台计算机通讯时,两台计算机之间的通信需要畅通且可靠,这样才能保证正确收发数据。例如,当你想查看网页或查看电子邮件时,希望完整且按顺序查看网页,而不丢失任何内容。当你下载文件时,希望获得的是完整的文件,而不仅仅是文件的一部分,因为如果数据丢失或乱序,都不是你希望得到的结果,于是就用到了 TCP。

TCP 协议全称是传输控制协议是一种面向连接的、可靠的、基于字节流的传输层通信协议,由 IETF 的 RFC 793 定义。TCP 是面向连接的、可靠的流协议。流就是指不间断的数据结构,你可以把它想象成排水管中的水流。

1.TCP 连接过程

如下图所示,可以看到建立一个 TCP 连接的过程为(三次握手的过程):

第一次握手

客户端向服务端发送连接请求报文段。该报文段中包含自身的数据通讯初始序号。请求发送后,客户端便进入 SYN-SENT 状态。

第二次握手

服务端收到连接请求报文段后,如果同意连接,则会发送一个应答,该应答中也会包含自身的数据通讯初始序号,发送完成后便进入 SYN-RECEIVED 状态。

第三次握手

当客户端收到连接同意的应答后,还要向服务端发送一个确认报文。客户端发完这个报文段后便进入 ESTABLISHED 状态,服务端收到这个应答后也进入 ESTABLISHED 状态,此时连接建立成功。

这里可能大家会有个疑惑:为什么 TCP 建立连接需要三次握手,而不是两次?这是因为这是为了防止出现失效的连接请求报文段被服务端接收的情况,从而产生错误。

2.TCP 断开链接


TCP 是全双工的,在断开连接时两端都需要发送 FIN 和 ACK。

第一次握手

若客户端 A 认为数据发送完成,则它需要向服务端 B 发送连接释放请求。

第二次握手

B 收到连接释放请求后,会告诉应用层要释放 TCP 链接。然后会发送 ACK 包,并进入 CLOSE_WAIT 状态,此时表明 A 到 B 的连接已经释放,不再接收 A 发的数据了。但是因为 TCP 连接是双向的,所以 B 仍旧可以发送数据给 A。

第三次握手

B 如果此时还有没发完的数据会继续发送,完毕后会向 A 发送连接释放请求,然后 B 便进入 LAST-ACK 状态。

第四次握手

A 收到释放请求后,向 B 发送确认应答,此时 A 进入 TIME-WAIT 状态。该状态会持续 2MSL(最大段生存期,指报文段在网络中生存的时间,超时会被抛弃) 时间,若该时间段内没有 B 的重发请求的话,就进入 CLOSED 状态。当 B 收到确认应答后,也便进入 CLOSED 状态。

3.TCP 协议的特点

  • 面向连接

面向连接,是指发送数据之前必须在两端建立连接。建立连接的方法是“三次握手”,这样能建立可靠的连接。建立连接,是为数据的可靠传输打下了基础。

  • 仅支持单播传输

每条 TCP 传输连接只能有两个端点,只能进行点对点的数据传输,不支持多播和广播传输方式。

  • 面向字节流

TCP 不像 UDP 一样那样一个个报文独立地传输,而是在不保留报文边界的情况下以字节流方式进行传输。

  • 可靠传输

对于可靠传输,判断丢包,误码靠的是 TCP 的段编号以及确认号。TCP 为了保证报文传输的可靠,就给每个包一个序号,同时序号也保证了传送到接收端实体的包的按序接收。然后接收端实体对已成功收到的字节发回一个相应的确认(ACK);如果发送端实体在合理的往返时延(RTT)内未收到确认,那么对应的数据(假设丢失了)将会被重传。

  • 提供拥塞控制

当网络出现拥塞的时候,TCP 能够减小向网络注入数据的速率和数量,缓解拥塞

  • TCP 提供全双工通信

TCP 允许通信双方的应用程序在任何时候都能发送数据,因为 TCP 连接的两端都设有缓存,用来临时存放双向通信的数据。当然,TCP 可以立即发送一个数据段,也可以缓存一段时间以便一次发送更多的数据段(最大的数据段大小取决于 MSS)

四、TCP 和 UDP 的比较

1.对比

UDP TCP
是否连接 无连接 面向连接
是否可靠 不可靠传输,不使用流量控制和拥塞控制 可靠传输,使用流量控制和拥塞控制
连接对象个数 支持一对一,一对多,多对一和多对多交互通信 只能是一对一通信
传输方式 面向报文 面向字节流
首部开销 首部开销小,仅 8 字节 首部最小 20 字节,最大 60 字节
适用场景 适用于实时应用(IP 电话、视频会议、直播等) 适用于要求可靠传输的应用,例如文件传输

2.总结

  • TCP 向上层提供面向连接的可靠服务 ,UDP 向上层提供无连接不可靠服务。
  • 虽然 UDP 并没有 TCP 传输来的准确,但是也能在很多实时性要求高的地方有所作为
  • 对数据准确性要求高,速度可以相对较慢的,可以选用 TCP

参考文章与书籍

深入理解JavaScript作用域和作用域链

前言

JavaScript中有一个被称为作用域(Scope)的特性。虽然对于许多新手开发者来说,作用域的概念并不是很容易理解,本文我会尽我所能用最简单的方式来解释作用域和作用域链,希望大家有所收获!

作用域(Scope)

1.什么是作用域

作用域是在运行时代码中的某些特定部分中变量,函数和对象的可访问性。换句话说,作用域决定了代码区块中变量和其他资源的可见性。可能这两句话并不好理解,我们先来看个例子:

function outFun2() {
    var inVariable = "内层变量2";
}
outFun2();//要先执行这个函数,否则根本不知道里面是啥
console.log(inVariable); // Uncaught ReferenceError: inVariable is not defined

从上面的例子可以体会到作用域的概念,变量inVariable在全局作用域没有声明,所以在全局作用域下取值会报错。我们可以这样理解:作用域就是一个独立的地盘,让变量不会外泄、暴露出去。也就是说作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。

ES6 之前 JavaScript 没有块级作用域,只有全局作用域和函数作用域。ES6的到来,为我们提供了‘块级作用域’,可通过新增命令let和const来体现。

2.全局作用域和函数作用域

在代码中任何地方都能访问到的对象拥有全局作用域,一般来说以下几种情形拥有全局作用域:

  • 最外层函数 和在最外层函数外面定义的变量拥有全局作用域
var outVariable = "我是最外层变量"; //最外层变量
function outFun() { //最外层函数
    var inVariable = "内层变量";
    function innerFun() { //内层函数
        console.log(inVariable);
    }
    innerFun();
}
console.log(outVariable); //我是最外层变量
outFun(); //内层变量
console.log(inVariable); //inVariable is not defined
innerFun(); //innerFun is not defined
  • 所有末定义直接赋值的变量自动声明为拥有全局作用域
function outFun2() {
    variable = "未定义直接赋值的变量";
    var inVariable2 = "内层变量2";
}
outFun2();//要先执行这个函数,否则根本不知道里面是啥
console.log(variable); //未定义直接赋值的变量
console.log(inVariable2); //inVariable2 is not defined
  • 所有window对象的属性拥有全局作用域

一般情况下,window对象的内置属性都拥有全局作用域,例如window.name、window.location、window.top等等。

全局作用域有个弊端:如果我们写了很多行 JS 代码,变量定义都没有用函数包括,那么它们就全部都在全局作用域中。这样就会 污染全局命名空间, 容易引起命名冲突。

// 张三写的代码中
var data = {a: 100}

// 李四写的代码中
var data = {x: true}

这就是为何 jQuery、Zepto 等库的源码,所有的代码都会放在(function(){....})()中。因为放在里面的所有变量,都不会被外泄和暴露,不会污染到外面,不会对其他的库或者 JS 脚本造成影响。这是函数作用域的一个体现。

函数作用域,是指声明在函数内部的变量,和全局作用域相反,局部作用域一般只在固定的代码片段内可访问到,最常见的例如函数内部。

function doSomething(){
    var blogName="浪里行舟";
    function innerSay(){
        alert(blogName);
    }
    innerSay();
}
alert(blogName); //脚本错误
innerSay(); //脚本错误

作用域是分层的,内层作用域可以访问外层作用域的变量,反之则不行。我们看个例子,用泡泡来比喻作用域可能好理解一点:

最后输出的结果为 2, 4, 12

  • 泡泡1是全局作用域,有标识符foo;
  • 泡泡2是作用域foo,有标识符a,bar,b;
  • 泡泡3是作用域bar,仅有标识符c。

值得注意的是:块语句(大括号“{}”中间的语句),如 if 和 switch 条件语句或 for 和 while 循环语句,不像函数,它们不会创建一个新的作用域。在块语句中定义的变量将保留在它们已经存在的作用域中。

if (true) {
    // 'if' 条件语句块不会创建一个新的作用域
    var name = 'Hammad'; // name 依然在全局作用域中
}
console.log(name); // logs 'Hammad'

JS 的初学者经常需要花点时间才能习惯变量提升,而如果不理解这种特有行为,就可能导致
bug 。正因为如此, ES6 引入了块级作用域,让变量的生命周期更加可控。

3.块级作用域

块级作用域可通过新增命令let和const声明,所声明的变量在指定块的作用域外无法被访问。块级作用域在如下情况被创建:

  1. 在一个函数内部
  2. 在一个代码块(由一对花括号包裹)内部

let 声明的语法与 var 的语法一致。你基本上可以用 let 来代替 var 进行变量声明,但会将变量的作用域限制在当前代码块中。块级作用域有以下几个特点:

  • 声明变量不会提升到代码块顶部

let/const 声明并不会被提升到当前代码块的顶部,因此你需要手动将 let/const 声明放置到顶部,以便让变量在整个代码块内部可用。

function getValue(condition) {
if (condition) {
let value = "blue";
return value;
} else {
// value 在此处不可用
return null;
}
// value 在此处不可用
}
  • 禁止重复声明

如果一个标识符已经在代码块内部被定义,那么在此代码块内使用同一个标识符进行 let 声明就会导致抛出错误。例如:

var count = 30;
let count = 40; // Uncaught SyntaxError: Identifier 'count' has already been declared

在本例中, count 变量被声明了两次:一次使用 var ,另一次使用 let 。因为 let 不能在同一作用域内重复声明一个已有标识符,此处的 let 声明就会抛出错误。但如果在嵌套的作用域内使用 let 声明一个同名的新变量,则不会抛出错误。

var count = 30;
// 不会抛出错误
if (condition) {
let count = 40;
// 其他代码
}
  • 循环中的绑定块作用域的妙用

开发者可能最希望实现for循环的块级作用域了,因为可以把声明的计数器变量限制在循环内,例如:

for (let i = 0; i < 10; i++) {
  // ...
}
console.log(i);
// ReferenceError: i is not defined

上面代码中,计数器i只在for循环体内有效,在循环体外引用就会报错。

var a = [];
for (var i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 10

上面代码中,变量i是var命令声明的,在全局范围内都有效,所以全局只有一个变量i。每一次循环,变量i的值都会发生改变,而循环内被赋给数组a的函数内部的console.log(i),里面的i指向的就是全局的i。也就是说,所有数组a的成员里面的i,指向的都是同一个i,导致运行时输出的是最后一轮的i的值,也就是 10。

如果使用let,声明的变量仅在块级作用域内有效,最后输出的是 6。

var a = [];
for (let i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 6

上面代码中,变量i是let声明的,当前的i只在本轮循环有效,所以每一次循环的i其实都是一个新的变量,所以最后输出的是6。你可能会问,如果每一轮循环的变量i都是重新声明的,那它怎么知道上一轮循环的值,从而计算出本轮循环的值?这是因为 JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量i时,就在上一轮循环的基础上进行计算。

另外,for循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。

for (let i = 0; i < 3; i++) {
  let i = 'abc';
  console.log(i);
}
// abc
// abc
// abc

上面代码正确运行,输出了 3 次abc。这表明函数内部的变量i与循环变量i不在同一个作用域,有各自单独的作用域。

作用域链

1.什么是自由变量

首先认识一下什么叫做 自由变量 。如下代码中,console.log(a)要得到a变量,但是在当前的作用域中没有定义a(可对比一下b)。当前作用域没有定义的变量,这成为 自由变量 。自由变量的值如何得到 —— 向父级作用域寻找(注意:这种说法并不严谨,下文会重点解释)。

var a = 100
function fn() {
    var b = 200
    console.log(a) // 这里的a在这里就是一个自由变量
    console.log(b)
}
fn()

2.什么是作用域链

如果父级也没呢?再一层一层向上寻找,直到找到全局作用域还是没找到,就宣布放弃。这种一层一层的关系,就是 作用域链 。

var a = 100
function F1() {
    var b = 200
    function F2() {
        var c = 300
        console.log(a) // 自由变量,顺作用域链向父作用域找
        console.log(b) // 自由变量,顺作用域链向父作用域找
        console.log(c) // 本作用域的变量
    }
    F2()
}
F1()

3.关于自由变量的取值

关于自由变量的值,上文提到要到父作用域中取,其实有时候这种解释会产生歧义。

var x = 10
function fn() {
  console.log(x)
}
function show(f) {
  var x = 20
  (function() {
    f() //10,而不是20
  })()
}
show(fn)

在fn函数中,取自由变量x的值时,要到哪个作用域中取?——要到创建fn函数的那个作用域中取,无论fn函数将在哪里调用

所以,不要在用以上说法了。相比而言,用这句话描述会更加贴切:要到创建这个函数的那个域”。
作用域中取值,这里强调的是“创建”,而不是“调用”
,切记切记——其实这就是所谓的"静态作用域"

var a = 10
function fn() {
  var b = 20
  function bar() {
    console.log(a + b) //30
  }
  return bar
}
var x = fn(),
  b = 200
x() //bar()

fn()返回的是bar函数,赋值给x。执行x(),即执行bar函数代码。取b的值时,直接在fn作用域取出。取a的值时,试图在fn作用域取,但是取不到,只能转向创建fn的那个作用域中去查找,结果找到了,所以最后的结果是30

作用域与执行上下文

许多开发人员经常混淆作用域和执行上下文的概念,误认为它们是相同的概念,但事实并非如此。

我们知道JavaScript属于解释型语言,JavaScript的执行分为:解释和执行两个阶段,这两个阶段所做的事并不一样:

解释阶段:

  • 词法分析
  • 语法分析
  • 作用域规则确定

执行阶段:

  • 创建执行上下文
  • 执行函数代码
  • 垃圾回收

JavaScript解释阶段便会确定作用域规则,因此作用域在函数定义时就已经确定了,而不是在函数调用时确定,但是执行上下文是函数执行之前创建的。执行上下文最明显的就是this的指向是执行时确定的。而作用域访问的变量是编写代码的结构确定的。

作用域和执行上下文之间最大的区别是:
执行上下文在运行时确定,随时可能改变;作用域在定义时就确定,并且不会改变

一个作用域下可能包含若干个上下文环境。有可能从来没有过上下文环境(函数从来就没有被调用过);有可能有过,现在函数被调用完毕后,上下文环境被销毁了;有可能同时存在一个或多个(闭包)。同一个作用域下,不同的调用会产生不同的执行上下文环境,继而产生不同的变量的值

参考文章和书籍

如何在线预览 GitHub 项目

本文框架图

一、Git是什么,它与Github什么关系

Git是一款免费、开源的分布式源代码管理工具。
Github是用Git做版本控制的代码托管平台。

二、Git常用命令

一般来说,Git命令日常使用只要记住上图6个命令,就可以了。但是熟练使用,恐怕要记住60~100个命令。

接下来归纳了一些常用的命令:

把普通目录变为git工作区
$ git init

添加当前目录的所有文件到暂存区
$ git add .

提交暂存区到仓库区
$ git commit -m “XXX”

恢复暂存区的指定文件到工作区
$ git checkout [file]
checkout命令用于从历史提交(或者暂存区域)中拷贝文件到工作目录,也可用于切换分支。

上传本地指定分支到远程仓库(master分支)
$ git push origin master

取回远程仓库的变化,并与本地分支合并
$ git pull [remote] [branch]

列出所有本地分支
$ git branch

新建一个分支,但依然停留在当前分支
$ git branch [branch-name]//比如git branch dev创建“dev"分支

新建一个分支,并切换到该分支
$ git checkout -b [branch] //比如git checkout -b dev切换到“dev"分支

合并指定分支到当前分支
$ git merge [branch]

三、如何进行多人协作(分支创建与合并)

1.什么是分支?

分支是为了将修改记录的整体流程分叉保存。分叉后的分支不受其他分支的影响,所以在同一个数据库里可以同时进行多个修改。

2.为什么要创建分支?

为了不受其他开发人员的影响,您可以在主分支上建立自己专用的分支。完成工作后,将自己分支上的修改合并到主分支。常常适用于当前开发的项目新增功能时,代码还未写完或者出现bug未调试好,这时创建分支将有效提高工作效率。因为每一次提交的历史记录都会在创建分支保存,所以当发生问题时,定位和修改造成问题的提交就容易多了。

3.如何创建分支以及合并分支?

git checkout -b dev//新建一个分支,并切换到该分支
git branch//列出所有本地分支:*dev master,并显示当前停留在dev分支上 
vi(m) a.js//在分支上修改文件,然后再上传远程仓库
git add.
git commit -m "update part2"
git push origin dev//上传到dev分支远程,接下去要合并分支
git checkout master//合并分支之前必须在master上
git pull origin master//在当前master上同步到最新版本
git merge dev//合并分支
git push origin master//重新提交到远程仓库

分支创建与合并

四、如何用Github的gh-pages分支展示自己的项目

master分支仅是浏览代码,而无法将页面直接在网页打开,而gh-pages分支则是用于直接浏览源码页面的分支。当我们写项目的时候,需要部署一个预览页面,就可以采用github的gh-pages分支制作GitHub Pages。
接下来举个例子:如何将我的电脑上“在线教育页面作品”上传到GitHub,并实现在线预览
git安装完成后,首先配置信息

配置用户名:  git config --global user.name "用户名"
配置邮箱: git config--global user.email "邮箱地址"
查看配置信息: git config --list

1.先将作品上传到master分支上

git init //把目标文件夹变为git工作区

git add .
git commit -m "education"

接下来创建education仓库,如下图

创建仓库

创建好仓库后,页面自动跳转到如下页面:

创建好仓库后

接下来输入以下两行命令行:

git remote add origin https://github.com/ljianshu/education.git
git push -u origin master

输入两行命令行后,按提示输入账户密码,如下图:

image.png

出现如下页面,意味着文件已经全部上传到master分支上,此时你也可以添加一个README.md文件,便于让他人看懂你上传文件。

成功上传到master

2.创建gh-pages分支,再将作品重新上传到该分支上

git checkout -b gh-pages
git push -u origin gh-pages

gh-pages分支

那这时候,我们看到已经多出了一个gh-pages分支,那么作品展示地址就是Github用户名.github.io/创建的仓库名。本例子线上预览https://ljianshu.github.io/education。当然你也可以购买一个特色域名,并且绑定在GitHub服务器上。

3.master修改后如何自动同步到gh-pages分支

每次当作品发生更改变动后,要先提交到master分支然后切换到gh-pages分支又重新提交一次,显然这个过程非常繁琐。接下我们介绍一种方法,只需提交到master分支即可,gh-pages分支无需重新提交,就可实现在线预览:
打开github项目文件的根目录,先找到.git 这个文件夹(文件夹默认是隐藏的),然后找到config这个文件,并打开该文件,在文件里加入以下两句代码即可:

push = +refs/heads/master:refs/heads/gh-pages
push = +refs/heads/master:refs/heads/master

config

五、Hexo+Github搭建自己个人博客

Hexo 是一个基于 Node.js 的快速、简洁且高效的博客框架,可以方便的生成静态网页托管在github和Heroku上。Hexo 因其界面简洁、美观且对各类人群(不只是程序猿)友好而广受欢迎,声望不亚于大名鼎鼎的WordPress。
它有以下几个特点:

  • 超快速度----Node.js 所带来的超快生成速度,让上百个页面在几秒内瞬间完成渲染。
  • 一键部署----只需一条指令即可部署到 GitHub Pages, Heroku 或其他网站。
  • 支持 Markdown----Hexo 支持 GitHub Flavored Markdown 的所有功能,甚至可以整合 Octopress 的大多数插件。
  • 丰富的插件----Hexo 拥有强大的插件系统,安装插件可以让 Hexo 支持 Jade, CoffeeScript。

具体实践操作可以查看官网Hexo,以后关于这个主题会更新一篇,敬请关注!

JavaScript常见的六种继承方式

前言

面向对象编程很重要的一个方面,就是对象的继承。A 对象通过继承 B 对象,就能直接拥有 B 对象的所有属性和方法。这对于代码的复用是非常有用的。

大部分面向对象的编程语言,都是通过“类”(class)实现对象的继承。传统上,JavaScript 语言的继承不通过 class(ES6 引入了class 语法),而是通过“原型对象”(prototype)实现。那么在JS中常见的继承方式有几种呢?

如需本文源码,请猛戳 常见的六种继承方式

如果觉得文章对你有些许帮助,欢迎在我的GitHub博客点赞和关注,感激不尽!

方式一、原型链继承

这种方式关键在于:子类型的原型为父类型的一个实例对象。

//父类型
 function Person(name, age) {
   this.name = name,
   this.age = age,
   this.play = [1, 2, 3]
   this.setName = function () {}
 }
 Person.prototype.setAge = function () {}
 //子类型
 function Student(price) {
   this.price = price
   this.setScore = function () {}
 }
 Student.prototype = new Person() // 子类型的原型为父类型的一个实例对象
 var s1 = new Student(15000)
 var s2 = new Student(14000)
 console.log(s1,s2)     


但这种方式实现的本质是通过将子类的原型指向了父类的实例,所以子类的实例就可以通过__proto__访问到 Student.prototype 也就是Person的实例,这样就可以访问到父类的私有方法,然后再通过__proto__指向父类的prototype就可以获得到父类原型上的方法。于是做到了将父类的私有、公有方法和属性都当做子类的公有属性

子类继承父类的属性和方法是将父类的私有属性和公有方法都作为自己的公有属性和方法,我们都知道在操作基本数据类型的时候操作的是值,在操作引用数据类型的时候操作的是地址,如果说父类的私有属性中有引用类型的属性,那它被子类继承的时候会作为公有属性,这样子类1操作这个属性的时候,就会影响到子类2。

s1.play.push(4)
console.log(s1.play, s2.play)
console.log(s1.__proto__ === s2.__proto__)//true
console.log(s1.__proto__.__proto__ === s2.__proto__.__proto__)//true      


s1中play属性发生变化,与此同时,s2中play属性也会跟着变化。

另外注意一点的是,我们需要在子类中添加新的方法或者是重写父类的方法时候,切记一定要放到替换原型的语句之后

function Person (name, age) {
 this.name = name,
 this.age = age
}
Person.prototype.setAge = function () {
 console.log("111")
}
function Student (price) {
 this.price = price
 this.setScore = function () { }
}
// Student.prototype.sayHello = function () { }//在这里写子类的原型方法和属性是无效的,
//因为会改变原型的指向,所以应该放到重新指定之后
Student.prototype = new Person()
Student.prototype.sayHello = function () { }
var s1 = new Student(15000)
console.log(s1)      

特点

  • 父类新增原型方法/原型属性,子类都能访问到
  • 简单,易于实现

缺点

  • 无法实现多继承
  • 来自原型对象的所有属性被所有实例共享
  • 创建子类实例时,无法向父类构造函数传参
  • 要想为子类新增属性和方法,必须要在Student.prototype = new Person() 之后执行,不能放到构造器中

方式二: 借用构造函数继承

这种方式关键在于:在子类型构造函数中通用call()调用父类型构造函数

  function Person(name, age) {
    this.name = name,
    this.age = age,
    this.setName = function () {}
  }
  Person.prototype.setAge = function () {}
  function Student(name, age, price) {
    Person.call(this, name, age)  // 相当于: this.Person(name, age)
    /*this.name = name
    this.age = age*/
    this.price = price
  }
  var s1 = new Student('Tom', 20, 15000)


这种方式只是实现部分的继承,如果父类的原型还有方法和属性,子类是拿不到这些方法和属性的。

console.log(s1.setAge())//Uncaught TypeError: s1.setAge is not a function

特点

  • 解决了原型链继承中子类实例共享父类引用属性的问题
  • 创建子类实例时,可以向父类传递参数
  • 可以实现多继承(call多个父类对象)

缺点

  • 实例并不是父类的实例,只是子类的实例
  • 只能继承父类的实例属性和方法,不能继承原型属性和方法
  • 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能

方式三: 原型链+借用构造函数的组合继承

这种方式关键在于:通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用。

function Person (name, age) {
  this.name = name,
  this.age = age,
  this.setAge = function () { }
}
Person.prototype.setAge = function () {
  console.log("111")
}
function Student (name, age, price) {
  Person.call(this, name, age)
  this.price = price
  this.setScore = function () { }
}
Student.prototype = new Person()
Student.prototype.constructor = Student//组合继承也是需要修复构造函数指向的
Student.prototype.sayHello = function () { }
var s1 = new Student('Tom', 20, 15000)
var s2 = new Student('Jack', 22, 14000)
console.log(s1)
console.log(s1.constructor) //Student
console.log(p1.constructor) //Person       


这种方式融合原型链继承和构造函数的优点,是 JavaScript 中最常用的继承模式。不过也存在缺点就是无论在什么情况下,都会调用两次构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数的内部,子类型最终会包含父类型对象的全部实例属性,但我们不得不在调用子类构造函数时重写这些属性。

优点

  • 可以继承实例属性/方法,也可以继承原型属性/方法
  • 不存在引用属性共享问题
  • 可传参
  • 函数可复用

缺点

  • 调用了两次父类构造函数,生成了两份实例

方式四: 组合继承优化1

这种方式通过父类原型和子类原型指向同一对象,子类可以继承到父类的公有方法当做自己的公有方法,而且不会初始化两次实例方法/属性,避免的组合继承的缺点

function Person (name, age) {
  this.name = name,
  this.age = age,
  this.setAge = function () { }
}
Person.prototype.setAge = function () {
  console.log("111")
}
function Student (name, age, price) {
  Person.call(this, name, age)
  this.price = price
  this.setScore = function () { }
}
Student.prototype = Person.prototype
Student.prototype.sayHello = function () { }
var s1 = new Student('Tom', 20, 15000)
console.log(s1)       


但这种方式没办法辨别是对象是子类还是父类实例化

console.log(s1 instanceof Student, s1 instanceof Person)//true true
console.log(s1.constructor)//Person

优点

  • 不会初始化两次实例方法/属性,避免的组合继承的缺点

缺点

  • 没办法辨别是实例是子类还是父类创造的,子类和父类的构造函数指向是同一个。

方式五: 组合继承优化2

借助原型可以基于已有的对象来创建对象,var B = Object.create(A)以A对象为原型,生成了B对象。B继承了A的所有属性和方法。

function Person (name, age) {
  this.name = name,
  this.age = age
}
Person.prototype.setAge = function () {
  console.log("111")
}
function Student (name, age, price) {
  Person.call(this, name, age)
  this.price = price
  this.setScore = function () { }
}
Student.prototype = Object.create(Person.prototype)//核心代码
Student.prototype.constructor = Student//核心代码
var s1 = new Student('Tom', 20, 15000)
console.log(s1 instanceof Student, s1 instanceof Person) // true true
console.log(s1.constructor) //Student
console.log(s1)       

同样的,Student继承了所有的Person原型对象的属性和方法。目前来说,最完美的继承方法!

方式六:ES6中class 的继承

ES6中引入了class关键字,class可以通过extends关键字实现继承,还可以通过static关键字定义类的静态方法,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。

ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。

需要注意的是,class关键字只是原型的语法糖,JavaScript继承仍然是基于原型实现的

class Person {
  //调用类的构造方法
  constructor(name, age) {
    this.name = name
    this.age = age
  }
  //定义一般的方法
  showName () {
    console.log("调用父类的方法")
    console.log(this.name, this.age);
  }
}
let p1 = new Person('kobe', 39)
console.log(p1)
//定义一个子类
class Student extends Person {
  constructor(name, age, salary) {
    super(name, age)//通过super调用父类的构造方法
    this.salary = salary
  }
  showName () {//在子类自身定义方法
    console.log("调用子类的方法")
    console.log(this.name, this.age, this.salary);
  }
}
let s1 = new Student('wade', 38, 1000000000)
console.log(s1)
s1.showName()     


优点

  • 语法简单易懂,操作更方便

缺点

  • 并不是所有的浏览器都支持class关键字

参考文章

细说数组常用遍历的方法

前言

本文主要介绍数组常见遍历方法:forEach、map、filter、find、every、some、reduce,它们有个共同点:不会改变原始数组。

一、forEach:遍历数组

var colors = ["red","blue","green"];
// ES5遍历数组方法
for(var i = 0; i < colors.length; i++){ 
 console.log(colors[i]);//red blue green
}
// ES6 forEach
colors.forEach(function(color){
 console.log(color);//red blue green
});

我们再来看个例子:遍历数组中的值,并计算总和

var numbers = [1,2,3,4,5];
var sum = 0;
numbers.forEach(number=>sum+=number)
console.log(sum)//15

二、map:将数组映射成另一个数组

map通过指定函数处理数组的每个元素,并返回处理后新的数组,map 不会改变原始数组。

forEach和map的区别在于,forEach没有返回值。
map需要返回值,如果不给return,默认返回undefined

使用场景1
假定有一个数值数组(A),将A数组中的值以双倍的形式放到B数组

var numbers = [1,2,3];
var doubledNumbers = [];
// es5写法
for(var i = 0; i < numbers.length; i++){
 doubledNumbers.push(numbers[i] * 2);
}
console.log(doubledNumbers);//[2,4,6]
// es6 map方法
var doubled = numbers.map(function(number){
   return number * 2;
})
console.log(doubled);//[2,4,6]

使用场景2 假定有一个对象数组(A),将A数中对象某个属性的值存储到B数组中

var cars = [
  {model:"Buick",price:"CHEAP"},
  {model:"BMW",price:"expensive"}
];
var prices = cars.map(function(car){
    return car.price;
})
console.log(prices);//["CHEAP", "expensive"]

三、filter:从数组中找出所有符合指定条件的元素

filter() 检测数值元素,并返回符合条件所有元素的数组。 filter() 不会改变原始数组。

使用场景1:假定有一个对象数组(A),获取数组中指定类型的对象放到B数组中

var porducts = [
  {name:"cucumber",type:"vegetable"},
  {name:"banana",type:"fruit"},
  {name:"celery",type:"vegetable"},
  {name:"orange",type:"fruit"}
];
// es5写法
var filteredProducts = [];
for(var i = 0; i < porducts.length; i++){
    if(porducts[i].type === "vegetable"){
      filteredProducts.push(porducts[i]);
    }
}
console.log(filteredProducts);//[{name: "cucumber", type: "vegetable"},
                                 {name: "celery", type: "vegetable"}]
// es6 filter
var filtered2 = porducts.filter(function(product){
  return product.type === "vegetable";
})
console.log(filtered2);

使用场景2:假定有一个对象数组(A),过滤掉不满足以下条件的对象
条件: 蔬菜 数量大于0,价格小于10

var products = [
  {name:"cucumber",type:"vegetable",quantity:0,price:1},
  {name:"banana",type:"fruit",quantity:10,price:16},
  {name:"celery",type:"vegetable",quantity:30,price:8},
  {name:"orange",type:"fruit",quantity:3,price:6}
];
products = products.filter(function(product){
    return product.type === "vegetable" 
    && product.quantity > 0 
    && product.price < 10
})
console.log(products);//[{name:"celery",type:"vegetable",quantity:30,price:8}]

使用场景3:假定有两个数组(A,B),根据A中id值,过滤掉B数组不符合的数据

var post = {id:4,title:"Javascript"};
var comments = [
   {postId:4,content:"Angular4"},
   {postId:2,content:"Vue.js"},
   {postId:3,content:"Node.js"},
   {postId:4,content:"React.js"},
];
function commentsForPost(post,comments){
   return comments.filter(function(comment){
     return comment.postId === post.id;
   })
}
console.log(commentsForPost(post,comments));//[{postId:4,content:"Angular4"},{postId:4,content:"React.js"}]

四、find:返回通过测试(函数内判断)的数组的第一个元素的值

它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined。
使用场景1
假定有一个对象数组(A),找到符合条件的对象

 var users = [
  {name:"Jill"},
  {name:"Alex",id:2},
  {name:"Bill"},
  {name:"Alex"}
 ];
// es5方法
 var user;
 for(var i = 0; i < users.length; i++){
  if(users[i].name === "Alex"){
    user = users[i];
    break;//找到后就终止循环
  }
 }
 console.log(user);// {name:"Alex",id:2}
// es6 find
user = users.find(function(user){
  return user.name === "Alex";
})
console.log(user);// {name:"Alex",id:2}找到后就终止循环

使用场景2:假定有一个对象数组(A),根据指定对象的条件找到数组中符合条件的对象

var posts = [
 {id:3,title:"Node.js"},
 {id:1,title:"React.js"}
];
var comment = {postId:1,content:"Hello World!"};
function postForComment(posts,comment){
 return posts.find(function(post){
   return post.id === comment.postId;
 })
}
console.log(postForComment(posts,comment));//{id: 1, title: "React.js"}

五、every&some

every:数组中是否每个元素都满足指定的条件

some:数组中是否有元素满足指定的条件

使用场景1:计算对象数组中每个电脑操作系统是否可用,大于16位操作系统表示可用,否则不可用

//ES5方法
var computers = [
 {name:"Apple",ram:16},
 {name:"IBM",ram:4},
 {name:"Acer",ram:32}
];
var everyComputersCanRunProgram = true;
var someComputersCanRunProgram = false;
for(var i = 0; i < computers.length; i++){
 var computer = computers[i];
 if(computer.ram < 16){
   everyComputersCanRunProgram = false;
 }else{
   someComputersCanRunProgram = true;
 }
}
console.log(everyComputersCanRunProgram);//false
console.log(someComputersCanRunProgram);//true
//ES6 some every 
var every = computers.every(function(computer){
  return computer.ram > 16;
})
console.log(every);//false
var some = computers.some(function(computer){
 return computer.ram > 16;
})
console.log(some);//true

一言以蔽之:Some: 一真即真;Every: 一假即假

使用场景2:假定有一个注册页面,判断所有input内容的长度是否大于0

function Field(value){
  this.value = value;
}
Field.prototype.validate = function(){
  return this.value.length > 0;
}
//ES5方法
var username = new Field("henrywu");
var telephone = new Field("18888888888");
var password = new Field("my_password");
console.log(username.validate());//true
console.log(telephone.validate());//true
console.log(password.validate());//true
//ES6 some every
var fields = [username,telephone,password];
var formIsValid = fields.every(function(field){
 return field.validate();
})
console.log(formIsValid);//true
if(formIsValid){
 // 注册成功
}else{
  // 给用户一个友善的错误提醒
}

六、reduce:将数组合成一个值

reduce() 方法接收一个方法作为累加器,数组中的每个值(从左至右) 开始合并,最终为一个值。

使用场景1: 计算数组中所有值的总和

 var numbers = [10,20,30];
 var sum = 0;
//es5 方法
for(var i = 0; i < numbers.length; i++){
  sum += numbers[i];
}
console.log(sum);
// es6 reduce
var sumValue = numbers.reduce(function(sum2,number2){
  console.log(sum2);//0 10 30 60
  return sum2 + number2;
},0);//sum2初始值为0
console.log(sumValue);

使用场景2
将数组中对象的某个属性抽离到另外一个数组中

 var primaryColors = [
   {color:"red"},
   {color:"yellow"},
   {color:"blue"}
 ];
 var colors = primaryColors.reduce(function(previous,primaryColor){
    previous.push(primaryColor.color);
    return previous;
 },[]);
 console.log(colors);//["red", "yellow", "blue"]

使用场景3:判断字符串中括号是否对称

function balancedParens(string){
  return !string.split("").reduce(function(previous,char){
    if(previous < 0) { return previous;}
    if(char == "("){ return ++previous;}
    if(char == ")"){ return --previous;}
    return previous;
  },0);
}
console.log(balancedParens("((())))"));

前端模块化详解(完整版)

前言

在JavaScript发展初期就是为了实现简单的页面交互逻辑,寥寥数语即可;如今CPU、浏览器性能得到了极大的提升,很多页面逻辑迁移到了客户端(表单验证等),随着web2.0时代的到来,Ajax技术得到广泛应用,jQuery等前端库层出不穷,前端代码日益膨胀,此时在JS方面就会考虑使用模块化规范去管理。
本文内容主要有理解模块化,为什么要模块化,模块化的优缺点以及模块化规范,并且介绍下开发中最流行的CommonJS, AMD, ES6、CMD规范。本文试图站在小白的角度,用通俗易懂的笔调介绍这些枯燥无味的概念,希望诸君阅读后,对模块化编程有个全新的认识和理解!

建议下载本文源代码,自己动手敲一遍,请猛戳GitHub个人博客(全集)

模块化规范

一、模块化的理解

1.什么是模块?

  • 将一个复杂的程序依据一定的规则(规范)封装成几个块(文件), 并进行组合在一起
  • 块的内部数据与实现是私有的, 只是向外部暴露一些接口(方法)与外部其它模块通信

2.模块化的进化过程

  • 全局function模式 : 将不同的功能封装成不同的全局函数
    • 编码: 将不同的功能封装成不同的全局函数
    • 问题: 污染全局命名空间, 容易引起命名冲突或数据不安全,而且模块成员之间看不出直接关系
function m1(){
  //...
}
function m2(){
  //...
}
  • namespace模式 : 简单对象封装
    • 作用: 减少了全局变量,解决命名冲突
    • 问题: 数据不安全(外部可以直接修改模块内部的数据)
let myModule = {
  data: 'www.baidu.com',
  foo() {
    console.log(`foo() ${this.data}`)
  },
  bar() {
    console.log(`bar() ${this.data}`)
  }
}
myModule.data = 'other data' //能直接修改模块内部的数据
myModule.foo() // foo() other data

这样的写法会暴露所有模块成员,内部状态可以被外部改写。

  • IIFE模式:匿名函数自调用(闭包)
    • 作用: 数据是私有的, 外部只能通过暴露的方法操作
    • 编码: 将数据和行为封装到一个函数内部, 通过给window添加属性来向外暴露接口
    • 问题: 如果当前这个模块依赖另一个模块怎么办?
// index.html文件
<script type="text/javascript" src="module.js"></script>
<script type="text/javascript">
    myModule.foo()
    myModule.bar()
    console.log(myModule.data) //undefined 不能访问模块内部数据
    myModule.data = 'xxxx' //不是修改的模块内部的data
    myModule.foo() //没有改变
</script>
// module.js文件
(function(window) {
  let data = 'www.baidu.com'
  //操作数据的函数
  function foo() {
    //用于暴露有函数
    console.log(`foo() ${data}`)
  }
  function bar() {
    //用于暴露有函数
    console.log(`bar() ${data}`)
    otherFun() //内部调用
  }
  function otherFun() {
    //内部私有的函数
    console.log('otherFun()')
  }
  //暴露行为
  window.myModule = { foo, bar } //ES6写法
})(window)

最后得到的结果:

  • IIFE模式增强 : 引入依赖

这就是现代模块实现的基石

// module.js文件
(function(window, $) {
  let data = 'www.baidu.com'
  //操作数据的函数
  function foo() {
    //用于暴露有函数
    console.log(`foo() ${data}`)
    $('body').css('background', 'red')
  }
  function bar() {
    //用于暴露有函数
    console.log(`bar() ${data}`)
    otherFun() //内部调用
  }
  function otherFun() {
    //内部私有的函数
    console.log('otherFun()')
  }
  //暴露行为
  window.myModule = { foo, bar }
})(window, jQuery)
 // index.html文件
  <!-- 引入的js必须有一定顺序 -->
  <script type="text/javascript" src="jquery-1.10.1.js"></script>
  <script type="text/javascript" src="module.js"></script>
  <script type="text/javascript">
    myModule.foo()
  </script>

上例子通过jquery方法将页面的背景颜色改成红色,所以必须先引入jQuery库,就把这个库当作参数传入。这样做除了保证模块的独立性,还使得模块之间的依赖关系变得明显

3. 模块化的好处

  • 避免命名冲突(减少命名空间污染)
  • 更好的分离, 按需加载
  • 更高复用性
  • 高可维护性

4. 引入多个<script>后出现出现问题

  • 请求过多

首先我们要依赖多个模块,那样就会发送多个请求,导致请求过多

  • 依赖模糊

我们不知道他们的具体依赖关系是什么,也就是说很容易因为不了解他们之间的依赖关系导致加载先后顺序出错。

  • 难以维护

以上两种原因就导致了很难维护,很可能出现牵一发而动全身的情况导致项目出现严重的问题。
模块化固然有多个好处,然而一个页面需要引入多个js文件,就会出现以上这些问题。而这些问题可以通过模块化规范来解决,下面介绍开发中最流行的commonjs, AMD, ES6, CMD规范。

二、模块化规范

1.CommonJS

(1)概述

Node 应用由模块组成,采用 CommonJS 模块规范。每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。在服务器端,模块的加载是运行时同步加载的;在浏览器端,模块需要提前编译打包处理。

(2)特点

  • 所有代码都运行在模块作用域,不会污染全局作用域。
  • 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
  • 模块加载的顺序,按照其在代码中出现的顺序。

(3)基本语法

  • 暴露模块:module.exports = valueexports.xxx = value
  • 引入模块:require(xxx),如果是第三方模块,xxx为模块名;如果是自定义模块,xxx为模块文件路径

此处我们有个疑问:CommonJS暴露的模块到底是什么? CommonJS规范规定,每个模块内部,module变量代表当前模块。这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。加载某个模块,其实是加载该模块的module.exports属性

// example.js
var x = 5;
var addX = function (value) {
  return value + x;
};
module.exports.x = x;
module.exports.addX = addX;

上面代码通过module.exports输出变量x和函数addX。

var example = require('./example.js');//如果参数字符串以“./”开头,则表示加载的是一个位于相对路径
console.log(example.x); // 5
console.log(example.addX(1)); // 6

require命令用于加载模块文件。require命令的基本功能是,读入并执行一个JavaScript文件,然后返回该模块的exports对象。如果没有发现指定模块,会报错

(4)模块的加载机制

CommonJS模块的加载机制是,输入的是被输出的值的拷贝。也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。这点与ES6模块化有重大差异(下文会介绍),请看下面这个例子:

// lib.js
var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  counter: counter,
  incCounter: incCounter,
};

上面代码输出内部变量counter和改写这个变量的内部方法incCounter。

// main.js
var counter = require('./lib').counter;
var incCounter = require('./lib').incCounter;

console.log(counter);  // 3
incCounter();
console.log(counter); // 3

上面代码说明,counter输出以后,lib.js模块内部的变化就影响不到counter了。这是因为counter是一个原始类型的值,会被缓存。除非写成一个函数,才能得到内部变动后的值

(5)服务器端实现

①下载安装node.js

②创建项目结构

注意:用npm init 自动生成package.json时,package name(包名)不能有中文和大写

|-modules
  |-module1.js
  |-module2.js
  |-module3.js
|-app.js
|-package.json
  {
    "name": "commonJS-node",
    "version": "1.0.0"
  }

③下载第三方模块

npm install uniq --save // 用于数组去重

④定义模块代码

//module1.js
module.exports = {
  msg: 'module1',
  foo() {
    console.log(this.msg)
  }
}
//module2.js
module.exports = function() {
  console.log('module2')
}
//module3.js
exports.foo = function() {
  console.log('foo() module3')
}
exports.arr = [1, 2, 3, 3, 2]
// app.js文件
// 引入第三方库,应该放置在最前面
let uniq = require('uniq')
let module1 = require('./modules/module1')
let module2 = require('./modules/module2')
let module3 = require('./modules/module3')

module1.foo() //module1
module2() //module2
module3.foo() //foo() module3
console.log(uniq(module3.arr)) //[ 1, 2, 3 ]

⑤通过node运行app.js

命令行输入node app.js,运行JS文件

(6)浏览器端实现(借助Browserify)

①创建项目结构

|-js
  |-dist //打包生成文件的目录
  |-src //源码所在的目录
    |-module1.js
    |-module2.js
    |-module3.js
    |-app.js //应用主源文件
|-index.html //运行于浏览器上
|-package.json
  {
    "name": "browserify-test",
    "version": "1.0.0"
  }

②下载browserify

  • 全局: npm install browserify -g
  • 局部: npm install browserify --save-dev

③定义模块代码(同服务器端)

注意:index.html文件要运行在浏览器上,需要借助browserify将app.js文件打包编译,如果直接在index.html引入app.js就会报错!

④打包处理js

根目录下运行browserify js/src/app.js -o js/dist/bundle.js

⑤页面使用引入

在index.html文件中引入<script type="text/javascript" src="js/dist/bundle.js"></script>

2.AMD

CommonJS规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。AMD规范则是非同步加载模块,允许指定回调函数。由于Node.js主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式,所以CommonJS规范比较适用。但是,如果是浏览器环境,要从服务器端加载模块,这时就必须采用非同步模式,因此浏览器端一般采用AMD规范。此外AMD规范比CommonJS规范在浏览器端实现要来着早。

(1)AMD规范基本语法

定义暴露模块:

//定义没有依赖的模块
define(function(){
   return 模块
})
//定义有依赖的模块
define(['module1', 'module2'], function(m1, m2){
   return 模块
})

引入使用模块:

require(['module1', 'module2'], function(m1, m2){
   使用m1/m2
})

(2)未使用AMD规范与使用require.js

通过比较两者的实现方法,来说明使用AMD规范的好处。

  • 未使用AMD规范
// dataService.js文件
(function (window) {
  let msg = 'www.baidu.com'
  function getMsg() {
    return msg.toUpperCase()
  }
  window.dataService = {getMsg}
})(window)
// alerter.js文件
(function (window, dataService) {
  let name = 'Tom'
  function showMsg() {
    alert(dataService.getMsg() + ', ' + name)
  }
  window.alerter = {showMsg}
})(window, dataService)
// main.js文件
(function (alerter) {
  alerter.showMsg()
})(alerter)
// index.html文件
<div><h1>Modular Demo 1: 未使用AMD(require.js)</h1></div>
<script type="text/javascript" src="js/modules/dataService.js"></script>
<script type="text/javascript" src="js/modules/alerter.js"></script>
<script type="text/javascript" src="js/main.js"></script>

最后得到如下结果:

这种方式缺点很明显:首先会发送多个请求,其次引入的js文件顺序不能搞错,否则会报错!

  • 使用require.js

RequireJS是一个工具库,主要用于客户端的模块管理。它的模块管理遵守AMD规范,RequireJS的基本**是,通过define方法,将代码定义为模块;通过require方法,实现代码的模块加载
接下来介绍AMD规范在浏览器实现的步骤:

①下载require.js, 并引入

  • 官网: http://www.requirejs.cn/
  • github : https://github.com/requirejs/requirejs

然后将require.js导入项目: js/libs/require.js

②创建项目结构

|-js
  |-libs
    |-require.js
  |-modules
    |-alerter.js
    |-dataService.js
  |-main.js
|-index.html

③定义require.js的模块代码

// dataService.js文件
// 定义没有依赖的模块
define(function() {
  let msg = 'www.baidu.com'
  function getMsg() {
    return msg.toUpperCase()
  }
  return { getMsg } // 暴露模块
})
//alerter.js文件
// 定义有依赖的模块
define(['dataService'], function(dataService) {
  let name = 'Tom'
  function showMsg() {
    alert(dataService.getMsg() + ', ' + name)
  }
  // 暴露模块
  return { showMsg }
})
// main.js文件
(function() {
  require.config({
    baseUrl: 'js/', //基本路径 出发点在根目录下
    paths: {
      //映射: 模块标识名: 路径
      alerter: './modules/alerter', //此处不能写成alerter.js,会报错
      dataService: './modules/dataService'
    }
  })
  require(['alerter'], function(alerter) {
    alerter.showMsg()
  })
})()
// index.html文件
<!DOCTYPE html>
<html>
  <head>
    <title>Modular Demo</title>
  </head>
  <body>
    <!-- 引入require.js并指定js主文件的入口 -->
    <script data-main="js/main" src="js/libs/require.js"></script>
  </body>
</html>

④页面引入require.js模块:

在index.html引入 <script data-main="js/main" src="js/libs/require.js"></script>

**此外在项目中如何引入第三方库?**只需在上面代码的基础稍作修改:

// alerter.js文件
define(['dataService', 'jquery'], function(dataService, $) {
  let name = 'Tom'
  function showMsg() {
    alert(dataService.getMsg() + ', ' + name)
  }
  $('body').css('background', 'green')
  // 暴露模块
  return { showMsg }
})
// main.js文件
(function() {
  require.config({
    baseUrl: 'js/', //基本路径 出发点在根目录下
    paths: {
      //自定义模块
      alerter: './modules/alerter', //此处不能写成alerter.js,会报错
      dataService: './modules/dataService',
      // 第三方库模块
      jquery: './libs/jquery-1.10.1' //注意:写成jQuery会报错
    }
  })
  require(['alerter'], function(alerter) {
    alerter.showMsg()
  })
})()

上例是在alerter.js文件中引入jQuery第三方库,main.js文件也要有相应的路径配置。
小结:通过两者的比较,可以得出AMD模块定义的方法非常清晰,不会污染全局环境,能够清楚地显示依赖关系。AMD模式可以用于浏览器环境,并且允许非同步加载模块,也可以根据需要动态加载模块。

3.CMD

CMD规范专门用于浏览器端,模块的加载是异步的,模块使用时才会加载执行。CMD规范整合了CommonJS和AMD规范的特点。在 Sea.js 中,所有 JavaScript 模块都遵循 CMD模块定义规范。

(1)CMD规范基本语法

定义暴露模块:

//定义没有依赖的模块
define(function(require, exports, module){
  exports.xxx = value
  module.exports = value
})
//定义有依赖的模块
define(function(require, exports, module){
  //引入依赖模块(同步)
  var module2 = require('./module2')
  //引入依赖模块(异步)
    require.async('./module3', function (m3) {
    })
  //暴露模块
  exports.xxx = value
})

引入使用模块:

define(function (require) {
  var m1 = require('./module1')
  var m4 = require('./module4')
  m1.show()
  m4.show()
})

(2)sea.js简单使用教程

①下载sea.js, 并引入

然后将sea.js导入项目: js/libs/sea.js

②创建项目结构

|-js
  |-libs
    |-sea.js
  |-modules
    |-module1.js
    |-module2.js
    |-module3.js
    |-module4.js
    |-main.js
|-index.html

③定义sea.js的模块代码

// module1.js文件
define(function (require, exports, module) {
  //内部变量数据
  var data = 'atguigu.com'
  //内部函数
  function show() {
    console.log('module1 show() ' + data)
  }
  //向外暴露
  exports.show = show
})
// module2.js文件
define(function (require, exports, module) {
  module.exports = {
    msg: 'I Will Back'
  }
})
// module3.js文件
define(function(require, exports, module) {
  const API_KEY = 'abc123'
  exports.API_KEY = API_KEY
})
// module4.js文件
define(function (require, exports, module) {
  //引入依赖模块(同步)
  var module2 = require('./module2')
  function show() {
    console.log('module4 show() ' + module2.msg)
  }
  exports.show = show
  //引入依赖模块(异步)
  require.async('./module3', function (m3) {
    console.log('异步引入依赖模块3  ' + m3.API_KEY)
  })
})
// main.js文件
define(function (require) {
  var m1 = require('./module1')
  var m4 = require('./module4')
  m1.show()
  m4.show()
})

④在index.html中引入

<script type="text/javascript" src="js/libs/sea.js"></script>
<script type="text/javascript">
  seajs.use('./js/modules/main')
</script>

最后得到结果如下:

4.ES6模块化

ES6 模块的设计**是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。

(1)ES6模块化语法

export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。

/** 定义模块 math.js **/
var basicNum = 0;
var add = function (a, b) {
    return a + b;
};
export { basicNum, add };
/** 引用模块 **/
import { basicNum, add } from './math';
function test(ele) {
    ele.textContent = add(99 + basicNum);
}

如上例所示,使用import命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载。为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到export default命令,为模块指定默认输出。

// export-default.js
export default function () {
  console.log('foo');
}
// import-default.js
import customName from './export-default';
customName(); // 'foo'

模块默认输出, 其他模块加载该模块时,import命令可以为该匿名函数指定任意名字。

(2)ES6 模块与 CommonJS 模块的差异

它们有两个重大差异:

① CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用

② CommonJS 模块是运行时加载,ES6 模块是编译时输出接口

第二个差异是因为 CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

下面重点解释第一个差异,我们还是举上面那个CommonJS模块的加载机制例子:

// lib.js
export let counter = 3;
export function incCounter() {
  counter++;
}
// main.js
import { counter, incCounter } from './lib';
console.log(counter); // 3
incCounter();
console.log(counter); // 4

ES6 模块的运行机制与 CommonJS 不一样。ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块

(3) ES6-Babel-Browserify使用教程

简单来说就一句话:使用Babel将ES6编译为ES5代码,使用Browserify编译打包js

①定义package.json文件

 {
   "name" : "es6-babel-browserify",
   "version" : "1.0.0"
 }

②安装babel-cli, babel-preset-es2015和browserify

  • npm install babel-cli browserify -g
  • npm install babel-preset-es2015 --save-dev
  • preset 预设(将es6转换成es5的所有插件打包)

③定义.babelrc文件

  {
    "presets": ["es2015"]
  }

④定义模块代码

//module1.js文件
// 分别暴露
export function foo() {
  console.log('foo() module1')
}
export function bar() {
  console.log('bar() module1')
}
//module2.js文件
// 统一暴露
function fun1() {
  console.log('fun1() module2')
}
function fun2() {
  console.log('fun2() module2')
}
export { fun1, fun2 }
//module3.js文件
// 默认暴露 可以暴露任意数据类项,暴露什么数据,接收到就是什么数据
export default () => {
  console.log('默认暴露')
}
// app.js文件
import { foo, bar } from './module1'
import { fun1, fun2 } from './module2'
import module3 from './module3'
foo()
bar()
fun1()
fun2()
module3()

⑤ 编译并在index.html中引入

  • 使用Babel将ES6编译为ES5代码(但包含CommonJS语法) : babel js/src -d js/lib
  • 使用Browserify编译js : browserify js/lib/app.js -o js/lib/bundle.js

然后在index.html文件中引入

 <script type="text/javascript" src="js/lib/bundle.js"></script>

最后得到如下结果:

此外第三方库(以jQuery为例)如何引入呢
首先安装依赖npm install jquery@1
然后在app.js文件中引入

//app.js文件
import { foo, bar } from './module1'
import { fun1, fun2 } from './module2'
import module3 from './module3'
import $ from 'jquery'

foo()
bar()
fun1()
fun2()
module3()
$('body').css('background', 'green')

三、总结

  • CommonJS规范主要用于服务端编程,加载模块是同步的,这并不适合在浏览器环境,因为同步意味着阻塞加载,浏览器资源是异步加载的,因此有了AMD CMD解决方案。
  • AMD规范在浏览器环境中异步加载模块,而且可以并行加载多个模块。不过,AMD规范开发成本高,代码的阅读和书写比较困难,模块定义方式的语义不顺畅。
  • CMD规范与AMD规范很相似,都用于浏览器编程,依赖就近,延迟执行,可以很容易在Node.js中运行。不过,依赖SPM 打包,模块的加载逻辑偏重
  • ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案

后记

花了很长时间(>10h)终于把"JS模块化"讲清楚,自己对模块化的认识又加深了一步,事实上,理解一件事并不难,难的是如何将一件事通俗分享给别人,并让别人也有所收获,一直以来我也是这样要求自己!文章如有错误和不正之处,欢迎指正和批评,同时也希望大家多多支持,我会有更大的创作动力!

参考文章

Javascript 面试核心考点(基础版)

引言

Javascript是前端面试的重点,本文重点梳理下 Javascript 中的常考知识点,然后就一些容易出现的题目进行解析。限于文章的篇幅,无法将知识点讲解的面面俱到,本文只罗列了一些重难点,如果想要了解更多内容欢迎点击我的博客

一、变量类型

1.JS 的数据类型分类

根据 JavaScript 中的变量类型传递方式,分为基本数据类型和引用数据类型。其中基本数据类型包括Undefined、Null、Boolean、Number、String、Symbol (ES6新增,表示独一无二的值),而引用数据类型统称为Object对象,主要包括对象、数组和函数。

在参数传递方式上,有所不同:

  • 函数的参数如果是简单类型,会将一个值类型的数值副本传到函数内部,函数内部不影响函数外部传递的参数变量

  • 如果是一个参数是引用类型,会将引用类型的地址值复制给传入函数的参数,函数内部修改会影响传递参数的引用对象。

题目:基本类型和引用类型的区别

基本类型和引用类型存储于内存的位置不同,基本类型直接存储在栈中,而引用类型的对象存储在堆中,与此同时,在栈中存储了指针,而这个指针指向正是堆中实体的起始位置。下面通过一个小题目,来看下两者的主要区别:

// 基本类型
var a = 10
var b = a
b = 20
console.log(a)  // 10
console.log(b)  // 20

上述代码中,a b都是值类型,两者分别修改赋值,相互之间没有任何影响。再看引用类型的例子:

// 引用类型
var a = {x: 10, y: 20}
var b = a
b.x = 100
b.y = 200
console.log(a)  // {x: 100, y: 200}
console.log(b)  // {x: 100, y: 200}

上述代码中,a b都是引用类型。在执行了b = a之后,修改b的属性值,a的也跟着变化。因为a和b都是引用类型,指向了同一个内存地址,即两者引用的是同一个值,因此b修改属性时,a的值随之改动

2.数据类型的判断

1)typeof

typeof返回一个表示数据类型的字符串,返回结果包括:number、boolean、string、symbol、object、undefined、function等7种数据类型,但不能判断null、array等

typeof Symbol(); // symbol 有效
typeof ''; // string 有效
typeof 1; // number 有效
typeof true; //boolean 有效
typeof undefined; //undefined 有效
typeof new Function(); // function 有效
typeof null; //object 无效
typeof [] ; //object 无效
typeof new Date(); //object 无效
typeof new RegExp(); //object 无效

2)instanceof

instanceof 是用来判断A是否为B的实例,表达式为:A instanceof B,如果A是B的实例,则返回true,否则返回false。instanceof 运算符用来测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性,但它不能检测null 和 undefined

[] instanceof Array; //true
{} instanceof Object;//true
new Date() instanceof Date;//true
new RegExp() instanceof RegExp//true
null instanceof Null//报错
undefined instanceof undefined//报错

3)constructor

constructor作用和instanceof非常相似。但constructor检测 Object与instanceof不一样,还可以处理基本数据类型的检测。
不过函数的 constructor 是不稳定的,这个主要体现在把类的原型进行重写,在重写的过程中很有可能出现把之前的constructor给覆盖了,这样检测出来的结果就是不准确的。

4)Object.prototype.toString.call()

Object.prototype.toString.call() 是最准确最常用的方式

Object.prototype.toString.call('') ;   // [object String]
Object.prototype.toString.call(1) ;    // [object Number]
Object.prototype.toString.call(true) ; // [object Boolean]
Object.prototype.toString.call(undefined) ; // [object Undefined]
Object.prototype.toString.call(null) ; // [object Null]
Object.prototype.toString.call(new Function()) ; // [object Function]
Object.prototype.toString.call(new Date()) ; // [object Date]
Object.prototype.toString.call([]) ; // [object Array]
Object.prototype.toString.call(new RegExp()) ; // [object RegExp]
Object.prototype.toString.call(new Error()) ; // [object Error]

3.浅拷贝与深拷贝

浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存

浅拷贝的实现方式(详见浅拷贝与深拷贝):

  • Object.assign():需注意的是目标对象只有一层的时候,是深拷贝
  • Array.prototype.concat()
  • Array.prototype.slice()

深拷贝就是在拷贝数据的时候,将数据的所有引用结构都拷贝一份。简单的说就是,在内存中存在两个数据结构完全相同又相互独立的数据,将引用型类型进行复制,而不是只复制其引用关系。

深拷贝的实现方式:

  • 热门的函数库lodash,也有提供_.cloneDeep用来做深拷贝
  • jquery 提供一个$.extend可以用来做深拷贝
  • JSON.parse(JSON.stringify())
  • 手写递归方法

递归实现深拷贝的原理:要拷贝一个数据,我们肯定要去遍历它的属性,如果这个对象的属性仍是对象,继续使用这个方法,如此往复。

//定义检测数据类型的功能函数
function checkedType(target) {
  return Object.prototype.toString.call(target).slice(8, -1)
}
//实现深度克隆---对象/数组
function clone(target) {
  //判断拷贝的数据类型
  //初始化变量result 成为最终克隆的数据
  let result,
    targetType = checkedType(target)
  if (targetType === 'Object') {
    result = {}
  } else if (targetType === 'Array') {
    result = []
  } else {
    return target
  }
  //遍历目标数据
  for (let i in target) {
    //获取遍历数据结构的每一项值。
    let value = target[i]
    //判断目标结构里的每一值是否存在对象/数组
    if (checkedType(value) === 'Object' || checkedType(value) === 'Array') {
      //对象/数组里嵌套了对象/数组
      //继续遍历获取到value值
      result[i] = clone(value)
    } else {
      //获取到value值是基本的数据类型或者是函数。
      result[i] = value
    }
  }
  return result
}

二、作用域和闭包

1.执行上下文和执行栈

执行上下文就是当前 JavaScript 代码被解析和执行时所在环境的抽象概念, JavaScript 中运行任何的代码都是在执行上下文中运行。
执行上下文的生命周期包括三个阶段:创建阶段→执行阶段→回收阶段,我们重点介绍创建阶段。

创建阶段(当函数被调用,但未执行任何其内部代码之前)会做以下三件事:

  • 创建变量对象:首先初始化函数的参数arguments,提升函数声明和变量声明。
  • 创建作用域链:下文会介绍
  • 确定this指向:下文会介绍
function test(arg){
    // 1. 形参 arg 是 "hi"
    // 2. 因为函数声明比变量声明优先级高,所以此时 arg 是 function
    console.log(arg);  
    var arg = 'hello'; // 3.var arg 变量声明被忽略, arg = 'hello'被执行
    function arg(){
	console.log('hello world') 
    }
    console.log(arg);  
}
test('hi');
/* 输出:
function arg() {
    console.log('hello world');
    }
hello 
*/

这是因为当函数执行的时候,首先会形成一个新的私有的作用域,然后依次按照如下的步骤执行:

  • 如果有形参,先给形参赋值
  • 进行私有作用域中的预解释,函数声明优先级比变量声明高,最后后者会被前者所覆盖,但是可以重新赋值
  • 私有作用域中的代码从上到下执行

函数多了,就有多个函数执行上下文,每次调用函数创建一个新的执行上下文,那如何管理创建的那么多执行上下文呢?

JavaScript 引擎创建了执行栈来管理执行上下文。可以把执行栈认为是一个存储函数调用的栈结构,遵循先进后出的原则


从上面的流程图,我们需要记住几个关键点:

  • JavaScript执行在单线程上,所有的代码都是排队执行。
  • 一开始浏览器执行全局的代码时,首先创建全局的执行上下文,压入执行栈的顶部。
  • 每当进入一个函数的执行就会创建函数的执行上下文,并且把它压入执行栈的顶部。当前函数执行完成后,当前函数的执行上下文出栈,并等待垃圾回收。
  • 浏览器的JS执行引擎总是访问栈顶的执行上下文。
  • 全局上下文只有唯一的一个,它在浏览器关闭时出栈。

2.作用域与作用域链

ES6 到来JavaScript 有全局作用域、函数作用域和块级作用域(ES6新增)。我们可以这样理解:作用域就是一个独立的地盘,让变量不会外泄、暴露出去。也就是说作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突
在介绍作用域链之前,先要了解下自由变量,如下代码中,console.log(a)要得到a变量,但是在当前的作用域中没有定义a(可对比一下b)。当前作用域没有定义的变量,这成为 自由变量。

var a = 100
function fn() {
    var b = 200
    console.log(a) // 这里的a在这里就是一个自由变量
    console.log(b)
}
fn()

自由变量的值如何得到 —— 向父级作用域(创建该函数的那个父级作用域)寻找。如果父级也没呢?再一层一层向上寻找,直到找到全局作用域还是没找到,就宣布放弃。这种一层一层的关系,就是作用域链 。

function F1() {
    var a = 100
    return function () {
        console.log(a)
    }
}
function F2(f1) {
    var a = 200
    console.log(f1())
}
var f1 = F1()
F2(f1) // 100 

上述代码中,自由变量a的值,从函数F1中查找而不是F2,这是因为当自由变量从作用域链中去寻找,依据的是函数定义时的作用域链,而不是函数执行时。

3.闭包是什么

闭包这个概念也是JavaScript中比较抽象的概念,我个人理解,闭包是就是函数中的函数(其他语言不能这样),里面的函数可以访问外面函数的变量,外面的变量的是这个内部函数的一部分。

闭包的作用:

  • 使用闭包可以访问函数中的变量。
  • 可以使变量长期保存在内存中,生命周期比较长

闭包不能滥用,否则会导致内存泄露,影响网页的性能。闭包使用完了后,要立即释放资源,将引用变量指向null。

闭包主要有两个应用场景:

  • 函数作为参数传递(见作用域部分例子)
  • 函数作为返回值(如下例)
function outer() {
  var num = 0 //内部变量
  return function add() {
    //通过return返回add函数,就可以在outer函数外访问了。
    num++ //内部函数有引用,作为add函数的一部分了
    console.log(num)
  }
}
var func1 = outer() //
func1() //实际上是调用add函数, 输出1
func1() //输出2
var func2 = outer()
func2() // 输出1
func2() // 输出2

4.this全面解析

先搞明白一个很重要的概念 —— this的值是在执行的时候才能确认,定义的时候不能确认! 为什么呢 —— 因为this是执行上下文环境的一部分,而执行上下文需要在代码执行之前确定,而不是定义的时候。看如下例子:

// 情况1
function foo() {
  console.log(this.a) //1
}
var a = 1
foo()

// 情况2
function fn(){
  console.log(this);
}
var obj={fn:fn};
obj.fn(); //this->obj

// 情况3
function CreateJsPerson(name,age){
//this是当前类的一个实例p1
this.name=name; //=>p1.name=name
this.age=age; //=>p1.age=age
}
var p1=new CreateJsPerson("尹华芝",48);

// 情况4
function add(c, d){
  return this.a + this.b + c + d;
}
var o = {a:1, b:3};
add.call(o, 5, 7); // 1 + 3 + 5 + 7 = 16
add.apply(o, [10, 20]); // 1 + 3 + 10 + 20 = 34

// 情况5
<button id="btn1">箭头函数this</button>
<script type="text/javascript">   
    let btn1 = document.getElementById('btn1');
    let obj = {
        name: 'kobe',
        age: 39,
        getName: function () {
            btn1.onclick = () => {
                console.log(this);//obj
            };
        }
    };
    obj.getName();
</script>

接下来我们逐一解释上面几种情况

  • 对于直接调用 foo 来说,不管 foo 函数被放在了什么地方,this 一定是 window
  • 对于 obj.foo() 来说,我们只需要记住,谁调用了函数,谁就是 this,所以在这个场景下 foo 函数中的 this 就是 obj 对象
  • 在构造函数模式中,类中(函数体中)出现的this.xxx=xxx中的this是当前类的一个实例
  • call、apply和bind:this 是第一个参数
  • 箭头函数this指向:箭头函数没有自己的this,看其外层的是否有函数,如果有,外层函数的this就是内部箭头函数的this,如果没有,则this是window。

三、异步

1.同步 vs 异步

同步,我的理解是一种线性执行的方式,执行的流程不能跨越。比如说话后在吃饭,吃完饭后在看手机,必须等待上一件事完了,才执行后面的事情。

异步,是一种并行处理的方式,不必等待一个程序执行完,可以执行其它的任务。比方说一个人边吃饭,边看手机,边说话,就是异步处理的方式。在程序中异步处理的结果通常使用回调函数来处理结果。

// 同步
console.log(100)
alert(200);
console.log(300)  //100 200 300
// 异步
console.log(100) 
setTimeout(function(){ 
  console.log(200) 
}) 
console.log(300) //100 300 200 

2.异步和单线程

JS 需要异步的根本原因是 JS 是单线程运行的,即在同一时间只能做一件事,不能“一心二用”。为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。

一个 Ajax 请求由于网络比较慢,请求需要 5 秒钟。如果是同步,这 5 秒钟页面就卡死在这里啥也干不了了。异步的话,就好很多了,5 秒等待就等待了,其他事情不耽误做,至于那 5 秒钟等待是网速太慢,不是因为 JS 的原因。

3.前端异步的场景

前端使用异步的场景

  • 定时任务:setTimeout,setInterval
  • 网络请求:ajax请求,动态加载
  • 事件绑定

4.Event Loop

一个完整的 Event Loop 过程,可以概括为以下阶段:

  • 一开始执行栈空,我们可以把执行栈认为是一个存储函数调用的栈结构,遵循先进后出的原则。micro 队列空,macro 队列里有且只有一个 script 脚本(整体代码)。

  • 全局上下文(script 标签)被推入执行栈,同步代码执行。在执行的过程中,会判断是同步任务还是异步任务,通过对一些接口的调用,可以产生新的 macro-task 与 micro-task,它们会分别被推入各自的任务队列里。同步代码执行完了,script 脚本会被移出 macro 队列,这个过程本质上是队列的 macro-task 的执行和出队的过程。

  • 上一步我们出队的是一个 macro-task,这一步我们处理的是 micro-task。但需要注意的是:当 macro-task 出队时,任务是一个一个执行的;而 micro-task 出队时,任务是一队一队执行的。因此,我们处理 micro 队列这一步,会逐个执行队列中的任务并把它出队,直到队列被清空。

  • 执行渲染操作,更新界面

  • 检查是否存在 Web worker 任务,如果有,则对其进行处理

  • 上述过程循环往复,直到两个队列都清空

接下来我们看道例子来介绍上面流程:

Promise.resolve().then(()=>{
  console.log('Promise1')  
  setTimeout(()=>{
    console.log('setTimeout2')
  },0)
})
setTimeout(()=>{
  console.log('setTimeout1')
  Promise.resolve().then(()=>{
    console.log('Promise2')    
  })
},0)

最后输出结果是Promise1,setTimeout1,Promise2,setTimeout2

  • 一开始执行栈的同步任务(这属于宏任务)执行完毕,会去查看是否有微任务队列,上题中存在(有且只有一个),然后执行微任务队列中的所有任务输出Promise1,同时会生成一个宏任务 setTimeout2
  • 然后去查看宏任务队列,宏任务 setTimeout1 在 setTimeout2 之前,先执行宏任务 setTimeout1,输出 setTimeout1
  • 在执行宏任务setTimeout1时会生成微任务Promise2 ,放入微任务队列中,接着先去清空微任务队列中的所有任务,输出 Promise2
  • 清空完微任务队列中的所有任务后,就又会去宏任务队列取一个,这回执行的是 setTimeout2

四、原型链与继承

1.原型和原型链

原型:在JavaScript中原型是一个prototype对象,用于表示类型之间的关系。

原型链:JavaScript万物都是对象,对象和对象之间也有关系,并不是孤立存在的。对象之间的继承关系,在JavaScript中是通过prototype对象指向父类对象,直到指向Object对象为止,这样就形成了一个原型指向的链条,专业术语称之为原型链。

var Person = function() {
  this.age = 18
  this.name = '匿名'
}
var Student = function() {}
//创建继承关系,父类实例作为子类原型
Student.prototype = new Person()
var s1 = new Student()
console.log(s1)

原型关系图:

当试图得到一个对象的某个属性时,如果这个对象本身没有这个属性,那么会去它的__proto__(即它的构造函数的prototype)中寻找。如果一直找到最上层都没有找到,那么就宣告失败,返回undefined。最上层是什么 —— Object.prototype.__proto__ === null

2.继承

介绍几种常见继承方式(如需了解更多,请点击JavaScript常见的六种继承方式):

  • 原型链+借用构造函数的组合继承
function Parent(value) {
  this.val = value
}
Parent.prototype.getValue = function() {
  console.log(this.val)
}
function Child(value) {
  Parent.call(this, value)
}
Child.prototype = new Parent()
const child = new Child(1)
child.getValue() // 1
child instanceof Parent // true

以上继承的方式核心是在子类的构造函数中通过 Parent.call(this) 继承父类的属性,然后改变子类的原型为 new Parent() 来继承父类的函数。

这种继承方式优点在于构造函数可以传参,不会与父类引用属性共享,可以复用父类的函数,但是也存在一个缺点就是在继承父类函数的时候调用了父类构造函数,导致子类的原型上多了不需要的父类属性,存在内存上的浪费。

  • 寄生组合继承:这种继承方式对上一种组合继承进行了优化
function Parent(value) {
  this.val = value
}
Parent.prototype.getValue = function() {
  console.log(this.val)
}
function Child(value) {
  Parent.call(this, value)
}
Child.prototype = Object.create(Parent.prototype, {
  constructor: {
    value: Child,
    enumerable: false,
    writable: true,
    configurable: true
  }
})
const child = new Child(1)
child.getValue() // 1
child instanceof Parent // true

以上继承实现的核心就是将父类的原型赋值给了子类,并且将构造函数设置为子类,这样既解决了无用的父类属性问题,还能正确的找到子类的构造函数。

  • ES6中class 的继承

ES6中引入了class关键字,class可以通过extends关键字实现继承,还可以通过static关键字定义类的静态方法,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。需要注意的是,class关键字只是原型的语法糖,JavaScript继承仍然是基于原型实现的

class Parent {
  constructor(value) {
    this.val = value
  }
  getValue() {
    console.log(this.val)
  }
}
class Child extends Parent {
  constructor(value) {
    super(value)
    this.val = value
  }
}
let child = new Child(1)
child.getValue() // 1
child instanceof Parent // true

class 实现继承的核心在于使用 extends 表明继承自哪个父类,并且在子类构造函数中必须调用 super,因为这段代码可以看成 Parent.call(this, value)

五、DOM操作与BOM操作

1.DOM操作

当网页被加载时,浏览器会创建页面的文档对象模型(DOM),我们可以认为 DOM 就是 JS 能识别的 HTML 结构,一个普通的 JS 对象或者数组。接下来我们介绍常见DOM操作:

  • 新增节点和移动节点
var div1 = document.getElementById('div1')
// 添加新节点
var p1 = document.createElement('p')
p1.innerHTML = 'this is p1'
div1.appendChild(p1) // 添加新创建的元素
// 移动已有节点。注意,这里是“移动”,并不是拷贝
var p2 = document.getElementById('p2')
div1.appendChild(p2)
  • 获取父元素
var div1 = document.getElementById('div1')
var parent = div1.parentElement
  • 获取子元素
var div1 = document.getElementById('div1')
var child = div1.childNodes
  • 删除节点
var div1 = document.getElementById('div1')
var child = div1.childNodes
div1.removeChild(child[0])

2.DOM事件模型和事件流

DOM事件模型分为捕获和冒泡。一个事件发生后,会在子元素和父元素之间传播(propagation)。这种传播分成三个阶段。

(1)捕获阶段:事件从window对象自上而下向目标节点传播的阶段;

(2)目标阶段:真正的目标节点正在处理事件的阶段;

(3)冒泡阶段:事件从目标节点自下而上向window对象传播的阶段。

DOM事件捕获的具体流程

捕获是从上到下,事件先从window对象,然后再到document(对象),然后是html标签(通过document.documentElement获取html标签),然后是body标签(通过document.body获取body标签),然后按照普通的html结构一层一层往下传,最后到达目标元素。

接下来我们看个事件冒泡的例子:

// 事件冒泡
<div id="outer">
    <div id="inner"></div>
</div>
......
window.onclick = function() {
    console.log('window');
};
document.onclick = function() {
    console.log('document');
};
document.documentElement.onclick = function() {
    console.log('html');
};
document.body.onclick = function() {
    console.log('body');
}
outer.onclick = function(ev) {
    console.log('outer');
};
inner.onclick = function(ev) {
    console.log('inner');
};

如何阻止冒泡?

通过event.stopPropagation() 方法阻止事件冒泡到父元素,阻止任何父事件处理程序被执行。
我们可以在上例中inner元素的click事件上,添加event.stopPropagation()这句话后,就阻止了父事件的执行,最后只打印了'inner'。

 inner.onclick = function(ev) {
    console.log('inner')
    ev.stopPropagation()
}

3.事件代理(事件委托)

由于事件会在冒泡阶段向上传播到父节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件。这种方法叫做事件的代理。

我们设定一种场景,如下代码,一个<div>中包含了若干个<a>,而且还能继续增加。那如何快捷方便地为所有<a>绑定事件呢?

<div id="div1">
    <a href="#">a1</a>
    <a href="#">a2</a>
    <a href="#">a3</a>
    <a href="#">a4</a>
</div>
<button>点击增加一个 a 标签</button>

如果给每个<a>标签一一都绑定一个事件,那对于内存消耗是非常大的。借助事件代理,我们只需要给父容器div绑定方法即可,这样不管点击的是哪一个后代元素,都会根据冒泡传播的传递机制,把父容器的click行为触发,然后把对应的方法执行,根据事件源,我们可以知道点击的是谁,从而完成不同的事。

var div1 = document.getElementById('div1')
div1.addEventListener('click', function (e) {
    // e.target 可以监听到触发点击事件的元素是哪一个
    var target = e.target
    if (e.nodeName === 'A') {
        // 点击的是 <a> 元素
        alert(target.innerHTML)
    }
})

最后,使用代理的优点如下:

  • 使代码简洁
  • 减少浏览器的内存占用

4.BOM 操作

BOM(浏览器对象模型)是浏览器本身的一些信息的设置和获取,例如获取浏览器的宽度、高度,设置让浏览器跳转到哪个地址。

  • window.screen对象:包含有关用户屏幕的信息
  • window.location对象:用于获得当前页面的地址(URL),并把浏览器重定向到新的页面
  • window.history对象:浏览历史的前进后退等
  • window.navigator对象:常常用来获取浏览器信息、是否移动端访问等等

获取屏幕的宽度和高度

console.log(screen.width)
console.log(screen.height)

获取网址、协议、path、参数、hash 等

// 例如当前网址是 https://juejin.im/timeline/frontend?a=10&b=10#some
console.log(location.href)  // https://juejin.im/timeline/frontend?a=10&b=10#some
console.log(location.protocol) // https:
console.log(location.pathname) // /timeline/frontend
console.log(location.search) // ?a=10&b=10
console.log(location.hash) // #some

另外,还有调用浏览器的前进、后退功能等

history.back()
history.forward()

获取浏览器特性(即俗称的UA)然后识别客户端,例如判断是不是 Chrome 浏览器

var ua = navigator.userAgent
var isChrome = ua.indexOf('Chrome')
console.log(isChrome)

5.Ajax与跨域

Ajax 是一种异步请求数据的一种技术,对于改善用户的体验和程序的性能很有帮助。
简单地说,在不需要重新刷新页面的情况下,Ajax 通过异步请求加载后台数据,并在网页上呈现出来。常见运用场景有表单验证是否登入成功、百度搜索下拉框提示和快递单号查询等等。Ajax的目的是提高用户体验,较少网络数据的传输量

如何手写 XMLHttpRequest 不借助任何库

var xhr = new XMLHttpRequest()
xhr.onreadystatechange = function () {
    // 这里的函数异步执行
    if (xhr.readyState == 4) {
        if (xhr.status == 200) {
            alert(xhr.responseText)
        }
    }
}
xhr.open("GET", "/api", false)
xhr.send(null)

因为浏览器出于安全考虑,有同源策略。也就是说,如果协议、域名或者端口有一个不同就是跨域,Ajax 请求会失败。

那么是出于什么安全考虑才会引入这种机制呢? 其实主要是用来防止 CSRF 攻击的。简单点说,CSRF 攻击是利用用户的登录态发起恶意请求。

然后我们来考虑一个问题,请求跨域了,那么请求到底发出去没有? 请求必然是发出去了,但是浏览器拦截了响应。

常见的几种跨域解决方案(具体如何实现详见九种跨域方式实现原理(完整版)):

  • JSONP:利用同源策略对<script>标签不受限制,不过只支持GET请求
  • CORS:实现 CORS 通信的关键是后端,服务端设置 Access-Control-Allow-Origin 就可以开启,备受推崇的跨域解决方案,比 JSONP 简单许多
  • Node中间件代理或nginx反向代理:主要是通过同源策略对服务器不加限制

6.存储

sessionStorage 、localStorage 和 cookie 之间的区别

  • 共同点:都是保存在浏览器端,且都遵循同源策略。
  • 不同点:在于生命周期与作用域的不同

作用域:localStorage只要在相同的协议、相同的主机名、相同的端口下,就能读取/修改到同一份localStorage数据。sessionStorage比localStorage更严苛一点,除了协议、主机名、端口外,还要求在同一窗口(也就是浏览器的标签页)下


生命周期:localStorage 是持久化的本地存储,存储在其中的数据是永远不会过期的,使其消失的唯一办法是手动删除;而 sessionStorage 是临时性的本地存储,它是会话级别的存储,当会话结束(页面被关闭)时,存储内容也随之被释放。

三者区别

六、模块化

几种常见模块化规范的简介(详情请点击前端模块化详解(完整版)):

  • CommonJS规范主要用于服务端编程,加载模块是同步的,这并不适合在浏览器环境,因为同步意味着阻塞加载,浏览器资源是异步加载的,因此有了AMD CMD解决方案
  • AMD规范在浏览器环境中异步加载模块,而且可以并行加载多个模块。不过,AMD规范开发成本高,代码的阅读和书写比较困难,模块定义方式的语义不顺畅。
  • CMD规范与AMD规范很相似,都用于浏览器编程,依赖就近,延迟执行,可以很容易在Node.js中运行。不过,依赖SPM 打包,模块的加载逻辑偏重
  • ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案

欢迎关注公众号:前端工匠,你的成长我们一起见证!

参考资料

解读HTTP/2 及 HTTP/3特性

前言

HTTP/2 相比于 HTTP/1.1,可以说是大幅度提高了网页的性能,只需要升级到该协议就可以减少很多之前需要做的性能优化工作,当然兼容问题以及如何优雅降级应该是国内还不普遍使用的原因之一。

虽然 HTTP/2 提高了网页的性能,但是并不代表它已经是完美的了,HTTP/3 就是为了解决 HTTP/2 所存在的一些问题而被推出来的。

一、HTTP/1.1发明以来发生了哪些变化?

如果仔细观察打开那些最流行的网站首页所需要下载的资源的话,会发现一个非常明显的趋势。 近年来加载网站首页需要的下载的数据量在逐渐增加,并已经超过了2100K。但在这里我们更应该关心的是:平均每个页面为了完成显示与渲染所需要下载的资源数已经超过了100个。

正如下图所示,从2011年以来,传输数据大小与平均请求资源数量不断持续增长,并没有减缓的迹象。该图表中绿色直线展示了传输数据大小的增长,红色直线展示了平均请求资源数量的增长。

HTTP/1.1自从1997年发布以来,我们已经使用HTTP/1.x 相当长一段时间了,但是随着近十年互联网的爆炸式发展,从当初网页内容以文本为主,到现在以富媒体(如图片、声音、视频)为主,而且对页面内容实时性高要求的应用越来越多(比如聊天、视频直播),于是当时协议规定的某些特性,已经无法满足现代网络的需求了。

二、HTTP/1.1的缺陷

1.高延迟--带来页面加载速度的降低

虽然近几年来网络带宽增长非常快,然而我们却并没有看到网络延迟有对应程度的降低。网络延迟问题主要由于队头阻塞(Head-Of-Line Blocking),导致带宽无法被充分利用

队头阻塞是指当顺序发送的请求序列中的一个请求因为某种原因被阻塞时,在后面排队的所有请求也一并被阻塞,会导致客户端迟迟收不到数据。针对队头阻塞,人们尝试过以下办法来解决:

  • 将同一页面的资源分散到不同域名下,提升连接上限。 Chrome有个机制,对于同一个域名,默认允许同时建立 6 个 TCP持久连接,使用持久连接时,虽然能公用一个TCP管道,但是在一个管道中同一时刻只能处理一个请求,在当前的请求没有结束之前,其他的请求只能处于阻塞状态。另外如果在同一个域名下同时有10个请求发生,那么其中4个请求会进入排队等待状态,直至进行中的请求完成。
  • Spriting合并多张小图为一张大图,再用JavaScript或者CSS将小图重新“切割”出来的技术。
  • 内联(Inlining)是另外一种防止发送很多小图请求的技巧,将图片的原始数据嵌入在CSS文件里面的URL里,减少网络请求次数。
.icon1 {
    background: url(data:image/png;base64,<data>) no-repeat;
  }
.icon2 {
    background: url(data:image/png;base64,<data>) no-repeat;
  }
  • 拼接(Concatenation)将多个体积较小的JavaScript使用webpack等工具打包成1个体积更大的JavaScript文件,但如果其中1个文件的改动就会导致大量数据被重新下载多个文件。

2.无状态特性--带来的巨大HTTP头部

由于报文Header一般会携带"User Agent""Cookie""Accept""Server"等许多固定的头字段(如下图),多达几百字节甚至上千字节,但Body却经常只有几十字节(比如GET请求、
204/301/304响应),成了不折不扣的“大头儿子”。Header里携带的内容过大,在一定程度上增加了传输的成本。更要命的是,成千上万的请求响应报文里有很多字段值都是重复的,非常浪费。

3.明文传输--带来的不安全性

HTTP/1.1在传输数据时,所有传输的内容都是明文,客户端和服务器端都无法验证对方的身份,这在一定程度上无法保证数据的安全性。

你有没有听说过"免费WiFi陷阱”之类的新闻呢?
黑客就是利用了HTTP明文传输的缺点,在公共场所架设一个WiFi热点开始“钓鱼”,诱骗网民上网。一旦你连上了这个WiFi热点,所有的流量都会被截获保存,里面如果有银行卡号、网站密码等敏感信息的话那就危险了,黑客拿到了这些数据就可以冒充你为所欲为。

4.不支持服务器推送消息

三、SPDY 协议与 HTTP/2 简介

1.SPDY 协议

上面我们提到,由于HTTP/1.x的缺陷,我们会引入雪碧图、将小图内联、使用多个域名等等的方式来提高性能。不过这些优化都绕开了协议,直到2009年,谷歌公开了自行研发的 SPDY 协议,主要解决HTTP/1.1效率不高的问题。谷歌推出SPDY,才算是正式改造HTTP协议本身。降低延迟,压缩header等等,SPDY的实践证明了这些优化的效果,也最终带来HTTP/2的诞生。

HTTP/1.1有两个主要的缺点:安全不足和性能不高,由于背负着 HTTP/1.x 庞大的历史包袱,所以协议的修改,兼容性是首要考虑的目标,否则就会破坏互联网上无数现有的资产。如上图所示,
SPDY位于HTTP之下,TCP和SSL之上,这样可以轻松兼容老版本的HTTP协议(将HTTP1.x的内容封装成一种新的frame格式),同时可以使用已有的SSL功能。

SPDY 协议在Chrome浏览器上证明可行以后,就被当作 HTTP/2 的基础,主要特性都在 HTTP/2 之中得到继承。

2.HTTP/2 简介

2015年,HTTP/2 发布。HTTP/2是现行HTTP协议(HTTP/1.x)的替代,但它不是重写,HTTP方法/状态码/语义都与HTTP/1.x一样。HTTP/2基于SPDY,专注于性能,最大的一个目标是在用户和网站间只用一个连接(connection)。从目前的情况来看,国内外一些排名靠前的站点基本都实现了HTTP/2的部署,使用HTTP/2能带来20%~60%的效率提升。

HTTP/2由两个规范(Specification)组成:

  1. Hypertext Transfer Protocol version 2 - RFC7540
  2. HPACK - Header Compression for HTTP/2 - RFC7541

四、HTTP/2 新特性

1.二进制传输

HTTP/2传输数据量的大幅减少,主要有两个原因:以二进制方式传输和Header 压缩。我们先来介绍二进制传输,HTTP/2 采用二进制格式传输数据,而非HTTP/1.x 里纯文本形式的报文 ,二进制协议解析起来更高效。 HTTP/2 将请求和响应数据分割为更小的帧,并且它们采用二进制编码

它把TCP协议的部分特性挪到了应用层,把原来的"Header+Body"的消息"打散"为数个小片的二进制"帧"(Frame),用"HEADERS"帧存放头数据、"DATA"帧存放实体数据。HTP/2数据分帧后"Header+Body"的报文结构就完全消失了,协议看到的只是一个个的"碎片"。

HTTP/2 中,同域名下所有通信都在单个连接上完成,该连接可以承载任意数量的双向数据流。每个数据流都以消息的形式发送,而消息又由一个或多个帧组成。多个帧之间可以乱序发送,根据帧首部的流标识可以重新组装

2.Header 压缩

HTTP/2并没有使用传统的压缩算法,而是开发了专门的"HPACK”算法,在客户端和服务器两端建立“字典”,用索引号表示重复的字符串,还采用哈夫曼编码来压缩整数和字符串,可以达到50%~90%的高压缩率。

具体来说:

  • 在客户端和服务器端使用“首部表”来跟踪和存储之前发送的键-值对,对于相同的数据,不再通过每次请求和响应发送;
  • 首部表在HTTP/2的连接存续期内始终存在,由客户端和服务器共同渐进地更新;
  • 每个新的首部键-值对要么被追加到当前表的末尾,要么替换表中之前的值

例如下图中的两个请求, 请求一发送了所有的头部字段,第二个请求则只需要发送差异数据,这样可以减少冗余数据,降低开销

3.多路复用

在 HTTP/2 中引入了多路复用的技术。多路复用很好的解决了浏览器限制同一个域名下的请求数量的问题,同时也接更容易实现全速传输,毕竟新开一个 TCP 连接都需要慢慢提升传输速度。

大家可以通过 该链接 直观感受下 HTTP/2 比 HTTP/1 到底快了多少。

在 HTTP/2 中,有了二进制分帧之后,HTTP /2 不再依赖 TCP 链接去实现多流并行了,在 HTTP/2中,

  • 同域名下所有通信都在单个连接上完成。
  • 单个连接可以承载任意数量的双向数据流。
  • 数据流以消息的形式发送,而消息又由一个或多个帧组成,多个帧之间可以乱序发送,因为根据帧首部的流标识可以重新组装。

这一特性,使性能有了极大提升:

  • 同个域名只需要占用一个 TCP 连接,使用一个连接并行发送多个请求和响应,这样整个页面资源的下载过程只需要一次慢启动,同时也避免了多个TCP连接竞争带宽所带来的问题。
  • 并行交错地发送多个请求/响应,请求/响应之间互不影响。
  • 在HTTP/2中,每个请求都可以带一个31bit的优先值,0表示最高优先级, 数值越大优先级越低。有了这个优先值,客户端和服务器就可以在处理不同的流时采取不同的策略,以最优的方式发送流、消息和帧。


如上图所示,多路复用的技术可以只通过一个 TCP 连接就可以传输所有的请求数据。

4.Server Push

HTTP2还在一定程度上改变了传统的“请求-应答”工作模式,服务器不再是完全被动地响应请求,也可以新建“流”主动向客户端发送消息。比如,在浏览器刚请求HTML的时候就提前把可能会用到的JS、CSS文件发给客户端,减少等待的延迟,这被称为"服务器推送"( Server Push,也叫 Cache push)

例如下图所示,服务端主动把JS和CSS文件推送给客户端,而不需要客户端解析HTML时再发送这些请求。

另外需要补充的是,服务端可以主动推送,客户端也有权利选择是否接收。如果服务端推送的资源已经被浏览器缓存过,浏览器可以通过发送RST_STREAM帧来拒收。主动推送也遵守同源策略,换句话说,服务器不能随便将第三方资源推送给客户端,而必须是经过双方确认才行。

5.提高安全性

出于兼容的考虑,HTTP/2延续了HTTP/1的“明文”特点,可以像以前一样使用明文传输数据,不强制使用加密通信,不过格式还是二进制,只是不需要解密。

但由于HTTPS已经是大势所趋,而且主流的浏览器Chrome、Firefox等都公开宣布只支持加密的HTTP/2,所以“事实上”的HTTP/2是加密的。也就是说,互联网上通常所能见到的HTTP/2都是使用"https”协议名,跑在TLS上面。HTTP/2协议定义了两个字符串标识符:“h2"表示加密的HTTP/2,“h2c”表示明文的HTTP/2。

六、HTTP/3 新特性

1.HTTP/2 的缺点

虽然 HTTP/2 解决了很多之前旧版本的问题,但是它还是存在一个巨大的问题,主要是底层支撑的 TCP 协议造成的。HTTP/2的缺点主要有以下几点:

  • TCP 以及 TCP+TLS建立连接的延时

HTTP/2都是使用TCP协议来传输的,而如果使用HTTPS的话,还需要使用TLS协议进行安全传输,而使用TLS也需要一个握手过程,这样就需要有两个握手延迟过程

①在建立TCP连接的时候,需要和服务器进行三次握手来确认连接成功,也就是说需要在消耗完1.5个RTT之后才能进行数据传输。

②进行TLS连接,TLS有两个版本——TLS1.2和TLS1.3,每个版本建立连接所花的时间不同,大致是需要1~2个RTT。

总之,在传输数据之前,我们需要花掉 3~4 个 RTT。

  • TCP的队头阻塞并没有彻底解决

上文我们提到在HTTP/2中,多个请求是跑在一个TCP管道中的。但当出现了丢包时,HTTP/2 的表现反倒不如 HTTP/1 了。因为TCP为了保证可靠传输,有个特别的“丢包重传”机制,丢失的包必须要等待重新传输确认,HTTP/2出现丢包时,整个 TCP 都要开始等待重传,那么就会阻塞该TCP连接中的所有请求(如下图)。而对于 HTTP/1.1 来说,可以开启多个 TCP 连接,出现这种情况反到只会影响其中一个连接,剩余的 TCP 连接还可以正常传输数据。

读到这里,可能就会有人考虑为什么不直接去修改 TCP 协议?其实这已经是一件不可能完成的任务了。因为 TCP 存在的时间实在太长,已经充斥在各种设备中,并且这个协议是由操作系统实现的,更新起来不大现实。

2.HTTP/3简介

Google 在推SPDY的时候就已经意识到了这些问题,于是就另起炉灶搞了一个基于 UDP 协议的“QUIC”协议,让HTTP跑在QUIC上而不是TCP上。
而这个“HTTP over QUIC”就是HTTP协议的下一个大版本,HTTP/3。它在HTTP/2的基础上又实现了质的飞跃,真正“完美”地解决了“队头阻塞”问题。

QUIC 虽然基于 UDP,但是在原本的基础上新增了很多功能,接下来我们重点介绍几个QUIC新功能。不过HTTP/3目前还处于草案阶段,正式发布前可能会有变动,所以本文尽量不涉及那些不稳定的细节。

3.QUIC新功能

上面我们提到QUIC基于UDP,而UDP是“无连接”的,根本就不需要“握手”和“挥手”,所以就比TCP来得快。此外QUIC也实现了可靠传输,保证数据一定能够抵达目的地。它还引入了类似HTTP/2的“流”和“多路复用”,单个“流"是有序的,可能会因为丢包而阻塞,但其他“流”不会受到影响。具体来说QUIC协议有以下特点:

  • 实现了类似TCP的流量控制、传输可靠性的功能。

虽然UDP不提供可靠性的传输,但QUIC在UDP的基础之上增加了一层来保证数据可靠性传输。它提供了数据包重传、拥塞控制以及其他一些TCP中存在的特性。

  • 实现了快速握手功能。

由于QUIC是基于UDP的,所以QUIC可以实现使用0-RTT或者1-RTT来建立连接,这意味着QUIC可以用最快的速度来发送和接收数据,这样可以大大提升首次打开页面的速度。0RTT 建连可以说是 QUIC 相比 HTTP2 最大的性能优势

  • 集成了TLS加密功能。

目前QUIC使用的是TLS1.3,相较于早期版本TLS1.3有更多的优点,其中最重要的一点是减少了握手所花费的RTT个数。

  • 多路复用,彻底解决TCP中队头阻塞的问题

和TCP不同,QUIC实现了在同一物理连接上可以有多个独立的逻辑数据流(如下图)。实现了数据流的单独传输,就解决了TCP中队头阻塞的问题。

七、总结

  • HTTP/1.1有两个主要的缺点:安全不足和性能不高。
  • HTTP/2完全兼容HTTP/1,是“更安全的HTTP、更快的HTTPS",头部压缩、多路复用等技术可以充分利用带宽,降低延迟,从而大幅度提高上网体验;
  • QUIC 基于 UDP 实现,是 HTTP/3 中的底层支撑协议,该协议基于 UDP,又取了 TCP 中的精华,实现了即快又可靠的协议

参考文章

函数节流和防抖

前言

事件的触发权很多时候都属于用户,有些情况下会产生问题:

  • 向后台发送数据,用户频繁触发,对服务器造成压力

  • 一些浏览器事件:window.onresize、window.mousemove等,触发的频率非常高,会造成浏览器性能问题

如果你碰到这些问题,那就需要用到函数节流和防抖了。

一、函数节流(throttle)

函数节流:一个函数执行一次后,只有大于设定的执行周期后才会执行第二次
有个需要频繁触发函数,出于优化性能角度,在规定时间内,只让函数触发的第一次生效,后面不生效。

1.如何实现

其原理是用时间戳来判断是否已到回调该执行时间,记录上次执行的时间戳,然后每次触发 scroll 事件执行回调,回调中判断当前时间戳距离上次执行时间戳的间隔是否已经到达 规定时间段,如果是,则执行,并更新上次执行的时间戳,如此循环;

 html,
 body {
    height: 500%; // 让其出现滚动条
}
function throttle(fn, delay) {
    // 记录上一次函数触发的时间
    var lastTime = 0;
    return function() {
        // 记录当前函数触发的时间
        var nowTime = Date.now();
        if (nowTime - lastTime > delay) {
        // 修正this指向问题
            fn.call(this);
        // 同步时间
          lastTime = nowTime;
        }
    }
}
document.onscroll = throttle(function() { console.log('scroll事件被触发了' + Date.now()) }, 200)

上例中用到了闭包的特性--可以使变量lastTime的值长期保存在内存中。

2.函数节流的应用场景

需要间隔一定时间触发回调来控制函数调用频率:

  • DOM 元素的拖拽功能实现(mousemove)
  • 搜索联想(keyup)
  • 计算鼠标移动的距离(mousemove)
  • Canvas 模拟画板功能(mousemove)
  • 射击游戏的 mousedown/keydown 事件(单位时间只能发射一颗子弹)
  • 监听滚动事件判断是否到页面底部自动加载更多:给 scroll 加了 debounce 后,只有用户停止滚动后,才会判断是否到了页面底部;如果是 throttle 的话,只要页面滚动就会间隔一段时间判断一次

二、函数防抖(debounce)

防抖函数:一个需要频繁触发的函数,在规定时间内,只让最后一次生效,前面的不生效。

1.如何实现

其原理就第一次调用函数,创建一个定时器,在指定的时间间隔之后运行代码。当第二次调用该函数时,它会清除前一次的定时器并设置另一个。如果前一个定时器已经执行过了,这个操作就没有任何意义。然而,如果前一个定时器尚未执行,其实就是将其替换为一个新的定时器,然后延迟一定时间再执行。

<button id='btn'>按钮</button>
<script type="text/javascript">
function debounce(fn, delay) {
    // 记录上一次的延时器
   var timer = null;
    return function() {
    // 清除上一次延时器
    clearTimeout(timer)
    timer = setTimeout(function() {
            fn.apply(this)
        }, delay)
    }
}
document.getElementById('btn').onclick = debounce(function() {
    console.log('点击事件被触发' + Date.now())
}, 1000)
</script>

上例中也用到了闭包的特性--可以使变量timer的值长期保存在内存中。

2.函数防抖的应用场景

对于连续的事件响应我们只需要执行一次回调:

  • 每次 resize/scroll 触发统计事件
  • 文本输入的验证(连续输入文字后发送 AJAX 请求进行验证,验证一次就好)

三、总结

函数节流和函数去抖的核心其实就是限制某一个方法被频繁触发,而一个方法之所以会被频繁触发,大多数情况下是因为 DOM 事件的监听回调,而这也是函数节流以及防抖多数情况下的应用场景。

参考文章

函数节流与函数防抖

JavaScript 函数节流和函数去抖应用场景辨析

函数节流、函数防抖实现原理分析

JavaScript中的垃圾回收和内存泄漏

前言

程序的运行需要内存。只要程序提出要求,操作系统或者运行时就必须供给内存。所谓的内存泄漏简单来说是不再用到的内存,没有及时释放。为了更好避免内存泄漏,我们先介绍Javascript垃圾回收机制。

在C与C++等语言中,开发人员可以直接控制内存的申请和回收。但是在Java、C#、JavaScript语言中,变量的内存空间的申请和释放都由程序自己处理,开发人员不需要关心。也就是说Javascript具有自动垃圾回收机制(Garbage Collecation)。

一、垃圾回收的必要性

下面这段话引自《JavaScript权威指南(第四版)》

  由于字符串、对象和数组没有固定大小,所有当他们的大小已知时,才能对他们进行动态的存储分配。JavaScript程序每次创建字符串、数组或对象时,解释器都必须分配内存来存储那个实体。只要像这样动态地分配了内存,最终都要释放这些内存以便他们能够被再用,否则,JavaScript的解释器将会消耗完系统中所有可用的内存,造成系统崩溃。

这段话解释了为什么需要系统需要垃圾回收,JavaScript不像C/C++,它有自己的一套垃圾回收机制。

JavaScript垃圾回收的机制很简单:找出不再使用的变量,然后释放掉其占用的内存,但是这个过程不是时时的,因为其开销比较大,所以垃圾回收器会按照固定的时间间隔周期性的执行。

var a = "浪里行舟";
var b = "前端工匠";
var a = b; //重写a

这段代码运行之后,“浪里行舟”这个字符串失去了引用(之前是被a引用),系统检测到这个事实之后,就会释放该字符串的存储空间以便这些空间可以被再利用。

二、垃圾回收机制

垃圾回收机制怎么知道,哪些内存不再需要呢?

垃圾回收有两种方法:标记清除、引用计数。引用计数不太常用,标记清除较为常用。

1.标记清除

这是javascript中最常用的垃圾回收方式。当变量进入执行环境是,就标记这个变量为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到他们。当变量离开环境时,则将其标记为“离开环境”。

垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记。然后,它会去掉环境中的变量以及被环境中的变量引用的标记。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后。垃圾收集器完成内存清除工作,销毁那些带标记的值,并回收他们所占用的内存空间。

我们用个例子,解释下这个方法:

var m = 0,n = 19 // 把 m,n,add() 标记为进入环境。
add(m, n) // 把 a, b, c标记为进入环境。
console.log(n) // a,b,c标记为离开环境,等待垃圾回收。
function add(a, b) {
  a++
  var c = a + b
  return c
}

2.引用计数

所谓"引用计数"是指语言引擎有一张"引用表",保存了内存里面所有的资源(通常是各种值)的引用次数。如果一个值的引用次数是0,就表示这个值不再用到了,因此可以将这块内存释放。


上图中,左下角的两个值,没有任何引用,所以可以释放。

如果一个值不再需要了,引用数却不为0,垃圾回收机制无法释放这块内存,从而导致内存泄漏。

var arr = [1, 2, 3, 4];
arr = [2, 4, 5]
console.log('浪里行舟');

上面代码中,数组[1, 2, 3, 4]是一个值,会占用内存。变量arr是仅有的对这个值的引用,因此引用次数为1。尽管后面的代码没有用到arr,它还是会持续占用内存。至于如何释放内存,我们下文介绍。

第三行代码中,数组[1, 2, 3, 4]引用的变量arr又取得了另外一个值,则数组[1, 2, 3, 4]的引用次数就减1,此时它引用次数变成0,则说明没有办法再访问这个值了,因而就可以将其所占的内存空间给收回来。

但是引用计数有个最大的问题: 循环引用

function func() {
    let obj1 = {};
    let obj2 = {};

    obj1.a = obj2; // obj1 引用 obj2
    obj2.a = obj1; // obj2 引用 obj1
}

当函数 func 执行结束后,返回值为 undefined,所以整个函数以及内部的变量都应该被回收,但根据引用计数方法,obj1 和 obj2 的引用次数都不为 0,所以他们不会被回收。

要解决循环引用的问题,最好是在不使用它们的时候手工将它们设为空。上面的例子可以这么做:

obj1 = null;
obj2 = null;

三、哪些情况会引起内存泄漏?

虽然JavaScript会自动垃圾收集,但是如果我们的代码写法不当,会让变量一直处于“进入环境”的状态,无法被回收。下面列一下内存泄漏常见的几种情况:

1.意外的全局变量

function foo(arg) {
    bar = "this is a hidden global variable";
}

bar没被声明,会变成一个全局变量,在页面关闭之前不会被释放。

另一种意外的全局变量可能由 this 创建:

function foo() {
    this.variable = "potential accidental global";
}
// foo 调用自己,this 指向了全局对象(window)
foo();

在 JavaScript 文件头部加上 'use strict',可以避免此类错误发生。启用严格模式解析 JavaScript ,避免意外的全局变量。

2.被遗忘的计时器或回调函数

var someResource = getData();
setInterval(function() {
    var node = document.getElementById('Node');
    if(node) {
        // 处理 node 和 someResource
        node.innerHTML = JSON.stringify(someResource));
    }
}, 1000);

这样的代码很常见,如果id为Node的元素从DOM中移除,该定时器仍会存在,同时,因为回调函数中包含对someResource的引用,定时器外面的someResource也不会被释放。

3.闭包

function bindEvent(){
  var obj=document.createElement('xxx')
  obj.onclick=function(){
    // Even if it is a empty function
  }
}

闭包可以维持函数内局部变量,使其得不到释放。上例定义事件回调时,由于是函数内定义函数,并且内部函数--事件回调引用外部函数,形成了闭包。

// 将事件处理函数定义在外面
function bindEvent() {
  var obj = document.createElement('xxx')
  obj.onclick = onclickHandler
}
// 或者在定义事件处理函数的外部函数中,删除对dom的引用
function bindEvent() {
  var obj = document.createElement('xxx')
  obj.onclick = function() {
    // Even if it is a empty function
  }
  obj = null
}

解决之道,将事件处理函数定义在外部,解除闭包,或者在定义事件处理函数的外部函数中,删除对dom的引用。

4.没有清理的DOM元素引用

有时,保存 DOM 节点内部数据结构很有用。假如你想快速更新表格的几行内容,把每一行 DOM 存成字典(JSON 键值对)或者数组很有意义。此时,同样的 DOM 元素存在两个引用:一个在 DOM 树中,另一个在字典中。将来你决定删除这些行时,需要把两个引用都清除。

var elements = {
    button: document.getElementById('button'),
    image: document.getElementById('image'),
    text: document.getElementById('text')
};
function doStuff() {
    image.src = 'http://some.url/image';
    button.click();
    console.log(text.innerHTML);
}
function removeButton() {
    document.body.removeChild(document.getElementById('button'));
    // 此时,仍旧存在一个全局的 #button 的引用
    // elements 字典。button 元素仍旧在内存中,不能被 GC 回收。
}

虽然我们用removeChild移除了button,但是还在elements对象里保存着#button的引用,换言之,DOM元素还在内存里面。

四、内存泄漏的识别方法

新版本的chrome在 performance 中查看:


步骤:

  • 打开开发者工具 Performance
  • 勾选 Screenshots 和 memory
  • 左上角小圆点开始录制(record)
  • 停止录制

图中 Heap 对应的部分就可以看到内存在周期性的回落也可以看到垃圾回收的周期,如果垃圾回收之后的最低值(我们称为min),min在不断上涨,那么肯定是有较为严重的内存泄漏问题。

避免内存泄漏的一些方式:

  • 减少不必要的全局变量,或者生命周期较长的对象,及时对无用的数据进行垃圾回收
  • 注意程序逻辑,避免“死循环”之类的
  • 避免创建过多的对象

总而言之需要遵循一条原则:不用了的东西要及时归还

五、垃圾回收的使用场景优化

1.数组array优化

将[]赋值给一个数组对象,是清空数组的捷径(例如: arr = [];),但是需要注意的是,这种方式又创建了一个新的空对象,并且将原来的数组对象变成了一小片内存垃圾!实际上,将数组长度赋值为0(arr.length = 0)也能达到清空数组的目的,并且同时能实现数组重用,减少内存垃圾的产生。

const arr = [1, 2, 3, 4];
console.log('浪里行舟');
arr.length = 0  // 可以直接让数字清空,而且数组类型不变。
// arr = []; 虽然让a变量成一个空数组,但是在堆上重新申请了一个空数组对象。

2. 对象尽量复用

对象尽量复用,尤其是在循环等地方出现创建新对象,能复用就复用。不用的对象,尽可能设置为null,尽快被垃圾回收掉。

var t = {} // 每次循环都会创建一个新对象。
for (var i = 0; i < 10; i++) {
  // var t = {};// 每次循环都会创建一个新对象。
  t.age = 19
  t.name = '123'
  t.index = i
  console.log(t)
}
t = null //对象如果已经不用了,那就立即设置为null;等待垃圾回收。

3.在循环中的函数表达式,能复用最好放到循环外面。

// 在循环中最好也别使用函数表达式。
for (var k = 0; k < 10; k++) {
  var t = function(a) {
    // 创建了10次  函数对象。
    console.log(a)
  }
  t(k)
}
// 推荐用法
function t(a) {
  console.log(a)
}
for (var k = 0; k < 10; k++) {
  t(k)
}
t = null

参考资料

[vue全家桶]制作一个精致的美团项目

一、项目展示:

美团

注意:如果gif动态图看不了,麻烦大家点击github美团项目中mt-app/src/assets/美团.gif便可以观看!

本项目很适合vue初学者,如果业务逻辑看不懂,很欢迎一起讨论!

源码地址:mt-app,欢迎 star 和 fork

如果对你有些许帮助,不妨点赞、关注我一下啊

二、项目涉及到技术栈:

  • vue全家桶:Vue、Vue-router、Vue-cli等
  • 组件化:单Vue文件
  • 模块化:ES6 Module
  • 第三方模块:better-scroll axios等
  • 基础开发环境和包管理:Node、npm
  • 构建工具:webpack
  • 编辑器:webstrom

三、项目主要功能

一言而蔽之:一款集点菜、用户评价和商家信息为一体的移动端点餐APP

  1. 点餐页面
    点选商品后自动添加到购物车,并计算好总价,在商品列表、购物车列表和商品详情页都可以随意增减数目,此外左侧商品分类和右侧的商品相互关联,通过better-scroll插件滑动商品列表时,相应的商品分类也会跟着跳转。
  2. 用户评价页面
    主要包括一个TAB栏,包括三部分:全部、有图和评价三个模块
  3. 商家信息页面
    主要介绍一些商家基本信息,可以通过better-scroll插件,进行左右滑动图片

四、项目难点

该项目业务逻辑主要集中在点餐模块,而点餐模块中难点便是如何实现商品列表滑动,以及右侧商品分类和左侧商品列表如何联动?

首先要实现商品列表的滑动,就需要用到一个better-scroll插件,better-scroll 是一款重点解决移动端(已支持 PC)各种滚动场景需求的插件。

  • 安装better-scroll npm install better-scroll --save
  • 在Good.vue文件中script引入import BScroll from 'better-scroll'

这些准备工作做好后,实现左右两边列表联动,总结起来有以下四个步骤:
1. 计算商品分类的区间高度

//template部分
  <!--商品列表-->
    <div class="foods-wrapper" ref="foodScroll">
      <ul>
        <!--专场-->
        <li class="container-list food-list-hook">
          <div v-for="(item,index) in container.operation_source_list" :key="index">
            <img :src="item.pic_url">
          </div>
        </li>
        <!-- 具体分类 -->
        <li v-for="(item,index) in goods" :key="index" class="food-list food-list-hook">
          <h3 class="title">{{item.name}}</h3>
          <!-- 具体的商品列表 -->
          <ul>
            <li v-for="(food,index) in item.spus" :key="index" @click="showDetail(food)" class="food-item">
            ......
//JS部分
methods:{
  calculateHeight() {   // 计算分类的区间总高度(包括专场和所有具体分类的总高)
        let foodlist = this.$refs.foodScroll.getElementsByClassName("food-list-hook")
        let height = 0
        this.listHeight.push(height)
        for (let i = 0; i < foodlist.length; i++) {
          let item = foodlist[i]
          height += item.clientHeight    // 累加
          this.listHeight.push(height)
        }
        console.log(this.listHeight)//[0, 43, 1231, 2401, 3589, 4451, 6121, 7656, 8497, 9344, 10080]
      },
   initScroll() {
        this.menuScroll = new BScroll(this.$refs.menuScroll, {  //实例化
          click: true  //点击事件才能生效
        })
        this.foodScroll = new BScroll(this.$refs.foodScroll, {
          probeType: 3,
          click: true
        })
},
   created() {
      fetch("/api/goods")
        .then(res => res.json())
        .then(response => {
          if (response.code == 0) {
           this.container = response.data.container_operation_source
           this.goods = response.data.food_spu_tags
           this.poiInfo = response.data.poi_info 
           this.$nextTick(() => {  //在created中数据虽已初始化,但dom未生成,页面还没显示,要使用回调函数,确保DOM已经更新
           this.initScroll()        // 执行滚动方法
           this.calculateHeight() //调用计算分类区间高度的方法
            })
          }
        })
    }

2. 监听滚动的位置

initScroll() {
        this.menuScroll = new BScroll(this.$refs.menuScroll)
        this.foodScroll = new BScroll(this.$refs.foodScroll, {
          probeType: 3,//在屏幕滑动的过程中实时派发 scroll 事件
         click:true//点击事件才能生效
        })
        //foodScroll监听事件
        this.foodScroll.on("scroll", (pos) => {
          this.scrollY = Math.abs(pos.y)
          console.log(this.scrollY)
        })
      }

3. 根据滚动位置确认下标,与左侧对应

computed:{
    currentIndex(){
      for(let i = 0; i < this.listHeight.length; i++){
        let height1 = this.listHeight[i]
        let height2 = this.listHeight[i+1] // 获取商品区间的范围
        if(!height2 || (this.scrollY >= height1 && this.scrollY < height2)){
          return i;    // 是否在上述区间中
        }
      }
      return 0
    }
        <li class="menu-item"
          :class="{'current':currentIndex === 0}"//动态绑定一个样式,.current设置样式
          @click="selectMenu(0)">
          <p class="text">
            <img class="icon" :src="container.tag_icon" v-if="container.tag_icon">
            {{container.tag_name}}
          </p>
        </li>
        <li class="menu-item"
            :class="{'current':currentIndex === index + 1}"
            v-for="(item,index) in goods" :key="index"
            @click="selectMenu(index+1)"
            >

4. 通过下标实现点击左侧,滚动右侧

<li class="menu-item" :class="{'current':currentIndex===0}" @click="selectMenu(0)">
<li class="menu-item" :class="{'current':currentIndex===index+1}" 
v-for="(item,index) in goods" :key="index" @click="selectMenu(index+1)">//同一个函数传值不一样
 selectMenu(index) {
        let foodlist = this.$refs.foodScroll.getElementsByClassName("food-list-hook")
        let element = foodlist[index]
        this.foodScroll.scrollToElement(element, 250)
      }

五、项目总结

  • vue数据和视图的分离,以数据驱动视图,只关心数据变化,DOM操作被封装,在实际开发过程中大大提高了效率。
  • Vue的组件化功能可谓是它的一大亮点,通过将页面上某一组件的html、CSS、JS代码放入一个.vue的文件中进行管理可以大大提高代码的维护性。
  • 项目中未做移动端适配,在不同屏幕手机上打开,可能用户体验会差些

ES6核心特性

前言

ES6 虽提供了许多新特性,但我们实际工作中用到频率较高并不多,根据二八法则,我们应该用百分之八十的精力和时间,好好专研这百分之二十核心特性,将会收到事半功倍的奇效!写文章不容易,请大家多多支持与关注!

一、开发环境配置

这部分着重介绍:babel 编译ES6语法,如何用webpack实现模块化。

1.babel

为啥需要babel?

ES6 提供了许多新特性,但并不是所有的浏览器都能够完美支持。下图是各个浏览器对ES6兼容性一览表(以export为例)

export各个浏览器兼容性一览表

由上图可知,有些浏览器对于ES6并不是很友好,针对 ES6 的兼容性问题,很多团队为此开发出了多种语法解析转换工具(比如babel,jsx,traceur 等),可以把我们写的 ES6 语法转换成 ES5,相当于在 ES6 和浏览器之间做了一个翻译官。其中Babel是一个广泛使用的转码器,可以将ES6代码转为ES5代码,从而在现有环境执行。

如何配置babel?

·首先要先安装node.js,运行npm init,然后会生成package.json文件
·npm install --save-dev babel-core babel-preset-es2015 babel-preset-latest
·创建并配置.babelrc文件//存放在项目的根目录下,与node_modules同级
·npm install -g babel-cli
·babel-version

Babel的配置文件是.babelrc,存放在项目的根目录下。该文件用来设置转码规则和插件,具体内容如下:

//.babelrc文件
{
    "presets": ["es2015", "latest"],
    "plugins": []
}

验证配置是否成功

·创建./src/index.js
·内容:[1,2,3].map(item=>item+1);
·运行babel./src/index.js

运行后得到以下部分,说明已经成功配置了babel

"use strict";
[1, 2, 3].map(function (item) {
  return item + 1;
});

2.webpack

为啥要使用WebPack?

现今的很多网页其实可以看做是功能丰富的应用,它们拥有着复杂的JavaScript代码和一大堆依赖包,模快化工具就应运而生了,其中webpack 功能强大深受人们喜爱。
Webpack的工作方式是:把你的项目当做一个整体,通过一个给定的主文件(如:index.js),Webpack将从这个文件开始找到你的项目的所有依赖文件,使用loaders处理它们,最后打包为一个(或多个)浏览器可识别的JavaScript文件。

如何配置webpack?

·npm install webpack babel-loader --save-dev
·创建并配置 webpack.config.js//webpack.config.js文件与package.json同级
·配置 package.json中的scripts
·运行 npm start
//配置 webpack.config.js  针对.js结尾的文件除了node_modules都用babel解析
module.exports = {
    entry: './src/index.js',
    output: {
        path: __dirname,
        filename: './build/bundle.js'
    },
    module: {
        rules: [{
            test: /\.js?$/,
            exclude: /(node_modules)/,
            loader: 'babel-loader'
        }]
    }
}
//配置 package.json中的scripts
"scripts": {
    "start": "webpack",
    "test": "echo \"Error: no test specified\" && exit 1"
  }

二、块级作用域

ES5 只有全局作用域和函数作用域(例如,我们必须将代码包在函数内来限制作用域),这导致很多问题:

情况1:内层变量覆盖外层变量

var tmp = new Date();
function f() {
  console.log(tmp); //undefined
  if (false) {   
    var tmp = "hello world";
  }
}

情况2:变量泄露,成为全局变量

var s = 'hello';
for (var i = 0; i < s.length; i++) {
  console.log(s[i]);
}
console.log(i); // 5

ES6 提供 let 和 const 来代替 var 声明变量,新的声明方式支持用大括号表示的块级作用域,这会带来一些好处:

1.不再需要立即执行的函数表达式(IIFE)
在 ES5 中,我们需要构造一个立即执行的函数表达式去保证我们不污染全局作用域。在 ES6中, 我们可以使用更简单的大括号({}),然后使用 const 或者 let 代替 var 来达到同样的效果。

2.循环体中的闭包不再有问题
在 ES5 中,如果循环体内有产生一个闭包,访问闭包外的变量,会产生问题。在 ES6,你可以使用 “let” 来避免问题。

3.防止重复声明变量
ES6 不允许在同一个作用域内用 let 或 const 重复声明同名变量。这对于防止在不同的 js 库中存在重复声明的函数表达式十分有帮助。

三、数组的扩展

1. Array.from() : 将伪数组对象或可遍历对象转换为真数组

如果一个对象的所有键名都是正整数或零,并且有length属性,那么这个对象就很像数组,称为伪数组。典型的伪数组有函数的arguments对象,以及大多数 DOM 元素集,还有字符串。

...
<button>测试1</button>
<br>
<button>测试2</button>
<br>
<button>测试3</button>
<br>
<script type="text/javascript">
let btns = document.getElementsByTagName("button")
console.log("btns",btns);//得到一个伪数组
btns.forEach(item=>console.log(item)) Uncaught TypeError: btns.forEach is not a function
</script>

针对伪数组,没有数组一般方法,直接遍历便会出错,ES6新增Array.from()方法来提供一种明确清晰的方式以解决这方面的需求。

Array.from(btns).forEach(item=>console.log(item))将伪数组转换为数组

2.Array.of(v1, v2, v3) : 将一系列值转换成数组

当调用 new Array( )构造器时,根据传入参数的类型与数量的不同,实际上会导致一些不同的结果, 例如:

let items = new Array(2) ;
console.log(items.length) ; // 2
console.log(items[0]) ; // undefined
console.log(items[1]) ;
let items = new Array(1, 2) ;
console.log(items.length) ; // 2
console.log(items[0]) ; // 1
console.log(items[1]) ; // 2

当使用单个数值参数来调用 Array 构造器时,数组的长度属性会被设置为该参数。 如果使用多个参数(无论是否为数值类型)来调用,这些参数也会成为目标数组的项。数组的这种行为既混乱又有风险,因为有时可能不会留意所传参数的类型。

ES6 引入了Array.of( )方法来解决这个问题。该方法的作用非常类似Array构造器,但在使用单个数值参数的时候并不会导致特殊结果。Array.of( )方法总会创建一个包含所有传入参数的数组,而不管参数的数量与类型

let items = Array.of(1, 2);
console.log(items.length); // 2
console.log(items[0]); // 1
console.log(items[1]); // 2
items = Array.of(2);
console.log(items.length); // 1
console.log(items[0]); // 2

Array.of基本上可以用来替代Array()或newArray(),并且不存在由于参数不同而导致的重载,而且他们的行为非常统一。

3.数组实例的 find() 和 findIndex()

数组实例的find方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined。

[1, 4, -5, 10].find((n) => n < 0) // -5

数组实例的findIndex方法的用法与find方法非常类似,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1。

[1, 5, 10, 15].findIndex(function(value, index, arr) {
  return value > 9;
}) // 2

4.数组实例的includes()

Array.prototype.includes方法返回一个布尔值,表示某个数组是否包含给定的值。该方法的第二个参数表示搜索的起始位置,默认为0。如果第二个参数为负数,则表示倒数的位置,如果这时它大于数组长度(比如第二个参数为-4,但数组长度为3),则会重置为从0开始。

[1, 2, 3].includes(2)   // true
[1, 2, 3].includes(3, -1); // true
[1, 2, 3, 5, 1].includes(1, 2); // true

没有该方法之前,我们通常使用数组的indexOf方法,检查是否包含某个值。indexOf方法有两个缺点,一是不够语义化,它的含义是找到参数值的第一个出现位置,所以要去比较是否不等于-1,表达起来不够直观。二是,它内部使用严格相等运算符(===)进行判断,这会导致对NaN的误判

[NaN].indexOf(NaN) // -1
[NaN].includes(NaN) // true

5.数组实例的 entries(),keys() 和 values()

ES6 提供entries(),keys()和values(),用于遍历数组。它们都返回一个遍历器对象,可以用for...of循环进行遍历,唯一的区别是keys()是对键名的遍历、values()是对键值的遍历,entries()是对键值对的遍历。

for (let index of ['a', 'b'].keys()) {
  console.log(index);
}
// 0
// 1

for (let elem of ['a', 'b'].values()) {
  console.log(elem);
}
// 'a'
// 'b'

for (let [index, elem] of ['a', 'b'].entries()) {
  console.log(index, elem);
}
// 0 "a"
// 1 "b"

四、箭头函数

ES6 允许使用“箭头”(=>)定义函数。它主要有两个作用:缩减代码和改变this指向,接下来我们详细介绍:

1. 缩减代码

const double1 = function(number){
   return number * 2;   //ES5写法
}
const double2 = (number) => {
 return number * 2;    //ES6写法
}
const double4 = number => number * 2; //可以进一步简化

多个参数记得加括号

 const double6 = (number,number2) => number + number2;

如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用return语句返回

 const double = (number,number2) => {
   sum = number + number2 
   return sum;
 }

由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号,否则会报错

// 报错
let getTempItem = id => { id: id, name: "Temp" };
// 不报
let getTempItem = id => ({ id: id, name: "Temp" });

此外还有个好处就是简化回调函数

// 正常函数写法
[1,2,3].map(function (x) {
  return x * x;
});
// 箭头函数写法
[1,2,3].map(x => x * x);//[1, 4, 9]

2. 改变this指向

长期以来,JavaScript 语言的this对象一直是一个令人头痛的问题,在对象方法中使用this,必须非常小心。箭头函数”绑定”this,很大程度上解决了这个困扰。我们不妨先看一个例子:

const team = {
  members:["Henry","Elyse"],
  teamName:"es6",
  teamSummary:function(){
    return this.members.map(function(member){
      return `${member}隶属于${this.teamName}小组`;    // this不知道该指向谁了
    })
  }
}
console.log(team.teamSummary());//["Henry隶属于undefined小组", "Elyse隶属于undefined小组"]

teamSummary函数里面又嵌了个函数,这导致内部的this的指向发生了错乱。
那如何修改:

方法一、let self = this

const team = {
  members:["Henry","Elyse"],
  teamName:"es6",
  teamSummary:function(){
    let self = this;
    return this.members.map(function(member){
      return `${member}隶属于${self.teamName}小组`;
    })
  }
}
console.log(team.teamSummary());//["Henry隶属于es6小组", "Elyse隶属于es6小组"]

方法二、bind函数

const team = {
  members:["Henry","Elyse"],
  teamName:"es6",
  teamSummary:function(){
    return this.members.map(function(member){
      // this不知道该指向谁了
      return `${member}隶属于${this.teamName}小组`;
    }.bind(this))
  }
}
console.log(team.teamSummary());//["Henry隶属于es6小组", "Elyse隶属于es6小组"]

方法三、 箭头函数

const team = {
  members:["Henry","Elyse"],
  teamName:"es6",
  teamSummary:function(){
    return this.members.map((member) => {
      // this指向的就是team对象
      return `${member}隶属于${this.teamName}小组`;
    })
  }
}
console.log(team.teamSummary());//["Henry隶属于es6小组", "Elyse隶属于es6小组"]

3.使用注意点

(1)函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。

(2)不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。

(3)不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。

(4)不可以使用yield命令,因此箭头函数不能用作 Generator 函数。

五、rest 参数

ES6 引入 rest 参数(形式为...变量名),用于获取函数的多余参数,这样就不需要使用arguments对象了。

rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。
我们举个例子:如何实现一个求和函数?

传统写法:

function addNumbers(a,b,c,d,e){
  var numbers = [a,b,c,d,e];
  return numbers.reduce((sum,number) => {
    return sum + number;
  },0)
 }
 console.log(addNumbers(1,2,3,4,5));//15

ES6写法:

 function addNumbers(...numbers){
  return numbers.reduce((sum,number) => {
    return sum + number;
  },0)
 }
 console.log(addNumbers(1,2,3,4,5));//15

也可以与解构赋值组合使用

var array = [1,2,3,4,5,6];
var [a,b,...c] = array;
console.log(a);//1
console.log(b);//2
console.log(c);//[3, 4, 5, 6]

rest 参数还可以与箭头函数结合

const numbers = (...nums) => nums;
numbers(1, 2, 3, 4, 5)// [1,2,3,4,5]  

注意:①每个函数最多只能声明一个rest参数,而且 rest参数必须是最后一个参数,否则报错。

②rest参数不能用于对象字面量setter之中

let object = {
    set name(...value){   //报错
        //执行一些逻辑
    }
}

六、展开运算符

与剩余参数关联最密切的就是扩展运算符。剩余参数允许你把多个独立的参数合并到一个数组中;而扩展运算符则允许将一个数组分割,并将各个项作为分离的参数传给函数。

当用在字符串或数组前面时称为扩展运算符,个人觉得可以理解为rest参数的逆运算,用于将数组或字符串进行拆解。有些时候,函数不允许传入数组,此时使用展开运算符就很方便,不信的话,咱们看个例子:Math.max()方法,它接受任意数量的参数,并会返回其中的最大值。

let value1 = 25,				
let value2 = 50;
console.log(Math.max(value1, value2));	//	50

但若想处理数组中的值,此时该如何找到最大值?Math.max()方法并不允许你传入一个数组。其实你可以像使用rest参数那样在该数组前添加...,并直接将其传递给 Math.max()

let values = [25,50,75,	100]
//等价于console.log(Math.max(25,50,75,100));
console.log(Math.max(...values));	//100

扩展运算符还可以与其他参数混用

let values = [-25,-50,-75,-100]
console.log(Math.max(...values,0));	//0

扩展运算符拆解字符串与数组

var array = [1,2,3,4,5];
console.log(...array);//1 2 3 4 5
var str = "String";
console.log(...str);//S t r i n g

还可以实现拼接

var defaultColors = ["red","greed"];
var favoriteColors = ["orange","yellow"];
var fallColors = ["fire red","fall orange"];
console.log(["blue","green",...fallColors,...defaultColors,...favoriteColors]
//["blue", "green", "fire red", "fall orange", "red", "greed", "orange", "yellow"]

七、解构赋值----更方便的数据访问

ES6 新增了解构,这是将一个数据结构分解为更小的部分的过程。

1.解构为何有用?

在ES5及更早版本中,从对象或数组中获取信息、并将特定数据存入本地变量,需要书写许多并且相似的代码。例如:

 var expense = {
   type: "es6",
   amount:"45"
 };
 var type = expense.type;
 var amount = expense.amount;
 console.log(type,amount);

此代码提取了expense对象的type与amount值,并将其存在同名的本地变量上。虽然 这段代码看起来简单,但想象一下若有大量变量需要处理,你就必须逐个为其赋值;并且若有一个嵌套的数据结构需要遍历以寻找信息,你可能会为了一点数据而挖掘整个结构。

这就是ES6为何要给对象与数组添加解构。当把数据结构分解为更小的部分时,从中提取你要的数据会变得容易许多。

2.对象

上个例子中如果采用对象解构的方法,就很容易获取expense对象的type与amount值。

const { type,amount } = expense;
console.log(type,amount);

我们再来看个例子:

let node = {type:"Identifier",	name:"foo"},	
type = "Literal",name = 5;
({type,name}= node);//	使用解构来分配不同的值 
console.log(type); //	"Identifier" 
console.log(name); //	"foo"

注意:你必须用圆括号包裹解构赋值语句,这是因为暴露的花括号会被解析为代码块语句,而块语句不允许在赋值操作符(即等号)左侧出现。圆括号标示了里面的花括号并不是块语句、而应该被解释为表达式,从而允许完成赋值操作。

默认值:
可以选择性地定义一个默认值,以便在指定属性不存在时使用该值。若要这么做,需要在 属性名后面添加一个等号并指定默认值,就像这样:

let node = {
  type: "Identifier",
  name: "foo"
};
let {
  type,
  name,
  value = true
} = node;
console.log(type); //	"Identifier" 
console.log(name); //	"foo" 
console.log(value); //	true

嵌套对象解构:
使用类似于对象字面量的语法,可以深入到嵌套的对象结构中去提取你想要的数据。

let node = {
  type: "Identifier",
  name: "foo",
  loc: {
    start: {
      line: 1,
      column: 1
    },
    end: {
      line: 1,
      column: 4
    }
  }
};
let { loc: { start }} = node;
console.log(start.line); //	1 
console.log(start.column); //	1

本例中的解构模式使用了花括号,表示应当下行到node对象的loc属性内部去寻找start属性。

必须传值的解构参数

function setCookie(name, value, {
  secure,
  path,
  domain,
  expires
}) {
  //	设置cookie的代码 
}
  setCookie("type", "js");//报错

在此函数内,name与value参数是必需的,而secure、path、domain与expires则不是。默认情况下调用函数时未给参数解构传值会抛出错误。像上例中如果setCookie不传第三个参数,就会报错。若解构参数是可选的,可以给解构的参数提供默认值来处理这种错误。

function setCookie(name, value, {
  secure,
  path,
  domain,
  expires
} = {}) {}
setCookie("type", "js");//不会报错

3.数组

const names = ["Henry","Bucky","Emily"];
const [name1,name2,name3] = names;
console.log(name1,name2,name3);//Henry Bucky Emily
const [name,...rest] = names;//结合展开运算符
console.log(rest);//["Bucky", "Emily"]

用{}解构返回数组个数

const {length} = names;
console.log(length);//3

数组解构也可以用于赋值上下文,但不需要用小括号包裹表达式。这点跟对象解构的约定不同。

let colors = ["red", "green", "blue"],
  firstColor = "black",
  secondColor = "purple";
[firstColor, secondColor] = colors;
console.log(firstColor); //	"red" 
console.log(secondColor);	// "green"

默认值:数组解构赋值同样允许在数组任意位置指定默认值。当指定位置的项不存在、或其值为undefined,那么该默认值就会被使用。

let colors = ["red"];
let [firstColor, secondColor = "green"] = colors;
console.log(firstColor); //	"red" 
console.log(secondColor);//	"green"

与rest参数搭配

在ES5中常常使用concat()方法来克隆数组,例如:

//在ES5中克隆数组 
var colors = ["red", "green", "blue"];
var clonedColors = colors.concat();
console.log(clonedColors); //"[red,green,blue]"

在ES6中,你可以使用剩余项的语法来达到同样效果

//在ES6中克隆数组 
let colors = ["red", "green", "blue"];
let [...clonedColors] = colors;
console.log(clonedColors); //[red,green,blue]

接下我们看个例子:如何将数组转化为对象

const points = [
  [4,5],
  [10,1],
  [0,40]
];
//期望得到的数据格式如下,如何实现?
// [
//   {x:4,y:5},
//   {x:10,y:1},
//   {x:0,y:40}
// ]
let newPoints = points.map(pair => {
  const [x,y] = pair;
  return {x,y}
})
//还可以通过以下办法,更为简便
let newPoints = points.map(([x,y]) => {
  return {x,y}
})
console.log(newPoints);

混合解构

const people = [
  {name:"Henry",age:20},
  {name:"Bucky",age:25},
  {name:"Emily",age:30}
];
//es5 写法 
var age = people[0].age;
console.log(age);
//es6 解构
const [age] = people;
console.log(age);//第一次解构数组 {name:"Henry",age:20}
const [{age}] = people;//再一次解构对象
console.log(age);//20

4.注意点

当使用解构来配合var、let、const来声明变量时,必须提供初始化程序(即等号右边的值)。下面的代码都会因为缺失初始化程序而抛出语法错误:

var { type, name }; // 语法错误! 
let { type, name }; // 语法错误!
const { type, name }; // 语法错误!

八、模板字符串(template string)

模板字符串是增强版的字符串,用反引号(`)标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。
模板字符串中嵌入变量和函数,需要将变量名写在${}之中。

let name = "Henry";
function makeUppercase(word){
  return word.toUpperCase();
}
let template = 
  `
  <h1>${makeUppercase('Hello')}, ${name}!</h1>//可以存放函数和变量
  <p>感谢大家收看我们的视频, ES6为我们提供了很多遍历好用的方法和语法!</p>
  <ul>
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
    <li>5</li>
  </ul>
  `;
document.getElementById('template').innerHTML = template;

再举个例子,工作中常用到ElementUI库,在自定义一个弹出框时,使用模板字符串就很方便:

   await this.$alert(
          `<p><strong>确认是否升级${
            this.lectureName
          }</strong><br>(若已存在讲义套件,升级后请重新生成)</p>`,
          {
            dangerouslyUseHTMLString: true
          }
        )

九、Class 和传统构造函数有何区别

从概念上讲,在 ES6 之前的 JS 中并没有和其他面向对象语言那样的“类”的概念。长时间里,人们把使用 new 关键字通过函数(也叫构造器)构造对象当做“类”来使用。由于 JS 不支持原生的类,而只是通过原型来模拟,各种模拟类的方式相对于传统的面向对象方式来说非常混乱,尤其是处理当子类继承父类、子类要调用父类的方法等等需求时。
ES6提供了更接近传统语言的写法,引入了Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类。但是类只是基于原型的面向对象模式的语法糖

对比在传统构造函数和 ES6 中分别如何实现类:

//传统构造函数
function MathHandle(x,y){
  this.x=x;
  this.y=y;
}
MathHandle.prototype.add =function(){
  return this.x+this.y;
};
var m=new MathHandle(1,2);
console.log(m.add())
//class语法
class MathHandle {
 constructor(x,y){
  this.x=x;
  this.y=y;
}
 add(){
   return this.x+this.y;
  }
}
const m=new MathHandle(1,2);
console.log(m.add())

这两者有什么联系?其实这两者本质是一样的,只不过是语法糖写法上有区别。所谓语法糖是指计算机语言中添加的某种语法,这种语法对语言的功能没有影响,但是更方便程序员使用。比如这里class语法糖让程序更加简洁,有更高的可读性。

typeof MathHandle //"function"
MathHandle===MathHandle.prototype.constructor //true

对比在传统构造函数和 ES6 中分别如何实现继承:

//传统构造函数继承
function Animal() {
    this.eat = function () {
        alert('Animal eat')
    }
}
function Dog() {
    this.bark = function () {
        alert('Dog bark')
    }
}
Dog.prototype = new Animal()// 绑定原型,实现继承
var hashiqi = new Dog()
hashiqi.bark()//Dog bark
hashiqi.eat()//Animal eat
//ES6继承
class Animal {
    constructor(name) {
        this.name = name
    }
    eat() {
        alert(this.name + ' eat')
    }
}
class Dog extends Animal {
    constructor(name) {
        super(name) // 有extend就必须要有super,它代表父类的构造函数,即Animal中的constructor
        this.name = name
    }
    say() {
        alert(this.name + ' say')
    }
}
const dog = new Dog('哈士奇')
dog.say()//哈士奇 say
dog.eat()//哈士奇 eat

Class之间可以通过extends关键字实现继承,这比ES5的通过修改原型链实现继承,要清晰和方便很多。

Class 和传统构造函数有何区别

  • Class 在语法上更加贴合面向对象的写法
  • Class 实现继承更加易读、易理解,对初学者更加友好
  • 本质还是语法糖,使用prototype

十、Promise的基本使用和原理

在JavaScript的世界中,所有代码都是单线程执行的。由于这个“缺陷”,导致JavaScript的所有网络操作,浏览器事件,都必须是异步执行。Promise 是异步编程的一种解决方案,比传统的解决方案(回调函数和事件)更合理和更强大。

回调地狱

ES6中的promise的出现给我们很好的解决了回调地狱的问题,所谓的回调地狱是指当太多的异步步骤需要一步一步执行,或者一个函数里有太多的异步操作,这时候就会产生大量嵌套的回调,使代码嵌套太深而难以阅读和维护。ES6认识到了这点问题,现在promise的使用,完美解决了这个问题。

Promise原理

一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。promise 对象初始化状态为 pending ;当调用resolve(成功),会由pending => fulfilled ;当调用reject(失败),会由pending => rejected。具体流程见下图:

Promise原理

Promise的使用流程

  1. new Promise一个实例,而且要 return
  2. new Promise 时要传入函数,函数有resolve reject 两个参数
  3. 成功时执行 resolve,失败时执行reject
  4. then 监听结果
function loadImg(src){
   const promise=new Promise(function(resolve,reject){
     var img=document.createElement('img')
     img.onload=function(){
        resolve(img)
   }
     img.onerror=function(){
        reject()
   }
    img.src=src
 })
  return promise//返回一个promise实例
}
var src="http://www.imooc.com/static/img/index/logo_new.png"
var result=loadImg(src)
result.then(function(img){
    console.log(img.width)//resolved(成功)时候的回调函数
},function(){
    console.log("failed")//rejected(失败)时候的回调函数
})
result.then(function(img){
    console.log(img.height)
})

promise会让代码变得更容易维护,像写同步代码一样写异步代码,同时业务逻辑也更易懂。

十一、Iterator 和 for...of 循环

JavaScript 原有的表示“集合”的数据结构,主要是数组(Array)和对象(Object),ES6 又添加了Map和Set。这样就需要一种统一的接口机制,来处理所有不同的数据结构。遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)

1.Iterator的作用:

  • 为各种数据结构,提供一个统一的、简便的访问接口;
  • 使得数据结构的成员能够按某种次序排列
  • ES6创造了一种新的遍历命令for...of循环,Iterator接口主要供for...of消费。

2.原生具备iterator接口的数据(可用for of遍历)

  • Array
  • set容器
  • map容器
  • String
  • 函数的 arguments 对象
  • NodeList 对象
let arr3 = [1, 2, 'kobe', true];
for(let i of arr3){
   console.log(i); // 1 2 kobe true
}
let str = 'abcd';
for(let item of str){
   console.log(item); // a b c d
}   
var engines = new Set(["Gecko", "Trident", "Webkit", "Webkit"]);
for (var e of engines) {
  console.log(e);
}
// Gecko
// Trident
// Webkit    

3.几种遍历方式比较

  • for of 循环不仅支持数组、大多数伪数组对象,也支持字符串遍历,此外还支持 Map 和 Set 对象遍历。
  • for in循环可以遍历字符串、对象、数组,不能遍历Set/Map
  • forEach 循环不能遍历字符串、对象,可以遍历Set/Map

十二、ES6模块化

ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,旨在成为浏览器和服务器通用的模块解决方案。其模块功能主要由两个命令构成:export和import。export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。

/** 定义模块 math.js **/
var basicNum = 0;
var add = function (a, b) {
    return a + b;
};
export { basicNum, add };
/** 引用模块 **/
import { basicNum, add } from './math';
function test(ele) {
    ele.textContent = add(99 + basicNum);
}

如上例所示,使用import命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载。为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到export default命令,为模块指定默认输出。

// export-default.js
export default function () {
  console.log('foo');
}

上面代码是一个模块文件export-default.js,它的默认输出是一个函数。
其他模块加载该模块时,import命令可以为该匿名函数指定任意名字。

// import-default.js
import customName from './export-default';
customName(); // 'foo'

上面代码的import命令,可以用任意名称指向export-default.js输出的方法,这时就不需要知道原模块输出的函数名。需要注意的是,这时import命令后面,不使用大括号。

如果觉得文章对你有些许帮助,欢迎在我的GitHub博客点赞和关注,感激不尽!

参考文章

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.