ruirui's blog

ruirui's 备忘录


  • Home

  • Archives

你不知道的Javascript(下卷)笔记

Posted on 2019-05-23

深入编程

Javascript 是解释型的,实际上是动态编译程序,然后立即执行编译后的代码。
编写代码是为了给开发者阅读的,代码应该解释为什么,而非是什么。

深入 Javascript

Javascript 中”假”值的列表:

  • “” (空字符串)
  • 0, -0, NaN (无效数字)
  • null、undefined
  • false

任何不在”假”值列表中的值都是”真”值,例如[],{}等。
在Jvascript中,闭包最常见的应用是模块模式。模块模式允许你定义外部不可见的私有实现细节(变量、函数),同时也可以提供允许从外部访问的公开API。

ES6及更新版本

transpiling:通过transpiling(transformation+compiling,转换+编译)的技术,将ES6代码转化为等价(或近似)的可以在ES5环境下工作的代码。
let+for

1
2
3
4
5
6
7
var funcs = [];
for(let i = 0; i < 5; i++) {
funcs.push(function() {
console.log(i);
})
}
funcs[3](); // 3

for循环头部的let i 不只是为for循环本身声明了一个i,而是为循环的每一次迭代都重新声明了一个新的i。这意味着loop迭代内部创建的闭包封闭的是每次迭代中的变量。

spread/rest

ES6引入的新的运算符…,通常称为spread/rest运算符,取决于在哪/如何被使用。

1
2
3
4
// spread
var a = [2,3,4];
var b = [1, ...a, 5];
console.log(b); // [1,2,3,4,5]

1
2
3
4
5
6
7
8
9
10
// rest
function foo(x, y, ...z) {
console.log(x, y, z)
}
foo(1,2,3,4,5) // 1 2 [3, 4, 5]
// 如果没有明明参数的话,会收集所有的参数
function foo(...args) {
console.log(args);
}
foo(1,2,3,4,5); // [1,2,3,4,5]

这种用法最好的一点是,为类数组 arguments 提供了非常可靠的替代形式。

箭头函数

箭头函数总是函数表达式,并不存在箭头函数声明,箭头函数是匿名函数表达式,它们没有用于递归或者事件绑定/解绑定的命名引用。
箭头函数的主要设计目的是以特定的方式改变this的行为特性。

1
2
3
4
5
6
7
8
9
var controller = {
makeRequest: function() {
var self = this;
btn.addEventListner('click', function(){
//
self.makeRequest(...);
}, false)
}
}

因为 this 是动态的,通过变量 self 依赖于词法作用域的可预测性。在箭头函数内部,this 绑定不是动态的,是词法的。这是箭头函数的主要设计特性。
除了词法 this,箭头函数还有词法 arguments,它们没有自己的 arguments 数组,而是继承自父层–词法 super 和 new.target 也是一样。

代码组织

模块

  • ES6 使用基于文件的模块,也就是一个文件一个模块。
  • ES6 模块的 API 是静态的。也就是说,需要在模块的公开 API 中静态定义所有最高层导出,之后无法补充。
  • ES6 模块是单例。
  • 模块的公开 API 暴露的属性和方法并不仅仅是普通的值或引用的赋值。他们是到内部模块定义中的标识符的实际绑定(几乎类似于指针)。
  • 导入模块和静态请求加载(如果还没加载的话)这个模块是一样的。

import 和 export 都必须出现在使用它们的最顶层作用域。举例来说,不能把 import 或 export 放在 if 条件中,它必须出现在所有代码块和函数的外面。
比较下面两段代码:

1
2
3
4
function foo() {
// ...
}
export default foo;

1
2
3
4
function foo() {
// ...
}
export { foo as default };

第一段代码中,导出的是此刻到函数表达式的绑定,而不是标识符foo,也就是说,export default… 接受的是一个表达式,如果之后 foo 赋了一个不同的值,模块导入得到的仍然是原来导出的函数,而不是新的值。
第二段代码,默认导出绑定实际上绑定到 foo 标识符而不是它的值,所以如果之后修改了 foo 的值,导入的值也会更新。

模块依赖环

import 语句使用外部环境(浏览器、Node.js等)提供的独立机制,来实际把模块标识符字符串解析成可用的指令,用于寻找和加载所需的模块,这个机制就是系统模块加载器。浏览器中环境提供的模块加载器会把模块标识符解析为URL,在Node.js这种服务器上就解析为本地文件系统路径。

类

// TODO

Docker 常用命令整理

Posted on 2019-05-22

image

列出所有镜像

1
docker images

删除所有 none 镜像

1
2
docker rmi $(docker images | grep "none" | awk '{print $3}')
docker images | grep none | awk '{print $3 }' | xargs docker rmi

生成镜像

1
docker image build -t {{containerName:version}} .

container

列出容器

1
2
docker ps 
docker ps -a

查看所有正在运行容器状态

1
docker stats

运行容器

1
docker run [-d] --name {{containerName}} [--restart=always] -p 8080:8080 {{imageName}}

启动容器

1
docker start {{containerName}}

重启容器

1
docker restart {{containerName}}

停止容器

1
docker stop {{containerName}}

停止所有容器

1
docker stop $(docker ps -aq)

删除所有已停止的容器

1
docker rm $(docker ps -aq)

强制停止并删除容器

1
docker rm -f {{containerName or containerID}}

进入交互式容器

1
docker exec -it {{containerName or containerID}} bash

查看容器日志

1
docker logs -f {{containerName}}

你不知道的Javascript(中卷)笔记

Posted on 2019-05-14

类型

typeof 判断类型,返回的是类型的字符串值
特殊:

1
2
3
4
5
typeof null === 'object'
typeof function a() {} === 'function'
var a
typeof a // 'undefined'
typeof b // 'undefined' // 即使b未声明,也会返回undefined

函数是对象的一个子集,也有属性,比方说a.length,是声明的参数个数。
变量没有类型,但他们的值持有类型。

值

如果字符串键值能被强制类型转换为十进制数字的话,它就会被当做数字索引来处理。

1
2
3
var a = [];
a['13'] = 42;
a,length; //14

字符串是类数组,具有 length 属性以及 indexof() 和 concat() 方法。
字符串是不可变的,也就是字符串的成员函数不会改变其原始值,数组是可变的。有的操作可以先”借用”数组的方法。
undefined 只有一个值,即 undefined,null 类型也只有一个值,即 Null。它们的名称既是类型也是值。

1
2
null:指空值;曾经赋过值,但目前没有值;是一个特殊的关键字,不是标识符
undefined:指没有值,从未赋值;是一个标识符,可以被当做变量来使用和赋值

NaN 指”不是一个数字”,理解为”无效数值”、”失败数值”、”坏数值”。他和自身不相等,是唯一一个非自反的值,即 NaN != NaN 为true。
怎么判断一个值是 NaN 呢? ES6 有工具函数Number.isNaN()可以判断。

原生函数

所有 typeof 返回值为”object” 的对象都包含一个内部属性[[Class]],这个属性无法直接访问。一般通过Object.prototype.toString(...)来查看。
基本类型是没有.length 和 .toString() 这样的属性和方法,需要通过封装类型才能访问,此时 Javascript 会自动为基本类型值包装一个封装对象。

1
2
3
4
var a = new Boolean(false);
if(!a) {
console.log('Oops'); // 执行不到这里
}

获取一个封装对象中的基本类型值,可以使用 valueOf() 函数。

强制类型转换

