Coder Social home page Coder Social logo

blog's Introduction

🤔 Hi

A front-end developer, often use Vue, React, NodeJs to realize my idea.

javascript typescript react vue nodejs

Repo

🤨 Want to replace those charging tools that are commonly used at work.

Thinking

✍️ Want to find a standard solution.

  • 💁‍♂️ Form linkage
  • 💁‍♂️ Time travel
  • 💁‍♂️ Rich text editor based on slatejs
  • 💁‍♂️ XProcess based on SVG Canvas

blog's People

Contributors

jmingzi avatar

Stargazers

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

Watchers

 avatar  avatar  avatar  avatar

Forkers

zhl501305483

blog's Issues

事件节流

Throttle

概念

事件节流是指控制事件在一段时间内只执行一次,比如window的resize/scroll,还比如mousemovetouhchmove,还有输入框的input/change,当然输入框的更适合防抖debounce去做。

简单实现

throttle的实现方式

  1. 对比前后的时间戳,再去执行
  2. setTimeout定时器
// common callback
function cl(e) {
  console.log('resize callback', e)
}

// 方法1
function throttle_1 (fn, wait) {
  // 初始化start时间
  let previous = 0

  return function() {
    let now = new Date().getTime()
    
    if (previous === 0) {
      previous = now
      // 第一次调用
      // 可以用参数控制第一次是否执行
      fn.apply(this, arguments)
    }

    // 时间大于wait
    if (now - previous > wait) {
      fn.apply(this, arguments)
      // 记录执行结束的时间
      previous = now
    }
  }
}
window.addEventListener('resize', throttle_1(cl, 2000))

// 方案2
// 网上很多都说定时器可以实现,但其实是实现不了节流,可以实现防抖,
function throttle_2 (fn, wait) {
  let timer = null

  return function() {
    let context = this
    let args = arguments

    if (timer) {
      // 清除timer后,又得等待wait时间后才执行
      clearTimeout(timer)
      // clearTimeout后timer并不会清除,需手动置为null
      timer = null
    }

    // 第一次初始化
    // 可以选择是否直接执行,还是在wait后执行
    // 如果一直hold住,就始终不会回调,就和debounce需要的一毛一样
    timer = setTimeout(()=> {
      fn.apply(context, args)
    }, wait)
  }
}
window.addEventListener('resize', throttle_2(cl, 2000))

简单实现的问题

  • 方法一:初始化都会调一次,而结束的时候不会回调。除非结束的时间差刚好为你需要的wait值
  • 方法二: 初始化的时候不会调用,而结束的时候始终都会调一次,因为是定时器。

underscore实现方式

虽然定时器不能实现节流,但是可以配合方法1来更好的解决上述的问题。

underscore的节流可以控制头尾的回调是否执行,也就是第三个参数options

{
  // 首次不回调
  leading: false,
  // 最后不回调
  trailing: false
}

让我们来逐行分析它是如何结合2者的(以下为拷贝的underscore源码)

// Returns a function, that, when invoked, will only be triggered at most once
// during a given window of time. Normally, the throttled function will run
// as much as it can, without ever going more than once per `wait` duration;
// but if you'd like to disable the execution on the leading edge, pass
// `{leading: false}`. To disable execution on the trailing edge, ditto.
_.throttle = function(func, wait, options) {

  // 初始化声明缓存变量,利用了闭包变量不销毁的原理,始终保持在内存中
  // 为了防止内存泄漏,执行完成后一要将变量置为null
  var timeout, context, args, result;
  var previous = 0;
  if (!options) options = {};

  var later = function() {
    // 初次回调不执行将previous = 0,也就是previous = now
    previous = options.leading === false ? 0 : _.now();
    timeout = null;
    result = func.apply(context, args);
    if (!timeout) context = args = null;
  };

  var throttled = function() {
    // 事件回调时间
    var now = _.now();
    
    // 判断初次回调 是否关闭
    if (!previous && options.leading === false) previous = now;

    // 执行下次回调需要等待的时间
    // 1.如果初次不回调,则remaining === wait,也就是previous = now
    // 2.初次回调 remaining < 0
    // 3.手动调整客户端的时间后,remaining > wait
    var remaining = wait - (now - previous);

    context = this;
    args = arguments;

    if (remaining <= 0 || remaining > wait) {
      // 初次回调执行

      // 此处的timeout可能有值
      // 是上一次回调的timeout
      if (timeout) {
        clearTimeout(timeout);
        timeout = null;
      }
      
      // 记录回调执行的时间
      previous = now;
      result = func.apply(context, args);

      // 手动置为null
      if (!timeout) context = args = null;
    } else if (!timeout && options.trailing !== false) {
      // 如果已经存在一个定时器,则不会进入该 if 分支
      // 初次回调不执行
      // 末尾回调执行
      timeout = setTimeout(later, remaining);
    }
    return result;
  };

  throttled.cancel = function() {
    clearTimeout(timeout);
    previous = 0;
    timeout = context = args = null;
  };

  return throttled;
}
window.addEventListener('resize', _.throttle(cl, 2000))

总结

underscore的节流函数考虑到了很多方面:

  1. 首尾回调可传参控制

  2. 一定时间间隔内只会触发回调一次,且间隔的时间是动态计算的,也就是说定时器的时间是动态的。上面写的方法2中说的定时器实现不了的问题,hold住不放,定时器会从头计算等待的时间,再开始触发回调

  3. 计算时间差与定时器来解决的好处就是可以动态赋值定时器的remaining time,这也是二者结合的最根本。

参考

underscore 函数节流的实现
JS魔法堂:函数节流(throttle)与函数去抖(debounce)

学习编写shell脚本(1)

学习它的目的

  1. 快速执行一系列操作,节省时间
    一个项目的打包、提交、分支切换、merge、push,包括额外的操作,修改版本号、然后build、publish等。这一系列的操作完全可以用一个shell脚本完成,因为每次手动输入命令会很繁琐。

  2. 规范git commit
    我们可以在shell脚本中添加commit规范,这样使得每次提交都会至少标明规范所需要的。关于commit规范,可以自行查阅

# 规范的commit提交的格式类似

<type>(<scope>) : <subject>
feat(location): add something ...

# type
feat 新功能
fix 修复bug
doc 修改了文档
debug 调试
style 代码格式改变
refactor 某个已有功能重构
perf 性能优化
test 增加测试
build 改变了build工具 如 grunt换成了 npm
revert 撤销上一次的 commit 

# scope 用来说明此次修改的影响范围
all 表示影响面大 ,如修改了网络框架  会对真个程序产生影响
location 表示会影响某个小小的功能
module 表示会影响某个模块 

# subject 用来简要描述本次改动
  1. 理解shell的工作原理,对于webpack或node构建工具的熟悉有帮助

脚本解析器

查看Element UI中的release.sh,部分代码:

#!/usr/bin/env sh
set -e
echo "Enter release version: "
read VERSION

其中的#!/usr/bin/env sh 告诉操作系统用来解释脚本的程序位置。

#通常用作注释,但是#!放在一起就标志着这是一个shell script,其后的路径指出了用来解释这个script的程序。

目前shell脚本中主要有以下两种方式:

  • #!/bin/sh
  • #!/bin/bash

区别:使用sh调用执行脚本相当于打开了bash的POSIX标准模式,即

/bin/sh ===  /bin/bash --posix

--posix表示当某行代码出错时,不继续往下解释。

而Element Ui中的/usr/bin/env sh等于/bin/sh

参考 Mac OS X 执行shell以及bash shell的区别

shell 基础入门

在“抛砖引玉”理解之后,我们需要真正系统的了解下Shell Script语法。接下来的计划就是系统的学习Shell了。

【业务】搜索页面中iOS input 不能focus产生的问题及妥协处理办法

问题1

京东首页 到 搜索页为2个页面,在iOS中,input输入框获取焦点即input.focus()必须由用户触发,否则无效。

即使使用setTimeout or nextTick也无效。

I think this is a feature of mobile Safari rather than a bug. In our work on FastClick, my colleagues and I found that iOS will only allow focus to be triggered on other elements, from within a function, if the first function in the call stack was triggered by a non-programmatic event. In your case, the call to setTimeout starts a new call stack, and the security mechanism kicks in to prevent you from setting focus on the input.

解决办法

第一步

由将搜索页面作为组件分别引入首页和列表等其他需要用到搜索的页面,在点击的时候去触发focus

于是又来带另外一个问题,作为组件引入的时候,只能通过state来控制显示与隐藏,而input的focus必须是display:block;的元素才行。

由于设置了state后,需要vue本身的watcher去更新视图,此时是个异步的过程,focus就无效了。

第二步

手动操作dom为block后再focus,此时带来的牺牲是搜索页展现的时候没有动画效果了。

流程图如下

image

动画对比

首页兼容的没有动画列表页不能focus有动画 对比

首页兼容的没有动画

参考

iOS下setTimeout无法触发focus事件的解决方案
Mobile Safari Autofocus text field

避免按钮重复点击请求的正确办法

不应该是用一个变量flag去标记是否应该return,我也一直觉得很别扭。。。

应该是在下一个请求发出前,cancel掉之前的请求。

例如在使用axios

const CancelToken = axios.CancelToken
const source = CancelToken.source()

// get
axios.get('/url/params', {
  cancelToken: source.token
})

// post
axios.post('/user/12345', { 
  name: 'new name'
}, {
  cancelToken: source.token
})

source.cancel('Operation canceled by the user.')

Vue源码阅读笔记 - this._init方法

// 伪代码

let uid = 0

Vue.prototype._init = function(options) {
  const vm = this
  // 
  vm._uid = uid++
  // a flag to avoid this being observed
  vm._isVue = true
    
  // merge options
  if (options && options._isComponent) {
    
  }

  // 
  if (dev) {
    initProxy(vm)
  } else {
    // 含义?
    vm._renderProxy = vm
  }

  vm._self = vm
  
  // 初始化生命周期
  // $parent $root  $children $refs 
  // _watcher _inactive _directInactive 
  // _isMounted _isDestroyed _isBeingDestroyed
  initLifecycle(vm)

  // 绑定事件
  // 为什么是 init parent attached events
  // 即 vm.$options._parentListeners
  initEvents(vm)

  // $slots $scopedSlots
  // _c $createElement
  // 绑定attrs和listeners
  initRender(vm)
  
  // 在调用生命周期钩子时,是可以调用多次
  // handlers[i].call(vm)
  callHook(vm, 'beforeCreate')

  // 
  initInjections(vm)

  // 初始化props、data、methods、computed
  // 如果定义了watch,也初始化watch
  // 其中有observe(value)
  initState(vm)

  // 
  callHook(vm, 'created')

  // 如果el存在
  vm.$mount(vm.$options.el)
}

h5问题收集一览

  • ios7不支持promise,采用polyfill
import Promise from 'promise-polyfill'
if (!window.Promise) {
    window.Promise = Promise;
}

flex

本文介绍flex基础属性点的含义和归类

flex 通用样式类库 https://github.com/lzxb/flex.css/blob/master/docs/zh-ch.md

flex的作用方式分为容器和元素,x轴和y轴,将属性归纳起来便于理解

容器类:

