封面pid: 82755969

HTTP

常用请求方法
|method|description|
|-|-|
|GET|请求资源,返回实体|
|HEAD|只返回响应头的GET|
|POST|向资源提交数据|
|PUT|上传、替换资源|
|DELETE|删除资源|
|OPTIONS|查询目标资源的通信选项|

常用请求头
|字段|描述|例子值|
|-|-|-|
|accept|接受内容类型|application/json|
|accept-charset|接受字符编码|utf-8|
|accept-encoding|接受编码格式|gzip,deflate|
|accept-language|接受的语言|zh-CN|
|cache-control|缓存控制|no-cache|
|connection|连接类型|keep-alive|
|content-length|请求体长度|233|
|content-type|请求体mime类型|application/x-www-form-urlencoded|
|cookie|cookie|var1='2333'|
|date|请求发送时间|- -|
|host|服务器域名和端口|oshinonya.com|
|if-none-match|协商缓存策略|资源etag|
|if-modified-since|协商缓存策略|资源最后修改时间|
|origin|跨域请求发起源|https://oshinonya.com|
|range|请求部分资源|bytes=0-511|
|referer|请求的发起页面|https://oshinonya.com|
|user-agent|用户代理|- -|

常用响应头
|字段|描述|例子值|
|-|-|-|
|access-crontrol-allow-*|跨域设置|- -|
|accept-ranges|分段请求的类型|bytes|
|allow|允许的请求方法,出现在405状态|GET,POST|
|cache-control|缓存控制|public, max-age=3600|
|content-disposition|弹出下载框,指定文件名|attachment;filename="me.png"|
|content-range|标识资源的所属部分|bytes 0-127/233|
|content-encoding/language/length/type|同上|- -|
|etag|资源标识符|- -|
|expires|资源过期时间|- -|
|last-modified|资源最后修改时间|- -|
|location|重定向或创建资源|https://oshinonya.com|
|refresh|支持延迟的location|5;url=https://oshinonya.com|
|server|服务器名|- -|
|set-cookie|cookie设置|var1='2333'|
|transfer-encoding|响应体编码格式|chunked|

常用状态码

  • 100等待剩余请求
  • 200201已创建、202已接收正处理、206部分处理get请求
  • 301永久移动、302/307暂时移动、304资源无修改
  • 401未授权、403禁止、404405方法禁止、406不接受(无法满足accpet字段)、408超时、409冲突、410资源已删除、413请求体过大、414URI过长
  • 500内部错误、502错误网关、503服务不可用、504网关超时

HTTPS

http报文采用明文传输,所以中途容易被截获窥视篡改,而且收发双方不能确认对方的真伪。
因此https出现了,在http的基础上使用ssl/tls加密数据包,实现了网站服务器的身份认证,保护交换数据的隐私与完整性。
162e3583d5cb5b1e.png
其中的公钥的获取需要CA的参与

  • 用户向web服务器发起一个安全连接的请求
  • 服务器返回经过CA认证的数字证书,证书里面包含了服务器的public key(被CA加密)
  • 用户拿到数字证书,用自己浏览器内置的CA证书解密得到服务器的public key
  • 用户用服务器的public key加密一个用于接下来的对称加密算法的密钥,传给web服务器
  • 因为只有服务器有private key可以解密,所以不用担心中间人拦截这个加密的密钥
  • 服务器拿到这个加密的密钥,解密获取密钥,再使用对称加密算法,和用户完成接下来的网络通信

http/2特性:二进制分帧、多路复用、头部压缩、服务器推送

浏览器缓存

分为强缓存和协商缓存,强缓存优先度高

  • 强缓存,在客户端判断。首次加载资源时其cache-control(高优先)和expire字段将被缓存,两个字段用于之后相同资源的命中判断
  • 协商缓存,在服务端判断。当强缓存没命中时,客户端设置if-none-match/if-modified-since字段携带资源的etag/last-modified发往服务端,由服务器指示客户端是否使用缓存
    • 当请求的if-none-match与资源的标识相等时,资源无变化,服务器返回304状态指示使用缓存;否则返回资源内容和新的etag
    • if-modified-since同理,但是使用资源的最后修改时间来判断