抽象值操作

  • ToString
    负责处理非字符串到字符串的强制类型转换
    基本类型规则:null 转换为 “null”,undefined 转换为 “undefined”,true 转换为 “true”。
    普通对象来说,除非自定义,否则 toString() 会返回内部属性[[Class]]的值,如”[[object Object]]”。
    数组的 toString() 经过了重新定义,将所有单元字符串化后再用”,”进行连接。
  • ToNumber
    true 转换为1,false 转换为0,null 转换为0,undefined 转换为NaN。
    对象(包括数组)会首先被转换为相应的基本类型值,如果返回的是非数字的基本类型值,再通过规则将其转换为数字。
    将值转换为基本数据类型,ES规范会检查是否有 valueOf() 方法,如果有并且返回基本类型值,则使用该值进行强制类型转换,如果没有就使用toString()的返回值进行强制类型转换。
  • ToBoolean
    以下这些是假值:undefined、null、false、+0、-0 和 NaN、””。假值类型的布尔强制类型转换结果为 false。
    假值对象:他们都是封装了假值类型的对象。
    1
    2
    3
    var a = new Boolean(false); // true
    var b = new Number(0); // true
    var c = new String(""); // true

显式强制类型转换

  • 字符串和数字的显示转换

字符串和数字之间的转换是通过 String(…) 和 Number(…) 这两个内建函数(原生构造函数)来实现的,请注意它们前面没有 new 关键字,并不创建封装对象。
日期转换为数字

隐式强制类型转换

  • 字符串和数字的隐士强制类型转换

    • 运算符能用于数字加法,也能用于字符串拼接,那么怎么判断执行哪个操作呢?
      1
      2
      3
      4
      5
      6
      7
      8
      1 + '1' // 11
      1 + true // 2
      1 + false // 1
      1 + undefined // NaN
      'a' + true // atrue
      var a = [1, 2]
      var b = [3, 4]
      a+b // "1,23,4"

    如果 + 的其中一个操作数是字符串(或者通过以下步骤可以得到字符串)则执行字符串拼接,否则执行数字加法。
    步骤:如果其中一个操作数是对象(包括数组),对其首先进行 ToPrimitive 抽象操作,该抽象操作再调用[[DefaultValue]],以数字作为上下文。

  • 布尔值和数字的隐士强制类型转换

宽松相等和严格相等

常见误区是:“==检查值是否相等,===检查值和类型是否相等”。
正确解释是:“==允许在相等比较中进行强制类型转换,而===不允许”。

  • 字符串和数字之间的相等比较
    x == y:x 是数字,y 是字符串,则将 字符串y 转换为数字(ToNumber(y))进行判断。
  • 其他类型和布尔值的相等比较
    x == y: x 是布尔值,则将布尔值 x 转换为数字(ToNumber(x))进行判断。
    1
    2
    3
    var x = true;
    var y = "42";
    x == y // false

首先将 x 转换为数字1,则接下来进行 1 == “42”的比较,”42”转换为数字42,最后变成 1== 42,则为 false。
建议:无论什么情况下都不要使用 == true 和 == false

  • null 和 undefined 的相等比较
    x == y:x 为 null, y为 undefined,则结果为 true
    1
    2
    3
    4
    var a = dosomething();
    if(a == null) {
    // ...
    }

条件判断仅在 dosomething() 返回 null 和 undefined 时才成立,除此之外都不成立,包括0、 false 和 “” 这样的假值。

  • 对象和非对象直接的相等比较
    x == y: x 是字符串或数字,y 是对象,则将 y 转换为 ToPrimitive(y) 进行比较。

  • 安全运用隐士强制类型转换
    如果两边值中有 true 或 false,千万不要使用 ==。
    如果两边值有[]、””或0,千万不要使用==。

从源码角度理解 Vue 生命周期和响应式原理

Posted on 2019-05-03
1
2
3
4
5
6
var vm = new Vue({
el: '#app',
data: {
test: 1
}
})

beforeCreate && created

在 new Vue() 创建实例时,会先执行 _init 方法进行初始化.

1
2
3
4
5
6
7
8
9
// src/core/instance/index.js
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// src/core/instance/init.js
Vue.prototype._init = function (options?: Object) {
const vm: Component = this

// ...

// merge options
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')

// ...

if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}

上面列出了 Vue 初始化时做的工作,在 beforeCreate 之前,主要是合并配置,初始化生命周期,初始化事件中心,初始化渲染。beforeCreate之后主要是 initState,initState 主要是初始化 props、data、methods、watch、computed,methods 等这些属性,在初始化完之后调用 create 钩子。所以在 beforeCreate 钩子函数被调用时,与上述属性相关的内容都不能访问,created 之后才可以访问。

beforeMounted && mounted

在 created 之后,开始执行挂载 $mount。因为 $mount 和平台、构建方式都有关,Vue 原型上定义 $mount 主要有两处,一处是 Runtime 版(运行版) Vue 的入口文件。

1
2
3
4
5
6
7
8
9
// src/platforms/web/runtime/index.js
// public mount method
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}

通过 mountComponent 函数完成真正的挂载工作。
另一处是 Runtime + Compiler 版(完整版)的入口文件。

1
2
3
4
5
6
7
8
9
// src/platforms/web/entry-runtime-with-compiler.js
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
// ...
return mount.call(this, el, hydrating)
}

这两个版本的区别如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 运行时+编译器
// 需要编译器
new Vue({
template: '<div>{{ hi }}</div>'
})

// 运行时版本
// 不需要编译器
new Vue({
render (h) {
return h('div', this.hi)
}
})

完整版比运行时版多了编译器的处理,我们平时写项目时,vue-loader 会帮我们把 template 转化成 render 函数,采用运行时构建即刻。
将省略的编译器的部分展开分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
  el = el && query(el)
/* istanbul ignore if */
if (el === document.body || el === document.documentElement) {
process.env.NODE_ENV !== 'production' && warn(
`Do not mount Vue to <html> or <body> - mount to normal elements instead.`
)
return this
}

const options = this.$options
// resolve template/el and convert to render function
if (!options.render) {
let template = options.template
// 处理 template
if (template) {
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
template = idToTemplate(template)
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !template) {
warn(
`Template element not found or is empty: ${options.template}`,
this
)
}
}
} else if (template.nodeType) {
template = template.innerHTML
} else {
if (process.env.NODE_ENV !== 'production') {
warn('invalid template option:' + template, this)
}
return this
}
} else if (el) {
template = getOuterHTML(el)
}
if (template) {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile')
}

const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns

/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile end')
measure(`vue ${this._name} compile`, 'compile', 'compile end')
}
}

首先挂载点不能是 body 或 html 元素,接着看是否有渲染函数 render,如果有则直接调用运行时版的 $mount,如果没有则先将 template 或 el 处理成字符串,再通过调用 compileToFunctions 成渲染函数 render。接着去执行真正的挂载函数 mountComponent。

mountComponent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// src/core/instance/lifecycle.js
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
// ...
}
callHook(vm, 'beforeMount')

let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = () => {
// ...
}
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}

new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)

// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}

render 函数必须存在,否则会报警告,如果 render 存在首先调用 beforeMount 钩子,接着根据不同的环境定义 updateComponent 方法

updateComponent

1
2
3
updateComponent = () => {
vm._update(vm._render(), hydrating)
}