flex-direction  // 决定子元素的排列方向,x轴 or y轴
flex-wrap  // 决定子元素的是否换行
justify-content  // 决定子元素在x轴中空余space的分配
align-content  // 决定子元素在y轴中空余space的分配
align-items // 子元素在y轴上的排列方式

元素类:

order  // 排序
flex-grow // 对空间占用的大小
flex-shrink 
align-self  // 功能与align-items一致,会覆盖align-items

参考 https://css-tricks.com/snippets/css/a-guide-to-flexbox/

熟悉 Proxy 及其场景

本文同步发表在个人博客 熟悉 Proxy 及其场景

概述

要想熟悉 Vue 3 源码,熟悉 Proxy 特性必不可少,本文主要内容:

  • proxy 概念
  • 相关 API
  • proxy 实现双向绑定
  • 遇到的一些问题

基于 javascript 的复杂数据类型的特点,衍生出的代理的概念,因为对于复杂的数据类型,变量存储的是引用。代理 proxy 就在引用和值之间。

另外还需要注意 Reflect,它拥有的13个方法与 proxy 一致,用来代替 Object 的默认行为。很显然,例如我们用 proxy 修改了对象属性的 getter,那如何使用原本默认行为?

例如以下代码会陷入死循环:

const obj = { name: '' }
new Proxy(obj, {
  get: function (target, prop) {
    // 错误的
    return target[prop]
    // 应该使用 Reflect 来得到默认行为
    return Reflect.get(target, prop)
  } 
})

相比于 Object.defineProperty ,Proxy 有 polyfill 可以 hack,兼容性会更好。

熟悉 13 个 API

  • get / set

和对象描述符中访问器属性 get/set 一样,一般在使用 Object.defineProperty 时重新定义对象属性的描述符。

{
  get: function (target, property, receiver) {
    return Reflect.get(target, property)
  },
  set:  function (target, property, value, receiver) {
    // 必须返回一个 Boolean
    return Reflect.set(target, property, value)
  }
}

关于 receiver,一般情况下 receiver === proxy 实例 即原对象的代理对象,例如:

const proxy = new Proxy({}, {
  get: function(target, property, receiver) {
    console.log(receiver === proxy)
    return receiver
  }
})

⚠️当 proxy 为一个对象的原型时,receiver 就不是 proxy 实例了,而是该对象本身。

const proxy = new Proxy({}, {
  get: function(target, property, receiver) {
    // 不要试图在这里获取 receiver,否则会造成死循环
    // 因为 receiver === d
    // console.log(target, receiver)
    console.log(receiver.name) // ym
    return receiver
  }
})

const d = {
  name: 'ym'
}
d.__proto__ = proxy
// const d = Object.create(proxy);
console.log(d.a === d) // true

其实也不用想那么多,receiver 就是调用对象本身,而 target 是设置 Proxy 的对象。

  • apply/construct

apply用于拦截函数调用,construct方法用于拦截new命令。

{
  // newTarget 和 receiver 类似
  // 这里就只有一种情况,是 proxy 的实例
  construct: function (target, args, newTarget) {
    // 必须返回一个对象
    return new target(...args)
  },
  // ctx 是函数调用的上下文
  apply: function (target, ctx, args) {
  }
}
  • has/deleteProperty

它们对应于 in/delete,将这些操作变成函数行为。

剩下一些 API 相对简单,使用时可以查看 MDN 文档,没必要刻意记忆。

  • defineProperty/ownKeys
  • getPrototypeOf/setPrototypeOf
  • isExtensible/preventExtensions
  • getOwnPropertyDescriptor

实现双向绑定

响应式的三要素:模版、观察者、事件中心。

以下是使用的例子:

<template>
  <div id="app"></div>
</template>

<script>
new M({
  template: `<div><p>输入框的值:{{ name }}</p><input type="text" v-model="name"></div>`,
  data () {
    return {
      name: ''
    }
  }
}).mount('#app')
</script>

我们 M 类的实现:

function M(opts) {
  // 为 data 设置代理
  this.data = observe(opts.data())
  // 得到模版节点
  this.node = getNodes(opts.template)
  // 解析模版节点
  this.compileElement(this.node)
}

M.prototype.mount = function (selector) {
 document.querySelector(selector).appendChild(this.node)
}

M.prototype.compileElement = function (node) {
  // 递归处理dom节点
  Array.from(node.childNodes).forEach(node => {
    let text = node.textContent
    let reg = /\{\{(.*)\}\}/

    if (node.nodeType === 1) {
      this.compile(node)
    } else if (node.nodeType === 3 && reg.test(text)) {
      this.compileText(node, reg.exec(text)[1])
    }

    if (node.childNodes && node.childNodes.length > 0) {
      this.compileElement(node)
    }
  })
}

// 处理文本节点
M.prototype.compileText = function (node, expression) {
  let reg = /\{\{.*\}\}/
  expression = expression.trim()
  let value = this.data[expression]
  const oldText = node.textContent
  value = typeof value === 'undefined' ? '' : value
  node.textContent = oldText.replace(reg, value)

  // 添加事件处理
  add(expression, (value, oldValue) => {
    console.log(value, oldValue)
    value = typeof value === 'undefined' ? '' : value
    node.textContent = oldText.replace(reg, value)
  })
}

// 简单的处理 v-model
M.prototype.compile = function (node) {
  Array.from(node.attributes).forEach(attr => {
    if (attr.name === 'v-model') {
      node.value = this.data[attr.value]
      node.addEventListener('input', e => {
        this.data[attr.value] = e.target.value
      })
      node.removeAttribute(attr.name)
    }
  })
  return node
}

简单的观察者与事件中心:

// 简单的事件中心
const observeMap = {}
function add(k, cb) {
  observeMap[k] = cb
}

// 观察者,我们使用 proxy
function observe(tar) {
  const handler = {
    get: function (target, property, receiver) {
      return Reflect.get(target, property)
    },
    set: function (target, property, value, receiver) {
      const oldValue = Reflect.get(target, property)
      const setResult = Reflect.set(target, property, value)
      // 只是简单的处理下存在与否的判断
      if (observeMap[property]) {
        Reflect.apply(observeMap[property], receiver, [value, oldValue])
      }
      return setResult
    }
  }
  return new Proxy(tar, handler)
}

function getNodes(str) {
  const div = document.createElement('div')
  div.innerHTML = str
  return div.childNodes[0]
}

问题

Proxy 本身的特性所带来的问题

1. 对于嵌套的对象,只会代理第一层

var obj = { a: { b: 3 } }
// var arr = [1]
const proxy = new Proxy(obj, {
  get: function(target, prop, receiver) {
    console.log('get', prop)
    return Reflect.get(target, prop)
  },
  set: function(target, prop, value, receiver) {
    console.log('set', prop, value)
    return Reflect.set(target, prop, value)
  }
})
// proxy.push(2)
proxy.a.b = 1

// 只会打印 get a

所以如果我们要实现代理嵌套对象需要做递归处理。

2. 由于数组本身操作的特点,proxy 的 get 和 set 会被多次触发。

var arr = [1]
const proxy = new Proxy(arr, {
  get: function(target, prop, receiver) {
    console.log('get', prop)
    return Reflect.get(target, prop)
  },
  set: function(target, prop, value, receiver) {
    console.log('set', prop, value)
    return Reflect.set(target, prop, value)
  }
})
proxy.push(2)
// get push
// get length
// set 1 2
// set length 2

对于 push 操作,会先去get push 方法,再 set 数组下标 1 的值为 2,然后修改数组 length 属性值为 2,最后返回数组的长度 length。

也就是说,对于代理数组对象去触发回调需要考虑到触发的类型。

遍历对象属性的方法有哪些?以及它们的区别

在 es5 中,我们常用的获取对象属性的方式:

  • in 操作符
  • for ... in
  • Object.keys
  • Object.getOwnPropertyNames()

在熟悉了 Reflect 之后,我们要使用新的方式 Reflect.ownKeys()

⚠️它们的区别在于是否自身拥有这个属性、或者该属性存在与原型中。当我们在讨论一个对象是否存在某个属性时,已经是在讨论一个实例本身了。

以上提到的 es5 中的4种方法中,只有 in 操作符不区分属性所在的位置,其它都要求对象实例本身拥有该属性。

同时,该对象的属性描述符 enumerable 为 true 才能被遍历到。

function Person(name, age) {
}
Person.prototype.name = 'cjh'
const person = new Person('ym', 18)

'name' in person // true
Object.keys(person) // []
Reflect.ownKeys(person) // []

person.name = 'ym'
Reflect.ownKeys(person) // ['ym']

delete person.name
Reflect.ownKeys(person) // []

后续会学习 Vue 3 源码,敬请关注。

vue ssr及需要掌握的点

讲些别人没有讲到的,前提是看完了官方的教程,另外,一篇文章肯定是讲不完的,此处将持续更新。

vue ssr本身之外涉及的点

vue ssr整体原理

在前后端分离的背景下,ssr(服务端渲染)就是做类似php和java模版引擎做的事情

so,用node做服务器,在访问vue 客户端路由的时候,匹配出涉及到的组件,即getMatchedComponents,检查这些组件是否需要服务端的数据渲染,即组件内置method asyncData。在所有涉及到的接口都返回后,将数据以context传递给html拼接,window.__INITIAL_STATE__,这就是server 端所做的事情。

到了客户端挂载的时候,需要将服务端之前取过的数据,即window.__INITIAL_STATE__状态对接,即官方的混合,其次,如果客户端存在需要预取数据的组件,还要从这些组件中筛选出当前没有被服务端渲染过的,再去请求拿数据存到store里。

以上都是构建之外的代码逻辑,还有很重要的一点就是构建。

服务端的打包,就是将客户端中需要在服务端渲染的代码映射成一个json文件,即server-bundle.json,通过bundleRenderer去渲染从接口获取的数据,并拼接需要引用哪些模块的js文件即clientManifest里标注的,然后根据给定的模版拼接好返回给express响应请求,最后去执行客户端打包生成的文件,client-bundle,执行客户端的逻辑。

image

源码结构:

src
├── components
│   ├── Foo.vue
│   ├── Bar.vue
│   └── Baz.vue
├── App.vue
├── app.js # 通用 entry(universal entry)
├── entry-client.js # 仅运行于浏览器
└── entry-server.js # 仅运行于服务器

vue ssr路由与数据(router与store)的混合

router与store同app.js里创建vue实例原理一样,需要创建一个工厂函数来返回新的实例

因为运行在服务端的代码是保存在内存中的,有且仅有一份,这么做是为了使不同的客户端得到不同的实例。

路由

路由对应的组件应该是按需加载的,和在客户端创建vue实例一样

export function createRouter() {
  return new Router({
    // 只能使用history,因为hash的变化express检测不到
    mode: 'history',
    routes: [
      {
        path: '/:id',
        component: r => require.ensure([], () => r(require('../views/Index.vue')), 'Index')
      }
    ]
  })
}

不同的是,由于组件是异步的,需要等到所有组件加载完成之后才能创建实例并开始走流程。

在访问一个地址后,会push到router中,利用vuex-router-sync,将router与同步到store中,以便保存这个“数据快照”即window.__INITIAL_STATE__

数据

在路由加载完成后,会根据当前路由匹配出需要渲染的组件去取数据,此时会调用组件构造函数上的一个方法(非实例)即官方的asyncData(可随意命名),将当前storerouter作为参数传递给它

