Vue组件data为什么必须是个函数?
- 根实例对象
data
可以是对象也可以是函数 (根实例是单例),不会产生数据污染情况 - 组件实例对象
data
必须为函数 一个组件被复用多次的话,也就会创建多个实例。本质上,这些实例用的都是同一个构造函数。如果data
是对象的话,对象属于引用类型,会影响到所有的实例。所以为了保证组件不同的实例之间data
不冲突,data
必须是一个函数,
简版理解
代码语言:javascript复制// 1.组件的渲染流程 调用Vue.component -> Vue.extend -> 子类 -> new 子类
// Vue.extend 根据用户定义产生一个新的类
function Vue() {}
function Sub() { // 会将data存起来
this.data = this.constructor.options.data();
}
Vue.extend = function(options) {
Sub.options = options; // 静态属性
return Sub;
}
let Child = Vue.extend({
data:()=>( { name: 'zf' })
});
// 两个组件就是两个实例, 希望数据互不感染
let child1 = new Child();
let child2 = new Child();
console.log(child1.data.name);
child1.data.name = 'poetry';
console.log(child2.data.name);
// 根不需要 任何的合并操作 根才有vm属性 所以他可以是函数和对象 但是组件mixin他们都没有vm 所以我就可以判断 当前data是不是个函数
相关源码
代码语言:javascript复制// 源码位置 src/core/global-api/extend.js
export function initExtend (Vue: GlobalAPI) {
Vue.extend = function (extendOptions: Object): Function {
extendOptions = extendOptions || {}
const Super = this
const SuperId = Super.cid
const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
if (cachedCtors[SuperId]) {
return cachedCtors[SuperId]
}
const name = extendOptions.name || Super.options.name
if (process.env.NODE_ENV !== 'production' && name) {
validateComponentName(name)
}
const Sub = function VueComponent (options) {
this._init(options)
}
// 子类继承大Vue父类的原型
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
Sub.cid = cid
Sub.options = mergeOptions(
Super.options,
extendOptions
)
Sub['super'] = Super
// For props and computed properties, we define the proxy getters on
// the Vue instances at extension time, on the extended prototype. This
// avoids Object.defineProperty calls for each instance created.
if (Sub.options.props) {
initProps(Sub)
}
if (Sub.options.computed) {
initComputed(Sub)
}
// allow further extension/mixin/plugin usage
Sub.extend = Super.extend
Sub.mixin = Super.mixin
Sub.use = Super.use
// create asset registers, so extended classes
// can have their private assets too.
ASSET_TYPES.forEach(function (type) {
Sub[type] = Super[type]
})
// enable recursive self-lookup
if (name) {
Sub.options.components[name] = Sub // 记录自己 在组件中递归自己 -> jsx
}
// keep a reference to the super options at extension time.
// later at instantiation we can check if Super's options have
// been updated.
Sub.superOptions = Super.options
Sub.extendOptions = extendOptions
Sub.sealedOptions = extend({}, Sub.options)
// cache constructor
cachedCtors[SuperId] = Sub
return Sub
}
}
Vue-router 路由有哪些模式?
一般有两种模式:
代码语言:txt复制 (1)**hash 模式**:后面的 hash 值的变化,浏览器既不会向服务器发出请求,浏览器也不会刷新,每次 hash 值的变化会触发 hashchange 事件。
代码语言:txt复制 (2)**history 模式**:利用了 HTML5 中新增的 pushState() 和 replaceState() 方法。这两个方法应用于浏览器的历史记录栈,在当前已有的 back、forward、go 的基础之上,它们提供了对历史记录进行修改的功能。只是当它们执行修改时,虽然改变了当前的 URL,但浏览器不会立即向后端发送请求。
前端vue面试题详细解答
对keep-alive的理解,它是如何实现的,具体缓存的是什么?
如果需要在组件切换的时候,保存一些组件的状态防止多次渲染,就可以使用 keep-alive 组件包裹需要保存的组件。
(1)keep-alive
keep-alive有以下三个属性:
- include 字符串或正则表达式,只有名称匹配的组件会被匹配;
- exclude 字符串或正则表达式,任何名称匹配的组件都不会被缓存;
- max 数字,最多可以缓存多少组件实例。
注意:keep-alive 包裹动态组件时,会缓存不活动的组件实例。
主要流程
- 判断组件 name ,不在 include 或者在 exclude 中,直接返回 vnode,说明该组件不被缓存。
- 获取组件实例 key ,如果有获取实例的 key,否则重新生成。
- key生成规则,cid "∶∶" tag ,仅靠cid是不够的,因为相同的构造函数可以注册为不同的本地组件。
- 如果缓存对象内存在,则直接从缓存对象中获取组件实例给 vnode ,不存在则添加到缓存对象中。 5.最大缓存数量,当缓存组件数量超过 max 值时,清除 keys 数组内第一个组件。
(2)keep-alive 的实现
代码语言:javascript复制const patternTypes: Array<Function> = [String, RegExp, Array] // 接收:字符串,正则,数组
export default {
name: 'keep-alive',
abstract: true, // 抽象组件,是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在父组件链中。
props: {
include: patternTypes, // 匹配的组件,缓存
exclude: patternTypes, // 不去匹配的组件,不缓存
max: [String, Number], // 缓存组件的最大实例数量, 由于缓存的是组件实例(vnode),数量过多的时候,会占用过多的内存,可以用max指定上限
},
created() {
// 用于初始化缓存虚拟DOM数组和vnode的key
this.cache = Object.create(null)
this.keys = []
},
destroyed() {
// 销毁缓存cache的组件实例
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys)
}
},
mounted() {
// prune 削减精简[v.]
// 去监控include和exclude的改变,根据最新的include和exclude的内容,来实时削减缓存的组件的内容
this.$watch('include', (val) => {
pruneCache(this, (name) => matches(val, name))
})
this.$watch('exclude', (val) => {
pruneCache(this, (name) => !matches(val, name))
})
},
}
render函数:
- 会在 keep-alive 组件内部去写自己的内容,所以可以去获取默认 slot 的内容,然后根据这个去获取组件
- keep-alive 只对第一个组件有效,所以获取第一个子组件。
- 和 keep-alive 搭配使用的一般有:动态组件 和router-view
render () {
//
function getFirstComponentChild (children: ?Array<VNode>): ?VNode {
if (Array.isArray(children)) {
for (let i = 0; i < children.length; i ) {
const c = children[i]
if (isDef(c) && (isDef(c.componentOptions) || isAsyncPlaceholder(c))) {
return c
}
}
}
}
const slot = this.$slots.default // 获取默认插槽
const vnode: VNode = getFirstComponentChild(slot)// 获取第一个子组件
const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions // 组件参数
if (componentOptions) { // 是否有组件参数
// check pattern
const name: ?string = getComponentName(componentOptions) // 获取组件名
const { include, exclude } = this
if (
// not included
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
// 如果不匹配当前组件的名字和include以及exclude
// 那么直接返回组件的实例
return vnode
}
const { cache, keys } = this
// 获取这个组件的key
const key: ?string = vnode.key == null
// same constructor may get registered as different local components
// so cid alone is not enough (#3269)
? componentOptions.Ctor.cid (componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key
if (cache[key]) {
// LRU缓存策略执行
vnode.componentInstance = cache[key].componentInstance // 组件初次渲染的时候componentInstance为undefined
// make current key freshest
remove(keys, key)
keys.push(key)
// 根据LRU缓存策略执行,将key从原来的位置移除,然后将这个key值放到最后面
} else {
// 在缓存列表里面没有的话,则加入,同时判断当前加入之后,是否超过了max所设定的范围,如果是,则去除
// 使用时间间隔最长的一个
cache[key] = vnode
keys.push(key)
// prune oldest entry
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
}
// 将组件的keepAlive属性设置为true
vnode.data.keepAlive = true // 作用:判断是否要执行组件的created、mounted生命周期函数
}
return vnode || (slot && slot[0])
}
keep-alive 具体是通过 cache 数组缓存所有组件的 vnode 实例。当 cache 内原有组件被使用时会将该组件 key 从 keys 数组中删除,然后 push 到 keys数组最后,以便清除最不常用组件。
实现步骤:
- 获取 keep-alive 下第一个子组件的实例对象,通过他去获取这个组件的组件名
- 通过当前组件名去匹配原来 include 和 exclude,判断当前组件是否需要缓存,不需要缓存,直接返回当前组件的实例vNode
- 需要缓存,判断他当前是否在缓存数组里面:
- 存在,则将他原来位置上的 key 给移除,同时将这个组件的 key 放到数组最后面(LRU)
- 不存在,将组件 key 放入数组,然后判断当前 key数组是否超过 max 所设置的范围,超过,那么削减未使用时间最长的一个组件的 key
- 最后将这个组件的 keepAlive 设置为 true
(3)keep-alive 本身的创建过程和 patch 过程
缓存渲染的时候,会根据 vnode.componentInstance(首次渲染 vnode.componentInstance 为 undefined) 和 keepAlive 属性判断不会执行组件的 created、mounted 等钩子函数,而是对缓存的组件执行 patch 过程∶ 直接把缓存的 DOM 对象直接插入到目标元素中,完成了数据更新的情况下的渲染过程。
首次渲染
- 组件的首次渲染∶判断组件的 abstract 属性,才往父组件里面挂载 DOM
// core/instance/lifecycle
function initLifecycle (vm: Component) {
const options = vm.$options
// locate first non-abstract parent
let parent = options.parent
if (parent && !options.abstract) { // 判断组件的abstract属性,才往父组件里面挂载DOM
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
parent.$children.push(vm)
}
vm.$parent = parent
vm.$root = parent ? parent.$root : vm
vm.$children = []
vm.$refs = {}
vm._watcher = null
vm._inactive = null
vm._directInactive = false
vm._isMounted = false
vm._isDestroyed = false
vm._isBeingDestroyed = false
}
- 判断当前 keepAlive 和 componentInstance 是否存在来判断是否要执行组件 prepatch 还是执行创建 componentlnstance
// core/vdom/create-component
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) { // componentInstance在初次是undefined!!!
// kept-alive components, treat as a patch
const mountedNode: any = vnode // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode) // prepatch函数执行的是组件更新的过程
} else {
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
},
prepatch 操作就不会在执行组件的 mounted 和 created 生命周期函数,而是直接将 DOM 插入
(4)LRU (least recently used)缓存策略
LRU 缓存策略∶ 从内存中找出最久未使用的数据并置换新的数据。
LRU(Least rencently used)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是 "如果数据最近被访问过,那么将来被访问的几率也更高"。 最常见的实现是使用一个链表保存缓存数据,详细算法实现如下∶
- 新数据插入到链表头部
- 每当缓存命中(即缓存数据被访问),则将数据移到链表头部
- 链表满的时候,将链表尾部的数据丢弃。
常见的事件修饰符及其作用
.stop
:等同于 JavaScript 中的event.stopPropagation()
,防止事件冒泡;.prevent
:等同于 JavaScript 中的event.preventDefault()
,防止执行预设的行为(如果事件可取消,则取消该事件,而不停止事件的进一步传播);.capture
:与事件冒泡的方向相反,事件捕获由外到内;.self
:只会触发自己范围内的事件,不包含子元素;.once
:只会触发一次。
Vue 组件间通信有哪几种方式?
代码语言:txt复制Vue 组件间通信是面试常考的知识点之一,这题有点类似于开放题,你回答出越多方法当然越加分,表明你对 Vue 掌握的越熟练。Vue 组件间通信只要指以下 3 类通信:父子组件通信、隔代组件通信、兄弟组件通信,下面我们分别介绍每种通信方式且会说明此种方法可适用于哪类组件间通信。
(1)props / $emit
适用 父子组件通信
这种方法是 Vue 组件的基础,相信大部分同学耳闻能详,所以此处就不举例展开介绍。
(2)ref
与 $parent / $children
适用 父子组件通信
ref
:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例$parent
/$children
:访问父 / 子实例
(3)EventBus ($emit / $on)
适用于 父子、隔代、兄弟组件通信
这种方法通过一个空的 Vue 实例作为中央事件总线(事件中心),用它来触发事件和监听事件,从而实现任何组件间的通信,包括父子、隔代、兄弟组件。
(4)$attrs
/$listeners
适用于 隔代组件通信
$attrs
:包含了父作用域中不被 prop 所识别 (且获取) 的特性绑定 ( class 和 style 除外 )。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 ( class 和 style 除外 ),并且可以通过v-bind="$attrs"
传入内部组件。通常配合 inheritAttrs 选项一起使用。$listeners
:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过v-on="$listeners"
传入内部组件
(5)provide / inject
适用于 隔代组件通信
祖先组件中通过 provider 来提供变量,然后在子孙组件中通过 inject 来注入变量。 provide / inject API 主要解决了跨级组件间的通信问题,不过它的使用场景,主要是子组件获取上级组件的状态,跨级组件间建立了一种主动提供与依赖注入的关系。
(6)Vuex 适用于 父子、隔代、兄弟组件通信
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库)。“store” 基本上就是一个容器,它包含着你的应用中大部分的状态 ( state )。
- Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
- 改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化。
Vue 修饰符有哪些
事件修饰符
- .stop 阻止事件继续传播
- .prevent 阻止标签默认行为
- .capture 使用事件捕获模式,即元素自身触发的事件先在此处处理,然后才交由内部元素进行处理
- .self 只当在 event.target 是当前元素自身时触发处理函数
- .once 事件将只会触发一次
- .passive 告诉浏览器你不想阻止事件的默认行为
v-model 的修饰符
- .lazy 通过这个修饰符,转变为在 change 事件再同步
- .number 自动将用户的输入值转化为数值类型
- .trim 自动过滤用户输入的首尾空格
键盘事件的修饰符
- .enter
- .tab
- .delete (捕获“删除”和“退格”键)
- .esc
- .space
- .up
- .down
- .left
- .right
系统修饰键
- .ctrl
- .alt
- .shift
- .meta
鼠标按钮修饰符
- .left
- .right
- .middle
了解nextTick吗?
异步方法,异步渲染最后一步,与JS事件循环联系紧密。主要使用了宏任务微任务(setTimeout
、promise
那些),定义了一个异步方法,多次调用nextTick
会将方法存入队列,通过异步方法清空当前队列。
简述 mixin、extends 的覆盖逻辑
(1)mixin 和 extends mixin 和 extends均是用于合并、拓展组件的,两者均通过 mergeOptions 方法实现合并。
- mixins 接收一个混入对象的数组,其中混入对象可以像正常的实例对象一样包含实例选项,这些选项会被合并到最终的选项中。Mixin 钩子按照传入顺序依次调用,并在调用组件自身的钩子之前被调用。
- extends 主要是为了便于扩展单文件组件,接收一个对象或构造函数。
(2)mergeOptions 的执行过程
- 规范化选项(normalizeProps、normalizelnject、normalizeDirectives)
- 对未合并的选项,进行判断
if (!child._base) {
if (child.extends) {
parent = mergeOptions(parent, child.extends, vm);
}
if (child.mixins) {
for (let i = 0, l = child.mixins.length; i < l; i ) {
parent = mergeOptions(parent, child.mixins[i], vm);
}
}
}
- 合并处理。根据一个通用 Vue 实例所包含的选项进行分类逐一判断合并,如 props、data、 methods、watch、computed、生命周期等,将合并结果存储在新定义的 options 对象里。
- 返回合并结果 options。
vue的优点
轻量级框架:只关注视图层,是一个构建数据的视图集合,大小只有几十kb;
简单易学:国人开发,中文文档,不存在语言障碍 ,易于理解和学习;
双向数据绑定:保留了angular的特点,在数据操作方面更为简单;
组件化:保留了react的优点,实现了html的封装和重用,在构建单页面应用方面有着独特的优势;
视图,数据,结构分离:使数据的更改更为简单,不需要进行逻辑代码的修改,只需要操作数据就能完成相关操作;
虚拟DOM:dom操作是非常耗费性能的,不再使用原生的dom操作节点,极大解放dom操作,但具体操作的还是dom不过是换了另一种方式;
运行速度更快:相比较与react而言,同样是操作虚拟dom,就性能而言,vue存在很大的优势。
Vue template 到 render 的过程
vue的模版编译过程主要如下:template -> ast -> render函数
vue 在模版编译版本的码中会执行 compileToFunctions 将template转化为render函数:
代码语言:javascript复制// 将模板编译为render函数const { render, staticRenderFns } = compileToFunctions(template,options//省略}, this)
CompileToFunctions中的主要逻辑如下∶ (1)调用parse方法将template转化为ast(抽象语法树)
代码语言:javascript复制constast = parse(template.trim(), options)
- parse的目标:把tamplate转换为AST树,它是一种用 JavaScript对象的形式来描述整个模板。
- 解析过程:利用正则表达式顺序解析模板,当解析到开始标签、闭合标签、文本的时候都会分别执行对应的 回调函数,来达到构造AST树的目的。
AST元素节点总共三种类型:type为1表示普通元素、2为表达式、3为纯文本
(2)对静态节点做优化
代码语言:javascript复制optimize(ast,options)
这个过程主要分析出哪些是静态节点,给其打一个标记,为后续更新渲染可以直接跳过静态节点做优化
深度遍历AST,查看每个子树的节点元素是否为静态节点或者静态节点根。如果为静态节点,他们生成的DOM永远不会改变,这对运行时模板更新起到了极大的优化作用。
(3)生成代码
代码语言:javascript复制const code = generate(ast, options)
generate将ast抽象语法树编译成 render字符串并将静态部分放到 staticRenderFns 中,最后通过 new Function(`` render``)
生成render函数。
Vue中封装的数组方法有哪些,其如何实现页面更新
在Vue中,对响应式处理利用的是Object.defineProperty对数据进行拦截,而这个方法并不能监听到数组内部变化,数组长度变化,数组的截取变化等,所以需要对这些操作进行hack,让Vue能监听到其中的变化。 那Vue是如何实现让这些数组方法实现元素的实时更新的呢,下面是Vue中对这些方法的封装:
代码语言:javascript复制// 缓存数组原型
const arrayProto = Array.prototype;
// 实现 arrayMethods.__proto__ === Array.prototype
export const arrayMethods = Object.create(arrayProto);
// 需要进行功能拓展的方法
const methodsToPatch = [
"push",
"pop",
"shift",
"unshift",
"splice",
"sort",
"reverse"
];
/** * Intercept mutating methods and emit events */
methodsToPatch.forEach(function(method) {
// 缓存原生数组方法
const original = arrayProto[method];
def(arrayMethods, method, function mutator(...args) {
// 执行并缓存原生数组功能
const result = original.apply(this, args);
// 响应式处理
const ob = this.__ob__;
let inserted;
switch (method) {
// push、unshift会新增索引,所以要手动observer
case "push":
case "unshift":
inserted = args;
break;
// splice方法,如果传入了第三个参数,也会有索引加入,也要手动observer。
case "splice":
inserted = args.slice(2);
break;
}
//
if (inserted) ob.observeArray(inserted);// 获取插入的值,并设置响应式监听
// notify change
ob.dep.notify();// 通知依赖更新
// 返回原生数组方法的执行结果
return result;
});
});
简单来说就是,重写了数组中的那些原生方法,首先获取到这个数组的ob,也就是它的Observer对象,如果有新的值,就调用observeArray继续对新的值观察变化(也就是通过target__proto__ == arrayMethods
来改变了数组实例的型),然后手动调用notify,通知渲染watcher,执行update。
delete和Vue.delete删除数组的区别
delete
只是被删除的元素变成了empty/undefined
其他的元素的键值还是不变。Vue.delete
直接删除了数组 改变了数组的键值。
Vue模版编译原理知道吗,能简单说一下吗?
简单说,Vue的编译过程就是将template
转化为render
函数的过程。会经历以下阶段:
- 生成AST树
- 优化
- codegen
首先解析模版,生成AST语法树
(一种用JavaScript对象的形式来描述整个模板)。 使用大量的正则表达式对模板进行解析,遇到标签、文本的时候都会执行对应的钩子进行相关处理。
Vue的数据是响应式的,但其实模板中并不是所有的数据都是响应式的。有一些数据首次渲染后就不会再变化,对应的DOM也不会变化。那么优化过程就是深度遍历AST树,按照相关条件对树节点进行标记。这些被标记的节点(静态节点)我们就可以跳过对它们的比对
,对运行时的模板起到很大的优化作用。
编译的最后一步是将优化后的AST树转换为可执行的代码
。
$nextTick 原理及作用
Vue 的 nextTick 其本质是对 JavaScript 执行原理 EventLoop 的一种应用。
nextTick 的核心是利用了如 Promise 、MutationObserver、setImmediate、setTimeout的原生 JavaScript 方法来模拟对应的微/宏任务的实现,本质是为了利用 JavaScript 的这些异步回调任务队列来实现 Vue 框架中自己的异步回调队列。
nextTick 不仅是 Vue 内部的异步队列的调用方法,同时也允许开发者在实际项目中使用这个方法来满足实际应用中对 DOM 更新数据时机的后续逻辑处理
nextTick 是典型的将底层 JavaScript 执行原理应用到具体案例中的示例,引入异步更新队列机制的原因∶
- 如果是同步更新,则多次对一个或多个属性赋值,会频繁触发 UI/DOM 的渲染,可以减少一些无用渲染
- 同时由于 VirtualDOM 的引入,每一次状态发生变化后,状态变化的信号会发送给组件,组件内部使用 VirtualDOM 进行计算得出需要更新的具体的 DOM 节点,然后对 DOM 进行更新操作,每次更新状态后的渲染过程需要更多的计算,而这种无用功也将浪费更多的性能,所以异步渲染变得更加至关重要
Vue采用了数据驱动视图的思想,但是在一些情况下,仍然需要操作DOM。有时候,可能遇到这样的情况,DOM1的数据发生了变化,而DOM2需要从DOM1中获取数据,那这时就会发现DOM2的视图并没有更新,这时就需要用到了nextTick
了。
由于Vue的DOM操作是异步的,所以,在上面的情况中,就要将DOM2获取数据的操作写在$nextTick
中。
this.$nextTick(() => { // 获取数据的操作...})
所以,在以下情况下,会用到nextTick:
- 在数据变化后执行的某个操作,而这个操作需要使用随数据变化而变化的DOM结构的时候,这个操作就需要方法在
nextTick()
的回调函数中。 - 在vue生命周期中,如果在created()钩子进行DOM操作,也一定要放在
nextTick()
的回调函数中。
因为在created()钩子函数中,页面的DOM还未渲染,这时候也没办法操作DOM,所以,此时如果想要操作DOM,必须将操作的代码放在nextTick()
的回调函数中。
$route
和$router
的区别
$route
是“路由信息对象”,包括path
,params
,hash
,query
,fullPath
,matched
,name
等路由信息参数。- 而
$router
是“路由实例”对象包括了路由的跳转方法,钩子函数等
你是怎么处理vue项目中的错误的?
分析
- 这是一个综合应用题目,在项目中我们常常需要将App的异常上报,此时错误处理就很重要了。
- 这里要区分错误的类型,针对性做收集。
- 然后是将收集的的错误信息上报服务器。
思路
- 首先区分错误类型
- 根据错误不同类型做相应收集
- 收集的错误是如何上报服务器的
回答范例
- 应用中的错误类型分为"
接口异常
"和“代码逻辑异常
” - 我们需要根据不同错误类型做相应处理:接口异常是我们请求后端接口过程中发生的异常,可能是请求失败,也可能是请求获得了服务器响应,但是返回的是错误状态。以
Axios
为例,这类异常我们可以通过封装Axios
,在拦截器中统一处理整个应用中请求的错误。代码逻辑异常
是我们编写的前端代码中存在逻辑上的错误造成的异常,vue
应用中最常见的方式是使用全局错误处理函数app.config.errorHandler
收集错误 - 收集到错误之后,需要统一处理这些异常:分析错误,获取需要错误信息和数据。这里应该有效区分错误类型,如果是请求错误,需要上报接口信息,参数,状态码等;对于前端逻辑异常,获取错误名称和详情即可。另外还可以收集应用名称、环境、版本、用户信息,所在页面等。这些信息可以通过
vuex
存储的全局状态和路由信息获取
实践
axios
拦截器中处理捕获异常:
// 响应拦截器
instance.interceptors.response.use(
(response) => {
return response.data;
},
(error) => {
// 存在response说明服务器有响应
if (error.response) {
let response = error.response;
if (response.status >= 400) {
handleError(response);
}
} else {
handleError(null);
}
return Promise.reject(error);
},
);
vue
中全局捕获异常:
import { createApp } from 'vue'
const app = createApp(...)
app.config.errorHandler = (err, instance, info) => {
// report error to tracking services
}
处理接口请求错误:
代码语言:javascript复制function handleError(error, type) {
if(type == 1) {
// 接口错误,从config字段中获取请求信息
let { url, method, params, data } = error.config
let err_data = {
url, method,
params: { query: params, body: data },
error: error.data?.message || JSON.stringify(error.data),
})
}
}
处理前端逻辑错误:
代码语言:javascript复制function handleError(error, type) {
if(type == 2) {
let errData = null
// 逻辑错误
if(error instanceof Error) {
let { name, message } = error
errData = {
type: name,
error: message
}
} else {
errData = {
type: 'other',
error: JSON.strigify(error)
}
}
}
}
Vuex 的原理
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库)。“store” 基本上就是一个容器,它包含着你的应用中大部分的状态 ( state )。
- Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
- 改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样可以方便地跟踪每一个状态的变化。
Vuex为Vue Components建立起了一个完整的生态圈,包括开发中的API调用一环。 (1)核心流程中的主要功能:
- Vue Components 是 vue 组件,组件会触发(dispatch)一些事件或动作,也就是图中的 Actions;
- 在组件中发出的动作,肯定是想获取或者改变数据的,但是在 vuex 中,数据是集中管理的,不能直接去更改数据,所以会把这个动作提交(Commit)到 Mutations 中;
- 然后 Mutations 就去改变(Mutate)State 中的数据;
- 当 State 中的数据被改变之后,就会重新渲染(Render)到 Vue Components 中去,组件展示更新后的数据,完成一个流程。
(2)各模块在核心流程中的主要功能:
Vue Components
∶ Vue组件。HTML页面上,负责接收用户操作等交互行为,执行dispatch方法触发对应action进行回应。dispatch
∶操作行为触发方法,是唯一能执行action的方法。actions
∶ 操作行为处理模块。负责处理Vue Components接收到的所有交互行为。包含同步/异步操作,支持多个同名方法,按照注册的顺序依次触发。向后台API请求的操作就在这个模块中进行,包括触发其他action以及提交mutation的操作。该模块提供了Promise的封装,以支持action的链式触发。commit
∶状态改变提交操作方法。对mutation进行提交,是唯一能执行mutation的方法。mutations
∶状态改变操作方法。是Vuex修改state的唯一推荐方法,其他修改方式在严格模式下将会报错。该方法只能进行同步操作,且方法名只能全局唯一。操作之中会有一些hook暴露出来,以进行state的监控等。state
∶ 页面状态管理容器对象。集中存储Vuecomponents中data对象的零散数据,全局唯一,以进行统一的状态管理。页面显示所需的数据从该对象中进行读取,利用Vue的细粒度数据响应机制来进行高效的状态更新。getters
∶ state对象读取方法。图中没有单独列出该模块,应该被包含在了render中,Vue Components通过该方法读取全局state对象。
了解history有哪些方法吗?说下它们的区别
history
这个对象在html5
的时候新加入两个api
history.pushState()
和history.repalceState()
这两个API
可以在不进行刷新的情况下,操作浏览器的历史纪录。唯一不同的是,前者是新增一个历史记录,后者是直接替换当前的历史记录。
从参数上来说:
代码语言:javascript复制window.history.pushState(state,title,url)
//state:需要保存的数据,这个数据在触发popstate事件时,可以在event.state里获取
//title:标题,基本没用,一般传null
//url:设定新的历史纪录的url。新的url与当前url的origin必须是一样的,否则会抛出错误。url可以时绝对路径,也可以是相对路径。
//如 当前url是 https://www.baidu.com/a/,执行history.pushState(null, null, './qq/'),则变成 https://www.baidu.com/a/qq/,
//执行history.pushState(null, null, '/qq/'),则变成 https://www.baidu.com/qq/
window.history.replaceState(state,title,url)
//与pushState 基本相同,但她是修改当前历史纪录,而 pushState 是创建新的历史纪录
另外还有:
window.history.back()
后退window.history.forward()
前进window.history.go(1)
前进或者后退几步
从触发事件的监听上来说:
pushState()
和replaceState()
不能被popstate
事件所监听- 而后面三者可以,且用户点击浏览器前进后退键时也可以
在Vue中使用插件的步骤
- 采用
ES6
的import ... from ...
语法或CommonJS
的require()
方法引入插件 - 使用全局方法
Vue.use( plugin )
使用插件,可以传入一个选项对象Vue.use(MyPlugin, { someOption: true })
Vue-router 路由模式有几种
vue-router
有 3
种路由模式:hash
、history
、abstract
,对应的源码如下所示
switch (mode) {
case 'history':
this.history = new HTML5History(this, options.base)
break
case 'hash':
this.history = new HashHistory(this, options.base, this.fallback)
break
case 'abstract':
this.history = new AbstractHistory(this, options.base)
break
default:
if (process.env.NODE_ENV !== 'production') {
assert(false, `invalid mode: ${mode}`)
}
}
其中,3 种路由模式的说明如下:
hash
: 使用URL hash
值来作路由,支持所有浏览器history
: 依赖HTML5 History API
和服务器配置abstract
: 支持所有JavaScript
运行环境,如Node.js
服务器端。如果发现没有浏览器的API
,路由会自动强制进入这个模式.