liul0703 / blog Goto Github PK
View Code? Open in Web Editor NEWHome Page: https://liul0703.github.io
Home Page: https://liul0703.github.io
props是一个父组件传递给子组件的数据流,这个数据流可以一直传递到子孙组件。而state代表的是一个组件内部自身的状态
条件 | state | props |
---|---|---|
能否从父组件获取初始值? | √ | √ |
能否被父组件改变? | √ | ✘ |
能否在组件内设置默认值? * | √ | √ |
能否在组件内改变? | ✘ | √ |
能否设置子组件的初始值? | √ | √ |
能否在子组件中改变? | √ | ✘ |
*:props 和 state 从父组件获取的初始值都会被在组件内设置的默认值覆盖。
If a Component needs to alter one of its attributes at some point in time, that attribute should be part of its state, otherwise it should just be a prop for that Component.
如果一个组件在某个时间点需要改变自身的某个属性 那么这个属性应该用state 否则使用props
props
Props (short for properties) are a Component's configuration. They are received from above and immutable as far as the Component receiving them is concerned. A Component cannot change its props, but it is responsible for putting together the props of its child Components. Props do not have to just be data -- callback functions may be passed in as props.
props(properties的简称)是一个组件的配置选项 它们从上而下被指定且不可变 一个组件不能改变自身的props 主要负责设置子组件的props props不仅仅可以作为数据传递也当做callback函数传递
state
The state is a data structure that starts with a default value when a Component mounts. It may be mutated across time, mostly as a result of user events.
A Component manages its own state internally. Besides setting an initial state, it has no business fiddling with the state of its children. You might conceptualize state as private to that component.
state是一种数据结构 当组件挂载是时state有一个默认值 它或许会随着时间的改变而变化 主要原因是用户事件触发
一个组件在内部管理自己的state 除了设置和初始化state 与其子组件state没有关系 可以认为state是一个组件的私有属性
State
如果component的某些状态需要被改变,并且会影响到component的render,那么这些状态就应该用state表示。
例如:一个购物车的component,会根据用户在购物车中添加的产品和产品数量,显示不同的价格,那么“总价”这个状态,就应该用state表示。
Props
如果component的某些状态由外部所决定,并且会影响到component的render,那么这些状态就应该用props表示。
例如:一个下拉菜单的component,有哪些菜单项,是由这个component的使用者和使用场景决定的,那么“菜单项”这个状态,就应该用props表示,并且由外部传入。
当没有合适的属性和元素时,自定义的 data 属性是能够存储页面或 App 的私有的自定义数据
这个自定义data属性的用法非常的简单, 就是你可以往HTML标签上添加任意以 "data-"开头的属性, 这些属性页面上是不显示的,它不会影响到你的页面布局和风格,但它却是可读可写的.
<div id="day2-meal-expense"data-drink="coffee"data-food="sushi"data-meal="lunch">¥20.12</div>
var expenseday2 = document.getElementById('day2-meal-expense');
console.log(expenseday2.dataset.food); // "sushi"
console.log(expenseday2.dataset.drink); // "coffee"
console.log(expenseday2.dataset.meal); // "lunch"
jquery获取
$('#day2-meal-expense').data('drink');
HTML5使用data-* 要注意的地方
data-其后的属性名命名,发现如果为大写字母,则会转为 “-” + “小写字母”
DOM.dataset.newAttrHaha 将会转换为属性“data-new-attr-haha”
使用setAttribute定义的属性,如果中间包含”-”,转换规则有所不同,中间所有大写字母均转换为小写之母
DOM.setAttribute("data-newAttr2-abc", "22222") ==> data-newattr2-abc=”2222”,获取它可以使用getAttribute(‘设定时的名称’)
DOM.getAttribute(“data-newAttr2-abc”)或是DOM.dataset.newattr2Abc获取。获取则刚好相反 “-” + “小写”-->“大写”,如果是“-”+ “数字”,则保持原样不变
设置属性时尽量使用setAttribute(“data-xxx”),其中属性命名最好不要有大写的出现,避免出现“-”,推荐使用“char_char”,如:“favo_obj_id”
data-* 全局属性 构成一类名称为自定义数据属性的属性,允许通过脚本在HTML 和其 DOM 表示之间交换专有信息。所有这些自定义数据都可以通过属性设置的元素的 HTMLElement 接口来访问。 HTMLElement.dataset 属性可以访问它们
原理:往Web页面里插入恶意可执行网页脚本,当用户浏览该网页时,嵌入其中的Web代码被执行,从而达到盗取用户信息或侵犯隐私的目的
XSS类型:
反射型XSS漏洞:通过给他人发送带有恶意脚本代码参数的URL 诱导点击触发,恶意代码被执行
特征: 即时性不经过服务器存储,直接通过GET或POST请求就可完成一次攻击拿到用户隐私数据
需要诱导点击触发
盗取用户敏感保密信息
防范:Web页面渲染的所有内容或渲染数据必须都来自于服务器端
尽量不要从URL,document.referrer,document.forms这种DOM API获取数据直接渲染
不要使用eval等可执行字符串方法
前端渲染时对任何字段都要做escape转义编码
存储型XSS露洞:通过向服务器提交数据将恶意代码存储于数据库持久保存当前页面获取数据后将其渲染
特征:持久性,植入在数据库中,
危害面广 盗取用户敏感私密信息
防范:后端入库前将所有字段统一转义处理
后端输出给前端数据统一转义处理
前端渲染页面DOM时将任何字段都做转义处理
基于字符串集的XSS:绕过转义处理的一种方式
防范:指定
基于Flash的跨站XSS
防范:严格管理cookie的读写权限
未经验证的跳转XSS
防范:对跳转URL参数做白名单处理或者以某种规则过滤 对敏感信息的保护,对cookie使用来
源做验证
if(!Function.prototype.bind){
Function.prototype.bind = function(oThis){
if(typeof this !== 'function'){
throw new TypeError();
}
var aArgs = [].slice.call(arguments,1),
self = this,
fNOP = function(){},
fBound = function() {
return self.apply(this instanceof fNOP
? this
: oThis,
aArgs.concat([].slice.call(arguments)));
};
if(this.prototype){
fNOP.prototype = this.prototype;
}
fBound = new fNOP();
return fBound;
};
}
思路
Function.prototype.call = function(context){
var context = cotext || window;
context.fn = this;
var args = [];
for(var i = 1; i < arguments.length; i++){
args.push('arguments['+i+']');
}
var result = eval('context.fn('+args+')');
delete context.fn;
return result;
}
Function.prototype.apply = function(context, arr){
var context = context || window;
contex.fn = this;
var reslut = [];
if(!arr){
result = context.fn(arr);
}else{
var args = [];
for(var i = 0; i < arr.length; i++){
args.push('arguments['+i+']');
}
result = eval('context.fn('+arr')');
}
delete context.fn;
return result;
}
Promise实现一个 并发限制的方法 限制每次最多同时发送n个请求
async function pMap (iterable, mapper, concurrency) {
return new Promise((resolve, reject_)=>{
const result = []
const iterator = iterable[Symbol.iterator]()
let currentIndex = 0
let resolvingCount = 0
let isRejected = false
let isIterableDone = false
const reject = (reason) => {
isRejected = true
reject(reason)
}
const next = () => {
if (isRejected) {
return
}
const nextItem = iterator.next()
const index = currentIndex
currentIndex++
if (nextItem.done) {
isIterableDone = true
if (resolvingCount === 0) {
resolve(result)
}
return
}
resolvingCount++
(async()=>{
try {
const element = nextItem.value
if (isRejected) {
return
}
const value = await mapper(element, index)
result[index] = value
resolvingCount--
next()
} catch (error) {
reject(error)
}
})()
}
for(let index = 0; index < concurrency; index++) {
try {
next()
} catch(error) {
reject(error)
break
}
if (isIterableDone || isRejected) {
break
}
}
})
}
const mapper = (time) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(time)
resolve(time)
}, time);
});
}
pMap([1000, 3000, 2000, 4000], mapper, 2)
git 删除远程分支:git push origin --delete{}
git查看远程分支:git branch -r
git 切换远程分支:git checkout [name]
git stash 保存当前修改 但不提交commit
git stash apply{stash@{n}} 回到某个stash状态
将某次提交应用到指定分支
git checkout {branch-name}
git cherry-pick {hash}
git branch -m new-name
git push origin :old-name new-name
git push origin -u new-name
关于浅复制很容易,想必大家都清楚,比如Object.assign()
亦或者是利用ES6的扩展运算符(object spread) ,之前也有总结过一部分,最近看了一些文章,所以这次我们主要来聊聊深复制
function deepCopy(p,c){
var i;
c = c||{};
for(i in p){
if(p.hasOwnProperty(i)){
if(typeof p[i]==="object"){
c[i] = Array.isArray(p[i])?[]:{};
deepCopy(p[i],c[i]);
}else{
c[i] = p[i];
}
}
}
return c;
}
上述代码在复制时会进行类型检查 如果复制对象为一个数组或者对象会递归的遍历属性对象并将属性元素复制出来,有个问题就是无法解决循环引用
const x = {};
const y = {x};
x.y = y;
const copy = deepCopy(x) // Uncaught RangeError: Maximum call stack size exceeded
算是一种比较老的方法,通过将对象字符串化然后解析转化回对象,可能看上去有些头重脚轻,但是确实有效
const obj = /* ... */;
const copy = JSON.parse(JSON.stringify(obj));
关于此方法同样有一个潜在的缺陷是无法处理对象中的循环引用
const x = {};
const y = {x};
x.y = y;
const copy = JSON.parse(JSON.stringify(x)); //Uncaught TypeError: Converting circular structure to JSON
还有一点是它无法用于处理JS的一些内置类型比如 Maps,Sets,RegExps,Dates,ArrayBuffers...等
利用postMessage API 可以解决上述JSON.parse无法解决的循环引用,和无法处理内置类型的问题
function structuralClone(obj){
return new Promise(resolve ={
const {port1,port2} = new MessageChannel();
port2.onmessage = ev=> resolve(ev.data);
port1.postMessage(obj);
});
}
const obj = /* ... */;
const copy = await structuralClone(obj);
这个方法有一个缺点就是它是异步的,虽然不是什么大问题,但是有时候你可能需要的是一个同步的深复制办法
如果你用过history.pushState()来构建SPA应用的话,那么你肯定知道你可以通过提供一个state对象来存储URL,并且它是同步的,这里我们为了避免带来其他不必要的麻烦和问题,在使用完state后记得还原它,同时我们用history.replaceState()来代替history.pushState()
function structuralClone(obj){
const oldState = history.state;
history.replaceState(obj,document.title);
const copy = history.state;
history.replaceState(oldState,document.title);
return copy;
}
const obj = /* ... */;
const copy = structuralClone(obj);
tips: Safari浏览器对replaceState 做出了限制一个窗口30s内调次数不超过100次
利用Notification API深复制对象
function structuralClone(obj){
return new Notification('',{data:obj,slient:true}).data;
}
const obj = /* ... */;
const copy = structuralClone(obj);
tips: Safari 由于某些原因 总是返回undefined
当我们在写React时候 会用到ES6中的class语法 ,比较常见的情况如下:
class MyClass extends React.Component{
constructor(){
super()
}
}
这里有两个问题:
是否有必要在constructor
中调用super()
函数?
调用super()
和super(props)
有何区别 ?
解答 Q1:
Always call super() if you have a constructor and don't worry about it if you don't have a constructor
只有当你有一个constructor
时候调用super()才是必须的 看代码:
class MyClass extends React.component{
render(){
return <div>Hello {this.props.world}</div>
}
}
上述代码完全符合规定所以你其实并没有必要去为你创建的每个React Component 调用super()
话分两头 如果你的代码中有constrctor
你就必须调用super()
class MyClass extends React.component {
constructor(){
console.log(this) //Error: 'this' is not allowed before super()
}
}
出现上述错误的原因是 super()
未被调用之前 this
还未被初始化 (uninitialized) [更多]
或许聪敏的你会想着 使用一个空的constructor
从而摆脱super()
class MyClass extends React.component {
constructor(){} // Error: missing super() call in constructor
}
ES6的class
的constructors如果属于子类就 必须调用super()
方法 所以一旦你的代码有 constructor
你就必须调用用super()
解答Q 2:
Call super(props) only if you want to access this.props inside the constructor. React automatically set it for you if you want to access it anywhere else.
假使你想获取到constructor
中的this.props
你就必须调用super(props)
然后React就会自动为你自动为你配置好它 以便你可以在随便什么地方调用它
看一下使用super()
和 super(props)
的不同 :
class MyClass extends React.component{
constructor(props){
super();
console.log(this.props); // this.props is undefined
}
}
当使用super(props)
时 你可以从constructor
中获取到this.props
class MyClass extends React.component{
constructor(props){
super(props);
console.log(this.props); // prints out whatever is inside props
}
}
当然还有一点 当你想在其他地方使用它时 也没有必要将props传递到constructor中 React会自动为你设置好它 [更多]
class MyClass extends React.component{
render(){
// There is no need to call `super(props)` or even having a constructor
// this.props is automatically set for you by React
// not just in render but another where else other than the constructor
console.log(this.props); // it works!
}
}
我的理解是 总之 需要绑定 this.
方法或是需要在 constructor 使用操作 props 定义 state,就需要 constructor ,否则 例如在其他方法中(如 render()
)使用 this.props 则没必要要使用 constructor
函数去抖核心是指:为了不让一个函数单位时间内执行次数太多 从而导致性能问题 限制其在一定时间的连续的函数调用只让其执行一次
某些代码不可以在没有间断的情况连续重复执行,第一次调用函数创建一个定时器 在指定的时间间隔之后运行代码 当第二次调用该函数时 就会清除前一次的定时器并设置另外一个 如果前一个定时器已经执行过了这个操作就没有意义了 然而如果前一个定时器尚未执行 其实就是将其替换为一个新型的定时器 目的是只有在执行函数的请求停止一段时间后才执行(高程3 P614)
经典实现代码如下:
function debounce(fn,time){
var timer;
return function(){
clearTimeout(timer);
var context = this;
var args = arguments;
timer = setTimeout(function(){
fn.apply(context,args);
},time)
}
}
函数去抖在一段时间只执行一次,而函数节流则是间隔时间段执行 具体如下
函数节流的核心是指:为了不让一个函数执行的太频繁 减少一些过快的调用 降低函数触发的回调频率
// 定时器实现
function throttle(fn,wait){
var timer;
return function(){
var context = this;
var args = arguments;
if(!timer){
timer = setTimeout(function(){
fn.apply(context,args);
},wait)
}
}
}
// 时间戳实现
function throttle(fn,wait){
var timer;
var prev = 0;
return function(){
var args = arguments;
var now = +new Date();
if(now - prev > wait){
fn.apply(this,args);
prev = now;
}
}
}
// swr
function throttle(fn, wait){
let pending = false;
return (...args) => {
if(pending) return;
pending = true;
fn(...args);
setTimeout(()=> (pending = false), wait);
}
}
回顾一下二月到三月之间经历的面试
如下会列出面试过程中问的比较多的一些问题,希望可以帮到有需要的同学,如果都可以答上来,那么前两面应该差不多了三四面就看运气了。
⭐️ 个数用来表示频次
由于对Vue不是很熟 问的比较少
总体来说没有碰到hard的 基本都是easy或Meduim难度
比如 : 最大子列和 链表倒数第k个元素 二叉树反转 二叉树最大深度 树形对象中找指定个元素输出路径等等
性能优化(因为简历有写)所以问的最多也最详细牵扯到项目 基本每个公司都会问 深挖细节很多
不知道你有没有注意到,在基于Webkit的浏览器上执行某些CSS操作时页面会出现不流畅或者闪一下的情况,尤其是执行CSS动画的时候,那你之前可能已经听过“硬件加速(hardware acceleration)”这个专业术语了。
简单来说硬件加速意味着浏览器会帮你把一部分对渲染页面来说较繁重的任务交给GPU(Graphics Processing Unit),而不是一股脑都交给CPU(Central Processing Unit )去处理,当这部分CSS操作得到硬件加速时,可以使页面渲染速度更快。
CPU在电脑主板上,就像是计算机的”大脑“,CPU几乎会做所有事,GPU位于计算机的显卡上,主要用做图形渲染,此外,GPU是专为执行图形渲染所需的复杂数学和几何计算而设计的,所以将一些操作移到GPU上去处理可以带来巨大的性能提升并且减轻了CPU的压力,在移动端尤为明显。
硬件加速(又名GPU加速)依赖于浏览器在渲染页面时使用的分层模型,当对页面上的元素执行某些操作(例如3D变换),对应元素会被提升到到自己的图层,独立于页面的其余部分的呈现并在后期合成(绘制到屏幕上),这样单独把元素隔离,可以使得当页面上只有这个元素发生变换(transform)的时候其余元素不需要重新渲染,从而带来速度提升的优点,值得一提的是,只有3D变换才有资格拥有自己的图层,2D变换则没有。
CSS animation,transform,transition这些属性并不会自动被硬件加速,而是由浏览器的渲染引擎去执行的,但是一些浏览器通过某些属性提供硬件加速,从而提升页面的渲染性能。例如CSS中的opacity属性是少数几个可以被浏览器认定为可被硬件加速的属性之一,因为GPU可以很容易的实现。一般来说,任何想要通过CSS transition或动画淡化不透明度的图层的行为,浏览器会把它丢给GPU去处理从而提高处理速度。所有的CSS属性中opacity
是性能最好属性的之一,其他常见的硬件加速操作是CSS 3D变换
Hack方法来硬件加速:translateZ() 或者 translate3d()
使用translateZ()
(或translate3d()
)这种hack方式(有时也称为null变换hack)来让浏览器对animation
或transform
行为使用硬件加速,通过向一个不会在三维空间中转换的元素添加简单的3D变换来实现硬件加速。例如通过给一个二维空间中动画添加简单的规则来硬件加速。
transform:translate3d(0,0,0)
硬件加速操作会创建所谓的合成层,合成层会被上传到GPU并由GPU合成,但是这种hack的方法去创建图层并不是万能的,图层创建可以加快页面加载速度,但会带来其他的成本:它会占用系统RAM和GPU上的内存(限于移动设备),并且很多时候都会带来不良影响(特别是在移动设备上),所以这种方法要合理的去使用,你必须要清楚的知道使用这种方式是不是真的可以提高页面性能,不能使这个操作反而成了影响页面性能的瓶颈。
除了这种创建图层的方法,CSS引入了一个新的属性,它允许我们提前告知浏览器可能会对元素进行哪些操作,让浏览器去优化并提前处理那些潜在的比较消耗性能的操作比如在动画开始之前,提前处理元素的动画行为。这个属性就是will-change
will-change
的荣光will-change
属性允许你提前通知浏览器你可能会对某个元素做什么类型的操作,以便于浏览器在需要的时候采取适当的优化方案。因此,避免了可能对页面的响应性产生负面影响的非必要成本,使元素可以更快地呈现。渲染并快速更新,从而获得更流畅的体验。
举个例子,当对元素使用CSS transform时,元素及其内容可能会提升为图层,如之前所言,之后会将他们合成(composited)(绘制在屏幕上),但是将一个元素提升到一个新图层是很消耗性能的,这可能会使transform动画的开始延迟明显的几分之一秒,从而引起明显的“闪烁”。
为了避免这种情况发生,我们可以提前告知浏览器,让浏览器可以提前做准备,那么当同样的操作发生时,因为元素的图层已准备就绪,然后就可以立刻执行转换动画,从而渲染元素并快速更新页面。
使用will-change
可以提示浏览器将会发生的转化,语法非常简单
will-change:transform
也可以添加多个你期望改变的确切属性的值。如:
will-change:transform,opacity
确切指定你想改变属性可以让浏览器更好的决策如何以更优的办法处理这些变动,这明显是比使用hack手法让浏览器被迫创建可能无用的图层的更好且更有效的一种方法了
是,也不是,这取决于你给定的属性。如果该属性的任何非初始值将在元素上创建堆栈上下文,则在will-change
中指定该属性将在元素上创建堆栈上下文。举个例子:将clip-path
属性和opecity
属性用于初始值以外的值时,都会在它们所应用的元素上创建堆栈上下文。因此,使用这些属性中的一个(或两个)作为will-change
的值,即使在更改实际发生之前,也会在元素上创建堆叠上下文,这同样适用于将在元素上创建堆栈上下文的其他属性
同样,某些属性可能导致为固定位置的元素创建一个包含块。例如,一个转换后的元素为其所有定位子孙元素创建一个包含块,即使那些已被设置为position:fixed
的元素。因此,如果某个属性会导致创建一个包含块,那么将其指定为will-change
的值也将导致为固定位置元素生成包含块,所以如前所述,某些will-change
属性的变化的导致创建新的合成层,但是,GPU不支持大多数浏览器中CPU所支持的亚像素抗锯齿功能,所以有时会导致内容模糊(尤其是文本)
另外will-change
属性不会直接影响对应元素,它只是给浏览器打个预防针,让浏览器以更高效的渲染方式去呈现元素内容,除了在之前提到的一些情况下创建堆栈上下文和包含块外,它对对应元素没有直接影响。
will-change
的注意事项不要使用will-change
去尝试优化一切操作,有时候相反会带来反作用,要更加聪明且合理的使用它,will-change
也有一些无法直接检测到的副作用,毕竟这也只是一种和浏览器后台通话的方式而已,不能指望它为你做所有事情,因为使用此属性时,请牢记以下几点,以确保在最大程度上利用该属性,同时避免因滥用该属性而带来的危害。
will-change
声明对太多属性或元素的更改如同之前所提到的 直接让浏览器针对所有元素上的所有属性可能发生的更改进行优化的想法是很诱人的,所以可能会写出如下代码
*,
*::before,
*::after {
will-change: all;
}
看上去好像很完美,但其实是有很大的问题的,首先all
不是合法的will-change
的值,其次这样笼统的规则根本就没什么用,这就好像在告诉浏览器这些所有的属性都可能变化且都需要要优化,那浏览器完全和没优化的版本处理没什么区别,因为没什么优先级也没有任何有用信息。并且如我们之前提到的hack方法,浏览器已经在自己做优化了,所以这样写没有任何意义,而且不可忽视的是因为浏览器要处理will-change
相关的属性也会占用大量计算机资源,过度使用反而使得页面卡顿甚至崩溃
will-change
属性顾名思义:只告知浏览器即发生的变化,所以这个阶段什么改变也没发生,我们使用will-change
是以便于浏览器对我们声明的更改进行某些优化,浏览器需要时间去处理优化这些更改,为了在这些变化在真正发生时可以立即生效,所以在元素更改之前立即设置will-change
几乎没有任何效果,并且可能比不设置更糟糕,举个例子:
.element:hover {
will-change: transform;
transition: transform 2s;
transform: rotate(30deg) scale(1.5);
}
这相当告诉浏览器对已经发生的变化进行优化,这完全没有作用,而且也不符合will-change
的定义,你应该找到一种方法,至少可以提前一点时间预测某种改变会发生,然后设置will-change
的值,举个例子,当点击一个元素时发生变化,然后在该元素悬停时设置will-change
属性,这样将为浏览器提供足够的时间来优化该更改,从悬停元素到用户实际单击元素之间的时间足以使浏览器进行优化了。
.element {
/* style rules */
transition: transform 1s ease-out;
}
.element:hover {
will-change: transform;
}
.element:active {
transform: rotateY(180deg);
}
但是如果我们期望的是在鼠标悬停时变化要怎么做呢?正如我们所提到的,上面的代码也是无用的。在这种情况下,通常仍然可以找到某种方法来在动作发生之前对其进行预测,例如悬停在目标元素的祖先元素上可以提供浏览器足够的反应执行时间,因为将鼠标悬停在其祖先元素上并不一定总是表明该元素将与之交互,因此这个阶段,可以执行诸如设置will-change
属性之类的操作
.element {
transition: opacity .3s linear;
}
/* declare changes on the element when the mouse enters / hovers its ancestor */
.ancestor:hover .element {
will-change: opacity;
}
/* apply change when element is hovered */
.element:hover {
opacity: .5;
}
will-change
浏览器对即将发生的更改进行的优化通常成本很高,而且正如我们前面提到的,它会占用计算机的大部分资源,浏览器进行优化的通常行为是删除这些优化并尽快恢复到正常行为。但是,will-change
会覆盖此行为,从而使优化保持的时间比浏览器原本的时间要长得多,正因为如此在使用完后要记得移除will-change
来释放资源
如果在样式中直接写死will-change
,声明则无法直接移除,所以基本推荐使用JS来处理,通过脚本,可以在浏览器中声明你的更改,然后在更改完成后通过侦听更改完成的时间来移除will-change
,举个例子:如我们之前提到的可以通过监听是否悬停在对应元素(或其祖先元素)上的mouseEnter事件中来设置will-change
,如果你要对元素进行动画处理,则可以使用DOM事件animationEnd来侦听动画何时结束,然后在animationEnd触发后移除will-change
。
// Rough generic example
// Get the element that is going to be animated on click, for example
var el = document.getElementById('element');
// Set will-change when the element is hovered
el.addEventListener('mouseenter', hintBrowser);
el.addEventListener('animationEnd', removeHint);
function hintBrowser() {
// The optimizable properties that are going to change
// in the animation's keyframes block
this.style.willChange = 'transform, opacity';
}
function removeHint() {
this.style.willChange = 'auto';
}
will-change
如之前提到的will-change
被用来向浏览器提示一个元素将在几毫秒内发生的更改,这是允许will-change
直接写在css中的用例之一,尽管我们建议使用JavaScript来设置和取消will-change
,但是在某些情况下,在css中进行设置(并保留)是更好的选择。
一个例子:在少数可能被用户反复交互且应该以快速的方式响应用户的互动的元素上设置will-change
,因为元素数量有限,这就意味着浏览器进行的优化不会被过度使用,因此不会造成太大的伤害,例如:通过在用户请求时将其滑出来转换边栏。如下规则就很合适:
.sidebar {
will-change: transform;
}
另一个例子是在几乎不断变化的元素上使用will-change
比如一个元素响应用户的鼠标移动,并随着鼠标移动在屏幕上移动,这种情况下直接在css中声明will-change
的值就可以。因为它准确地描述了元素将有规律/不断地变化,因此应保持优化状态
.annoying-element-stuck-to-the-mouse-cursor {
will-change: left, top;
}
will-change
合法值will-change
属性可取四种可能的值:auto, scroll-position,contents,<custom-ident>
<custom-ident>
值用于指定你希望更改的一个或多个属性的名称,多个属性值之间用逗号分隔,如下是合法的will-change
属性名称的声明
will-change: transform;
will-change: opacity;
will-change: top, left, bottom, right;
<custom-ident>
值不包括will-change, none, all, auto,scroll-position, contents
,所以如同文章之前提到的will-change:all
不是合法的属性值会被浏览器忽略,
auto
则表示没有特定的意图,意味着浏览器不会做任何特殊处理
scoll-position
顾名思义,表示你期望将来随时可以更改元素的滚动位置,这个值很有用,因为在使用时,浏览器将准备并呈现超出可滚动元素的滚动窗口中可见内容的内容。浏览器通常仅在滚动窗口中渲染内容,而某些内容超过该窗口,平衡了因跳过渲染而节省的内存和时间,从而使滚动看起来顺滑,使用will-change:scroll-position
,它可以进行进一步的渲染优化,以便可以平滑地完成更长和或更快的内容滚动。
contents
表示元素的内容可能会发生变化,浏览器通常会对元素做缓存,因为大部分情况下元素不会经常性发生变化,有时候仅仅只是位置的更改。这个值用来作为告诉浏览器减少或者避免对指定元素进行缓存的信号。因为如果元素的内容不断的变化,那么保留内容的缓存将毫无用处并且浪费时间,因为只要元素的内容发生更改,浏览器就会停止缓存并继续从头开始渲染该元素。
如同我们之前提到的。如果在will-change
中指定某些属性,这些属性将无效,因为浏览器不会对大多数属性的更改进行任何特殊的优化。
原文链接: Everything You Need to Know About the CSS will-change Property
Introduction
上篇我们大致分析了在处理JavaScript阶段和Style阶段需要注意的问题,这篇我们就来看下在Layout、Paint、Composite阶段以及处理用户行为的时候,应该关注的问题所在。
Layout阶段浏览器将计算元素的大小,在页面中的位置,其他元素的影响等等,与样式计算(Style calculation)类似,基本限制因素如下:
TL;DR
当更改样式时,浏览器会去检查需不需重新计算触发Layout,一般来说修改元素的几何属性(geometric properties)例如:宽高,布局定位都会触发Layout
.box {
width: 200px;
height: 200px;
}
// 改变元素宽高 触发Layout
.box-expanded: {
width: 300px;
height: 300px;
}
Layout是作用于全局整个文档流的,所以如果有大量的元素需要处理,就会消耗很长时间去计算这些元素的大小和定位。
如果无法避免触发Layout,可以通过Performance查看Layout阶段的耗时是否是影响性能的瓶颈。
在Performance中我们可以清楚的看到Layout阶段消耗的时间,以及涉及的节点数(如图为314个元素)
https://csstriggers.com/ 列出了一些CSS属性会触发渲染的哪个阶段,可以作为对照参考。
另外使用flexbox布局要比传统的通过float或者相对定位绝对定位实现布局更快。
正常情况下渲染步骤是先执行JavaScript,然后是style calculation 然后触发Layout。但是有种情况是触发Layout的时间点早于JavaScript的执行,这种情况叫强制同步布局(forced synchoronous layout)
要明确的是在JavaScript运行时,前一帧的布局属性值都是已知的。举个例子来说如果你想在帧(frame)开始前获取某个元素的高度,就可以这样写:
requestAnimtionFrame(logBoxHeight);
function logBoxHeight(){
console.log(element.offsetHeight);
}
但是如果你先改变的元素的样式然后在获取元素高就会出问题
function logBoxHeight(){
element.classList.add('big');
console.log(element.offsetHeight);
}
现在的情况就变成这样,由于添加了新的class后要输入元素的offsetHeight,浏览器必须先重新进行布局计算才能拿到正确的offsetHeight的值,这完全是没必要的,而且这个例子中通常情况下都是不需要先去设置样式再去取属性值的,直接使用最后一帧的属性值完全足够了。所以一般情况下最好是先去读取需要的属性值,然后再做更改。
function logBoxHeight(){
console.log(element.offsetHeight);
element.classList.add('big');
}
还有一种更糟糕的情况是反复不断的强制同步触发layout。看下面的代码
function resizeAllParagraphsToMatchBlockWidth(){
// 让浏览器陷入读写循环
for(let i = 0; i < paragraphs.length; i++){
paragraphs[i].style.width = element.offsetHeight + 'px';
}
}
打眼一看好像没什么问题,其实这种问题很常见每次迭代都会去读取element.offsetHeight属性,然后用它去更新paragraph的width属性。解决办法也很常见就是读取一次做一个缓存。
const width = element.offsetHeight;
function resizeAllparagraphsToMatchBlockWidth(){
for(let i = 0; i < paragraphs.length;i++){
paragraphs[i].style.width = width + 'px';
}
}
Paint是一个填充像素(pixels)的过程,最终这些像素会通过合成器合成到屏幕。这个阶段通常是渲染元素整个过程中最消耗时间的阶段,所以要尽可能的避免
TL;DR
如果触发Layout肯定触发Paint,因为改变元素的几何属性(宽高等)意味着需要重新布局定位。当然修改一些非几何属性例如:background text-color,shadow这些也会触发paint,只不过不会触发layout所以整个渲染过程就会跳过Layout阶段。
利用Chrome DevTools来观察渲染过程中最消耗性能的部分,可以看到如下图绿色部分表示的是需要被重绘的区域。
可以使用will-change属性或者类似的hack手段让浏览器创建一个新的图层来减少需要被Paint的区域。关于will-change的详细内容可以看这篇文章【关于will-change属性你需要知道的事】此处不在赘述。
尽可能简化Paint的过程,在Paint阶段有的步骤是非常消耗性能的,比如任何涉及到模糊(blur)的过程(例如:shadow属性),就CSS而言这些属性之间看上去没什么性能上的差异,但实际在Paint阶段是区别还是很明显的。
composite阶段是将Paint过程中的内容汇集起来显示在屏幕上。
这个过程中主要有两个影响页面性能的关键因素:一个是需要整合的合成层(compoaitor layers)数量,另一个是用于动画的相关属性
TL;DR
渲染过程中最好的情况是避免触发Layout和Paint只需要合成(compositing)阶段处理变更。要做到这一点只需要一直使用只通过合成器处理的属性即可。(只有transform和opacity属性可以做到)
Postion transform: translate(npx,npx);
Scale transform: scale(n);
Rotation transform: rotate(ndeg);
Skew transform: skew(X|Y)(ndeg);
Matrix transform: matrix(3d)(...);
Opacity opacity: 0 <= n <= 1
使用transform和opacity的注意点是对应元素要在自身的合成图层,如果没有自身图层就要创建一个图层。这里涉及到创建图层和硬件加速的内容可以参考【关于will-change属性你需要知道的事】
通过提升或创建图层有助于性能的提升这个技巧诱惑力是大的,所以有可能就会写出如下代码:
* {
will-change: transform;
transform: translateZ(0);
}
如同在 【关于will-change属性你需要知道的事】里提到的这种做法非但不能带来性能上的提升,反而会占用过多系统资源,对CPU和GPU都会带来额外的负担。
最后和我们之前提到的类似Chrome DevTools提供了供开发者查看页面图层的工具,可以看到当前页面上有多少层级,每个层级的大小、渲染的次数以及合成的原因等等,我们可以通过这些信息去分析和做对应的优化。
处理用户输入也是潜在的可能会影响性能的因素,因为其可能会阻塞其他内容的加载并且导致不必要的布局(layout)工作
TL;DR
页面交互最快的情况是,当用户与页面交互时,页面的合成器线程接受用户的触摸输入并将内容四处移动。这个过程不需要与主线程通信,而是直接提交给GPU处理。所以不需要等待主线程对JS的处理、以及布局(layout)、绘制(paint)等操作完成。
但是,如果附加了输入处理程序(如touchstart,touchmove,或者touchend)后,合成器线程必须等待该处理程序执行完毕,因为有可能调用了preventDefault()来阻止触摸滚动事件的发生。即使没有调用preventDefault(),合成器也必须等待其执行完毕,这样用户的滚动操作就被阻止就可能导致帧丢失从而引起卡顿。
总而言之,你应该确保运行的所有输入处理程序都快速执行,并允许合成器执行其工作。
输入处理程序被安排在requestAnimtionFrame回调之前运行。如果在这个处理程序中做样式上的修改,那么在requestAnimationFrame开始处有需要更改的样式处理,这会触发强制同步布局。
上面两个问题的解决方案是相同的:你应该对下一个requestAnimationFrame的回调中做样式更改的情况做防抖处理。
function onScroll(evt){
lastScrollY = window.scrollY;
if(scheduleAnimationFrame)
retun;
scheduleAnimationFrame = true;
requestAnimationFrame(readAndUpdatePage);
}
window.addEventListener('scroll',onScroll);
这样做还有一个好处,就是保持输入处理程序的轻量,因为这样就不会阻塞比如滚动等其他操作。
对于iPhone X等全面屏的适配问题,IOS11提出了一个安全区域的概念,安全区域指的是一个可视窗口范围,处于安全区域的内容不受圆角(corners)、齐刘海(sensor housing)、小黑条(Home Indicator)影响,要做好适配,必须保证页面可视、可操作区域是在安全区域内
对于适配ios全面屏,ios对现有 viewport meta 标签的一个扩展,用于设置网页在可视窗口的布局方式,可设置三个值:
【注意:网页默认不添加扩展的表现是 viewport-fit=contain,需要适配 iPhoneX 必须设置 viewport-fit=cover,这是适配的关键步骤】
iOS11 新增特性,Webkit 的一个 CSS 函数,用于设定安全区域与边界的距离,有四个预定义的变量
【当 viewport-fit=contain 时 env() 是不起作用的,必须要配合 viewport-fit=cover 使用。对于不支持env() 的浏览器,浏览器将会忽略它】
同时为了向后兼容ios11.2~ios11.0 需要使用关键字 constant 而不是env
css-attribute: constant(safe-area-inset-bottom); /* 兼容 iOS < 11.2 */
css-attribute: env(safe-area-inset-bottom); /* 兼容 iOS >= 11.2 */
注意:env() 跟 constant() 需要同时存在,而且顺序不能换
更多内容可以参考文档:webkit.org
因为JavaScript中没有块级作用域这一概念 而使用let 则可以达到这一效果
let允许把变量作用域限制在块级域中 与var不同的是:var申明要么是全局的 要么是函数级的 而 无法是块级作用域
let作用域 是 块级 var 作用域是函数
for(var i = 0;i<5;i++){
console.log(i); // 0, 1, 2, 3, 4
}
console.log(i) // 5
for(let i = 0; i< 5;i++){
console.log(i); // 0, 1, 2, 3, 4
}
console.log(i) // i is not defined
其他: let 声明会提升到块顶部 ,从块顶部到该变量的初始化语句,这块区域叫做 TDZ(临时死区)
如果你在 TDZ 内使用该变量,JS 就会报错
很多语言中都有块级作用域 JavaScript中使用var 声明变量 以function来划分作用域 用大括号 "{}"
无法限制var的作用域 使用var声明的变量据欧变量提升(declaration hoisting)的效果
ES6中增加的let 在 {} ,if for 里声明 用法与var 相同 但是作用域限定在块级 let声明变量不存在变量提升
'use strict';
function f1(){
var a = 1;
let n = 2;
if(true){
var a = 20;
let n = 10;
}
console.log(n); // 2
console.log(a); // 20
}
f1();
const 用来声明和创建一个常量
const num = 10;
num = 20;
console.log(num); //10
const num = 10;
var num = 20;
console.log(num); // 'num' has already been declared
const C = {};
C.a = 1;
console.log(C.a) // 1
const D = Object.freeze({});
D.a = 2; // Error
console.log(D.a) // undefined
类声明式定义类的一种方式 使用class关键字后跟一个类名就可以定义一个类
'use strict';
class Polygon{
constructor(height,width){
this.height = height;
this.width = width;
}
}
类声明与函数声明不同的一点是 函数声明存在变量提升现象 而类声明不存在 也就是说 必须先声明类 才能使用 否则会抛出异常
var p = new Polygon(); // ReferenceError
class Polygon{}
"use strict"
class Person{
constructor(name){
this.name = name;
}
sayName(){
console.log("My Name is "+this.name);
}
}
var l= new Person("Lily");
l.sayName(); // "My Name is Lily"
类表达式是另外一种方式 就像函数表达式一样 在类表达式中 类名是可有可无的 如果定义了类名 则该类名只有在类体中才可以访问到
"use strict";
// 匿名函数表达式
var Polygon = class {
constructor(height,width){
this.height = height;
this.width = width;
}
};
// 命名函数
var Polygon = class Polygon{
constructor(height,width){
this.height = height;
this.width = width;
}
};
类成员包括类构造器和类方法(包括静态方法和实例方法)
class 根据constructor方法来创建和初始化对象
constructor方法是类的默认方法 通过new 命令生成对象实例时 自动调用该方法 一个类只能有一个constructor方法 如果没有显示定义就会被隐式添加
constructor(){}
constructor默认返回实例对象(即this)完全可以指定返回另一个对象
"use strict"
class Foo{
constructor(){
return Object.create(null);
}
}
new Foo() instanceof Foo // false
constructor 方法是一个特殊的类方法 它既不是静态方法也不是实例方法 它仅会在实例化一个类的时候被调用 一个类只能有一个名为constructor属性的方法 否则 会抛出SyntaxError异常
static关键字定义两个类的静态方法 静态方法被称为无需实例化类也可以当类被实例化 静态方法通常用于为应用程序创建实用函数
"use strict";
class Point{
constructor(x,y){
this.x = x;
this.y = y;
}
static distance(a,b){
const dx = a.x - b.x;
const dy = a.y - b.y;
return Math.sqrt(dx*dx+ddy*dy);
}
}
const p1 = new Point(5,5);
const p2 = new Point(10,10);
console.log(Point.distance(p1,p2));
extends 关键字可以用来创建继承于某个类的子类
栗子:根据Animal 类创建一个Dog类
class Animal{
constructor(name){
this.name = name;
}
speak(){
console.log(this.name+" makes a noise");
}
}
class Dog extends Animal{
speak(){
console.log(this.name +" barks");
}
}
var dog = new Dog("Mini");
dog.speak(); // "Mini barks"
map对象是一个简单的键值 任何值(包括对象和原始值)都可以作为一个键或者一个值
var m = new Map();
var o = {p:"Hello World"};
m.set(o,"content");
m.get(o) // "content"
console.log(m) // Map { { p: 'Hwll' } => 'content' }
上面代码使用set方法 将对象o作为m的一个键
Map也接受一个数组作为参数 该数组的成员是一个个表示键值对的数组
var map = new Map([["name","zhangsan"],["title","Author"]]);
map.size // 2
map.get("name") // "zhangsan"
map.get("title") // "Author"
注意Map的键实际上是和内存地址绑定的 知道内存地址不一样就视为两个键
var myMap = new Map();
myMap.set(NaN, "not a number");
myMap.get(NaN); // "not a number"
var otherNaN = Number("foo");
myMap.get(otherNaN); // "not a number"
两个NaN作为Map的键来说是没有区别的
size属性返回Map结构的成员总数 即返回映射对象中的键/值对数目
**set(key,val)**属性设置key对应的键值 然后返回整个Map结构 如果key已有值 则覆盖原有值 否则就生成这个键值
var m = new Map();
m.set("edition",6);
m.set(272,"ozil");
m.set(undefined,"nah");
set方法返回的是Map本身 因此可以采用链式写法
var m = new Map();
m.set("a",1);
m.set("b",2);
m.set("keys",3);
m.has("keys") // true
m.delete("a")
console.log(m) // Map(2) {"b" => 2, "keys" => 3}
m.clear() // {}
var myMap = new Map();
myMap.set(0,"zero");
myMap.set(1,"one");
maMap.set(2,"two");
for(var key of myMap.keys()){
console.log(key);
}
// 0 1 2
for(var item of myMap.entries()){
console.log(item);
}
// [0, "zero"] [1, "one"] [2, "two"]
myMap.forEach(function(val,key){
console.log(key+ "="+val);
},myMap)
// 0=zero 1=one 2=two
WeakMap结构与Map结构基本类似 唯一的区别是它只接受对象作为键名(null除外)不接受其他类型的值作为键名 而且键名所指向的对象 不计入垃圾回收机制
var map = new WeakMap();
map.set(1,2);
// TypeError: 1 is not an object
map.set(Symbol(),2);
// TypeError:Invalid value used as weak map key
WeakMap的设计目的在于,键名是对象的弱引用(垃圾回收机制不将该引用考虑在内),所以其所对应的对象可能会被自动回收。当对象被回收后,WeakMap自动移除对应的键值对。
典型应用是,一个对应DOM元素的WeakMap结构,当某个DOM元素被清除,其所对应的WeakMap记录就会自动被移除。基本上,WeakMap的专用场合就是,它的键所对应的对象,可能会在将来消失。WeakMap结构有助于防止内存泄漏。
WeakMap与Map在API上的区别主要是两个,一是没有遍历操作(即没有key()、values()和entries()方法),也没有size属性;二是无法清空,即不支持clear方法。这与WeakMap的键不被计入引用、被垃圾回收机制忽略有关。
vm = new Vue({ /*创建一个Vue实例 */})
概念 :当Vue实例被创建时,向Vue响应式系统(reactivity system)中加入其对象中所有能找到的属性,当这些属性发生变化时,视图将会更新匹配最新值【只有在实例被创建时存在的属性是响应式的】
指令(Directives) : 带有v-
前缀的特殊特性,作用是当表达式的值改变时,将其产生的连带影响响应式的作用于DOM
参数 : 一些指令能够接收一个"参数"
beforCreate —— created —— beforeMount —— mounted —— beforeUpdate —— updated—— beforeDestroy—— destroyed
beforeCreate ——> 实例初始化后 数据观测和event/watcher事件配置之前被调用
created ——>实例创建完后立即调用,挂载阶段未开始
beforeMount ——> 挂载开始之前被调用,相关的render函数首次被调用【服务器端渲染时不被调用】
mounted ——>已挂载到实例上
beforeUpdate ——> 数据更新时调用 未更新前
updated ——> 组件已更新
activated ——> keep-alive组件激活时使用
deactivated——> keep-alive组件停用时调用
beforeDestory ——> 实例销毁之前,实例仍可用
destroyed——> 实例销毁后调用【子组件被销毁,事件监听器被移除,解除其他绑定】
errorCaptured ——> 当捕获到子孙组件错误时被调用,返回false阻止错误向上传播
使用"{{ }}"
语法 做文本插入
<span>Message: {{msg }}</span>
使用指令v-html
插入原生html代码
<p>someting :<span v-html='rawHtml'></span></p>
Mustache语法不能作用在HTML特性上,需要使用v-bind
指令
<div v-bind:id="bindId"></div>
<div v-bind:disable="bool"></div>
v-bind
v-on
的缩写
<a v-bind:href="url">...</a>
<!--缩写-->
<a :href="url">...</a>
<a v-on:click="doSomething">...</a>
<!---缩写--->
<a @click="doSometing">...</a>
通过 v-bind:class
一个对象,动态切换class
,也可以与普通的class属性共存
<div v-bind:class="{active:isActive}"></div>
<div
class="static"
v-bind:class="{active: isActive}"
></div>
可以将一个数组传给 v-bind:class
应用一个class列表
<div v-bind:class="[activeClass, errorClass]"></div>
data:{
activeClass: 'active',
errorClass: 'text-danger'
}
也可以在其中使用三目运算符,也可以在其中使用对象语法
<h1 v-if="ok">Yes</h1>
<h1 v-else>No</h1>
在<template>
元素上使用v-if
条件渲染分组
v-else
v-else-if
必须紧跟在v-if
或者 v-else-if
元素后面,否则不被识别
v-show
根据条件展示元素的指令【原理是切换display的值,始终保留在DOM中】
<h1 v-show="ok">Hello</h1>
如果非常频繁的切换则使用v-show
如果运行时条件很少改变则使用v-if
v-if
v-for
一起使用时,v-for
比 v-if
优先级更高
<ul id="example-1">
<li v-for="item in items">
{{item.message}}
</li>
</ul>
var example1 = new Vue({
el: '#example-1',
data: {
items:[
{message: '1'},
{message: '2'}
]
}
})
v-for
中第二个可选参数值为当前项的索引,也可以用v-for
迭代一个对象属性
<div v-for="(value, key, index) in object">
{{index}}.{{ key}}: {{value}}
</div>
<div v-for="item of items" :key="item.id">
<!--context--->
</div>
变异方法【会修改原数组】 push pop unshift shift splice sort reverse
Vue无法监测到如下数组的变动
vm.item[indexOfItem] = newValue
vm.items.length = newLength
解决直接设置值的问题可以利用如下方法
Vue.set(vm.items, indexOfItem, newValue)
vm.items.splice(indexOfItem, 1, newValue)
vm.$set(vm.items, indexOfItem, newValue)
//vm.$set是全局Vue.set方法的别名
解决修改数组长度的问题可以使用splcie
:
vm.items.splice(newLength)
当需要显示一个数组的过滤或者排序副本时,可以选择非变异方法的计算属性
<li v-for="n in evenNumbers">{{n}}</li>
data : {
numbers: [1,2,3,4]
},
computed:{
evenNumbers: function(){
return this.numbers.filter(function(number){
return number % 2 === 0
})
}
}
计算属性不适应的情况下(例如在嵌套v-for
循环中)可以使用methods方法:
<li v-for="n in even(numbers)">{{n}}</li>
data: {
numbers: [1,2,3,4]
},
methods: {
even: function(numbers){
return numbers.filter(function(number){
return number % 2 === 0
})
}
}
Vue无法直接检测到对象属性的添加或删除,可以使用```Vue.set(object, key, value)``方法向对象添加响应式属性
var vm = new Vue({
data: {
userProfile: {
name: 'Anika'
}
}
})
//添加一个新的age属性
Vue.set(vm.userProfile, 'age',27)
// vm.$set(vm.userProfile, 'age, 27')
当需要为已有对象赋新属性时,添加响应式属性
vm.userProfile = Object.assign({}, vm.userProfile, {
age: 29,
favoriteColor: 'red'
})
// don't do that
Object.assign(vm.userProfile,{
age: 29,
favoriteColor: 'red'
})
用v-on
监听DOM事件,v-on
也可以接收一个需要调用的方法名称。需要在内联语句处理器中访问原始的DOM事件,可以用特殊的变量$event
传入methods
<div id="example-1">
<button v-on:click="counter+=1">Add One</button>
<button v-on:click="greet">Greet</button>
<button v-on:click="say('hi')">Greet</button>
<button v-on:click="warn('omg',$event)">Greet</button>
</div>
var example = new Vue({
el: '#example-1',
data:{
counter: 0,
name: 'Vue'
},
methods:{
greet:functino(event){
console.log(this.name);
if(event){
console.log(event.target.tagName);
}
},
say:function(hi){
console.log(hi)
},
warn:function(msg, event){
if(event) event.preventDefault()
console.log(msg);
}
}
})
.stop .prevent .capture .self .once .passive
<!-- 阻止表单事件继续传播 -->
<a v-on:click.stop="doThis" ></a>
修饰符顺序执行 .passive
不要和 .prevent
一起使用后者会被忽略
<!--只有key是enter时调用-->
<input v-on:keyup.enter="submit" />
<!-- Alt+C -->
<input @keyup.alt.67="clear">
<!-- Ctrl+Click -->
<div @click.ctrl="doSomething">
Do Something
</div>
v-model指令在<input /><textarea> <select>
元素上创建双向数据绑定
v-model在内部使用不同的属性为不同的输入元素抛出不同的事件
<input v-model="message" />
<p>{{message}}</p>
组件是可复用的Vue实例,类似于react中的自定义Component
Vue.component('button-counter,{
data: function(){
return {
count: 0,
}
},
template:'<button v-on:click="count++">Clicked{{count}}</button>'
})
组件中data必须是一个函数,每个实例可维护一份被返回对象的拷贝
组件通过prop向子组件传值
Vue.component('blog-post',{
props:['title],
template:'<h3>{{title}}</h3>'
})
<blog-post title="Something"></blog-post>
父组件可以通过v-on监听子组件实例的任意事件,同时子组件可以通过内建方法$emit方法并传入事件名称来触发一个事件
<blog-post
...
v-on:enlarge-text="postFontSize+=0.1"
></blog-post>
<button v-on:click="$emit('enlarge-text',0.1)">Enlarge Text</button>
<input v-model="searchText" />
相当于 ====>
<input
v-bind:value="searchText"
v-on:input="searchText = $event.target.value"
/>
prop使得父子组件之间形成了一个单向下行绑定
props的类型检查类似react的TypeProps
监听原生事件可以v-on 的 .native修饰符
<base-input v-on:focus.native="onFocus"></base-input>
有时候.native不会预期的被调用Vue提供了$listeners属性,包含作用在这个组件上的所有监听器
父模板里的内容实在父作用域中编译的,子模板是在子作用域中编译的
<a
v-bind:href="url"
class="nav-link">
<solt></solt>
</a>
<navigation-link url='/profile'>
{{ url}} <!-- undefined -->
</navigation-link>
slot有一个name属性,可以用来定义额外的插槽
<div>
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
<base-layout>
<template v-solt:header>
<h1></h1>
</template>
<p></p>
<template v-slot:footer>
<p></p>
</template>
</base-layout>
通过bind可以将元素特性绑定,使父级作用域可访问
具名插槽的缩写将v-slot: 替换为 # 例如
v-slot:header ==>> #header
必须在有参数的情况下才能使用
缓存初次创建的组件实例
<keep-alive>
<component v-bind:is="currentComponent"></component>
</keep-alive>
利用.$root 获取跟组件的数据
.$parent属性可以从子组件访问父组件实例
Computed
属性【对于复杂的逻辑 可以使用computed
属性】
区别是computed
是可以缓存的,只在相关依赖发生变化时才会重新求值而methods
每次渲染时都会求值
watch
【监听响应数据变化】:观察动作,监听props
$emit
或者组件自身的异步操作 无缓存性
Vue数据修改后不会立即更新,Vue中的DOM更新是异步的,只要观察到数据变化Vue将开启一个任务队列并缓冲在同一事件循环中发生的所有数据变化,如果同一个watcher被多次触发,只会被推入到队列中一次,在下一次事件循环tick中,Vue刷新队列,并实行实际工作,在created钩子函数执行DOM相关操作时,将其放在Vue.nextTick中去执行
感觉有些类似于EvenLoop的事件循环机制
vm.msg = 'hello'
// DOM未更新
Vue.nextTick(function(){
// DOM已更新
})
在同一事件循环中,当同步执行操作完成后再调用nextTick
同步执行数据全部更新完毕后DOM开始渲染,同一事件中存在多个nextTick则依次执行
assets ----------------用于组织未编译的静态资源如less,sass,js
components ----------用于组织应用的Vue.js组件 Nuxt.js不会扩展该目录
layouts ---------------用于组织应用布局组件
middleware -----------存放中间件
pages -----------------组织应用的路由及视图,Nuxt.js读取该目录下的Vue文件,并生成路由配置
plugins ----------------组织需要在根vue.js应用实例化之前需要运行的Js插件
static ------------------用于存放静态文件,此类文件不会被Nuxt.js构建编译处理,服务器启动时该目录文件会映射到根路径 / 下
sotre ------------------组织应用的Vuex状态时树
nuxt.config.js ---------组织Nuxt.js应用的个性化设置覆盖默认配置
Nuxt.js中定义带参数的动态路由需要创建对应的以下划线为前缀的文件或目录,可在动态路由中定义参数校验
export default{
validate({params}){
Return /^\d+$/.test(params.id) // 必须为number类型
}
}
asyncData方法在组件每次加载之前被调用,第一次参数设定为当前页面的上下文对象,asyncData返回的数据融合组件data方法后将数据传给组件【初始化前被调用无法使用this引用组件实例】
// 上下文例子
export default{
async asyncData({req, res}){
if(process.server){
return {host: req.headers.host}
}
return {}
}
}
// 访问动态路由数据
export default{
async asyncData({params}){
return {slug : params.slug}
}
}
不需要通过Webpack处理的静态资源文件放在static文件目录
<!--- 编译前 -->
<template>
<img src="~/assets/xxx.png" />
</template>
// 编译后
createElement('img', {attrs: {src: require('~/assets/xxx.png')}})
实例化Vue.js应用程序之前运行js插件【在Vue组件的生命周期中,只有beforeCreate和created这两个方法会在客户端和服务端被调用起亚生命周期函数仅在客户端被调用】
// plugins/vue-notifications.js
import Vue from 'vue'
import VueNotifications from 'vue-notifications'
Vue.use(VueNotfications)
// nuxt.config.js
module.exports = {
plugins: ['~/plugins/vue-notifications']
}
主要用于扩展可信功能并添加无限的集成
使用Vuex管理state
首先需要将状态导出为函数,将变量和操作作为文件对象导出
//store/index.js
export const state = ()=>({
counter:0
})
export const mutations = {
increment(state){
state.counter++
}
}
// store/todos.js
export const state = ()=>({
list : []
})
export const mutations = {
add(state, text){
state.list.push({
text:text,
done: false
})
},
remove(state,todo){
state.list.splice(state.list.indexOf(todo),1)
},
toggle(state, todo){
todo.done = !todo.done
}
}
// 生成后
new Vuex.Store({
state: ()=>({
counter: 0
}),
mutations:{
increment(state){
state.counter++
}
},
modules:{
todos:{
namespcaed: true,
state:()=>({
list: []
}),
mutations:{
add(state, {text}){
state.list.push({
text,
done: false,
})
},
remove(state,{todo}){
state.list.splice(state.list.indexOf(todo),1)
},
toggle(state,{todo}){
todo.done = !todo.done
}
}
}
}
})
将模块文件分解为单独的文件:state.js actions.js,mitations.js, getter.js 【注意:拆分文件模块时,必须使用箭头函数功能使得this在此法可用】
插件【可将其他插件添加到store(模块模式下),将其放入store/index.js】
import myPlugin from 'myPlug'
export const plugins = [myPlugin]
export const state = ()=>({
counter: 0
})
export const mutations = {
increment(state){
state.counter++
}
}
fetch会在渲染页面前被调用作用是填充store数据 ,与asyncData方法类似不同的是它不会设置组件的数据
let str = 'qwert&code=1243&qerfvs=6577';
str.replace(/(?<=code=)\d+/,'4321'); // "qwert&code=4321&qerfvs=6577"
"(?<!(T|t)he\s)(cat)" => The cat sat on cat.
"(T|t)he(?=\sfat)" => The fat cat sat on the mat.
// 匹配 紧跟fat的 The或the
"(T|t)he(?!\sfat)" => The fat cat sat on the mat.
// 匹配文本后面不紧跟fat的The 或the
我们创建每一个函数 都有一个prototype属性 该属性指向一个对象 这个对象就是原型
我们创建一个对象时候 可以根据自己的需求 选择性的将一些方法属性通过prototype属性 挂载在原型对象上 而每一个new出来的实例都有一个__proto__属性 该属性指向构造函数的原型对象 通过这个属性 让实例方法能够访问 到原型对象上的这方法
当函数可以记住并访问所在的词法作用域时,就产生了闭包,这个函数持有对该词法作用域的引用,这个引用就叫做闭包
闭包本质还是函数,只不过这个函数绑定了上下文环境(函数内部引用的所有变量)
缺点:常驻内存,会增大内存使用量,使用不当很容易造成内存泄露。
作用(使用场景):可以用来管理私有变量和私有方法,将对变量(状态)的变化封装在安全的环境中,使得这些变量不能被外部随意修改,同时又可以通过指定的函数接口来操作。
闭包有三个特性:
1.函数嵌套函数
2.函数内部可以引用外部的参数和变量
3.参数和变量不会被垃圾回收机制回收
function Animals(){
this.name = ['cat','dog'];
this.other = 'animals';
}
function Cat(){
};
// 继承
Cat.prototype = new Animals();
var c1 = new Cat();
var c2 = new Cat();
c1.name.push('pig');
console.log(c1.name); // ["cat", "dog", "pig"]
console.log(c2.name); // ["cat", "dog", "pig"]
c1.other = 'cat';
console.log(c1.other); // cat
console.log(c2.name); // animals
注:通过原型链实现继承时 不能使用对象字面量创建原型方法 因为会重写整个原型链
function Animals(){
this.name = ['cat','dog'];
this.other = 'animals';
}
function Cat(){
Animals.call(this);
};
var c1 = new Cat();
var c2 = new Cat();
c1.name.push('pig');
console.log(c1.name); // ["cat", "dog", "pig"]
console.log(c2.name); // ["cat", "dog"]
function Animals(name){
this.name = name;
this.animals = ['cat','dog'];
}
Animals.prototype.sayName = function(){
console.log(this.name);
}
function Cat(name,age){
// 属性继承
Animals.call(this,name);
this.age = age;
}
Cat.prototype = new Animals();
Cat.prototype.constructor = Cat();
Cat.prototype.sayAge= function(){
console.log(this.age);
};
var c1 = new Cat('Cat1',18);
c1.animals.push('pig');
console.log(c1.animals); // ["cat", "dog", "pig"]
c1.sayName(); // Cat1
c1.sayAge(); // 18
var c2 = new Cat('Cat2',19);
console.log(c2.animals); // ["cat", "dog"]
c2.sayName(); // Cat2
c2.sayAge(); // 19
创建一个仅用于封装继承过程的函数 该函数在内部以某种方式来增强对象 最后返回对象
function Animals(orginal){
var c = Object(orginal);
c.sayHi = function(){
console.log('Hi');
}
return c;
}
var c = {
name:'cat',
others:['cat1','cat2']
}
var cat = Animals(c);
cat.sayHi(); // 'Hi'
基本思路是:不必为了指定子类型的原型而调用父类型的构造函数 我们只需要的是一个父类原型的副本 本质上就是使用寄生式继承来继承父类的原型 然后讲结果指定给子类型的原型
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
function inheritPrototype(child, parent) {
var prototype = object(parent.prototype);
prototype.constructor = child;
child.prototype = prototype;
}
// 继承
inheritPrototype(Child, Parent);
众所周知,微前端是一种架构方式,与后端的微服务可以说是一脉相承。其核心**就是"分而治之"。简单来说就是讲某单体应用解耦拆分为若干个子应用,这些子应用可以独立运行、开发、部署、和维护、且互不影响。且各个子应用可以选择不同的技术栈开发等等。目前在项目中有所尝试,下面内容就以实际业务为基础的微前端实践
说之前先来分析一下为什么需要微前端?
背景分析: 项目中的子应用越来越多,其中部分功能越来越复杂,加之系统开发中碰到的问题越来越多
开发过程中的痛点:
可以实现微前端的方案有很多,比较常见的一些微前端实现方案比如:iframe、Module Federation、动态路由等
iframe : 实现简单;可同时挂载多个应用;天然支持隔离,但是存在路由不同步,刷新页面状态丢失,通信困难只能通过postMessage等方式比较麻烦。
动态路由: 常见的实现方式比如single-spa 、qiankun等,通过统一维护所有应用的注册加载,挂载和卸载,针对不同技术栈的应用做聚合。但是通讯难,接入成本高。
Module Federation: 依赖webpack5,存在加载第三方依赖、版本控制等问题,会增加试错成本。
考虑到若选用上述方式,则会对项目本身带来较大影响,并且改造成本太高。 结合项目现状和改造成本,决定通过借鉴动态路由的**实现通过以React为基座的特定动态路由的方案。对于动态路由的方式来说,关键部分就是注册、加载、挂载与卸载,由于我们的技术栈都是React,而且react-router自身就具备对组件挂载和卸载的能力。利用这一点优势,将所有子应用作为一个component,将挂载卸载的工作交由react-router来完成,也不用每次切换路由都重新初始化整个子应用,用户体验和单页应用保持一致。
todo
基座:从配置文件中读取配置信息,在路由注册层注册信息。当路由匹配到子应用后,请求获取到子应用的打包文件,由react-router进行加载和卸载的控制
子应用: 导出umd资源,上传到服务器,更新配置文件中对应的子应用信息
整个执行流程如图所示
todo
在基座项目里创建子应用配置文件subApp.js
// container/scr/subApp.js
const app = [
{
name: 'subApp', // 子应用名称
host: process.env.REACT_APP_SUBAPP_HOST, // 子应用的静态资源地址
appName: 'App', // 导出的应用名 默认为'App'
},
...
]
引入subApp.js配置信息,通过AppManager进行子应用管理
// container/src/index.js
function renderApp () {
apps.forEach(AppManager.registerApp)
ReactDOM.render(<App />,document.getElementById('root'));
}
在AppManager中维护子应用信息,及加载子应用的loadSubApp函数,loadSubApp函数获取到子应用的资源文件地址后,通过添加script标签的方式加载资源,并将返回的资源组价挂载到window上
// container/src/AppManager.js
class AppManager {
subApps = {}
subAppModules = {}
registerApp = (subApp) => {
this.subApps[subApp.name] = subApp
}
loadSubApp = (subAppInfo) => {
const { name, host } = typeof subAppInfo === 'string'? this.subApps[subAppInfo] : subAppInfo
if (!this.subAppModules[name]) {
this.subAppModules[name] = new Promise((resolve, reject)=> {
fetch(`${host}/${name}/asset-manifest.json`)
.then(res => res.json())
.then(manifest => {
const script = document.createElement('script');
script.src = `${host}${manifest.files['main.js']}`;
const timeout = setTimeout(()=>{
console.error(`MicroApp ${name} timeout`);
reject(new Error(`MicroApp ${name} timeout`));
}, 10000)
script.onload = () => {
clearTimeout(timeout)
const app = window[name]
console.log(`MicroApp ${name} loaded success`);
resolve(app)
}
script.onerror = (e) => {
clearTimeout(timeout);
console.error(`MicroApp ${name} loaded error`, e);
reject(e)
}
document.body.appendChild(script);
// 加载css
const link = document.createElement('link')
link.rel = 'stylesheet'
link.href = `${host}${manifest.files['main.css']}`
document.head.appendChild(link)
})
})
}
return this.subAppModules[name]
}
}
todo
何时使用:
Quick Checklist for Choosing HTTP GET or POST
Use GET if:The interaction is more like a question (i.e., it is a safe operation such as a query, read operation, or lookup).
Use POST if:The interaction is more like an order, or The interaction changes the state of the resource in a way that the user would perceive (e.g., a subscription to a service), or The user be held accountable for the results of the interaction.However, before the final decision to use HTTP GET or POST, please also consider considerations for sensitive data and practical considerations.
将多维数组转化为一维数组(类似_Underscore.js中的_flatten()函数)
var flatten = function(arr) {
var res = [], idx = 0, val;
for (var i = 0, length = arr && arr.length; i < length; i++) {
val = arr[i];
if (val && val.length >= 0 && Array.isArray(val)) {
val= flatten(val);
var j = 0, len = val.length;
res.length += len;
while (j < len) {
res[idx++] = val[j++];
}
} else {
res[idx++] = val;
}
}
return res;
}
flatten([1, [2], [3, [[4]]]]) //[1,2,3,4]
如果仅是字符串数组 则可用
arr = arr.join(",").split(",")
["1", ["2"], ["3", [["4"]]]].join(",").split(",") // ["1", "2", "3", "4"]
递归
function flatten(arr){
var res = [];
for(var i=0;i<arr.length;i++){
if(Array.isArray(arr[i])){
res = res.concat(flatten(arr[i]));
}else{
res.push(arr[i]);
}
}
return res;
}
利用reduce
function flatten(arr){
return arr.reduce(function(prev,item){
return prev.concat(Array.isArray(item)?flatten(item):item);
},[]);
}
<div class="box">
<div class="left"></div>
<div class="right"></div>
</div>
.box{ display: relative }
.left{ position: absolute; width:100px }
.right{ margin-left:100px }
.box{display:flex;}
.left{width:100px; }
.right { flex:1;}
<div class="container">
<div class="left"></div>
<div class="center"></div>
<div class="right"></div>
</div>
.container { display: flex }
.container>div { height: 200px }
.left { width: 300px }
.center { flex: 1 }
.right { width: 300px }
<div class="left"></div>
<div class="right"></div>
<div class="center"></div>
</div>
.container>div{
height: 200px;
}
.left {
float: left;
width: 300px;
}
.right {
float: right;
width: 300px;
}
.center {
overflow: hidden;
}
<div></div>
div { width:x;
height: y;
position: absolute;
left:50%;
top:50%;
margin-left:-x/2px;
margin-top:-y/2px;
}
div {
width:x;
height: y;
position: absolute;;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
}
div {
width:x;
height: y;
position: absolute;;
top: calc(50% - x/2);
left: calc(50% - y/2);
}
div {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
<div class="box">
<div></div>
</div>
.box {
display: flex;
justify-content: center;
align-items: center;
}
<div class="red blue">okokok</div>
<div class="blue red">okokok</div>
.blue { color: blue }
.red { color: red }
优先级是由类在css文件中定义的顺序决定
不管class中类名顺序如何改变都会展示css中靠后的样式
function green(){
console.log('green');
}
function yellow(){
console.log('yellow');
}
function red(){
console.log('red');
}
function light(fn,delay){
return new Promise((resolve,reject)=>{
setTimeout(function(){
fn();
resolve();
},delay);
})
}
const start = function(){
Promise.resolve()
.then(()=>light(green,3000))
.then(()=>light(yellow,2000))
.then(()=>light(red,1000))
.then(()=>start())
}
start()
function sleep(delay){
return new Promise(resolve=>{
setTimeout(resolve,delay);
})
}
const fn = async _ =>{
let pre = new Date()
console.log(pre);
await sleep(2000);
let after = new Date();
console.log(after - pre);
}
console.log('a');
async function async1 () {
console.log('b');
await async2();
console.log('c');
}
async function async2(){
console.log('d');
}
async1();
setTimeout(()=>{
console.log('e');
})
new Promise((resolve)=>{
console.log('f');
resolve();
}).then(()=>{
console.log('g');
})
console.log('h');
// a b d f h c g e
如今大部分设备的刷新频率数60fps,什么意思呢?意思就是每秒屏幕刷新60次。举个例子:页面上出现动画或者渐变的效果,又或者用户滚动页面,那么浏览器渲染动画或页面的每一帧的频率也需要跟设备屏幕的刷新率保持一致。每帧的预算时间是16.66ms,这个时间段中浏览器要处理很多事情,所以最好的情况是在10ms内将所有工作做完,如果超出预算时间,那么帧率会下降,就会出现常见的卡顿现象,对用户体验带来负面影响
要想在预期时间内完成页面更新则主要有5个关键点需要关心,这些点决定了页面的渲染时间
这些部分都是很重要的,处理不当就会导致页面出现卡顿情况,所以为了做好这一点必须要确切的知道你的代码到底会影响渲染的哪个阶段
JS/CSS ➔ Style ➔ Layout ➔ Paint ➔ Composite
如果修改元素的"layout"属性,即改变元素的几何属性(例如宽高,或位置),那么浏览器必须检查其他所有元素并重新计算,受到影响的部分要重新绘制,最终进行合成。所以要重走整个过程
JS/CSS ➔ Style ➔ Paint ➔ Composite
如果修改的内容是属于“Paint”属性(比如:背景图片,文字颜色或阴影),这些不会影响到页面布局,所以浏览器会直接跳过Layout阶段。
JS/CSS ➔ Style ➔ Composite
如果修改的属性既不需要页面重新布局,也不需要重新绘制,那浏览器会跳过Paint和Layout阶段,这种情况开销最低,适合用在动画或滚动的情况
下面来详细说说每个阶段需要注意的问题
JavaScript经常会触发一些页面视觉上改变,有时候是直接改变样式,有时候是更新页面数据,有时候是执行一些动画效果等等。JavaScript运行时间通常是影响性能的关键因素,所以接下来我们可以看一下如何去尽量减小这些因素的影响。
JavaScript的性能分析可能是一门艺术了,因为你所写的JavaScript代码和实际执行时的完全不同。如今的浏览器都采用的是JIT(Just In Time)编译器,同时会使用各种优化技巧以供最快速的执行,但是正因为如此却改变了代码本来的动态性。
如上所言,那么下面会给出一些建议来帮你更好的执行JavaScript代码
TL;DR
requestAnimationFrame
代替setTimeout
或者setInterval
来处理页面上的视觉变化某些情况下在页面视觉上发生变化时,你可能想正好在每一帧开始时执行某些操作,那么requestAnimationFrame是唯一可以准确保证在每一帧执行前执行JS代码的方法
/**
* 作为requestAnimationFrame的回调函数,会在每帧开始前执行
*/
function updateScreen(time){
// ...
}
requestAnimationFrame(updateScreen);
一些框架或者示例可能使用setTimeout或者setInterval来做一些视觉上的变动比如动画,但是问题是无法确定这些回调函数的执行时间点,有可能恰巧是在每帧的结尾,那就可能导致帧丢失,从而导致页面卡顿,这完全不是我们想要的。
实际上jQuery以前也用setTimeout来执行动画,在后来的版本中改用requestAnimationFrame了,如果你还在使用旧版本的话可以检查一下,有必要可以考虑升级。(应该人很少了吧)
JavaScript运行在浏览器的主线程上,与此同时主线程还要执行样式计算,布局,绘制等等。如果JavaScript代码长时间执行则会阻塞这些任务,就可能出现帧丢失的情况。
所以需要考量JavaScript代码的执行时间点和执行时长。举个例子:如果在执行滚动操作,那么理想情况下应该保持JS代码的执行时间保持在3~4ms内,如果超过这个时间,就要考虑采取优化手段了,如果是在空闲时间段那就可以放宽时间限制了。
很多情况下如果不需要访问DOM,就可以把一些纯计算的工作交给Web Worker去执行,对于数据的处理或者搜索排序等等操作都非常适合在这里处理
const dataSortWorker = new Worker('sort-worker.js');
dataSortWorker.postMessage(dataToSort);
// 主线程则可以做其他事情
dataSortWorker.addEventListener('message',(e)=>{
const sortedData = e.data;
// ...
})
也不是所有情况都适合:Web Worker无法访问DOM。在必须在主线程执行的工作,可以考虑采用批处理方法,什么意思呢?就是把较大的任务分割成多个task,每个task不超过几毫秒,并且放在requestAnimationFrame中,让其在每帧的开始去执行。
const taskList = breakBigTaskIntoMicroTasks(monsterTaskList);
requestAnimationFrame(processTaskList);
function processTaskList(taskStartTiem){
let taskFinishTime;
do {
// 假设下一个task已经推到栈里了
const nextTask = taskList.pop();
// 执行下一个task
processTask(nextTask);
//
taskFinishTime = window.performance.now();
} while(taskFinishTime - taskStartTime < 3);
if(taskList.length > 0){
requestAinmationFrame(processTaskList);
}
}
这种处理方法从UI方面考虑,可以加一个进度标识图标以便让用户知晓任务正在执行。不管怎样它都可以保证程序的主线程是空闲状态,因此不会影响用户交互行为。
在评估一个库或者一个框架亦或自己的代码时,逐帧分析JS代码的执行消耗的时间是很必要的。特别是在动画或者一些过渡效果方面时尤为重要。
Chrome DevTools提供的Performance功能是查看JS每帧执行消耗时间的非常好的工具。
通过这个工具提供的信息分析后就可以找出影响性能的原因,如我们之前所提到的,如果在主线程中长时间执行的JS代码是非必要的就可以把它移到Web Worker中来让主线程执行其他任务。【Performance的使用方法】
通过添加和删除元素,更改属性,或者通过动画来改变DOM结构等都会导致浏览器重新计算元素样式,很多情况下都会重新对整个页面或其中部分布局(layout)(或者回流[reflow]),这个过程也叫样式计算。
💡:关于repaint和reflow的区别:repaint 指元素只发生了外观的变化,但是不影响布局,页面只需要做重绘即可。会触发repaint的常见CSS属性比如:outline, visibility, background, 或 color。relfow 则是发生了几何上的变化需要对元素进行重新计算和布局。例如元素的width,height等。
样式计算的第一步就是创建一个与之对应的选择器集合,实际上就是让浏览器确定哪些类哪些伪类选择器和ID该应用于哪个元素,第二步是从匹配的选择器中获取所有样式,并计算最终样式。在Blink(Chrome和Opera的渲染引擎)中这个过程还是相当消耗性能的。
这个过程中渲染引擎大概有50%的时间在匹配选择器,剩下的一半时间在计算最终样式。
TL;DR
我们知道浏览器在解析匹配CSS规则时是从右向左查找匹配的,嵌套越深选择匹配的负担越重,最好不要超过三层。同时浏览器在解析生成页面时分别解析构建DOM Tree 和CSSOM,在DOM树构建完成CSSOM未构建完成时,是不会直接把html放出来的,所以CSS不能太大,否则会有一段白屏时间,所以把字体或者图片转成base64放在CSS里是不太推荐的做法。
有些CSS优化建议说要按照如下优先级书写:
其实这些顺序对浏览器来说是一样的,因为浏览器在解析构建CSS规则时并不立马进行渲染,而是把这些属性值合并、归类、并将最终计算出的属性值放到computedStyle里,之后交由Layout阶段去计算实际显示值,Paint阶段才会去绘制。所以顺序并不会带来性能上的影响,对浏览器而言都是一样的。
关于样式中的计算比较典型一个例子的可能就是对元素使用rgba函数(或者使用calc),当CSS解析时就需要先去执行rgba函数计算颜色,所以直接写成16位的色值更好一些。
参考链接: Rendering Performance
域名收敛即DNS优化
onInput 属性实时监控< input> 或者<textarea>中的value值得变化状况 Object.target.value.length 可以获取文本框中的字符长度
onInput事件类似于 onchange 事件。不同之处在于 oninput 事件在元素值发生变化是立即触发, onchange 在元素失去焦点时触发。另外一点不同是 onchange 事件也可以作用于 < keygen> 和 < select> 元素
关于getComputedStyle() 与style直接获取元素的CSS属性值相比 style仅能获取内嵌在html代码中的css元素属性 而 getComputedStyle则都可以获取 但是是只读属性[针对IE的话要用currentStyle属性] 兼容一下的话:
(element.currentStyle? element.currentStyle : window.getComputedStyle(element, null)).height
getPropertyValue方法可以获取CSS样式申明对象上的属性值(直接属性名) 例如:window.getComputedStyle(element, null).getPropertyValue("float");
一直以来,如何能准确的衡量一个页面从初始化到页面的主要内容已对用户可见的这个过程所耗时长是一个非常棘手的问题。以前的一些指标如load或者DOMContentLoaded都无法准确的描述用户所看到的内容,较新的一些指标如First Contentful Paint(FCP)只能捕获到首次加载时间,如果这个页面的展示的一个进度条或者开机动画,那么这个时间其实就和用户毫无关联。
在过去我们比较推荐的性能指标比如First Meaningful Paint(FMP)和Speed Index(SI)来帮助我们测算在初始渲染后页面内容加载所需的时间,但是这些指标通常都比较复杂很难解释清楚,甚至于有些时候是错误的,所以这就意味着它也不能准确表示到底什么时候页面的主要内容被加载渲染了。就FMP而言,对于不同类型的页面内容FMP其实并不相同,比如一个博客更重要的部分应该是文章的标题和内容简介对用户可见,所以其FMP应该是文本内容,但是对于一些商品站点或者电商而言,图片才是至关重要的关注点,所以它的FMP应该是图片加载完成。所以这也是为什么FMP至今无法被标准化的原因。
现在我们有一种全新的方案来衡量页面主要内容何时加载更准确,这个方案就是查看页面中最大元素的呈现时间,即Largest Contentful Paint (LCP)
Largest Contentful Paint (LCP) 是一个非常重要的用来衡量页面加载速度的指标,它标志着页面的主要内容的加载时长,LCP所需时间越短用户体验越好
LCP这个指标是用来表述在视口中可见的最大内容元素的渲染时间。
根据目前的Largest Contentful Paint API 中规范(草案阶段),如下类型的元素会被考虑:
<img>
元素<svg>
中的<image>
元素<video>
元素(被使用的海报)url()
函数加载(比如 CSS gradients)这个元素的Largest Contentful Paint大小取决于这个元素在可视区内的可见尺寸。如果一个元素延伸到可视区外比如这个元素的溢出部分或者被裁减的不可见部分都不会被计算在内。换言之,所见即所得,可看到最大元素面积多大它的LCP就是多大。
margin、padding、border
这些属性都不会考虑在内。页面通常是分阶段加载的,所以页面上最大的元素可能会发生变化。为了应对这种潜在变化,浏览器在绘制完第一帧后会立即调度一个类型为largest-contentful-paint的PerformanceEntry来标识最大内容元素(Largest contentful element),在后续每帧更新的过程中只要最大内容元素(Largest contentful element)发生变化,就会重新分配一个PerformanceEntry。
很重要的一点是只有当那些已经被渲染并且对用户可见的元素才会被考虑为最大内容元素(Largest Contentful Element),比如还未加载完成的图片就不会被考虑。又比如使用特殊字体的文本节点,这种情况下当较小的元素被当做Largest contentful element的时候,一旦更大的元素完成渲染,就会调度分发一个新的PerformanceEntry对象
除了有延迟加载图像和字体情况外,也可能会在页面有新的内容可用时(available),在DOM中添加新元素,如果有任意新元素比之前的最大内容元素更大,同样的也会调度一个新的PerformanceEntry
如果从页面的DOM结构中移除了某个元素,那么这个元素就不会再被考虑了,类似的如果一个元素关联的image资源发生了变化(比如通过JavaScript改变了img.src属性),那么只有当这个图片加载完成后才会被列入参考范围
一旦用户开始与页面进行交互(点击,滚动或按键),浏览器将停止上报新条目(entries),因为用户交互过程中通常会改变用户可见的内容(例如滚动)
加载时间 vs 渲染时间
出于安全考虑。对于请求头缺少Timing-Allow-Origin的跨域图片,不会显示图片的渲染时间点,只显示加载时间。为了使你的LCP指标更准确推荐尽可能的给加上Timing-Allow-Origin请求头
为了使页面计算和分配新性能条目的性能开销更低,对于元素大小或位置的变更不会生成新的LCP,仅考虑元素的初始大小和在视口中的位置。这意味着可能无法上报最初在屏幕外通过动画显示在屏幕上的图片,这也意味着最初在视口中渲染的元素随后被移出视口范围,但仍将上报其初始时在视口内的大小
但是,(如上所述)如果某个元素已从DOM中删除或与其相关的图像资源发生了变化,则该元素将不会再被当做LCP的候选元素。
如下是几个热门网站加载过程的Timeline及何时触发Largest Contentful Paint:
上面的两个例子的最大元素都随着内容的加载不断变化,第一个例子中新内容被添加到DOM结构中并且成为了最大内容元素。第二个例子中由于布局变化之前的最大内容元素被移出了视口范围。
通常情况下,延迟加载的内容要比页面上已有的内容更大,但也不一定都是这种情况。接下来的两个示例显示了在页面完全加载之前触发了Largest Contentful Paint。
第一个例子中Instagram的logo加载出来相对较早,即便其他内容也逐渐加载完成,它依然是最大元素。第二个例子中最大的元素是一段文本,该文本在其他任一图片或logo加载完成之前就显示了。由于所有单个图片均小于它,所以在整个加载过程中它始终是最大元素。
你可能会注意到了,在第一个例子中的Instagram时间轴的第一帧中,相机logo没有被绿色框框起来。这是因为它是一个<svg>
元素,而 <svg>
元素现在还不被视为LCP候选对象。所以第一个LCP候选对象是第二帧中的那段文本。
在JavaScript中通过使用Largest Contentful Paint API
计算LCP。如下的例子展示了如何通过创建一个PerformanceObserver
来监听largest-contentful-paint
let lcp;
const po = new PerformanceObserver((entryList) => {
const entries = entryList.getEnteries();
const lastEntry = entries[entries.length - 1];
lcp = lastEntry.renderTime || lastEntry.loadTime;
})
po.observe({type: 'largest-contentful-paint', buffered: true });
addEvevtListener('visibilitychangre', function fn(){
if (lcp && document.visiblityState === 'hidden') {
console.log('LCP', lcp);
removeEventListener('visibilitychange',fn, true);
}
}, true);
在某些情况下页面中最重要的元素却不是最大元素,所以对我们来说更重要的元素的渲染时间更有价值,当然这是可以通过Element Timing API 来实现自定义指标
LCP 通常受以下三个因素影响:
如果你的页面是客户端渲染(client-rendered),并且页面的最大内容元素是通过JavaScript添加到DOM中,那么你的脚本解析编译执行的时间都会是影响LCP的原因。
如下给出了一些优化方案:
如下是一些常用的衡量性能的指标(Important metrics to measure)
使用~~向下取整 类似与Math.floor();
~~12.1 // 12
~~12.7 //12
数组去重
[...new Set(array)]
数组中的最大值
Math.max(...[1,2,3,4,5]) // 5
null表示"没有对象",即该处不应该有值
简单的用法如:
undefined表示"缺少值",就是此处应该有一个值,但是还没有定义
其他
null === undefined // false
null == undefined // true
null === null // true
null = 'value' // ReferenceError
undefined = 'value' // 'value'
特点 | Cookie | LocalStorage | sessionStorage |
---|---|---|---|
存放数据大小 | 4k左右 | 一般为5M | |
存放周期 | 一般由服务器生成,可人为设置失效时间。如果在浏览器端生成,默认是关闭浏览器后失效 | 除非被清除,否则永久保存 | 仅在当前会话下有效,关闭页面或浏览器后被清除 |
与服务器端通信 | 每次都携带在HTTP请求头中 | 仅在客户端中保存不参与服务器通信 | |
概念 | 是服务器发送到用户浏览器并保存在浏览器上的一块数据,它会在浏览器下一次发起请求时被携带并发送到服务器上。比较经典的,可以它用来确定两次请求是否来自于同一个浏览器,从而能够确认和保持用户的登录状态。Cookie的使用使得基于无状态的HTTP协议上记录稳定的状态信息成为了可能 | 没有时间限制的数据存储 (持久化) | 针对一个 session 的数据存储,当用户关闭浏览器窗口后(不包括重载),数据会被删除。 |
用途 | 1.会话状态管理(如用户登录状态、购物车) 2.个性化设置(如用户自定义设置) 3.浏览器行为跟踪(如跟踪分析用户行为) | 用于本地存储数据❶ | 可以将一部分数据在当前会话中保存下来,刷新页面数据依旧存在。但当页面关闭后,sessionStorage 中的数据就会被清空可以用来存储比较重要的数据 安全性高于cookie❶ |
缺陷 | 1.cookie会被加在每个http请求中 增加了流量 2. cookie以明文传递有安全问题 3.存储大小限制 | It works on same-origin policy. So, data stored will only be available on the same origin.(受同源策略的影响 数据只能在同源网站内存储和使用) | 1. The data is available only inside the window/tab in which it was set.(数据只在当前网页中有效) 2. The data is not persistent i.e. it will be lost once the window/tab is closed.(数据不能持续保存 关闭网页或浏览器后数据丢失) 3. Like localStorage, tt works on same-origin policy. So, data stored will only be available on the same origin(和localStorage类似 受同源策略影响 数据只在符合同源策略的网页中使用) |
相同点 | localStorage, sessionStorage and cookies are all subject to "same-origin" rules which means browsers should prevent access to the data except from the domain that set the information to start with. |
处理用户输入也是潜在的可能会影响性能的因素,因为其可能会阻塞其他内容的加载并且导致不必要的布局(layout)工作
TL;DR
页面交互最快的情况是,当用户与页面交互时,页面的合成器线程接受用户的触摸输入并将内容四处移动。这个过程不需要与主线程通信,而是直接提交给GPU处理。所以不需要等待主线程对JS的处理、以及布局(layout)、绘制(paint)等操作完成。
但是,如果附加了输入处理程序(如touchstart,touchmove,或者touchend)后,合成器线程必须等待该处理程序执行完毕,因为有可能调用了preventDefault()来阻止触摸滚动事件的发生。即使没有调用preventDefault(),合成器也必须等待其执行完毕,这样用户的滚动操作就被阻止就可能导致帧丢失从而引起卡顿。
总而言之,你应该确保运行的所有输入处理程序都快速执行,并允许合成器执行其工作。
输入处理程序被安排在requestAnimtionFrame回调之前运行。如果在这个处理程序中做样式上的修改,那么在requestAnimationFrame开始处有需要更改的样式处理,这会触发强制同步布局。
上面两个问题的解决方案是相同的:你应该对下一个requestAnimationFrame的回调中做样式更改的情况做防抖处理。
function onScroll(evt){
lastScrollY = window.scrollY;
if(scheduleAnimationFrame)
retun;
scheduleAnimationFrame = true;
requestAnimationFrame(readAndUpdatePage);
}
window.addEventListener('scroll',onScroll);
这样做还有一个好处,就是保持输入处理程序的轻量,因为这样就不会阻塞比如滚动等其他操作。
通过添加和删除元素,更改属性,或者通过动画来改变DOM结构等都会导致浏览器重新计算元素样式,很多情况下都会重新对整个页面或其中部分布局(layout)(或者回流[reflow]),这个过程也叫样式计算
样式计算的第一步就是创建一个与之对应的选择器集合,实际上就是让浏览器确定哪些类哪些伪类选择器和ID该应用于哪个元素,第二步是从匹配的选择器中获取所有样式,并计算最终样式。在Blink(Chrome和Opera的渲染引擎)中这个过程还是相当消耗性能的。
这个过程中渲染引擎大概有50%的时间在匹配选择器,剩下的一半时间在计算最终样式。
TL;DR
降低选择器的复杂度
我们知道浏览器在解析匹配CSS规则时是从右向左查找匹配的,嵌套越深选择匹配的负担越重,最好不要超过三层。同时浏览器在解析生成页面时分别解析构建DOM Tree 和CSSOM,在DOM树构建完成CSSOM未构建完成时,是不会直接把html放出来的,所以CSS不能太大,否则会有一段白屏时间,所以把字体或者图片转成base64放在CSS里是不太推荐的做法。
有些CSS优化建议说要按照如下优先级书写:
其实这些顺序对浏览器来说是一样的,因为浏览器在解析构建CSS规则时并不立马进行渲染,而是把这些属性值进行计算将最终的属性值放到computedStyle里。所以顺序并不会带来性能上的影响,对浏览器而言都是一样的。
关于样式中的计算比较典型一个例子的可能就是对元素使用rgba函数,当CSS解析时就需要先去执行rgba函数计算颜色,所以直接写成16位的色值更好一些。
BigInt
目前已经进入Stage 4阶段 下一个版本将会作为新特性出现在ECMAScript,下面我们来一起了解一下Bigint
。
BigInt
是什么? BigInt
是JavaScript中一种可以用来表示任意精度整数的基本数据类型
BigInt
可以用来表示任意精度整数的特性为JavaScript解锁了更多的*操作,使用BigInt
可以告别过去因为整数运算导致溢出的痛苦。特别是金融方面因为涉及大量的数据运算,比如高精度时间戳,或者数值过大的ID,这些是无法安全的用Number
类型去存储的,所以退而求其次使用String
类型去存储,有了BigInt
类型后就可以安全的将其存储为数值类型。
另外BigInt
的实现也为实现BigDecimal
打下坚实基础,那将对于以十进制精度表示货币金额并对其进行精确运算(也就是0.10 + 0.20 !== 0.30
问题)非常有帮助
此前已经有不少库实现了BigInt式的整数存储,当BigInt完全可用时,就可以拿掉那些依赖了,因为相比于使用这些依赖库,Native BigInt
则更具优势。因为与之相比,NativeBigInt
不需要加载解析编译的额外时间,并且在性能上表现更好。
JavaScript中Number是以64位双精度浮点型存储,所以会有精度限制,JavaScript中可以准确表示的最大整数是Number.MAX_SAFE_INTEGER
这个值是2^53-1
const max = Number.MAX_SAFE_INTEGER;
// → 9_007_199_254_740_991
Tips:为了可读性使用下划线作为分隔符进行分组 The numeric literal separators proposal
当自增一次时,可以得到正确值:
max + 1;
// 9_007_199_254_740_992 ✅
当自增两次时,我们发现结果并非预期
max + 2;
// → 9_007_199_254_740_992 ❌
max+1
和max+2
的结果一致,这就导致我们无法保证在JavaScript中获取到的这个值的准确性,JavaScript中任何超出安全值范围的计算都会丢失精度,正因为如此我们只能信任安全值范围内的整数。
BigInt
是JavaScript中一种可以用来表示任意精度(arbitrary precision)整数的基本数据类型,使用BigInt
可以安全的存储和操作任意大小的整数而不受Number
类型的安全值范围的限制。
生成一个BigInt
类型的值只需要在任意整数后加上n做后缀即可。例如:123
用BigInt
类型表示123n
,也可以通过全局函数BigInt(number)
来将Number
类型转化为BigInt
类型,换言之BigInt(123) === 123n
,让我们用BigInt
来解决一下上文所提到的问题
BigInt(Number.MAX_SAFE_INTEGER) + 2n;
// 9_007_199_254_740_993n ✅
再看一个两个Number类型的数值相乘的例子
1234567890123456789*123;
// -> 151851850485185200000 ❌
仔细看这个结果肯定是不对的,因为最低位一个是9一个是3所以正确值肯定是以7结尾的(3*9=27),但是这里却是一串0结尾,我们来用BigInt重新计算一下
1234567890123456789n*123n;
// -> 151851850485185185047n ✅
很显然这次是对的,当我们用BigInt
来处理时不会受到Number
中的安全值范围的限制,所以不用担心精度丢失
BigInt
是JavaScript中新的的基础类型,所以可以用typeof
操作符去检测
typeof 123
// 'number'
typeof 123n
// 'bigint'
因为Bigint
是一个单独的类型,所以BigInt
类型值和Number
严格模式下不相等,e.g. 4!== 4n
,BigInt类型和Number类型作比较时需要将自身类型转化为相互的类型,或者直接使用严格相等(===)
4n === BigInt(4);
// => true
4n == 4;
// => true
4n === 4;
// => false
当强制类型转化为布尔值时(例如在使用if
,&&
,||
,或者Boolean(int)
时触发),BigInt
遵循和Numebr
一样的规则
if(0n){
console.log('if');
}else{
console.log('else');
}
// 输出:'else', 因为0n是假值
0n || 12n
// -> 12n
0n && 12n
// -> 0n
Boolean(0n);
// -> false
Boolean(12n);
// -> true
!12n
// -> false
!0n
// -> true
BigInt支持那些常见的运算符例如:+,-,*,/ ** %
,包括一些按位运算符如|, & , <<, >> ^
,它和Number类型值的表现一致
(7 + 6 - 5) * 4 ** 3 / 2 % 3;
// → 1
(7n + 6n - 5n) * 4n ** 3n / 2n % 3n;
// → 1n
一元运算符-
可以用来表示BigInt中的负数,如:-42n ,但是一元运算符+
不支持,因为如果支持则会导致+x
表示结果为非Number
值,从而引起和现有逻辑的冲突。
一个值得注意的点是不要混合操作BigInt类型和Number类型,因为任何隐式强制类型转化都会导致精度丢失,看下面的例子:
BigInt(Number.MAX_SAFE_INTEGER)+2.5
// => ?? 🤔
可以猜一下结果,其实并没有合理的答案。因为BigInt
无法表示小数,而Number
则无法正确表示BigInt
类型的超出安全范围的值,因此当混用BigInt
和Number
时会报TypeError
其中唯一的例外是比较运算符,比如 ===
<
>
<=
>=
等,因为这类操作符最终会返回一个布尔类型值,不存在精度丢失的情况:
1+1n
// -> TypeError
123<124n;
// -> true
建议:BigInt和Number
一般情况下不要混合操作,BigInt
对于可能操作较大整数的情况下是合理的选择,Number
则对于在安全值范围内的操作更合适,所以选定一种合适的类型用下去,不要相互混用。
注意
关于BigInt的几个API BigInt()
BigInt.asIntN(width, value)
BigInt.asUintN(width, value)
BigInt64Array
,BigUint64Array
BigInt
函数,这个BigInt全局构造函数和Number
的构造函数类似,将传入的参数转化为BigInt
类型,如果转化失败,会报SyntaxError
或者RangeError
BigInt(123);
// -> 123n
BigInt(1.2);
// -> RangeError
BigInt('1.2');
// -> SyntaxError
BigInt.asIntN(width, value)
BigInt.asUintN(width, value)
,通过这两个库函数,可以将BigInt值包装为有符号或无符号整数,并限制在特定位数。其中BigInt.asIntN(width,value)
将BigInt
类型值包装为有符号二进制整数,BigInt.asUintN(width,value)
将BigInt类型值包装为无符号二进制整数。例如:如果你要执行64位算术运算,则可以使用它们来将其保持在适当的范围内: // BigInt类型值所能表示的最大的有符号的64位整数值
const max = 2n**(64n - 1n) - 1n;
BigInt.asIntN(64,max);
// -> 9_223_372_036_854_775_807n
BigInt.asIntN(64, max+1n);
// -> -9_223_372_036_854_775_808n
// ^ 变为负值 因为溢出了
// 一旦传给超过64位整数范围(即63位的绝对数值+1位符号位)的BigInt值,就会发生溢出。
BigInt.asUintN(64,max);
// -> 9_223_372_036_854_775_807n
BigInt.asUintN(64,max+1n)
// -> 9_223_372_036_854_775_808n
BigInt64Array
和BigUint64Array
可以使我们更加容易且有效地表示和操作此类值的列表。 const view = new BigInt64Array(4);
// -> [0n,0n,0n,0n]
view.length;
// -> 4
view[0];
// -> 0n
view[0] = 40n;
view[0];
// -> 40n
BigInt64Array
可以确保其值保持在有符号的64位限制范围内。BigUint64Array
则确保其值保持在无符号位的64位限制范围内
// BigInt类型值所能表示的最大的有符号的64位整数值
const max = 2n**(64n - 1n) - 1n;
view[0] = max;
view[0]
// -> 9_223_372_036_854_775_807n
view[0] = max + 1n;
view[0];
// -> -9_223_372_036_854_775_808n
// ^ 溢出了
const view_u = new BigUint64Array(4);
view_u[0] = max;
view_u[0];
// -> 9_223_372_036_854_775_807n
view_u[0] = max+1n;
view_u[0];
// -> 9_223_372_036_854_775_808n
到目前为止已经实现BigInt
的有Chrome(67+),Firefox(68+),Opear(54+),Node(10.4.0
+),其中Safari正在实现中
参考链接:
想知道BigInt是如何在Chrome中的实现的吗?请看下篇BigInt在V8中的实现(还没写呢)loading...
1 . 创建一个空对象
2 . 将所创建对象的__proto__属性值设成构造函数的prototype属性值
3 . 执行构造函数中的代码,构造函数中的this指向该对象
4 . 返回该对象(除非构造函数中返回一个对象)
function Person(a, b) {
this.name = a;
this.age = b;
}
Person.prototype.show = function() {
console.log(this.name, this.age);
};
var p = new Person('cat', 1);
console.log(p); // Person {name: "cat", age: 1}
function Person(a, b) {
this.name = a;
this.age = b;
}
Person.prototype.show = function() {
console.log(this.name, this.age);
};
// var p = new Person('cat', 1);
var p = {};
p.__proto__ = Person.prototype;
Person.call(p, 'cat', 1);
console.log(p); // Person {name: "cat", age: 1} 与上段代码输出表现一致
function fakeNew() {
const obj = Object.create(null)
const C = [].shift.call(arguments)
obj.__proto__ = C.prototype
const ret = C.apply(obj, arguments)
return typeof ret === 'object' && ret !== null ? ret : obj
}
事件委托又叫事件代理 事件委托就是利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件
事件委托的原理:
事件委托是利用事件的冒泡原理来实现的,何为事件冒泡呢?就是事件从最深的节点开始,然后逐步向上传播事件 举个例子:页面上有这么一个节点树,div>ul>li>a;比如给最里面的a加一个click点击事件,那么这个事件就会一层一层的往外执行,执行顺序a>li>ul>div,有这样一个机制,那么我们给最外面的div加点击事件,那么里面的ul,li,a做点击事件的时候,都会冒泡到最外层的div上,所以都会触发,这就是事件委托,委托它们父级代为执行事件
说到缓存其实分很多种,比如CDN缓存,代理服务器缓存,数据库缓存,浏览器缓存等等。这里我们主要说浏览器的缓存。其中重点部分是HTTP的缓存策略。当一个请求发出后,浏览器接收到响应,需要根据响应头里的缓存策略来决定是否使用以及如何使用缓存。我们以Chrome为例,Chrome在很多抽象层面都实现了缓存,通常可以大致分为三类:
本文主要介绍HTTP缓存部分。关于HTTP缓存最为熟知和关键,通过网络发出的每个请求都严格遵循RFC的HTTP规范标准,其中直接决定HTTP缓存的几个规则是Cache-Control、ETag、Last-Modified、Expires
。
Cache-Control的可取值有如下两种类型值,需要注意的一点是request请求头里带的值并不代表最终响应值。最终值仍需要服务端决定,请求头中的值只会作为一个参考,所以我们会重点关注响应头字段的内容。
Cache-Control : cache-request-directive | cache-response-directive
cache-request-directive: no-cache | no-store | max-age | max-stale | min-fresh | no-transform | only-if-cached
cache-response-directive :public| private | no-cache | no-store | no-transform | must-revalidate | proxy-revalidate | max-age | s-maxage
public表示可以任意缓存,可被作为共享缓存使用。即可以再多个用户之间共享。private则表示只针对特定用户做缓存并且不可作为共享缓存。MSDN中描述 Cache-Control 的默认值为private。
如图表示此响应只可作为私有缓存,而且每次请求都需要服务器验证其有效性。
must-revalidate表示缓存过期前浏览器、缓存服务器可以正常使用,一旦缓存过期则必须去源服务器验证响应数据的有效性。
proxy-revalidate类似于must-revalidate,区别是其仅针对共享缓存。
no-cache表示浏览器或缓存服务器可做缓存,但不管缓存是否过期,使用前必须经过源服务器验证其缓存的有效性。no-cache可以看做是must-revalidate的定制版,不同点是must-revalidate是在请求过期后直接从服务器获取响应,而no-cache则默认缓存是立即过期的。举个例子来说:如果某个响应缓存的时间是10秒有效,则在这个时间范围内must-revalidate会在10秒后使用缓存时被触发,而no-cache则是0秒触发must-revalidate。
no-store表示绝对🚫禁止缓存响应,每次必须向源服务器请求新的资源。
如图所以:此站点的这个请求通过添加no-cache no-store must-revalidate以及pragm:no-cache(用于兼容HTTP 1.0)来禁止缓存,每次请求都要发往源服务器。
再看一个例子,所有的Chrome extension采用的策略是no-cache,即缓存服务器在返回备份响应前需要向源服务器校验其有效性,如果可用则返回备份,否则要向源服务器发起请求。
max-age用来表示表示缓存的有效时间。单位是秒(s)。超出这个时间段后被认为过期。那么在这个max-age时间段内再次发出的请求,浏览器会直接使用缓存,不会再向浏览器发出请求。另外如果max-age和Expires同时存在时,会覆盖掉Expires字段(限于HTTP 1.1及之后版本)。
s-maxage只用来表示共享缓存(比如CDN缓存)的有效时间,作用类似max-age单位同样是秒(s),即max-age适用于普通缓存,s-maxage适用于共享缓存。如果response header里同时存在max-age和Expires(后文有提到)则会被s-maxage覆盖,需要注意的是如果缓存是private类型 则s-maxage会被直接忽略。
如下的例子中此缓存的有效期是一分钟。
no-transform表示不得对资源进行任何变更,不能修改Content-Encoding,Content-Range和Content-Type的请求头。用到情况很少,不做过多说明。
Cache-Control除了上述这些属于规范的取值外还有一种可取的范围值类型 cache-extension
,它不属于文档规范的一部分,所以有兼容性问题。这类值中比较典型的代表如:immutable、stale-while-revalidate=、stale-if-error=等。
immutable用来描述响应内容不会随时间变化。因为资源(如果未过期)在服务器上不变,所以即使在用户刷新页面时,也不需要再重新验证其有效性(例如,If-None-Match或If-Modified-Since)
stale-while-revalidate表示客户端在指定时间内允许在异步检查校验新的响应时,可接收已过期的资源响应。例如:Cache-Control : max-age=600, stale-while-revalidate=30
上述规则表示这条资源的有效期的600秒,若在异步状态下检查新的响应时过了有效期但依然可以最多保持30秒可用。如果验证未通过或者没有被触发,则在30秒后stale-while-revalidate规则不在有效,缓存真正过期。
state-if-error用来表示如果客户端获取最新资源失败在指定的时间内可接受已过期的资源响应。例如:Cache-Control: max-age=600, stale-if-error=1200
它表示资源的有效期为600秒如果在过期后向服务器请求遇到错误(比如服务器返回500、502、503、504等)则额外增加1200秒有效期。如果在1800秒后去请求则缓存过期直接返回错误信息。
如图是一个简略版的Cache-Control字段的判断步骤
Expires用来指定缓存的过期时间,是一个绝对时间,由服务端生成。Expires字段告诉浏览器在响应过期前的时间内无需再次请求服务器,可以直接使用缓存值,除非强制刷新或者缓存被清空。同时还有个缺陷在于服务器时间和用户端时间可能存在不一致,所以HTTP 1.1加入Cache-Control来改进这个问题。Expires的时间其实就是请求时的时间➕max-age。上文我们提到如果存在Cache-Control字段并且值为max-age则Expires字段就会被覆盖,也就意味着Cache-Control的优先级要高于Expires。Expires字段一般还要搭配Last-Modified使用。
想象这么一个场景:一个请求的缓存资源有效期是120秒,过了这个时间点因为缓存已经过了有效期,所以浏览器不能使用。当一个新的请求发出后,浏览器需要重新获取整个响应资源,但是如果响应内容未发生变化的话,这样是很浪费资源的。ETag就是来解决这个问题的。
ETag是由实体内容生成的一段hash值,由服务端生成,于If-None-Match搭配使用,当客户端再次请求时通过If-None-Match带上ETag的值向发送给服务端,服务端通过对比ETag值是否相等来验证资源是否发生变化。如果未变化则返回304和一个空响应,从而更高效的利用缓存节省资源消耗。
如图所示:因为其资源未发生变化,所以服务器返回304和空响应。
用来表示响应文件的最后修改时间,可以用来检查资源是否更新的一种方式。当客户端再次向服务端请求时,会向服务器发送带If-Modified-Since字段来判断资源在Last-Modified时间点后是否被修改过。如果没有被修改则返回304和空响应,反之重新向服务器获取最新资源。当然说到这里会发现Last-Modified和ETag字段作用相同,但是它们还是细微的差别的。HTTP1.1中ETag的出现主要是为了解决Last-Modified的一些不足。
Last-Modified与If-Modified-Since配合使用,当响应头带上Last-Modified,那么下次再请求时会把这个值加到请求头的If-Modified-Since中,服务器通过对比这个值来判断资源是否发生变化,如果没有发生变化则返回304和空响应。
用于兼容HTTP 1.0,因为HTTP 1.0不支持Cache-Control,当Cache-Control可用时会被忽略。主要用来做Cache-Control:no-cache
的备胎。它只有一个可取值即:no-cache
。使用的唯一场景就是向后兼容HTTP 1.0。
上面四种缓存规则的优先级为:Cache-Control > Expires > ETag > Last-Modified
小知识:浏览器中我们经常会看到Memory Cache 和Disk Cache那么来看下它们之间有什么区别?实际上Memory Cache是将资源存储在RAM里,所以访问速度会很快,但是当你关闭浏览器后缓存立马失效,而Disk Cache则是将缓存资源写在磁盘里,读取资源时从磁盘中加载读取,有效期更持久。
浏览器的其他缓存策略比如HTML的Meta标签,Manifest,LocalStorage SessionStorage等
Manifest已经被移除出标准规范所以不在推荐使用,建议使用Service Workers。
关于LocalStorage和SessionStorage可以看之前总结的这篇文章Cookie, LocalStorage 与 SessionStorage
参考链接:
This is problematic both server rendering (where the external data won’t be used) and the upcoming async rendering mode (where the request might be initiated multiple times). There is a common misconception that fetching in componentWillMount lets you avoid the first empty rendering state. In practice this was never true because React has always executed render immediately after componentWillMount. If the data is not available by the time componentWillMount fires, the first render will still show a loading state regardless of where you initiate the fetch. This is why moving the fetch to componentDidMount has no perceptible effect in the vast majority of cases.
to help ensure purity which is important because it fires during interruptible phase ----- by Dan
This proposal is intended to reduce the risk of writing async-compatible React components.
Choosing lifecycle method names that have a clearer, more limited purpose.
Making certain lifecycles static to prevent unsafe access of instance properties. ----- react-rfc
Reconciliation / Render phase: React builds the work in progress tree and finds out the changes it needs to make without flushing them to the renderer. This is interruptible.
Commit phase: all the changes are flushed to DOM. This is uninterruptible.
useLayoutEffect: If you need to mutate the DOM and/or DO need to perform measurements
useLayoutEffect fires synchronously after all DOM mutations but before Paint phase. Use this to read layout(styles or layout information) from the DOM and then perform blocking custom DOM mutations based on layout.
useEffect: If you don't need to interact with the DOM at all or your DOM changes are unobservable (seriously, most of the time you should use this).
useEffect runs after the render is committed to the screen i.e. after Layout and Paint phase. Use this whenever possible to avoid blocking visual updates
- Guaranteeing Internal Consistency
- Enabling Concurrent Updates
- Performance optimization
RFClarification: why is setState
asynchronous?
type
type代表JSX的类型,在JSX中标签是什么type就是什么,例如<div>
type就是div,<Example>
type就是Example,所以说这里的标签是啥type就是啥。即 type === Example
$$typeof
$$typeof是对JSX中type的细化,一般能渲染出的比如<div>
<Modal>
等的自定义component的$$typeof都是Symbol('react.element'),而无法渲染出的(React自身定义的一些封装类型)则由具体类型决定,例如<React.Portal>
的$$typeof就为Symbol('react.portal') <React.Fragment>
则为Symbol('react.fragment'),还有memo,lazy,forwardRef都类似。Why Do React Elements Have a $$typeof Property? 【PS:为了安全】
tag
tag是针对fiber的类型标识(type和$$typeof是针对element或者说JSX的返回值)主要为了react处理不同任务。
Defines the type of the fiber. It’s used in the reconciliation algorithm to determine what work needs to be done. As mentioned earlier, the work varies depending on the type of React element. The function createFiberFromTypeAndProps maps a React element to the corresponding fiber node type
... 待补充
function quickSort(arr){
if(arr.length === 0) return [];
var l = [];
var r= [];
var m = ~~(arr.length/2);
var flag = arr.splice(m,1);
for(let i = 0; i < arr.length; i++){
arr[i] < flag ? l.push(arr[i]): r.push(arr[i]);
}
return quickSort(l).concat(flag,quickSort(r));
}
quickSort([5,4,3,2,7,1]) // [1,2,3,4,5,7]
function bubbleSort(arr){
for(let i = 0; i < arr.length; i++){
for(let j = 0; j < arr.length - 1 - i; j++){
if(arr[j]>arr[j+1]){
[arr[j],arr[j+1]] = [arr[j+1],arr[j]];
}
}
}
return arr;
}
var selectSort = function(arr){
for(var i = 0; i < arr.length; i++){
var min_index = i;
for(var j = i+1; j < arr.length; j++){
if(arr[min_index]>arr[j]){
min_index = j;
}
}
[arr[min_index],arr[i]] = [arr[i],arr[min_index]];
}
return arr;
}
function merge(left, right){
const res = [];
while(left.length > 0 && right.length > 0 ) {
if(left[0] > right[0]) {
res.push(right.unshift());
}else{
res.push(left.unshift());
}
}
if(left.length > 0){
res.concat(left);
}
if(right.length > 0){
res.concat(right);
}
return res;
}
function mergeSort(arr){
const lens = arr.length;
if(len < 2) {
return arr;
}
const mid = ~~(lens / 2);
const left = mergeSort(arr.slice(0,mid));
const right = mergeSort(arr.slice(mid));
return merge(left,right);
}
display:none指的是元素完全不陈列出来,不占据空间,涉及到了DOM结构,故产生reflow与repaint
visibility:hidden指的是元素不可见但存在,保留空间,不影响结构,故只产生repaint
标准的盒子模型:盒子总宽度 =margin + border + padding + content
W3C中的width = content
IE盒模型:盒子总宽度 = margin+content
IE中盒模型的Width = border+padding+content
onInput 属性实时监控< input> 或者<textarea>中的value值得变化状况 Object.target.value.length 可以获取文本框中的字符长度 onInput事件类似于 onchange 事件。不同之处在于 oninput 事件在元素值发生变化是立即触发, onchange 在元素失去焦点时触发。另外一点不同是 onchange 事件也可以作用于 < keygen> 和 < select> 元素 。关于getComputedStyle() 与style直接获取元素的CSS属性值相比 style仅能获取内嵌在html代码中的css元素属性 而 getComputedStyle则都可以获取 但是是只读属性[针对IE的话要用currentStyle属性] 兼容一下的话:
(element.currentStyle? element.currentStyle : window.getComputedStyle(element, null)).height
getPropertyValue方法可以获取CSS样式申明对象上的属性值(直接属性名) 例如:window.getComputedStyle(element, null).getPropertyValue("float");
事件委托又叫事件代理 事件委托就是利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。事件委托的原理:事件委托是利用事件的冒泡原理来实现的,何为事件冒泡呢?就是事件从最深的节点开始,然后逐步向上传播事件 举个例子:页面上有这么一个节点树,div>ul>li>a;比如给最里面的a加一个click点击事件,那么这个事件就会一层一层的往外执行,执行顺序a>li>ul>div,有这样一个机制,那么我们给最外面的div加点击事件,那么里面的ul,li,a做点击事件的时候,都会冒泡到最外层的div上,所以都会触发,这就是事件委托,委托它们父级代为执行事件
CSS Sprites
一种网页图片应用处理方式。它允许你将一个页面涉及到的所有零星图片都包含到一张大图中去,这样一来,当访问该页面时,载入的图片就不会像以前那样一幅一幅地慢慢显示出来了。利用CSS的“background-image”,“background- repeat”,“background-position”的组合进行背景定位,background-position可以用数字精确的定位出背景图片的位置。
利用CSS Sprites能很好地减少网页的http请求,从而大大的提高页面的性能,这也是CSS Sprites最大的优点,也是其被广泛传播和应用的主要原因;
屏幕分辨率
判断一个变量是不是对象非常简单。值类型的类型判断用typeof(Null除外),引用类型的类型判断用instanceof
使用var操作符定义的变量将成为该变量的作用域中的局部变量,如果在函数中使用var定义一个变量,那么这个变量在函数退出后就会被销毁;省略var操作符会创建一个全局变量,但在局部作用域中定义的全局变量很难维护,也会由于相应变量不会马上就有定义而导致不必要的混乱,给未经声明的变量赋值在严格模式下会导致抛出Reference错误
(function() { var a = b = 5; })(); console.log(b); console.log(a);
输出:5
a is not defined
数组:Array.isArray(arr) || Object.prototype.toString.call(arr) === '[object Array]'
对象:typeof obj === 'function' || typeof obj === 'obj' && !!obj;
使用~~向下取整 类似与Math.floor();
~~12.1 // 12
~~12.7 //12
数组去重
[...new Set(array)]
数组中的最大值
Math.max(...[1,2,3,4,5]) // 5
Math.max.apply(null, [1,4,2,5,6,0,-2]) // 6
数组中的最小值
Math.min.apply(null, [1,4,2,5,6,0,-2]) // -2
判断IE
function detectIE() {
var ua = window.navigator.userAgent;
var msie = ua.indexOf('MSIE ');
if (msie > 0) {
// IE 10 or older => return version number
return parseInt(ua.substring(msie + 5, ua.indexOf('.', msie)), 10);
}
var trident = ua.indexOf('Trident/');
if (trident > 0) {
// IE 11 => return version number
var rv = ua.indexOf('rv:');
return parseInt(ua.substring(rv + 3, ua.indexOf('.', rv)), 10);
}
var edge = ua.indexOf('Edge/');
if (edge > 0) {
// Edge (IE 12+) => return version number
return parseInt(ua.substring(edge + 5, ua.indexOf('.', edge)), 10);
}
// other browser
return false;
}
num.toString().replace(/(\d)(?=(\d{3})+$)/g, '$1,');
1234567890..toString().replace(/(\d)(?=(\d{3})+$)/g, '$1,') // "1,234,567,890"
或者利用toLocaleString Number.prototype.toLocaleString.call(num);
const random = (arr)=>{
for(let i =1; i < arr.length; i++){
let random = ~~(Math.random()*(i+1));
[arr[i], arr[random]] = [arr[random], arr[i]];
}
return arr;
}
判断一个对象是否为空
// because Object.keys(new Date()).length === 0;
// we have to do some additional check
Object.keys(obj).length === 0 && obj.constructor === Object
实现一个sleep
function sleep(ms){
let template = new Promise((resolve)=>{
setTimeout(resolve,ms);
})
return template;
}
sleep(1000).then(()=>{
// do someting
})
defer不会阻塞HTML解析 HTML解析时并行下载JS文件,等到HTML解析完执行,defer会始终按照顺序执行
async可能阻塞HTML解析 HTML解析时并行下载JS文件,一旦下载完成立即执行,不一定按照顺序执行
域名收敛即DNS优化
DOMContentLoaded与load
从页面空白到展示出页面内容,会触发DOMContentLoaded事件。而这段时间就是HTML文档被加载和解析完成
页面上所有的资源(图片,音频,视频等)被加载以后才会触发load事件,简单来说,页面的load事件会在DOMContentLoaded被触发之后才触发
将js文件放在尾部是为了减少First Paint的时间
浏览器渲染过程是先解析HTML文件 构建DOM树,创建一个或多个图层独立的绘制,将图作为纹理上传至GPU,复合多个图层来完成最终的图像,所以每个层的样式出现调整后,要重新计算样式,重新布局,使用top-left-margin这种方法只会创建一个图层,而使用translate则容器中的元素不会和自身放在一个图层,而是放在GPU单独的渲染层中,这样带来的好处有三点
额外的渲染层导致更多的线程间通信
"fatal: unable to access 'https://chromium.googlesource.com/chromium/tools/depot_tools.git/': Failed to connect to chromium.googlesource.com port 443: Operation timed out"
chromium
解决办法:git config --global http.proxy 127.0.0.1:端口号【下载时候发现又不能用了,那就去 vi .gitconfig 里把设置的http和https删掉】
fetch chromium zsh: command not found: fetch
解决办法:export PATH="$PATH:{path}/depot_tools/depot_tools"
export http_proxy=http://127.0.0.1:1087;export https_proxy=http://127.0.0.1:1087;
gclient sync You have unstaged changes
解决办法: gclient sync -f
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.