asyncData({ store, route }) {
    return store.dispatch('fetchItem', route.params.id)
},

这样它就可以获取数据并保存在store中。

最重要的混合步骤是在client-entry中初始化时做的

// entry-client.js

const { app, router, store } = createApp()

if (window.__INITIAL_STATE__) {
  store.replaceState(window.__INITIAL_STATE__)
}

【参考】

自动化部署流程图

image

【参考】

Vue源码阅读笔记 - 生命周期

在生命周期core/instance/lifecycle.js中,有很多方法

export function initLifecycle() {}
export function lifecycleMixin() {}
export function mountComponent() {}
export function updateChildComponent() {}
export function activateChildComponent() {}
export function deactivateChildComponent() {}
export function callHook() {}

接着上一节的思路,我们来看mountComponent方法

1、校验options.render是否存在
如果入口文件为entry-runtime.js,且未传入render,会将赋值默认的render函数createEmptyVNode
2、调用beforeMount
3、调用实例_render,创建updateComponent、Watcher
4、调用mounted
5、返回实例

我们看到这是生命周期mount的部分,可以发现create包括初始api这些操作没有。
所以我们得从new Vue()生成实例开始

2xc 6 1l6d bkl984 1fpb

core/index.js中,从instance/index.js引入了实例的构造函数Vue, 其次是initGlobalAPI(Vue)方法

instance/index.js中,都是将方法挂载到原型Vue.prototype

// 初始化`_init(options)`方法
initMixin(Vue)

// 初始化data、props、$set、$delete、$watch
stateMixin(Vue)

// 初始化$on、$once、$off、$emit方法
eventsMixin(Vue)

// 定义`_update`, `$forceUpdate`, `$destroy`
lifecycleMixin(Vue)

// 初始化$nextTick,_render方法
renderMixin(Vue)

最后初始化api,initGlobalAPI,将方法挂载到构造函数上,也就是全局公用的方法

global-api/index.js

  • 初始化set,delete,nextTick,options
  • extend了components/keep-alive组件
// 不知其用意,mark
extend(Vue.options.components, builtInComponents)
  • init
// 初始化插件
initUse(Vue)

// 初始化mixin,本质是mergeOptions
initMixin(Vue)

// 初始化extend方法
// 用cid标识constructor的唯一性
// 从而对每个constructor缓存
initExtend(Vue)
initAssetRegisters(Vue)

完成构造函数初始化和原型初始化后

this._init(options)

一切以此开始!

5e n82f3__i m5w q_47xp8

接下来,我打算从实例初始化开始instance/init.js initMixin()

http协议相关(ajax)

http 协议格式,报头,请求行,应答行,头部域,数据

1、基础概念

HTTP是一个没有状态的(不会记住或存储上一次的HTTP过程的状态)请求-响应(request-response) 协议。
请求和响应都包含请求头或响应头header和可选的请求体或响应体body(自由的文本),只有POST请求包含一个body:

  • GET:严格的说,GET只用于获取数据,而不会使web服务器(中的数据)发生变化。GET请求不包含请求体body,请求参数包含在URL的请求字符串中。
  • POST:用于更新web服务器上的数据,通过请求体body传递参数或数据(data);

2、MIME类型(即form表单的enctype)

类型 说明
application/x-wwwform-urlencoded 经过编码的键值对请求字符串(body),web服务器需要解码字符串获得参数,所有字符都会进行编码(空格转换为 "+" 加号)
application/json;charset=UTF-8 JSON格式的对象,提交时需要JSON.stringify({k: v, k: v})
multipart/form-data 不对字符编码,在使用包含文件上传控件的表单时,必须使用该值。
text/plain 空格转换为 "+" 加号,但不对特殊字符编码。

form标签的 enctype 属性
深入理解HTTP协议之POST方法——ajax实例
Ajax与HTTP协议

【参考】
TAT.tennylvHTTP,HTTP2.0,SPDY,HTTPS你应该知道的一些事
HTTP2简介和基于HTTP2的Web优化

http学习(二):简单的http协议

基本信息

  1. 请求报文是由请求uri、协议版本、可选的请求头部字段、内容实体构成
  2. 响应报文是由协议版本、状态码、用以解释状态吗的原因短语、可选的响应头部字段、实体主体构成。
  3. cookie的由来是为了解决HTTP/1.1无状态即不保存请求。
  4. 可以用OPTIONS * HTTP/1.1来表示查询http服务器端支持的http方法种类

方法详解

  • GET
    用来请求访问已被uri识别的资源
  • POST
    用来传输实体的主体

虽然get方法也可以传输实体的主体,但一般不用get方法进行传输,而使用post。
虽然post与get很相似,但post的主要目的不是为了获取响应主体的内容。

  • HEAD
    head与get一样,只是不返回响应的内容,用来确认uri的有效性以及资源的更新日期

  • OPTIONS
    用来查询针对请求URI制定资源支持的方法
    aa7a9226-9ceb-48d8-922a-05d61133bc32

持久链接 keep-alive

第一章我们说道,从输入URL到得到响应主体,要经历的步骤之一是建立tcp连接:

在传输层TCP协议中,需要3次握手,以确保报文准确可靠的送达,
那每一次请求的发出都需要建立tcp链接,传输结束后,才断开tcp。

为了解决tcp链接的大开销问题,提出了持久链接,只要任意一端没有提出要断开,tcp永远保持链接。

HTTP/1.1所有的连接默认都是持久链接

并发请求(请求管线化)

在持久链接的基础上,将请求发出 => 收到响应,接着再发出请求...,管线化后,如图:
0c86cb4a-5259-40ad-8472-146f2562b9d2

spa首屏优化的思考

利用ssr结合pwa,将app shell缓存在本地

由于做了code-spliting,需要收集路由依赖,将对应的js preloadprefetch

app-shell为webpack打包时,将依赖的js收集起来,生成新的service-worker.js,cache到本地,另外在需要显示的页面,利用组件的空状态先将app-shell渲染出来,再在下个eventloop的时候new Vue

关于本地缓存的更新,由于生成了新的service-worker.js,里面cachelist的文件也是由webpack打包带hash的,所以delete掉不在cachelist的文件即可

以上处理完带hash的js,都会在打包时插入到html中

另外,如果非ssr怎么办?

首先,app shell不可能手动开发,它可能只存在与首页路由里

html是可以被cache的,因为只要service-worker.js会更新就不会有问题

那么,我们可以这样做

  • app shell可以用工具生成,然后插入到html中,是否显示需要根据当前路由来判断
  • 利用pwa缓存html、app shell的依赖及其它不常更新的文件

一张流程图

architecture

延伸阅读:

Vue源码阅读笔记 - 文件结构

doz n 66 vm9k tb_9 0wm

浏览器端,入口文件为:

  • entry-runtime-with-compiler.js
  • entry-runtime.js

如果我们结合webpack打包来写项目,是vue-loader帮我们做了单文件解析以及compiler,所以入口文件为entry-runtime.js

如果是单独写,类似:

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

入口文件就是entry-runtime-with-compiler.js

其本质就类似于我们对函数参数类型或值的hack,Vue的options支持传eltemplaterender参数,这个文件就是对这些参数的处理hack

将得到的template字符串用compiler/index.js中的方法编译后,得到render/staticRenderFns函数,然后重写options.render/options.staticRenderFns,和Vue.prototype.$mount方法

核心是

const { render, staticRenderFns } = compileToFunctions(template, {
        // 2个方法都是判断浏览器是否encode html属性上的值,因为template支持语法
        // 是否decode换行符,ie会encode而其他不会
        shouldDecodeNewlines,

        // 是否decode a[href],chrome会encode
        shouldDecodeNewlinesForHref,
        
        // 解析模版字符串的分隔符,默认是`{{  }}`
        delimiters: options.delimiters,  
         // 是否保留注释,默认false
        comments: options.comments 
}, this)
options.render = render
options.staticRenderFns = staticRenderFns

另外关于runtime/index.js中 初始化时,用extend 挂载了默认的directivescomponents,和domDiff__patch__

最后,会将得到的options和el根节点传入core/instance/lifecycle mountComponent方法。

接下来,我们看mountComponent方法做了哪些事

promise笔记

觉得promise最实用的莫过于多个请求等待完成后再去执行某个操作。
像用jquery ajax的时候得声明多个变量去标识才行,而promise很简便的实现。

方法一览:

  • Promise.prototype.then
  • Promise.prototype.catch
  • Promise.all
  • Promise.race
  • Promise.resolve
  • Promise.reject

demo1 多个Promise等待完成后再执行

var promiseFunc = (time, type) => {
    return new Promise((resolve, reject) => {

        setTimeout(() => {
            type ? resolve('success') : reject('error')
        }, time*1000);

    })
}
let Time1 = promiseFunc(3, true)
let Time2 = promiseFunc(5, true)
let Time3 = promiseFunc(0, true)

Promise.all([Time1, Time2, Time3]).then((d) => {
    console.log(d)
}).catch((e) => {
    console.log(e);
})

【参考】Promise对象

http学习(一):web及网络基础

以下为《图解http》的第一章

http

1.http是一种协议,规范,而web是建立在http协议上的通信
2.http协议是TCP/IP协议族的子集

TCP/IP

1.TCP/IP是互联网相关的各类协议族的总称
2.TCP/IP分层管理:应用层,传输层,网络层和数据链路层

  • 应用层
    用于通信时的各种应用服务(ftp,dns,http)
  • 传输层
    对上层应用层,管理2台计算机之间的传输。有2个协议:TCP-传输控制协议;UDP-用户数据报协议
  • 网络层
    规定数据包通过怎样的路径传输,IP协议
  • 链路层
    用来处理链接网络的硬件部分,比如控制操作系统,驱动,光纤,以太网。属于硬件范畴

IP协议与IP地址

IP协议的作用是把各种数据包传送给对方 ,
IP地址指明了节点被分配的地址,MAC地址是指网卡所属的固定地址,它俩可相互匹配,IP地址可变,MAC地址不会更改

疑问1:

我们知道了在网络层需要决定哪条传输路径来传递数据包,此时必须要有2个条件满足才能做到,IP地址和MAC地址。IP地址我们是知道的,但MAC地址很可能不知道,这时采用ARP解析地址协议,根据IP地址反查出MAC地址。

疑问2:

在传输的过程中,可能并不是一次性传输到目的地,而是通过各个中转站,至于各个中转站是如何通过ARP解析出各自的MAC地址的还不清楚?为了传输方便,在传输层,TCP将数据分割成报文传输,为了保证传输的可靠性,采用3次握手。发送端先发送SYN标志,接收方说收到了,则回传SYN和ACK标志以示传达确认信息,最后发送端再回传ACK标志,标识握手结束。若在过程中被终端,则重新开始。

vue ssr疑问点

1. 关于getMatchedComponents

entry-server中,根据当前路由匹配出的组件,只包含路由组件么?而不包含路由组件中引用的子组件。
例如:

// route Component A
// 引入了 BottomTab 组件,在BottomTab组件中需要预取数据
<route-a>
  <bottom-tab />
</route-a>

而此时组件BottomTab,并没有被getMatchedComponents匹配到。

2.关于在客户端预取数据

