http协议概览(http1.0/http1.1/http2)

http介绍

到目前为止,绝大多数web应用都是基于http来进行开的,我们对web的操作都是通过http协议进行数据传输。总结来说就是http协议是client 与 server通信的一种协议、标准或者格式。

http主要版本

1.http1.0
2.http1.1
3.http2.0

各版本间区别

首先说下http1.0 跟 http1.1,这俩时间线上最接近,但是差别也不小,http1.1基本是现在使用最广泛的http协议,也已经稳定运行了好多年。

http1.0 跟 http1.1区别

这两货之间我觉得最大的区别是1.1支持持久化链接(长链接),而1.0默认短连接即每次与服务器交互都需要新开一个新链接(如果需要开启长链接需加上keep-alive的请求首部)。
这就要人命了,试想一下:请求一张图片 或者 csss js等静态资源都需要新开一条链接,而http是基于tcp协议的,新开链接都得经过3次握手,四次挥手,现在一个网页光静态资源数几十上百个,如果使用1.0的话,那用户恐怕得等到骂街了,而且服务器资源有限,可以同时开的链接数有限。
1.1通过持久化链接解决了这个问题:一次链接,多次复用,但是如果阻塞了同样会新开链接(一个tcp链接支持的请求数有限)。
相对于持久化链接,http1.1还有几个比较重要的点
1.增加host字段
2.引入Chunked transfer-coding,范围请求,实现断点续传(实际上就是利用HTTP消息头使用分块传输编码,将实体主体分块传输)
3.HTTP 1.1管线化(pipelining)理论,客户端可以同时发出多个HTTP请求,而不用一个个等待响应之后再请求
*** 注意:这个pipelining仅仅是限于理论场景下,大部分桌面浏览器仍然会选择默认关闭HTTP pipelining!所以现在使用HTTP1.1协议的应用,都是有可能会开多个TCP连接的!***

下面开始介绍http2,首先我说下http2的基础知识

http1.1跟 http2区别

上面提到http1.1理论上支持管线化,但这个功能浏览器默认还是关闭的,所以基本没有实用性。下面我放张图,大家可以看下管线化跟非管线化之间有何区别

无论是HTTP1.0还是HTTP1.1提出了Pipelining理论,但还是会出现阻塞的情况。从专业的名词上说这种情况,叫做线头阻塞(Head of line blocking)简称:HOLB

下面来看看具体http2是如何解决这些问题的
http2 性能增强的核心:二进制分帧层, 它定义了如何封装http消息并在client 跟 server之间传输

与HTTP1.x的采用的换行符分隔文本不同,HTTP/2 消息被分成很小的消息和frame,然后每个消息和frame用二进制编码。客户端和服务端都采用二进制编码和解码。

流、消息、帧
:已经建立的连接之间双向流动的字节,它能携带一个至多个消息。
消息:一个完整的帧序列,它映射到逻辑的请求和响应消息。
:在HTTP/2通信的最小单元。每个桢包括一个帧头,里面有个很小标志,来区别是属于哪个流。
1.所有的通信都建立在一个TCP连接上,可以传递大量的双向流通的流。
2.每个流都有独一无二的标志和优先级。
3.每个消息都是逻辑上的请求和相应消息。由一个或者多个帧组成。
4.来自不同流的帧可以通过帧头的标志来关联和组装起来。

在这个的基础上,http2实现了真正意义上的多路复用:
在HTTP/1.x中,用户想要多个并行的请求来提高性能,但是这样必须得使用多个TCP连接.这样的操作是属于HTTP/1.x 发送模型的直接序列.它能保证在每次连接中在一个时间点只有一个响应被发送出去.更糟糕的是,它使得队头阻塞和重要TCP连接的低效使用.
在HTTP/2中,新的二进制帧层,解除了这个限制.使得所有的请求和响应多路复用.通过允许客户端和服务端把HTTP消息分解成独立的帧,交错传输,然后在另一端组装.交错的多个并行的请求或者响应,而不需要阻塞.这样就不用新开tcp链接,所有请求在一个链接上就能实现,淘汰没必要的潜在因素来降低页面载入的时间.提升可用网络容积的使用率.

