Vuex和单纯的全局对象有什么区别?
- Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
- 不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样可以方便地跟踪每一个状态的变化,从而能够实现一些工具帮助更好地了解我们的应用。
Vue中v-html会导致哪些问题
- 可能会导致
xss
攻击 v-html
会替换掉标签内部的子元素
let template = require('vue-template-compiler');
let r = template.compile(`<div v-html="'<span>hello</span>'"></div>`)
// with(this){return _c('div',{domProps: {"innerHTML":_s('<span>hello</span>')}})}
console.log(r.render);
// _c 定义在core/instance/render.js
// _s 定义在core/instance/render-helpers/index,js
if (key === 'textContent' || key === 'innerHTML') {
if (vnode.children) vnode.children.length = 0
if (cur === oldProps[key]) continue // #6601 work around Chrome version <= 55 bug where single textNode // replaced by innerHTML/textContent retains its parentNode property
if (elm.childNodes.length === 1) {
elm.removeChild(elm.childNodes[0])
}
}
前端vue面试题详细解答
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
将被展开为:
代码语言:javascript复制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.observable你有了解过吗?说说看
一、Observable 是什么
Observable
翻译过来我们可以理解成可观察的
我们先来看一下其在Vue
中的定义
Vue.observable
,让一个对象变成响应式数据。Vue
内部会用它来处理data
函数返回的对象
返回的对象可以直接用于渲染函数和计算属性内,并且会在发生变更时触发相应的更新。也可以作为最小化的跨组件状态存储器
代码语言:javascript复制Vue.observable({ count : 1})
其作用等同于
代码语言:javascript复制new vue({ count : 1})
在 Vue 2.x
中,被传入的对象会直接被 Vue.observable
变更,它和被返回的对象是同一个对象
在 Vue 3.x
中,则会返回一个可响应的代理,而对源对象直接进行变更仍然是不可响应的
二、使用场景
在非父子组件通信时,可以使用通常的bus
或者使用vuex
,但是实现的功能不是太复杂,而使用上面两个又有点繁琐。这时,observable
就是一个很好的选择
创建一个js
文件
// 引入vue
import Vue from 'vue
// 创建state对象,使用observable让state对象可响应
export let state = Vue.observable({
name: '张三',
'age': 38
})
// 创建对应的方法
export let mutations = {
changeName(name) {
state.name = name
},
setAge(age) {
state.age = age
}
}
在.vue
文件中直接使用即可
<template>
<div>
姓名:{{ name }}
年龄:{{ age }}
<button @click="changeName('李四')">改变姓名</button>
<button @click="setAge(18)">改变年龄</button>
</div>
</template>
import { state, mutations } from '@/store
export default {
// 在计算属性中拿到值
computed: {
name() {
return state.name
},
age() {
return state.age
}
},
// 调用mutations里面的方法,更新数据
methods: {
changeName: mutations.changeName,
setAge: mutations.setAge
}
}
三、原理分析
源码位置:srccoreobserverindex.js
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
// 判断是否存在__ob__响应式属性
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
// 实例化Observer响应式对象
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount
}
return ob
}
Observer
类
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
// 实例化对象是一个对象,进入walk方法
this.walk(value)
}
}
walk
函数
walk (obj: Object) {
const keys = Object.keys(obj)
// 遍历key,通过defineReactive创建响应式对象
for (let i = 0; i < keys.length; i ) {
defineReactive(obj, keys[i])
}
}
defineReactive
方法
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
let childOb = !shallow && observe(val)
// 接下来调用Object.defineProperty()给对象定义响应式属性
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
// #7981: for accessor properties without setter
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
// 对观察者watchers进行通知,state就成了全局响应式对象
dep.notify()
}
})
}
Vue-Router 的懒加载如何实现
非懒加载:
代码语言:javascript复制import List from '@/components/list.vue'
const router = new VueRouter({
routes: [
{ path: '/list', component: List }
]
})
(1)方案一(常用):使用箭头函数 import动态加载
代码语言:javascript复制const List = () => import('@/components/list.vue')
const router = new VueRouter({
routes: [
{ path: '/list', component: List }
]
})
(2)方案二:使用箭头函数 require动态加载
代码语言:javascript复制const router = new Router({
routes: [
{
path: '/list',
component: resolve => require(['@/components/list'], resolve)
}
]
})
(3)方案三:使用webpack的require.ensure技术,也可以实现按需加载。 这种情况下,多个路由指定相同的chunkName,会合并打包成一个js文件。
代码语言:javascript复制// r就是resolve
const List = r => require.ensure([], () => r(require('@/components/list')), 'list');
// 路由也是正常的写法 这种是官方推荐的写的 按模块划分懒加载
const router = new Router({
routes: [
{
path: '/list',
component: List,
name: 'list'
}
]
}))
Watch中的deep:true是如何实现的
当用户指定了
watch
中的deep属性为true
时,如果当前监控的值是数组类型。会对对象中的每一项进行求值,此时会将当前watcher
存入到对应属性的依赖中,这样数组中对象发生变化时也会通知数据更新
源码相关
代码语言:javascript复制get () {
pushTarget(this) // 先将当前依赖放到 Dep.target上
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
if (this.deep) { // 如果需要深度监控
traverse(value) // 会对对象中的每一项取值,取值时会执行对应的get方法
}popTarget()
}
Vue中的过滤器了解吗?过滤器的应用场景有哪些?
过滤器实质不改变原始数据,只是对数据进行加工处理后返回过滤后的数据再进行调用处理,我们也可以理解其为一个纯函数
Vue 允许你自定义过滤器,可被用于一些常见的文本格式化
ps: Vue3
中已废弃filter
如何用
vue中的过滤器可以用在两个地方:双花括号插值和 v-bind
表达式,过滤器应该被添加在 JavaScript表达式的尾部,由“管道”符号指示:
<!-- 在双花括号中 -->
{ message | capitalize }
<!-- 在 `v-bind` 中 -->
<div v-bind:id="rawId | formatId"></div>
定义filter
在组件的选项中定义本地的过滤器
代码语言:javascript复制filters: {
capitalize: function (value) {
if (!value) return ''
value = value.toString()
return value.charAt(0).toUpperCase() value.slice(1)
}
}
定义全局过滤器:
代码语言:javascript复制Vue.filter('capitalize', function (value) {
if (!value) return ''
value = value.toString()
return value.charAt(0).toUpperCase() value.slice(1)
})
new Vue({
// ...
})
注意:当全局过滤器和局部过滤器重名时,会采用局部过滤器
过滤器函数总接收表达式的值 (之前的操作链的结果) 作为第一个参数。在上述例子中,capitalize
过滤器函数将会收到 message
的值作为第一个参数
过滤器可以串联:
代码语言:javascript复制{ message | filterA | filterB }
在这个例子中,filterA
被定义为接收单个参数的过滤器函数,表达式 message
的值将作为参数传入到函数中。然后继续调用同样被定义为接收单个参数的过滤器函数 filterB
,将 filterA
的结果传递到 filterB
中。
过滤器是 JavaScript
函数,因此可以接收参数:
{{ message | filterA('arg1', arg2) }}
这里,filterA
被定义为接收三个参数的过滤器函数。
其中 message
的值作为第一个参数,普通字符串 'arg1'
作为第二个参数,表达式 arg2
的值作为第三个参数
举个例子:
代码语言:html复制<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,')
})
原理分析
使用过滤器
代码语言:javascript复制{{ 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 computed 实现
- 建立与其他属性(如:
data
、Store
)的联系; - 属性改变后,通知计算属性重新计算
实现时,主要如下
- 初始化
data
, 使用Object.defineProperty
把这些属性全部转为getter/setter
。 - 初始化
computed
, 遍历computed
里的每个属性,每个computed
属性都是一个watch
实例。每个属性提供的函数作为属性的getter
,使用Object.defineProperty
转化。 Object.defineProperty getter
依赖收集。用于依赖发生变化时,触发属性重新计算。- 若出现当前
computed
计算属性嵌套其他computed
计算属性时,先进行其他的依赖收集
在Vue中使用插件的步骤
- 采用
ES6
的import ... from ...
语法或CommonJS
的require()
方法引入插件 - 使用全局方法
Vue.use( plugin )
使用插件,可以传入一个选项对象Vue.use(MyPlugin, { someOption: true })
SPA首屏加载速度慢的怎么解决
一、什么是首屏加载
首屏时间(First Contentful Paint
),指的是浏览器从响应用户输入网址地址,到首屏内容渲染完成的时间,此时整个网页不一定要全部渲染完成,但需要展示当前视窗需要的内容
首屏加载可以说是用户体验中最重要的环节
关于计算首屏时间
利用performance.timing
提供的数据:
通过DOMContentLoad
或者performance
来计算出首屏时间
// 方案一:
document.addEventListener('DOMContentLoaded', (event) => {
console.log('first contentful painting');
});
// 方案二:
performance.getEntriesByName("first-contentful-paint")[0].startTime
// performance.getEntriesByName("first-contentful-paint")[0]
// 会返回一个 PerformancePaintTiming的实例,结构如下:
{
name: "first-contentful-paint",
entryType: "paint",
startTime: 507.80000002123415,
duration: 0,
};
二、加载慢的原因
在页面渲染的过程,导致加载速度慢的因素可能如下:
- 网络延时问题
- 资源文件体积是否过大
- 资源是否重复发送请求去加载了
- 加载脚本的时候,渲染内容堵塞了
三、解决方案
常见的几种SPA首屏优化方式
- 减小入口文件积
- 静态资源本地缓存
- UI框架按需加载
- 图片资源的压缩
- 组件重复打包
- 开启GZip压缩
- 使用SSR
1. 减小入口文件体积
常用的手段是路由懒加载,把不同路由对应的组件分割成不同的代码块,待路由被请求的时候会单独打包路由,使得入口文件变小,加载速度大大增加
在vue-router
配置路由的时候,采用动态加载路由的形式
routes:[
path: 'Blogs',
name: 'ShowBlogs',
component: () => import('./components/ShowBlogs.vue')
]
以函数的形式加载路由,这样就可以把各自的路由文件分别打包,只有在解析给定的路由时,才会加载路由组件
2. 静态资源本地缓存
后端返回资源问题:
- 采用
HTTP
缓存,设置Cache-Control
,Last-Modified
,Etag
等响应头 - 采用
Service Worker
离线缓存
前端合理利用localStorage
3. UI框架按需加载
在日常使用UI
框架,例如element-UI
、或者antd
,我们经常性直接引用整个UI
库
import ElementUI from 'element-ui'
Vue.use(ElementUI)
但实际上我用到的组件只有按钮,分页,表格,输入与警告 所以我们要按需引用
代码语言:javascript复制import { Button, Input, Pagination, Table, TableColumn, MessageBox } from 'element-ui';
Vue.use(Button)
Vue.use(Input)
Vue.use(Pagination)
4. 组件重复打包
假设A.js
文件是一个常用的库,现在有多个路由使用了A.js
文件,这就造成了重复下载
解决方案:在webpack
的config
文件中,修改CommonsChunkPlugin
的配置
minChunks: 3
minChunks
为3表示会把使用3次及以上的包抽离出来,放进公共依赖文件,避免了重复加载组件
5. 图片资源的压缩
图片资源虽然不在编码过程中,但它却是对页面性能影响最大的因素
对于所有的图片资源,我们可以进行适当的压缩
对页面上使用到的icon
,可以使用在线字体图标,或者雪碧图,将众多小图标合并到同一张图上,用以减轻http
请求压力。
6. 开启GZip压缩
拆完包之后,我们再用gzip
做一下压缩 安装compression-webpack-plugin
cnmp i compression-webpack-plugin -D
在vue.congig.js
中引入并修改webpack
配置
const CompressionPlugin = require('compression-webpack-plugin')
configureWebpack: (config) => {
if (process.env.NODE_ENV === 'production') {
// 为生产环境修改配置...
config.mode = 'production'
return {
plugins: [new CompressionPlugin({
test: /.js$|.html$|.css/, //匹配文件名
threshold: 10240, //对超过10k的数据进行压缩
deleteOriginalAssets: false //是否删除原文件
})]
}
}
在服务器我们也要做相应的配置 如果发送请求的浏览器支持gzip
,就发送给它gzip
格式的文件 我的服务器是用express
框架搭建的 只要安装一下compression
就能使用
const compression = require('compression')
app.use(compression()) // 在其他中间件使用之前调用
7. 使用SSR
SSR(Server side ),也就是服务端渲染,组件或页面通过服务器生成html字符串,再发送到浏览器
从头搭建一个服务端渲染是很复杂的,vue
应用建议使用Nuxt.js
实现服务端渲染
四、小结
减少首屏渲染时间的方法有很多,总的来讲可以分成两大部分 :资源加载优化
和 页面渲染优化
下图是更为全面的首屏优化的方案
大家可以根据自己项目的情况选择各种方式进行首屏渲染的优化
v-model实现原理
我们在
vue
项目中主要使用v-model
指令在表单input
、textarea
、select
等元素上创建双向数据绑定,我们知道v-model
本质上不过是语法糖(可以看成是value input
方法的语法糖),v-model
在内部为不同的输入元素使用不同的属性并抛出不同的事件:
text
和textarea
元素使用value
属性和input
事件checkbox
和radio
使用checked
属性和change
事件select
字段将value
作为prop
并将change
作为事件
所以我们可以v-model进行如下改写:
代码语言:html复制<input v-model="sth" />
<!-- 等同于 -->
<input :value="sth" @input="sth = $event.target.value" />
当在
input
元素中使用v-model
实现双数据绑定,其实就是在输入的时候触发元素的input
事件,通过这个语法糖,实现了数据的双向绑定
- 这个语法糖必须是固定的,也就是说属性必须为
value
,方法名必须为:input
。 - 知道了
v-model
的原理,我们可以在自定义组件上实现v-model
//Parent
<template>
{{num}}
<Child v-model="num">
</template>
export default {
data(){
return {
num: 0
}
}
}
//Child
<template>
<div @click="add">Add</div>
</template>
export default {
props: ['value'], // 属性必须为value
methods:{
add(){
// 方法名为input
this.$emit('input', this.value 1)
}
}
}
原理
会将组件的 v-model
默认转化成value input
const VueTemplateCompiler = require('vue-template-compiler');
const ele = VueTemplateCompiler.compile('<el-checkbox v-model="check"></el- checkbox>');
// 观察输出的渲染函数:
// with(this) {
// return _c('el-checkbox', {
// model: {
// value: (check),
// callback: function ($$v) { check = $$v },
// expression: "check"
// }
// })
// }
代码语言:javascript复制// 源码位置 core/vdom/create-component.js line:155
function transformModel (options, data: any) {
const prop = (options.model && options.model.prop) || 'value'
const event = (options.model && options.model.event) || 'input'
;(data.attrs || (data.attrs = {}))[prop] = data.model.value
const on = data.on || (data.on = {})
const existing = on[event]
const callback = data.model.callback
if (isDef(existing)) {
if (Array.isArray(existing) ? existing.indexOf(callback) === -1 : existing !== callback ) {
on[event] = [callback].concat(existing)
}
} else {
on[event] = callback
}
}
原生的 v-model
,会根据标签的不同生成不同的事件和属性
const VueTemplateCompiler = require('vue-template-compiler');
const ele = VueTemplateCompiler.compile('<input v-model="value"/>');
// with(this) {
// return _c('input', {
// directives: [{ name: "model", rawName: "v-model", value: (value), expression: "value" }],
// domProps: { "value": (value) },
// on: {"input": function ($event) {
// if ($event.target.composing) return;
// value = $event.target.value
// }
// }
// })
// }
代码语言:javascript复制编译时:不同的标签解析出的内容不一样
platforms/web/compiler/directives/model.js
if (el.component) {
genComponentModel(el, value, modifiers) // component v-model doesn't need extra runtime
return false
} else if (tag === 'select') {
genSelect(el, value, modifiers)
} else if (tag === 'input' && type === 'checkbox') {
genCheckboxModel(el, value, modifiers)
} else if (tag === 'input' && type === 'radio') {
genRadioModel(el, value, modifiers)
} else if (tag === 'input' || tag === 'textarea') {
genDefaultModel(el, value, modifiers)
} else if (!config.isReservedTag(tag)) {
genComponentModel(el, value, modifiers) // component v-model doesn't need extra runtime
return false
}
代码语言:javascript复制运行时:会对元素处理一些关于输入法的问题
platforms/web/runtime/directives/model.js
inserted (el, binding, vnode, oldVnode) {
if (vnode.tag === 'select') { // #6903
if (oldVnode.elm && !oldVnode.elm._vOptions) {
mergeVNodeHook(vnode, 'postpatch', () => {
directive.componentUpdated(el, binding, vnode)
})
} else {
setSelected(el, binding, vnode.context)
}
el._vOptions = [].map.call(el.options, getValue)
} else if (vnode.tag === 'textarea' || isTextInputType(el.type)) {
el._vModifiers = binding.modifiers
if (!binding.modifiers.lazy) {
el.addEventListener('compositionstart', onCompositionStart)
el.addEventListener('compositionend', onCompositionEnd)
// Safari < 10.2 & UIWebView doesn't fire compositionend when
// switching focus before confirming composition choice
// this also fixes the issue where some browsers e.g. iOS Chrome
// fires "change" instead of "input" on autocomplete.
el.addEventListener('change', onCompositionEnd) /* istanbul ignore if */
if (isIE9) {
el.vmodel = true
}
}
}
}
怎么缓存当前的组件?缓存后怎么更新
缓存组件使用keep-alive
组件,这是一个非常常见且有用的优化手段,vue3
中keep-alive
有比较大的更新,能说的点比较多
思路
- 缓存用
keep-alive
,它的作用与用法 - 使用细节,例如缓存指定/排除、结合
router
和transition
- 组件缓存后更新可以利用
activated
或者beforeRouteEnter
- 原理阐述
回答范例
- 开发中缓存组件使用
keep-alive
组件,keep-alive
是vue
内置组件,keep-alive
包裹动态组件component
时,会缓存不活动的组件实例,而不是销毁它们,这样在组件切换过程中将状态保留在内存中,防止重复渲染DOM
<keep-alive>
<component :is="view"></component>
</keep-alive>
- 结合属性
include
和exclude
可以明确指定缓存哪些组件或排除缓存指定组件。vue3
中结合vue-router
时变化较大,之前是keep-alive
包裹router-view
,现在需要反过来用router-view
包裹keep-alive
<router-view v-slot="{ Component }">
<keep-alive>
<component :is="Component"></component>
</keep-alive>
</router-view>
- 缓存后如果要获取数据,解决方案可以有以下两种
beforeRouteEnter
:在有vue-router的
项目,每次进入路由的时候,都会执行beforeRouteEnter
beforeRouteEnter(to, from, next){
next(vm=>{
console.log(vm)
// 每次进入路由执行
vm.getData() // 获取数据
})
},
actived
:在keep-alive
缓存的组件被激活的时候,都会执行actived
钩子
activated(){
this.getData() // 获取数据
},
keep-alive
是一个通用组件,它内部定义了一个map
,缓存创建过的组件实例,它返回的渲染函数内部会查找内嵌的component
组件对应组件的vnode
,如果该组件在map
中存在就直接返回它。由于component
的is
属性是个响应式数据,因此只要它变化,keep-alive
的render
函数就会重新执行
Vue组件渲染和更新过程
渲染组件时,会通过
Vue.extend
方法构建子组件的构造函数,并进行实例化。最终手动调用$mount()
进行挂载。更新组件时会进行patchVnode
流程,核心就是diff
算法
二、如何解决
解决跨域的方法有很多,下面列举了三种:
- JSONP
- CORS
- Proxy
而在vue
项目中,我们主要针对CORS
或Proxy
这两种方案进行展开
CORS
CORS (Cross-Origin Resource Sharing,跨域资源共享)是一个系统,它由一系列传输的HTTP头组成,这些HTTP头决定浏览器是否阻止前端 JavaScript 代码获取跨域请求的响应
CORS
实现起来非常方便,只需要增加一些 HTTP
头,让服务器能声明允许的访问来源
只要后端实现了 CORS
,就实现了跨域
以koa
框架举例
添加中间件,直接设置Access-Control-Allow-Origin
响应头
app.use(async (ctx, next)=> {
ctx.set('Access-Control-Allow-Origin', '*');
ctx.set('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild');
ctx.set('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS');
if (ctx.method == 'OPTIONS') {
ctx.body = 200;
} else {
await next();
}
})
ps: Access-Control-Allow-Origin
设置为*其实意义不大,可以说是形同虚设,实际应用中,上线前我们会将Access-Control-Allow-Origin
值设为我们目标host
Proxy
代理(Proxy)也称网络代理,是一种特殊的网络服务,允许一个(一般为客户端)通过这个服务与另一个网络终端(一般为服务器)进行非直接的连接。一些网关、路由器等网络设备具备网络代理功能。一般认为代理服务有利于保障网络终端的隐私或安全,防止攻击
方案一
如果是通过vue-cli
脚手架工具搭建项目,我们可以通过webpack
为我们起一个本地服务器作为请求的代理对象
通过该服务器转发请求至目标服务器,得到结果再转发给前端,但是最终发布上线时如果web应用和接口服务器不在一起仍会跨域
在vue.config.js
文件,新增以下代码
amodule.exports = {
devServer: {
host: '127.0.0.1',
port: 8084,
open: true,// vue项目启动时自动打开浏览器
proxy: {
'/api': { // '/api'是代理标识,用于告诉node,url前面是/api的就是使用代理的
target: "http://xxx.xxx.xx.xx:8080", //目标地址,一般是指后台服务器地址
changeOrigin: true, //是否跨域
pathRewrite: { // pathRewrite 的作用是把实际Request Url中的'/api'用""代替
'^/api': ""
}
}
}
}
}
通过axios
发送请求中,配置请求的根路径
axios.defaults.baseURL = '/api'
方案二
此外,还可通过服务端实现代理请求转发
以express
框架为例
var express = require('express');
const proxy = require('http-proxy-middleware')
const app = express()
app.use(express.static(__dirname '/'))
app.use('/api', proxy({ target: 'http://localhost:4000', changeOrigin: false
}));
module.exports = app
方案三
通过配置nginx
实现代理
server {
listen 80;
location / {
root /var/www/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
location /api {
proxy_pass http://127.0.0.1:3000;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
谈一谈对Vue组件化的理解
- 组件化开发能大幅提高开发效率、测试性、复用性等
- 常用的组件化技术:属性、自定义事件、插槽
- 降低更新频率,只重新渲染变化的组件
- 组件的特点:高内聚、低耦合、单向数据流
Vue组件为什么只能有一个根元素
vue3
中没有问题
Vue.createApp({
components: {
comp: {
template: `
<div>root1</div>
<div>root2</div>
`
}
}
}).mount('#app')
vue2
中组件确实只能有一个根,但vue3
中组件已经可以多根节点了。- 之所以需要这样是因为
vdom
是一颗单根树形结构,patch
方法在遍历的时候从根节点开始遍历,它要求只有一个根节点。组件也会转换为一个vdom
vue3
中之所以可以写多个根节点,是因为引入了Fragment
的概念,这是一个抽象的节点,如果发现组件是多根的,就创建一个Fragment
节点,把多个根节点作为它的children
。将来patch
的时候,如果发现是一个Fragment
节点,则直接遍历children
创建或更新
Vue的事件绑定原理
原生事件绑定是通过
addEventListener
绑定给真实元素的,组件事件绑定是通过Vue
自定义的$on
实现的。如果要在组件上使用原生事件,需要加.native
修饰符,这样就相当于在父组件中把子组件当做普通html
标签,然后加上原生事件。
$on
、$emit
是基于发布订阅模式的,维护一个事件中心,on
的时候将事件按名称存在事件中心里,称之为订阅者,然后 emit
将对应的事件进行发布,去执行事件中心里的对应的监听器
EventEmitter(发布订阅模式--简单版)
代码语言:javascript复制// 手写发布订阅模式 EventEmitter
class EventEmitter {
constructor() {
this.events = {};
}
// 实现订阅
on(type, callBack) {
if (!this.events) this.events = Object.create(null);
if (!this.events[type]) {
this.events[type] = [callBack];
} else {
this.events[type].push(callBack);
}
}
// 删除订阅
off(type, callBack) {
if (!this.events[type]) return;
this.events[type] = this.events[type].filter(item => {
return item !== callBack;
});
}
// 只执行一次订阅事件
once(type, callBack) {
function fn() {
callBack();
this.off(type, fn);
}
this.on(type, fn);
}
// 触发事件
emit(type, ...rest) {
this.events[type] && this.events[type].forEach(fn => fn.apply(this, rest));
}
}
// 使用如下
const event = new EventEmitter();
const handle = (...rest) => {
console.log(rest);
};
event.on("click", handle);
event.emit("click", 1, 2, 3, 4);
event.off("click", handle);
event.emit("click", 1, 2);
event.once("dbClick", () => {
console.log(123456);
});
event.emit("dbClick");
event.emit("dbClick");
源码分析
- 原生 dom 的绑定
Vue
在创建真是dom
时会调用createElm
,默认会调用invokeCreateHooks
- 会遍历当前平台下相对的属性处理代码,其中就有
updateDOMListeners
方法,内部会传入add
方法
function updateDOMListeners (oldVnode: VNodeWithData, vnode: VNodeWithData) {
if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) {
return
}
const on = vnode.data.on || {}
const oldOn = oldVnode.data.on || {}
target = vnode.elm normalizeEvents(on)
updateListeners(on, oldOn, add, remove, createOnceHandler, vnode.context)
target = undefined
}
function add ( name: string, handler: Function, capture: boolean, passive: boolean ) {
target.addEventListener( // 给当前的dom添加事件
name,
handler,
supportsPassive ? { capture, passive } : capture
)
}
vue
中绑定事件是直接绑定给真实dom
元素的
- 组件中绑定事件
export function updateComponentListeners ( vm: Component, listeners: Object, oldListeners: ?Object ) {
target = vm updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm)
target = undefined
}
function add (event, fn) {
target.$on(event, fn)
}
组件绑定事件是通过
vue
中自定义的$on
方法来实现的
为什么Vue采用异步渲染
Vue 是组件级更新,如果不采用异步更新,那么每次更新数据都会对当前组件进行重新渲染,所以为了性能, Vue 会在本轮数据更新后,在异步更新视图。核心思想
nextTick
源码相关
代码语言:javascript复制
dep.notify()
通知watcher
进行更新,subs[i].update
依次调用watcher
的update
,queueWatcher
将watcher
去重放入队列,nextTick
(flushSchedulerQueue
)在下一tick
中刷新watcher
队列(异步)
update () { /* istanbul ignore else */
if (this.lazy) {
this.dirty = true
}
else if (this.sync) {
this.run()
}
else {
queueWatcher(this); // 当数据发生变化时会将watcher放到一个队列中批量更新
}
}
export function queueWatcher (watcher: Watcher) {
const id = watcher.id // 会对相同的watcher进行过滤
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i 1, 0, watcher)
}
// queue the flush
if (!waiting) {
waiting = true
if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
nextTick(flushSchedulerQueue) // 调用nextTick方法 批量的进行更新
}
}
}
watch 原理
watch
本质上是为每个监听属性 setter
创建了一个 watcher
,当被监听的属性更新时,调用传入的回调函数。常见的配置选项有 deep
和 immediate
,对应原理如下
deep
:深度监听对象,为对象的每一个属性创建一个watcher
,从而确保对象的每一个属性更新时都会触发传入的回调函数。主要原因在于对象属于引用类型,单个属性的更新并不会触发对象setter
,因此引入deep
能够很好地解决监听对象的问题。同时也会引入判断机制,确保在多个属性更新时回调函数仅触发一次,避免性能浪费。immediate
:在初始化时直接调用回调函数,可以通过在created
阶段手动调用回调函数实现相同的效果
说说你对slot的理解?slot使用场景有哪些
一、slot是什么
在HTML中 slot
元素 ,作为 Web Components
技术套件的一部分,是Web组件内的一个占位符
该占位符可以在后期使用自己的标记语言填充
举个栗子
代码语言:html复制<template id="element-details-template">
<slot name="element-name">Slot template</slot>
</template>
<element-details>
<span slot="element-name">1</span>
</element-details>
<element-details>
<span slot="element-name">2</span>
</element-details>
template
不会展示到页面中,需要用先获取它的引用,然后添加到DOM
中,
customElements.define('element-details',
class extends HTMLElement {
constructor() {
super();
const template = document
.getElementById('element-details-template')
.content;
const shadowRoot = this.attachShadow({mode: 'open'})
.appendChild(template.cloneNode(true));
}
})
在Vue
中的概念也是如此
Slot
艺名插槽,花名“占坑”,我们可以理解为solt
在组件模板中占好了位置,当使用该组件标签时候,组件标签里面的内容就会自动填坑(替换组件模板中slot
位置),作为承载分发内容的出口
二、使用场景
通过插槽可以让用户可以拓展组件,去更好地复用组件和对其做定制化处理
如果父组件在使用到一个复用组件的时候,获取这个组件在不同的地方有少量的更改,如果去重写组件是一件不明智的事情
通过slot
插槽向组件内部指定位置传递内容,完成这个复用组件在不同场景的应用
比如布局组件、表格列、下拉选、弹框显示内容等
SSR也就是服务端渲染,也就是将Vue在客户端把标签渲染成HTML的工作放在服务端完成,然后再把html直接返回给客户端
SSR的优势:
- 更好的SEO
- 首屏加载速度更快
SSR的缺点:
- 开发条件会受到限制,服务器端渲染只支持beforeCreate和created两个钩子;
- 当需要一些外部扩展库时需要特殊处理,服务端渲染应用程序也需要处于Node.js的运行环境;
- 更多的服务端负载。
action 与 mutation 的区别
mutation
是同步更新,$watch
严格模式下会报错action
是异步操作,可以获取数据后调用mutation
提交最终数据
前端vue面试题详细解答
slot是什么?有什么作用?原理是什么?
slot又名插槽,是Vue的内容分发机制,组件内部的模板引擎使用slot元素作为承载分发内容的出口。插槽slot是子组件的一个模板标签元素,而这一个标签元素是否显示,以及怎么显示是由父组件决定的。slot又分三类,默认插槽,具名插槽和作用域插槽。
- 默认插槽:又名匿名查抄,当slot没有指定name属性值的时候一个默认显示插槽,一个组件内只有有一个匿名插槽。
- 具名插槽:带有具体名字的插槽,也就是带有name属性的slot,一个组件可以出现多个具名插槽。
- 作用域插槽:默认插槽、具名插槽的一个变体,可以是匿名插槽,也可以是具名插槽,该插槽的不同点是在子组件渲染作用域插槽时,可以将子组件内部的数据传递给父组件,让父组件根据子组件的传递过来的数据决定如何渲染该插槽。
实现原理:当子组件vm实例化时,获取到父组件传入的slot标签的内容,存放在vm.$slot
中,默认插槽为vm.$slot.default
,具名插槽为vm.$slot.xxx
,xxx 为插槽名,当组件执行渲染函数时候,遇到slot标签,使用$slot
中的内容进行替换,此时可以为插槽传递数据,若存在数据,则可称该插槽为作用域插槽。
Vue的生命周期方法有哪些
Vue
实例有一个完整的生命周期,也就是从开始创建
、初始化数据
、编译模版
、挂载Dom -> 渲染
、更新 -> 渲染
、卸载
等一系列过程,我们称这是Vue
的生命周期Vue
生命周期总共分为8个阶段创建前/后
,载入前/后
,更新前/后
,销毁前/后
beforeCreate
=>created
=>beforeMount
=>Mounted
=>beforeUpdate
=>updated
=>beforeDestroy
=>destroyed
。keep-alive
下:activated
deactivated
生命周期vue2 | 生命周期vue3 | 描述 |
---|---|---|
|
| 在实例初始化之后,数据观测( |
|
| 实例已经创建完成之后被调用。在这一步,实例已完成以下的配置:数据观测( |
|
| 在挂载开始之前被调用:相关的 |
|
|
|
|
| 组件数据更新之前调用,发生在虚拟 |
|
| 由于数据更改导致的虚拟 |
|
| 实例销毁之前调用。在这一步,实例仍然完全可用 |
|
| 实例销毁后调用。调用后, |
其他几个生命周期
生命周期vue2 | 生命周期vue3 | 描述 |
---|---|---|
|
|
|
|
|
|
|
| 捕获一个来自子孙组件的错误时被调用 |
| 调试钩子,响应式依赖被收集时调用 | |
| 调试钩子,响应式依赖被触发时调用 | |
|
|
- 要掌握每个生命周期内部可以做什么事
beforeCreate
初始化vue
实例,进行数据观测。执行时组件实例还未创建,通常用于插件开发中执行一些初始化任务created
组件初始化完毕,可以访问各种数据,获取接口数据等beforeMount
此阶段vm.el
虽已完成DOM
初始化,但并未挂载在el
选项上mounted
实例已经挂载完成,可以进行一些DOM
操作beforeUpdate
更新前,可用于获取更新前各种状态。此时view
层还未更新,可用于获取更新前各种状态。可以在这个钩子中进一步地更改状态,这不会触发附加的重渲染过程。updated
完成view
层的更新,更新后,所有状态已是最新。可以执行依赖于DOM
的操作。然而在大多数情况下,你应该避免在此期间更改状态,因为这可能会导致更新无限循环。 该钩子在服务器端渲染期间不被调用。destroyed
可以执行一些优化操作,清空定时器,解除绑定事件- vue3
beforeunmount
:实例被销毁前调用,可用于一些定时器或订阅的取消 - vue3
unmounted
:销毁一个实例。可清理它与其它实例的连接,解绑它的全部指令及事件监听器
<div id="app">{{name}}</div>
<script>
const vm = new Vue({
data(){
return {name:'poetries'}
},
el: '#app',
beforeCreate(){
// 数据观测(data observer) 和 event/watcher 事件配置之前被调用。
console.log('beforeCreate');
},
created(){
// 属性和方法的运算, watch/event 事件回调。这里没有$el
console.log('created')
},
beforeMount(){
// 相关的 render 函数首次被调用。
console.log('beforeMount')
},
mounted(){
// 被新创建的 vm.$el 替换
console.log('mounted')
},
beforeUpdate(){
// 数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。
console.log('beforeUpdate')
},
updated(){
// 由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。
console.log('updated')
},
beforeDestroy(){
// 实例销毁之前调用 实例仍然完全可用
console.log('beforeDestroy')
},
destroyed(){
// 所有东西都会解绑定,所有的事件监听器会被移除
console.log('destroyed')
}
});
setTimeout(() => {
vm.name = 'poetry';
setTimeout(() => {
vm.$destroy()
}, 1000);
}, 1000);
</script>
- 组合式API生命周期钩子
你可以通过在生命周期钩子前面加上 “on
” 来访问组件的生命周期钩子。
下表包含如何在 setup()
内部调用生命周期钩子:
选项式 API | Hook inside setup |
---|---|
| 不需要* |
| 不需要* |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
代码语言:javascript复制因为
setup
是围绕beforeCreate
和created
生命周期钩子运行的,所以不需要显式地定义它们。换句话说,在这些钩子中编写的任何代码都应该直接在setup
函数中编写
export default {
setup() {
// mounted
onMounted(() => {
console.log('Component is mounted!')
})
}
}
setup
和created
谁先执行?
beforeCreate
:组件被创建出来,组件的methods
和data
还没初始化好setup
:在beforeCreate
和created
之间执行created
:组件被创建出来,组件的methods
和data
已经初始化好了
代码语言:javascript复制由于在执行
setup
的时候,created
还没有创建好,所以在setup
函数内我们是无法使用data
和methods
的。所以vue
为了让我们避免错误的使用,直接将setup
函数内的this
执行指向undefined
import { ref } from "vue"
export default {
// setup函数是组合api的入口函数,注意在组合api中定义的变量或者方法,要在template响应式需要return{}出去
setup(){
let count = ref(1)
function myFn(){
count.value =1
}
return {count,myFn}
},
}
- 其他问题
- 什么是vue生命周期? Vue 实例从创建到销毁的过程,就是生命周期。从开始创建、初始化数据、编译模板、挂载Dom→渲染、更新→渲染、销毁等一系列过程,称之为
Vue
的生命周期。
- vue生命周期的作用是什么? 它的生命周期中有多个事件钩子,让我们在控制整个Vue实例的过程时更容易形成好的逻辑。
- vue生命周期总共有几个阶段? 它可以总共分为
8
个阶段:创建前/后、载入前/后、更新前/后、销毁前/销毁后。 - 第一次页面加载会触发哪几个钩子? 会触发下面这几个
beforeCreate
、created
、beforeMount
、mounted
。 - 你的接口请求一般放在哪个生命周期中? 接口请求一般放在
mounted
中,但需要注意的是服务端渲染时不支持mounted
,需要放到created
中 - DOM 渲染在哪个周期中就已经完成? 在
mounted
中, - 注意
mounted
不会承诺所有的子组件也都一起被挂载。如果你希望等到整个视图都渲染完毕,可以用vm.$nextTick
替换掉mounted
mounted: function () {
代码语言:txt复制 this.$nextTick(function () {
代码语言:txt复制 // Code that will run only after the
代码语言:txt复制 // entire view has been rendered
代码语言:txt复制 })
代码语言:txt复制}
代码语言:txt复制
代码语言:txt复制### Vue 单页应用与多页应用的区别
**概念:**
- SPA单页面应用(SinglePage Web Application),指只有一个主页面的应用,一开始只需要加载一次js、css等相关资源。所有内容都包含在主页面,对每一个功能模块组件化。单页应用跳转,就是切换相关组件,仅仅刷新局部资源。
- MPA多页面应用 (MultiPage Application),指有多个独立页面的应用,每个页面必须重复加载js、css等相关资源。多页应用跳转,需要整页资源刷新。
### Vue的性能优化有哪些
**(1)编码阶段**
- 尽量减少data中的数据,data中的数据都会增加getter和setter,会收集对应的watcher
- v-if和v-for不能连用
- 如果需要使用v-for给每项元素绑定事件时使用事件代理
- SPA 页面采用keep-alive缓存组件
- 在更多的情况下,使用v-if替代v-show
- key保证唯一
- 使用路由懒加载、异步组件
- 防抖、节流
- 第三方模块按需导入
- 长列表滚动到可视区域动态加载
- 图片懒加载
**(2)SEO优化**
- 预渲染
- 服务端渲染SSR
**(3)打包优化**
- 压缩代码
- Tree Shaking/Scope Hoisting
- 使用cdn加载第三方模块
- 多线程打包happypack
- splitChunks抽离公共文件
- sourceMap优化
**(4)用户体验**
- 骨架屏
- PWA
- 还可以使用缓存(客户端缓存、服务端缓存)优化、服务端开启gzip压缩等。
## 如何理解Vue中模板编译原理
> `Vue` 的编译过程就是将 `template` 转化为 `render` 函数的过程
* **解析生成AST树** 将`template`模板转化成`AST`语法树,使用大量的正则表达式对模板进行解析,遇到标签、文本的时候都会执行对应的钩子进行相关处理
* **标记优化** 对静态语法做静态标记 `markup`(静态节点如`div`下有`p`标签内容不会变化) `diff`来做优化 静态节点跳过`diff`操作
* `Vue`的数据是响应式的,但其实模板中并不是所有的数据都是响应式的。有一些数据首次渲染后就不会再变化,对应的`DOM`也不会变化。那么优化过程就是深度遍历`AST`树,按照相关条件对树节点进行标记。这些被标记的节点(静态节点)我们就可以跳过对它们的比对,对运行时的模板起到很大的优化作用
* 等待后续节点更新,如果是静态的,不会在比较`children`了
* **代码生成** 编译的最后一步是将优化后的`AST`树转换为可执行的代码
回答范例
**思路**
* 引入`vue`编译器概念
* 说明编译器的必要性
* 阐述编译器工作流程
**回答范例**
1. `Vue`中有个独特的编译器模块,称为`compiler`,它的主要作用是将用户编写的`template`编译为`js`中可执行的`render`函数。
2. 之所以需要这个编译过程是为了便于前端能高效的编写视图模板。相比而言,我们还是更愿意用`HTML`来编写视图,直观且高效。手写`render`函数不仅效率底下,而且失去了编译期的优化能力。
3. 在`Vue`中编译器会先对`template`进行解析,这一步称为`parse`,结束之后会得到一个`JS`对象,我们称为 **抽象语法树AST** ,然后是对`AST`进行深加工的转换过程,这一步成为`transform`,最后将前面得到的`AST`生成为`JS`代码,也就是`render`函数
**可能的追问**
1. `Vue`中编译器何时执行?
![](https://s.poetries.work/uploads/2022/08/d1162df23e6b6fa4.png)
> 在 `new Vue()`之后。 `Vue` 会调用 `_init` 函数进行初始化,也就是这里的 i`nit` 过程,它会初始化生命周期、事件、 `props`、 `methods`、 `data`、 `computed` 与 `watch`等。其中最重要的是通过 `Object.defineProperty` 设置 `setter` 与 `getter` 函数,用来实现「响应式」以及「依赖收集」
* 初始化之后调用 `$mount` 会挂载组件,如果是运行时编译,即不存在 `render function` 但是存在 `template` 的情况,需要进行「编译」步骤
* `compile`编译可以分成 `parse`、`optimize` 与 `generate` 三个阶段,最终需要得到 `render function`
2. `React`有没有编译器?
`react` 使用`babel`将`JSX`语法解析
```html
<div id="app"></div>
<script>
代码语言:txt复制let vm = new Vue({
代码语言:txt复制 el: '#app',
代码语言:txt复制 template: `<div>
代码语言:txt复制 // <span>hello world</span> 是静态节点
代码语言:txt复制 <span>hello world</span>
代码语言:txt复制 // <p>{{name}}</p> 是动态节点
代码语言:txt复制 <p>{{name}}</p>
代码语言:txt复制 </div>`,
代码语言:txt复制 data() {
代码语言:txt复制 return { name: 'test' }
代码语言:txt复制 }
代码语言:txt复制});
</script>
代码语言:txt复制源码分析
```javascript
export function compileToFunctions(template) {
// 我们需要把html字符串变成render函数
// 1.把html代码转成ast语法树 ast用来描述代码本身形成树结构 不仅可以描述html 也能描述css以及js语法
// 很多库都运用到了ast 比如 webpack babel eslint等等
let ast = parse(template);
// 2.优化静态节点:对ast树进行标记,标记静态节点
代码语言:txt复制if (options.optimize !== false) {
代码语言:txt复制 optimize(ast, options);
代码语言:txt复制}
// 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;
}
代码语言:txt复制## Vue实例挂载的过程中发生了什么
#### 简单
TIP
**分析**
挂载过程完成了最重要的两件事:
* 初始化
* 建立更新机制
把这两件事说清楚即可!
**回答范例**
1. 挂载过程指的是`app.mount()`过程,这个过程中整体上做了两件事:**初始化**和**建立更新机制**
2. 初始化会创建组件实例、初始化组件状态,创建各种响应式数据
3. 建立更新机制这一步会立即执行一次组件更新函数,这会首次执行组件渲染函数并执行`patch`将前面获得`vnode`转换为`dom`;同时首次执行渲染函数会创建它内部响应式数据之间和组件更新函数之间的依赖关系,这使得以后数据变化时会执行对应的更新函数
来看一下源码,在`src/core/instance/index.js` 中
```javascript
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
代码语言:txt复制!(this instanceof Vue)
) {
代码语言:txt复制warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
代码语言:txt复制可以看到 `Vue` 只能通过 `new` 关键字初始化,然后会调用 `this._init` 方法, 该方法在 `src/core/instance/init.js` 中定义
```javascript
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid
vm._uid = uid
let startTag, endTag
/ istanbul ignore if /
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
代码语言:txt复制startTag = `vue-perf-start:${vm._uid}`
代码语言:txt复制endTag = `vue-perf-end:${vm._uid}`
代码语言:txt复制mark(startTag)
}
// a flag to avoid this being observed
vm._isVue = true
// merge options
if (options && options._isComponent) {
代码语言:txt复制// optimize internal component instantiation
代码语言:txt复制// since dynamic options merging is pretty slow, and none of the
代码语言:txt复制// internal component options needs special treatment.
代码语言:txt复制initInternalComponent(vm, options)
} else {
代码语言:txt复制vm.$options = mergeOptions(
代码语言:txt复制 resolveConstructorOptions(vm.constructor),
代码语言:txt复制 options || {},
代码语言:txt复制 vm
代码语言:txt复制)
}
/ istanbul ignore else /
if (process.env.NODE_ENV !== 'production') {
代码语言:txt复制initProxy(vm)
} else {
代码语言:txt复制vm._renderProxy = vm
}
// expose real self
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
/ istanbul ignore if /
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
代码语言:txt复制vm._name = formatComponentName(vm, false)
代码语言:txt复制mark(endTag)
代码语言:txt复制measure(`vue ${vm._name} init`, startTag, endTag)
}
if (vm.$options.el) {
代码语言:txt复制vm.$mount(vm.$options.el)
}
}
代码语言:txt复制> `Vue` 初始化主要就干了几件事情,`合并配置`,`初始化生命周期`,`初始化事件中心`,`初始化渲染`,`初始化 data`、`props`、`computed`、`watcher` 等
## Vue中如何检测数组变化
**前言**
`Vue` 不能检测到以下数组的变动:
* 当你利用索引直接设置一个数组项时,例如:`vm.items[indexOfItem] = newValue`
* 当你修改数组的长度时,例如:`vm.items.length = newLength`
`Vue` 提供了以下操作方法
```javascript
// Vue.set
Vue.set(vm.items, indexOfItem, newValue)
// vm.$set,Vue.set的一个别名
vm.$set(vm.items, indexOfItem, newValue)
// Array.prototype.splice
vm.items.splice(indexOfItem, 1, newValue)
代码语言:txt复制**分析**
> 数组考虑性能原因没有用 `defineProperty` 对数组的每一项进行拦截,而是选择对 `7` 种数组(`push`,`shift`,`pop`,`splice`,`unshift`,`sort`,`reverse`)方法进行重写(`AOP` 切片思想)
所以在 `Vue` 中修改数组的索引和长度是无法监控到的。需要通过以上 `7` 种变异方法修改数组才会触发数组对应的 `watcher` 进行更新
* 用函数劫持的方式,重写了数组方法,具体呢就是更改了数组的原型,更改成自己的,用户调数组的一些方法的时候,走的就是自己的方法,然后通知视图去更新
* 数组里每一项可能是对象,那么我就是会对数组的每一项进行观测,(且只有数组里的对象才能进行观测,观测过的也不会进行观测)
**原理**
> `Vue` 将 `data` 中的数组,进行了原型链重写。指向了自己定义的数组原型方法,这样当调用数组`api` 时,可以通知依赖更新,如果数组中包含着引用类型。会对数组中的引用类型再次进行监控。
![](https://img-blog.csdnimg.cn/img_convert/817272b5d140c873c67a77bfc8b7bbb8.png)
手写简版分析
```javascript
let oldArray = Object.create(Array.prototype);
'shift', 'unshift', 'push', 'pop', 'reverse','sort'.forEach(method => {
代码语言:txt复制oldArray[method] = function() { // 这里可以触发页面更新逻辑
代码语言:txt复制 console.log('method', method)
代码语言:txt复制 Array.prototype[method].call(this,...arguments);
代码语言:txt复制}
});
let arr = 1,2,3;
arr.proto = oldArray;
arr.unshift(4);
代码语言:txt复制源码分析
```javascript
// 拿到数组原型拷贝一份
const arrayProto = Array.prototype
// 然后将arrayMethods继承自数组原型
// 这里是面向切片编程思想(AOP)--不破坏封装的前提下,动态的扩展功能
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'
methodsToPatch.forEach(function (method) { // 重写原型方法
代码语言:txt复制const original = arrayProto[method] // 调用原数组的方法
代码语言:txt复制def(arrayMethods, method, function mutator (...args) {
代码语言:txt复制 // 这里保留原型方法的执行结果
代码语言:txt复制 const result = original.apply(this, args)
代码语言:txt复制 // 这句话是关键
代码语言:txt复制 // this代表的就是数据本身 比如数据是{a:[1,2,3]} 那么我们使用a.push(4) this就是a ob就是a.__ob__ 这个属性就是上段代码增加的 代表的是该数据已经被响应式观察过了指向Observer实例
代码语言:txt复制 const ob = this.__ob__
代码语言:txt复制 // 这里的标志就是代表数组有新增操作
代码语言:txt复制 let inserted
代码语言:txt复制 switch (method) {
代码语言:txt复制 case 'push':
代码语言:txt复制 case 'unshift':
代码语言:txt复制 inserted = args
代码语言:txt复制 break
代码语言:txt复制 case 'splice':
代码语言:txt复制 inserted = args.slice(2)
代码语言:txt复制 break
代码语言:txt复制 }
代码语言:txt复制 // 如果有新增的元素 inserted是一个数组 调用Observer实例的observeArray对数组每一项进行观测
代码语言:txt复制 if (inserted) ob.observeArray(inserted)
代码语言:txt复制 ob.dep.notify() // 当调用数组方法后,手动通知视图更新
代码语言:txt复制 return result
代码语言:txt复制})
})
this.observeArray(value) // 进行深度监控
代码语言:txt复制> `vue3`:改用 `proxy` ,可直接监听对象数组的变化
### 双向数据绑定的原理
Vue.js 是采用**数据劫持**结合**发布者-订阅者模式**的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。主要分为以下几个步骤:
1. 需要observe的数据对象进行递归遍历,包括子属性对象的属性,都加上setter和getter这样的话,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化
2. compile解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
3. Watcher订阅者是Observer和Compile之间通信的桥梁,主要做的事情是: ①在自身实例化时往属性订阅器(dep)里面添加自己 ②自身必须有一个update()方法 ③待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功成身退。
4. MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果。
### 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
### Vue的基本原理
当一个Vue实例创建时,Vue会遍历data中的属性,用 Object.defineProperty(vue3.0使用proxy )将它们转为 getter/setter,并且在内部追踪相关依赖,在属性被访问和修改时通知变化。 每个组件实例都有相应的 watcher 程序实例,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的setter被调用时,会通知watcher重新计算,从而致使它关联的组件得以更新。
### Vue complier 实现
* 模板解析这种事,本质是将数据转化为一段 `html` ,最开始出现在后端,经过各种处理吐给前端。随着各种 `mv*` 的兴起,模板解析交由前端处理。
* 总的来说,`Vue complier` 是将 `template` 转化成一个 `render` 字符串。
> 可以简单理解成以下步骤:
* `parse` 过程,将 `template` 利用正则转化成`AST` 抽象语法树。
* `optimize` 过程,标记静态节点,后 `diff` 过程跳过静态节点,提升性能。
* `generate` 过程,生成 `render` 字符串
### vue初始化页面闪动问题
使用vue开发时,在vue初始化之前,由于div是不归vue管的,所以我们写的代码在还没有解析的情况下会容易出现花屏现象,看到类似于{{message}}的字样,虽然一般情况下这个时间很短暂,但是还是有必要让解决这个问题的。
首先:在css里加上以下代码:
```javascript
v-cloak { display: none;}
代码语言:txt复制如果没有彻底解决问题,则在根元素加上`style="display: none;" :style="{display: 'block'}"`
### 对 React 和 Vue 的理解,它们的异同
**相似之处:**
- 都将注意力集中保持在核心库,而将其他功能如路由和全局状态管理交给相关的库;
- 都有自己的构建工具,能让你得到一个根据最佳实践设置的项目模板;
- 都使用了Virtual DOM(虚拟DOM)提高重绘性能;
- 都有props的概念,允许组件间的数据传递;
- 都鼓励组件化应用,将应用分拆成一个个功能明确的模块,提高复用性。
**不同之处 :**
**1)数据流**
Vue默认支持数据双向绑定,而React一直提倡单向数据流
**2)虚拟DOM**
Vue2.x开始引入"Virtual DOM",消除了和React在这方面的差异,但是在具体的细节还是有各自的特点。
- Vue宣称可以更快地计算出Virtual DOM的差异,这是由于它在渲染过程中,会跟踪每一个组件的依赖关系,不需要重新渲染整个组件树。
- 对于React而言,每当应用的状态被改变时,全部子组件都会重新渲染。当然,这可以通过 PureComponent/shouldComponentUpdate这个生命周期方法来进行控制,但Vue将此视为默认的优化。
**3)组件化**
React与Vue最大的不同是模板的编写。
- Vue鼓励写近似常规HTML的模板。写起来很接近标准 HTML元素,只是多了一些属性。
- React推荐你所有的模板通用JavaScript的语法扩展——JSX书写。
具体来讲:React中render函数是支持闭包特性的,所以import的组件在render中可以直接调用。但是在Vue中,由于模板中使用的数据都必须挂在 this 上进行一次中转,所以 import 一个组件完了之后,还需要在 components 中再声明下。 **4)监听数据变化的实现原理不同**
- Vue 通过 getter/setter 以及一些函数的劫持,能精确知道数据变化,不需要特别的优化就能达到很好的性能
- React 默认是通过比较引用的方式进行的,如果不优化(PureComponent/shouldComponentUpdate)可能导致大量不必要的vDOM的重新渲染。这是因为 Vue 使用的是可变数据,而React更强调数据的不可变。
**5)高阶组件**
react可以通过高阶组件(HOC)来扩展,而Vue需要通过mixins来扩展。
高阶组件就是高阶函数,而React的组件本身就是纯粹的函数,所以高阶函数对React来说易如反掌。相反Vue.js使用HTML模板创建视图组件,这时模板无法有效的编译,因此Vue不能采用HOC来实现。
**6)构建工具**
两者都有自己的构建工具:
- React ==> Create React APP
- Vue ==> vue-cli
**7)跨平台**
- React ==> React Native
- Vue ==> Weex
### 能说下 vue-router 中常用的 hash 和 history 路由模式实现原理吗?
**(1)hash 模式的实现原理**
早期的前端路由的实现就是基于 location.hash 来实现的。其实现原理很简单,location.hash 的值就是 URL 中 # 后面的内容。比如下面这个网站,它的 location.hash 的值为 '#search':
https://www.word.com#search
代码语言:txt复制hash 路由模式的实现主要是基于下面几个特性:
- URL 中 hash 值只是客户端的一种状态,也就是说当向服务器端发出请求时,hash 部分不会被发送;
hash 值的改变,都会在浏览器的访问历史中增加一个记录。因此我们能通过浏览器的回退、前进按钮控制hash 的切换;
- 可以通过 a 标签,并设置 href 属性,当用户点击这个标签后,URL 的 hash 值会发生改变;或者使用 JavaScript 来对 loaction.hash 进行赋值,改变 URL 的 hash 值;
- 我们可以使用 hashchange 事件来监听 hash 值的变化,从而对页面进行跳转(渲染)。
**(2)history 模式的实现原理**
HTML5 提供了 History API 来实现 URL 的变化。其中做最主要的 API 有以下两个:history.pushState() 和 history.repalceState()。这两个 API 可以在不进行刷新的情况下,操作浏览器的历史纪录。唯一不同的是,前者是新增一个历史记录,后者是直接替换当前的历史记录,如下所示:
window.history.pushState(null, null, path);
window.history.replaceState(null, null, path);
代码语言:txt复制history 路由模式的实现主要基于存在下面几个特性:
- pushState 和 repalceState 两个 API 来操作实现 URL 的变化 ;
- 我们可以使用 popstate 事件来监听 url 的变化,从而对页面进行跳转(渲染);
- history.pushState() 或 history.replaceState() 不会触发 popstate 事件,这时我们需要手动触发页面跳转(渲染)。
### template和jsx的有什么分别?
对于 runtime 来说,只需要保证组件存在 render 函数即可,而有了预编译之后,只需要保证构建过程中生成 render 函数就可以。在 webpack 中,使用`vue-loader`编译.vue文件,内部依赖的`vue-template-compiler`模块,在 webpack 构建过程中,将template预编译成 render 函数。与 react 类似,在添加了jsx的语法糖解析器`babel-plugin-transform-vue-jsx`之后,就可以直接手写render函数。
所以,template和jsx的都是render的一种表现形式,不同的是:JSX相对于template而言,具有更高的灵活性,在复杂的组件中,更具有优势,而 template 虽然显得有些呆滞。但是 template 在代码结构上更符合视图与逻辑分离的习惯,更简单、更直观、更好维护。
### nextTick 使用场景和原理
nextTick 中的回调是在下次 DOM 更新循环结束之后执行的延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。主要思路就是采用微任务优先的方式调用异步方法去执行 nextTick 包装的方法
相关代码如下
```javascript
let callbacks = [];
let pending = false;
function flushCallbacks() {
pending = false; //把标志还原为false
// 依次执行回调
for (let i = 0; i < callbacks.length; i ) {
代码语言:txt复制callbacks[i]();
}
}
let timerFunc; //定义异步方法 采用优雅降级
if (typeof Promise !== "undefined") {
// 如果支持promise
const p = Promise.resolve();
timerFunc = () => {
代码语言:txt复制p.then(flushCallbacks);
};
} else if (typeof MutationObserver !== "undefined") {
// MutationObserver 主要是监听dom变化 也是一个异步方法
let counter = 1;
const observer = new MutationObserver(flushCallbacks);
const textNode = document.createTextNode(String(counter));
observer.observe(textNode, {
代码语言:txt复制characterData: true,
});
timerFunc = () => {
代码语言:txt复制counter = (counter 1) % 2;
代码语言:txt复制textNode.data = String(counter);
};
} else if (typeof setImmediate !== "undefined") {
// 如果前面都不支持 判断setImmediate
timerFunc = () => {
代码语言:txt复制setImmediate(flushCallbacks);
};
} else {
// 最后降级采用setTimeout
timerFunc = () => {
代码语言:txt复制setTimeout(flushCallbacks, 0);
};
}
export function nextTick(cb) {
// 除了渲染watcher 还有用户自己手动调用的nextTick 一起被收集到数组
callbacks.push(cb);
if (!pending) {
代码语言:txt复制// 如果多次调用nextTick 只会执行一次异步 等异步队列清空之后再把标志变为false
代码语言:txt复制pending = true;
代码语言:txt复制timerFunc();
}
}
代码语言:txt复制
你有对 Vue 项目进行哪些优化?
(1)代码层面的优化
- v-if 和 v-show 区分使用场景
- computed 和 watch 区分使用场景
- v-for 遍历必须为 item 添加 key,且避免同时使用 v-if
- 长列表性能优化
- 事件的销毁
- 图片资源懒加载
- 路由懒加载
- 第三方插件的按需引入
- 优化无限列表性能
- 服务端渲染 SSR or 预渲染
(2)Webpack 层面的优化
- Webpack 对图片进行压缩
- 减少 ES6 转为 ES5 的冗余代码
- 提取公共代码
- 模板预编译
- 提取组件的 CSS
- 优化 SourceMap
- 构建结果输出分析
- Vue 项目的编译优化
(3)基础的 Web 技术的优化
- 开启 gzip 压缩
- 浏览器缓存
- CDN 的使用
- 使用 Chrome Performance 查找性能瓶颈
vue 中使用了哪些设计模式
1.工厂模式 - 传入参数即可创建实例
虚拟 DOM 根据参数的不同返回基础标签的 Vnode 和组件 Vnode
2.单例模式 - 整个程序有且仅有一个实例
vuex 和 vue-router 的插件注册方法 install 判断如果系统存在实例就直接返回掉
3.发布-订阅模式 (vue 事件机制)
4.观察者模式 (响应式数据原理)
5.装饰模式: (@装饰器的用法)
6.策略模式 策略模式指对象有某个行为,但是在不同的场景中,该行为有不同的实现方案-比如选项的合并策略