起初看到官方文档数据的预取与渲染这里时,对客户端数据的预取很不理解,客户端为何要预取数据?

官方有2种实现:

// 1. 在entry-client.js中实现
router.onReady(() => {
  // 在路由初始化完成后,注册钩子函数去处理路由变化时,对组件的一些操作,比如数据预取。
  router.beforeResolve((to, from, next) => {
    // 在这里会根据to, from,来getMatchedComponents
    // 从而匹配出相应的路由组件
    // 筛选出to和from不一致的情况下,也就是跳转的路由与当前不一致时,才去预取数据
    // 为什么会这么做?我猜是在这种情况下,例如获取文章详情,或分类列表
    // 它们的路由看起来是这样的:`/detail/1`,`/list/1`,当路由变化时,组件并没有改变
    // 所以不需要去预取数据的,所以也就避免了二次预取(double-fetch)已有的数据
    const matched = router.getMatchedComponents(to)
    const prevMatched = router.getMatchedComponents(from)
    let diffed = false
    const activated = matched.filter((c, i) => {
      return diffed || (diffed = (prevMatched[i] !== c))
    })
    if (!activated.length) {
      return next()
    }
  })
})

我们再来看第二种实现来论证这个观点:

// 2. 通过全局的mixin,来注册生命周期钩子函数
Vue.mixin({
 // beforeMount只会在组件初始化时调用
 // 在`/detail/1`,`/list/1`这种路由发生变化时,是不会调用的
 // 也就论证了上面的说法成立
 beforeMount () {
   const { asyncData } = this.$options
   if (asyncData) {
     // 将获取数据操作分配给 promise
     // 以便在组件中,我们可以在数据准备就绪后
     // 通过运行 `this.dataPromise.then(...)` 来执行其他任务
     this.dataPromise = asyncData({
       store: this.$store,
       route: this.$route
     })
   }
 }
})

在实际的项目中,规避double-fetch的做法有很多,客户端数据预取的实现也有很多,通常我们是在created钩子中去做这个事情,例如:

{
  created() {
    if (this.list === null) {
      // 当list为null时,才去取数据
      this.fetchData()
    }
  }
}

我想,这跟官方的数据的预取是一个道理吧

V8内存控制

本文为《深入浅出node》第5章笔记

本文主要为垃圾回收算法

V8内存限制

V8是javascript解析引擎,所以Node的性能也与之息息相关。

内存限制:

  • 64位系统下约为1.4G
  • 32位系统约为0.7G
    也直接影响node直接操作大内存对象

我们可以在启动node时传递参数来增大这些限制

// 增加老生带内存
$ node --max-old-space-size=1700 test.js // 单位为MB
// 增加新生代内存
$ node --max-new-space-size=1024 test.js // 单位为KB

内存限制的原因:

  • 表层:主要用于浏览器端解析Javascript,不太可能遇到使用大内存场景
  • 深层:垃圾回收机制限制
    因为执行垃圾回收时,是需要暂停javascript解析的,言外之意就是用户需要等待,非增量式的回收1.5G的垃圾,需要等待1S以上。所以直接限制堆内存的大小是最好的选择

回收算法

1.疑问:

此书5.1.3小节第3自然段说到

当我们在代码中声明变量并赋值时,说使用对象的内存就分配在堆中

也就是说在js中所用到的基本数据类型或引用数据类型解析时都是基于对象的?

然后结合“红宝书”4.2小节中:

执行环境定义了变量或函数有权访问的其它数据,每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。
全局执行环境被认为是window对象

于是明白javascript中使用变量时的内存都是指的,而不是。栈我们能联想到的场景是函数及异步调用操作都是存放在栈里。

2.V8的垃圾回收策略主要基于分代式垃圾回收。

概念:按对象的存活时间将内存的垃圾回收进行不同的分代,分为:

  • 新生代
    为存活时间短的对象
  • 老生代
    存活时间较长或常驻内存的对象

内存绝大部分为老生带内存(略大于2/3),我们可以使用process.memoryUsage()查看Node进程内存的使用情况

$ node
> process.memoryUsage()
{ rss: 22032384,  // resident set size的缩写,即进程的常驻内存部分
  heapTotal: 7684096,  // 堆中总共申请的内存量
  heapUsed: 4934752,  // 堆中目前使用的内存量
  external: 9359 }

3. Scavenge算法

用于处理新生代内存中的对象,基于Cheney算法,即采用复制方式实现的。

Cheney特点:

  • 只能使用堆内存的一半(典型的牺牲空间换取时间)

原理:
image

Mark-Sweep 和 Mark-Compact

对于老生代内存中的对象,存活对象占比大,再采用Scavenge有2个问题:

  • 复制存活对象效率低
  • 浪费一半空间

因此在老生代中主要采取Mark-Sweep 和 Mark-Compact结合

Mark-Sweep是标记清除的意思,即标记存活的对象,清除释放非存活。

由于Mark-Sweep清除死亡对象后很有内存空间可能出现不连续的状态,这种内存碎片会对后续的内存分配造成问题,于是,需要Mark-Compact来处理碎片内存。

Mark-Compact的作用是,在Mark-Sweep将对象被标记为死亡后,在整理的过程中,将存活对象移动到一起,移动完成后清除掉边界外的内存。

Incremental Marking

在处理垃圾回收时,解析会暂停,而老生代中,存活对象多,,全栈垃圾回收的标记清理与移动整理造成的停顿比较大。
增量式标记,用来降低全栈垃圾回收带来的停顿时间,将一口气标记的做法分为增量式的。

小结

垃圾回收是影响性能的因素之一,提高性能,让垃圾回收尽量少的进行,尤其是全栈垃圾回收。

扩展阅读

[es6] - Map数据结构

在Set和Map中,NaN被用作键时,视为是相等的。
普通的Object对象“键值对”中,键名只能是字符串,而Map可以是“值对值”。

Map构造函数可以接受数组,Set或Map实例为参数

// 1 数组必须是二维的,表示键值对的关系
var a = new Map([[1, 2], [3, 4]])
// Map(2) {1 => 2, 3 => 4}

// 2 Set
var b = new Map(new Set([[1, 2], [3, 4]]))
// Map(2) {1 => 2, 3 => 4}

// 3 Map
var c = new Map(b)
// Map(2) {1 => 2, 3 => 4}

Map的属性和方法:

  • size属性返回 Map 结构的成员总数
  • set(key, value)
    set方法设置键名key对应的键值为value,然后返回整个 Map 结构。如果key已经有值,则键值会被更新,否则就新生成该键。
    set方法返回的是当前的Map对象,因此可以采用链式写法。

同Set的add

  • get(key)
  • has(key)
  • delete(key)
  • clear()

Map 结构原生提供三个遍历器生成函数和一个遍历方法。

const map = new Map([
  ['F', 'no'],
  ['T',  'yes'],
]);

for (let key of map.keys()) {
  console.log(key);
}
// "F"
// "T"

for (let value of map.values()) {
  console.log(value);
}
// "no"
// "yes"

for (let item of map.entries()) {
  console.log(item[0], item[1]);
}
// "F" "no"
// "T" "yes"

// 或者
for (let [key, value] of map.entries()) {
  console.log(key, value);
}
// "F" "no"
// "T" "yes"

// 等同于使用map.entries()
for (let [key, value] of map) {
  console.log(key, value);
}
// "F" "no"
// "T" "yes"

表示 Map 结构的默认遍历器接口(Symbol.iterator属性),就是entries方法。

map[Symbol.iterator] === map.entries
// true

Map 的作用

  1. Map 结构转为数组结构
  2. 结合数组的map方法、filter方法,可以实现 Map 的遍历和过滤(Map 本身没有map和filter方法)。
  3. Map 还有一个forEach方法,与数组的forEach方法类似,也可以实现遍历。forEach方法还可以接受第二个参数,用来绑定this。

http学习(五):http首部字段

图解http第6章

  • HTTP/1.1
请求发出 首部字段名 说明
Cache-Control 控制缓存行为
Date 创建报文时间
Pragma 报文指令,1.1之前的指令,此处作向下兼容用,与Cache-Control作用一致
Transfer-Encoding 报文主体的传输编码方式
Via 代理服务器的相关信息
Accept 用户代理可处理的媒体类型
Accept-Charset 优先的字符集
Authorization web认证信息
Host 请求资源所在的服务器
If-Match 比较实体标记
If-modified-Since 比较资源的更新时间
Proxy-Authorization 代理服务器要求的认证信息
Range 实体的直接范围请求
Referer 对请求中URI的原始获取方
User-Agent 客户端的信息
实体首部字段名 说明
Allow 资源可支持的http方法
Content-MD5 实体主体的报文摘要
Content-Type 实体主体的媒体类型
Expires 实体主体的过期时间
Last-Modified 资源最后的修改时间
  • 非HTTP/1.1首部字段

Cookie、Set-Cookie

  • 端到端首部
    必须保存在由缓存生成的响应中,且一定会被转发

  • 逐跳首部字段
    只对单次转发有效,会因通过缓存或代理而不再转发

字段名 说明
Connection 逐跳首部连接管理
Keep-Alive 是否长链接

字段详解

  • Cache-Control 参数用逗号分隔
// 1. 缓存请求指令

Cache-Control: no-cache, no-store, max-age=100, min-fresh=100, only-if-cached, no-transform
// no-cache:不缓存过期资源,每次请求都强制向服务器再次验证一遍,即使是缓存的资源
// no-cache需要服务端在返回资源时指定,资源才不会被缓存,否则拿到的都是缓存资源
// 也就是说这个指令一般用在资源响应时才有效

// no-store:不缓存请求或响应的任何内容,真正的不缓存。
// max-age:缓存最大的时间,单位秒,在改时间内都直接拿缓存资源,不请求服务器。
// 当max-age:0时,则每次都将请求服务器

// min-fresh:期望在指定时间内响应仍有效
// only-if-cached:从缓存资源获取
// no-transform:代理不可更改媒体类型

// 2. 缓存响应指令 
// 在前端开发中,一般都是作为缓存请求方
  • Connection
    • 控制不再转发的代理的首部字段
    • 管理持久连接
// 意思是Connection可指定哪些字段,代理服务器不转发。
Connection:Content-Type
// 代理都不会转发Content-Type字段

我们可以看到,在浏览器请求中
d7cf162d-55f0-48c8-8b74-a97cb9a1f54c
服务器明确指出断开长连接

Connection: close
  • q指定权重,默认q=1.0
Accept: text/plain;q=0.3, text/text/html=0.4