MVVM

Model-View-ViewModel,ViewModel作为中介分别和Model、View进行双向通信,实现响应式、数据驱动。

  • Model:数据模型,定义数据操作的业务逻辑
  • View:视图,将数据可视化,提供人机交互
  • ViweModel:通过双向数据绑定链接前两者,自动完成前两者的同步工作

生命周期

  • 创建前/后:白手起家/已数据绑定、事件绑定
  • 载入前/后:已编译模板、render/已挂载el、html渲染
  • 更新前/后:数据更新前/已重新渲染虚拟dom、打补丁
  • 销毁前/后

双向绑定

Vue2中双向绑定通过Object.defineProperty()(数据劫持)和发布——订阅模式实现,在数据变动时发布消息给订阅者,触发相应的监听回调。

整个机制拆分成以下实现:

  • 实现一个监听器Observer,对数据对象的所有属性进行监听,如有变动带着新值通知订阅者
  • 实现一个订阅者Watcher,在接收到消息时执行编译时预先绑定的回调函数
  • 实现一个编译器Compile,初始化视图、解析源代码根据指令初始化Watcher为其绑定回调函数
  • 消息订阅器Dep用于收集订阅者,转发监听器的通知

图源:https://segmentfault.com/a/1190000006599500
实现图解

监听器

监听器的任务是劫持数据对象的所有属性(遍历实现),拦截属性的读写行为,在属性被修改的时候发送消息给消息订阅器,通知订阅者。

function Observer(data) {
  this.data = data;
  this.walk(data);
}

Observer.prototype = {
  walk: function(data) {
    var self = this;
    //遍历对象,劫持属性
    Object.keys(data).forEach(function(key) {
      self.defineReactive(data, key, data[key]);
    });
  },

  defineReactive: function(data, key, val) {
    var dep = new Dep();
    // 劫持当前属性
    var childObj = observe(val);
    Object.defineProperty(data, key, {
      enumerable: true,
      configurable: true,
      get() {
        if (Dep.target) // 添加一个订阅者
          dep.addSub(Dep.target);
        return val;
      },
      set(newVal) {
        if (newVal === val) {
          return;
        }
        val = newVal;
        // 新的值是object的话,进行监听
        childObj = observe(newVal);
        // 通知watcher(订阅者)数据变更,执行对应订阅者的更新函数
        dep.notify();
      }
    });
  }
};

function observe(value) {
  if (!value || typeof value !== 'object') {
    return;
  }
  // 如果是属性是对象,递归劫持
  return new Observer(value);
};

// 消息订阅器Dep,负责收集订阅者,然后在属性变化的时候通知订阅者更新
function Dep () {
  this.subs = [];
}
Dep.prototype = {
  // 添加订阅者
  addSub: function(sub) {
    this.subs.push(sub);
  },
  // 通知订阅者
  notify: function() {
    this.subs.forEach(function(sub) {
      sub.update();
    });
  }
};
Dep.target = null;

其中Dep.target是订阅者,后面订阅者创建时主动申请加入订阅器。

Watcher

订阅者要监听某个属性,就带着这个属性名和回调函数申请加入订阅器,等待被通知

function Watcher(vm, exp, cb) {
  this.cb = cb; //回调函数
  this.vm = vm; //vue实例
  this.exp = exp; //要监听的属性
  this.value = this.get();  // 将自己添加到订阅器的操作
}

Watcher.prototype = {
  update: function() {
      this.run();
  },
  run: function() {
      var value = this.vm.data[this.exp];
      var oldVal = this.value;
      if (value !== oldVal) {
          this.value = value;
          this.cb.call(this.vm, value, oldVal);
      }
  },
  get: function() {
      Dep.target = this;  // 缓存自己
      var value = this.vm.data[this.exp]  // 强制执行监听器里的get函数,订阅成功
      Dep.target = null;  // 释放自己
      return value;
  }
};

