虚拟DOM(Virtual DOM)是指一个虚拟的、内存中的DOM节点树,它是通过JavaScript对象来模拟真实的DOM结构,而不是直接操作真实的DOM。虚拟DOM通常会在每次页面渲染时被创建,通过对虚拟DOM的修改来实现对页面的更新。
虚拟DOM的实现原理是在JavaScript中对DOM树进行操作,然后将修改的结果映射到真实的DOM上。这种映射方式可以大大减少DOM操作的次数,从而提高页面性能。因为真实的DOM是非常庞大而复杂的,而且在每次更新时都要重新计算布局和样式,所以频繁地直接操作真实的DOM会非常消耗性能。
通过使用虚拟DOM,开发者可以在JavaScript中对DOM进行修改,然后由虚拟DOM引擎自动计算出DOM树的差异,最后只需要更新差异部分的真实DOM,以此来实现高效的页面渲染和更新。
优点:
降低浏览器性能消耗
因为Javascript的运算速度远大于DOM操作的执行速度,因此,运用patching算法来计算出真正需要更新的节点,最大限度地减少DOM操作,从而提高性能。
在vnode技术出现之前,我们要改变页面展示的内容只能通过遍历查询 dom 树的方式找到需要修改的 dom,然后修改样式行为或者结构,来达到更新 ui 的目的。这种方式相当消耗计算资源,因为每次查询 dom 几乎都需要遍历整颗 dom树。
在vnode技术出现之后,我们建立一个虚拟 dom 对象来对应真实的 dom 树,那么每次 dom 的更改就变成了 js 对象的属性的更改 ,这样一来就能查找 js 对象的属性变化要比查询 dom 树的 性能开销小。
diff算法,减少回流和重绘
通过diff算法,优化遍历,对真实dom进行打补丁式的新增、修改、删除,实现局部更新,减少回流和重绘。
vnode优化性能核心思想,就是每次更新 dom 都尽量避免刷新整个页面,而是有针对性的 去刷新那被更改的一部分 ,来释放掉被无效渲染占用的 gpu,cup性能。同时,也减少了大量的dom操作,减少了浏览器的回流和重绘。
跨平台
虚拟 DOM 本质上是 JavaScript 对象,而 DOM 与平台强相关,相比之下虚拟 DOM ,可以进行更方便地跨平台操作,例如:服务器渲染、weex 开发等等
缺点
首次显示要慢些:
首次渲染大量DOM时,由于多了一层虚拟DOM的计算, 会比innerHTML插入慢
无法进行极致优化:
虽然虚拟 DOM + 合理的优化,足以应对绝大部分应用的性能需求,但在一些性能要求极高的应用中 无法进行针对性的极致优化。
这是一个性能 vs. 可维护性的取舍。框架的意义在于为你掩盖底层的 DOM 操作,让你用更声明式的方式来描述你的目的,从而让你的代码更容易维护。
没有任何框架可以比纯手动的优化 DOM 操作更快,因为框架的 DOM 操作层需要应对任何上层 API 可能产生的操作,它的实现必须是普适的。
XSS(Cross-Site Scripting,跨站脚本攻击)是一种代码注入攻击。攻击者在目标网站上注入恶意代码,当被攻击者登陆网站时就会执行这些恶意代码,这些脚本可以读取 cookie,session tokens,或者其它敏感的网站信息,对用户进行钓鱼欺诈,甚至发起蠕虫攻击等。
XSS避免方式
CSRF(Cross-site request forgery)跨站请求伪造:攻击者诱导受害者进入第三方网站,在第三方网站中,向被攻击网站发送跨站请求。利用受害者在被攻击网站已经获取的注册凭证,绕过后台的用户验证,达到冒充用户对被攻击的网站执行某项操作的目的。
CSRF避免方式:
DDoS又叫分布式拒绝服务,全称 Distributed Denial of Service,其原理就是利用大量的请求造成资源过载,导致服务不可用。
DDos避免方式
性能优化大全:
【前端面试题】10—21道关于性能优化的面试题(附答案) (qq.com)
重排/回流(Reflow)
当DOM的变化影响了元素的几何信息,浏览器需要重新计算元素的几何属性,将其安放在界面中的正确位置,这个过程叫做重排。表现为重新生成布局,重新排列元素。
重绘(Repaint):
当一个元素的外观发生改变,但没有改变布局,重新把元素外观绘制出来的过程,叫做重绘。表现为某些元素的外观被改变
单单改变元素的外观,肯定不会引起网页重新生成布局,但当浏览器完成重排之后,将会重新绘制受到此次重排影响的部分
代价
重排和重绘代价是高昂的,它们会破坏用户体验,并且让UI展示非常迟缓,而相比之下重排的性能影响更大,在两者无法避免的情况下,一般我们宁可选择代价更小的重绘。
划重点
『重绘』不一定会出现『重排』,『重排』必然会出现『重绘』。
create阶段
:vue实例被创建beforeCreate:
最初调用触发,创建前,此时data和methods中的数据都还没有初始化,data和events都不能用created:
创建完毕,data中有值,未挂载,data和events已经初始化好,data已经具有响应式;在这里可以发送请求mount阶段:
vue实例被挂载到真实DOM节点beforeMount:
在模版编译之后,渲染之前触发,可以发起服务端请求,去数据,ssr中不可用,基本用不上这个hookmounted:
在渲染之后触发,此时可以操作DOM,并能访问组件中的DOM以及$ref,SSR中不可用update阶段:
当vue实例里面的data数据变化时,触发组件的重新渲染beforeUpdate
:更新前,在数据变化后,模版改变前触发,切勿使用它监听数据变化updated:
更新后,在数据改变后,模版改变后触发,常用于重渲染案后的打点,性能检测或触发vue组件中非vue组件的更新destroy阶段
:vue实例被销毁beforeDestroy
:实例被销毁前,组件卸载前触发,此时可以手动销毁一些方法,可以在此时清理事件、计时器或者取消订阅操作destroyed
: 卸载完毕后触发,销毁后,可以做最后的打点或事件触发操作渲染顺序问题:v-if 和 v-for 在渲染顺序上存在冲突。Vue.js 的渲染顺序规定,v-for 的优先级高于 v-if。也就是说,在每次循环迭代时,v-if 的条件判断都会执行一次。这可能会导致一些意外的结果,特别是在有大量数据和复杂条件逻辑的情况下。
可读性和维护性:同时使用 v-if 和 v-for 可能会增加代码的复杂性,降低代码的可读性和维护性。两个指令的同时使用可能导致逻辑难以理解和排查错误,特别是在较大的代码库中。
如何解决这个问题:
使用计算属性:将需要根据条件进行过滤的数据处理逻辑放到计算属性中,并在模板中使用该计算属性进行循环渲染。这样可以避免在模板中使用 v-if 和 v-for 同时存在的问题。
数据预处理:在数据传递给模板之前,先对数据进行处理,根据条件将需要渲染的数据进行筛选或过滤,然后再进行循环渲染。
使用 元素:如果你确实需要在同一个元素上同时使用 v-if 和 v-for,可以使用 元素来包裹它们。 元素在渲染时不会被保留,可以作为一个容器来使用这两个指令,避免同时使用 v-if 和 v-for 时可能导致的问题。
共同点
都能控制元素的显示和隐藏;
不同点
实现本质方法不同,v-show本质就是通过控制css中的display设置为none,控制隐藏,只会编译一次;v-if是动态的向DOM树内添加或者删除DOM元素,若初始值为false,就不会编译了。而且v-if不停的销毁和创建比较消耗性能。
如果要频繁切换某节点,使用v-show(切换开销比较小,初始开销较大)。如果不需要频繁切换某节点使用v-if(初始渲染开销较小,切换开销比较大)。
作用
key的作用是为了在diff算法执行时更快的找到对应的节点,提高diff速度,更高效的更新虚拟DOM;
vue和react都是采用diff算法来对比新旧虚拟节点,从而更新节点。在vue的diff函数中,会根据新节点的key去对比旧节点数组中的key,从而找到相应旧节点。如果没找到就认为是一个新增节点。而如果没有key,那么就会采用遍历查找的方式去找到对应的旧节点。一种一个map映射,另一种是遍历查找。相比而言。map映射的速度更快。
副作用
为了在数据变化时强制更新组件,以避免“就地复用”带来的副作用。
当 Vue.js 用 v-for 更新已渲染过的元素列表时,它默认用“就地复用”策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序,而是简单复用此处每个元素,并且确保它在特定索引下显示已被渲染过的每个元素。重复的key会造成渲染错误。
hash:使用URL的hash值作为路由。
Vue的默认路由模式。
支持所有浏览器。
history:使用History API:pushState() 和 replaceState() 方法。
HTML5之后支持。
abstract:支持所有 JavaScript 运行环境(包括Node.js 服务器端)
如果发现没有浏览器的 API,路由会自动强制进入这个模式。
根据MDN:Location 接口的 hash 属性返回一个 USVString,包含URL标识中的 '#' 和 后面URL片段标识符,被称为 hash。
例如:http://www.abc.com/#/article,hash 的值为 #/article。
第一个#后的所有字符,都会被浏览器解读为位置标识符,它只用来表示页面位置(一个锚点)。这些字符都不会被发送到服务器端。
单单改变#后的部分,浏览器只会滚动到相应位置,不会重新加载网页。
每次改变#后的部分,都会在浏览器的访问历史中增加一个记录。见:此文
window.location.hash 表示 hash 值。此属性可读可写。
使用 window.addEventListener("hashchange", fun) 可以监听 hash 的变化
原理
Vue的Router的hash模式的原理是:使用 window.addEventListener("hashchange", fun) 监听 hash 的变化,hash变化之后,根据这个新的hash找到相应的页面,然后更新视图。
优点
后端不需要额外配置
原因:#及之后的字符不会被发到服务器
例如:http://www.abc.com/#/article在Vue Router中有对应的路由,而我直接输入了http://www.abc.com/#/article/id,Vue Router中没有对应路由。
此时并不会报错。
原因:只有http://www.abc.com会被发送到服务器。
缺点
不美观(url中带了个“#”)
window.history 提供了两类API:
跳到某个浏览记录:back(), forward(), go(),
添加/修改历史记录:pushState(), replaceState()
这些方法都只修改当前url,不会向后端发起请求。
原理
Vue监听url改变这个事件:window.addEventListener('popstate', fun);
Vue在切换页面时,使用pushState(), replaceState()来修改当前的url
切换页面之后,popstate事件被触发,调用相应的回调函数更新视图
优点
美观(url中不带“#”)
缺点
后端需要额外配置
原因:当直接访问一个url时会请求后端。例如:刷新页面、直接通过url访问
例如:http://www.abc.com/article在Vue Router中有对应的路由,而我直接输入了http://www.abc.com/article/id,Vue Router中没有对应路由。
此时会报错:404。
原因:后端没有相应的接口。
解决方案:
后端(Apache 或 Nginx)进行简单的路由配置
Vue配置路由的 404 页面。例如:
const router = new Router({
mode: 'history',
routes: [
{ path: '*', component: NotFoundComponent }
]
})
abstract 模式针对的是没有浏览器环境的情况。
比如 Weex 客户端开发,内部是没有浏览器 API 的,那么 Vue-Router 自身会对环境做校验,强制切换到 abstract 模式。
如果在 Vue-Router 的配置项中不写 mode 的值,在浏览器环境下会默认启用 Hash 模式,在移动客户端下使用 abstract 模式。
原理
通过 stack 和 index 2个变量,模拟出浏览器的历史调用记录。
var
let
const
function unique (arr) {
return Array.from(new Set(arr))
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
//[1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {}, {}]
不考虑兼容性,这种去重的方法代码最少。这种方法还无法去掉“{}”空对象,后面的高阶方法会添加去掉重复“{}”的方法。
function unique(arr){
for(var i=0; i
双层循环,外层循环元素,内层循环时比较值。值相同时,则删去这个值。
想快速学习更多常用的ES6语法,可以看我之前的文章《学习ES6笔记──工作中常用到的ES6语法》。
function unique(arr) {
if (!Array.isArray(arr)) {
console.log('type error!')
return
}
var array = [];
for (var i = 0; i < arr.length; i++) {
if (array .indexOf(arr[i]) === -1) {
array .push(arr[i])
}
}
return array;
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
// [1, "true", true, 15, false, undefined, null, NaN, NaN, "NaN", 0, "a", {…}, {…}] //NaN、{}没有去重
新建一个空的结果数组,for 循环原数组,判断结果数组是否存在当前元素,如果有相同的值则跳过,不相同则push进数组。
function unique(arr) {
if (!Array.isArray(arr)) {
console.log('type error!')
return;
}
arr = arr.sort()
var arrry= [arr[0]];
for (var i = 1; i < arr.length; i++) {
if (arr[i] !== arr[i-1]) {
arrry.push(arr[i]);
}
}
return arrry;
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
// [0, 1, 15, "NaN", NaN, NaN, {…}, {…}, "a", false, null, true, "true", undefined] //NaN、{}没有去重
利用sort()排序方法,然后根据排序后的结果进行遍历及相邻元素比对。
function unique(arr) {
if (!Array.isArray(arr)) {
console.log('type error!')
return
}
var array =[];
for(var i = 0; i < arr.length; i++) {
if( !array.includes( arr[i]) ) {//includes 检测数组是否有某个值
array.push(arr[i]);
}
}
return array
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
//[1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {…}, {…}] //{}没有去重
function unique(arr) {
var obj = {};
return arr.filter(function(item, index, arr){
return obj.hasOwnProperty(typeof item + item) ? false : (obj[typeof item + item] = true)
})
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
//[1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {…}] //所有的都去重了
利用hasOwnProperty 判断是否存在对象属性
function unique(arr) {
return arr.filter(function(item, index, arr) {
//当前元素,在原始数组中的第一个索引==当前索引值,否则返回当前元素
return arr.indexOf(item, 0) === index;
});
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
//[1, "true", true, 15, false, undefined, null, "NaN", 0, "a", {…}, {…}]
function unique(arr) {
var array= arr;
var len = array.length;
array.sort(function(a,b){ //排序后更加方便去重
return a - b;
})
function loop(index){
if(index >= 1){
if(array[index] === array[index-1]){
array.splice(index,1);
}
loop(index - 1); //递归loop,然后数组去重
}
}
loop(len-1);
return array;
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
//[1, "a", "true", true, 15, false, 1, {…}, null, NaN, NaN, "NaN", 0, "a", {…}, undefined]
function arrayNonRepeatfy(arr) {
let map = new Map();
let array = new Array(); // 数组用于返回结果
for (let i = 0; i < arr.length; i++) {
if(map .has(arr[i])) { // 如果有该key值
map .set(arr[i], true);
} else {
map .set(arr[i], false); // 如果没有该key值
array .push(arr[i]);
}
}
return array ;
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
//[1, "a", "true", true, 15, false, 1, {…}, null, NaN, NaN, "NaN", 0, "a", {…}, undefined]
创建一个空Map数据结构,遍历需要去重的数组,把数组的每一个元素作为key存到Map中。由于Map中不会出现相同的key值,所以最终得到的就是去重后的结果
节流
事件触发后,规定时间内,事件处理函数不能再次被调用。也就是说在规定的时间内,函数只能被调用一次,且是最先被触发调用的那次。
防抖
多次触发事件,事件处理函数只能执行一次,并且是在触发操作结束时执行。也就是说,当一个事件被触发准备执行事件函数前,会等待一定的时间(这时间是码农自己去定义的,比如 1 秒),如果没有再次被触发,那么就执行,如果被触发了,那就本次作废,重新从新触发的时间开始计算,并再次等待 1 秒,直到能最终执行!
使用场景:
节流:滚动加载更多、搜索框搜的索联想功能、高频点击、表单重复提交……
防抖:搜索框搜索输入,并在输入完以后自动搜索、手机号,邮箱验证输入检测、窗口大小 resize 变化后,再重新渲染。
/**
* 节流函数 一个函数执行一次后,只有大于设定的执行周期才会执行第二次。有个需要频繁触发的函数,出于优化性能的角度,在规定时间内,只让函数触发的第一次生效,后面的不生效。
* @param fn要被节流的函数
* @param delay规定的时间
*/
function throttle(fn, delay) {
//记录上一次函数触发的时间
var lastTime = 0;
return function(){
//记录当前函数触发的时间
var nowTime = Date.now();
if(nowTime - lastTime > delay){
//修正this指向问题
fn.call(this);
//同步执行结束时间
lastTime = nowTime;
}
}
}
document.onscroll = throttle(function () {
console.log('scllor事件被触发了' + Date.now());
}, 200);
/**
* 防抖函数 一个需要频繁触发的函数,在规定时间内,只让最后一次生效,前面的不生效
* @param fn要被节流的函数
* @param delay规定的时间
*/
function debounce(fn, delay) {
//记录上一次的延时器
var timer = null;
return function () {
//清除上一次的演示器
clearTimeout(timer);
//重新设置新的延时器
timer = setTimeout(function(){
//修正this指向问题
fn.apply(this);
}, delay);
}
}
document.getElementById('btn').onclick = debounce(function () {
console.log('按钮被点击了' + Date.now());
}, 1000);
类型
包括 :值类型
(基本对象类型)和引用类型
(复杂对象类型)
基本类型(值类型)
: Number(数字),String(字符串),Boolean(布尔),Symbol(符号),null(空),undefined(未定义)在内存中占据固定大小,保存在栈内存中
引用类型(复杂数据类型)
: Object(对象)、Function(函数)。其他还有Array(数组)、Date(日期)、RegExp(正则表达式)、特殊的基本包装类型(String、Number、Boolean) 以及单体内置对象(Global、Math)等 引用类型的值是对象 保存在堆内存中,栈内存存储的是对象的变量标识符以及对象在堆内存中的存储地址。
使用场景:
Symbol:使用Symbol来作为对象属性名(key) 利用该特性,把一些不需要对外操作和访问的属性使用Symbol来定义
BigInt:由于在 Number 与 BigInt 之间进行转换会损失精度,因而建议仅在值可能大于253 时使用 BigInt 类型,并且不在两种类型之间进行相互转换。
(1)通过以下代码创建新节点。
createDocument Fragment ()
//创建一个D0M片段
createElement ()
//创建一个具体的元素
createTextNode ()
//创建一个文本节点
(2)通过以下代码添加、移除、替换、插入节点
appendchild()
removechild()
eplacechild ()
insertBefore ()
//并没有 insertAfter()
(3)通过以下代码查找节点。
getElementsByTagName ()
//通过标签名称查找节点
getElementsByName ()
//通过元素的name属性的值查找节点(IE容错能力较强,会得到一个数//组,其中包括id等于name值的节点)
getElementById(
//通过元素Id查找节点,具有唯一性
get是获取数据,post是修改数据
get把请求的数据放在url上, 以?分割URL和传输数据,参数之间以&相连,所以get不太安全。而post把数据放在HTTP的包体内(request body 相对安全)
get提交的数据最大是2k( 限制实际上取决于浏览器), post理论上没有限制。
GET产生一个TCP数据包,浏览器会把http header和data一并发送出去,服务器响应200(返回数据); POST产生两个TCP数据包,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)。
GET请求会被浏览器主动缓存,而POST不会,除非手动设置。
本质区别:GET是幂等的,而POST不是幂等的
这里的幂等性:幂等性是指一次和多次请求某一个资源应该具有同样的副作用。简单来说意味着对同一URL的多个请求应该返回同样的结果。
正因为它们有这样的区别,所以不应该且不能用get请求做数据的增删改这些有副作用的操作。因为get请求是幂等的,在网络不好的隧道中会尝试重试。如果用get请求增数据,会有重复操作的风险,而这种重复操作可能会导致副作用(浏览器和操作系统并不知道你会用get请求去做增操作)。