private

   时间已是下午1点半,本应睡觉的点可我却了无睡意了。长时间的不去练习文法,说话变得生涩,干硬了起来,就像现在的自己,只顾着拼命的奔向理想中的远方,却使得曾经亲密的朋友变的不再熟悉。  

   想到了那些曾经帮助过我的人们,我生命中真是不可缺失的人们,伴随着我的成长。我怕哪天忘了他们,我怕再也记不起那段时光里他们陪我成长,陪我欢笑,陪我悲伤,陪我一起唱歌,一起网吧通宵的日子。有些事情过去了就是过去了,再也回不来,他们将会成为我一辈子的遗憾!  

   初中开始就有,他是maj,从每个星期一起骑自行车回家并且经常去他家玩吃饭一直到高中每个月一起坐车回家,然后就是第一年高考后就变得越来越远。  

   高中的时候有一个人,他是wuz,那时候的条件是八人寝室,而我跟他的认识就是一个铺开始的,他很多习惯都是好的,自从认识我后习惯都变得跟我一样越来越差了,我们一起读完三年高中,第一年高考完后他便去了**当兵,那时候他跟我说2年后抓住机会要考上军校。记得每次都是他在厕所偷偷的用手机打电话给我,我从来没打过。我上大学后渐渐的与他断了联系。  

   高中的时候还有一个人,她是xutt,应该说是从高三到复读的2年,她给了我很多支持与交流,她第一年高考后直接上了大学,似乎什么大学我都记不清了。那时候我喜欢写文字,每一篇她都会看,还会评论,第二年高考至我上大学一年级后,我便渐渐与她断了联系。  

   大学了,准确的说是大一,大二那会,他就是我的助理班主任,wanj,那时候有可能他跟别人也很好,但我经常跟他交流,问他问题,很多事情他也很愿意帮我,可以算是大学里到认知社会的一个启蒙人物。到后面他出去实习,我也有了女朋友,那时候我们经常玩游戏,我苦恼每次我们一帮子人聊天扯白时他经常拿我举例子说我菜,对他的言语感到不满,还删了他的游戏id,渐渐的断了联系。后来我才明白,我现在也是没事做就喜欢说我女票丑,一无是处,但是又很喜欢,我才明白,越是熟悉的人才会说这样的话。  

   如今,我身边依然有这样一个人,他是luozh,他做事条条稳稳,而我偏于急躁,虽然性格不是很合拍,兴趣与追求也有所不一致,但是很多时候我依旧对我自己说,每一个这样的时刻,要想起曾经错过的他们,生命中不可再有类似的遗憾了。  

   我很感激上苍,在我生命中的每个阶段,都赐予我一个这样的人物来为我的人生护航。以此,致敬!  


                                                                                       2017年2月8日 杨明

[es6] - Iterator遍历器

1. Iterator(遍历器)的概念

iterator是一种统一的接口机制,用来处理4种数据结构:Array,Object,Set,Map。

任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。

其作用:

  • 为各种数据结构,提供一个统一的、简便的访问接口;
  • 使得数据结构的成员能够按某种次序排列;
  • ES6 创造了一种新的遍历命令for...of循环,Iterator 接口主要供for...of消费。

2. 默认 Iterator 接口

"只要部署 Iterator 接口"的意思是

一个数据结构只要具有Symbol.iterator属性,就可以认为是“可遍历的”(iterable)。Symbol.iterator属性本身是一个函数,就是当前数据结构默认的遍历器生成函数。执行这个函数,就会返回一个遍历器。至于属性名Symbol.iterator,它是一个表达式,返回Symbol对象的iterator属性,这是一个预定义好的、类型为 Symbol 的特殊值,所以要放在方括号内(参见 Symbol 一章)。

原生具有Symbol.iterator属性的数据结构有

  • Array
  • Map
  • Set
  • String
  • TypedArray
  • 函数的 arguments 对象
  • NodeList 对象

不具备的是Object对象,我们可以手动给实例加上Symbol.iterator属性

var likeArray = {
   0: 1,
   1: 2,
   length: 2,
   [Symbol.iterator]: Array.prototype[Symbol.iterator]
}

for (let item of likeArray) {
  console.log(item)  // 1, 2
}

[...likeArray]  // 1, 2

likeArray可以称之为类似数组对象,具有iterator接口,所有可以遍历。非类似数组的对象虽然也可以遍历,但是并不能取到值了

var notLikeArray = {
   a: 1,
   b: 2,
   length: 2,
   [Symbol.iterator]: Array.prototype[Symbol.iterator]
}

for (let item of notLikeArray) {
  console.log(item)  // undefined, undefined
}

[...notLikeArray] // undefined, undefined

3. Iterator 接口使用场合

  • 解构赋值

对数组和 Set 结构进行解构赋值时,会默认调用Symbol.iterator方法。

let set = new Set().add('a').add('b').add('c')

let [x,y] = set
// x='a'; y='b'

let [first, ...rest] = set
// first='a'; rest=['b','c']
  • 扩展运算符
  • yield*
  • 其他场合
    由于数组的遍历会调用遍历器接口,所以任何接受数组作为参数的场合,其实都调用了遍历器接口。下面是一些例子。
- for...of
- Array.from()
- Map(), Set(), WeakMap(), WeakSet()(比如new Map([['a',1],['b',2]]))
- Promise.all()
- Promise.race()

http学习(三): http报文信息

基本信息

  • 报文是由报文首部和报文主体组成,其中报文主体是非必需的,它们之间的区分是由(CR+LF作换行符)来划分。

  • 请求/响应报文结构
    dffe1ca6-f973-4297-a8f1-7886bf8e63b3

  • 请求首部又包含:通用首部、(请求首部/响应首部)、实体首部、其他如cookie等

  • 通常报文主体等于实体主体,只有在传输中进行编码操作时,实体主体的内容发生变化,才导致他和报文主体产生差异。

将实体主体进行编码以提升传输速率

编码的操作都是由计算机来完成,消耗更多的cpu
解码都是由接收的客户端完成

  1. 内容编码

内容编码的几种方式:

  • gzip
  • compress unix系统的标准压缩
  • deflate (zlib)
  • identity (不进行编码)
  1. 分块编码传输

在传输大容量数据时,把数据分割成多块,以便浏览器能逐步显示页面

多种数据的多部分对象集合(multipart)

发送的一份报文可能包含多类型的实体,例如上传图片或文件。包含的对象如下:

  • multipart/form-data form表单类型
  • multipart/byteranges 标识是多重范围,状态码206为 响应报文包含多个范围内容时使用

fa639106-3669-49eb-8e0e-372795116171

图中内容详解:

  • Content-type 在报文中使用多部分对象集合时,需要在首部字段里加上它
  • boundary 用来划分各类实体
    • 例如 --AaB03--来标记开始,用--AaB03--标识结束

获取部分内容的请求范围

对于下载大文件,下载中断时,解决重新下载的问题,就必须要指定下载的实体范围,从而恢复下载。

如首部为

Content-type: multipart/byteranges
Content-Range: bytes -3000, 5000-7000, 8000-

就表示,从开头到3000字节,5000-7000字节,8000到结束范围内需要下载

vue ssr 性能研究

查了2篇文章,印象比较深刻

实测 Vue SSR 的渲染性能:避开 20 倍耗时
Vue SSR 从入门到 Case Study

  • 前者说的是虚拟dom的渲染过程,即template => vNode => html

步骤1:模板字符串通过正则解析成virtual-dom对象
步骤2:生成with绑定上下文对象的render函数(该函数被缓存)
步骤3:_h ----> _createElement 执行render函数返回 包含了业务数据 的vnode对象
步骤4:遍历dom对象属性 拼接字符串 HTML渲染完成

耗时的是步骤3和4

  • 后者说的是node模块vm的原因

https://smallpath.me/

vue ssr memery leak

在服务端,一个数组类型的变量最容易造成泄漏,泄漏的根本原因是堆“用完了”,而有时候我们可以利用事件循环机制来避免这个问题,因为在执行下一个事件循环前,会清空函数或变量。

BFC模型浅识

1.基本概念

BFC为Block Formatting Context的简写,简称为“块级格式化上下文”,BFC为浏览器渲染某一区域的机制,CSS2.1 中只有BFC和IFC, CSS3中还增加了GFC和FFC。

2.产生条件

  • float的值不为none。
  • overflow的值为auto,scroll或hidden。

    block boxes with ‘overflow’ other than ‘visible’ (except when that value has been propagated to the viewport)
    意思是:overflow的值为auto,scroll或hidden,且这个属性没有传播到视口,何为传播到视口?详见3.2

  • display的值为table-cell, table-caption, inline-block中的任何一个。
  • position的值不为relative和static。

一点总结:display:table-cell; 是一个比较好用的属性(ie8+),跟inline-block具有相同的效果。但是我们知道,当元素inline-block后,相邻元素之间会有空隙(准确的说法详见3.3),而table-cell不会。
另外table-cell的宽度永远不会超过父元素的宽度。

3.关联产生的疑问?

1.垂直margin折叠

因为在bfc的介绍中(戳我查看),有这样一段描述,从而产生了这样的疑问,描述如下:

In a block formatting context, boxes are laid out one after the other, vertically, beginning at the top of a containing block. The vertical distance between two sibling boxes is determined by the ‘margin’ properties. Vertical margins between adjacent block-level boxes in a block formatting context collapse.

意思是:在bfc中,盒子从包含块的顶端开始垂直地一个接一个地排列,两个盒子之间的垂直的间隙是由他们的margin 值所决定的。两个相邻的块级盒子的垂直外边距会发生叠加。

那么我们在没有bfc的情况下垂直外边距也是会折叠的,这是为何?什么情况下会产生?bfc里的边距合并和普通的边距合并有什么关系?

查了下资料,有赞前端的一篇解释的很好,深入理解CSS外边距折叠

2.overflow扩散到视口的解释

我们知道bfc模型里的布局不会影响到父元素以外的边距和浮动,当在body上设置overflow:hidden后,并没有达到这种效果。戳我查看问题描述demo

英文描述是:

user agents must instead apply the 'overflow' property from the first such child element to the viewport, if the value on the root element is 'visible'. The 'visible' value when used for the viewport must be interpreted as 'auto'. The element from which the value is propagated must have a used value for 'overflow' of 'visible'.

意思是用户不要在第一个body元素上设置overflow,而要设置在viewport上,也就是视口容器root element html元素。

言外之意就是,当使用具有扩散propagate属性的时候,需要设置在viewport元素上,也就是说以html元素上的属性为准,propagate属性有

3.inline-block间隙的产生原因

<div>
  <div style="display:inline-block;"></div>
  <div style="display:inline-block;"></div>
</div>

英文单词之间会有空格,而中文没有,当将元素设为inline-block后,具有inline的属性,所有的空格、换行或回车都会被视为一个空格占位字符,于是就产生了。

解决办法

svg入门(一)

1.svg文件引入

  • 可通过<object data="image.svg" type="image/svg+xml" /><iframe src="image.svg"></iframe>引入使用
  • 理论上同样可以使用 img 元素,但是在低于4.0版本的Firefox 中不起作用

2.svg坐标系统

  • 用户单位和屏幕单位的映射关系被称为用户坐标系统
  • 默认1用户像素 === 设备上的1像素(但是设备上可能会自己定义1像素到底是多大)

3.svg形状

  • react
  • circle,ellipse
  • line,polyline
  • polygon
  • path

4.svg形状之path

<path d="M20,230 Q40,205 50,230 T90,230" fill="none" stroke="blue" stroke-width="5"/>

属性d的值是一个“命令+参数”的序列,每一个命令都有两种表示方式:

  • 用大写字母,表示采用绝对定位 M10 10
  • 用小写字母,表示采用相对定位,例如:从上一个点开始,向上移动10px,向左移动7px m 10 10
    例如画一个矩形:
大写:
<path d="M10 10 H 90 V 90 H 10 Z" fill="transparent" stroke="black"/>

小写:
<path d="M10 10 h 80 v 80 h -80 Z" fill="transparent" stroke="black"/>