上面Object.defineProperty()这种实现的缺点是不能监听数组长度的修改和内容的修改。因此在Vue3中改用Proxy实现数据劫持

虚拟dom

dom对象的结构较为复杂,既要提供各种属性和事件回调,还要实现繁多的接口,因此增删和移动dom是比较费资源的。应该尽量避免频繁增删dom,转而修改现有dom实现等价效果。

既然直接操作复杂的dom不够高效,那就不要直接操作dom呗,为此引入了虚拟dom。虚拟dom的思想是用较轻量的JS对象来模拟复杂的dom结构(只保留标签名、属性、内文本、父子兄节点等重要属性),将原本对dom的操作转为对虚拟dom的操作,最后再将虚拟dom一次性映射为真实dom。这样其实相当操作整合,减少重绘。下面是一个例子

//Element 返回一个虚拟dom对象
function Element(tagName,props,children){
  if(!(this instanceof Element))
    return new Element(tagName,props,children)

  this.tagName = tagName;
  this.props = props || {};
  this.children = children || [];
  this.key = props?.key;

  let count = 0;
  this.children.forEach(child=>{
    if(child instanceof Element)
      count += child.count;
    count++;
  });
  this.count = count;
}

//虚拟dom渲染方法
Element.prototype.render = function(){
  const el = document.createElement(this.tagName);

  for (let propName in this.props)
    el.setAttribute(propName,this.props[propName])

  this.children.forEach(child=>{
    const childEl = (child instanceof Element)?
    child.render():document.createElement(child.tagName);
    el.appendChild(childEl);
  })

  return el;
}

//构造虚拟dom并渲染
let tree = Element('ul',{},[
  Element('li',{class:'item'},['item1']),
  Element('li',{class:'item'},['item1']),
])
tree.render();

上面的虚拟dom最后映射为以下结构

<ul>
  <li class="item">item1</li>
  <li class="item">item1</li>
</ul>

虚拟dom的这种特性非常适合数据驱动的框架(实际上它就源于此),即将数据映射为虚拟dom,再将虚拟dom映射为真实dom。为了进一步优化,虚拟dom考虑了节点重用,如果变化前后节点没发生本质变化(节点类型),则只要修改该虚拟dom的属性/移动该虚拟dom位置即可,再最后映射为真实dom时按照上面找到的变化进行局部更新即可。

另:虚拟dom模拟了dom却不是dom,也就解决了跨平台没有dom不能渲染的问题

上面这种找变化的算法称为diff算法,将变化反映到dom的算法称为patch(打补丁)算法。因为实际使用很少出现跨层更新,diff算法放弃深度遍历转而采用平层比较。平层diff对比结果可以分成以下情况:

  • 节点类型改变,记为REPLACE,dom需要进行替换
  • 属性/属性值改变,记为PROPS,只需更新dom属性即可
  • 文本改变,记为TEXT,只需更新dom的innerText即可
  • 增删移动节点,记为REORDER,需要移动dom
  • 完全一样

在Vue2中两者是同时进行的,只存在名为patch的方法(在数据发生变化被通知调用),接受新旧vdom,在对比的同时进行更新dom。整个patch过程分为两部分

  • patchAttr:对比vdom、更新dom的属性
  • patchChildren
    • 如果只有新vdom有children,为该dom创建并插入子节点
    • 如果只有旧vdom有children,删除dom的子节点
    • 新旧vdom都有children,对比并更新其children(同层递归patch)

第一部分很容易理解,旧vdom有的新vdom没的属性要删除;旧vdom没的新vdom有的属性要新增;新旧vdom都有的属性如果发生变化以新vdom的为准。

第二部分对比子vdom的方法非常巧妙,为两个children序列引入头尾指针:oldStartoldEnd分别指向旧children的头尾;newStartnewEnd分别指向新children的头尾。现在针对这四个位置进行比较,分为以下几种情况:

  1. 头相同/尾==相同==
  2. 头尾==相同==
  3. 新增节点
  4. 删除节点
  5. 其他(发生更新、偏移)节点

