LRU 算法
实现代码如下:
// 一个Map对象在迭代时会根据对象中元素的插入顺序来进行
// 新添加的元素会被插入到map的末尾,整个栈倒序查看
class LRUCache {
constructor(capacity) {
this.secretKey = new Map();
this.capacity = capacity;
}
get(key) {
if (this.secretKey.has(key)) {
let tempValue = this.secretKey.get(key);
this.secretKey.delete(key);
this.secretKey.set(key, tempValue);
return tempValue;
} else return -1;
}
put(key, value) {
// key存在,仅修改值
if (this.secretKey.has(key)) {
this.secretKey.delete(key);
this.secretKey.set(key, value);
}
// key不存在,cache未满
else if (this.secretKey.size < this.capacity) {
this.secretKey.set(key, value);
}
// 添加新key,删除旧key
else {
this.secretKey.set(key, value);
// 删除map的第一个元素,即为最长未使用的
this.secretKey.delete(this.secretKey.keys().next().value);
}
}
}
// let cache = new LRUCache(2);
// cache.put(1, 1);
// cache.put(2, 2);
// console.log("cache.get(1)", cache.get(1))// 返回 1
// cache.put(3, 3);// 该操作会使得密钥 2 作废
// console.log("cache.get(2)", cache.get(2))// 返回 -1 (未找到)
// cache.put(4, 4);// 该操作会使得密钥 1 作废
// console.log("cache.get(1)", cache.get(1))// 返回 -1 (未找到)
// console.log("cache.get(3)", cache.get(3))// 返回 3
// console.log("cache.get(4)", cache.get(4))// 返回 4
复制代码
前端进阶面试题详细解答
setTimeout(fn, 0)
多久才执行,Event Loop
setTimeout 按照顺序放到队列里面,然后等待函数调用栈清空之后才开始执行,而这些操作进入队列的顺序,则由设定的延迟时间来决定
vue-router
vue-router是vuex.js官方的路由管理器,它和vue.js的核心深度集成,让构建但页面应用变得易如反掌
<router-link> 组件支持用户在具有路由功能的应用中 (点击) 导航。 通过 to 属性指定目标地址
<router-view> 组件是一个 functional 组件,渲染路径匹配到的视图组件。
<keep-alive> 组件是一个用来缓存组件
router.beforeEach
router.afterEach
to: Route: 即将要进入的目标 路由对象
from: Route: 当前导航正要离开的路由
next: Function: 一定要调用该方法来 resolve 这个钩子。执行效果依赖 next 方法的调用参数。
介绍了路由守卫及用法,在项目中路由守卫起到的作用等等
复制代码
事件机制
涉及面试题:事件的触发过程是怎么样的?知道什么是事件代理嘛?
1. 简介
事件流是一个事件沿着特定数据结构传播的过程。冒泡和捕获是事件流在DOM
中两种不同的传播方法
事件流有三个阶段
事件捕获
事件捕获(event capturing
):通俗的理解就是,当鼠标点击或者触发dom
事件时,浏览器会从根节点开始由外到内进行事件传播,即点击了子元素,如果父元素通过事件捕获方式注册了对应的事件的话,会先触发父元素绑定的事件
事件冒泡
事件冒泡(dubbed bubbling):与事件捕获恰恰相反,事件冒泡顺序是由内到外进行事件传播,直到根节点
无论是事件捕获还是事件冒泡,它们都有一个共同的行为,就是事件传播
2. 捕获和冒泡
<div id="div1">
<div id="div2"></div>
</div>
<script>
let div1 = document.getElementById('div1');
let div2 = document.getElementById('div2');
div1.onClick = function(){
alert('1')
}
div2.onClick = function(){
alert('2');
}
</script>
复制代码
当点击 div2
时,会弹出两个弹出框。在 ie8/9/10
、chrome
浏览器,会先弹出”2”再弹出“1”,这就是事件冒泡:事件从最底层的节点向上冒泡传播。事件捕获则跟事件冒泡相反
W3C 的标准是先捕获再冒泡, addEventListener
的第三个参数决定把事件注册在捕获(true
)还是冒泡(false
)
3. 事件对象
4. 事件流阻止
在一些情况下需要阻止事件流的传播,阻止默认动作的发生
事件的阻止在不同浏览器有不同处理
preventDefault 与 stopPropagation 的区别
5. 事件注册
通常我们使用 addEventListener
注册事件,该函数的第三个参数可以是布尔值,也可以是对象。对于布尔值 useCapture
参数来说,该参数默认值为 false
。useCapture
决定了注册的事件是捕获事件还是冒泡事件
一般来说,我们只希望事件只触发在目标上,这时候可以使用 stopPropagation
来阻止事件的进一步传播。通常我们认为 stopPropagation
是用来阻止事件冒泡的,其实该函数也可以阻止捕获事件。stopImmediatePropagation
同样也能实现阻止事件,但是还能阻止该事件目标执行别的注册事件
node.addEventListener('click',(event) =>{
event.stopImmediatePropagation()
console.log('冒泡')
},false);
// 点击 node 只会执行上面的函数,该函数不会执行
node.addEventListener('click',(event) => {
console.log('捕获 ')
},true)
复制代码
6. 事件委托
假设有100
个li
,每个li
有相同的点击事件。如果为每个Li
都添加事件,则会造成dom
访问次数过多,引起浏览器重绘与重排的次数过多,性能则会降低。 使用事件委托则可以解决这样的问题
原理
实现事件委托是利用了事件的冒泡原理实现的。当我们为最外层的节点添加点击事件,那么里面的ul
、li
、a
的点击事件都会冒泡到最外层节点上,委托它代为执行事件
<ul id="ul">
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<script>
window.onload = function(){
var ulEle = document.getElementById('ul');
ul.onclick = function(ev){
//兼容IE
ev = ev || window.event;
var target = ev.target || ev.srcElement;
if(target.nodeName.toLowerCase() == 'li'){
alert( target.innerHTML);
}
}
}
</script>
复制代码
对节流与防抖的理解
防抖函数的应用场景:
节流函数的适⽤场景:
用过 TypeScript 吗?它的作用是什么?
为 JS 添加类型支持,以及提供最新版的 ES 语法的支持,是的利于团队协作和排错,开发大型项目
图片懒加载
实现:getBoundClientRect
的实现方式,监听 scroll
事件(建议给监听事件添加节流),图片加载完会从 img
标签组成的 DOM 列表中删除,最后所有的图片加载完毕后需要解绑监听事件。
// scr 加载默认图片,data-src 保存实施懒加载后的图片
// <img src="./default.jpg" data-src="https://xxx.jpg" alt="" />
let imgs = [...document.querySelectorAll("img")];
const len = imgs.length;
let lazyLoad = function() {
let count = 0;
let deleteImgs = [];
// 获取当前可视区的高度
let viewHeight = document.documentElement.clientHeight;
// 获取当前滚动条的位置(距离顶部的距离,等价于document.documentElement.scrollTop)
let scrollTop = window.pageYOffset;
imgs.forEach((img) => {
// 获取元素的大小,及其相对于视口的位置,如 bottom 为元素底部到网页顶部的距离
let bound = img.getBoundingClientRect();
// 当前图片距离网页顶部的距离
// let imgOffsetTop = img.offsetTop;
// 判断图片是否在可视区内,如果在就加载(两种判断方式)
// if(imgOffsetTop < scrollTop + viewHeight)
if (bound.top < viewHeight) {
img.src = img.dataset.src; // 替换待加载的图片 src
count++;
deleteImgs.push(img);
// 最后所有的图片加载完毕后需要解绑监听事件
if(count === len) {
document.removeEventListener("scroll", imgThrottle);
}
}
});
// 图片加载完会从 `img` 标签组成的 DOM 列表中删除
imgs = imgs.filter((img) => !deleteImgs.includes(img));
}
window.onload = function () {
lazyLoad();
};
// 使用 防抖/节流 优化一下滚动事件
let imgThrottle = debounce(lazyLoad, 1000);
// 监听 `scroll` 事件
window.addEventListener("scroll", imgThrottle);
复制代码
点击刷新按钮或者按 F5、按 Ctrl+F5 (强制刷新)、地址栏回车有什么区别?
点击刷新按钮或者按 F5: 浏览器直接对本地的缓存文件过期,但是会带上 If-Modifed-Since,If-None-Match,这就意味着服务器会对文件检查新鲜度,返回结果可能是 304,也有可能是 200。
用户按 Ctrl+F5(强制刷新): 浏览器不仅会对本地文件过期,而且不会带上 If-Modifed-Since,If-None-Match,相当于之前从来没有请求过,返回结果是 200。
地址栏回车: 浏览器发起请求,按照正常流程,本地检查是否过期,然后服务器检查新鲜度,最后返回内容。
实现有并行限制的 Promise 调度器
题目描述:JS 实现一个带并发限制的异步调度器 Scheduler,保证同时运行的任务最多有两个
addTask(1000,"1");
addTask(500,"2");
addTask(300,"3");
addTask(400,"4");
的输出顺序是:2 3 1 4
整个的完整执行流程:
一开始1、2两个任务开始执行
500ms时,2任务执行完毕,输出2,任务3开始执行
800ms时,3任务执行完毕,输出3,任务4开始执行
1000ms时,1任务执行完毕,输出1,此时只剩下4任务在执行
1200ms时,4任务执行完毕,输出4
复制代码
实现代码如下:
class Scheduler {
constructor(limit) {
this.queue = [];
this.maxCount = limit;
this.runCounts = 0;
}
add(time, order) {
const promiseCreator = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(order);
resolve();
}, time);
});
};
this.queue.push(promiseCreator);
}
taskStart() {
for (let i = 0; i < this.maxCount; i++) {
this.request();
}
}
request() {
if (!this.queue || !this.queue.length || this.runCounts >= this.maxCount) {
return;
}
this.runCounts++;
this.queue
.shift()()
.then(() => {
this.runCounts--;
this.request();
});
}
}
const scheduler = new Scheduler(2);
const addTask = (time, order) => {
scheduler.add(time, order);
};
addTask(1000, "1");
addTask(500, "2");
addTask(300, "3");
addTask(400, "4");
scheduler.taskStart();
复制代码
说一下 SPA 单页面有什么优缺点?
优点:
1.体验好,不刷新,减少 请求 数据ajax异步获取 页面流程;
2.前后端分离
3.减轻服务端压力
4.共用一套后端程序代码,适配多端
缺点:
1.首屏加载过慢;
2.SEO 不利于搜索引擎抓取
复制代码
setInterval 模拟 setTimeout
描述:使用setInterval
模拟实现setTimeout
的功能。
思路:setTimeout
的特性是在指定的时间内只执行一次,我们只要在setInterval
内部执行 callback
之后,把定时器关掉即可。
实现:
const mySetTimeout = (fn, time) => {
let timer = null;
timer = setInterval(() => {
// 关闭定时器,保证只执行一次fn,也就达到了setTimeout的效果了
clearInterval(timer);
fn();
}, time);
// 返回用于关闭定时器的方法
return () => clearInterval(timer);
}
// 测试
const cancel = mySetTimeout(() => {
console.log(1);
}, 1000);
// 一秒后打印 1
复制代码
说一下 data 为什么是一个函数而不是一个对象?
JavaScript中的对象是引用类型的数据,当多个实例引用同一个对象时,只要一个实例对这个对象进行操作,其他实例中的数据也会发生变化。而在Vue中,我们更多的是想要复用组件,那就需要每个组件都有自己的数据,这样组件之间才不会相互干扰。所以组件的数据不能写成对象的形式,而是要写成函数的形式。数据以函数返回值的形式定义,这样当我们每次复用组件的时候,就会返回一个新的data,也就是说每个组件都有自己的私有数据空间,它们各自维护自己的数据,不会干扰其他组件的正常运行。
代码输出结果
const first = () => (new Promise((resolve, reject) => {
console.log(3);
let p = new Promise((resolve, reject) => {
console.log(7);
setTimeout(() => {
console.log(5);
resolve(6);
console.log(p)
}, 0)
resolve(1);
});
resolve(2);
p.then((arg) => {
console.log(arg);
});
}));
first().then((arg) => {
console.log(arg);
});
console.log(4);
复制代码
输出结果如下:
3
7
4
1
2
5
Promise{<resolved>: 1}
复制代码
代码的执行过程如下:
首先会进入 Promise,打印出 3,之后进入下面的 Promise,打印出 7;
遇到了定时器,将其加入宏任务队列;
执行 Promise p 中的 resolve,状态变为 resolved,返回值为 1;
执行 Promise first 中的 resolve,状态变为 resolved,返回值为 2;
遇到 p.then,将其加入微任务队列,遇到 first().then,将其加入任务队列;
执行外面的代码,打印出 4;
这样第一轮宏任务就执行完了,开始执行微任务队列中的任务,先后打印出 1 和 2;
这样微任务就执行完了,开始执行下一轮宏任务,宏任务队列中有一个定时器,执行它,打印出 5,由于执行已经变为 resolved 状态,所以resolve(6)
不会再执行;
最后console.log(p)
打印出Promise{<resolved>: 1}
;
怎么解决白屏问题
HTTP 世界全览
互联网上绝大部分资源都使用 HTTP
协议传输;
浏览器是 HTTP 协议里的请求方,即 User Agent
;
服务器是 HTTP 协议里的应答方,常用的有 Apache
和 Nginx
;
CDN
位于浏览器和服务器之间,主要起到缓存加速的作用;
爬虫是另一类 User Agent
,是自动访问网络资源的程序。
TCP/IP
是网络世界最常用的协议,HTTP
通常运行在 TCP/IP
提供的可靠传输基础上
DNS
域名是 IP
地址的等价替代,需要用域名解析实现到 IP
地址的映射;
URI
是用来标记互联网上资源的一个名字,由“协议名 + 主机名 + 路径”构成,俗称 URL;
HTTPS
相当于“HTTP+SSL/TLS+TCP/IP
”,为 HTTP
套了一个安全的外壳;
代理是 HTTP
传输过程中的“中转站”,可以实现缓存加速、负载均衡等功能
vue-router
mode
跳转
占位
<router-view></router-view>
复制代码
vue-router 源码实现
作为一个插件存在:实现VueRouter
类和install
方法
实现两个全局组件:router-view
用于显示匹配组件内容,router-link
用于跳转
监控url
变化:监听hashchange
或popstate
事件
响应最新url
:创建一个响应式的属性current
,当它改变时获取对应组件并显示
// 我们的插件:
// 1.实现一个Router类并挂载期实例
// 2.实现两个全局组件router-link和router-view
let Vue;
class VueRouter {
// 核心任务:
// 1.监听url变化
constructor(options) {
this.$options = options;
// 缓存path和route映射关系
// 这样找组件更快
this.routeMap = {}
this.$options.routes.forEach(route => {
this.routeMap[route.path] = route
})
// 数据响应式
// 定义一个响应式的current,则如果他变了,那么使用它的组件会rerender
Vue.util.defineReactive(this, 'current', '')
// 请确保onHashChange中this指向当前实例
window.addEventListener('hashchange', this.onHashChange.bind(this))
window.addEventListener('load', this.onHashChange.bind(this))
}
onHashChange() {
// console.log(window.location.hash);
this.current = window.location.hash.slice(1) || '/'
}
}
// 插件需要实现install方法
// 接收一个参数,Vue构造函数,主要用于数据响应式
VueRouter.install = function (_Vue) {
// 保存Vue构造函数在VueRouter中使用
Vue = _Vue
// 任务1:使用混入来做router挂载这件事情
Vue.mixin({
beforeCreate() {
// 只有根实例才有router选项
if (this.$options.router) {
Vue.prototype.$router = this.$options.router
}
}
})
// 任务2:实现两个全局组件
// router-link: 生成一个a标签,在url后面添 // <router-link to="/about">aaa</router-link>
Vue.component('router-link', {
props: {
to: {
type: String,
required: true
},
},
render(h) {
// h(tag, props, children)
return h('a',
{ attrs: { href: '#' + this.to } },
this.$slots.default
)
// 使用jsx
// return <a href={'#'+this.to}>{this.$slots.default}</a>
}
})
Vue.component('router-view', {
render(h) {
// 根据current获取组件并render
// current怎么获取?
// console.log('render',this.$router.current);
// 获取要渲染的组件
let component = null
const { routeMap, current } = this.$router
if (routeMap[current]) {
component = routeMap[current].component
}
return h(component)
}
})
}
export default VueRouter
复制代码
watch 的理解
watch
没有缓存性,更多的是观察的作用,可以监听某些数据执行回调。当我们需要深度监听对象中
的属性时,可以打开 deep:true 选项,这样便会对对象中的每一项进行监听。这样会带来性能问题,优化的话可以使用字符串形式监听
注意:Watcher : 观察者对象 , 实例分为渲染 watcher
(render watcher),计算属性 watcher
(computed watcher),侦听器 watcher
(user watcher)三种
代码输出结果
function runAsync (x) {
const p = new Promise(r => setTimeout(() => r(x, console.log(x)), 1000))
return p
}
Promise.all([runAsync(1), runAsync(2), runAsync(3)]).then(res => console.log(res))
复制代码
输出结果如下:
首先,定义了一个 Promise,来异步执行函数 runAsync,该函数传入一个值 x,然后间隔一秒后打印出这个 x。
之后再使用Promise.all
来执行这个函数,执行的时候,看到一秒之后输出了 1,2,3,同时输出了数组[1, 2, 3],三个函数是同步执行的,并且在一个回调函数中返回了所有的结果。并且结果和函数的执行顺序是一致的。
Service Worker
Service workers
本质上充当 Web 应用程序与浏览器之间的代理服务器,也可以在网络可用时作为浏览器和网络间的代理。它们旨在(除其他之外)使得能够创建有效的离线体验,拦截网络请求并基于网络是否可用以及更新的资源是否驻留在服务器上来采取适当的动作。他们还允许访问推送通知和后台同步 API
浏览器对 ServiceWorker 做了很多限制
在 ServiceWorker
中无法直接访问 DOM
,但可以通过 postMessage
接口发送的消息来与其控制的页面进行通信
ServiceWorker
只能在本地环境下或 HTTPS
网站中使用
ServiceWorker
有作用域的限制,一个 ServiceWorker
脚本只能作用于当前路径及其子路径;
目前该技术通常用来做缓存文件,提高首屏速度
// index.js
if (navigator.serviceWorker) {
navigator.serviceWorker
.register("sw.js")
.then(function(registration) {
console.log("service worker 注册成功");
})
.catch(function(err) {
console.log("servcie worker 注册失败");
});
}
// sw.js
// 监听 `install` 事件,回调中缓存所需文件
self.addEventListener("install", e => {
e.waitUntil(
caches.open("my-cache").then(function(cache) {
return cache.addAll(["./index.html", "./index.js"]);
})
);
});
// 拦截所有请求事件
// 如果缓存中已经有请求的数据就直接用缓存,否则去请求数据
self.addEventListener("fetch", e => {
e.respondWith(
caches.match(e.request).then(function(response) {
if (response) {
return response;
}
console.log("fetch source");
})
);
});
复制代码
打开页面,可以在开发者工具中的 Application
看到 Service Worker
已经启动了
在 Cache 中也可以发现我们所需的文件已被缓存
当我们重新刷新页面可以发现我们缓存的数据是从 Service
Worker
中读取的
说一下前端登录的流程?
初次登录的时候,前端调后调的登录接口,发送用户名和密码,后端收到请求,验证用户名和密码,验证成功,就给前端返回一个 token,和一个用户信息的值,前端拿到 token,将 token 储存到 Vuex 中,然后从 Vuex 中把 token 的值存入浏览器 Cookies 中。把用户信息存到 Vuex 然后再存储到 LocalStroage 中,然后跳转到下一个页面,根据后端接口的要求,只要不登录就不能访问的页面需要在前端每次跳转页面师判断 Cookies 中是否有 token,没有就跳转到登录页,有就跳转到相应的页面,我们应该再每次发送 post/get 请求的时候应该加入 token,常用方法再项目 utils/service.js 中添加全局拦截器,将 token 的值放入请求头中 后端判断请求头中有无 token,有 token,就拿到 token 并验证 token 是否过期,在这里过期会返回无效的 token 然后有个跳回登录页面重新登录并且清除本地用户的信息
评论