edwardzerb / blog Goto Github PK
View Code? Open in Web Editor NEW个人在项目用到,以及在博客、书学习总结的笔记,主要是以个人的知识体系为基础,慢慢的展开。JavaScript、Vue、node、webpack
个人在项目用到,以及在博客、书学习总结的笔记,主要是以个人的知识体系为基础,慢慢的展开。JavaScript、Vue、node、webpack
F:first 的缩写 ,意思第一次
P: paint 的缩写,意思为绘制
C:contentful,意思为内容
FP(First Paint):首次绘制,页面在屏幕上首次发生视觉上变化的事件
FCP(First Contentful Paint):首次内容绘制,浏览器第一次在屏幕上渲染内容
FMP(First Meaniful Paint):首次有效绘制,表示页面的 “主要内容” 开始出现在屏幕上的时间点,该指标是测量用户加载体验的主要指标
LCP(Large Contentful Paint):表示可视区域中 内容 最大的可见元素开始出现在屏幕上的时间点
TTI(Time To Interactive):可交互时间,网页中第一次 完全达到可交互状态 的时间点。主线程的任务均不超过 50 毫秒(time slicing)。
TTFB(Time To First Byte):浏览器接受第一个字节时间
FCI(First CPU Idle):CPU 第一次空闲的时间,CPU空闲,说明主线程已经空闲下来了,此时就可以接受用户的响应了
FID(First Input Delay):首次输入延迟,可在TTI前开始与网页产生交互,也可能在TTI之后才与网页产生交互
可在 head 标签里注册一个事件(click、mousedown、keydown、touchstart、pointerdown),事件响应函数中使用当前时间减去时间对象被创建的时间
FP与FCP 的区别
关键资源:阻塞网页首次渲染的资源(FP)
下面的流程就是前端的基本流程,根据这个流程,我们可以看看每一个阶段都可以做哪些性能优化
类似vue,最终都是将tempalte编译成render 函数,预编译就可以省去在运行时才将template 编译成render函数
tree-shaking,在构建过程中,清楚无用代码,可以减少构建后文件的体积
/**
* tree shaking: 去除无用代码(js、css)
* 前提:1.必须使用ES6模块化
* 2.开启production环境
* 作用:减少代码体积
*
* package.json中配置
* "sideEffects": false 所有代码都没有副作用(都可以进行 tree shaking)
* 问题:可能会把 css/@babel/polyfill(副作用)文件干掉
* "sideEffects": ["*.css", "*.less"]
*/
两种方式:
/**
* 单入口:
* 可以将 node_modules 中代码单独打包一个chunk 最终输出
* 多入口:
* 自动分析多入口chunk中,有没有公共的文件。如果有会打包成单独一个chunk,并且都引用这个chunk
*/
optimization: {
splitChunks: {
chunks: 'all',
// 分割的 chunk 最小为30kb
miniSize: 30 * 1024,
// 没有最大限制
maxSize: 0,
// 要提取的chunk最少被引用1次
minChunks: 1,
// 按需加载时并行加载的文件的最大数量
maxAsyncRequests: 5,
// 入口js文件最大并行请求数量
maxInitialRequests: 3,
// 名称连接符
automaticNameDelimiter: '~',
// 可以使用命名规则
name: true,
cacheGroups: {
// 分割chunk的组
// node_moudules文件会被打包到 vendors 组的chunk中 --> vendors~xxx.js
vendors: {
test: /[\\/]node_modules[\\/]/,
// 优先级
priority: -10
},
default: {
// 要提取的chunk最少被引用2次
minChunks: 2,
// 优先级
priority: -20,
// 如果当前要打包的模块,和之前已经被提取的模块是同一个,就会复用,而不是重新打包
reuseExistingChunk: true
}
}
}
}
// 按需加载 import
/**
* 在不配置多入口的情况下,如何将引用的js文件打包成独立的文件
* 通过js代码,让某个文件被单独打包成一个chunk
* import 动态导入语法:能将某个文件单独打包
*/
import(/* webpackChunkName: 'test' */ './print)
.then(({ mul, count }) => {
// 文件加载成功
console.log(mul(2, 5));
})
.catch(() => {
console.log('文件加载失败')
});
浏览器缓存:通过 HTTP 请求获取到的资源缓存在浏览器,分为强缓存、协商缓存;这是一个递进关系,先强缓存,缓存不命中再到协商缓存。
详情可看我另一篇文章:浏览器缓存
DNS(Domain Name System):域名系统,域名和 IP 地址相互映射的一个分布式数据库。
浏览器会根据自定义的规则,先提前去解析后面可能会使用到的域名,提前解析,不需要等到要去加载资源的时候再去解析。默认的情况下,网页里的 a 标签里的href属性带的域名会自动去启用DNS Prefetching(不需要在 link 里手动设置),HTTPS 下该默认规则无效。HTTPS 下可以在 meta 标签上操作
<link rel="dns-prefetch" href="//test.com">
// 让 a 标签在 HTTPS 下也能使用 dns-prefetch
<meta http-equiv="x-dns-prefetch-control" content="on">
当强缓存没有命中的时候,就要去服务端获取数据。一个TCP连接下,(chrome下)同个域名的HTTP请求最大并发连接数是6个,多处的请求需要排队等候;其它的浏览器也都有限制。
有一个衡量网络性能的指标,RTT(Round Trip Time),客户端到服务端的往返时延,从发送端发送数据开始,到发送端收到来自接收端的确认,总共的耗时;TCP的传输大小也是有限制,一个RRT只能传输14KB的资源,对此我们要对这个RTT进行优化。
这个阶段从三个点来优化:
影响 RTT 的因素,就是资源大小,资源数量,所以方式就是使用上面的两种方式结合。
CDN(Content Delivery Network):内容分发网络,由分布在不同地理位置的 Web 服务器组成。当服务器的地理位置距离我们越远,那传播的延迟就越高;而 CDN 就是让服务器距离客户端更近。
GLSB(全局负载均衡系统):
SLB(本地负载均衡系统):
使用了 CDN 作为缓存的流程:
当我们去把服务端里的数据请求回来以后,就需要进行 HTML 解析,加载关键资源(JS、CSS),我们来看看 JS 是如何影响DOM生成的。正常来说JavaScript文件的下载是同步的,会阻塞DOM的解析;chrome 在这里做了很多优化,在这里会开启预解析操作,当渲染引擎接受到字节流以后,就会开启一个预解析线程去分析HTML中包含的JS、CSS 文件,当解析到相关文件以后,就会提前下载这些文件。
由于JavaScript线程会阻塞DOM,可采纳以下策略进行相关优化:
上面的优化是从文件加载的角度来看的,下面是从JS文件执行的角度来优化。
长任务:主动交互的角度是从 TTI 开始,就是主要是保证用户在做交互的时候要保证流畅,不要长时间的占用主线程(尽量保证任务的执行时间小于50ms)。
相当于是开启了一个新的线程,把需要循环的任务放在当中执行,这样就不会占用主线层,缺点是无法操作 DOM
const testWorker = new Worker('./worker.js')
setTimeout(_ => {
testWorker.postMessage({})
testWorker.onmessage = function (ev) {
console.log(ev.data)
}
}, 5000)
// worker.js
self.onmessage = function () {
const start = performance.now()
while (performance.now() - start < 1000) {}
postMessage('done!')
}
Time Slicing 就是把一个长任务切割成多个执行时间短的任务,
核心的实现是调用 setTimeout 会将任务添加到宏任务中、yield 的可以暂停执行
function block() {
test(function *() {
const start = performance.now();
while(performance.now() - start < 1000) {
console.log(222);
yield;
}
console.log('done!');
})
}
setTimeout(block, 5000);
function test(gen) {
if(typeof gen === 'function') gen = gen();
if(!gen || typeof gen.next !== 'function') return
// 立即执行函数
(function next() {
// 调用next,拿到返回来的 {value: , done: }
const rest = gen.next();
// 结束
if(res.done) return;
setTimeout(next);
})()
}
原理使用了数据劫持和发布订阅,通过Object.defineProperty() 来劫持各个属性的 getter 和 setter
数据变动时,发布消息给依赖收集器(Dep),去通知观察者(Watcher)
作出对应的回调,去更新视图或数据
MVVM 作为绑定的入口,整合 Observer、Compile、Watcher 三者
通过Observer来监听 model 数据变化表,通过Compile来解析编译模版指令
最终利用Watcher搭起Observer、Compiler之间的通信桥梁,达到数据变化 =》视图更新
视图交互变化 =》数据model变更的双向绑定效果
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Document</title>
</head>
<body>
<div id="app">
<h2>{{person.name}} --- {{person.age}}</h2>
<h3>{{person.fav}}</h3>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<h3>{{msg}}</h3>
<div v-text="msg"></div>
<div v-text="person.text"></div>
<div v-html="htmlStr"></div>
<div v-bind:class="className">sss</div>
<input type="text" v-model="msg" />
<button v-on:click="handleClick">点击 v-on</button>
<button @click="handleClick">点击@</button>
</div>
</body>
<script src="./Obeserver.js"></script>
<script src="./MyVue.js"></script>
<script>
let vm = new MVue({
data: {
person: {
name: "xfy",
age: "18",
fav: "study"
},
msg: "success",
htmlStr: "亲爱的来了",
className: "test"
},
el: "#app",
methods: {
handleClick() {
console.log(this);
this.$data.person.name = "tom";
}
}
});
</script>
</html>
// compileUtil
const compileUtil = {
getVal(expr, vm) {
// [person, name]
return expr.split('.').reduce((data, currentVal) => {
return data[currentVal];
}, vm.$data);
},
setVal(expr, vm, inputVal) {
return expr.split('.').reduce((data, currentVal) => {
data[currentVal] = inputVal;
}, vm.$data);
},
getContentVal(expr, vm) {
return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
return this.getVal(args[1], vm);
})
},
// expr: msg <div v-text="person.text"></div>
// {{}}
text(node, expr, vm) {
let value;
if (expr.indexOf('{{') !== -1) {
// {person.name}} --- {{person.age}}
value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
// 绑定观察者,将来数据发生变化,触发这里的回调 进行更新
new Watcher(vm, args[1], (newVal) => {
this.updater.textUpdater(node, this.getContentVal(expr, vm));
})
return this.getVal(args[1], vm);
})
} else {
value = this.getVal(expr, vm);
}
this.updater.textUpdater(node, value);
},
html(node, expr, vm) {
const value = this.getVal(expr, vm);
new Watcher(vm, expr, (newVal) => {
this.updater.htmlUpdater(node, newVal);
})
this.updater.htmlUpdater(node, value);
},
model(node, expr, vm) {
const value = this.getVal(expr, vm);
// 绑定更新函数 数据 =》 视图
new Watcher(vm, expr, (newVal) => {
this.updater.modelUpdater(node, newVal);
})
// 视图 =》 数据 =》 视图
node.addEventListener('input', (e) => {
// 设置值
this.setVal(expr, vm, e.target.value);
})
this.updater.modelUpdater(node, value);
},
on(node, expr, vm, eventName) {
let fn = vm.$options.methods && vm.$options.methods[expr];
node.addEventListener(eventName, fn.bind(vm));
},
bind(node, expr, vm, attrName) {
const value = this.getVal(expr, vm);
this.updater.bindUpdater(node, attrName, value);
},
// 更新的函数
updater: {
textUpdater(node, value) {
node.textContent = value;
},
htmlUpdater(node, value) {
node.innerHTML = value;
},
modelUpdater(node, value) {
node.value = value;
},
bindUpdater(node, attrName, value) {
// node.attributes[attrName] = value;
node.setAttribute(attrName, value);
}
}
}
class Compile {
constructor(el, vm) {
this.el = this.isElementNode(el) ? el : document.querySelector(el);
this.vm = vm;
// 1.获取文档碎片对象 放入内存中会减少页面的回流和重绘
const fragment = this.node2Fragment(this.el);
// 2.编译模版
this.compile(fragment);
// 3.追加子元素到根元素
this.el.appendChild(fragment);
}
compile(fragment) {
// 1.获取子节点
const childNodes = fragment.childNodes;
[...childNodes].forEach(child => {
// console.log(child);
if (this.isElementNode(child)) {
// 是元素节点
// 编译元素节点
this.compileElement(child);
} else {
this.compileText(child);
// 文本
}
if (child.childNodes && child.childNodes.length) {
this.compile(child);
}
});
}
// 编译元素节点
compileElement(node) {
// <div v-text='msg'></div>
const attributes = node.attributes;
[...attributes].forEach(attr => {
const { name, value } = attr;
if (this.isDirective(name)) { // 是一个指令
const [, directive] = name.split('-'); // text html model on:click
const [dirName, eventName] = directive.split(':'); // text html model on
// 更新数据 数据驱动视图
compileUtil[dirName](node, value, this.vm, eventName);
// 删除有指令的标签上的属性
node.removeAttribute('v-' + directive);
} else if (this.isEventName(name)) { // @click='handleclick'
let [, eventName] = name.split('@');
compileUtil['on'](node, value, this.vm, eventName);
// 删除有指令的标签上的属性
node.removeAttribute('@' + eventName);
}
});
}
// 编译文本节点
compileText(node) {
// {{}} v-text
const content = node.textContent;
if (/\{\{(.+?)\}\}/.test(content)) {
compileUtil['text'](node, content, this.vm);
}
}
// @click
isEventName(attrName) {
return attrName.startsWith('@');
}
// 指令
isDirective(attrName) {
return attrName.startsWith('v-');
}
node2Fragment(el) {
const f = document.createDocumentFragment();
let firstChild;
while (firstChild = el.firstChild) {
f.appendChild(firstChild);
}
return f;
}
isElementNode(node) {
return node.nodeType === 1;
}
}
class MyVue {
constructor(options) {
this.$data = options.data;
this.$el = options.el;
this.$options = options;
if (this.$el) {
// 1.实现一个数据观察者
new Observer(this.$data);
// 2.实现一个指令解析器
new Compile(this.$el, this);
}
}
}
Observer
// Watcher
class Watcher {
constructor(vm, expr, cb) {
this.vm = vm;
this.expr = expr;
this.cb = cb;
// 先把旧值保存起来
this.oldVal = this.getOldVal();
}
getOldVal() {
// 初始化的时候
Dep.target = this;
const oldVal = compileUtil.getVal(this.expr, this.vm);
// 如果这里不置为 null 的话
// 那么后续每一次调用 expr(例如 vm.$data.hrmlStr) 这个属性
// 都会往监听数组里面添加 watcher
// 重点要记住,只有在 模版编译的时候才会创建 watcher(暂时)
Dep.target = null;
return oldVal;
}
update() {
const newVal = compileUtil.getVal(this.expr, this.vm);
if (newVal !== this.oldVal) {
this.cb(newVal);
}
}
}
// Dep
class Dep {
constructor() {
this.subs = [];
}
// 收集观察者
addSub(watcher) {
this.subs.push(watcher);
}
// 通知观察者更新
notify() {
console.log('通知观察者', this.subs);
this.subs.forEach(w => w.update());
}
}
// Observer
class Observer {
constructor(data) {
this.observe(data);
}
observe(data) {
if (data && typeof data === 'object') {
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key]);
});
}
}
defineReactive(obj, key, value) {
// 递归遍历
this.observe(value);
let dep = new Dep();
// 劫持并监听所有的属性
Object.defineProperty(obj, key, {
enumerable: true,
configurable: false,
get() {
// 初始化(data初始化,wathcer初始化)
// 订阅数据变化时,往 Dep 中添加观察者
Dep.target && dep.addSub(Dep.target);
return value;
},
set: (newVal) => {
// 特殊情况,当遇到data.person 被重新赋值的时候,需要重新劫持
this.observe(newVal);
if (value !== newVal) {
value = newVal;
}
// 告诉 Dep 通知变化
dep.notify();
}
})
}
}
数组监听的步骤:
获取Array.prtotype 上的七个方法
然后监听根据数组原型创建出来的对象,把上面的方法传到key里面去
然后就把经过加工过的 ArrayMethods 添加到要监听的 arr.proto 上
加工的方法里面就可以把通过函数算出来的值进行响应式
(后期继续完善Array)
虚拟DOM(Virtual DOM):
React、Vue都使用到了虚拟 DOM ,也为其带来了跨平台的能力(React-Native、Weex)。
虚拟 DOM 其实就是一个普通的JavaScript对象,包含了 tag、props、children 属性
本质上来说,就是使用JavaScript来描述DOM
<div id="app">
<p class="test">hello vue</p>
</div>
// 把上面的 HTML 转换成虚拟DOM
{
// 节点名称 p、ul、li
tag: 'div',
// 节点上的数据 attrs、class、style等等
data: {
id: 'app'
},
// 当前组件 vue 的实例
context: {},
// 当前节点的子节点列表
children: [
{
tag: 'p',
props: {
class: 'test'
},
children: [
'hello vue'
]
}
]
}
当我们数据变化的时候,我们需要进行更新页面,三种方式:
在 vue1.0 版本的时候,通过更细的粒度,一定程度上知道哪些节点使用了这个状态,当这些节点需要进行更新操作的时候,不需要去比对,可直接更新视图;因为粒度太细了,一个绑定对应一个watcher,当需要追踪的数据多时,那这个内存消耗就会非常大。
所以在 vue2.0 以后的版本,选择了一个中等粒度的方案,引入虚拟 DOM,组件级别就是 watcher 实例,此时,当一个组件内有多个节点使用到了某个状态,也就只有一个 watcher 在观察这个状态的变化。
Vue之所以能随意的调整绑定的粒度,主要还是因为使用了变化侦测。
vue会通过编译将模版转换成渲染函数(render),执行这个渲染函数以后就会得到一个虚拟节点树,这个虚拟节点树就可以用来渲染页面。
因为操作DOM的速度慢,避免在操作DOM上面损耗太多性能,虚拟DOM就可以在节点映射到视图中的时候,可以跟旧的虚拟节点(oldVnode)进行对比,找到需要更新的节点,然后在DOM上进行操作。
所以vue中的虚拟DOM主要是做了两件事:
浏览器缓存其实就是浏览器保存通过 HTTP 获取的所有资源,是浏览器将网络资源存储在本地的一种行为
将资源缓存在 memory cache 中,下次再访问时,并不需要重新下载资源,可直接从缓存中获取
Webkit 已经支持 memoryCache
Webkit的资源分为两类:
DiskCache顾名思义,就是将资源缓存到磁盘中,等待下次访问时不需要重新下载资源,而直接从磁盘中获取,它的直接操作对象为CurlCacheManager。
*| memory cache | disk cache
---|---|---|---|
相同点 | 只存储一些派生资源文件 | 只存储一些派生资源文件
不同点 | 退出进程时,数据会被清除 | 退出进程时,数据不清除
存储资源 | 一般脚本、字体、图片会存在内存中 | 一般非脚本会存在内存中,如css
因为CSS文件加载一次就可渲染出来,我们不会频繁读取它,所以它不适合缓存到内存中,但是js之类的脚本却随时可能会执行,如果脚本在磁盘当中,我们在执行脚本的时候需要从磁盘取到内存中来,这样IO开销就很大了,有可能导致浏览器失去响应。
三级缓存原理(缓存访问的优先级)
请求获取到的资源缓存在内存和硬盘
浏览器缓存
优点
浏览器缓存的过程:
浏览器在加载资源时,会根据本地缓存资源的header 中的信息判断是否命中 强缓存,如果命中,则加载缓存中的资源,不会再发起请求
当协商缓存没有命中时,浏览器会发送一个请求给服务器,服务器会判断 header 中 部分信息来判断是否命中缓存,若命中,返回304
Last-Modified 与 ETag 是可以一起使用的,服务器会优先验证 ETag,一致的情况下,才会继续比对 Last-Modified,最后才决定是否返回 304。
当浏览器再次访问一个已经访问过的资源时,它会这样做:
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.