注:patch的条件是两vdom ==“相同”==,这里 ==“相同”== 的依据并不是vdom指向同一个dom,只要两者指向的dom是同类型的(如都是div),就认为 ==“相同”==。这是因为Vue会尽可能复用dom,减少dom的移动,所以就算 ==“相同”==,其一些属性和children还是可能不同的,需要++patch++。下文出现的 ==“相同”== 皆为该意。

算法图解算法核心代码。算法的简要步骤如下,检查当前指针,下面的情况选一个进行处理:

  • 头相同,++patch++,两个头指针后移
  • 尾相同,同上,两个尾指针前移
  • 头尾相同
    • oldStartnewEnd相同:++patch++,oldStart的dom后移至oldEnd的dom的后面,指针移动
    • newStartoldEnd相同:++patch++,oldEnd的dom前移到oldStart的dom的前面,指针移动
  • newStart指向的vdom不能在旧children中找到 ,说明是新增的,创建dom插入对应位置,newStart指向后移
  • newStart指向的vdom能在旧children的指针区间中找到
    • 且相同:可以复用移动,++patch++,将原vdom标记为已处理(直接赋值为undefined),newStart的dom移到oldStart的dom的前面,newStart指向后移
    • 且不同:放弃复用,创建dom插入对应位置,newStart指向后移

重复以上操作,直至两对指针中的一对相互越过,最后处理剩下的待删/增节点:

  • 如果new指针对先越过,那old指针闭区间内未处理的vdom对应删除的节点
  • 如果old指针对先越过,那new指针闭区间内的vdom对应新增的节点

Vue render

通常情况下,我们使用模板(template)来定义组件,Vue会分析模板生成AST,并根据AST编译出render函数(渲染函数),用该render函数生成组件对应的vdom树(组件在render的时候watcher会收集组件的依赖并在依赖更新的时候re-render),才有后面的响应式++patch++。

render的小伙伴们

无论怎样,vdom树都是靠render函数生成的,render函数可以由模板自动转换而来,也可以在组件直接提供,编程式的描述vdom树的构造方式(更灵活)。如下面的render函数对应模板<p>{{content}}</p>

data(){
  return{
    content:'2333' 
  }
},
// render函数需要返回一个vdom
render(createElement){
  return createElement('p', this.content)
}

createElement函数用于创建vdom,最多可以接受三个参数

第一个参数{String | Object | Function},对应dom标签

  • 可以是表示标签名的string
  • 可以是包含template的object
  • 或是能返回上述两数据的function

第二个参数{Object},对应dom上各种属性,更多参考

{
  //dom的class
  'class': {
    foo: true,
    bar: false
  },
  //dom的style
  style: {
    color: 'red',
    fontSize: '14px'
  },
  //标签上的属性
  attrs: {
    id: 'boo'
  },
  //组件 prop
  props: {
    myProp: 'bar'
  },
  //dom的属性
  domProps: {
    innerHTML: 'Hello Vue!'
  }
  //事件绑定
  on: {
    click: this.clickHandler
  }
  //...
}

第三个参数{String | Array},对应子vdom(集),需要注意的是这些子vdom必须是唯一的,不能出现引用的重复。第三个参数可以顶替第二个参数

render函数中要模拟v-ifv-for指令,可以使用if/elsemap实现。要使用插槽可以使用this.$slots。要模拟v-model则需要自己绑定input事件。

props: ['value'],
render: function (createElement) {
  var self = this
  return createElement('input', {
    domProps: {
      value: self.value
    },
    on: {
      input: function (event) {
        self.$emit('input', event.target.value)
      }
    }
  })
}

    添加新评论 | #

    Markdown Supported
    简单数学题:NaN + NaN =
    表情

    Comments | ?? 条评论

    • 单身为狗 21 年

    • 朝循以始 夜继以终

    • Blog: Von by Bersder

    • Alive: 0 天 0 小时 0 分

    Support:

    frame ssr server DNS music player

    © 2019-2021 ᛟᛊᚺᛁᚾᛟ

    back2top