Vue的优点
- 轻量级框架:只关注视图层,是一个构建数据的视图集合,大小只有几十
kb
; - 简单易学:国人开发,中文文档,不存在语言障碍 ,易于理解和学习;
- 双向数据绑定:保留了
angular
的特点,在数据操作方面更为简单; - 组件化:保留了
react
的优点,实现了html
的封装和重用,在构建单页面应用方面有着独特的优势; - 视图,数据,结构分离:使数据的更改更为简单,不需要进行逻辑代码的修改,只需要操作数据就能完成相关操作;
- 虚拟DOM:
dom
操作是非常耗费性能的,不再使用原生的dom
操作节点,极大解放dom
操作,但具体操作的还是dom
不过是换了另一种方式; - 运行速度更快:相比较于
react
而言,同样是操作虚拟dom
,就性能而言,vue
存在很大的优势。
Vue2.x 响应式数据原理
整体思路是数据劫持+观察者模式
对象内部通过 defineReactive
方法,使用 Object.defineProperty
来劫持各个属性的 setter
、getter
(只会劫持已经存在的属性),数组则是通过重写数组7个方法
来实现。当页面使用对应属性时,每个属性都拥有自己的 dep
属性,存放他所依赖的 watcher
(依赖收集),当属性变化后会通知自己对应的 watcher
去更新(派发更新)
Object.defineProperty基本使用
function observer(value) { // proxy reflect
if (typeof value === 'object' && typeof value !== null)
for (let key in value) {
defineReactive(value, key, value[key]);
}
}
function defineReactive(obj, key, value) {
observer(value);
Object.defineProperty(obj, key, {
get() { // 收集对应的key 在哪个方法(组件)中被使用
return value;
},
set(newValue) {
if (newValue !== value) {
observer(newValue);
value = newValue; // 让key对应的方法(组件重新渲染)重新执行
}
}
})
}
let obj1 = { school: { name: 'poetry', age: 20 } };
observer(obj1);
console.log(obj1)
源码分析
class Observer {
// 观测值
constructor(value) {
this.walk(value);
}
walk(data) {
// 对象上的所有属性依次进行观测
let keys = Object.keys(data);
for (let i = 0; i < keys.length; i++) {
let key = keys[i];
let value = data[key];
defineReactive(data, key, value);
}
}
}
// Object.defineProperty数据劫持核心 兼容性在ie9以及以上
function defineReactive(data, key, value) {
observe(value); // 递归关键
// --如果value还是一个对象会继续走一遍odefineReactive 层层遍历一直到value不是对象才停止
// 思考?如果Vue数据嵌套层级过深 >>性能会受影响
Object.defineProperty(data, key, {
get() {
console.log("获取值");
//需要做依赖收集过程 这里代码没写出来
return value;
},
set(newValue) {
if (newValue === value) return;
console.log("设置值");
//需要做派发更新过程 这里代码没写出来
value = newValue;
},
});
}
export function observe(value) {
// 如果传过来的是对象或者数组 进行属性劫持
if (
Object.prototype.toString.call(value) === "[object Object]" ||
Array.isArray(value)
) {
return new Observer(value);
}
}
说一说你对vue响应式理解回答范例
- 所谓数据响应式就是能够使数据变化可以被检测并对这种变化做出响应的机制
MVVM
框架中要解决的一个核心问题是连接数据层和视图层,通过数据驱动应用,数据变化,视图更新,要做到这点的就需要对数据做响应式处理,这样一旦数据发生变化就可以立即做出更新处理- 以
vue
为例说明,通过数据响应式加上虚拟DOM
和patch
算法,开发人员只需要操作数据,关心业务,完全不用接触繁琐的DOM操作,从而大大提升开发效率,降低开发难度 vue2
中的数据响应式会根据数据类型来做不同处理,如果是 对象则采用Object.defineProperty()
的方式定义数据拦截,当数据被访问或发生变化时,我们感知并作出响应;如果是数组则通过覆盖数组对象原型的7个变更方法 ,使这些方法可以额外的做更新通知,从而作出响应。这种机制很好的解决了数据响应化的问题,但在实际使用中也存在一些缺点:比如初始化时的递归遍历会造成性能损失;新增或删除属性时需要用户使用Vue.set/delete
这样特殊的api
才能生效;对于es6
中新产生的Map
、Set
这些数据结构不支持等问题- 为了解决这些问题,
vue3
重新编写了这一部分的实现:利用ES6
的Proxy
代理要响应化的数据,它有很多好处,编程体验是一致的,不需要使用特殊api
,初始化性能和内存消耗都得到了大幅改善;另外由于响应化的实现代码抽取为独立的reactivity
包,使得我们可以更灵活的使用它,第三方的扩展开发起来更加灵活了
为什么要用 Vuex 或者 Redux
由于传参的方法对于多层嵌套的组件将会非常繁琐,并且对于兄弟组件间的状态传递无能为力。我们经常会采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝。以上的这些模式非常脆弱,通常会导致代码无法维护。
所以需要把组件的共享状态抽取出来,以一个全局单例模式管理。在这种模式下,组件树构成了一个巨大的"视图",不管在树的哪个位置,任何组件都能获取状态或者触发行为。
另外,通过定义和隔离状态管理中的各种概念并强制遵守一定的规则,代码将会变得更结构化且易维护。
delete和Vue.delete删除数组的区别?
delete
只是被删除的元素变成了empty/undefined
其他的元素的键值还是不变。Vue.delete
直接删除了数组 改变了数组的键值。
var a=[1,2,3,4]
var b=[1,2,3,4]
delete a[0]
console.log(a) //[empty,2,3,4]
this.$delete(b,0)
console.log(b) //[2,3,4]
vue-cli 工程常用的 npm 命令有哪些
- 下载
node_modules
资源包的命令:
npm install
- 启动
vue-cli
开发环境的 npm命令:
npm run dev
vue-cli
生成 生产环境部署资源 的npm
命令:
npm run build
- 用于查看
vue-cli
生产环境部署资源文件大小的npm
命令:
npm run build --report
在浏览器上自动弹出一个 展示
vue-cli
工程打包后app.js
、manifest.js
、vendor.js
文件里面所包含代码的页面。可以具此优化vue-cli
生产环境部署的静态资源,提升 页面 的加载速度
Vue项目中你是如何解决跨域的呢
一、跨域是什么
跨域本质是浏览器基于同源策略的一种安全手段
同源策略(Sameoriginpolicy),是一种约定,它是浏览器最核心也最基本的安全功能
所谓同源(即指在同一个域)具有以下三个相同点
- 协议相同(protocol)
- 主机相同(host)
- 端口相同(port)
反之非同源请求,也就是协议、端口、主机其中一项不相同的时候,这时候就会产生跨域
一定要注意跨域是浏览器的限制,你用抓包工具抓取接口数据,是可以看到接口已经把数据返回回来了,只是浏览器的限制,你获取不到数据。用postman请求接口能够请求到数据。这些再次印证了跨域是浏览器的限制。
vue-loader是什么?它有什么作用?
回答范例
vue-loader
是用于处理单文件组件(SFC
,Single-File Component
)的webpack loader
- 因为有了
vue-loader
,我们就可以在项目中编写SFC
格式的Vue
组件,我们可以把代码分割为<template>
、<script>
和<style>
,代码会异常清晰。结合其他loader
我们还可以用Pug
编写<template>
,用SASS
编写<style>
,用TS
编写<script>
。我们的<style>
还可以单独作用当前组件 webpack
打包时,会以loader
的方式调用vue-loader
vue-loader
被执行时,它会对SFC
中的每个语言块用单独的loader
链处理。最后将这些单独的块装配成最终的组件模块
原理
vue-loader
会调用@vue/compiler-sfc
模块解析SFC
源码为一个描述符(Descriptor
),然后为每个语言块生成import
代码,返回的代码类似下面
// source.vue被vue-loader处理之后返回的代码
// import the <template> block
import render from 'source.vue?vue&type=template'
// import the <script> block
import script from 'source.vue?vue&type=script'
export * from 'source.vue?vue&type=script'
// import <style> blocks
import 'source.vue?vue&type=style&index=1'
script.render = render
export default script
我们想要script
块中的内容被作为js
处理(当然如果是<script lang="ts">
被作为ts
理),这样我们想要webpack
把配置中跟.js
匹配的规则都应用到形如source.vue?vue&type=script
的这个请求上。例如我们对所有*.js
配置了babel-loader
,这个规则将被克隆并应用到所在Vue SFC
import script from 'source.vue?vue&type=script
将被展开为:
import script from 'babel-loader!vue-loader!source.vue?vue&type=script'
类似的,如果我们对.sass
文件配置了style-loader + css-loader + sass-loader
,对下面的代码
<style scoped lang="scss">
vue-loader
将会返回给我们下面结果:
import 'source.vue?vue&type=style&index=1&scoped&lang=scss'
然后webpack
会展开如下:
import 'style-loader!css-loader!sass-loader!vue-loader!source.vue?vue&type=style&index=1&scoped&lang=scss'
- 当处理展开请求时,
vue-loader
将被再次调用。这次,loader
将会关注那些有查询串的请求,且仅针对特定块,它会选中特定块内部的内容并传递给后面匹配的loader
- 对于
<script>
块,处理到这就可以了,但是<template>
和<style>
还有一些额外任务要做,比如- 需要用
Vue
模板编译器编译template
,从而得到render
函数 - 需要对
<style scoped>
中的CSS
做后处理(post-process
),该操作在css-loader
之后但在style-loader
之前
- 需要用
实现上这些附加的loader
需要被注入到已经展开的loader
链上,最终的请求会像下面这样:
// <template lang="pug">
import 'vue-loader/template-loader!pug-loader!source.vue?vue&type=template'
// <style scoped lang="scss">
import 'style-loader!vue-loader/style-post-loader!css-loader!sass-loader!vue-loader!source.vue?vue&type=style&index=1&scoped&lang=scss'
参考:前端vue面试题详细解答
vue 中使用了哪些设计模式
1.工厂模式 - 传入参数即可创建实例
虚拟 DOM 根据参数的不同返回基础标签的 Vnode 和组件 Vnode
2.单例模式 - 整个程序有且仅有一个实例
vuex 和 vue-router 的插件注册方法 install 判断如果系统存在实例就直接返回掉
3.发布-订阅模式 (vue 事件机制)
4.观察者模式 (响应式数据原理)
5.装饰模式: (@装饰器的用法)
6.策略模式 策略模式指对象有某个行为,但是在不同的场景中,该行为有不同的实现方案-比如选项的合并策略
如何理解Vue中模板编译原理
Vue
的编译过程就是将template
转化为render
函数的过程
- 解析生成AST树 将
template
模板转化成AST
语法树,使用大量的正则表达式对模板进行解析,遇到标签、文本的时候都会执行对应的钩子进行相关处理 - 标记优化 对静态语法做静态标记
markup
(静态节点如div
下有p
标签内容不会变化)diff
来做优化 静态节点跳过diff
操作Vue
的数据是响应式的,但其实模板中并不是所有的数据都是响应式的。有一些数据首次渲染后就不会再变化,对应的DOM
也不会变化。那么优化过程就是深度遍历AST
树,按照相关条件对树节点进行标记。这些被标记的节点(静态节点)我们就可以跳过对它们的比对,对运行时的模板起到很大的优化作用- 等待后续节点更新,如果是静态的,不会在比较
children
了
- 代码生成 编译的最后一步是将优化后的
AST
树转换为可执行的代码
回答范例
思路
- 引入
vue
编译器概念 - 说明编译器的必要性
- 阐述编译器工作流程
回答范例
Vue
中有个独特的编译器模块,称为compiler
,它的主要作用是将用户编写的template
编译为js
中可执行的render
函数。- 之所以需要这个编译过程是为了便于前端能高效的编写视图模板。相比而言,我们还是更愿意用
HTML
来编写视图,直观且高效。手写render
函数不仅效率底下,而且失去了编译期的优化能力。 - 在
Vue
中编译器会先对template
进行解析,这一步称为parse
,结束之后会得到一个JS
对象,我们称为 抽象语法树AST ,然后是对AST
进行深加工的转换过程,这一步成为transform
,最后将前面得到的AST
生成为JS
代码,也就是render
函数
可能的追问
Vue
中编译器何时执行?
在
new Vue()
之后。Vue
会调用_init
函数进行初始化,也就是这里的 init
过程,它会初始化生命周期、事件、props
、methods
、data
、computed
与watch
等。其中最重要的是通过Object.defineProperty
设置setter
与getter
函数,用来实现「响应式」以及「依赖收集」
- 初始化之后调用
$mount
会挂载组件,如果是运行时编译,即不存在render function
但是存在template
的情况,需要进行「编译」步骤 compile
编译可以分成parse
、optimize
与generate
三个阶段,最终需要得到render function
React
有没有编译器?
react
使用babel
将JSX
语法解析
<div id="app"></div>
<script>
let vm = new Vue({
el: '#app',
template: `<div>
// <span>hello world</span> 是静态节点
<span>hello world</span>
// <p>{{name}}</p> 是动态节点
<p>{{name}}</p>
</div>`,
data() {
return { name: 'test' }
}
});
</script>
源码分析
export function compileToFunctions(template) {
// 我们需要把html字符串变成render函数
// 1.把html代码转成ast语法树 ast用来描述代码本身形成树结构 不仅可以描述html 也能描述css以及js语法
// 很多库都运用到了ast 比如 webpack babel eslint等等
let ast = parse(template);
// 2.优化静态节点:对ast树进行标记,标记静态节点
if (options.optimize !== false) {
optimize(ast, options);
}
// 3.通过ast 重新生成代码
// 我们最后生成的代码需要和render函数一样
// 类似_c('div',{id:"app"},_c('div',undefined,_v("hello"+_s(name)),_c('span',undefined,_v("world"))))
// _c代表创建元素 _v代表创建文本 _s代表文Json.stringify--把对象解析成文本
let code = generate(ast);
// 使用with语法改变作用域为this 之后调用render函数可以使用call改变this 方便code里面的变量取值
let renderFn = new Function(`with(this){return ${code}}`);
return renderFn;
}
Vue中的过滤器了解吗?过滤器的应用场景有哪些?
过滤器实质不改变原始数据,只是对数据进行加工处理后返回过滤后的数据再进行调用处理,我们也可以理解其为一个纯函数
Vue 允许你自定义过滤器,可被用于一些常见的文本格式化
ps: Vue3
中已废弃filter
如何用
vue中的过滤器可以用在两个地方:双花括号插值和 v-bind
表达式,过滤器应该被添加在 JavaScript表达式的尾部,由“管道”符号指示:
<!-- 在双花括号中 -->
{ message | capitalize }
<!-- 在 `v-bind` 中 -->
<div v-bind:id="rawId | formatId"></div>
定义filter
在组件的选项中定义本地的过滤器
filters: {
capitalize: function (value) {
if (!value) return ''
value = value.toString()
return value.charAt(0).toUpperCase() + value.slice(1)
}
}
定义全局过滤器:
Vue.filter('capitalize', function (value) {
if (!value) return ''
value = value.toString()
return value.charAt(0).toUpperCase() + value.slice(1)
})
new Vue({
// ...
})
注意:当全局过滤器和局部过滤器重名时,会采用局部过滤器
过滤器函数总接收表达式的值 (之前的操作链的结果) 作为第一个参数。在上述例子中,capitalize
过滤器函数将会收到 message
的值作为第一个参数
过滤器可以串联:
{ message | filterA | filterB }
在这个例子中,filterA
被定义为接收单个参数的过滤器函数,表达式 message
的值将作为参数传入到函数中。然后继续调用同样被定义为接收单个参数的过滤器函数 filterB
,将 filterA
的结果传递到 filterB
中。
过滤器是 JavaScript
函数,因此可以接收参数:
{{ message | filterA('arg1', arg2) }}
这里,filterA
被定义为接收三个参数的过滤器函数。
其中 message
的值作为第一个参数,普通字符串 'arg1'
作为第二个参数,表达式 arg2
的值作为第三个参数
举个例子:
<div id="app">
<p>{{ msg | msgFormat('疯狂','--')}}</p>
</div>
<script>
// 定义一个 Vue 全局的过滤器,名字叫做 msgFormat
Vue.filter('msgFormat', function(msg, arg, arg2) {
// 字符串的 replace 方法,第一个参数,除了可写一个 字符串之外,还可以定义一个正则
return msg.replace(/单纯/g, arg+arg2)
})
</script>
小结:
- 部过滤器优先于全局过滤器被调用
- 一个表达式可以使用多个过滤器。过滤器之间需要用管道符“|”隔开。其执行顺序从左往右
应用场景
平时开发中,需要用到过滤器的地方有很多,比如单位转换
、数字打点
、文本格式化
、时间格式化
之类的等
比如我们要实现将30000 => 30,000
,这时候我们就需要使用过滤器
Vue.filter('toThousandFilter', function (value) {
if (!value) return ''
value = value.toString()
return .replace(str.indexOf('.') > -1 ? /(\d)(?=(\d{3})+\.)/g : /(\d)(?=(?:\d{3})+$)/g, '$1,')
})
原理分析
使用过滤器
{{ message | capitalize }}
在模板编译阶段过滤器表达式将会被编译为过滤器函数,主要是用过parseFilters
,我们放到最后讲
_s(_f('filterFormat')(message))
首先分析一下_f
:
_f
函数全名是:resolveFilter
,这个函数的作用是从this.$options.filters
中找出注册的过滤器并返回
// 变为
this.$options.filters['filterFormat'](message) // message为参数
关于resolveFilter
import { indentity,resolveAsset } from 'core/util/index'
export function resolveFilter(id){
return resolveAsset(this.$options,'filters',id,true) || identity
}
内部直接调用resolveAsset
,将option
对象,类型,过滤器id
,以及一个触发警告的标志作为参数传递,如果找到,则返回过滤器;
resolveAsset
的代码如下:
export function resolveAsset(options,type,id,warnMissing){ // 因为我们找的是过滤器,所以在 resolveFilter函数中调用时 type 的值直接给的 'filters',实际这个函数还可以拿到其他很多东西
if(typeof id !== 'string'){ // 判断传递的过滤器id 是不是字符串,不是则直接返回
return
}
const assets = options[type] // 将我们注册的所有过滤器保存在变量中
// 接下来的逻辑便是判断id是否在assets中存在,即进行匹配
if(hasOwn(assets,id)) return assets[id] // 如找到,直接返回过滤器
// 没有找到,代码继续执行
const camelizedId = camelize(id) // 万一你是驼峰的呢
if(hasOwn(assets,camelizedId)) return assets[camelizedId]
// 没找到,继续执行
const PascalCaseId = capitalize(camelizedId) // 万一你是首字母大写的驼峰呢
if(hasOwn(assets,PascalCaseId)) return assets[PascalCaseId]
// 如果还是没找到,则检查原型链(即访问属性)
const result = assets[id] || assets[camelizedId] || assets[PascalCaseId]
// 如果依然没找到,则在非生产环境的控制台打印警告
if(process.env.NODE_ENV !== 'production' && warnMissing && !result){
warn('Failed to resolve ' + type.slice(0,-1) + ': ' + id, options)
}
// 无论是否找到,都返回查找结果
return result
}
下面再来分析一下_s
:
_s
函数的全称是 toString
,过滤器处理后的结果会当作参数传递给 toString
函数,最终 toString
函数执行后的结果会保存到Vnode
中的text属性中,渲染到视图中
function toString(value){
return value == null
? ''
: typeof value === 'object'
? JSON.stringify(value,null,2)// JSON.stringify()第三个参数可用来控制字符串里面的间距
: String(value)
}
最后,在分析下parseFilters
,在模板编译阶段使用该函数阶段将模板过滤器解析为过滤器函数调用表达式
function parseFilters (filter) {
let filters = filter.split('|')
let expression = filters.shift().trim() // shift()删除数组第一个元素并将其返回,该方法会更改原数组
let i
if (filters) {
for(i = 0;i < filters.length;i++){
experssion = warpFilter(expression,filters[i].trim()) // 这里传进去的expression实际上是管道符号前面的字符串,即过滤器的第一个参数
}
}
return expression
}
// warpFilter函数实现
function warpFilter(exp,filter){
// 首先判断过滤器是否有其他参数
const i = filter.indexof('(')
if(i<0){ // 不含其他参数,直接进行过滤器表达式字符串的拼接
return `_f("${filter}")(${exp})`
}else{
const name = filter.slice(0,i) // 过滤器名称
const args = filter.slice(i+1) // 参数,但还多了 ‘)’
return `_f('${name}')(${exp},${args}` // 注意这一步少给了一个 ')'
}
}
小结:
- 在编译阶段通过
parseFilters
将过滤器编译成函数调用(串联过滤器则是一个嵌套的函数调用,前一个过滤器执行的结果是后一个过滤器函数的参数) - 编译后通过调用
resolveFilter
函数找到对应过滤器并返回结果 - 执行结果作为参数传递给
toString
函数,而toString
执行后,其结果会保存在Vnode
的text
属性中,渲染到视图
Vue项目中有封装过axios吗?主要是封装哪方面的?
一、axios是什么
axios
是一个轻量的 HTTP
客户端
基于 XMLHttpRequest
服务来执行 HTTP
请求,支持丰富的配置,支持 Promise
,支持浏览器端和 Node.js
端。自Vue
2.0起,尤大宣布取消对 vue-resource
的官方推荐,转而推荐 axios
。现在 axios
已经成为大部分 Vue
开发者的首选
特性
- 从浏览器中创建
XMLHttpRequests
- 从
node.js
创建http
请求 - 支持
Promise
API - 拦截请求和响应
- 转换请求数据和响应数据
- 取消请求
- 自动转换
JSON
数据 - 客户端支持防御
XSRF
基本使用
安装
// 项目中安装
npm install axios --S
// cdn 引入
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
导入
import axios from 'axios'
发送请求
axios({
url:'xxx', // 设置请求的地址
method:"GET", // 设置请求方法
params:{ // get请求使用params进行参数凭借,如果是post请求用data
type: '',
page: 1
}
}).then(res => {
// res为后端返回的数据
console.log(res);
})
并发请求axios.all([])
function getUserAccount() {
return axios.get('/user/12345');
}
function getUserPermissions() {
return axios.get('/user/12345/permissions');
}
axios.all([getUserAccount(), getUserPermissions()])
.then(axios.spread(function (res1, res2) {
// res1第一个请求的返回的内容,res2第二个请求返回的内容
// 两个请求都执行完成才会执行
}));
二、为什么要封装
axios
的 API 很友好,你完全可以很轻松地在项目中直接使用。
不过随着项目规模增大,如果每发起一次HTTP
请求,就要把这些比如设置超时时间、设置请求头、根据项目环境判断使用哪个请求地址、错误处理等等操作,都需要写一遍
这种重复劳动不仅浪费时间,而且让代码变得冗余不堪,难以维护。为了提高我们的代码质量,我们应该在项目中二次封装一下 axios
再使用
举个例子:
axios('http://localhost:3000/data', {
// 配置代码
method: 'GET',
timeout: 1000,
withCredentials: true,
headers: {
'Content-Type': 'application/json',
Authorization: 'xxx',
},
transformRequest: [function (data, headers) {
return data;
}],
// 其他请求配置...
})
.then((data) => {
// todo: 真正业务逻辑代码
console.log(data);
}, (err) => {
// 错误处理代码
if (err.response.status === 401) {
// handle authorization error
}
if (err.response.status === 403) {
// handle server forbidden error
}
// 其他错误处理.....
console.log(err);
});
如果每个页面都发送类似的请求,都要写一堆的配置与错误处理,就显得过于繁琐了
这时候我们就需要对axios
进行二次封装,让使用更为便利
三、如何封装
- 封装的同时,你需要和 后端协商好一些约定,请求头,状态码,请求超时时间.......
- 设置接口请求前缀:根据开发、测试、生产环境的不同,前缀需要加以区分
- 请求头 : 来实现一些具体的业务,必须携带一些参数才可以请求(例如:会员业务)
- 状态码: 根据接口返回的不同
status
, 来执行不同的业务,这块需要和后端约定好 - 请求方法:根据
get
、post
等方法进行一个再次封装,使用起来更为方便 - 请求拦截器: 根据请求的请求头设定,来决定哪些请求可以访问
- 响应拦截器: 这块就是根据 后端`返回来的状态码判定执行不同业务
设置接口请求前缀
利用node
环境变量来作判断,用来区分开发、测试、生产环境
if (process.env.NODE_ENV === 'development') {
axios.defaults.baseURL = 'http://dev.xxx.com'
} else if (process.env.NODE_ENV === 'production') {
axios.defaults.baseURL = 'http://prod.xxx.com'
}
在本地调试的时候,还需要在vue.config.js
文件中配置devServer
实现代理转发,从而实现跨域
devServer: {
proxy: {
'/proxyApi': {
target: 'http://dev.xxx.com',
changeOrigin: true,
pathRewrite: {
'/proxyApi': ''
}
}
}
}
设置请求头与超时时间
大部分情况下,请求头都是固定的,只有少部分情况下,会需要一些特殊的请求头,这里将普适性的请求头作为基础配置。当需要特殊请求头时,将特殊请求头作为参数传入,覆盖基础配置
const service = axios.create({
...
timeout: 30000, // 请求 30s 超时
headers: {
get: {
'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8'
// 在开发中,一般还需要单点登录或者其他功能的通用请求头,可以一并配置进来
},
post: {
'Content-Type': 'application/json;charset=utf-8'
// 在开发中,一般还需要单点登录或者其他功能的通用请求头,可以一并配置进来
}
},
})
封装请求方法
先引入封装好的方法,在要调用的接口重新封装成一个方法暴露出去
// get 请求
export function httpGet({
url,
params = {}
}) {
return new Promise((resolve, reject) => {
axios.get(url, {
params
}).then((res) => {
resolve(res.data)
}).catch(err => {
reject(err)
})
})
}
// post
// post请求
export function httpPost({
url,
data = {},
params = {}
}) {
return new Promise((resolve, reject) => {
axios({
url,
method: 'post',
transformRequest: [function (data) {
let ret = ''
for (let it in data) {
ret += encodeURIComponent(it) + '=' + encodeURIComponent(data[it]) + '&'
}
return ret
}],
// 发送的数据
data,
// url参数
params
}).then(res => {
resolve(res.data)
})
})
}
把封装的方法放在一个api.js
文件中
import { httpGet, httpPost } from './http'
export const getorglist = (params = {}) => httpGet({ url: 'apps/api/org/list', params })
页面中就能直接调用
// .vue
import { getorglist } from '@/assets/js/api'
getorglist({ id: 200 }).then(res => {
console.log(res)
})
这样可以把api
统一管理起来,以后维护修改只需要在api.js
文件操作即可
请求拦截器
请求拦截器可以在每个请求里加上token,做了统一处理后维护起来也方便
// 请求拦截器
axios.interceptors.request.use(
config => {
// 每次发送请求之前判断是否存在token
// 如果存在,则统一在http请求的header都加上token,这样后台根据token判断你的登录情况,此处token一般是用户完成登录后储存到localstorage里的
token && (config.headers.Authorization = token)
return config
},
error => {
return Promise.error(error)
})
响应拦截器
响应拦截器可以在接收到响应后先做一层操作,如根据状态码判断登录状态、授权
// 响应拦截器
axios.interceptors.response.use(response => {
// 如果返回的状态码为200,说明接口请求成功,可以正常拿到数据
// 否则的话抛出错误
if (response.status === 200) {
if (response.data.code === 511) {
// 未授权调取授权接口
} else if (response.data.code === 510) {
// 未登录跳转登录页
} else {
return Promise.resolve(response)
}
} else {
return Promise.reject(response)
}
}, error => {
// 我们可以在这里对异常状态作统一处理
if (error.response.status) {
// 处理请求失败的情况
// 对不同返回码对相应处理
return Promise.reject(error.response)
}
})
小结
- 封装是编程中很有意义的手段,简单的
axios
封装,就可以让我们可以领略到它的魅力 - 封装
axios
没有一个绝对的标准,只要你的封装可以满足你的项目需求,并且用起来方便,那就是一个好的封装方案
MVVM的优缺点?
优点:
- 分离视图(View)和模型(Model),降低代码耦合,提⾼视图或者逻辑的重⽤性: ⽐如视图(View)可以独⽴于Model变化和修改,⼀个ViewModel可以绑定不同的"View"上,当View变化的时候Model不可以不变,当Model变化的时候View也可以不变。你可以把⼀些视图逻辑放在⼀个ViewModel⾥⾯,让很多view重⽤这段视图逻辑
- 提⾼可测试性: ViewModel的存在可以帮助开发者更好地编写测试代码
- ⾃动更新dom: 利⽤双向绑定,数据更新后视图⾃动更新,让开发者从繁琐的⼿动dom中解放
缺点:
- Bug很难被调试: 因为使⽤双向绑定的模式,当你看到界⾯异常了,有可能是你View的代码有Bug,也可能是Model的代码有问题。数据绑定使得⼀个位置的Bug被快速传递到别的位置,要定位原始出问题的地⽅就变得不那么容易了。另外,数据绑定的声明是指令式地写在View的模版当中的,这些内容是没办法去打断点debug的
- ⼀个⼤的模块中model也会很⼤,虽然使⽤⽅便了也很容易保证了数据的⼀致性,当时⻓期持有,不释放内存就造成了花费更多的内存
- 对于⼤型的图形应⽤程序,视图状态较多,ViewModel的构建和维护的成本都会⽐较⾼。
action 与 mutation 的区别
mutation
是同步更新,$watch
严格模式下会报错action
是异步操作,可以获取数据后调用mutation
提交最终数据
Vue模版编译原理知道吗,能简单说一下吗?
简单说,Vue的编译过程就是将template
转化为render
函数的过程。会经历以下阶段:
- 生成AST树
- 优化
- codegen
首先解析模版,生成AST语法树
(一种用JavaScript对象的形式来描述整个模板)。 使用大量的正则表达式对模板进行解析,遇到标签、文本的时候都会执行对应的钩子进行相关处理。
Vue的数据是响应式的,但其实模板中并不是所有的数据都是响应式的。有一些数据首次渲染后就不会再变化,对应的DOM也不会变化。那么优化过程就是深度遍历AST树,按照相关条件对树节点进行标记。这些被标记的节点(静态节点)我们就可以跳过对它们的比对
,对运行时的模板起到很大的优化作用。
编译的最后一步是将优化后的AST树转换为可执行的代码
。
v-model 的原理?
我们在 vue 项目中主要使用 v-model 指令在表单 input、textarea、select 等元素上创建双向数据绑定,我们知道 v-model 本质上不过是语法糖,v-model 在内部为不同的输入元素使用不同的属性并抛出不同的事件:
- text 和 textarea 元素使用 value 属性和 input 事件;
- checkbox 和 radio 使用 checked 属性和 change 事件;
- select 字段将 value 作为 prop 并将 change 作为事件。
以 input 表单元素为例:
<input v-model='something'>
相当于
<input v-bind:value="something" v-on:input="something = $event.target.value">
如果在自定义组件中,v-model 默认会利用名为 value 的 prop 和名为 input 的事件,如下所示:
父组件:
<ModelChild v-model="message"></ModelChild>
子组件:
<div>{{value}}</div>
props:{
value: String
},
methods: {
test1(){
this.$emit('input', '小红')
},
},
使用vue渲染大量数据时应该怎么优化?说下你的思路!
分析
企业级项目中渲染大量数据的情况比较常见,因此这是一道非常好的综合实践题目。
回答
- 在大型企业级项目中经常需要渲染大量数据,此时很容易出现卡顿的情况。比如大数据量的表格、树
- 处理时要根据情况做不同处理:
- 可以采取分页的方式获取,避免渲染大量数据
- vue-virtual-scroller (opens new window)等虚拟滚动方案,只渲染视口范围内的数据
- 如果不需要更新,可以使用v-once方式只渲染一次
- 通过v-memo (opens new window)可以缓存结果,结合
v-for
使用,避免数据变化时不必要的VNode
创建 - 可以采用懒加载方式,在用户需要的时候再加载数据,比如
tree
组件子树的懒加载 - 还是要看具体需求,首先从设计上避免大数据获取和渲染;实在需要这样做可以采用虚表的方式优化渲染;最后优化更新,如果不需要更新可以
v-once
处理,需要更新可以v-memo
进一步优化大数据更新性能。其他可以采用的是交互方式优化,无线滚动、懒加载等方案
computed 的实现原理
computed 本质是一个惰性求值的观察者。
computed 内部实现了一个惰性的 watcher,也就是 computed watcher,computed watcher 不会立刻求值,同时持有一个 dep 实例。
其内部通过 this.dirty 属性标记计算属性是否需要重新求值。
当 computed 的依赖状态发生改变时,就会通知这个惰性的 watcher,
computed watcher 通过 this.dep.subs.length 判断有没有订阅者,
有的话,会重新计算,然后对比新旧值,如果变化了,会重新渲染。 (Vue 想确保不仅仅是计算属性依赖的值发生变化,而是当计算属性最终计算的值发生变化时才会触发渲染 watcher 重新渲染,本质上是一种优化。)
没有的话,仅仅把 this.dirty = true。 (当计算属性依赖于其他数据时,属性并不会立即重新计算,只有之后其他地方需要读取属性的时候,它才会真正计算,即具备 lazy(懒计算)特性。)
说下$attrs和$listeners的使用场景
API考察,但$attrs
和$listeners
是比较少用的边界知识,而且vue3
有变化,$listeners
已经移除,还是有细节可说的
体验
一个包含组件透传属性的对象
<template>
<child-component v-bind="$attrs">
将非属性特性透传给内部的子组件
</child-component>
</template>
回答范例
- 我们可能会有一些属性和事件没有在
props
中定义,这类称为非属性特性,结合v-bind
指令可以直接透传给内部的子组件。 - 这类“属性透传”常常用于包装高阶组件时往内部传递属性,常用于爷孙组件之间传参。比如我在扩展A组件时创建了组件B组件,然后在C组件中使用B,此时传递给C的属性中只有
props
里面声明的属性是给B使用的,其他的都是A需要的,此时就可以利用v-bind="$attrs"
透传下去。 - 最常见用法是结合
v-bind
做展开;$attrs
本身不是响应式的,除非访问的属性本身是响应式对象。 vue2
中使用listeners
获取事件,vue3
中已移除,均合并到attrs
中,使用起来更简单了
原理
查看透传属性foo
和普通属性bar
,发现vnode
结构完全相同,这说明vue3
中将分辨两者工作由框架完成而非用户指定:
<template>
<h1>{{ msg }}</h1>
<comp foo="foo" bar="bar" />
</template>
<template>
<div>
{{$attrs.foo}} {{bar}}
</div>
</template>
<script setup>
defineProps({
bar: String
})
</script>
_createVNode(Comp, {
foo: "foo",
bar: "bar"
})
keep-alive 中的生命周期哪些
keep-alive是 Vue 提供的一个内置组件,用来对组件进行缓存——在组件切换过程中将状态保留在内存中,防止重复渲染DOM。
如果为一个组件包裹了 keep-alive,那么它会多出两个生命周期:deactivated、activated。同时,beforeDestroy 和 destroyed 就不会再被触发了,因为组件不会被真正销毁。
当组件被换掉时,会被缓存到内存中、触发 deactivated 生命周期;当组件被切回来时,再去缓存里找这个组件、触发 activated钩子函数。
构建的 vue-cli 工程都到了哪些技术,它们的作用分别是什么
vue.js
:vue-cli
工程的核心,主要特点是 双向数据绑定 和 组件系统。vue-router
:vue
官方推荐使用的路由框架。vuex
:专为Vue.js
应用项目开发的状态管理器,主要用于维护vue
组件间共用的一些 变量 和 方法。axios
( 或者fetch
、ajax
):用于发起GET
、或POST
等http
请求,基于Promise
设计。vuex
等:一个专为vue
设计的移动端UI组件库。- 创建一个
emit.js
文件,用于vue
事件机制的管理。 webpack
:模块加载和vue-cli
工程打包器。