因为属性d采用的是用户坐标系统,所以不需标明单位。

命令 表达方式(默认第一种写法) 描述
M M10 10 移动画笔位置,但是不画线,从一个点到另一个
L L10 10/平行线 H10垂直线 V10 绘制线条
Z Z不区分大小写 Z命令会从当前点画一条直线到路径的起点
贝塞尔曲线-在path元素里,只存在两种贝塞尔曲线:三次贝塞尔曲线C,和二次贝塞尔曲线Q
C C 20 20, 40 20, 50 10 最后一个坐标(x,y)表示的是曲线的终点,另外两个坐标是控制点,(x1,y1)是起点的控制点,(x2,y2)是终点的控制点
S S x2 y2, x y S命令可以用来创建与之前那些曲线一样的贝塞尔曲线,但是,如果S命令跟在一个C命令或者另一个S命令的后面,它的第一个控制点,就会被假设成前一个控制点的对称点。如果S命令单独使用,那么它的两个控制点就会被假设为同一个点。
Q Q x1 y1 只有一个控制点,用来确定起点和终点的曲线斜率
T T x y T命令前面是一个Q命令,或者是另一个T命令,T会通过前一个控制点推断与之定义的终点之间的控制点
弧形,基本上,弧形可以视为圆形或椭圆形的一部分
A

5.填充和边框

  • stroke
  • stroke-opacity
  • stroke-linecap: 'butt','square','round' 描边方式
  • stroke-width
  • stroke-linejoin: 'miter','round','bevel' 2条线段之间的描边方式
  • stroke-dasharray 是一组用逗号分割的数字组成的数列
  • stroke-dashoffset,定义虚线开始的位置
  • fill
  • fill-opacity
  • fill-rule,用于定义如何给图形重叠的区域上色

利用style设置svg css

表示定义,这里面可以定义一些不会在SVG图形中出现、但是可以被其他元素使用的元素。

<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg" version="1.1">
  <defs>
    <style type="text/css"><![CDATA[
       #MyRect {
         stroke: black;
         fill: red;
       }
    ]]></style>
  </defs>
  <rect x="10" height="180" y="10" width="180" id="MyRect"/>
</svg>

6.渐变

<svg width="120" height="240" version="1.1" xmlns="http://www.w3.org/2000/svg">
  <defs>
      <linearGradient id="Gradient1">
        <stop class="stop1" offset="0%"/>
        <stop class="stop2" offset="50%"/>
        <stop class="stop3" offset="100%"/>
      </linearGradient>
      <linearGradient id="Gradient2" x1="0" x2="0" y1="0" y2="1">
        <stop offset="0%" stop-color="red"/>
        <stop offset="50%" stop-color="black" stop-opacity="0"/>
        <stop offset="100%" stop-color="blue"/>
      </linearGradient>
      <style type="text/css"><![CDATA[
        #rect1 { fill: url(#Gradient1); }
        .stop1 { stop-color: red; }
        .stop2 { stop-color: black; stop-opacity: 0; }
        .stop3 { stop-color: blue; }
      ]]></style>
  </defs>
 
  <rect id="rect1" x="10" y="10" rx="15" ry="15" width="100" height="100"/>
  <rect x="10" y="120" rx="15" ry="15" width="100" height="100" fill="url(#Gradient2)"/>
  
</svg>

线性渐变linearGradient,渐变的方向可以通过两个点来控制,它们分别是属性x1、x2、y1和y2,这些属性定义了渐变路线走向
径向渐变radialGradient

vue源码阅读笔记 - 疑问汇总

源码阅读:

疑问点:

  • template截取0为何要用charAt?

  • hydrating参数是啥意思?

  • native Observe、Proxy的用法

  • JSON.stringify(obj, null, 2)支持传多个参数?

  • AST抽象语法树

  • with(this)return{}

  • 我们结合vue-loader进行的预编译,那源码中在哪里判断是否预编译了
    在初始化时,根据options.render/options.template来判断是否需要compile。options.render为函数,此时已经预编译了;options.template为Dom Node,所以还需要经过compile生成render。

  • 对于el参数,没有传:需要手动$mount;传了,vue在_init的最后调用。

http学习(四):状态码的工作机制

状态码含义

  • 4xx 表明客户端的错误
  • 400 请求报文存在语法错误
  • 401 表明发送请求需有通过http认证的认证信息
  • 403 资源请求被拒绝
  • 5xx 表明服务端发生错误

Vue中你需要注意的点

1. 组件的递归调用

例如在树形结构中

// m-tree
<template>
<m-tree>
  <m-tree-node />
</m-tree>
</template>
// m-tree-node
<template>
<div>
  <p>this is tree node</p>
  <!--此处可以直接引用-->
  <m-tree-node v-if="hasChild" />
</div>
</template>

<script>
export default {
  name: 'MTreeNode'
}
</script>

参考Element Tree组件

2. 必要的时候使用jsx

例如在Element UI的Message中,message支持传递vNode,此时的vNode需要手动h

const h = this.$createElement
const message = h('div', { class: 'test' }, [])
// ...

这样写会比较麻烦且阅读不清晰,此时转换成jsx就不一样了。

参考 babel-plugin-transform-vue-jsx

3. 组件/实例的选项应该有统一的顺序

推荐顺序

  1. 全局感知
    • name
  2. 组件类型
    • functional
  3. 模板依赖
    • components
    • directives
    • filters
  4. 组合
    • extends
    • mixins
  5. 接口
  6. 本地状态
    • data
    • computed
  7. 事件
    • watch
    • 生命周期钩子 (按照它们被调用的顺序)
  8. 非响应式的属性
    • methods
  9. 渲染

参考 组件/实例的选项的顺序

4. 元素属性的顺序

<div
  :is=""
  v-for=""
  v-if=""
  v-show=""
  ref=""
  key=""
  v-model=""
  v-on=""
  v-html=""
/>

参考 元素特性顺序

5. 组件/实例选项中的空行

// good
props: {
  value: {
    type: String,
    required: true
  },

  focused: {
    type: Boolean,
    default: false
  },

  label: String,
  icon: String
},

computed: {
  formattedValue: function () {
    // ...
  },

  inputClasses: function () {
    // ...
  }
}

6. 组件名应该始终是多个单词的,根组件 App 除外。

7. 多个特性的元素换行

但不能为了换行而换行,超过2个或以上时,建议换行

<!--bad-->
<div
  v-if="isShowDiv"
/>

<!--good-->
<div v-if="isShowDiv" />

<!--good-->
<div 
  v-if="isShowDiv"
  ref="myDiv"
  @handle-click="handleClick"
/>

这个规则类似于js换行(超过3个或以上时建议换行)

// bad
import {a, b, c} from Test

// good
import {
  a,
  b,
  c
} from Test

关于js浮点数精度问题

这是一个非常非常复杂的问题,我还并不知道具体原因。
我所了解到的:

  • js中只有number类型,不存在整型和浮点型的概念,所有的值都是以浮点型来存放的
  • 丢失精度在表示这个数值的时候就已经丢失。例如0.1 + 0.2,并不是计算后导致精度丢失,而是在表示0.1、0.2、0.3等的时候精度就已经丢失了,小数中只有0.00.5能精确表示,其他的数在转换成二进制的时候就已经丢失了。
  • 表示0.1实际上是一个无限循环的浮点数来表示。

解决办法:

    toFixed (num, s) {
        var times = Math.pow(10, s)
        var des = num * times + 0.5
        des = parseInt(des, 10) / times
        return des.toFixed(s)
    }

参考:
代码之谜(五)- 浮点数(谁偷了你的精度?)
JS数字精度丢失的一些典型问题
js 小数运算问题

vue-cli v3.0.1总览

vue-cli v3.0.1总览

3.0大改版就相当于重新定义了玩法规则,这个规则可能是当下流行的,你得学习一遍适应这个规则,才能"拥抱未来"。

说到这儿,就想到了我们作为公民,也是被当作一个个粒度的个体在一定的规则下去被玩,这被叫做"韭菜"。

扯远了,让我们进入主题

1.先从vue -h查看开始,提供了

  • create 3.0的方式创建项目
  • add 添加cli插件
  • invoke 调用cli插件
  • inspect 检查webpack配置
  • serve 单文件起服务
  • build 单文件打包
  • ui 图形化界面,依赖@vue/cli-ui
  • init 2.0的方式创建项目
  • config 查看~/.vuerc配置文件,也就是预置的配置和presets

cli的核心由2部分组成

  • cli

    本身提供的基础功能,例如create,add,invoke,inspect,config,ui。
    ui是依赖@vue/cli-ui的,我们查看lib/ui.js发现本质是利用require('@vue/cli-ui/server')起服务

  • cli-service-global

    serve和build是基于@vue/cli-service-global的,由名称我们也可以想到它是基于@vue/cli-service的,
    官方用词是"快速原型开发",也就是你提供一个Component.vue文件就可以直接起本地服务或打包,global在这里帮你提供了template,index.htmlmain.js

    @vue/cli-service是本地的一个包,处理配置插件,打包起服务都是靠它,还有项目初始化的结构模版也在里面。

2.插件

从上面我们基本了解了一遍他本身核心的部分:模版,脚手架。它还有一个重要的组成部分即插件。

初始化项目时会让你选配置项,便会生成~/.vuercvue config就是列出~/.vuerc中的全部内容,你选择的配置项也就是插件,这种将插件保存起来的部分称为preset

官方帮你写好了几个常用插件

  • cli-plugin-eslint
  • cli-plugin-babel

vue add @foo/bar就是用来添加插件的,满足在项目后期有添加插件的场景

3.polyfill

说完插件还有polyfill,同样的,官方为你写好了babel-preset-app,我们可以看到在官方的推荐中,是这么写的:

presets: [
  '@vue/app'
]

这和上面插件的添加写法一致,它被解析为组名/babel-preset-包名简写,插件是被解析为组名/cli-plugin-包名简写

所以这里的preset实际是加载的@vue/babel-preset-app,它默认为你做了几件事情:

  • polyfill useBuiltIns: 'usage'
  • 内置了es6.array.iteratores6.promisees7.promise.finally

他推荐我们使用浏览器不支持的属性时手动引入core-js,有个疑问是core-js中是es.promise,在他这里为es6.promise,不知哪里做的转换。

优化最大的一个做法是,在打包的时候加上--modern属性,它会打包2份代码,一份支持es module的现代浏览器,一份就是我们现在引用polyfill的代码。他们会同时被index.html引入,通过type = modulenomodule区分,因为浏览器支持这2个属性,还支持modulepreload

4.环境变量和配置

通过vue.config.js来代替之前的config/index.js

5.与2.x的差异

  1. 在倒入类似ElementUi时,babel.config.js里插件plugins选项不支持多个了,请戳babel-plugin-import

Options can't be an array in babel@7+, but you can add plugins with name to support multiple dependencies.

  1. 开启eslint需要在vue.config.js里配置lintOnSave: true

  2. 推荐显式地引入需要的 polyfill,具体请查看core-js modules,配置在preset

presets: [
    [
      '@vue/app', {
        polyfills: ['es6.string.includes']
      }
    ]
  ]

Vue nextTick的实现

nextTick的实现本质是采用了js Event Loop的知识实现。