updateComponent 方法中 vm._render 方法生成虚拟 Node,最终调用 vm._update 将虚拟 Node 渲染成真正的 DOM,_update 在首次渲染和数据更新时都会调用,核心方法是 patch ,将新虚拟节点和旧虚拟节点进行对比。
那什么时候调用 updateComponent 方法呢? 实例化一个 Watcher,将 updateComponent 作为参数传递进去,
在之前实现简单的MVVM中分析,初始化 Watcher 时,会通过传入的表达式(观察目标)进行一次求值,从而触发属性的 getter 属性进行依赖收集。对应到这里看源码对 Watcher 定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// src/core/observer/watcher.js
export default class Watcher {
// ...
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
// ...
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = function () {}
process.env.NODE_ENV !== 'production' && warn(
`Failed watching path: "${expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
this.value = this.lazy
? undefined
: this.get() // 注意这里

}
get () {
// ...
}

// ...
}

实例化 Watcher时,第一个参数是当前组件实例对象,第二个参数 expOrFn 是 updateComponent函数,第三个参数是回调 noop 空函数,但不代表数据更新时什么都不做,因为数据更新时,还会再去获取新的值(也就是对观察目标求值),同时触发回调。获取新值时也不会再去进行依赖收集,因为 Dep.target 已经设置为 null。 第四个参数 isRenderWatcher 为 true,表示是渲染 Watcher。初始化时,因为 updateComponent 是函数,将其赋值给 getter 属性,接着去执行 Watcher 的 get() 方法,并将 get 得到的 value 值赋值给该观察者实例对象的 value 属性,也就是说 this.value 里存的是观察目标的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
// ...
} finally {
// ...
}
return value
}

和之前分析的类似,执行 get() 方法时,Dep.target 指向当前的渲染 Watcher,使用 pushTarget 方法先将此 Watcher 添加进 Dep 的观察者对象列表中(也就是进行依赖收集),执行 value = this.getter.call(vm, vm) this.getter()对应的就是 updateComponent 方法,vm._update(vm._render(), hydrating) 渲染函数 render 在生成虚拟 DOM 的过程中会访问 vm 中的数据,从而触发 Observer 观察者中属性的 getter 方法,进行依赖收集。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export default class Dep {
// ...
}

Dep.target = null
const targetStack = []

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

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

每个属性都有一个对应的 dep 对象,在触发属性的 getter 时会调用 dep.depend() 方法,也就会执行 Dep.target.addDep(this),

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// src/core/observer/dep.js
class Dep {
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
}

addDep 将观察者加入到该属性对应的 Dep 实例的 subs 数组中,用来收集观察者并避免添加重复依赖。

总结挂载过程

  • 根据平台不同,将 template 转化为 render 函数
  • 触发 beforeMount 钩子
  • 实例化渲染 Watcher,执行 vm._render 函数生成虚拟 DOM,执行 vm._update 将虚拟 DOM 转化为真实 DOM,在这过程中进行了依赖收集
  • 触发 mounted 钩子, 组件挂载成功

beforeUpdate && update

还是看这段代码

1
2
3
4
5
6
7
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)

初始化挂载的时候,因为 _isMounted 为 false,所以不会触发 beforeUpdate。当挂载成功,某个响应式属性更新时,会调用对应属性 dep 实例的 notify 方法,通知所有的订阅者更新调用 update 方法。

1
2
3
4
5
6
7
8
9
10
11
// src/core/observer/dep.js
class Dep {
// ...
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}

update 做了什么呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/core/observer/watcher.js
class Watcher {
// ...
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
}

如果观察者是异步更新,调用 queueWatcher 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// src/core/observer/scheduler.js
const queue: Array<Watcher> = []
let has: { [key: number]: ?true } = {}
let waiting = false
let flushing = false
/**
* Push a watcher into the watcher queue.
* Jobs with duplicate IDs will be skipped unless it's
* pushed when the queue is being flushed.
*/
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// queue the flush
if (!waiting) {
waiting = true

if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
nextTick(flushSchedulerQueue)
}
}
}

当 queueWatcher 函数被调用时,会将该观察者放入队列 queue 中,并维护一个映射关系避免重复添加观察者,在 nextTick 中执行 flushSchedulerQueue。flushing 是一个标志,当观察者加入时队列正在更新时,放到队尾等待更新。wating 也是一个标志位,避免同时调用 nextTick。关于 nextTick 解析 查看之前的这篇Vue.nextTick 源码解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function flushSchedulerQueue () {
// ...
// 获取到 updatedQueue
callUpdatedHooks(updatedQueue)
}

function callUpdatedHooks (queue) {
let i = queue.length
while (i--) {
const watcher = queue[i]
const vm = watcher.vm
if (vm._watcher === watcher && vm._isMounted) {
callHook(vm, 'updated')
}
}
}

flushSchedulerQueue 函数的作用之一是将观察者统一执行更新,在这个过程中还会通过this.get()一次观察目标的值,触发 watcher 回调进行渲染。当获取到已经更新的 watcher 之后,调用 callUpdatedHooks 函数,对数组遍历,当 watcher 是当前 watcher 而且已经挂载成功,执行 updated 钩子表示更新完成。

总结更新过程

  • 调用 beforeUpdate 钩子
  • 触发属性 setter
  • 调用 dep.notify 方法
  • 通知 watcher 调用 update()
  • queueWatcher(nextTick(flushSchedulerQueue))
  • updated钩子

beforeDestroy & destroyed

组件销毁阶段,先调用 beforeDestroy 钩子,接着执行一系列销毁动作,包括删掉 watcher,vnode等,最后执行 destroyed

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
Vue.prototype.$destroy = function () {
const vm: Component = this
if (vm._isBeingDestroyed) {
return
}
callHook(vm, 'beforeDestroy')
vm._isBeingDestroyed = true
// remove self from parent
const parent = vm.$parent
if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
remove(parent.$children, vm)
}
// teardown watchers
if (vm._watcher) {
vm._watcher.teardown()
}
let i = vm._watchers.length
while (i--) {
vm._watchers[i].teardown()
}
// remove reference from data ob
// frozen object may not have observer.
if (vm._data.__ob__) {
vm._data.__ob__.vmCount--
}
// call the last hook...
vm._isDestroyed = true
// invoke destroy hooks on current rendered tree
vm.__patch__(vm._vnode, null)
// fire destroyed hook
callHook(vm, 'destroyed')
// turn off all instance listeners.
vm.$off()
// remove __vue__ reference
if (vm.$el) {
vm.$el.__vue__ = null
}
// release circular reference (#6759)
if (vm.$vnode) {
vm.$vnode.parent = null
}
}
}

彻底搞懂前端各种模块化规范

Posted on 2019-03-13

Javascript 中有两种源文件,一种叫做脚本,一种叫做模块。脚本是可以由浏览器或 Node 环境引入执行的,而模块只能由 Javascript 代码用 import 引入。
模块中包含:import声明,export声明、语句。脚本中包含的是语句。所以脚本和模块的区别仅仅在于是否包含 import 和 export。浏览器中的脚本通过script 标签引入,如果要引入模块,需要给 script 标签加上 type="module"。

1
<script type ="module" src=""></script>

下面来区分各种模块规范。本文代码示例在此

CommonJS

主要用于服务端,同步加载模块,只有加载完成,才能执行后面的操作。
CommonJS 定义的模块分为: 模块标识(module)、模块定义(exports) 、模块引用(require)

暴露模块

CommonJS 暴露模块有两个,exports 和 module.exports

1
2
module.exports = value
exports.a = value

那这两个有什么区别呢?

module.exports 和 exports 都是引用类型,require 引入的内容是 module.exports 指向的内存块内容,require 是看不到 exports 对象的,exports 只是 module.exports 的引用,如果改变了 exports 的引用(也就是指向了一个新的内存地址),require 的还是 module.exports 的内容,所以最好一直用 module.exports。

引入模块

1
require(xxx)

第一次加载某个模块时,Node 会缓存该模块。以后再加载该模块,就直接从缓存取出该模块的 module.exports 属性。
查看 CommonJS Demo

AMD: 异步模块定义,依赖前置。

CommonJS 规范主要用于服务端,那客户端的呢,而由于 CommonJS 同步的局限性,导致浏览器不能使用 CommonJS 规范,所以 AMD 规范就出来了,用于异步加载模块。AMD 是”Asynchronous Module Definition”的缩写,意思就是”异步模块定义”,
模块的加载不影响后续执行,依赖模块加载的语句,都会放在一个回调函数里,等到该模块加载完之后回调函数才运行。
define(id?, dependencies?, factory)
第一个参数 id:字符串,表示模块的标识。
第二个参数 dependencies:数组,元素是依赖模块的id。
第三个参数 factory:回调函数,在依赖的模块加载成功后,会执行这个回调函数,参数是所有依赖模块的引用。

暴露模块

1
2
3
4
5
6
7
8
//定义没有依赖的模块
define(function(){
return xxx
})
//定义有依赖的模块
define(['module1', 'module2'], function(m1, m2){
return xxx
})

AMD 是 RequireJS 在推广过程中对模块定义的规范化产出。RequireJS 通过 require 函数实现模块的加载。

引入模块

1
require([module], callback);

查看 AMD Demo

CMD: 通用模块定义,依赖就近。

CMD 是”Common Module Definition”的缩写,意思就是”通用模块定义”,CMD 是 Sea.js 在推广过程中对模块定义的规范化产出。

暴露模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//定义没有依赖的模块
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
})

引入模块

1
2
3
4
5
6
7
// 使用模块
define(function (require) {
var m1 = require('./module1')
var m2 = require('./module2')
m1.show()
m2.show()
})

查看 CMD Demo

CMD与AMD区别

共同点:

  • 都是为了实现浏览器 JavaScript 模块化开发。
  • AMD 是 RequireJS 在推广过程中对模块定义的规范化产出。
  • CMD 是 Sea.js 在推广过程中对模块定义的规范化产出。
    区别:
  • 对于依赖的模块,AMD 是 提前执行,CMD 是延迟执行。不过从 RequireJS 2.0开始,也可以延迟执行。
  • AMD 推崇依赖前置,CMD 推崇依赖就近。

ES6模块

在ES6之前,主要的模块规范是 CommonJS 和 AMD 规范,ES6模块设计思想是尽量静态化,使得编译时就确定模块的依赖关系(静态加载),便于做静态代码分析。同时,静态加载也限制了模块加载在文件中所有语句之前,并且不能使用表达式和变量这种运行时才能得到结果的语法结构。CommonJS 和 AMD 都只能在运行时确定。ES6 模块化的出现,旨在成为浏览器和服务器通用的模块解决方案,

export声明

模块导出变量的方式有两种,一种是独立使用 export 声明,一种是在声明型语句(let, const, var, function, class等)前加上 export。

1
2
3
4
// 独立使用export声明。
export {a, b, c};
// 声明型语句前加export。
export var a = 1;

export 还可以默认导出,使用 export default 表示默认导出一个变量值。使用 export default 来导出时,对应的 import 不需要大括号。

1
2
3
4
5
export default function add() { // 输出
// ...
}

import add from 'add'; // 输入

export default 输出一个 default 的变量,所以后面不能跟变量声明语句。

1
2
3
4
5
6
7
8
9
10

// 正确
export var a = 1;

// 正确
var a = 1;
export default a;

// 错误
export default var a = 1;

要注意的是,export 导出的必须是一个接口,与模块内的变量是一一对应的关系。不能直接导出变量值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 报错
export 1;

// 报错
var m = 1;
export m;

// 正确
// 写法一
export var m = 1;

// 写法二
var m = 1;
export {m};

// 写法三
var n = 1;
export {n as m};

import声明

1
2
3
import x from "./a.js"; // 导入默认值
import {a as x, modify} from './a.js'; // 引入模块中的变量
import * from "./a.js"; // 把模块中的所有变量以类似对象的方式引入

ES6模块与CommonJS模块区别

CommonJS 模块是运行时加载,ES6 模块是编译时输出接口

1
2
3
4
5
6
7
// CommonJS模块
let { stat, exists, readFile } = require('fs');
// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;

CommonJS 加载模块是加载整个对象,生成 _fs 对象,再从这三个对象上读取3个方法,因为只有运行时才能得到这个对象,所以称为“运行时加载”,而 ES6 模块

1
2
// ES6模块
import { stat, exists, readFile } from 'fs';

只会加载三个方法,别的不会加载,ES6 模块不是对象,对外接口是一种静态定义,所以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。
CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用
CommonJS 模块输出一个对象,结果被缓存,在第一次运行的时候加载一次,之后从缓存里读。JS 引擎在对脚本进行静态分析的时候,遇到模块加载命令 import,会先生成一个只读引用,等脚本真正执行的时候,才会去模块里执行。所以 ES6 模块是动态引用,不会缓存值。

记一次 addRoutes 实现动态路由的过程

Posted on 2019-03-07

权限控制是后台管理系统常见的需求,vue2 版本有一个 addRoutes 方法可以动态添加路由,记录一下用此方法踩坑的过程。

实现过程

基本思路:初始路由是不需要权限的页面例如 login 页面等,登录成功后拿到用户信息(权限列表),筛选出对应的路由,用 addRoutes 将需要的路由加进来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var constRoutes = [
{
path: '/login',
name: 'login',
component: login,
meta: {
title: '登录'
}
},
{
path: '*',
name: '404',
component: () => import('../../views/page404')
meta: {
title: '404'
}
}
]

var router = new Router({
routes: constRoutes
})

登录成功后,将用户信息放到 vuex 里存储,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const auth = {
state: {
userInfo: null,
routes: []
},
getters: {
dynamicMenu: (state) => {
// 筛选菜单
},
dynamicRoutes: (state) => {
// 筛选路由
return data;
}
},
mutations:{
SET_USERINFO: (state, userInfo) => {
state.userInfo = userInfo
}
},
actions: {
setUserInfo ({ state, dispatch }, data) {
return new Promise(resolve => {
// 持久化存储
localStorage.userInfo = data;
dispatch('SET_USERINFO', data)
resolve()
})
},

},
}
export auth;

假设根据用户权限信息匹配筛选得到的路由如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var dynamicRoutes = [
{
path: '/home',
component: () => import('../../views/home'),
children: [
{
path: '/page1',
component: () => import(/ '../../views/page1'),
title: '页面一'
},
{
path: '/page1',
component: () => import(/ '../../views/page1'),
title: '页面二'
}
]
}
]

用 addRoutes 添加到路由中:

1
2
vm.$router.addRoutes(dynamicRoutes);
vm.$router.push({path:'/page1'});

此时遇到第一个坑:当刷新页面时,会被重定向到404。因为刷新页面时 vue 会重新实例化,路由也会恢复到初始路由,匹配不到当前路由被重定向到404,所以要将404页面放到动态路由的最后。同时也需要将用户信息持久化,当刷新页面时,从持久化数据中拿到用户信息,获取到匹配到路由信息。
所以想到的处理办法是在 vuex 的 auth 里添加:

1
2
3
4
5
6
7
8
9
10
11
12
var userInfo = {};
if(localStorage.userInfo) { // 从持久化数据中取出用户信息
var userInfo = JSON.parse(localStorage.userInfo);
} else { // 不存在则跳转到登录页
router.push({name: 'login'})
}
const auth = {
state: {
userInfo: userInfo,
}
///...
}

在最外层APP.vue中添加

1
2
3
4
5
6
7
8
computed: {
dynamicRoutes() {
return this.$store.getters.dynamicRoutes
},
},
created() {
this.$router.addRoutes(this.dynamicRoutes)
}

这样在刷新时路由初始化的问题就解决了,退出登录的时候将本地的 userInfo 清空,然而还是想的太简单,第二个坑又来了:在退出登录跳转到登录页时,即使将 userInfo 清空了,但已经添加的路由还在,再次登录时会再重复添加路由信息,所以得想办法清空路由信息,而 vue-router 没有提供清空路由的方法,只能通过刷新页面来将路由初始化(因为userInfo 已经清空),此时退出登录的问题解决了。看了看网上开源的项目 vue-element-admin 使用 addRoutes 方法。

使用 addRoutes 实现动态路由的重点是:addRoutes在哪里调用?

之前的思路是在登录成功后调用一次,在系统最外层 App.vue 初始化的时候调用一次,将用户信息持久化,动态路由是根据用户信息计算得来,清空用户信息刷新页面时路由失效。vue-element-admin 实现是:在全局导航钩子里调用 addRoutes,根据用户是否登录(token)来判断,如果用户登录并且已经有用户信息(也就是拿到权限信息)则next(),避免重复添加路由。如果已经登录但没有用户信息说明是刷新了页面,此时先去获取用户信息然后调用 addRoutes。退出系统时将用户唯一标识 token 清掉并 reload 页面将路由清空。核心代码是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// permission judge function
function hasPermission(roles, permissionRoles) {
if (roles.indexOf('admin') >= 0) return true // admin permission passed directly
if (!permissionRoles) return true
return roles.some(role => permissionRoles.indexOf(role) >= 0)
}

const whiteList = ['/login', '/authredirect']// no redirect whitelist

router.beforeEach((to, from, next) => {
NProgress.start() // start progress bar
if (getToken()) { // determine if there has token
/* has token*/
if (to.path === '/login') {
next({ path: '/' })
NProgress.done() // if current page is dashboard will not trigger afterEach hook, so manually handle it
} else {
if (store.getters.roles.length === 0) { // 判断当前用户是否已拉取完user_info信息
console.log('shi sm ');
store.dispatch('GetUserInfo').then(res => { // 拉取user_info
const roles = res.data.roles // note: roles must be a array! such as: ['editor','develop']
store.dispatch('GenerateRoutes', { roles }).then(() => { // 根据roles权限生成可访问的路由表
router.addRoutes(store.getters.addRouters) // 动态添加可访问路由表
next({ ...to, replace: true }) // hack方法 确保addRoutes已完成 ,set the replace: true so the navigation will not leave a history record
})
}).catch((err) => {
store.dispatch('FedLogOut').then(() => {
Message.error(err)
next({ path: '/' })
})
})
} else {
// 没有动态改变权限的需求可直接next() 删除下方权限判断 ↓
if (hasPermission(store.getters.roles, to.meta.roles)) {
next()
} else {
next({ path: '/401', replace: true, query: { noGoBack: true }})
}
// 可删 ↑
}
}
} else {
/* has no token*/
if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,直接进入
next()
} else {
next(`/login?redirect=${to.path}`) // 否则全部重定向到登录页
NProgress.done() // if current page is login will not trigger afterEach hook, so manually handle it
}
}
})

你不知道的Javascript(上卷)笔记

Posted on 2019-02-18

词法作用域

词法作用域是由写代码时将变量和块作用域写在哪决定的。无论函数在哪里被调用,也无论何时被调用,它的词法作用域都只由函数被声明时所处的位置决定。
可以在运行时来”修改”欺骗词法作用域,Javascript 有两种机制来实现:eval()和width(),但欺骗词法作用域会导致性能下降,不建议这么做。

1
2
3
4
5
6
7
8
9
function foo() {
console.log(a); //2
}
function bar() {
var a = 3;
foo()
}
var a = 2;
bar();

Javascript 并不具有动态作用域,只有词法作用域。
主要区别就是:词法作用域是在写代码或者定义时确定,动态作用域在运行时确定(this也是),词法作用域关注声明在何处,动态作用域关注函数从何处调用。

函数作用域

函数声明和函数表达式的一个重要区别就是:它们的名称标识符将被绑定在何处。

1
2
3
4
5
6
7
8
9
// 函数声明
// foo 被绑定在所在作用域中
var a = 2;
function foo() {
var a = 3;
console.log(a)
}
foo();
console.log(a);

1
2
3
4
5
6
7
8
// 函数表达式
// foo 被绑定在函数表达式自身的函数中,不会污染外层作用域
var a = 2;
(function foo() {
var a = 3;
console.log(a)
})()
console.log(a);

(function foo() {…})是立即执行表达式,第一个()将函数变成表达式,第二个()执行了这个函数。改进的立即执行表达式:(function(){…}())。
var 声明包括变量和函数在内的所有声明都会在任何代码被执行前首先被处理。
var a = 2: 会被看成两个声明:var a 和 a = 2;第一个声明是在编译阶段进行,第二个声明会被留在原地等待执行。
函数声明会被提升,函数表达式不会。

1
2
3
4
5
foo() // 不是 ReferenceError, 而是 TypeError。
// 因为foo()被提升并分配给所在的作用域,因此foo()不会导致ReferenceError。
// 但是foo此时没有赋值,foo()由于对undefined值进行函数调用导致非法操作,所以抛出TypeError异常

var foo = function bar() {}

块作用域

Javascript 没有块作用域的概念。
es6 引入 let 关键字,可以将变量绑定在所在的任意作用域中(通常是{}内部)。let进行的声明不会在块作用域中进行提升。

1
2
console.log(bar) // ReferenceError!
let bar = 2

作用域与闭包

闭包:当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。

循环与闭包:

1
2
3
4
5
6
for(var i = 0; i <=5; i++) {
setTimeout(function(){
console.log(i)
}, 0)
}
// 6 6 6 6 6 6

要打印出0 1 2 3 4 5需要改成:

1
2
3
4
5
6
7
for(var i = 0; i <=5; i++) {
(function(j) {
setTimeout(function(){
console.log(j)
}, 0)
})(i)
}

因为IIFE(立即执行函数)会通过声明并立即执行一个函数来创建作用域。
在迭代内部使用IIFE会为每个迭代内部生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值的变量供我们访问。

关于 this

第一种误解:this 指向函数本身。
第二种误解:this 指向函数的作用域。
正确的是:this 是在运行时绑定的,并不是在编写时绑定,它的上下文只取决于函数调用时的各种条件,它的指向完全取决于函数在哪里被调用。

绑定规则

  1. 默认绑定:独立函数调用
    • 非严格模式:this 绑定到全局
    • 严格模式:this 被绑定到 undefined
  2. 隐式绑定:
    函数引用有上下文对象时,隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象。
    有个隐式丢失的问题:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function foo() {
    console.log(this.a)
    }
    var obj = {
    a: 2,
    foo: foo
    }
    var bar = obj.foo; // 函数别名!
    var a = "oops, global"
    bar(); // oops, global

    bar 是 obj.foo 的引用,但实际上引用的是 foo 函数本身,所以是默认绑定

  3. 显示绑定
    call(…) 和apply(…) 方法

  4. new 绑定
    new 调用函数时,会自动执行以下操作:

    • 创建(或者说构造)一个全新的对象。
    • 这个新对象会被执行[[prototype]]连接。
    • 这个新对象会绑定到函数调用的 this。
    • 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象。

判断 this

  1. 由 new 调用?绑定到新创建的对象。
  2. 由 call 或者 apply(或者)bind 调用? 绑定到指定对象。
  3. 由上下文对象调用? 绑定到那个上下文对象。
  4. 默认:严格模式下绑定到 undefined,否则绑定到全局对象。

绑定例外

this 词法
箭头函数不使用 this 的四种标准规则,而是根据外层(函数或全局)作用域来决定 this。
理解箭头函数的词法作用域,箭头函数的绑定无法被修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function foo() {
return (a)=>{
// this 继承自 foo(),取决于调用时foo() 的 this。
console.log(this.a)
}
}
var obj1 = {
a: 2
}
var obj2 = {
a: 3
}
var bar = foo.call(obj1);
bar.call(obj2); // 2

函数体内的 this 对象,就是定义时所在的对象,而不是使用时所在的对象。

1
2
3
4
5
6
7
8
9
10
11
function foo() {
setTimeout(() => {
// 箭头函数的定义生效是在foo函数生成时。如果是普通函数,这是21
console.log('id:', this.id);
}, 100);
}

var id = 21;

foo.call({ id: 42 });
// id: 42

对象

内置对象:
String、Number、Boolean、Object、Function、Array、Date、RegExp、Error

1
2
3
4
5
6
7
8
var strPrimitive = "I am a string";
typeof strPrimitive; // "string"
strPrimitive instanceof String; // false

var strObject = new String("I am a string");
typeof strObject; // "object"
strObject instanceof String; // true
Object.prototype.toString.call(strObject); // [object String]

Object.prototype.toString是子类型在内部借用了 Object 的 toString() 方法。

1
2
3
var strPrimitive = "I am a string";
strPrimitive.length // 13
strPrimitive.charAt(3) // "m"

strPrimitive 是一个字面量,能调用 .length 等方法,是因为引擎自动将字面量转化为 String 对象。

在对象中,属性名永远是字符串,如果用非 string 以外的值作为属性名,会首先把它转换为一个字符串。
可以给数组添加属性

1
2
3
4
var myArray = ["foo", 42, "bar"];
myArray.baz = "baz";
myArray.length //3
myArray.baz // baz

虽然添加了属性,但是长度 length 并没有发生变化。

1
2
3
4
var myArray = ["foo", 42, "bar"];
myArray["3"] = "baz";
myArray.length //4
myArray[3] // baz

当属性名”看起来”像是一个数字,就会变成一个数值下标。

混合对象”类”

原型

[[prototype]]

javascript 中有一个特殊的[[prototype]]内置属性,其实是对于其他对象的引用,几乎所有对象在创建时[[Prototype]]属性都会被赋予一个非空的值,在进行属性查找的时候,一直会沿着这条链进行查找。

类

Javascript 中只要有对象,实际上,Javascript 才是真正应该被称为”面向对象”的语言,因为它是少有的可以不通过类,直接创建对象的语言。在Javascript中,类无法描述对象的行为,(因为根本不存在类!)对象直接定义自己的行为。Javascript 中只有对象

在面向类的语言中,类可以被复制(或者说)实例化多次,意味着”把类的行为复制到物理对象中”。
但是在 Javascript 中,并没有类似的复制机制。你不能创建一个类的多个实例,只能创建多个对象,他们的 [[prototype]] 关联的是同一个对象,但是在默认情况下并不会进行复制,因此这些对象直接并不会完全失去联系,它们是互相关联的。

new Foo()会生成一个新对象(我们称之为a),这个新对象的内部链接 [[prototype]] 关联的是 Foo.prototype 对象。最后我们得到了两个对象,他们之间互相关联。

继承意味着复制操作,Javascript 默认不会复制对象属性,Javascript 会在两个对象之间创建关联,这样一个对象可以通过委托访问另一个对象的属性和函数

1
2
3
4
5
function Foo() {};
Foo.prototype = {};
var foo = new Foo();
foo.constructor === Foo // false
foo.constructor === Object // true

首先 foo 对象上并没有属性 constructor,foo.constructor是通过默认的[[prototype]]委托指向Foo,因为现在改写了Foo.prototype值,所以Foo.prototype上也没有了默认属性.constructor,所以会顺着原型链接着找,直到找到顶端 Object.prototype。
foo.constructor 是一个非常不可靠并且不安全的引用,通常来说要避免这些引用。

原型继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Foo(name) {
this.name = name;
}
Foo.prototype.myName = function() {
return this.name
}
function Bar(name, label) {

Foo.call(this, name);
this.label = label;
}
// 创建了一个新的Bar.prototype对象并关联到Foo.prototype,但同时会丢失contructor属性。
Bar.prototype = Object.create(Foo.prototype);
Bar.prototype.myLabel = function() {
return this.label;
}
var a = new Bar("a","obj a");
a.myName(); // "a"
a.myLabel(); // "obj a"

下面有两种常见的错误做法

1
2
3
4
5
// 直接将Bar.prototype指向Foo.prototype对象,这样如果修改某个prototype值也会修改另一个
// 并不会创建一个关联到Bar.prototype的新对象
Bar.prototype = Foo.prototype;
// 使用Foo()构造函数调用创建关联,如果Foo函数本身有副作用会影响到Bar()的后代。
Bar.prototype = new Foo();

ES6 提供了辅助函数Object.setPrototypeOf()来修改对象的[[prototype]]关联。

1
2
3
4
// ES6 之前 会抛弃默认的Bar.prototype,需要对抛弃的对象进行垃圾回收
Bar.prototype = Object.create(Foo.prototype);
// ES6 可以直接修改现有的Bar.prototype
object.setPrototypeOf(Bar.prototype, Foo.prototype);

检查”类”关系

检查一个实例(Javascript中的对象)的继承祖先(Javascript中的委托关联)通常称为内省(或者反射)。
第一种: a instanceof b
instanceof 只能判断对象和函数(带.protptype引用的Foo)之间的关系。如果你想判断两个对象直接是否通过 [[prototype]] 链关联,只用 instanceOf 无法实现。
第二种: Foo.prototype.isPrototypeOf(a)
isPrototypeOf() 回答的是: 在a的整条 [[prototype]] 链中是否出现过 Foo.prototype。
和 instanceof 类似,但是 isPrototypeOf 并不需要间接引用函数(Foo), 它的 .prototype 属性会被自动访问。
也就是说,只需要两个对象即可判断它们之间的关系,

1
2
// b 是否出现在 c 的 [[prototype]]链中?
b.isPrototypeOf(c)

直接获取一个对象的 [[prototype]] 链: Object.getPrototypeOf(a)

对象关联

var bar = Object.create(foo);
Object.create(…) 是在 ES5 中新增的函数,在 ES5 之前的环境如果要支持的话就需要使用一段简单的polyfill代码。

1
2
3
4
5
6
7
if(!Object.create) {
Object.create = function(o) {
function F() {};
F.prototype = o;
return new F();
}
}

行为委托

// TODO

细说图片懒加载(LazyLoad)

Posted on 2019-01-30

LazyLoad(懒加载),顾名思义,等到需要加载的时候再加载,主要是针对图片加载的优化,目前对页面加载速度来说,影响最大的还是图片。对于图片过多的页面,在图片出现在视野中的时候再加载,未出现在视野内的暂时不加载,这样在页面性能上会有很大的提升。

原理

1
<img src="default.jpg" data-src="http://season.jpanj.com/001.jpg" />

src指向默认图片或loading,设置data-src属性(自定义属性)为真实图片地址,监听滚动事件,当图片到达可视区域时,再将src值替换成data-src的值。

简单实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<head>
<title>Lazy-Load</title>
<style>
img {
display:block;
width: 200px;
height:200px;
background-color: gray;
margin-top: 20px;
}
</style>
</head>
<body>
<div class="container">
<img class="pic" alt="加载中" data-src="http://season.jpanj.com/001.jpg">
<img class="pic" alt="加载中" data-src="http://season.jpanj.com/002.jpg">
<img class="pic" alt="加载中" data-src="http://season.jpanj.com/003.jpg">
<img class="pic" alt="加载中" data-src="http://season.jpanj.com/004.jpg">
<img class="pic" alt="加载中" data-src="http://season.jpanj.com/005.jpg">
<img class="pic" alt="加载中" data-src="http://season.jpanj.com/006.jpg">
<img class="pic" alt="加载中" data-src="http://season.jpanj.com/007.jpg">
<img class="pic" alt="加载中" data-src="http://season.jpanj.com/008.jpg">
<img class="pic" alt="加载中" data-src="http://season.jpanj.com/009.jpg">
<img class="pic" alt="加载中" data-src="http://season.jpanj.com/010.jpg">
<img class="pic" alt="加载中" data-src="http://season.jpanj.com/011.jpg">
</div>
</body>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script>
const imgs = document.getElementsByTagName('img');
const viewHeight = window.innerHeight || document.documentElement.clientHeight;
let num = 0; // 避免每次都从第一张图片开始检查
function lazyload(){
for(let i = num; i < imgs.length; i++) {
let distance = viewHeight - imgs[i].getBoundingClientRect().top;
if(distance >= 0 ){ // 到达可视区域内
imgs[i].src = imgs[i].getAttribute('data-src');
num = i + 1;
}
}
}
window.addEventListener('scroll', lazyload, false);
lazyload();
</script>

js 实现的关键是判断图片到达可视区域,有两个关键的值:可视区域的高度和元素距离可视区域顶部的距离。当可视区域高度大于等于元素距离可视区域顶部的距离时,说明图片元素出现,需要将图片地址替换成真实的地址。
可视区域的高度:window.innerHeight || document.documentElement.clientHeight。
元素距离可视区域顶部的距离:getBoundingClientRect() 获取元素的尺寸,top属性获取距离顶部的高度。
展示效果
打开控制台可以看出,当img滚动到当前视野范围内时,img的src属性被赋值给真实的图片地址。

lazy-load

上述实现了基本的懒加载功能,但是有个关键点:scroll 事件!如果用户在滚动条到达底部的时候页面未加载出来时,频繁滚动滚动条,那浏览器就会频繁计算是否到达底部,而这种频繁触发事件导致页面大量的计算是特别消耗性能的,会引发页面的抖动甚至卡顿,所以我们需要对此类似的事件例如鼠标事件(mousemove等),键盘事件(keyup、keydown等)进行优化,控制事件被触发的频率。

优化

先引入两个概念:函数节流(throttle)与函数防抖(debounce)。

函数节流

核心思想:某段时间内,不管触发了多少回调,都只执行第一次。
对于上面图片处理则是:在固定的时间间隔内(例如300ms内),不管滚动条触发几次,都只执行第一次触发事件的回调,后面的自动忽略,300ms为一个周期。

实现就是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// fn是事件回调, interval表示时间间隔
function throttle(fn, interval) {
let last = 0; // 上次触发回调的时间

return function () {
let context = this; // 调用时this上下文
let args = arguments; // 调用时传入的参数
let now = +new Date(); // 触发本次事件的回调

if (now - last >= interval) { // 判断上次触发时间和本次触发时间间隔是否小于规定间隔
last = now;
fn.apply(context, args);
}
}
}

const better_scroll = throttle(lazyload, 300);

document.addEventListener('scroll', better_scroll);

实现效果
打开控制台,在触发回调的时候,会明显看到打印出来的触发信息频率变少。
lazy-load-throttle

函数防抖

核心思想:某段时间内,不管触发了多少回调,都只执行最后一次。
可以看到和函数节流的根本区别是:执行回调是以哪次为主。函数防抖典型的应用场景是文本输入的 ajax 验证,判断用户段时间内不输入再去验证,减少请求次数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function debounce(fn, delay) {
let timer = null;

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

// 每次事件被触发时,都去清除之前的旧定时器
if(timer) {
clearTimeout(timer)
}
// 设立新定时器
timer = setTimeout(function () {
fn.apply(context, args)
}, delay)
}
}

const better_scroll = debounce(lazyload, 300);

document.addEventListener('scroll', better_scroll);

实现效果
在快速滚动到某个位置后来回滚动滚动条,会发现图片长时间未加载,出现“卡顿”现象。因为用户频繁滚动滚动条,那每次计时器都清零,向后推迟300ms,回调函数就会一直推迟没有反应。
所以函数防抖需要设置一个最小延迟时间,如果时间间隔大于设置的最小时间间隔,则必须触发回调。也就是将函数节流与函数防抖结合起来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function throttle(fn, delay) {
let last = 0, timer = null;

return function () {
let context = this;
let args = arguments;
let now = +new Date();

if (now - last < delay) { // 当时间间隔小于设置的时间间隔,则设置一个新的定时器
clearTimeout(timer)
timer = setTimeout(function () {
last = now;
fn.apply(context, args);
}, delay)
} else { // 如果超过,则进行回调
last = now;
fn.apply(context, args);
}
}
}

const better_scroll = throttle(lazyload, 300);

document.addEventListener('scroll', better_scroll);

实现效果

Vue.nextTick 源码解析

Posted on 2019-01-24

在使用 vue 的过程中,如果碰到需要将更新的数据渲染到DOM上才进行后续操作的话,会用到$nextTick。用法如下:

在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。

1
2
3
4
5
6
7
8
9
10
11
12
// 修改数据
vm.msg = 'Hello'
// DOM 还没有更新
Vue.nextTick(function () {
// DOM 更新了
})

// 作为一个 Promise 使用 (2.1.0 起新增,详见接下来的提示)
Vue.nextTick()
.then(function () {
// DOM 更新了
})

也就是说,回调函数将会在下次 DOM 更新循环结束后调用。
那 DOM 更新循环是什么? 下次更新循环呢?

我们知道 Vue 是异步更新代码的,异步更新是就是每次属性修改会将更新操作先放入一个队列中,当所有的更新操作完成后,会一次性执行队列中所有的更新方法,同时清空队列。如果是同步更新,就会在每次属性变化的时候去渲染DOM,如果修改两个属性,就会渲染两次,这样的性能开销是非常大的。

在深入理解 nextTick 之前,先了解一下JS运行机制。
分析 Vue 的 nextTick 源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112

// v2.6.6 src/core/util/next-tick.js
/* @flow */
/* globals MutationObserver */

import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'

export let isUsingMicroTask = false

const callbacks = []
let pending = false

function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}

// Here we have async deferring wrappers using microtasks.
// In 2.5 we used (macro) tasks (in combination with microtasks).
// However, it has subtle problems when state is changed right before repaint
// (e.g. #6813, out-in transitions).
// Also, using (macro) tasks in event handler would cause some weird behaviors
// that cannot be circumvented (e.g. #7109, #7153, #7546, #7834, #8109).
// So we now use microtasks everywhere, again.
// A major drawback of this tradeoff is that there are some scenarios
// where microtasks have too high a priority and fire in between supposedly
// sequential events (e.g. #4521, #6690, which have workarounds)
// or even between bubbling of the same event (#6566).
let timerFunc

// The nextTick behavior leverages the microtask queue, which can be accessed
// via either native Promise.then or MutationObserver.
// MutationObserver has wider support, however it is seriously bugged in
// UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
// completely stops working after triggering a few times... so, if native
// Promise is available, we will use it:
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
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)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
// Use MutationObserver where native Promise is not available,
// e.g. PhantomJS, iOS7, Android 4.4
// (#6466 MutationObserver is unreliable in IE11)
let counter = 1
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// Fallback to setImmediate.
// Techinically it leverages the (macro) task queue,
// but it is still a better choice than setTimeout.
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// Fallback to setTimeout.
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}

export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
pending = true
timerFunc()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}

可以看到 nextTick 是用 Promise 实现的,也就是说是放入 microtask 中的,为什么不放入 macrotask 用 setTimeout 呢?

如果放入 macrotask 中,则会在当前脚本执行完后清空一次 microtask,然后执行 render 渲染页面,此时还未执行更新操作,因为更新操作在下一轮事件循环中的 macrotask,所以此时 DOM 并未修改,如果要渲染成功就需要两次事件循环。所以异步更新 DOM 操作要放到 microtask 中。尽可能的用 microtask,如果浏览器不支持,再用 macrotask。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||

MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {

let counter = 1
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
  • 当浏览器支持 Promise,使用 Promise 触发回调。
  • 否则,如果支持 MutationObserver,实例化观察者对象。
  • 否则,如果支持 setImmediate,则用 setImmediate。
  • 最后,如果都不支持,使用 setTimeout 0。

不管哪种任务,最终都赋值给 一个异步延迟的函数 timerFunc。

再接着看 nextTick 函数,它接收两个参数,第一个参数是回调函数 cb,第二个参数是当前环境上下文 ctx。将回调函数 cb 添加到 callbacks 数组中,此时回调函数并没有执行,当pending 为 false 时,开始执行。pending 在最开始文件首部定义let pending = false,它是一个标识,也就是锁,表示队列中是否有在等待调用的回调,当没有时,执行 timerFunc。执行时完后将 pending 恢复为 false(打开状态)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
// 检查是否有正在执行的异步任务队列(callback任务数组)
if (!pending) {
// 把锁锁上
pending = true
timerFunc()
}
// $flow-disable-line
// 如果没有提供回调,并且支持Promise,返回一个Promise
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}

timerFunc 里注册了回调事件 flushCallbacks,当调用栈执行完毕后再去执行里面的 flushCallbacks 事件,flushCallbacks 内部在执行 callbacks 回调时对回调函数队列做了一个备份,因为如果出现 nextTick 回调里又有 nextTick,那从设计角度看,内层nextTick应该放入新的 microtask 中,避免外层的 nextTick 回调函数和内层的 nextTick 回调函数在同一个 microtask 中被执行。

1
2
3
4
5
6
7
8
9
10
11
12
function flushCallbacks () {
pending = false
// 拷贝一份,防止出现cb回调函数执行时又往callback中添加回调
// 比方说nextTick的回调里又有nextTick,内层nextTick应该放入新的microtask中
// 拷贝一份执行完当前的即可
const copies = callbacks.slice(0)
// 拷贝完将callbacks清空
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}

所以,精简一下nextTick核心代码如下。(setTimeout 来模拟异步执行回调)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let callbacks = [];
let pending = false;
function nextTick(cb) {
callbacks.push(cb);
if(!pending) {
pending = true;
setTimeout(flushCallbacks, 0); // 用 setTimeout 模拟异步执行回调
}
}

function flushCallbacks() {
pending = false;
const copies = callbacks.slice(0);
callbacks.length = 0;
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}

Node 实践前端 HTTP 缓存

Posted on 2019-01-10

总结了 HTTP 缓存机制,为了加深理解,用 Node 实践了 HTTP 缓存。
本文demo地址: https://github.com/seasonrui/demo-for-blog/tree/master/node-cache-practice

缓存优先级:

  • service worker
  • memory cache
  • disk cache
  • 网络请求
    浏览器会一级级从上到下找,直到找到为止。

service worker

Service workers 本质上充当Web应用程序与浏览器之间的代理服务器,也可以在网络可用时作为浏览器和网络间的代理。它们旨在(除其他之外)使得能够创建有效的离线体验,拦截网络请求并基于网络是否可用以及更新的资源是否驻留在服务器上来采取适当的动作。他们还允许访问推送通知和后台同步API。

memory cache

内存中的缓存,几乎所有请求都会浏览器自动加入 memory cache 中,是浏览器自身的机制,不受开发者控制。关闭 tab 页即失效。

disk cache

存储在硬盘上的缓存,是持久存储的。会根据 http 头信息来判断哪些资源是可以缓存的。平时说的强制缓存,协商缓存都是此类缓存。

强制缓存

利用 http 响应头中的 Expires 和 Cache-Control 两个字段来控制,表示资源的缓存时间。

Expires

是 HTTP/1.0 的规范,表示的是绝对时间,值是一个GMT格式的时间字符串。例如:

1
Expires: Fri Jan 11 2019 14:03:22 GMT

告诉浏览器,在这个时间之前,都不需要再次请求。
因为是绝对时间,所以如果客户端和服务器时间不一致,会导致浏览器判断缓存失效。

Cache-Control

是 HTTP/1.1 的规范,表示的是相对时间,常用值如下:

  • max-age:表示相对有效时间,主要就是根据这个字段来判断时间。
  • no-cache:不使用本地缓存,使用协商缓存,主要目的是为了防止从缓存中读取过期资源。
  • no-store:真正意义上的”不走缓存”,禁止浏览器及任何中间件缓存资源。
  • public:可以被浏览器,CDN 等缓存。
  • private:只能被用户终端浏览器缓存,不允许 CDN 等服务器对其缓存。

Expires 可以和 Cache-Control 一起使用,Cache-Control 优先级更高。

例子1: max-age

一个简单的index.html页面,引入index.css。设置Cache-control: max-age=120,缓存两分钟。

  1. 第一次请求
    demo
    正常走网络请求。
  2. 刷新当前页面
    demo
    看到请求 from memory cache,因为浏览器自动将请求加入到memory cache中,耗时0ms。
  3. 关闭当前 tab,打开新的 tab 页。
    demo
    因为关闭 tab,memory cache 清空。接着去看 disk cache,因为设置了强制缓存2分钟,所以会看到 from disk cache,耗时比memory cache多。
    当设置 max-age 为 0 时,每次都会重新请求。

例子2: no-cache

服务器设置响应为 Cache-Control: no-cache。
index.html 页面引入多个资源,相同的资源都引入两次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="icon" href="data:;base64,=">
<script src="./index.js"></script>
<script src="./index.js"></script>
<link rel="stylesheet" href="./index.css"/>
<link rel="stylesheet" href="./index.css"/>
</head>
<body>
<img src="./1.jpeg">
<img src="./1.jpeg">
<script>
setTimeout(function () {
let img = document.createElement('img')
img.src = './1.jpeg'
document.body.appendChild(img)
}, 1000)
</script>
</body>
</html>

发现同步请求和异步请求都只请求了一次,也就是说no-cache不会影响本次请求的缓存,只是说下次请求需要协商缓存,本次如果有多个浏览器还是会缓存。
demo2-1
max-age=0和no-cache在浏览器实现上看基本一样,但是max-age为0表示的是到期需要重新验证,而no-cache表示每次必须向服务端验证。

例子3: no-store

服务器设置响应为Cache-Control: no-store
代码同 no-cache 一样,发现不管同步还是异步请求每个请求都不缓存,
demo2-1

协商缓存(对比缓存)

当强制缓存失效,就需要使用对比缓存,由服务器判断缓存资源是否可用,浏览器与服务器通过特定的标识来判断,分别是以下两组字段(成对出现):

Last-Modified & If-Modified-Since

浏览器第一次请求时,服务器返回的 header 中加上 Last-Modified,表示资源最后一次修改时间。
当浏览器再次请求时,请求头中会带上 If-Modify-Since,值是第一次请求返回的 Last-Modified 值,服务器收到请求会,会根据此字段进行比对,如果相等,表示未修改,返回304;反之,返回200状态码,并且返回数据。
缺点是:

  1. 如果文件被服务器动态修改,但是内容并没有修改,此时因为文件的更新时间是最新的,所以缓存起不到作用。
  2. If-Modified-Since能检测到的粒度是s级的,如果资源更新时间是秒以下的单位,则不起作用。

例子4: Last-Modified

服务器设置Cache-Control: no-cache,响应头带上Last-Modified。
第一次请求。
demo4-1

第二次请求会带上If-Modified-Since,值为第一次请求返回的Last-Modified值,服务器对比两次时间,如果相等就返回304,不一致就返回内容和Last-Modified。
demo4-2
打开新标签页,还是会携带If-Modified-Since进行判断。

ETag & If-None-Match

为了解决上述问题,出现了新的字段 ETag & If-None-Match。
ETag 存储的是文件唯一标识,判断流程与 Last-Modified 一样。Last-Modified 可以和 ETag 一起使用,ETag 优先级更高。

例子5:Etag

同 Last-Modified 过程一样,区别是 Last-Modified 值是精确到秒的时间。Etag 是文件的唯一标识。
demo5-1
demo5-2
修改一个js文件,刷新页面会发现返回200。并带上新的Etag信息以便下次请求时更新用。
demo5-4
demo5-3

以上就是所有的缓存机制,但如果什么都没设置呢,浏览器会怎么处理?
对于这种情况,浏览器会采用一个启发式的算法,通常会取响应头中的 Date 减去 Last-Modified 值的 10% 作为缓存时间。

刷新

URL回车:正常请求,查看 disk cache 中是否有匹配,没有则直接请求。
普通刷新:(F5) (Command + R): 当前 tab 没关闭,所以 memory cache 是可用的。会忽略 Expires/Cache-Control 的设置,向服务器发送请求,Last-Modified/Etag 还是有效的。
强制刷新:(Ctrl + F5) (Command + Shift + R):请求头会自动带上Cache-Control: no-cache,表示浏览器不使用缓存,服务器返回 200 和最新内容。

使用场景

频繁变动的资源

对于频繁变动的资源,就需要使用协商缓存了,也就是需要设置响应头 Cache-Control:no-cache,配合 Etag 或 Last-modified,每次都从服务器验证。

不经常变化的资源

通常设置 Cache-control 的 max-age 为较大的值,缓存时间长点,为了解决更新的问题,对文件名上进行 hash 处理。这样在更新的时候,HTML引入的文件名更新,之前的缓存失效。

参考文章: https://juejin.im/post/5c22ee806fb9a049fb43b2c5

1234

ruirui

37 posts
7 tags
© 2022 ruirui
Powered by Hexo
|
Theme — NexT.Muse v6.0.0