面试题总结
Vue系列
目录
转自:https://github.com/febobo/web-interview
1.对SPA(单页应用)的理解
一、什么是SPA
SPA:single-page application,是一种网络应用程序或网站的模型,它通过动态重写当前页面来与用户交互,这种方法避免了页面之间切换打断用户体验在单页应用中,所有必要的代码(HTML,css,JavaScript)都是通过单个页面的加载而检索,或者根据需要(通常是为响应用户操作)动态装载适当的资源并添加到页面,页面在任何时间点都不会重新加载,也不会将控制转移到其他页面。
二、SPA和MPA的区别
单页应用与多页应用的区别
单页应用 SPA | 多页应用 MPA | |
---|---|---|
组成 | 一个主页面和多个页面片段 | 多个主页面 |
刷新方式 | 局部刷新 | 整页刷新 |
url模式 | 哈希模式 | 历史模式 |
SEO搜索引擎优化 | 难实现,可使用SSR方式改善 | 容易实现 |
数据传递 | 容易 | 用过url、cookie、localStorage等传递 |
页面切换 | 速度快,用户体验良好 | 切换加载资源,速度慢,用户体验差 |
维护成本 | 相对容易 | 相对复杂 |
单页应用优缺点
优点:
- 具有桌面应用的即时性、网站的可移植性和可访问性
- 用户体验好、快、内容的改变不需要重新加载整个页面
- 良好的前后端分离,分工更明确
缺点:
- 不利于搜索引擎的抓取
- 首次渲染速度相对较慢
三、实现一个SPA
原理
- 监听地址栏中hash变化驱动界面变化
- 用pushstate记录浏览器的历史,驱动界面发生变化
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yLx95VjO-1675152546825)(null)]
实现
hash模式
核心是通过监听url中的hash来进行页面跳转
// 定义 Router
class Router {
constructor () {
this.routes = {}; // 存放路由path及callback
this.currentUrl = '';
// 监听路由change调用相对应的路由回调
window.addEventListener('load', this.refresh, false);
window.addEventListener('hashchange', this.refresh, false);
}
route(path, callback){
this.routes[path] = callback;
}
push(path) {
this.routes[path] && this.routes[path]()
}
}
// 使用 router
window.miniRouter = new Router();
miniRouter.route('/', () => console.log('page1'))
miniRouter.route('/page2', () => console.log('page2'))
miniRouter.push('/') // page1
miniRouter.push('/page2') // page2
history模式
history模式核心借用HTML5 history api,api提供了丰富的router相关属性先了解几个相关的api
- history.pushState浏览器历史记录添加记录
- history.replaceState修改浏览器历史记录中的当前记录
- history.popState当history发生变化时触发
// 定义 Router
class Router {
constructor () {
this.routes = {};
this.listerPopState()
}
init(path) {
history.replaceState({path: path}, null, path);
this.routes[path] && this.routes[path]();
}
route(path, callback){
this.routes[path] = callback;
}
push(path) {
history.pushState({path: path}, null, path);
this.routes[path] && this.routes[path]();
}
listerPopState () {
window.addEventListener('popstate' , e => {
const path = e.state && e.state.path;
this.routers[path] && this.routers[path]()
})
}
}
// 使用 Router
window.miniRouter = new Router();
miniRouter.route('/', ()=> console.log('page1'))
miniRouter.route('/page2', ()=> console.log('page2'))
// 跳转
miniRouter.push('/page2') // page2
四、如何给SPA做SEO
下面给出基于Vue的SPA如何实现SEO的三种方式
1.SSR服务端渲染
将组件或页面通过服务器生成html,再返回给浏览器,如nuxt.js
2.静态化
目前主流的静态化主要有两种:
(1)一种是通过程序将动态页面抓取并保存为静态页面,这样的页面的实际存在于服务器的硬盘中
(2)另一种是通过WEB服务器的URL Rewrite 的方式,它的原理是通过web服务器内部模块按一定规则将外部的URL请求转化为内部的文件地址,一句话来说就是把外部请求的静态地址转化为实际的动态页面地址,而静态页面实际是不存在的。
这两种方法都达到了实现URL静态化的效果。
3.使用Phantomjs针对爬虫处理
原理是通过nginx配置,判断访问来源是否为爬虫,如果是,则搜索引擎的爬虫请求会转发到一个node server,再通过phantomjs来解析完整的HTML,返回给爬虫。下面是大致的流程图:
2.对双向绑定的理解
一、什么是双向绑定
先从单行绑定切入,就是把model绑定到view,当我们 用JavaScript代码更新model时,view就会自动更新。双向绑定就是在单向绑定的基础上,用户更新了view,model的数据也自动被更新。
二、双向绑定的原理是什么
双向绑定由三个重要的部分组成
- 数据层(Model):应用的数据及业务逻辑
- 视图层(View):应用的展示效果,各类UI组件
- 业务逻辑层(ViewModel):框架封装的核心,它负责将数据与视图关联起来
以上即为MVVM
它的主要职责就是:
- 数据变化后更新视图
- 视图变化后更新数据
当然它还有两个主要部分组成:
- 监听器(Observer):对所有数据的属性进行监听
- 解析器(Compiler):对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数
三、实现双向绑定
Vue中双向绑定的流程:
new Vue()
首先执行初始化,对data
执行响应化处理,这个过程发生Observe
中- 同时对模板执行编译,找到其中动态绑定的数据,从
data
中获取并初始化视图,这个过程发生在Compile
中 - 同时定义⼀个更新函数和
Watcher
,将来对应数据变化时Watcher
会调用更新函数 - 由于
data
的某个key
在⼀个视图中可能出现多次,所以每个key
都需要⼀个管家Dep
来管理多个Watcher
- 将来data中数据⼀旦发生变化,会首先找到对应的
Dep
,通知所有Watcher
执行更新函数
流程图如下:
实现
先来一个构造函数:执行初始化,对data
执行响应化处理
class Vue {
constructor(options) {
this.$options = options;
this.$data = options.data;
// 对data选项做响应式处理
observe(this.$data);
// 代理data到vm上
proxy(this);
// 执行编译
new Compile(options.el, this);
}
}
编译Compile
对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数
class Compile {
constructor(el, vm) {
this.$vm = vm;
this.$el = document.querySelector(el); // 获取dom
if (this.$el) {
this.compile(this.$el);
}
}
compile(el) {
const childNodes = el.childNodes;
Array.from(childNodes).forEach((node) => { // 遍历子元素
if (this.isElement(node)) { // 判断是否为节点
console.log("编译元素" + node.nodeName);
} else if (this.isInterpolation(node)) {
console.log("编译插值⽂本" + node.textContent); // 判断是否为插值文本 {{}}
}
if (node.childNodes && node.childNodes.length > 0) { // 判断是否有子元素
this.compile(node); // 对子元素进行递归遍历
}
});
}
isElement(node) {
return node.nodeType == 1;
}
isInterpolation(node) {
return node.nodeType == 3 && /\{\{(.*)\}\}/.test(node.textContent);
}
}
依赖收集
视图中会用到data
中某key
,这称为依赖。同⼀个key
可能出现多次,每次都需要收集出来用⼀个Watcher
来维护它们,此过程称为依赖收集多个Watcher
需要⼀个Dep
来管理,需要更新时由Dep
统⼀通知
实现思路
defineReactive
时为每⼀个key
创建⼀个Dep
实例- 初始化视图时读取某个
key
,例如name1
,创建⼀个watcher1
- 由于触发
name1
的getter
方法,便将watcher1
添加到name1
对应的Dep中 - 当
name1
更新,setter
触发时,便可通过对应Dep
通知其管理所有Watcher
更新
// 负责更新视图
class Watcher {
constructor(vm, key, updater) {
this.vm = vm
this.key = key
this.updaterFn = updater
// 创建实例时,把当前实例指定到Dep.target静态属性上
Dep.target = this
// 读一下key,触发get
vm[key]
// 置空
Dep.target = null
}
// 未来执行dom更新函数,由dep调用的
update() {
this.updaterFn.call(this.vm, this.vm[this.key])
}
}
声明Dep
class Dep {
constructor() {
this.deps = []; // 依赖管理
}
addDep(dep) {
this.deps.push(dep);
}
notify() {
this.deps.forEach((dep) => dep.update());
}
}
创建watcher
时触发getter
class Watcher {
constructor(vm, key, updateFn) {
Dep.target = this;
this.vm[this.key];
Dep.target = null;
}
}
依赖收集,创建Dep
实例
function defineReactive(obj, key, val) {
this.observe(val);
const dep = new Dep();
Object.defineProperty(obj, key, {
get() {
Dep.target && dep.addDep(Dep.target);// Dep.target也就是Watcher实例
return val;
},
set(newVal) {
if (newVal === val) return;
dep.notify(); // 通知dep执行更新方法
},
});
}
3.对v-show和v-if理解
一、v-show与v-if的共同点
在vue中v-show和v-if的作用效果都是相同的,都能控制元素在页面中是否显示
在用法上也是相同的
<Model v-show="isShow" />
<Model v-if="isShow" />
- 当表达式为true时,都会占据页面的位置
- 当表达式为false时,都不会占据页面的位置
二、v-show与v-if的区别
- 控制手段不同
- 编译过程不同
- 编译条件不同
控制手段:v-show隐藏则是为该元素添加css–display:none,dom元素依旧存在。v-if显示隐藏是将dom元素整个添加或删除
编译过程:v-if切换有一个局部编译/卸载的过程,切换过程中合适的销毁和重建内部的事件监听和子组件;v-show只是简单的基于css的切换
编译条件:v-if是真正的条件渲染,它会确保在切换过程中条件块内的事件监听和子组件适当地销毁和重建。只有渲染条件为假时,并不操作,直到为真才渲染
- v-show由false变为true的时候不会触发组件的生命周期
- v-if由false变为true的时候,触发组件的beforeCreate、Create、beforeMount、mounted钩子,由true变false的时候触发组件的beforeDestory、destoryed方法
性能消耗:v-if有更高的切换消耗;v-show有更高的初始渲染消耗
三、v-show与v-if原理分析
大致流程:
- 将模板template转为ast结构的js对象
- 用ast得到的js对象拼装render和staticRenderFns函数
- render和staticRenderFns函数被调用后生成虚拟vnode节点,该节点包含创建DOM节点所需信息
- vm.patch函数通过虚拟DOM算法利用vnode节点创建真实dom节点
v-show原理
不管初始条件是什么,元素总是会被渲染
// https://github.com/vuejs/vue-next/blob/3cd30c5245da0733f9eb6f29d220f39c46518162/packages/runtime-dom/src/directives/vShow.ts
export const vShow: ObjectDirective<VShowElement> = {
beforeMount(el, { value }, { transition }) {
el._vod = el.style.display === 'none' ? '' : el.style.display
if (transition && value) {
transition.beforeEnter(el)
} else {
setDisplay(el, value)
}
},
mounted(el, { value }, { transition }) {
if (transition && value) {
transition.enter(el)
}
},
updated(el, { value, oldValue }, { transition }) {
// ...
},
beforeUnmount(el, { value }) {
setDisplay(el, value)
}
}
v-if原理
v-if在实现上比v-show复杂的多,因为还有else,else-if等条件需要处理,这里只摘抄源码中处理v-if的一小部分
返回一个node节点,render函数通过表达值来决定是否生成DOM
// https://github.com/vuejs/vue-next/blob/cdc9f336fd/packages/compiler-core/src/transforms/vIf.ts
export const transformIf = createStructuralDirectiveTransform(
/^(if|else|else-if)$/,
(node, dir, context) => {
return processIf(node, dir, context, (ifNode, branch, isRoot) => {
// ...
return () => {
if (isRoot) {
ifNode.codegenNode = createCodegenNodeForBranch(
branch,
key,
context
) as IfConditionalExpression
} else {
// attach this branch's codegen node to the v-if root.
const parentCondition = getParentCondition(ifNode.codegenNode!)
parentCondition.alternate = createCodegenNodeForBranch(
branch,
key + ifNode.branches.length - 1,
context
)
}
}
})
}
)
四、v-show与v-if的使用场景
v-if与v-show都能控制dom元素在页面的显示
v-if相比v-show开销更大,直接操作dom节点增加与删除
如果需要频繁的切换,用v-show比较好
如果在运行时条件很少改变,v-if较好