其使用场景在赋值state后使用,目的是等待watcher触发更新界面后调用回调。本文旨在理解Event Loop的基础上查看vue nextTick的实现,以便于在使用过程中更好的解决问题。

nextTick 伪代码

function nextTick(cb, context) {
  // 处理cb
  callbacks.push(() => {
     if (cb) {
       // 回调模式
       cb.call(context)
     } else {
       // 不传cb时,为promise形式
       _resolve()
     }
  })

  // 保证只有一次调用
  if (!pending) {
    pending = true
    // 当前事件队列结束后, 触发回调数组
    // 此处涉及事件队列知识,请戳https://github.com/Jmingzi/blog/issues/2
    // 至于为何要用microtasks和tasks,注释说明
    // 在vue2.4以下都只用microtasks,但是后来发现在连续的事件或同一事件的冒泡中,会有问题
    // 因为microtasks的优先级始终是最高的。默认使用microtasks
    // 何时用tasks?
    // 当组件内部的变化导致state变化时,就会使用tasks
    if (useMacroTask) {
      macroTimerFunc()
    } else {
      microTimerFunc()
    }
  }

  // 如果没传cb,则返回一个promise对象
  if (!cb) {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

然后再就是定义macroTimerFunc和microTimerFunc时所涉及的点

  • 定义macroTimerFunc
    在ie中支持setImmediate,非ie中,都是使用MessageChanel发送消息来保持callback始终以队列的形式调用的。除非二者不支持,最后才用的setTimeout保底。
if (setImmediate) {
  setImmediate(flushCallbacks)
} else if (MessageChanel) {
  // 关于MessageChanel,请戳
  // http://www.zhangxinxu.com/study/201202/web-messing-channel-messaging-two-iframe.html
  const channel = new MessageChannel()
  const port = channel.port2
  channel.port1.onmessage = flushCallbacks
  macroTimerFunc = () => {
    port.postMessage(1)
  }
} else {
  setTimeout(() => {
    flushCallbacks()
  }, 0)
}
  • 定义microTimerFunc
    用Promise模拟的,但是在iOS UIWebViews中有个bug,Promise.then并不会被触发,除非浏览器中有其他事件触发,例如处理setTimeout。所以手动加了个空的setTimeout

如果Promise都不支持,那microTimerFunc = macroTimerFunc

  const p = Promise.resolve()
  microTimerFunc = () => {
    p.then(flushCallbacks)
    // in problematic UIWebViews, Promise.then doesn't completely break, but
    // it can get stuck in a weird state where callbacks are pushed into the
    // microtask queue but the queue isn't being flushed, until the browser
    // needs to do some other work, e.g. handle a timer. Therefore we can
    // "force" the microtask queue to be flushed by adding an empty timer.
    if (isIOS) setTimeout(noop)
  }

疑问

  • 关于vue实例中使用microTask与task的区别(也就是注释中的问题描述)
  • 为何要使用MessageChanel来模拟task,而不是直接使用setTimeout
    w3c html规范中定义了setTimeout的默认最小时间为4ms,而嵌套的timeout表现为10ms,也就是说即使你赋值0,而实际却不是。再由于,setTimeout的时间,会受到任务队列的影响(或其它原因?)其实际时间远大于10ms,这或许就是vue不优先使用setTimeout的原因吧。
    此处参考
  • 何时用task,何时用microTask
    vue内置了一个函数withMacroTask,当组件的state变化时,就会使用task,其它默认都是microTask

推荐阅读

Tasks, microtasks, queues and schedules

最后,注释中的2个问题#4521, #6690,欢迎一起讨论。

Call Stack与执行上下文、变量对象、作用域链与闭包

JavaScript代码的整个执行过程,分为两个阶段,代码编译阶段与代码执行阶段。编译阶段由编译器完成,将代码翻译成可执行代码,这个阶段作用域规则会确定。执行阶段由引擎完成,主要任务是执行可执行代码,执行上下文在这个阶段创建。

以下概念:

  • 调用栈 === Call Stack
  • EC === Excution Context === 执行上下文
  • VO === Variable Object === 变量对象
  • AO === Active Object === 活动对象
  • ScopeChain === 作用域链
  • Closure === 闭包

1.Call Stack

#30 中有说到在js变量内存分配中,都是使用的堆,那什么时候使用栈呢?最常用的场景就是调用栈,即我们所熟知的call stack

js代码执行都是按照调用栈的规则后进先出去执行,程序初始化时,执行全局上下文,然后遇到其它函数,则push到栈中,如果当前没有其它调用栈,则执行最顶部的上下文。
image

2.Excution Context

我们知道多个EC就组成了Call Stack,而执行上下文由2部分组成

EC = {
  VO: { 
    // ...  变量对象
  },
  scopeChain: [ VO(innerTest), ..., VO(Global) ] // 作用域链
}

执行上下文的生命周期,当函数被调用激活时,就开始它的生命周期
image

3.Variable Object

变量对象为上下文创建时创建,在上下文执行时,变量对象转化为活动对象Active Object

// 创建阶段 变量对象
VO = {
  arguments: {},
  foo: <foo reference>,  // 表示foo的地址引用
  a: undefined // 其它local变量声明
}

// 执行阶段 活动对象
AO = {
    arguments: {...},
    foo: <foo reference>,
    a: 1,
    this: Window
}

变量对象的创建,依次经历了以下几个过程。

  • 建立arguments对象。检查当前上下文中的参数,建立该对象下的属性与属性值。
  • 检查当前上下文的函数声明,也就是使用function关键字声明的函数。在变量对象中以函数名建立一个属性,属性值为指向该函数所在内存地址的引用。如果函数名的属性已经存在,那么该属性将会被新的引用所覆盖。
  • 检查当前上下文中的变量声明,每找到一个变量声明,就在变量对象中以变量名建立一个属性,属性值为undefined。如果该变量名的属性已经存在,为了防止同名的函数被修改为undefined,则会直接跳过,原属性值不会被修改。

image

也就是说,先进行函数的声明,再进行变量的声明赋值。当进行变量的声明赋值时,如果该变量名已经被函数声明过,则跳过该声明不会覆盖。

console.log(foo)
function foo() {}
var foo = 1
// 结果是function

全局上下文的变量对象

// 以浏览器中为例,全局对象为window
// 全局上下文
windowEC = {
    VO: Window,  // 变量对象,就是window对象
    scopeChain: {},  
    this: Window  // this也是指向window
}

4.Scope Chain 与 Closure

作用域链,是由当前环境与上层环境的一系列变量对象组成,它保证了当前执行环境对符合访问权限的变量和函数的有序访问。

// 例子
var a = 20;
function test() {
    var b = a + 10;
    function innerTest() {
        var c = 10;
        return b + c;
    }
    return innerTest();
}
test();

// 执行上下文 innerTest
innerTestEC = {
    VO: {...},  // 变量对象
    scopeChain: [VO(innerTest), VO(test), VO(global)], // 作用域链
}

我们知道,闭包的存在会将变量存在内存中,而不会被释放,从而垃圾回收始终将它是为存活对象而得不到回收。
那闭包中保存的是这个变量还是当前执行上下文的变量对象呢?答案是变量对象,但该对象中并不是所有的变量,只包含引用到的变量。

  // 来个例子
  var fn = null;
  function foo() {
    var a = 2;
    var b = 3;
    function innnerFoo() {
      var c = 1
      console.log(innnerFoo.prototype)
      console.log(a, c);
    }
    fn = innnerFoo; // 将 innnerFoo的引用,赋值给全局变量中的fn
  }

  function bar() {
    fn(); // 此处的保留的innerFoo的引用
  }

  foo();
  bar(); // 2

image

上面的例子断点可知,闭包为foo而不是innnerFoo,可以看到innnerFoo中的执行上下文中包含了Scope Chain和活动对象,而Scope Chain中包含了foo的变量对象中被引用而未释放的变量。

Closure (foo) = {
  a: 2,
  innnerFoo: ƒ innnerFoo()
}

即如下图所示
image

5.疑点解析

  • 作用域与作用域链是2个完全不同的概念,为不同时期的“产物”
    • 由上,我们知道,在js代码的编译阶段,作用域就已经被确定了。在js中,一般来讲,作用域分为全局和函数。也就是说,我们写代码的时候,就已经确定了作用域。
    • 而作用域链,是在执行上下文EC创建时需要确定的东西。

本文为阅读以下文章的总结

对event loop事件循环的理解

对于这个概念,理解过多次才有点认知,本文理解尚浅,可查看后续文章加深

以前似乎并不知道有这个概念,(很害羞的说出这句话~),直到16年倒数第二天逛掘金时遇到这题:

setTimeout(function(){
	console.log(4)
}, 300)	
setTimeout(function(){
	console.log(3)
}, 200)	
for (var i=0; i<1000000; i++) {
	console.log(1)
}
setTimeout(function(){
	console.log(2)
}, 100)	

看到这题的时候,凭借“习惯”的想到,for循环的时间会不会很长,超过100ms,那这答案不确定了。后面再想到js是单线程从上到下执行,那很自然想到结果是1 2 4 3

后面又看到一题,直接懵逼

(function test() {
    setTimeout(function() {console.log(4)}, 0);
    new Promise(function executor(resolve) {
        console.log(1);
        for( var i=0 ; i<10000 ; i++ ) {
            i == 9999 && resolve();
        }
        console.log(2);
    }).then(function() {
        console.log(5);
    });
    console.log(3);
})()

看了题就发现其实知识还是无穷无尽的。好吧,废话说了这么多,进入正题~(以下内容仅属个人看法,有误之处希望批评指正)

基本概念

js 只有一个main thread 主进程和call-stack(一个调用堆栈),所以在对一个调用堆栈中的task处理的时候,其他的都要等着。task等待被加入调用堆栈等待执行的时候,被放在task queue事件队列之中。
每当主线程完成一个任务的时候,他就会去调用堆栈中获取task执行。

而task分为:

  • macrotask:也称为task,包含了script(整体代码),setTimeout, setInterval, setImmediate, I/O, UI rendering
  • microtask:process.nextTick, Promises, Object.observe, MutationObserver

二者的关系:

JavaScript引擎首先从macrotask queue中取出第一个任务,执行完毕后,将microtask queue中的所有任务取出,按顺序全部执行;
然后再从macrotask queue中取下一个,执行完毕后,再次将microtask queue中的全部取出;
循环往复,直到两个queue中的任务都取完。

【参考】Macrotask Queue和Microtask Quque

题目解答

有了这些概念以后再来看上面第一题。

  • 1 setTimeout 入栈,300ms后打印4
  • 2 setTimeout 入栈,200ms后打印3
  • 3 for循环,打印1
  • 4 setTimout 要等待for循环完成 再入栈,100ms后打印2

所以结果是1 3 4 2,关于2的打印顺序与for循环的时间有关,我们这里的for时间大于400ms,所以2才最后打印。

第二题是一个立即执行函数:

  • 1 setTimeout入栈
  • 2 声明函数executor,打印1,new Promise(executor)
  • 3 for循环
  • 4 等待for循环执行完毕,打印2,然后打印3
  • 5 for执行完后,调用Promise.resolve()
  • 6 根据microtask 和 task机制,所以先打印5,最后打印4

所以结果是1 2 3 5 4

【参考】从Promise来看JavaScript中的Event Loop、Tasks和Microtasks

最后再来一道题加深理解:

console.log('start')

const interval = setInterval(() => {  
  console.log('setInterval')
}, 0)

setTimeout(() => {  
  console.log('setTimeout 1')
  Promise.resolve()
      .then(() => {
        console.log('promise 3')
      })
      .then(() => {
        console.log('promise 4')
      })
      .then(() => {
        setTimeout(() => {
          console.log('setTimeout 2')
          Promise.resolve()
              .then(() => {
                console.log('promise 5')
              })
              .then(() => {
                console.log('promise 6')
              })
              .then(() => {
                clearInterval(interval)
              })
        }, 0)
      })
}, 0)

Promise.resolve()
    .then(() => {  
        console.log('promise 1')
    })
    .then(() => {
        console.log('promise 2')
    })

题解:
我们将macrotask 和 microtask 看作是2个队列,不断的清空入栈执行。

  1. setInterval和setTimeout 1被加入到task,promise 1和promise 2被加入到microtask
  2. 清空microtask,打印promise 1和promise 2,执行task队列,打印setInterval和setTimeout 1
  3. setInterval被加入到task,promise 3和promise 4被加入到microtask,setTimeout被加入到task
  4. 清空microtask,打印 promise 3和promise 4,执行task队列,打印setInterval,setTimeout 2
  5. setInterval被加入到task,promise 5和promise 6被加入到microtask
  6. 清空microtask,打印 promise 5和promise 6,clearInterval

所以打印顺序是:start, promise 1, promise 2, setInterval, setTimeout 1, promise 3, promise 4, setInterval, setTimeout 2, promise 5, promise 6

在一个事件循环的周期(cycle)中一个 (macro)task 应该从 macrotask 队列开始执行。当这个 macrotask 结束后,所有的 microtasks 将在同一个 cycle 中执行。在 microtasks 执行时还可以加入更多的 microtask,然后一个一个的执行,直到 microtask 队列清空。

【参考】
理解事件循环一(浅析)
理解事件循环二(macrotask和microtask)
Javascript 单线程

[es6] - Set数据结构

其结构都以Iterator 对象知识为基础

Set 实例的属性和方法

  • Set.prototype.constructor:构造函数,默认就是Set函数。
  • Set.prototype.size:返回Set实例的成员总数。

操作方法

  • add(value):添加某个值,返回 Set 结构本身。
  • delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
  • has(value):返回一个布尔值,表示该值是否为Set的成员。
  • clear():清除所有成员,没有返回值。

遍历操作

  • keys():返回键名的遍历器
  • values():返回键值的遍历器
  • entries():返回键值对的遍历器
  • forEach():使用回调函数遍历每个成员

可以解决的问题

  • 数组去重

Set 本身是一个构造函数,用来生成 Set 数据结构。Set 函数可以接受一个数组(或者具有 iterable 接口的其他数据结构)作为参数,用来初始化。

image

从chrome的控制台我们可以看到实例自身的属性和方法。

// method 1
let set = new Set([1, 2, 3, 3, 3, 4])
console.log(Array.from(set)) // 1, 2, 3, 4

// method 2
[...new Set([1, 2, 3, 3, 3, 4])] // 1, 2, 3, 4

为什么Array.from可以接受set实例作为参数?我们来看看它的解释:

Array.from方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)。

tips: 所谓类似数组的对象,本质特征只有一点,即必须有length属性。因此,任何有length属性的对象,都可以通过Array.from方法转为数组,而此时扩展运算符(展开运算符)就无法转换。因为扩展运算符背后调用的是遍历器接口(Symbol.iterator),如果一个对象没有部署这个接口,就无法转换。对于还没有部署该方法的浏览器,可以用Array.prototype.slice方法替代。

image

  • 实现数组的并集(Union)、交集(Intersect)和差集(Difference)

let a = new Set([1, 2, 3])
let b = new Set([4, 3, 2])

// 并集
let union = new Set([...a, ...b])
// Set {1, 2, 3, 4}

// 交集
let intersect = new Set([...a].filter(x => b.has(x)))
// set {2, 3}

// 差集
let difference = new Set([...a].filter(x => !b.has(x)))
// Set {1}

js常用公用方法集合

const getSearch = ()=> {
    let search = location.search.replace(/\?/g, ''),
        searchObj = {}, key = ''
    let splitArr = []
    if (search) {
        let paramArr = search.split("&")
        for (let i = 0; i < paramArr.length; i++) {
            splitArr = paramArr[i].split("=")
            key = splitArr[0]
            searchObj[key] = splitArr[1]
        }
        return searchObj
    } else return ""
}

export default {
   search: getSearch(),
   getCookie(name) {
    let cookieValue = null
    if (document.cookie && document.cookie !== '') {
        let cookies = document.cookie.split(';')
        for (let i = 0; i < cookies.length; i++) {
            let cookie = cookies[i].trim()
            if (cookie.substring(0, name.length + 1) === (name + '=')) {
                cookieValue = decodeURIComponent(cookie.substring(name.length + 1))
                break
            }
        }
    }
    // console.log(name, cookieValue)
    return cookieValue
},
/**
     * Get the first item that pass the test
     * by second argument function
     *
     * @param {Array} list
     * @param {Function} f
     * @return {*}
     */
    find (list, f) {
        return list.filter(f)[0]
    },
    /**
     * Deep copy the given object considering circular structure.
     * This function caches all nested objects and its copies.
     * If it detects circular structure, use cached copy to avoid infinite loop.
     *
     * @param {*} obj
     * @param {Array<Object>} cache
     * @return {*}
     */
    deepCopy(obj, cache = []) {
        // just return if obj is immutable value
        if (obj === null || typeof obj !== 'object') {
            return obj
        }

        // if obj is hit, it is in circular structure
        const hit = find(cache, c => c.original === obj)
        if (hit) {
            return hit.copy
        }

        const copy = Array.isArray(obj) ? [] : {}
        // put the copy into cache at first
        // because we want to refer it in recursive deepCopy
        cache.push({
            original: obj,
            copy
        })

        Object.keys(obj).forEach(key => {
            copy[key] = this.deepCopy(obj[key], cache)
        })

        return copy
    }
}

正向代理和反向代理

正向代理

  • 含义

代理的是客户端请求,发出请求从代理->服务端,服务端认识代理,不认识客户端。

一般由客户端设置nginx作为正向代理。

  • 场景
    • 翻墙
    • 可以做缓存,加速访问资源

305504-20161112124853014-1532060796

反向代理

  • 含义

代理的是服务端响应,发出请求从客户端->代理,客户端只认识代理,不认识服务端。

客户端不需要进行任何特别的设置,客户端向反向代理的命名空间(name-space)中的内容发送普通请求,接着反向代理将判断向何处(原始服务器)转交请求,并将获得的内容返回给客户端,就像这些内容原本就是它自己的一样。

  • 场景
    • 代理作为公网访问地址,内网在公网访问,提供WAF功能,阻止web攻击

305504-20161112124341280-1435223816

  • 负载均衡,通过反向代理服务器来优化网站的负载
    305504-20161112124423530-566240666

  • 反向代理 负载均衡 nginx配置示例

http {  
#   省略了前面一般的配置,直接从负载均衡这里开始  
#   设置地址池,后端3台服务器  
    upstream http_server_pool {  
        server 192.168.1.2:8080 weight=2 max_fails=2 fail_timeout=30s;  
        server 192.168.1.3:8080 weight=3 max_fails=2 fail_timeout=30s;  
        server 192.168.1.4:8080 weight=4 max_fails=2 fail_timeout=30s;  
    }  
#   一个虚拟主机,用来反向代理http_server_pool这组服务器  
    server {  
        listen       80;  
#       外网访问的域名          
        server_name  www.test.com;   
        location / {  
#           后端服务器返回500 503 404错误,自动请求转发到upstream池中另一台服务器  
            proxy_next_upstream error timeout invalid_header http_500 http_503 http_404;  
            proxy_pass http://http_server_pool;  
            proxy_set_header Host www.test.com;  
            proxy_set_header X-Real-IP $remote_addr;  
            proxy_set_header X-Forwarded-For  $proxy_add_x_forwarded_for;  
        }  
        access_log  logs/www.test.com.access.log  combined;  
    }  
}  

参考

  • 正向代理与反向代理【总结】###
  • [浅谈什么是正向代理和反向代理,如何使用nginx搭建正向代理和反向代理]###

[es6] - 变量的解构赋值

解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象。由于undefined和null无法转为对象,所以对它们进行解构赋值,都会报错。

数组解构

只要某种数据结构具有 Iterator 接口,都可以采用数组形式的解构赋值。

如果等号的右边不是数组(或者严格地说,不是可遍历的结构,参见《Iterator》一章),那么将会报错。

// 报错
let [foo] = 1;
let [foo] = false;
let [foo] = NaN;
let [foo] = undefined;
let [foo] = null;
let [foo] = {};

上面的语句都会报错,因为等号右边的值,要么转为对象以后不具备 Iterator 接口(前五个表达式),要么本身就不具备 Iterator 接口(最后一个表达式)。

由于数组本质是特殊的对象,因此可以对数组进行对象属性的解构。

let arr = [1, 2, 3];
let {0 : first, [arr.length - 1] : last} = arr;
first // 1
last // 3

解构赋值允许指定默认值。

let [x = 1] = [undefined];
x // 1

let [x = 1] = [null];
x // null

// 必须严格等于undefined

默认值可以是变量,也可以是函数

let [x = 1, y = x] = [];     // x=1; y=1
let [x = 1, y = x] = [2];    // x=2; y=2
let [x = 1, y = x] = [1, 2]; // x=1; y=2
let [x = y, y = 1] = [];     // ReferenceError

function f() {
  console.log('aaa')
}
let [x = f()] = [1]
// 函数是不会被调用

对象的解构

最常用的解构是类似 let { log, sin, cos } = Math

对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。

let { bar, foo } = { foo: "aaa", bar: "bbb" };
foo // "aaa"
bar // "bbb"

实际上,对象的解构赋值是下面形式的简写

let { foo: foo, bar: bar } = { foo: "aaa", bar: "bbb" };

// 复杂点的例子
let obj = {
  p: [
    'Hello',
    { y: 'World' }
  ]
};

let { p: [x, { y }] } = obj;
p // 并没有被赋值
x // "Hello"
y // "World"

const node = {
  loc: {
    start: {
      line: 1,
      column: 5
    }
  }
};

let { loc, loc: { start }, loc: { start: { line }} } = node;
line // 1
loc  // Object {start: Object}
start // Object {line: 1, column: 5}

// 容易总结出,解构赋值从左到右,就如同变量从上到下赋值一样。
// 找到相同的key的变量,然后将这个变量的值的变量赋值操作。

// 最后一个例子加深印象
let obj = {};
let arr = [];

({ foo: obj.prop, bar: arr[0] } = { foo: 123, bar: true });
obj // {prop:123}
arr // [true]

字符串的解构赋值

let { length: len } = 'abc'
// len = 3

用途

  • 交换变量的值 [x, y] = [y, x]
  • 遍历 Map 结构
  // 获取键名
for (let [key] of map) {
  // ...
}

// 获取键值
for (let [,value] of map) {
  // ...
}  

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.