还有几个点我觉得是http2提升比较重要的点
一、流的优先级
为了能方便流的传输顺序,HTTP/2.0提出,使每个流都有一个权重和依赖.
每个流的权重值在1~256之间
每个流可以详细给出对其他流的依赖
这样的话可以尽快把一些主要关键的资源响应给客户端,提高页面的响应速度。
二、服务端推送
服务器为单个客户端请求发送多个响应的能力。也就是说,除了对原始请求的响应之外,服务器还可以向客户端推送额外的资源,而不需要客户端明确请求每一个资源!这样可以提前有服务端控制哪些资源需要被提前加载,提高页面的响应速度
二、头部压缩
这个没太多可说的,主要就是可以节省不必要的流量开支

参考文档: HTTP/2 新特性总结(https://www.jianshu.com/p/67c541a421f9)

Vue中jsx的使用

标签(空格分隔): Vue jsx


背景

目前的项目主要都是依赖在Vue框架下进行开发的,一般情况下,在Vue文件中都是使用template来进行编写代码,但是有一些需要定制配置的页面,这样写起来就特别蛋疼,比如下面的代码:

    import preview from './preview'
    const config = {
        preview: {
            name: '名称',
            preIcon: 'el-icon-view',
            dialog: {
                slot: [
                    { key: 'body', child: preview, data: {name: '预览'}},
                ]
            },
            click: () => {
                console.log('触发点击')
            }
        },
    }

上面是一个渲染 按钮 – 弹窗 的组件,点击按钮可以触发一个弹窗,dialog里面是弹窗的具体内容,其中slot是自定义渲染的内容,渲染一个自定义的插槽来满足定制化的业务场景。这种写法可以满足业务封装的需求,使得项目在一定程度上可以配置化编写,有利于后期项目的拓展和维护。但是其实我的preview功能可能仅仅是这样:

    // preview.js
    <template>
        <div>data.name</div>
    </template>
    <script>
        export default {
            props: {
                data: Object
            }
        }
    </script>

其实我仅仅需要的就是一个div标签而已,但是使用vue的template却只能新创建一个vue文件来进行引入。如果换成jsx那么我们直接就可以这样写:


const config = { preview: { name: '名称', preIcon: 'el-icon-view', dialog: { slot: [ { key: 'body', child: <div>预览</div>}, ] }, click: () => { console.log('触发点击') } }, }

这样就简单很多了,而且也不需要单独创建一个.vue文件。

引申

从上述可以看出,有些情况下vue jsx确实会带来一定的优越性(特别对于写惯了react的切图仔)。下面介绍一下写vue jsx的时候需要注意的点,以及与react的不同。

与react jsx的不同

  1. React中父子之间传递的所有数据都是属性,即所有数据均挂载在props下(style, className, children, value, onChange等等。
  2. Vue则不然,仅仅属性就有三种:组件属性props,普通html属性attrs,Dom属性domProps。

如下示例所示:

const ButtonCounter = {
  name: "button-counter",
  props: ["count"],
  methods: {
    onClick() {
      this.$emit("change", this.count + 1);
    }
  },
  render() {
    return (
      <button>You clicked me {this.count} times.</button>
    );
  }
};

export default {
  name: "button-counter-container",
  data() {
    return {
      count: 0
    };
  },
  methods: {
    onChange(val) {
      this.count = val;
    }
  },
  render() {
    const { count, onChange } = this;
    return (
      <div>


      </div>
    );
  }
};

通过上述我们可以区分出这几种属性的区别:

  1. 组件属性props:指组件声明的属性,即上述示例中声明的props: [‘count’]。

  2. 普通html属性attrs: 指组件未声明的属性,即上述示例中的type=”button”,该属性默认会直接挂载到组件根节点的上,如果不需要挂载到根节点,可声明 inheritAttrs: false。

  3. Dom属性domProps:指的Dom属性,如上述示例中的innerHTML,它会覆盖组件内部的children, 这类属性我们一般很少使用到。

抽象程度较高的动态属性

一般情况下,在已知属性和方法的时候,我们可以直接将属性值传入已经定义好的组件即可,但是如果想高度抽象来复用一些组件,那静态属性传入就不太合适了,这个时候一般使用如下方法将属性传入子组件:

    const dynamicProps = {
        props: {},
        on: {},
    }
    if(hasValue) dynamicProps.props.value = value
    if(hasChange) dynamicProps.on.change = onChange

本文关于vue jsx的介绍就到这里,如果想了解更多,建议查看官方文档。

DOM事件流三连:捕获、目标、冒泡

DOM事件流三连:捕获、目标、冒泡

在浏览器前端的人机交互中,所有的操作都是通过浏览器实现的DOM事件来完成的,每当我们点击一个按钮的时候,会触发一系列绑定的事件,通常我们会去通过DOM Element的addEventListener来给元素绑定监听事件,但是有时候,多人协作开发的页面中,总会触发一些莫名其妙的连锁事件,这个时候我们怎么去避免或者处理这些问题,让他们各自在合适的时机完成各自的事件回调,这就需要我们了解浏览器的事件流过程。

事件流三阶段

在W3C的规定中完整的事件流包括三个阶段:事件捕获阶段、处于目标阶段和冒泡阶段。如下图所示:

W3C对事件流三个阶段的解释为:

捕获阶段:事件对象传播通过从目标的祖先Window到目标的父级节点,也就是上图的P1 –> P4–>m0。

目标阶段:事件对象到达事件对象的目标。这一阶段也称为在目标阶段。如果事件类型表明事件不用冒泡,那么事件对象将在完成这一阶段后停止,即上图的m0。

冒泡阶段:事件对象传播通过从目标开始,一直到祖先Window节点结束,冒泡阶段与捕获阶段是相反的顺序,也就是上图的m0 –> m1 –> m4。

事件对象在执行上面的流程时,并不是每次都完全执行,当遇到不支持某阶段的事件或者事件对象被停止了时,就会跳过部分阶段。

事件回调执行顺序

现在浏览器大多实现了DOM3级的事件流模型,而我们大多前端看的小红书还是DOM2级的事件模型,由于历史原因及其他原因,DOM2级的事件模型在不同的浏览器实现中都各有不同,而DOM3级的事件流模型也对此做了兼容和修正。在DOM3级的事件流模型中介绍了以下新概念:

  • 现在订阅事件监听器是有序的。在DOM Level 2中事件排序未指定。
  • 现在的事件流包括Window,包含浏览器已有的实现

为了清晰的展示事件是如何传播的我们看下面的示例,我们通过传递addEventListener的第三个参数区别事件是在捕获阶段还是冒泡阶段

<!-- dom结构 -->
<body class="elm" id="body">
    body
    <div class="box elm" id="box">
        box
        <div class="item1 elm" id="item1">
            item1
            <div class="sub elm" id="sub">sub</div>
        </div>
        <div class="item2 elm" id="item2">item2</div>
        <div class="item3 elm" id="item3">item3</div>
    </div>
</body>
// 下面方法仅适用于演示使用
window.id = 'window';
document.id = 'document';
var elms = [window, document].concat(
    Array.from(document.querySelectorAll('.elm'))
);
// 绑定事件监听
elms.forEach(function(el) {
    el.addEventListener(
        'click',
        function(e) {
            console.log('冒泡:', el.id);
            if (el.id === 'item1') {
                e.stopImmediatePropagation();
            }
        },
        false
    );
    el.addEventListener(
        'click',
        function() {
            console.log('捕获:', el.id);
        },
        true
    );
});

Edit static 点击按钮进入在线示例

在上例中,我们很明显的发现打印的结果并不是我们所设想的那样,而是如下图所示:

当点击sub元素的时候,捕获传递到sub的时候,先触发了他的冒泡事件,而后才是捕获事件,这是什么原因?

我们可以看到我们的事件绑定是这样的,示例1:

elms.forEach(function(el) {
    // 绑定冒泡
    el.addEventListener(
        'click',
        function() {
            console.log('冒泡:', el.id);
        },
        true
    );
    // 绑定捕获
    el.addEventListener(
        'click',
        function() {
            console.log('捕获:', el.id);
        },
        false
    );
});

那我们调整下代码绑定顺序,示例2:

elms.forEach(function(el) {
    // 绑定捕获
    el.addEventListener(
        'click',
        function() {
            console.log('捕获:', el.id);
        },
        false
    );
    // 绑定冒泡
    el.addEventListener(
        'click',
        function() {
            console.log('冒泡:', el.id);
        },
        true
    );
});

再次执行,得到结果:

这次符合我们的预期,由此可以看出,我们如果使用addEventListener来绑定事件,最终先执行目标的捕获阶段还是冒泡阶段是和顺序有关系的。所以如果我们没有注意绑定顺序。

中断事件冒泡

通常,我们停止一个事件的冒泡使用stopPropagation方法,但是在DOM3级事件模型中新增了一个stopImmediatePropagation,如果在sub绑定的冒泡事件中使用stopImmediatePropagation来阻止冒泡的话会发生什么?

elms.forEach(function(el) {
    // 冒泡
    el.addEventListener(
        'click',
        function(e) {
            console.log('冒泡:', el.id);
            if (el.id === 'sub') {
                // 停止冒泡
                e.stopImmediatePropagation();
            }
        },
        false
    );
    // 捕获
    el.addEventListener(
        'click',
        function() {
            console.log('捕获:', el.id);
        },
        true
    );
});

触发事件后,我们会发现,输出如下:

可以看到这样也会停止掉sub绑定的捕获事件,stopImmediatePropagation是DOM3级新增的事件方法,在W3C中对stopImmediatePropagation是如下定义的:

Invoking this method prevents event from reaching any registered event listeners after the current one finishes running and, when dispatched in a tree, also prevents event from reaching any other objects.

大致意思就是:如果有多个相同类型事件的监听函数绑定到同一个元素,当该类型的事件触发时,它们会按照被添加的顺序执行。如果其中某个监听函数执行了 event.stopImmediatePropagation() 方法,则当前元素剩下的监听函数将不会被执行。

有不同的事件

虽然大多数事件都有冒泡阶段,但是也有部分事件没有,具体为:

  • UI事件
    • load
    • unload
    • abort:
    • error:当一个资源加载失败时会触发error事件
    • select
    • scroll
    • resize
  • 焦点事件
    • blur
    • focus
  • 鼠标事件
    • mouseleave
    • mouseenter

上面我们说过事件流中包括Window,所以有时候我们做前端页面错误的收集是在捕获阶段通过Window来添加事件处理的。

浏览器的’事件循环’机制


事件循环 (event loop)本来是JavaScript里一个非常重要的概念,大体意思就是JavaScript会不断地循环一个任务队列,取出队列里的第一个任务执行,用代码描述如下:


while(true){ let task = queue.pop(); exec(task); }

如果单纯讲JavaScript的事件循环,好像这就够了 。但是解释不了JavaScript的异步运行机制,所以今天我们将JavaScript的事件循环机制和浏览器结合在一起,看看JavaScript在浏览器里是如何运行的。

我们知道,JavaScript是一门单线程语言,在任意时刻都只能做一件事件,那为什么在发出ajax请求、设置定时器、执行io操作的时候,也可以接着做其他任务呢?

原因很简单,因为浏览器是多线程的。

在浏览器里,除了有执行JavaScript的主线程外,还有处理dom事件的事件线程;处理定时任务的时间线程;处理ajax请求的网络线程;处理io操作的io线程(部分浏览器支持DB操作)

事件线程

在浏览器里,渲染出来的dom元素都具有监听用户操作的属性,比如用户点击事件:click,鼠标移动事件:mousemove等。

document.addEventListener('click',()=>{
    console.log('document click');
});

当我们在dom上添加事件监听的时候,实际上是给dom所监听的事件注册回调函数。
如果dom监听到回调函数的话,会把回调函数放进任务队列(queue)里,注意,是事件线程将回调函数扔进任务队列的。

时间线程

时间线程用于处理定时任务,如setTimeout,setInterval。
setTimeout和setInterval处理机制是一致的,区别在于setTimeout只执行一次,setInterval可以循环执行。

console.log(1);
setTimeout(()=>{
    console.log(2)
},200)

setTimeout(()=>{
    console.log(3)
},100)
let then = new Date().getTime();
while(new Date().getTime() -then < 1000){

}
console.log(4)

如上,当代码执行到setTimeout(arg1,arg2)的时候,会将此任务交给时间线程处理,主线程继续执行后面的代码。告诉时间线程,xxxms后,将回调放入任务队列(queue)里,xxx为函数第二个参数,回调为第一个参数。注意,是时间线程将回调函数扔进任务队列的。

网络线程

网络线程用于处理网络请求,如ajax请求,script,image请求等。
当网络请求成功(或失败),在监听请求状态变化后(比如xhr请求的readyState、script或者image的load事件),将回调扔进任务队列(queue)里。

IO线程

IO线程主要处理文件读写,DB操作等。
和网络线程类似,操作完成后将回调扔进任务队列里。

以上4类都是将回调放进宏任务队列(macro task)
JavaScript还有一个概念叫微任务(micro tasks),比如process.nextTick, Promise, Object.observer, MutationObserver等产生的回调,都会放进微任务队列里,微任务会优先于宏任务。

##总结

  1. JavaScript主线程其实是一直执行同步任务
  2. 所有的异步任务都会交给其他线程处理
  3. 线程处理完后将回调函数(如果有回调)扔进任务队列里
  4. JavaScript同步任务执行完毕后,优先选择微任务队列里的任务执行
  5. 轮询任务队列,如果有任务,取第一个执行,如果没有,继续轮询。
  6. 重复1-5