防抖函数的应用场景:
节流函数的适⽤场景:
1. setTimeout
setTimeout的运行机制:执行该语句时,是立即把当前定时器代码推入事件队列,当定时器在事件列表中满足设置的时间值时将传入的函数加入任务队列,之后的执行就交给任务队列负责。但是如果此时任务队列不为空,则需等待,所以执行定时器内代码的时间可能会大于设置的时间
setTimeout(() => {
console.log(1);
}, 0)
console.log(2);
输出 2, 1;
setTimeout
的第二个参数表示在执行代码前等待的毫秒数。上面代码中,设置为0,表面意思为 执行代码前等待的毫秒数为0,即立即执行。但实际上的运行结果我们也看到了,并不是表面上看起来的样子,千万不要被欺骗了。
实际上,上面的代码并不是立即执行的,这是因为setTimeout
有一个最小执行时间,HTML5标准规定了setTimeout()
的第二个参数的最小值(最短间隔)不得低于4毫秒
。 当指定的时间低于该时间时,浏览器会用最小允许的时间作为setTimeout
的时间间隔,也就是说即使我们把setTimeout
的延迟时间设置为0,实际上可能为 4毫秒后才事件推入任务队列
。
定时器代码在被推送到任务队列前,会先被推入到事件列表中,当定时器在事件列表中满足设置的时间值时会被推到任务队列,但是如果此时任务队列不为空,则需等待,所以执行定时器内代码的时间可能会大于设置的时间
setTimeout(() => {
console.log(111);
}, 100);
上面代码表示100ms
后执行console.log(111)
,但实际上实行的时间肯定是大于100ms后的, 100ms 只是表示 100ms 后将任务加入到"任务队列"中,必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码耗时很长,有可能要等很久,所以并没有办法保证,回调函数一定会在setTimeout()
指定的时间执行。
2. setTimeout 和 setInterval区别
setTimeout
: 指定延期后调用函数,每次setTimeout
计时到后就会去执行,然后执行一段时间后才继续setTimeout
,中间就多了误差,(误差多少与代码的执行时间有关)。setInterval
:以指定周期调用函数,而setInterval
则是每次都精确的隔一段时间推入一个事件(但是,事件的执行时间不一定就不准确,还有可能是这个事件还没执行完毕,下一个事件就来了).btn.onclick = function(){
setTimeout(function(){
console.log(1);
},250);
}
击该按钮后,首先将
onclick
事件处理程序加入队列。该程序执行后才设置定时器,再有250ms
后,指定的代码才被添加到队列中等待执行。 如果上面代码中的onclick
事件处理程序执行了300ms
,那么定时器的代码至少要在定时器设置之后的300ms
后才会被执行。队列中所有的代码都要等到javascript进程空闲之后才能执行,而不管它们是如何添加到队列中的。
如图所示,尽管在255ms
处添加了定时器代码,但这时候还不能执行,因为onclick
事件处理程序仍在运行。定时器代码最早能执行的时机是在300ms
处,即onclick
事件处理程序结束之后。
3. setInterval存在的一些问题:
JavaScript中使用 setInterval
开启轮询。定时器代码可能在代码再次被添加到队列之前还没有完成执行,结果导致定时器代码连续运行好几次,而之间没有任何停顿。而javascript引擎对这个问题的解决是:当使用setInterval()
时,仅当没有该定时器的任何其他代码实例时,才将定时器代码添加到队列中。这确保了定时器代码加入到队列中的最小时间间隔为指定间隔。
但是,这样会导致两个问题:
假设,某个onclick
事件处理程序使用setInterval()
设置了200ms
间隔的定时器。如果事件处理程序花了300ms
多一点时间完成,同时定时器代码也花了差不多的时间,就会同时出现跳过某间隔的情况
例子中的第一个定时器是在205ms
处添加到队列中的,但是直到过了300ms
处才能执行。当执行这个定时器代码时,在405ms处又给队列添加了另一个副本。在下一个间隔,即605ms处,第一个定时器代码仍在运行,同时在队列中已经有了一个定时器代码的实例。结果是,在这个时间点上的定时器代码不会被添加到队列中
使用setTimeout
构造轮询能保证每次轮询的间隔。
setTimeout(function () {
console.log('我被调用了');
setTimeout(arguments.callee, 100);
}, 100);
callee
是arguments
对象的一个属性。它可以用于引用该函数的函数体内当前正在执行的函数。在严格模式下,第5版 ECMAScript (ES5) 禁止使用arguments.callee()
。当一个函数必须调用自身的时候, 避免使用arguments.callee()
, 通过要么给函数表达式一个名字,要么使用一个函数声明.
setTimeout(function fn(){
console.log('我被调用了');
setTimeout(fn, 100);
},100);
这个模式链式调用了setTimeout()
,每次函数执行的时候都会创建一个新的定时器。第二个setTimeout()
调用当前执行的函数,并为其设置另外一个定时器。这样做的好处是,在前一个定时器代码执行完之前,不会向队列插入新的定时器代码,确保不会有任何缺失的间隔。而且,它可以保证在下一次定时器代码执行之前,至少要等待指定的间隔,避免了连续的运行。
4. requestAnimationFrame
4.1 60fps
与设备刷新率
目前大多数设备的屏幕刷新率为60次/秒
,如果在页面中有一个动画或者渐变效果,或者用户正在滚动页面,那么浏览器渲染动画或页面的每一帧的速率也需要跟设备屏幕的刷新率保持一致。
卡顿:其中每个帧的预算时间仅比16毫秒
多一点(1秒/ 60 = 16.6毫秒
)。但实际上,浏览器有整理工作要做,因此您的所有工作是需要在10毫秒
内完成。如果无法符合此预算,帧率将下降,并且内容会在屏幕上抖动。此现象通常称为卡顿,会对用户体验产生负面影响。
跳帧: 假如动画切换在 16ms, 32ms, 48ms时分别切换,跳帧就是假如到了32ms,其他任务还未执行完成,没有去执行动画切帧,等到开始进行动画的切帧,已经到了该执行48ms的切帧。就好比你玩游戏的时候卡了,过了一会,你再看画面,它不会停留你卡的地方,或者这时你的角色已经挂掉了。必须在下一帧开始之前就已经绘制完毕;
Chrome devtool 查看实时 FPS, 打开 More tools => Rendering, 勾选 FPS meter
4.2 requestAnimationFrame
实现动画
requestAnimationFrame
是浏览器用于定时循环操作的一个接口,类似于setTimeout,主要用途是按帧对网页进行重绘。
在 requestAnimationFrame
之前,主要借助 setTimeout/ setInterval
来编写 JS 动画,而动画的关键在于动画帧之间的时间间隔设置,这个时间间隔的设置有讲究,一方面要足够小,这样动画帧之间才有连贯性,动画效果才显得平滑流畅;另一方面要足够大,确保浏览器有足够的时间及时完成渲染。
显示器有固定的刷新频率(60Hz或75Hz),也就是说,每秒最多只能重绘60次或75次,requestAnimationFrame
的基本思想就是与这个刷新频率保持同步,利用这个刷新频率进行页面重绘。此外,使用这个API,一旦页面不处于浏览器的当前标签,就会自动停止刷新。这就节省了CPU、GPU和电力。
requestAnimationFrame
是在主线程上完成。这意味着,如果主线程非常繁忙,requestAnimationFrame
的动画效果会大打折扣。
requestAnimationFrame
使用一个回调函数作为参数。这个回调函数会在浏览器重绘之前调用。
requestID = window.requestAnimationFrame(callback);
window.requestAnimFrame = (function(){
return window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function( callback ){
window.setTimeout(callback, 1000 / 60);
};
})();
上面的代码按照1秒钟60次(大约每16.7毫秒一次),来模拟requestAnimationFrame
。
5. requestIdleCallback()
MDN上的解释:
requestIdleCallback()
方法将在浏览器的空闲时段内调用的函数排队。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。函数一般会按先进先调用的顺序执行,然而,如果回调函数指定了执行超时时间timeout,则有可能为了在超时前执行函数而打乱执行顺序。
requestAnimationFrame
会在每次屏幕刷新的时候被调用,而requestIdleCallback
则会在每次屏幕刷新时,判断当前帧是否还有多余的时间,如果有,则会调用requestAnimationFrame
的回调函数,
图片中是两个连续的执行帧,大致可以理解为两个帧的持续时间大概为16.67,图中黄色部分就是空闲时间。所以,requestIdleCallback
中的回调函数仅会在每次屏幕刷新并且有空闲时间时才会被调用.
利用这个特性,我们可以在动画执行的期间,利用每帧的空闲时间来进行数据发送的操作,或者一些优先级比较低的操作,此时不会使影响到动画的性能,或者和requestAnimationFrame
搭配,可以实现一些页面性能方面的的优化,
react 的
fiber
架构也是基于requestIdleCallback
实现的, 并且在不支持的浏览器中提供了polyfill
总结
单线程模型和任务队列
出发理解 setTimeout(fn, 0)
,并不是立即执行。requestAnimationFrame
会比 setInterval
效果更好requestIdleCallback()
常用来切割长任务,利用空闲时间执行,避免主线程长时间阻塞在 Vue3.0 中通过 Proxy
来替换原本的 Object.defineProperty
来实现数据响应式。
Proxy 是 ES6 中新增的功能,它可以用来自定义对象中的操作。
let p = new Proxy(target, handler)
target
代表需要添加代理的对象,handler
用来自定义对象中的操作,比如可以用来自定义 set
或者 get
函数。
下面来通过 Proxy
来实现一个数据响应式:
let onWatch = (obj, setBind, getLogger) => {
let handler = {
get(target, property, receiver) {
getLogger(target, property)
return Reflect.get(target, property, receiver)
},
set(target, property, value, receiver) {
setBind(value, property)
return Reflect.set(target, property, value)
}
}
return new Proxy(obj, handler)
}
let obj = { a: 1 }
let p = onWatch(
obj,
(v, property) => {
console.log(`监听到属性${property}改变为${v}`)
},
(target, property) => {
console.log(`'${property}' = ${target[property]}`)
}
)
p.a = 2 // 监听到属性a改变
p.a // 'a' = 2
在上述代码中,通过自定义 set
和 get
函数的方式,在原本的逻辑中插入了我们的函数逻辑,实现了在对对象任何属性进行读写时发出通知。
当然这是简单版的响应式实现,如果需要实现一个 Vue 中的响应式,需要在 get
中收集依赖,在 set
派发更新,之所以 Vue3.0 要使用 Proxy
替换原本的 API 原因在于 Proxy
无需一层层递归为每个属性添加代理,一次即可完成以上操作,性能上更好,并且原本的实现有一些数据更新不能监听到,但是 Proxy
可以完美监听到任何方式的数据改变,唯一缺陷就是浏览器的兼容性不好。
产生乱码的原因:
gbk
的编码,而内容中的中文字是utf-8
编码的,这样浏览器打开即会出现html
乱码,反之也会出现乱码;html
网页编码是gbk
,而程序从数据库中调出呈现是utf-8
编码的内容也会造成编码乱码;解决办法:
gbk
,而数据库储存数据编码格式是UTF-8
,此时需要程序查询数据库数据显示数据前进程序转码;共同点:
不同点:
箭头函数是ES6中的提出来的,它没有prototype,也没有自己的this指向,更不可以使用arguments参数,所以不能New一个箭头函数。
new操作符的实现步骤如下:
所以,上面的第二、三步,箭头函数都是没有办法执行的。
使用clear属性清除浮动,其语法如下:
clear:none|left|right|both
如果单看字面意思,clear:left 是“清除左浮动”,clear:right 是“清除右浮动”,实际上,这种解释是有问题的,因为浮动一直还在,并没有清除。
官方对clear属性解释:“元素盒子的边不能和前面的浮动元素相邻”,对元素设置clear属性是为了避免浮动元素对该元素的影响,而不是清除掉浮动。
还需要注意 clear 属性指的是元素盒子的边不能和前面的浮动元素相邻,注意这里“前面的”3个字,也就是clear属性对“后面的”浮动元素是不闻不问的。考虑到float属性要么是left,要么是right,不可能同时存在,同时由于clear属性对“后面的”浮动元素不闻不问,因此,当clear:left有效的时候,clear:right必定无效,也就是此时clear:left等同于设置clear:both;同样地,clear:right如果有效也是等同于设置clear:both。由此可见,clear:left和clear:right这两个声明就没有任何使用的价值,至少在CSS世界中是如此,直接使用clear:both吧。
一般使用伪元素的方式清除浮动:
.clear::after{ content:''; display: block; clear:both;}
clear属性只有块级元素才有效的,而::after等伪元素默认都是内联水平,这就是借助伪元素清除浮动影响时需要设置display属性值的原因。
通过 JS 对象模拟 DOM 的节点
。在 Facebook 构建 React 初期时,考虑到要提升代码抽象能力、避免人为的 DOM 操作、降低代码整体风险等因素,所以引入了虚拟 DOMPlain Object
,以 React 为例,在 render
函数中写的 JSX
会在 Babel
插件的作用下,编译为 React.createElement
执行 JSX
中的属性参数React.createElement
执行后会返回一个 Plain Object
,它会描述自己的 tag
类型、props
属性以及 children
情况等。这些 Plain Object
通过树形结构组成一棵虚拟 DOM
树。当状态发生变更时,将变更前后的虚拟 DOM
树进行差异比较,这个过程称为 diff
,生成的结果称为 patch
。计算之后,会渲染 Patch
完成对真实 DOM
的操作。改善大规模
DOM操作的性能
、规避 XSS 风险
、能以较低的成本实现跨平台开发
。DOM
除了渲染页面,虚拟 DOM 还有哪些应用场景?
这个问题考验面试者的想象力。通常而言,我们只是将虚拟 DOM 与渲染绑定在一起,但实际上虚拟 DOM 的应用更为广阔。比如,只要你记录了真实 DOM 变更,它甚至可以应用于埋点统计与数据记录等。
SSR原理
借助虚拟dom,服务器中没有dom概念的,react巧妙的借助虚拟dom,然后可以在服务器中nodejs可以运行起来react代码。
1. XSS
涉及面试题:什么是
XSS
攻击?如何防范XSS
攻击?什么是CSP
?
XSS
简单点来说,就是攻击者想尽一切办法将可以执行的代码注入到网页中。XSS
可以分为多种类型,但是总体上我认为分为两类:持久型和非持久型。举个例子,对于评论功能来说,就得防范持久型
XSS
攻击,因为我可以在评论中输入以下内容
URL
参数的方式加入攻击代码,诱导用户访问链接从而进行攻击。举个例子,如果页面需要从
URL
中获取某些参数作为内容的话,不经过过滤就会导致攻击代码被执行
<!-- http://www.domain.com?name=<script>alert(1)</script> -->
<div>{{name}}</div>
但是对于这种攻击方式来说,如果用户使用
Chrome
这类浏览器的话,浏览器就能自动帮助用户防御攻击。但是我们不能因此就不防御此类攻击了,因为我不能确保用户都使用了该类浏览器。
对于
XSS
攻击来说,通常有两种方式可以用来防御。
首先,对于用户的输入应该是永远不信任的。最普遍的做法就是转义输入输出的内容,对于引号、尖括号、斜杠进行转义
function escape(str) {
str = str.replace(/&/g, '&')
str = str.replace(/</g, '<')
str = str.replace(/>/g, '>')
str = str.replace(/"/g, '&quto;')
str = str.replace(/'/g, ''')
str = str.replace(/`/g, '`')
str = str.replace(/\//g, '/')
return str
}
通过转义可以将攻击代码
<script>alert(1)</script>
变成
// -> <script>alert(1)</script>
escape('<script>alert(1)</script>')
但是对于显示富文本来说,显然不能通过上面的办法来转义所有字符,因为这样会把需要的格式也过滤掉。对于这种情况,通常采用白名单过滤的办法,当然也可以通过黑名单过滤,但是考虑到需要过滤的标签和标签属性实在太多,更加推荐使用白名单的方式
const xss = require('xss')
let html = xss('<h1 id="title">XSS Demo</h1><script>alert("xss");</script>')
// -> <h1>XSS Demo</h1><script>alert("xss");</script>
console.log(html)
以上示例使用了
js-xss
来实现,可以看到在输出中保留了h1
标签且过滤了script
标签
CSP
本质上就是建立白名单,开发者明确告诉浏览器哪些外部资源可以加载和执行。我们只需要配置规则,如何拦截是由浏览器自己实现的。我们可以通过这种方式来尽量减少XSS
攻击。
通常可以通过两种方式来开启 CSP :
HTTP Header
中的 Content-Security-Policy
meta
标签的方式 <meta http-equiv="Content-Security-Policy">
这里以设置 HTTP Header
来举例
只允许加载本站资源
Content-Security-Policy: default-src ‘self’
只允许加载 HTTPS 协议图片
Content-Security-Policy: img-src https://*
允许加载任何来源框架
Content-Security-Policy: child-src 'none'
当然可以设置的属性远不止这些,你可以通过查阅 文档 (opens new window)的方式来学习,这里就不过多赘述其他的属性了。
对于这种方式来说,只要开发者配置了正确的规则,那么即使网站存在漏洞,攻击者也不能执行它的攻击代码,并且
CSP
的兼容性也不错。
2 CSRF
跨站请求伪造(英语:
Cross-site request forgery
),也被称为one-click attack
或者session riding
,通常缩写为CSRF
或者XSRF
, 是一种挟制用户在当前已登录的Web
应用程序上执行非本意的操作的攻击方法
CSRF
就是利用用户的登录态发起恶意请求
如何攻击
假设网站中有一个通过 Get 请求提交用户评论的接口,那么攻击者就可以在钓鱼网站中加入一个图片,图片的地址就是评论接口
<img src="http://www.domain.com/xxx?comment='attack'"/>
res.setHeader('Set-Cookie', `username=poetry2;sameSite = strict;path=/;httpOnly;expires=${getCookirExpires()}`)
在B网站,危险网站向A网站发起请求
<!DOCTYPE html>
<html>
<body>
<!-- 利用img自动发送请求 -->
<img src="http://localhost:8000/api/user/login" />
</body>
</html>
会带上A网站的cookie
// 在A网站下发cookie的时候,加上sameSite=strict,这样B网站在发送A网站请求,不会自动带上A网站的cookie,保证了安全
// NAME=VALUE 赋予Cookie的名称及对应值
// expires=DATE Cookie 的有效期
// path=PATH 赋予Cookie的名称及对应值
// domain=域名 作为 Cookie 适用对象的域名 (若不指定则默认为创建 Cookie 的服务器的域名) (一般不指定)
// Secure 仅在 HTTPS 安全通信时才会发送 Cookie
// HttpOnly 加以限制,使 Cookie 不能被 JavaScript 脚本访问
// SameSite Lax|Strict|None 它允许您声明该Cookie是否仅限于第一方或者同一站点上下文
res.setHeader('Set-Cookie', `username=poetry;sameSite=strict;path=/;httpOnly;expires=${getCookirExpires()}`)
如何防御
Get
请求不对数据进行修改Cookie
token
SameSite Cookies
: 只能当前域名的网站发出的http请求,携带这个Cookie
。当然,由于这是新的cookie属性,在兼容性上肯定会有问题CSRF攻击,仅仅是利用了http携带cookie的特性进行攻击的,但是攻击站点还是无法得到被攻击站点的cookie。这个和XSS不同,XSS是直接通过拿到Cookie等信息进行攻击的
在CSRF攻击中,就Cookie相关的特性:
3 密码安全
加盐
对于密码存储来说,必然是不能明文存储在数据库中的,否则一旦数据库泄露,会对用户造成很大的损失。并且不建议只对密码单纯通过加密算法加密,因为存在彩虹表的关系
// 加盐也就是给原密码添加字符串,增加原密码长度
sha256(sha1(md5(salt + password + salt)))
但是加盐并不能阻止别人盗取账号,只能确保即使数据库泄露,也不会暴露用户的真实密码。一旦攻击者得到了用户的账号,可以通过暴力破解的方式破解密码。对于这种情况,通常使用验证码增加延时或者限制尝试次数的方式。并且一旦用户输入了错误的密码,也不能直接提示用户输错密码,而应该提示账号或密码错误
前端加密
虽然前端加密对于安全防护来说意义不大,但是在遇到中间人攻击的情况下,可以避免明文密码被第三方获取
4. 总结
XSS
:跨站脚本攻击,是一种网站应用程序的安全漏洞攻击,是代码注入的一种。常见方式是将恶意代码注入合法代码里隐藏起来,再诱发恶意代码,从而进行各种各样的非法活动防范:记住一点 “所有用户输入都是不可信的”,所以得做输入过滤和转义
CSRF
:跨站请求伪造,也称 XSRF
,是一种挟制用户在当前已登录的Web
应用程序上执行非本意的操作的攻击方法。与 XSS
相比,XSS
利用的是用户对指定网站的信任,CSRF
利用的是网站对用户网页浏览器的信任。防范:用户操作验证(验证码),额外验证机制(
token
使用)等
1. 浏览器如何渲染网页
概述:浏览器渲染一共有五步
HTML
并构建 DOM
树。CSS
构建 CSSOM
树。DOM
与 CSSOM
合并成一个渲染树。GPU
绘制,合成图层,显示在屏幕上第四步和第五步是最耗时的部分,这两步合起来,就是我们通常所说的渲染
具体如下图过程如下图所示
渲染
重新渲染需要重复之前的第四步(重新生成布局)+第五步(重新绘制)或者只有第五个步(重新绘制)
CSSOM
树时,会阻塞渲染,直至 CSSOM
树构建完成。并且构建 CSSOM
树是一个十分消耗性能的过程,所以应该尽量保证层级扁平,减少过度层叠,越是具体的 CSS
选择器,执行速度越慢HTML
解析到 script
标签时,会暂停构建 DOM
,完成后才会从暂停的地方重新开始。也就是说,如果你想首屏渲染的越快,就越不应该在首屏就加载 JS
文件。并且CSS
也会影响 JS
的执行,只有当解析完样式表才会执行 JS
,所以也可以认为这种情况下,CSS
也会暂停构建 DOM
2. 浏览器渲染五个阶段
2.1 第一步:解析HTML标签,构建DOM树
在这个阶段,引擎开始解析
html
,解析出来的结果会成为一棵dom
树dom
的目的至少有2
个
getElementById
等等)当解析器到达script标签的时候,发生下面四件事情
html
解析器停止解析,js
引擎,执行js
代码html
解析器的控制权由此可以得到第一个结论1
<script>
标签是阻塞解析的,将脚本放在网页尾部会加速代码渲染。defer
和async
属性也能有助于加载外部脚本。defer
使得脚本会在dom
完整构建之后执行;async
标签使得脚本只有在完全available
才执行,并且是以非阻塞的方式进行的2.2 第二步:解析CSS标签,构建CSSOM树
html
解析器碰到脚本后会做的事情,接下来我们看下html
解析器碰到样式表会发生的情况js
会阻塞解析,因为它会修改文档(document
)。css
不会修改文档的结构,如果这样的话,似乎看起来css
样式不会阻塞浏览器html
解析。但是事实上 css
样式表是阻塞的。阻塞是指当cssom
树建立好之后才会进行下一步的解析渲染通过以下手段可以减轻cssom带来的影响
script
脚本放在页面底部css
样式表media type
和media query
区分,这样有助于我们将css
资源标记成非阻塞渲染的资源。2.3 第三步:把DOM和CSSOM组合成渲染树(render tree)
2.4 第四步:在渲染树的基础上进行布局,计算每个节点的几何结构
布局(
layout
):定位坐标和大小,是否换行,各种position
,overflow
,z-index
属性
2.5 调用 GPU 绘制,合成图层,显示在屏幕上
将渲染树的各个节点绘制到屏幕上,这一步被称为绘制
painting
3. 渲染优化相关
3.1 Load 和 DOMContentLoaded 区别
Load
事件触发代表页面中的 DOM
,CSS
,JS
,图片已经全部加载完毕。DOMContentLoaded
事件触发代表初始的 HTML
被完全加载和解析,不需要等待 CSS
,JS
,图片加载3.2 图层
一般来说,可以把普通文档流看成一个图层。特定的属性可以生成一个新的图层。不同的图层渲染互不影响,所以对于某些频繁需要渲染的建议单独生成一个新图层,提高性能。但也不能生成过多的图层,会引起反作用。
通过以下几个常用属性可以生成新图层
3D
变换:translate3d
、translateZ
will-change
video
、iframe
标签opacity
动画转换position: fixed
3.3 重绘(Repaint)和回流(Reflow)
重绘和回流是渲染步骤中的一小节,但是这两个步骤对于性能影响很大
color
就叫称为重绘回流必定会发生重绘,重绘不一定会引发回流。回流所需的成本比重绘高的多,改变深层次的节点很可能导致父节点的一系列回流
以下几个动作可能会导致性能问题
window
大小很多人不知道的是,重绘和回流其实和 Event loop 有关
Event loop
执行完Microtasks
后,会判断 document
是否需要更新。因为浏览器是 60Hz
的刷新率,每 16ms
才会更新一次。resize
或者 scroll
,有的话会去触发事件,所以 resize
和 scroll
事件也是至少 16ms
才会触发一次,并且自带节流功能。media query
requestAnimationFrame
回调IntersectionObserver
回调,该方法用于判断元素是否可见,可以用于懒加载上,但是兼容性不好requestIdleCallback
回调常见的引起重绘的属性
color
border-style
visibility
background
text-decoration
background-image
background-position
background-repeat
outline-color
outline
outline-style
border-radius
outline-width
box-shadow
background-size
3.4 常见引起回流属性和方法
任何会改变元素几何信息(元素的位置和尺寸大小)的操作,都会触发重排,下面列一些栗子
DOM
元素;input
框中输入文字resize
事件发生时offsetWidth
和 offsetHeight
属性style
属性的值回流影响的范围
由于浏览器渲染界面是基于流失布局模型的,所以触发重排时会对周围DOM重新排列,影响的范围有两种
html
开始对整个渲染树进行重新布局。全局范围回流
<body>
<div class="hello">
<h4>hello</h4>
<p><strong>Name:</strong>BDing</p>
<h5>male</h5>
<ol>
<li>coding</li>
<li>loving</li>
</ol>
</div>
</body>
当
p
节点上发生reflow
时,hello
和body
也会重新渲染,甚至h5
和ol
都会收到影响
局部范围回流
用局部布局来解释这种现象:把一个
dom
的宽高之类的几何信息定死,然后在dom
内部触发重排,就只会重新渲染该dom
内部的元素,而不会影响到外界
3.5 减少重绘和回流
使用
translate
替代top
<div class="test"></div>
<style>
.test {
position: absolute;
top: 10px;
width: 100px;
height: 100px;
background: red;
}
</style>
<script>
setTimeout(() => {
// 引起回流
document.querySelector('.test').style.top = '100px'
}, 1000)
</script>
visibility
替换 display: none
,因为前者只会引起重绘,后者会引发回流(改变了布局)DOM
离线后修改,比如:先把 DOM
给 display:none
(有一次 Reflow)
,然后你修改100
次,然后再把它显示出来DOM
结点的属性值放在一个循环里当成循环里的变量for(let i = 0; i < 1000; i++) {
// 获取 offsetTop 会导致回流,因为需要去获取正确的值
console.log(document.querySelector('.test').style.offsetTop)
}
table
布局,可能很小的一个小改动会造成整个 table
的重新布局requestAnimationFrame
CSS
选择符从右往左匹配查找,避免 DOM
深度过深video
标签,浏览器会自动将该节点变为图层。clear:both
的空 div
元素<div class="container">
<div class="left"></div>
<div class="right"></div>
<div style="clear:both"></div>
</div>
overflow:hidden
或者 auto
样式,触发BFC
<div class="container">
<div class="left"></div>
<div class="right"></div>
</div>
.container{
width: 300px;
background-color: #aaa;
overflow:hidden;
zoom:1; /*IE6*/
}
clear: both
属性的元素实现的。<div class="container clearfix">
<div class="left"></div>
<div class="right"></div>
</div>
.clearfix{
zoom: 1; /*IE6*/
}
.clearfix:after{
content: ".";
height: 0;
clear: both;
display: block;
visibility: hidden;
}
推荐使用第三种方法,不会在页面新增div,文档结构更加清晰
.square {
width: 10%;
height: 10vw;
background: tomato;
}
.square {
width: 20%;
height: 0;
padding-top: 20%;
background: orange;
}
.square {
width: 30%;
overflow: hidden;
background: yellow;
}
.square::after {
content: '';
display: block;
margin-top: 100%;
}
1. 浅拷贝的原理和实现
自己创建一个新的对象,来接受你要重新复制或引用的对象值。如果对象属性是基本的数据类型,复制的就是基本类型的值给新对象;但如果属性是引用数据类型,复制的就是内存中的地址,如果其中一个对象改变了这个内存中的地址,肯定会影响到另一个对象
方法一:object.assign
object.assign
是 ES6 中object
的一个方法,该方法可以用于 JS 对象的合并等多个用途,其中一个用途就是可以进行浅拷贝
。该方法的第一个参数是拷贝的目标对象,后面的参数是拷贝的来源对象(也可以是多个来源)。
object.assign 的语法为:Object.assign(target, ...sources)
object.assign 的示例代码如下:
let target = {};
let source = { a: { b: 1 } };
Object.assign(target, source);
console.log(target); // { a: { b: 1 } };
但是使用 object.assign 方法有几点需要注意
Symbol
类型的属性。let obj1 = { a:{ b:1 }, sym:Symbol(1)};
Object.defineProperty(obj1, 'innumerable' ,{
value:'不可枚举属性',
enumerable:false
});
let obj2 = {};
Object.assign(obj2,obj1)
obj1.a.b = 2;
console.log('obj1',obj1);
console.log('obj2',obj2);
从上面的样例代码中可以看到,利用
object.assign
也可以拷贝Symbol
类型的对象,但是如果到了对象的第二层属性 obj1.a.b 这里的时候,前者值的改变也会影响后者的第二层属性的值,说明其中依旧存在着访问共同堆内存的问题
,也就是说这种方法还不能进一步复制,而只是完成了浅拷贝的功能
方法二:扩展运算符方式
let cloneObj = { ...obj };
/* 对象的拷贝 */
let obj = {a:1,b:{c:1}}
let obj2 = {...obj}
obj.a = 2
console.log(obj) //{a:2,b:{c:1}} console.log(obj2); //{a:1,b:{c:1}}
obj.b.c = 2
console.log(obj) //{a:2,b:{c:2}} console.log(obj2); //{a:1,b:{c:2}}
/* 数组的拷贝 */
let arr = [1, 2, 3];
let newArr = [...arr]; //跟arr.slice()是一样的效果
扩展运算符 和
object.assign
有同样的缺陷,也就是实现的浅拷贝的功能差不多
,但是如果属性都是基本类型的值,使用扩展运算符进行浅拷贝会更加方便
方法三:concat 拷贝数组
数组的
concat
方法其实也是浅拷贝,所以连接一个含有引用类型的数组时,需要注意修改原数组中的元素的属性,因为它会影响拷贝之后连接的数组。不过concat
只能用于数组的浅拷贝,使用场景比较局限。代码如下所示。
let arr = [1, 2, 3];
let newArr = arr.concat();
newArr[1] = 100;
console.log(arr); // [ 1, 2, 3 ]
console.log(newArr); // [ 1, 100, 3 ]
方法四:slice 拷贝数组
slice
方法也比较有局限性,因为它仅仅针对数组类型
。slice方法会返回一个新的数组对象
,这一对象由该方法的前两个参数来决定原数组截取的开始和结束时间,是不会影响和改变原始数组的。
slice 的语法为:arr.slice(begin, end);
let arr = [1, 2, {val: 4}];
let newArr = arr.slice();
newArr[2].val = 1000;
console.log(arr); //[ 1, 2, { val: 1000 } ]
从上面的代码中可以看出,这就是
浅拷贝的限制所在了——它只能拷贝一层对象
。如果存在对象的嵌套,那么浅拷贝将无能为力
。因此深拷贝就是为了解决这个问题而生的,它能解决多层对象嵌套问题,彻底实现拷贝
手工实现一个浅拷贝
根据以上对浅拷贝的理解,如果让你自己实现一个浅拷贝,大致的思路分为两点:
const shallowClone = (target) => {
if (typeof target === 'object' && target !== null) {
const cloneTarget = Array.isArray(target) ? []: {};
for (let prop in target) {
if (target.hasOwnProperty(prop)) {
cloneTarget[prop] = target[prop];
}
}
return cloneTarget;
} else {
return target;
}
}
利用类型判断,针对引用类型的对象进行 for 循环遍历对象属性赋值给目标对象的属性,基本就可以手工实现一个浅拷贝的代码了
2. 深拷贝的原理和实现
浅拷贝只是创建了一个新的对象,复制了原有对象的基本类型的值,而引用数据类型只拷贝了一层属性,再深层的还是无法进行拷贝
。深拷贝则不同,对于复杂引用数据类型,其在堆内存中完全开辟了一块内存地址,并将原有的对象完全复制过来存放。
这两个对象是相互独立、不受影响的,彻底实现了内存上的分离。总的来说,深拷贝的原理可以总结如下
:
将一个对象从内存中完整地拷贝出来一份给目标对象,并从堆内存中开辟一个全新的空间存放新对象,且新对象的修改并不会改变原对象,二者实现真正的分离。
方法一:乞丐版(JSON.stringify)
JSON.stringify()
是目前开发过程中最简单的深拷贝方法,其实就是把一个对象序列化成为JSON
的字符串,并将对象里面的内容转换成字符串,最后再用JSON.parse()
的方法将JSON
字符串生成一个新的对象
let a = {
age: 1,
jobs: {
first: 'FE'
}
}
let b = JSON.parse(JSON.stringify(a))
a.jobs.first = 'native'
console.log(b.jobs.first) // FE
但是该方法也是有局限性的 :
undefined
symbol
RegExp
引用类型会变成空对象Date
引用类型会变成字符串NaN
、Infinity
以及 -Infinity
,JSON
序列化的结果会变成 null
obj[key] = obj
)。function Obj() {
this.func = function () { alert(1) };
this.obj = {a:1};
this.arr = [1,2,3];
this.und = undefined;
this.reg = /123/;
this.date = new Date(0);
this.NaN = NaN;
this.infinity = Infinity;
this.sym = Symbol(1);
}
let obj1 = new Obj();
Object.defineProperty(obj1,'innumerable',{
enumerable:false,
value:'innumerable'
});
console.log('obj1',obj1);
let str = JSON.stringify(obj1);
let obj2 = JSON.parse(str);
console.log('obj2',obj2);
使用
JSON.stringify
方法实现深拷贝对象,虽然到目前为止还有很多无法实现的功能,但是这种方法足以满足日常的开发需求,并且是最简单和快捷的。而对于其他的也要实现深拷贝的,比较麻烦的属性对应的数据类型,JSON.stringify
暂时还是无法满足的,那么就需要下面的几种方法了
方法二:基础版(手写递归实现)
下面是一个实现 deepClone 函数封装的例子,通过
for in
遍历传入参数的属性值,如果值是引用类型则再次递归调用该函数,如果是基础数据类型就直接复制
let obj1 = {
a:{
b:1
}
}
function deepClone(obj) {
let cloneObj = {}
for(let key in obj) { //遍历
if(typeof obj[key] ==='object') {
cloneObj[key] = deepClone(obj[key]) //是对象就再次调用该函数递归
} else {
cloneObj[key] = obj[key] //基本类型的话直接复制值
}
}
return cloneObj
}
let obj2 = deepClone(obj1);
obj1.a.b = 2;
console.log(obj2); // {a:{b:1}}
虽然利用递归能实现一个深拷贝,但是同上面的 JSON.stringify
一样,还是有一些问题没有完全解决,例如:
Symbol
类型;只是针对普通的引用类型的值做递归复制
,而对于 Array、Date、RegExp、Error、Function
这样的引用类型并不能正确地拷贝;循环引用没有解决
。这种基础版本的写法也比较简单,可以应对大部分的应用情况。但是你在面试的过程中,如果只能写出这样的一个有缺陷的深拷贝方法,有可能不会通过。
所以为了“拯救”这些缺陷,下面我带你一起看看改进的版本,以便于你可以在面试种呈现出更好的深拷贝方法,赢得面试官的青睐。
方法三:改进版(改进后递归实现)
针对上面几个待解决问题,我先通过四点相关的理论告诉你分别应该怎么做。
Symbol
类型,我们可以使用 Reflect.ownKeys
方法;Date、RegExp
类型,则直接生成一个新的实例返回;Object
的 getOwnPropertyDescriptors
方法可以获得对象的所有属性,以及对应的特性,顺便结合 Object.create
方法创建一个新对象,并继承传入原对象的原型链;WeakMap
类型作为 Hash
表,因为 WeakMap
是弱引用类型,可以有效防止内存泄漏(你可以关注一下 Map
和 weakMap
的关键区别,这里要用 weakMap
),作为检测循环引用很有帮助,如果存在循环,则引用直接返回 WeakMap
存储的值如果你在考虑到循环引用的问题之后,还能用 WeakMap
来很好地解决,并且向面试官解释这样做的目的,那么你所展示的代码,以及你对问题思考的全面性,在面试官眼中应该算是合格的了
实现深拷贝
const isComplexDataType = obj => (typeof obj === 'object' || typeof obj === 'function') && (obj !== null)
const deepClone = function (obj, hash = new WeakMap()) {
if (obj.constructor === Date) {
return new Date(obj) // 日期对象直接返回一个新的日期对象
}
if (obj.constructor === RegExp){
return new RegExp(obj) //正则对象直接返回一个新的正则对象
}
//如果循环引用了就用 weakMap 来解决
if (hash.has(obj)) {
return hash.get(obj)
}
let allDesc = Object.getOwnPropertyDescriptors(obj)
//遍历传入参数所有键的特性
let cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc)
// 把cloneObj原型复制到obj上
hash.set(obj, cloneObj)
for (let key of Reflect.ownKeys(obj)) {
cloneObj[key] = (isComplexDataType(obj[key]) && typeof obj[key] !== 'function') ? deepClone(obj[key], hash) : obj[key]
}
return cloneObj
}
// 下面是验证代码
let obj = {
num: 0,
str: '',
boolean: true,
unf: undefined,
nul: null,
obj: { name: '我是一个对象', id: 1 },
arr: [0, 1, 2],
func: function () { console.log('我是一个函数') },
date: new Date(0),
reg: new RegExp('/我是一个正则/ig'),
[Symbol('1')]: 1,
};
Object.defineProperty(obj, 'innumerable', {
enumerable: false, value: '不可枚举属性' }
);
obj = Object.create(obj, Object.getOwnPropertyDescriptors(obj))
obj.loop = obj // 设置loop成循环引用的属性
let cloneObj = deepClone(obj)
cloneObj.arr.push(4)
console.log('obj', obj)
console.log('cloneObj', cloneObj)
我们看一下结果,cloneObj
在 obj
的基础上进行了一次深拷贝,cloneObj
里的 arr
数组进行了修改,并未影响到 obj.arr
的变化,如下图所示
float + margin,float + calc
/* 方案1 */
.left {
width: 120px;
float: left;
}
.right {
margin-left: 120px;
}
/* 方案2 */
.left {
width: 120px;
float: left;
}
.right {
width: calc(100% - 120px);
float: left;
}
在 JavaScript 中,基本类型是没有属性和方法的,但是为了便于操作基本类型的值,在调用基本类型的属性或方法时 JavaScript 会在后台隐式地将基本类型的值转换为对象,如:
const a = "abc";
a.length; // 3
a.toUpperCase(); // "ABC"
在访问'abc'.length
时,JavaScript 将'abc'
在后台转换成String('abc')
,然后再访问其length
属性。
JavaScript也可以使用Object
函数显式地将基本类型转换为包装类型:
var a = 'abc'
Object(a) // String {"abc"}
也可以使用valueOf
方法将包装类型倒转成基本类型:
var a = 'abc'
var b = Object(a)
var c = b.valueOf() // 'abc'
看看如下代码会打印出什么:
var a = new Boolean( false );
if (!a) {
console.log( "Oops" ); // never runs
}
答案是什么都不会打印,因为虽然包裹的基本类型是false
,但是false
被包裹成包装类型后就成了对象,所以其非值为false
,所以循环体中的内容不会运行。
|| 和 && 首先会对第一个操作数执行条件判断,如果其不是布尔值就先强制转换为布尔类型,然后再执行条件判断。
|| 和 && 返回它们其中一个操作数的值,而非条件判断的结果
(1)创建新节点
createDocumentFragment() //创建一个DOM片段
createElement() //创建一个具体的元素
createTextNode() //创建一个文本节点
(2)添加、移除、替换、插入
appendChild(node)
removeChild(node)
replaceChild(new,old)
insertBefore(new,old)
(3)查找
getElementById();
getElementsByName();
getElementsByTagName();
getElementsByClassName();
querySelector();
querySelectorAll();
(4)属性操作
getAttribute(key);
setAttribute(key, value);
hasAttribute(key);
removeAttribute(key);
为了将值转换为相应的基本类型值,抽象操作 ToPrimitive 会首先(通过内部操作 DefaultValue)检查该值是否有valueOf()方法。如果有并且返回基本类型值,就使用该值进行强制类型转换。如果没有就使用 toString() 的返回值(如果存在)来进行强制类型转换。
如果 valueOf() 和 toString() 均不返回基本类型值,会产生 TypeError 错误。
在开发过程中遇到类似这样的问题:
let n1 = 0.1, n2 = 0.2
console.log(n1 + n2) // 0.30000000000000004
这里得到的不是想要的结果,要想等于0.3,就要把它进行转化:
(n1 + n2).toFixed(2) // 注意,toFixed为四舍五入
toFixed(num)
方法可把 Number 四舍五入为指定小数位数的数字。那为什么会出现这样的结果呢?
计算机是通过二进制的方式存储数据的,所以计算机计算0.1+0.2的时候,实际上是计算的两个数的二进制的和。0.1的二进制是0.0001100110011001100...
(1100循环),0.2的二进制是:0.00110011001100...
(1100循环),这两个数的二进制都是无限循环的数。那JavaScript是如何处理无限循环的二进制小数呢?
一般我们认为数字包括整数和小数,但是在 JavaScript 中只有一种数字类型:Number,它的实现遵循IEEE 754标准,使用64位固定长度来表示,也就是标准的double双精度浮点数。在二进制科学表示法中,双精度浮点数的小数部分最多只能保留52位,再加上前面的1,其实就是保留53位有效数字,剩余的需要舍去,遵从“0舍1入”的原则。
根据这个原则,0.1和0.2的二进制数相加,再转化为十进制数就是:0.30000000000000004
。
下面看一下双精度数是如何保存的:
对于0.1,它的二进制为:
0.00011001100110011001100110011001100110011001100110011001 10011...
转为科学计数法(科学计数法的结果就是浮点数):
1.1001100110011001100110011001100110011001100110011001*2^-4
可以看出0.1的符号位为0,指数位为-4,小数位为:
1001100110011001100110011001100110011001100110011001
那么问题又来了,指数位是负数,该如何保存呢?
IEEE标准规定了一个偏移量,对于指数部分,每次都加这个偏移量进行保存,这样即使指数是负数,那么加上这个偏移量也就是正数了。由于JavaScript的数字是双精度数,这里就以双精度数为例,它的指数部分为11位,能表示的范围就是0~2047,IEEE固定双精度数的偏移量为1023。
-1022~1013
。对于上面的0.1的指数位为-4,-4+1023 = 1019 转化为二进制就是:1111111011
.
所以,0.1表示为:
0 1111111011 1001100110011001100110011001100110011001100110011001
说了这么多,是时候该最开始的问题了,如何实现0.1+0.2=0.3呢?
对于这个问题,一个直接的解决方法就是设置一个误差范围,通常称为“机器精度”。对JavaScript来说,这个值通常为2-52,在ES6中,提供了Number.EPSILON
属性,而它的值就是2-52,只要判断0.1+0.2-0.3
是否小于Number.EPSILON
,如果小于,就可以判断为0.1+0.2 ===0.3
function numberepsilon(arg1,arg2){
return Math.abs(arg1 - arg2) < Number.EPSILON;
}
console.log(numberepsilon(0.1 + 0.2, 0.3)); // true
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。