vuex中模块下注意的点
如果在模块中没有定义namespaced:true时,每个模块中的mutations和actions都是在全局命名空间下的;
actions对象中的方法有一个参数对象ctx。里面分别{state,commit,rootState};actions默认只会提交本地模块中的mutations。如果需要提交全局或者其他模块,需要在commit方法的第三个参数上加上{root:true}。
vuex默认是不支持热更新的,要自己配置
export default ()=>{ // 这里需要赋给一个store变量
const store = new Vuex.Store({ state:state, mutations:mutations, getters:getters })
// 热更新模块
if(module.hot){
// 跟上面一样,写入对应的分割模块路径
module.hot.accept([
'./state/state',
'./mutations/mutations',
'./getters/getters'
],()=>{
// 开启热更替
const newState = require('./state/state').default
const newMutations = require('./mutations/mutations').default
const newGetters = require('./getters/getters').default
store.hotUpdate({
state:newState,
mutations:newMutations,
getters:newGetters
})
})
}
return store }
闭包
什么是闭包
闭包是指有权访问另一个函数作用域中变量的函数;
-
红宝书:闭包是指有权访问另一个函数作用域中变量的函数
-
MDN: 闭包是指那些能够访问自由变量的函数,这里的自由变量是指外部作用域中的变量
闭包的优缺点
优点 是私有化数据,在私有化数据的基础上保持数据;
缺点 使用不恰当会导致内存泄漏,在不需要用到的时候及时把变量置为null
- 闭包的应用是非常广泛的,比方我们常见的节流,防抖,函数柯理化,在vue,react源码中也应用广泛(
如果你们接的住vue,react源码中,具体的应用场景你就可以答)
内存泄漏
垃圾回收
垃圾回收机制(GC)
程序工作过程中会产生很多 垃圾,这些垃圾是程序不用的内存或者是之前用过了,以后不会再用的内存空间,而 GC 就是负责回收垃圾的;
JavaScript 垃圾回收机制的原理说白了也就是定期找出那些不再用到的内存(变量),然后释放其内存
垃圾回收算法策略
- 标记清除算法
- 引用计数算法
引用计数算法
引用计数(Reference Counting),这其实是早先的一种垃圾回收算法,它把 对象是否不再需要 简化定义为 对象有没有其他对象引用到它,如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收,目前很少使用这种算法了
它的策略是跟踪记录每个变量值被使用的次数
-
当声明了一个变量并且将一个引用类型赋值给该变量的时候这个值的引用次数就为 1
-
如果同一个值又被赋给另一个变量,那么引用数加 1
-
如果该变量的值被其他的值覆盖了,则引用次数减 1
-
当这个值的引用次数变为 0 的时候,说明没有变量在使用,这个值没法被访问了,回收空间,垃圾回收器会在运行的时候清理掉引用次数为 0 的值占用的内存
如下例
let a = new Object() // 此对象的引用计数为 1(a引用)
let b = a // 此对象的引用计数是 2(a,b引用)
a = null // 此对象的引用计数为 1(b引用)
b = null // 此对象的引用计数为 0(无引用)
... // GC 回收此对象
这种方式是不是很简单?确实很简单,不过在引用计数这种算法出现没多久,就遇到了一个很严重的问题——循环引用,即对象 A 有一个指针指向对象 B,而对象 B 也引用了对象 A ,如下面这个例子
function test(){
let A = new Object()
let B = new Object()
A.b = B
B.a = A
}
复制代码
如上所示,对象 A 和 B 通过各自的属性相互引用着,按照上文的引用计数策略,它们的引用数量都是 2,但是,在函数 test 执行完成之后,对象 A 和 B 是要被清理的,但使用引用计数则不会被清理,因为它们的引用数量不会变成 0,假如此函数在程序中被多次调用,那么就会造成大量的内存不会被释放
优点
引用计数算法的优点我们对比标记清除来看就会清晰很多,首先引用计数在引用值为 0 时,也就是在变成垃圾的那一刻就会被回收,所以它可以立即回收垃圾
而标记清除算法需要每隔一段时间进行一次,那在应用程序(JS脚本)运行过程中线程就必须要暂停去执行一段时间的 GC,另外,标记清除算法需要遍历堆里的活动以及非活动对象来清除,而引用计数则只需要在引用时计数就可以了
缺点
引用计数的缺点想必大家也都很明朗了,首先它需要一个计数器,而此计数器需要占很大的位置,因为我们也不知道被引用数量的上限,还有就是无法解决循环引用无法回收的问题,这也是最严重的
标记清除算法
此算法分为 标记 和 清除 两个阶段,标记阶段即为所有活动对象做上标记,清除阶段则把没有标记(也就是非活动对象)销毁;
整个标记清除算法大致过程就像下面这样
- 垃圾收集器在运行时会给内存中的所有变量都加上一个标记,假设内存中所有对象都是垃圾,全标记为0
- 然后从各个根对象开始遍历,把不是垃圾的节点改成1
- 清理所有标记为0的垃圾,销毁并回收它们所占用的内存空间
- 最后,把所有内存中对象标记修改为0,等待下一轮垃圾回收
优点
标记清除算法的优点只有一个,那就是实现比较简单,打标记也无非打与不打两种情况,这使得一位二进制位(0和1)就可以为其标记,非常简单;
PS:标记清除算法的缺点补充
归根结底,标记清除算法的缺点在于清除之后剩余的对象位置不变而导致的空闲内存不连续,所以只要解决这一点,两个缺点都可以完美解决了
而 标记整理(Mark-Compact)算法 就可以有效地解决,它的标记阶段和标记清除算法没有什么不同,只是标记结束后,标记整理算法会将活着的对象(即不需要清理的对象)向内存的一端移动,最后清理掉边界的内存(如下图)
事件循环
异步又分为宏任务和微任务
宏任务:定时器,网络请求,图片加载;
微任务:promise中的then(),async的await,mutationObserve(监听dom变化)
注意点: 微任务在下一轮dom渲染之前执行,宏任务在之后执行;
event loop
for和forEach哪个快
vue知识点
vue的生命周期
vue2和vue3中diff的算法区别
diff算法的注意点:
- 只比较同一层级,不跨级比较;
- tag不同则删除重建(不再去比较内部细节);
- 子节点通过key区分(updataChildren函数去比较子节点)
vue2是双端比较
vue3是最长递增子序列
vue循环时为什么必须使用key
- vdom 的diff算法会根据key来判断元素是否要删除;
- 匹配了key,则只移动元素 - 性能较好;
- 未匹配key,则删除重建-性能较差;
MeMoryHistory:
没有前进和后退的按钮,去点击时查看到相应的路由,而且地址中不会显示路由的名称;但是hash模式和webhistory模式的时候可以通过点击前进和后退按钮,进入相应的路由中;
vue组件通讯有几种方式
$attr
refs
refs:在父组件中获取子组件的属性和方法;
provide和inject
移动端H5 click有300ms延迟,怎么解决
300ms延迟产生的背景:双击屏幕时,为了判断是第一次点击,还是双击;
解决方法:
网络请求中,token和cookie的区别
cookie
HTTPs加密过程和原理
前端攻击手段有哪些,如何预防
xss
描述从输入URL到页面展示的完整过程
网络请求
- DNS(域名系统)[得到ip],建立TCP连接(三次握手)
- 浏览器通过ip地址,发起HTTP请求
- 收到请求响应,得到HTML源代码(此时只是代码的字符串)
- 解析HTML过程中,遇到静态资源【js,css,图片,视频】还会继续发起网络请求
- 静态资源可能有强缓存,此时不必请求;
解析
渲染(render树绘制到页面)
重新渲染:就可能引起重绘和重排
defer:延迟;
async:异步;
总结
- 网络请求阶段: URL输入到浏览器后,通个DNS获取到IP地址,建立TCP三次握手;浏览器发起HTTP请求;收到请求资源,获取对应的HTML源代码;
- 解析阶段:将HTML源代码结构化;解析HTML构建dom树;解析css构建cssom树(style tree);将两者结合构建成render Tree;
- 渲染阶段:计算各个dom的尺寸,定位最后绘制到页面中;遇到js可能会执行;异步css,图片加载,可能会触发重新渲染;
js代码常用的手写方法
用js函数实现数组扁平化
手写方法获取类型
new一个对象的全过程
两者打印都是{},但是obj2可以利用原型链去访问参数中的数据或者方法
遍历DOM树
<body>
<div id="box">
<p>hello<b>world</b></p>
<img src="" alt="" />
<!-- 注释 -->
<ul>
<li>a</li>
<li>b</li>
</ul>
</div>
<script>
//访问节点
function visitNode(n){
//注释
if(n instanceof Comment){
console.log('Comment node ---',n.textContent);
}
//文本
if(n instanceof Text){
var t = n.textContent?.trim()
if(t){
console.log('Text node ---',n.textContent.trim());
}
}
//元素内容
if(n instanceof HTMLElement){
console.log('HTMLElement node ---',n.tagName.toLowerCase());
}
}
//深度遍历dom:简洁的说就是一个节点将它的后代节点遍历完后再去遍历其他同级节点
function deepTraverse(root){
visitNode(root)
var childNodes = root.childNodes
if(childNodes.length){
//有多个字节点
childNodes.forEach(child => {
deepTraverse(child)
});
}
}
//广度遍历dom:简洁的说就是一层一层的遍历
function breadTraverse(root){
var quean = []
//根节点入队列
quean.unshift(root)
while(quean.length){
var nodeEle = quean.pop(); //先进先出
if(nodeEle == null) break;
visitNode(nodeEle)
//获取当前队首节点的子节点
var childNodes = nodeEle.childNodes;
if(childNodes.length){
childNodes.forEach(child=>{quean.unshift(child)})
}
}
}
var box = document.getElementById('box')
deepTraverse(box)
console.log('下面是广度遍历DOM');
breadTraverse(box)
</script>
</body>
深度优先遍历可以不用递归吗?
栈:先进后出
手写LazyMan实现sleep【异步】机制
手写一个函数,把其他函数柯里化
instanceof原理是什么,请写代码表示
//实现instanceof原理
function instanceofn(arg1,arg2){
if(arg1 == null) return false
//arg1:要验证的变量;arg2被验证的类型
//instanceof是通过原型链查找实现的,这也说明了值类型肯定是false
if(typeof arg1 !=='object' && typeof arg1 !== 'function') return false
var temIntanceof = arg1; //防止在验证的时候,修改arg1
//当arg1能在原型或者原型链上找到上找到对应
while(temIntanceof){
if(temIntanceof.__proto__ === arg2.prototype) return true;
//当对象不能对应到相应的原型对象时,去向原型链上查找
temIntanceof = temIntanceof.__proto__
}
return false
}
console.log(instanceofn([],Array));
console.log(instanceofn({},Object));
console.log(instanceofn({},Array));
console.log(instanceofn([],Object));
console.log(instanceofn('aabb',String));
手写bind,call,apply的方法
//实现bind()功能
Function.prototype._bind = function(ctx, ...args) {
// 下面的this就是调用_bind的函数,保存给_self
const _self = this
// bind 要返回一个函数, 就不会立即执行了
const newFn = function(...rest) {
var newArg = args.concat(rest)
// 调用 apply 修改 this 指向
return _self.apply(ctx, newArg)
// 调用 call 修改 this 指向
//return _self.call(ctx, ...args,...rest)
}
return newFn
}
function f(a,b,c){
console.log(this,a,b,c);
}
var fn = (x,y,z) =>{
console.log(this,x,y,z);
}
var f1 = f._bind({name:'aabb'},10,20)
console.log(f1(30));
var f2 = fn._bind({name:'ccdd'},40,50)
console.log(f2(60));
写apply和call
* Object()方法:new Object()
* 如果传入的是值类型 会返回对应类型的构造函数创建的实例
* 如果传入的是对象 返回对象本身
* 如果传入 undefined 或者 null 会返回空对象
*/
Function.prototype._call = function(ctx, ...args) {
// 判断上下文类型 如果是undefined或者 null 指向window
// 否则使用 Object() 将上下文包装成对象
const o = ctx == undefined ? window : Object(ctx)
// 如何把函数foo的this 指向 ctx这个上下文呢
// 把函数foo赋值给对象o的一个属性 用这个对象o去调用foo this就指向了这个对象o
// 下面的this就是调用_call的函数foo 我们把this给对象o的属性fn 就是把函数foo赋值给了o.fn
//给context新增一个独一无二的属性以免覆盖原有属性
const key = Symbol()
o[key] = this //_call的执行函数
// 立即执行一次
const result = o[key](...args)
// 删除这个属性即执行函数,防止污染
delete o[key]
// 把函数的返回值赋值给_call的返回值
return result
}
apply方法就是将第二个参数设置为数组格式Function.prototype._apply = function(ctx, array = []) {...}
手写EventBus自定义事件
<script>
//手写eventbus自定义事件,包括on,once
function EventBus(){
this.events = {}
//on是存储事件和对应执行的方法,可以一个事件对应多个方法;也可以多个事件对应一个方法
this.on = function(type,fn,isOnce=false){
var events = this.events
//如果key1在events没有
if(events[type] == null){
events[type] = []
}
//如果找到 key1,那么将对应的f1,f2,f3放到key1对应的数组中
events[type].push({fn,isOnce})
}
this.once = function(type,fn,isOnce){
this.on(type,fn,true)
}
//emit 触发key1中的方法f1,f2,f3
this.emit = function(type,...args){
var events = this.events;
//事件xxx不存在,直接终止
if(!events[type]) return;
var fnList = events[type]
//如果fn对应的是once存储的,那么只能执行一次
events[type] = fnList.filter(item=>{
var {fn,isOnce} = item;
fn(...args)
//isOnce是true时表示是once
if(!isOnce) return true; //触发on中的方法
//过滤掉once
return false;
})
}
this.off = function(type,fn){
//如果fn不存在,那么就是解绑对应的所有事件
var events = this.events;
if(fn == null){
events[type] = []
}else{
var fnList = events[type]
//只解绑key1对应的方法:f1或者f2或者f3
events[type] = fnList.filter(item => {
return item.fn !== fn
})
}
}
}
var bus = new EventBus();
function f1(a,b){
console.log('fn1----',a,b);
}
function f2(a,b){
console.log('fn2----',a,b);
}
function f3(a,b){
console.log('fn3----',a,b);
}
bus.on('key1',f1) //存储一个key1事件
bus.on('key1',f2) //存储一个key1事件
bus.once('key1',f3) // 存储一个key1事件
bus.emit('key1',10,20)
bus.off('key1',f2)
bus.emit('key1',30,40)
</script>
EventBus 事件总线思路总结
1. EventBus 事件总线中的事件(如key1)可以执行多个方法(如f1,f2,f3);
同时多个事件(如key1,key2)可以执行同一个方法(如f1),那这样EventBus对象中的数据结构就会如下:
'key1':[
{f1,isOnce:false}, // isOnce代表是on还是once
{f2,isOnce:false},
{f3,isOnce:true},
],
'key2':[
{f1,isOnce:false}, // isOnce代表是on还是once
{f2,isOnce:false},
{f3,isOnce:true},
],
2.on 和once 都是给EventBus存储事件;
1》通个type【key1】查找EventBus对象中有没有这个属性,如果没有就给key1设置一个空的数组[],
方便后面给key1事件上存储方法;
2》页面中执行一次on或者once就给EventBus对象的key1数组中新增一项对象{fn,isOnce}
3.emit方法是触发,EventBus对应中相应的key1中的方法(f1,f2,f3);
1》如果key3在EventBus中找不到,那么就直接终止了;
2》获取key1在EventBus中的数组,然后去过滤这些数组,如果数组项中的isOnce是true,
那么方法f3是通过once方法存储的,那么就只能触发一次,执行到此就里面过滤掉;
false, 那么方法f1,f2是通过on方法存储的,那么就满足条件,触发f1,f2;
4.off是将事件(key1,key2)上的方法(f1,f2,f3)解绑;
1》如果off方法中没有传入fn,那么代表将事件(key1,key2) 对应的数组中的方法全部解绑,
或者就只解绑key1数组中传入的方法(f2)对应的哪一项
on和once分开的情况
实际工作经验
首屏优化
LUR缓存
手写JS深拷贝-考虑各种数据类型和循环引用
<script>
//手写deepClone深拷贝,满足各种情况:new set(),new map(),循环引用
function deepClone(objKey,map=new WeakMap) {
//深拷贝对象的属性的值类型,那么直接返回,不用再递归了
if(typeof objKey !== 'object' || typeof objKey == null ) return objKey
//避免循环引用
var objFromMap = map.get(objKey)
if(objFromMap) return objFromMap
//最终返回的结果
var result = {}
//.....经过各种深拷贝后,有map.get就会有set(),针对循环引用
//循环引用的时候,set后就直接get了,不需要再重新计算了,这样就不会报错循环引用的时候溢出的结果
map.set(objKey,result)
//如果objKey的值是数组,解决数组深拷贝
if(objKey instanceof Array){
//数组map()返回一个新数组,然后对数组的每一项调用一次提供的函数
result = objKey.map(item => deepClone(item,map))
}
//如果objKey的值是对象,解决对象深拷贝
if(objKey instanceof Object){
//对象的健值是字符串,这个就不用递归了,但是对象的属性值这可以是任何形状了,那么就需要deepClone
for(let k in objKey){
var val = deepClone(objKey[k],map)
result[k] = val
}
}
//如果objKey的值是new set(),解决set深拷贝
if(objKey instanceof Set){
//类数组
result = new Set()
objKey.forEach(item =>{
var v = deepClone(item,map)
result.add(v)
})
}
//如果objKey的值是new Map(),解决Map深拷贝
if(objKey instanceof Map){
//类对象,但是键值类型不限
result = new Map()
objKey.forEach((v,k)=>{
var v1 = deepClone(v,map)
var k1 = deepClone(k,map)
result.set(k1,v1)
})
}
return result
}
//功能测试
var obj = {
set:new Set([1,2,3,4]),
map:new Map([['x',10],['y',20]]),
fn:()=>{
console.log('deepClone fn---',20);
},
info:{
city:'北京'
},
arr:[10,20,30],
a:10
}
obj.self = obj
// console.log(deepClone(obj));
var obj1 = deepClone(obj);
obj1.info.city = '上海'
obj1.map.set('a','aa')
obj1.self = obj1;
console.log(obj);
console.log(obj1);
</script>
常用的设计模式,以及使用场景
工厂模式
单列模式
实际项目中有哪些vue优化
请描述js-bridge的实现原理
常见实现方式
Vue项目打包文件过大(优化)
懒加载
什么叫懒加载?在需要的时候进行加载,随载随用。
常见的有:路由、图片等。
方法: 使用es6提案的import()方式
推荐使用es提案的import()方式是因为未修改前路由定义时也时用import的方式引用,若使用vue路由懒加载组件,修改的地方较多,不够快捷。
修改后引用方式如图,即:
把import login from '@/views/login/login'修改为
| 1 | const login = () => import(``'@/views/login/login'``) |
|---|
其他路由配置不需要变动。
按需加载
什么叫按需加载?根据需要去加载资源。
常见的有:UI库等。
不生成.map文件
修改config/index.js productionSourceMap: false 去除打包时生产.map文件,加快打包速度
通过cdn方式引入
将不怎么会改动的第三方包通过
externals: {
'vue': 'Vue',
'vue-router': 'VueRouter',
'axios': 'axios',
}
图片压缩
可利用一些网站对大体积图片进行压缩,例如:[tinypng]
你在使用Vue过程中遇到过哪些坑
内存泄漏
如何统一监听Vue组件报错