一、Vue基础
1、MVVM
- MVC:将应用抽象为数据层(Model)、视图层(View)、逻辑层(Controller),降低了项目的耦合度。
- View接收到用户请求时,将请求传递给Controller
- Controller解析用户的请求之后,调用Model层,完成Model的修改
- 之后Model层通知对应的View层更新(观察者模式)
这是最初的MVC设计,没有限制数据流,View和Model之间可以通信,两者耦合,后面又出现了变种MVC解决View对Model的依赖
-
MVP:限制了Model和View之间的通信,两者之间的交互必须要通过Presenter,现了Model和View的解耦,但与此同时Presenter负担过重,MVP之间的交互通过接口来进行。
-
MVVM:Model-View-ViewModel
- Model:数据模型,是一切的核心
- View:UI视图,数据最终体现在页面上
- ViewModel:构建双向数据流的桥梁(数据变化,视图渲染;视图变化,数据改变)
Model和View之间没有直接关系,通过ViewModel进行关联,并且ViewModel实现了视图的数据之间的双向绑定,数据更新会自动更新视图,开发者不需要关注视图,只需要关注数据。
MVVM的缺点:
- 对于大型的项目,视图状态较多,VM的构建和维护成本较高
2、Vue对于MVVM的实现/Vue的双向绑定原理
- 创建Vue实例时,首先将data注入到Vue实例中(能够通过this直接访问数据)
- 其次调用observer对象,将data转换为响应式对象
- 递归遍历数据对象,通过Object.defineProperty()劫持数据,重写数据的set和get方法
- getter中,将触发属性get的依赖添加到属性对应的依赖数组中
- setter中,遍历属性维护的依赖数组,调用依赖的update方法进行更新
- 调用compiler对象,对每个元素节点进行扫描分析,识别代码中的指令和插件表达式
- 当遇到代码中使用data的数据的地方,创建对应的watcher实例
- 在watcher的构造函数中,我们会将watcher实例挂载到一个静态对象上,并访问对应的属性值,可以在getter函数中将watcher实例添加到属性的订阅者数组中
- 这样页面首次渲染完成之后,数据和视图就关联起来了
依赖收集是observe和compiler共同协作实现的,简单来说就是:
- 数据劫持为每⼀个
key
创建⼀个Dep
实例 - 初始化视图时读取某个
key
,例如name1
,创建⼀个watcher1
- 由于触发
name1
的getter
方法,便将watcher1
添加到name1
对应的Dep中 - 当
name1
更新,setter
触发时,便可通过对应Dep
通知其管理所有Watcher
更新
发布者订阅者模式
- 发布者:属性被set时,向订阅中心发布消息
- 订阅中心:Dep,存储着消息事件和订阅者的缓存队列
- 订阅者:watcher,每个watcher实例都维护一个update方法
Vue
- 接收初始化参数
- data属性注入到vue实例中,转换成getter和setter
- 负责调用observe监听data中所有属性的变化
- 负责调用compiler解析指令/插值表达式
class Vue {
constructor (options) {
// 1. 通过属性保存选项的数据
this.$options = options || {}
this.$data = options.data || {}
this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el
// 2. 把data中的成员转换成 getter 和 setter,注入到 Vue 实例中
this._proxyData(this.$data)
// 3. 调用observer对象,监听数据的变化
new Observer(this.$data)
// 4. 调用compiler对象,解析指令和差值表达式
new Compiler(this)
}
// 约定 _ 开头,为私有属性
// 代理数据,即让 Vue 代理data中的属性
_proxyData (data) {
// 遍历data中的所有属性
Object.keys(data).forEach(key => {
// 把data的属性注入到vue实例中
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get () {
return data[key]
},
set (newValue) {
if (data[key] === newValue) {
return
}
data[key] = newValue
}
})
})
}
}
Observer
- 把data选项中的属性转换为响应式数据
class Observer {
constructor (data) {
this.walk(data)
}
walk (data) {
// 1. 判断 data 是否是对象
if (!data || typeof data !== 'object') {
return
}
// 2. 遍历data对象的所有属性
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key])
})
}
// 调用 Object.defineProperty() 将属性转换为 getter / setter
defineReactive (obj, key, val) {
const that = this
// 负责收集依赖,并发送通知
const dep = new Dep()
// 如果val是对象,把val内部的属性转换成响应式数据
this.walk(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get () {
// 收集依赖
Dep.target && dep.addSub(Dep.target)
// 此处不可以写成 obj[key],否则会发生死递归
// 这里使用闭包,扩展了val变量的作用域
return val
},
set (newValue) { // function,改变this
if (newValue === val) {
return
}
val = newValue
// 如果newValue是对象,把newValue内部的属性转换成响应式数据
that.walk(newValue)
// 发送通知
dep.notify()
}
})
}
}
Compiler
- 负责编译模板,解析指令和插值表达式
- 负责页面的首次渲染
- 当数据变化后,重新渲染视图
class Compiler {
constructor(vm) {
this.el = vm.$el // 记录模板
this.vm = vm // 记录 Vue 实例
this.compile(this.el)
}
// 编译模板,处理文本节点(差值表达式)和元素节点(指令)
compile(el) {
let childNodes = el.childNodes // 伪数组
// 将伪数组转换成数组
Array.from(childNodes).forEach(node => {
if (this.isTextNode(node)) {
// 处理文本节点
this.compileText(node)
} else if (this.isElementNode(node)) {
// 处理元素节点
this.compileElement(node)
}
// 判断node节点,是否有子节点,如果有子节点,要递归调用 compile
if (node.childNodes && node.childNodes.length) {
this.compile(node)
}
})
}
// 编译元素节点,处理指令
compileElement(node) {
// 遍历所有的属性节点
Array.from(node.attributes).forEach(attr => {
// 判断是否是指令
let attrName = attr.name // 获取属性名
if (this.isDirective(attrName)) {
// v-text ---> text
attrName = attrName.substr(2)
const key = attr.value // 获取属性值
this.update(node, key, attrName)
}
})
}
update (node, key, attrName) {
const updateFn = this[attrName + 'Updater']
// 改变 updateFn方法中的 this指向
updateFn && updateFn.call(this, node, this.vm[key], key)
}
// 处理 v-text 指令
textUpdater (node, value, key) {
node.textContent = value
new Watcher(this.vm, key, (newValue) => {
node.textContent = newValue
})
}
// 处理 v-model 指令
modelUpdater (node, value, key) {
node.value = value
new Watcher(this.vm, key, (newValue) => {
node.value = newValue
})
// 双向绑定
node.addEventListener('input', () => {
this.vm[key] = node.value
})
}
// 编译文本节点,处理 差值表达式
compileText(node) {
// {{ msg }}
// . 匹配任意的单个字符,不包括换行
// + 匹配前面修饰的字符出现一次或多次
// ? 表示非贪婪模式,即尽可能早的结束匹配
// 在正则表达式中,提取某个位置的内容,即添加(),进行分组
const reg = /\{\{(.+?)\}\}/ // 括号包裹的内容即为要提取的内容
const value = node.textContent
if (reg.test(value)) {
// 使用RegExp的构造函数,获取第一个分组的内容,即.$1
const key = RegExp.$1.trim()
node.textContent = value.replace(reg, this.vm[key])
// 创建watcher对象,当数据改变时更新视图
new Watcher(this.vm, key, (newValue) => {
node.textContent = newValue
})
}
}
// 判断元素属性是否是指令
isDirective(attrName) {
// 判断attrName是否以 v- 开头
return attrName.startsWith('v-')
}
// 判断节点是否是文本节点
isTextNode(node) {
return node.nodeType === 3
}
// 判断节点是否是元素节点
isElementNode(node) {
return node.nodeType === 1
}
}
Dep
- 收集依赖(添加观察者watcher)
- 通知所有的观察者
class Dep {
constructor() {
// 存储所有的观察者
this.subs = []
}
// 添加观察者
addSub(sub) {
if (sub && sub.update) {
this.subs.push(sub)
}
}
// 发送通知
notify() {
// 遍历所有的观察者
this.subs.forEach(sub => {
// 调用每一个观察者的update方法,更新视图
sub.update()
})
}
}
watcher
- 数据变化时,dep通知所有的watcher的update方法
- 自身实例化的时候,在Dep对象中添加自己
class Watcher {
constructor (vm, key, cb) {
this.vm = vm
// data 中的属性名称
this.key = key
// 回调函数负责更新视图
this.cb = cb
// 把watcher对象记录到Dep类的静态属性 target
Dep.target = this
// 触发get方法,在get方法中会调用addSub
this.oldValue = vm[key]
Dep.target = null // 防止重复添加
}
// 当数据发生变化的时候更新视图
update () {
const newValue = this.vm[this.key]
if (newValue === this.oldValue) {
return
}
// 当数据变化时,需要将新的值传递给回调函数,更新视图
this.cb(newValue)
}
}
3、Computed和Watch的区别
Computed
- 支持缓存,只有依赖的响应式数据(data/props)发生变化,才会重新计算
- 不支持异步
- 如果一个属性是由其他属性计算而来,这个属性依赖其它属性,一般会使用computed
- 如果computed属性的值是函数,那么默认使用get方法,函数的返回值就是属性的属性值
computed: {
example: {
get () {
return 'example'
},
set (newValue) {
console.log(newValue)
}
},
// 另一种形式的写法
a: function() {
return 'value'
}
}
Watch
- 侦听数据(响应式数据data/props)的变化,数据变化时,会触发函数,函数内部可以执行异步操作
- deep属性Vue性能消耗较大,对于要监听数据中某个属性的响应时,可以只给对应属性添加deep
watch: {
'obj.a': {
// 函数接收两个参数,第一个参数是最新的值,第二个参数是旧值
handler(newName, oldName) {
console.log('obj.a changed');
},
// 组件加载立即触发回调函数
immediate: true,
// 深度监听
deep: true
},
// 另一种形式写法
b(newval,oldVal) {
funtion()
}
}
- 初始加载情况下不会执行watch,可以设置immediate设置初始化的时候执行watch逻辑
- 此时watch的执行时机在created之前
- 如果watch监听的是一个computed属性,那么被监听的computed属性要排在watch之前,其次是watch,然后是created(其他未被监听的computed依旧在beforeCreate和created之间执行)
4、slot的作用以及原理
slot插槽,是Vue的内容分发机制,组件内部的模板引擎使用slot元素作为承载内容分发的出口。slot是子组件的一个模板标签元素,标签的显示由父组件决定。
- 默认插槽:又名匿名插槽,slot没有指定name,一个组件内只有一个默认插槽
- 具名插槽:可以指定name,将内容分发到不同的插槽中,一个组件可以有多个具名插槽
- 作用域插槽:默认插槽和具名插槽的一个变体,可以是具名插槽,也可以是匿名插槽。子组件渲染作用域插槽时,可以将子组件内部的数据传递给父组件
5、过滤器的作用以及实现
Vue中使用filters过滤数据,不会修改数据,只会改变用户看到的输出(computed和methods通过修改数据改变页面显示)
- 需要格式化数据并展示在页面上:改变时间、价格的输出显示
- 过滤器是一个函数,会把表达式中的值始终当做函数的第一个参数,过滤器用在插值表达式
{{ }}
和v-bind
表达式中
<li>商品价格:{{item.price | filterPrice}}</li>
filters: {
filterPrice (price) {
return price ? ('¥' + price) : '--'
}
}
6、如何保存组件当前的状态
- 将状态存储在localStorage/sessionStorage中
- Storage中存储的值是字符串,需要将对象转换为JSON字符串存储
- 路由传值
- keep-alive
7、常见的事件修饰符
.stop
:防止事件冒泡,等同于JavaScript中的event.stopPropagation().prevent
:防止事件的默认行为,等同于JavaScript的event.preventDefault().capture
:改变事件冒泡的方向,事件捕获由外到内.self
:只会触发自己范围内的事件,不包含子元素.once
:只触发一次
8、v-if、v-show、v-html
v-if
:根据v-if的值判断是否生成vnode,v-if为false的时候,不会render节点v-show
:会生成vnode,render时也会渲染成真实节点,只是在render的时候会再节点属性中修改属性值,设置displayv-html
:会首先移除所有子元素节点,将节点的innerHtml设置为v-html的值
v-if和v-show的区别
区别 | v-if | v-show |
---|---|---|
显隐的方式 | 动态向DOM树中添加/删除节点 | 设置DOM的display属性 |
编译过程 | 切换值会有一个局部编译/卸载的过程(销毁、重建内部的监听事件和子组件) | 基于CSS的切换 |
编译条件 | 惰性的,如果初始条件为假,则什么也不做,为真时进行编译 | 无论是否为真都会编译 |
性能消耗 | 切换消耗高 | 初始渲染消耗高 |
场景 | 适用于切换条件不易改变 | 适用于频繁切换条件 |
9、v-model的实现
(1)用在表单上
- input的 value值与数据绑定,数据改变引起视图改变
- 触发input输入事件时,修改数据的值,视图改变因此数据改变
<input v-model="sth" />
// 等同于
<input
v-bind:value="message"
v-on:input="message=$event.target.value"
>
//$event 指代当前触发的事件对象;
//$event.target 指代当前触发的事件对象的dom;
//$event.target.value 就是当前dom的value值;
//在@input方法中,value => sth;
//在:value中,sth => value;
(2)用在自定义组件上
- 本质是父子通信的语法糖,通过prop和$emit实现
<child v-model="message"></child>
<child :value="message" @input="function(e){message = e}"></child>
10、data为什么是一个函数而不是对象
JavaScript中的对象是引用类型,当多个实例引用同一个对象时,如果一个实例对对象进行了修改,其他实例中的数据也会修改。
Vue组件复用时,同一个组件的不同实例要有自己的数据。
因此不能将data写成对象,要写成函数的形式,每次复用组件时,都会返回一个新的数据对象,每个组件维护自己的数据,不会干扰其他组件实例。
11、keep-alive
(1)keep-alive
keep-alive
是vue
中的内置组件,能在组件切换过程中缓存不活动的组件实例,而不是销毁它,防止重复渲染DOM
,keep-alive
不会向DOM添加额外节点。
keep-alive
包裹动态组件时,只会渲染第一个子组件。
有以下三个属性:
- include:字符串或正则表达式,只有名称匹配的组件会被匹配
- exclude:字符串或正则表达式,被名称匹配的组件不会被缓存
- max:最多可以缓存多少组件实例
匹配首先检查组件自身的 name
选项,如果 name
选项不可用,则匹配它的局部注册名称 (父组件 components
选项的键值),匿名组件不能被匹配。
设置了 keep-alive 缓存的组件,会多出两个生命周期钩子(activated
与deactivated
):
- 首次进入组件时:
beforeRouteEnter
>beforeCreate
>created
>mounted
>activated
> ... ... >beforeRouteLeave
>deactivated
- 再次进入组件时:
beforeRouteEnter
>activated
> ... ... >beforeRouteLeave
>deactivated
<keep-alive include="a,b">
<component :is="view"></component>
</keep-alive>
<!-- 正则表达式 (使用 `v-bind`) -->
<keep-alive :include="/a|b/">
<component :is="view"></component>
</keep-alive>
<!-- 数组 (使用 `v-bind`) -->
<keep-alive :include="['a', 'b']">
<component :is="view"></component>
</keep-alive>
(2)原理
- 获取keep-alive下第一个子组件的实例对象,通过实例对象获取组件名
- 通过组件名去匹配include和exclude,判断当前组件是否需要缓存
- 不需要缓存,返回vnode
- 需要缓存,判断缓存数组中是否存在实例
- 存在,则将他原来位置上的 key 给移除,同时将这个组件的 key 放到数组最后面(LRU),
- 不存在,将组件 key 放入数组,然后判断当前 key数组是否超过 max 所设置的范围,超过,那么削减未使用时间最长的一个组件的 key
- 最后将这个组件的 keepAlive 设置为 true
// 源码 => vue/src/core/components/keep-alive.js
export default {
name: 'keep-alive',
abstract: true, //定义抽象组件 判断当前组件虚拟dom是否渲染成真实dom
props: {
include: patternTypes,
exclude: patternTypes,
max: [String, Number]
},
created () {
this.cache = Object.create(null) // 缓存VNode
this.keys = [] // 缓存VNode的key
},
destroyed () {
// 销毁时删除所有缓存的VNode
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys)
}
},
mounted () {
// 监听 include和exclude属性,及时的更新缓存
// pruneCache 对cache做遍历,把不符合新规则的VNode从缓存中移除
this.$watch('include', val => {
pruneCache(this, name => matches(val, name))
})
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
})
},
render() {
/* 获取默认插槽中的第一个组件节点 */
const slot = this.$slots.default
const vnode = getFirstComponentChild(slot)
/* 获取该组件节点的componentOptions */
const componentOptions = vnode && vnode.componentOptions
if (componentOptions) {
/* 获取该组件节点的名称,优先获取组件的name字段,如果name不存在则获取组件的tag */
const name = getComponentName(componentOptions)
const { include, exclude } = this
/* 如果name不在inlcude中或者存在于exlude中则表示不缓存,直接返回vnode */
if (
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
return vnode
}
const { cache, keys } = this
/* 获取组件的key值 */
const key = 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
/* 拿到key值后去this.cache对象中去寻找是否有该值,如果有则表示该组件有缓存,即命中缓存 */
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance
// make current key freshest
remove(keys, key)
keys.push(key)
}
/* 如果没有命中缓存,则将其设置进缓存 */
else {
cache[key] = vnode
keys.push(key)
// prune oldest entry
/* 如果配置了max并且缓存的长度超过了this.max,则从缓存中删除第一个 */
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
}
vnode.data.keepAlive = true
}
return vnode || (slot && slot[0])
}
}
(3)渲染流程
首次渲染时,keep-alive的render函数先执行,判断组件是否缓存,设置keepAlive的值。如果节点的keepAlive为true,首次渲染时isReactivated为undefined,执行组件的mount逻辑。因此对于首次渲染而言,除了在 <keep-alive>
中建立缓存,和普通组件渲染没什么区别。
非首次渲染时,会先执行keep-alive组件的render方法,如果命中缓存,从缓存中返回实例。
(4)LRU缓存策略
LRU 缓存策略∶ 从内存中找出最久未使用的数据并置换新的数据。
LRU(Least rencently used)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是 "如果数据最近被访问过,那么将来被访问的几率也更高" 。
12、$nextTick原理以及作用
官方定义:在下次DOM更新循环结束之后执行延迟回调,在修改数据之后立即使用这个方法,获取更新后的DOM。
也就是说:Vue更新DOM是异步操作,当数据发生变化之后,Vue将更新操作放在异步队列中,视图需要等同一事件循环中所有的数据变化完成之后,再进行更新。
因此,在修改数据之后,立即获取视图中更新的DOM节点,会发现获取到的是旧值。
(1)为什么要有nextTick
{{num}}
for(let i=0; i<100000; i++){
num = i
}
如果没有 nextTick
更新机制,那么 num
每次更新值都会触发视图更新(上面这段代码也就是会更新10万次视图),有了nextTick
机制,只需要更新一次,所以nextTick
本质是一种优化策略
// 修改数据
vm.message = '修改后的值'
// DOM 还没有更新
console.log(vm.$el.textContent) // 原始的值
// nextTick第一个参数为原始的值,第二个参数为执行上下文
Vue.nextTick(function () {
// DOM 更新了
console.log(vm.$el.textContent) // 修改后的值
})
// 组件内使用vm.$nextTick()实例方法只需要通this.$nextTick()
// 并且回调函数中的this将自动绑定到当前的Vue实例上
this.message = '修改后的值'
console.log(this.$el.textContent) // => '原始的值'
this.$nextTick(function () {
console.log(this.$el.textContent) // => '修改后的值'
})
// nextTick函数返回promise对象,可以使用async/await
this.message = '修改后的值'
console.log(this.$el.textContent) // => '原始的值'
await this.$nextTick()
console.log(this.$el.textContent) // => '修改后的值'
(2)nextTick实现原理
- 将回调函数放入callbacks等待执行
- 将执行函数放入任务队列
- 事件循环到了任务队列,依次取出更新函数并执行
export function nextTick(cb?: Function, ctx?: Object) {
let _resolve;
// cb 回调函数会经统一处理压入 callbacks 数组
callbacks.push(() => {
if (cb) {
// 给 cb 回调函数执行加上了 try-catch 错误处理
try {
cb.call(ctx);
} catch (e) {
handleError(e, ctx, 'nextTick');
}
} else if (_resolve) {
_resolve(ctx);
}
});
// 执行异步延迟函数 timerFunc
if (!pending) {
pending = true;
timerFunc();
}
// 当 nextTick 没有传入函数参数的时候,返回一个 Promise 化的调用
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve;
});
}
}
timerFunc
函数定义,这里是根据当前环境支持什么方法则确定调用哪个,分别有:Promise.then
、MutationObserver
、setImmediate
、setTimeout
通过上面任意一种方法,进行降级操作
export let isUsingMicroTask = false
if (typeof Promise !== 'undefined' && isNative(Promise)) {
//判断1:是否原生支持Promise
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
//判断2:是否原生支持MutationObserver
let counter = 1
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
//判断3:是否原生支持setImmediate
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
//判断4:上面都不行,直接用setTimeout
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
无论是微任务还是宏任务,都会放到flushCallbacks
使用
这里将callbacks
里面的函数复制一份,同时callbacks
置空
依次执行callbacks
里面的函数
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
13、Vue的数据对象添加新属性
添加的属性不是响应式数据,可以通过$set()
方法手动将数据转变为响应式数据
addObjB () (
this.$set(this.obj, 'b', 'obj.b')
console.log(this.obj)
}
14、vue中重新封装的数组方法
Object.defineProperty可以监听到数组方法,但是由于性能开销太大,vue中弃用了这种方法,重写数组方法以便执行方法之后更新视图。
- 执行数组方法,得到返回结果
- 如果有新增数组元素,监听数组元素
- 通知订阅者进行更新
在observer函数中,当数据类型为数组时,将数据的__ptoto__
指向arrayMethods,如果浏览器不支持原型,将arrayMethods中的方法直接定义在当前对象上。
// src/core/observer/array.js
// 获取数组的原型Array.prototype,上面有我们常用的数组方法
const arrayProto = Array.prototype
// 创建一个空对象arrayMethods,并将arrayMethods的原型指向Array.prototype
export const arrayMethods = Object.create(arrayProto)
// 列出需要重写的数组方法名
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
// 遍历上述数组方法名,依次将上述重写后的数组方法添加到arrayMethods对象上
methodsToPatch.forEach(function (method) {
// 保存一份当前的方法名对应的数组原始方法
const original = arrayProto[method]
// 将重写后的方法定义到arrayMethods对象上,function mutator() {}就是重写后的方法
def(arrayMethods, method, function mutator (...args) {
// 调用数组原始方法,并传入参数args,并将执行结果赋给result
const result = original.apply(this, args)
// 当数组调用重写后的方法时,this指向该数组,当该数组为响应式时,就可以获取到其__ob__属性
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// 将当前数组的变更通知给其订阅者
ob.dep.notify()
// 最后返回执行结果result
return result
})
})
15、单页应用与多页应用
- SPA:SinglePage Web Application,只有一个主页面,一开始只需要加载一次静态资源。所有的内容都包含在主页面中,对每一个功能模块组件化。单页面应用跳转就是切换组件,仅仅刷新局部资源。
- MPA:MultiPage Application,有多个独立页面的应用,每个页面都需要加载静态资源。多页应用跳转需要整个页面资源刷新。
单页应用优点:
- 用户体验好,内容的改变不需要重新加载整个页面
- 良好的前后端分离,前端负责交互逻辑,后端负责数据处理
- 减轻服务器负载压力
单页应用缺点:
- 首次渲染慢
- 不利于搜索引擎抓取
区别 | 单页面应用(SPA) | 多页面应用(MPA) |
---|---|---|
组成 | 一个主页面 + 许多功能模块组件 | 多个完整的页面 |
刷新方式 | 局部刷新 | 整页刷新 |
url模式 | 哈希模式 | 历史模式 |
SEO搜索引擎优化 | 难以实现,可使用SSR方式改善 | 容易实现 |
数据传递 | 容易 | 通过url、cookie、storage等传递 |
页面切换 | 速度较快,用户体验好 | 较慢 |
维护成本 | 较容易 | 较复杂 |
SEO优化
- SSR服务端渲染
- 静态化(?)
- 使用
Phantomjs
针对爬虫处理- 通过
Nginx
配置,判断访问来源是否为爬虫,如果是则搜索引擎的爬虫请求会转发到一个node server
,再通过PhantomJS
来解析完整的HTML
,返回给爬虫
- 通过
16、vue template 到 render过程
编译主要过程:template --> AST --> render
(1)调用parse方法将template转化为AST(抽象语法树)
利用正则表达式顺序解析模板,当解析到开始标签、闭合标签、文本时,会分别执行对应的回调函数,来达到构造AST树的目的
AST元素节点共有三种类型:type为1表示普通元素,2表达式,3纯文本
(2)对静态节点做优化
标记静态节点,后续渲染更新时可以跳过静态节点(DOM不会改变)
(3)generate方法将AST编译成render字符串,最后生成render函数
17、mixin、extends的覆盖逻辑
mixin和extends均用于合并、拓展组件。两者均使用mergeOptions方法实现合并
mixin:接受一个混入对象的数组,混入对象可以像正常实例一样包含各种实例选项,这些选项会被合并到最终的选项中
- 在多个组件之间重用一组组件选项
- mixin的hook优先级高于组件自己的hook
extends:为了便于扩展单文件组件,接收一个对象或构造函数,生成一个实例挂载到一个DOM元素上
- 作用是扩展组件生成一个构造器,通常会与
$mount
一起使用
// 创建组件构造器let
Component = Vue.extend({ template: '<div>test</div>'})
// 挂载到 #app 上
new Component().$mount('#app')
// 除了上面的方式,还可以用来扩展已有的组件
let SuperComponent = Vue.extend(Component)
new SuperComponent({
created() {
console.log(1)
}})
new SuperComponent().$mount('#app')
合并规则
18、vue的自定义指令
(1)如何实现一个自定义指令
全局注册:Vue.directive
// 注册一个全局自定义指令 `v-focus`
// 第一个参数是指令的名字,不需要加上v-前缀,第二个参数可以是对象数据,也可以是指令函数
Vue.directive('focus', {
// 当被绑定的元素插入到 DOM 中时……
inserted: function (el) {
// 聚焦元素
el.focus() // 页面加载完成之后自动让输入框获取到焦点的小功能
}
})
局部注册:options选项的directives属性
directives: {
focus: {
// 指令的定义
inserted: function (el) {
el.focus() // 页面加载完成之后自动让输入框获取到焦点的小功能
}
}
}
(2)自定义指令的钩子函数
钩子 | 说明 |
---|---|
bind | 只调用一次,指令第一次绑定元素时调用。在这里可以进行一次性的初始化设置 |
inserted | 被绑定元素插入父节点时调用 |
update | 组件的Vnode更新时调用 |
componentUpdated | 所有组件(包含子组件)更新完成后调用 |
unbind | 只调用一次,元素解绑时调用 |
钩子函数的参数
参数 | 说明 |
---|---|
el | 指令所绑定的元素,可以用来操作DOM |
binding | 一个对象,包含指令名name、指令绑定的值value、oldValue指令绑定的前一个值、expression字符串形式的指令表达式、arg传给指令的参数、modifiers包含修饰符的对象、vnode虚拟节点、oldVnode上一个虚拟节点 |
除了 el
之外,其它参数都应该是只读的,切勿进行修改。如果需要在钩子之间共享数据,建议通过元素的 dataset
来进行
(3)使用场景
- 表单防止重复提交
- 图片懒加载
- 一键copy功能
// 1.设置v-throttle自定义指令
Vue.directive('throttle', {
bind: (el, binding) => {
let throttleTime = binding.value; // 节流时间
if (!throttleTime) { // 用户若不设置节流时间,则默认2s
throttleTime = 2000;
}
let cbFun;
el.addEventListener('click', event => {
if (!cbFun) { // 第一次执行
cbFun = setTimeout(() => {
cbFun = null;
}, throttleTime);
} else {
event && event.stopImmediatePropagation();
}
}, true);
},
});
// 2.为button标签设置v-throttle自定义指令
<button @click="sayHello" v-throttle>提交</button>
19、子组件可以直接修改父组件的数据吗
不可以,Vue提倡单项数据流,父级的props属性的更新会流向子组件,反之不成立。防止多个子组件意外改变父组件的状态,导致页面数据流混乱,维护困难。
只能通过$emit
派发一个自定义事件,父组件监听到之后,由父组件修改数据
20、谈谈你对Vue的理解、Vue的优点、对比React
(1)template和jsx
- 对于runtime(程序在运行时的状态和行为)来说,只要保证组件存在render函数即可,浏览器运行函数生成页面
- webpack使用vue-loader编译文件。内部依赖的vue-template-compiler模块将template编译成render函数
- react将jsx代码解析成render函数
template和jsx的都是render的一种表现形式
- JSX:html结合js,比template更加灵活,在复杂的组件项目中,更具优势
- template:html、css、js逻辑分开,更简单直观,更好维护
(2)组件通信
- Vue中的组件通信:props/emit、eventBus、provide/inject、ref、listeners、children、Vuex/Pinia
- React中的组件通信:props、context、redux
react的组件通信方式过于复杂,vue的组件通信相对于来说简单
(3)状态管理
- Vue:Vuex、Pinia
- React:Redux
(4)生命周期
- Vue:开始创建、初始化数据、编译模板、挂载DOM、渲染更新、卸载
- React:挂载、更新、卸载
(5)diff算法
(6)组件化方式 vue组件和react组件
21、assets和static的区别
相同点:
- 存放静态资源
不同点:
- assets中存放的资源会被打包,打包后的资源会被放在static文件中,与index.html一同上传服务器
- static中的文件不会被打包,直接进入打包目录
static中的文件不会被打包,因此减少了打包时间,但是由于资源没有经历打包的压缩等操作,文件体积和放在assets中的相比略大
建议:将页面的css、js文件放在assets中,打包压缩,减少体积。将已经被处理好的第三方文件放在static中,不需要处理,直接上传
22、delete和Vue.delete删除数组的区别
- delete只是被删除的元素变成了empty/undefined,其他元素的键值不变,数组的索引位置不变,length不变
- Vue.delete直接删除了数组元素,改变的数组的键值,length改变
23、Vue的生命周期
Vue实例有一个完整的生命周期: 开始创建、初始化数据、编译模板、挂载DOM、渲染更新、卸载
- beforeCreate(创建前):数据观测和初始化事件还没有开始
- created(创建后):实例选项data、computed、watch、methods等都配置完成
- beforeMount(挂载前):首次调用render函数
- mounted(挂载后):将虚拟DOM挂载到真实DOM上
- beforeUpdated(更新前):响应式数据修改时调用,此时数据改变,但是视图没有更新
- updated(更新后):DOM已更新
- beforeDestory(销毁前):实例销毁之前调用。此时this仍然可用
- destoryed(销毁后):实例销毁后调用
24、父子组件钩子的执行顺序
- 加载渲染过程:父beforeCreate --> 父created --> 父beforeMount --> 子beforeCreate --> 子created --> 子beforeMount --> 子mounted --> 父mounted
- 更新过程:父beforeUpdate --> 子beforeUpdate --> 子updated --> 父updated
- 销毁过程:父beforeDestory --> 子beforeDestory --> 子destoryed --> 父destoryed
25、父子组件通信
(1)props、$emit
- 父组件通过props向子组件传值
- 如果props中数据的命名使用了驼峰形式,在模板中需要使用短横线
- 子组件通过触发$emit事件,将值传给父组件,父组件监听事件并进行数据处理
(2)eventBus事件总线
eventBus适用于父子组件、非父子组件之间的通信
- 创建事件中心管理组件之间的通信
- 假设有两个兄弟组件firstCom和secondCom
// 事件中心:event-bus.js
import Vue from 'vue'
export const EventBus = new Vue()
// 父组件
<template>
<div>
<first-com></first-com>
<second-com></second-com>
</div>
</template>
<script>
import firstCom from './firstCom.vue'
import secondCom from './secondCom.vue'
export default {
components: { firstCom, secondCom }
}
</script>
<template>
<div>
<button @click="add">加法</button>
</div>
</template>
// 使用$emit发送事件
<script>
import {EventBus} from './event-bus.js' // 引入事件中心
export default {
data(){
return{
num:0
}
},
methods:{
add(){
EventBus.$emit('addition', {
num:this.num++
})
}
}
}
</script>
// 使用$on接受事件
<template>
<div>求和: {{count}}</div>
</template>
<script>
import { EventBus } from './event-bus.js'
export default {
data() {
return {
count: 0
}
},
mounted() {
EventBus.$on('addition', param => {
this.count = this.count + param.num;
})
}
}
</script>
相当于将num存储在事件总线中,其他组件可以直接访问。事件总线相当于桥梁,不同组件可以通过它进行通信。
如果项目较大,使用这种方式通信,维护困难。
(3)依赖注入(provide、inject)
适用于父子/祖孙之间进行数据传递,当嵌套的层级过多时,可以使用这种方法。
依赖注入提供的属性是非响应式的
provide和inject是Vue提供的两个钩子,provide钩子用来发送数据或方法,inject用来接收数据或方法。
// 父组件中(和data同级)
provide() {
return {
num: this.num
};
}
//在子组件中
inject: ['num']
// 这种写法可以访问父组件的所有属性
provide() {
return {
app: this
};
}
data() {
return {
num: 1
};
}
inject: ['app']
console.log(this.app.num)
(4)ref、$refs
父子组件通信
ref:将这个属性用在子组件上,它的引用就指向了子组件的实例,可以通过实例访问子组件的属性和方法。
<template>
<child ref="child"></component-a>
</template>
<script>
import child from './child.vue'
export default {
components: { child },
mounted () {
console.log(this.$refs.child.name); // JavaScript
this.$refs.child.sayHello(); // hello
}
}
</script>
(5)$parent、$children
$parent
:可以访问父组件的实例(上一级父组件的属性和方法),可以通过$root访问根组件的实例
$children
:访问所有子组件的实例,但是并不保证顺序,访问的数据也不是响应式的
根组件#app``的$parent
是new Vue
的实例,再往上是undefined
最底层组件的$children
是空数组
// 子组件中
<template>
<div>
<span>{{message}}</span>
<p>获取父组件的值为: {{parentVal}}</p>
</div>
</template>
<script>
export default {
data() {
return {
message: 'Vue'
}
},
computed:{
parentVal(){
return this.$parent.msg;
}
}
}
</script>
// 父组件中
// 父组件中
<template>
<div class="hello_world">
<div>{{msg}}</div>
<child></child>
<button @click="change">点击改变子组件值</button>
</div>
</template>
<script>
import child from './child.vue'
export default {
components: { child },
data() {
return {
msg: 'Welcome'
}
},
methods: {
change() {
// 获取到子组件
this.$children[0].message = 'JavaScript'
}
}
}
</script>
(6)$attrs、$listeners
实现组件之间的隔代通信
$attrs
:继承所有的父组件属性(除了prop传递的属性、class 和 style )$listeners
:包含了作用在这个组件上的所有监听器
简单地说:通过子组件作为中间层,孙子组件可以拿到父组件的数据,触发父组件的监听器
<template>
<div class="father">
<div>爸爸</div>
<son
:text="text" //父组件向子组件传入text、msg和content
:msg="msg"
:content="content"
@handle1="handle1" //父组件中定义事件监听器,监听handle1和handle2事件
@handle2="handle2"
></son>
</div>
</template>
<template>
<div class="son">
<div @click="handle">子组件</div>
<grandson v-bind="$attrs" v-on="$listeners" @handle1="handle1"></grandson>
//子组件中通过 v-bind="$attrs" v-on="$listeners" 将父组件的数据和监听器传递给孙子组件
//子组件中定义事件监听器,监听handle1事件
</div>
</template>
<script>
import grandson from "./EventCustomChildChild.vue";
export default {
//为true,子组件的根元素为<div class="son" msg="12" content="123"> 没有被props接收的attribute会显示
//为false,子组件的根元素为<div class="son"></div>
inheritAttrs: false,
components: {
grandson,
},
props: ["text"],
methods: {
handle() {
// {msg: '12', content: '123'},获取父组件传过来,没有被props接收
console.log(this.$attrs);
// {handle1: ƒ, handle2: ƒ},获取父作用域中的(不含 `.native` 修饰器的) `v-on` 事件监听器
console.log(this.$listeners);
},
handle1() {
console.log("儿子组件中的handle1");
},
},
};
</script>
<template>
<div class="grandson">
<div @click="handle">孙子组件</div>
</div>
</template>
<script>
export default {
data() {
return {};
},
props: ["msg"],
methods: {
handle() {
// {content: '123'},子组件通过v-bind="$attrs"将非props传递到孙子组件
console.log(this.$attrs);
// 如果没有使用props,结果为{msg: '12', content: '123'},但如果调用孙子组件时,发生下述情况,msg值会被覆盖,结果变为{msg: '11111', content: '123'}
// <grandson v-bind="$attrs" v-on="$listeners" @handle1="handle1" :msg="11111"></grandson>
// {handle1: ƒ, handle2: ƒ},获取上级组件的所有(不含 `.native` 修饰器的) `v-on` 事件监听器
console.log(this.$listeners);
// 子组件和父组件都定义了handle1事件监听器,但他们不会覆盖,展开handle1,会发现里面包含两个函数
this.$emit("handle1");//儿子组件中的handle1、父组件的handle1函数,先触发子组件,再触发父组件
this.$emit("handle2");//父组件的handle2函数
},
},
};
</script>
二、路由
1、vue-router的懒加载实现
(1)import动态加载
const router = new VueRouter({
routes: [
{ path: '/list', component: () => import('@/components/list.vue') }
]
})
(2)require
const router = new Router({
routes: [
{
path: '/list',
component: resolve => require(['@/components/list'], resolve)
}
]
})
(3)webpack的require.ensure
// r就是resolve
const List = r => require.ensure([], () => r(require('@/components/list')), 'list');
// 路由也是正常的写法 这种是官方推荐的写的 按模块划分懒加载
const router = new Router({
routes: [
{
path: '/list',
component: List,
name: 'list'
}
]
}))
2、hash和history的区别
vue默认是哈希路由模式
(1)hash模式
- hash出现在url中的
#
后面,但不会随着请求发送到服务端,对服务端没有任何影响,改变hash值不会重新加载页面。 - hash模式的主要原理是onhashchange事件,页面hash发生变化时,不需要向服务器请求,即可按照规则加载相应的代码。
- hash变化对应的url会被浏览器记录下来,这样浏览器就能实现页面的前进后退。
(2)history模式
- url中不包含
#
,看起来比hash模式好看,但是一旦发生变化,会请求服务端,如果后台不支持当前路由,会报错404。 - API:可以分为两大部分,切换历史状态和修改历史状态
- 切换历史状态:forward、back、go(浏览器的前进、后退、跳转操作)
- 修改历史状态:修改浏览器历史记录栈,修改后并不会立即加载url,不会重新刷新页面,比如pushState、replaceState
(3)模式对比
- url值与历史栈
- history可以设置任意路径(与当前url同源的值),如果是一模一样的url,也会刷新并将记录添加到栈中。
- hash只能修改#后面的值,如果值不变,记录不会被添加到历史栈中。
- 路由参数
- history:任意类型数据,可以将数据存在一个特定的对象中(history.state)
- hash:基于url,只能传递字符串,且有体积限制
- 404错误
- history任一路由在服务端找不到会报错404
- hash只会判断#之前的链接能否在服务器上找到
- 兼容性
- history的兼容性略差,低版本浏览器不支持history API
3、获取页面的hash变化
(1)window.location.hash
- 读取hash值
- 修改hash,可以添加历史记录(前提是修改后的hash不能和当前页面的hash一致,否则不会添加到历史栈),但不会重载网页
(2)监听$route的变化
// 监听,当路由发生变化的时候执行
watch: {
$route: {
handler: function(val, oldVal){
console.log(val);
},
// 深度观察监听
deep: true
}
}
4、$route
和$router
$route
:路由信息对象,包括path、params、hash、query、fullpah、matched、name等路由参数信息$router
:路由实例对象,包括了路由的跳转方法,钩子函数
5、动态路由
this.$router.push({
path:`/home/${id}`,
})
// 路由要配置/:id
{
path:"/home/:id",
name:"Home",
component:Home
}
// 在组件中获取参数
this.$route.params.id
this.$router.push({
name:'Home',
params:{
id:id
}
})
// params用name传递参数,不使用/:id
{
path:'/home',
name:Home,
component:Home
}
this.$route.params.id
this.$router.push({
path:'/home',
query:{
id:id,
name:jack
}
})
{
path:'/home',
name:Home,
component:Home
}
this.$route.query.id
name + params:
- 刷新会丢失数据
- 参数不会显示在浏览器url中
path + query:
- 刷新不会丢失数据
- 参数会显示在浏览器的url中
6、路由钩子
- 全局前置:beforeEach、beforeResolve、afterEach
- 路由独享守卫:beforeEnter
- 组件守卫:beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave
三、Vuex
1、什么是Vuex
Vuex是一个专为vue.js开发的状态管理模式, 集中式存储管理应用的所有组件状态。
- vuex的状态存储是响应式的,vue组件从store中读取状态,若store的状态发生变化,相应的组件也会更新
- vuex的原理,vuex生成了一个store实例,并把这个实例挂在了所有的组件上,所有的组件引用的是同一个store实例。
2、vuex有哪几种属性
State、Mutation、Action、Getter、Module
- State:存储应用中的状态
- Mutation:修改State中的状态
- Action:用于提交Mutation
- Getter:从State中派生出一些状态
- Module:将Store分割成模块
State
Vuex使用单一状态树,即用一个对象就包含了全部的应用层级状态。每个应用仅包含一个store实例,我们在使用时能够简单直接的获取数据。
这个状态树是响应式的,当状态发生变化时,相关的组件将自动更新。
Mutation
用来更改state中的状态,mutation是唯一用来更改Vuex中状态的方法。
Action
action类似于mutation,不同在于
- action支持异步操作,但最终还是通过调用mutation来修改state,不会直接变更状态
- mutation必须是同步的,为了便于状态的追踪
Getter
从state中派生出一些状态,类似于Vue中的computed
Module
由于使用单一状态树,应用的所有状态会集中到一个比较大的对象中。当应用变得非常复杂时,state会变得非常臃肿。
为了解决以上问题,Vuex允许我们将store分割成模块,每个模块拥有自己的state、mutation、action、Getter、甚至是嵌套子模块
3、页面刷新后,Vuex数据丢失
原因:store中的数据保存在运行内存中的,当页面刷新时,页面会重新加载vue实例,store里面的数据就被被重新赋值初始化。
解决:vuex-along、web storage(页面刷新之前将数据放在storage中,刷新之后从storage中获取数据)
vuex-along的实质也是将vuex中的数据放到storage里面,只是存取的过程由组件帮我们完成。
4、vuex中的辅助函数
通过辅助函数mapState
、mapGetters
、mapActions
、mapMutations
,把vuex.store
中的属性映射到vue
实例身上,这样在vue
实例中就能访问vuex.store
中的属性了,对于操作vuex.store
就很方便了。
// 把state
computed:{
...Vuex.mapState({
key:state=>state.属性
})
}
5、vuex和redux区别
相同点:
- 使用state共享数据
- 流程一致:定义全局state,修改state,视图state变化
- 原理相似:通过全局注入store
不同点:
- 实现原理
- redux使用的是不可变数据,每次都是使用新的state替换旧的state;vuex可以直接修改数据
- redux在检测数据变化时,通过diff方式比较差异,vuex和vue原理一样,通过getter/setter
- 表现层
- vuex定义了state、getter、mutation、action,redux定义了state、reducer、action
- vuex触发使用commit同步,dispatch异步,redux中同步操作和异步操作都使用dispatch
6. Vuex的严格模式是什么,有什么作用,如何开启?
在严格模式下,无论何时发生了状态变更且不是由mutation函数引起的,将会抛出错误。这能保证所有的状态变更都能被调试工具跟踪到。
在Vuex.Store 构造器选项中开启,如下
const store = new Vuex.Store({
strict:true,
})
四、Vue3
1、Vue3的设计目标是什么
(1)设计目标
在vue3之前我们或许会面临以下问题:
- 随着功能增长,复杂组件代码越来越难以维护
- 缺少一种比较干净的在多个组件之间提取和复用逻辑的机制
- 类型推断不太友好
- bundle时间太久了
vue3做了哪些
- 更小
- 移除一些不常用的API
- 优化Tree-Shaking
- 更快
- diff算法优化
- 静态提升
- 事件监听缓存
- SSR优化
- TypeScript支持
- API设计一致性
- 提高自身的可维护性
- 开放更多底层功能
(2)优化方案
可以分成三个方面:源码、性能、语法API
1)源码
源码可以从两个层面展开:源码管理、TypeScript
源码管理
vue3整个源码是通过monorepo方式维护的,根据功能将不同模块拆分到packages目录下面的不同子目录中,使模块的拆分更细化,职责更明确,模块之间的依赖关系也更加明确,开发人员更容易阅读、理解和更改模块源码,提高代码的可维护性。
另外一些 package
(比如 reactivity
响应式库)是可以独立于 Vue
使用的,这样用户如果只想使用 Vue3
的响应式能力,可以单独依赖这个响应式库而不用去依赖整个 Vue
。
TypeScript
Vue3
是基于typeScript
编写的,提供了更好的类型检查,能支持复杂的类型推导
2)性能
性能:体积优化、编译优化、数据劫持优化
3)语法API
优化逻辑组织、优化逻辑复用
逻辑组织
Vue3:相同功能代码编写到一块 vue2:各个功能代码混杂在一起
逻辑复用
在vue2中,通过mixin实现功能混合,但有两个明显的问题:命名冲突和数据来源不清晰。
vue3可以将这些复用代码抽离成一个函数
2、Vue3和Vue2有什么区别
-
响应式系统:Vue3引入了Composition API,这是一个新的响应式系统。
- Composition提供了更灵活和更强大的组件状态和逻辑管理方式,使代码组织和重用更加方便
- Composition使用函数而不是对象,可以Tree Shaking的优化效果
-
更小的包体积
- Tree Shaking和更高效的运行时代码生成
-
性能优化
- 更快、更高效的渲染机制
-
作用域插槽替代为
<slot>
- 2.x 的机制导致作用域插槽变了,父组件会重新渲染
- 3.0 把作用域插槽改成了函数的方式,这样只会影响子组件的重新渲染,提升了渲染的性能
-
引入Teleport组件:可以在DOM树的不同位置渲染内容,用于创建模态框、工具提示和其他覆盖层效果
-
片段(Fragments):允许将多个元素进行分析,而无需添加额外的包装元素
-
更好的TypeScript支持
- vue2.x 中的组件是通过声明的方式传入一系列 option,和 TypeScript 的结合需要通过一些装饰器的方式来做,虽然能实现功能,但是比较麻烦
- 3.0 修改了组件的声明方式,改成了类式的写法,这样使得和 TypeScript 的结合变得很容易
-
简化API
<template>
<button @click="increment">
Count: {{ count }}
</button>
</template>
<script>
// Composition API 将组件属性暴露为函数,因此第一步是导入所需的函数
import { ref, computed, onMounted } from 'vue'
export default {
setup() {
// 使用 ref 函数声明了称为 count 的响应属性,对应于Vue2中的data函数
const count = ref(0)
// Vue2中需要在methods option中声明的函数,现在直接声明
function increment() {
count.value++
}
// 对应于Vue2中的mounted声明周期
onMounted(() => console.log('component mounted!'))
return {
count,
increment
}
}
}
</script>
3、Vue3的性能提升主要是通过哪几个方面体现
(1)编译阶段
优化点:diff算法优化、静态提升、事件监听缓存、SSR优化
diff算法优化
vue3
在diff
算法中相比vue2
增加了静态标记。
关于这个静态标记,其作用是为了会发生变化的地方添加一个flag
标记,下次发生变化的时候直接找该地方进行比较,静态节点直接不进行比较。
静态提升
Vue3
中对不参与更新的元素,会做静态提升,只会被创建一次,在渲染时直接复用,不需要再重复进行节点的创建。
事件监听缓存
默认情况下绑定事件行为会被视为动态绑定,所以每次都会去追踪它的变化
SSR优化
当静态内容大到一定量级时候,会用createStaticVNode
方法在客户端去生成一个static node,这些静态node
,会被直接innerHtml
,就不需要创建对象,然后根据对象渲染。
(2)源码体积
相比于Vue2,Vue3项目的整体体积变小了,Vue3源码中移除了一些不常用的API,此外就是Tree-Shaking
任何一个函数,如ref
、reavtived
、computed
等,仅仅在用到的时候才打包,没用到的模块都被摇掉,打包的整体体积变小。
(3)响应式系统
vue2
中采用 defineProperty
来劫持整个对象,然后进行深度遍历所有属性,给每个属性添加getter
和setter
,实现响应式。
vue3
采用proxy
重写了响应式系统,因为proxy
可以对整个对象进行监听,所以不需要深度遍历
- 可以监听动态属性的添加
- 可以监听到数组的索引和数组
length
属性 - 可以监听删除属性
4、Vue3为什么要用Proxy
(1)Object.defineProperty
定义:Object.defineProperty()
方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象
- 检测不到对象属性的添加和删除
- 一些数组
API
方法无法监听到 - 需要对每个属性进行遍历监听,如果嵌套对象,需要深层监听,造成性能问题
(2)Proxy
Proxy
直接可以劫持整个对象,并返回一个新对象,我们可以只操作新的对象达到响应式目的
Proxy
可以直接监听数组的变化(push
、shift
、splice
)
Proxy
不兼容IE,也没有 polyfill
, defineProperty
能支持到IE9
5、Composition API 和 Options API
(1)Options API
选项API:methods、computed、watch、data
当组件变得复杂,导致对应属性的列表也会增长,这可能会导致组件难以阅读和理解
(2)Composition API
Composition API:组件根据功能组织到一起,一个功能的所有API会放在一起(高内聚、低耦合)
在进行逻辑复用时,可以通过composition API将逻辑抽成一个函数,在需要的地方直接引用,得到逻辑属性/方法,而不是使用mixin。
- 在逻辑组织和逻辑复用方面,
Composition API
是优于Options API
Composition API
对tree-shaking
友好,代码也更容易压缩Composition API
中见不到this
的使用,减少了this
指向不明的情况- 如果是小型组件,可以继续使用
Options API
,也是十分友好的
五、虚拟DOM
1. 对虚拟DOM的理解?
从本质上来说,Virtual Dom是一个JavaScript对象,通过对象的方式来表示DOM结构。
将多次DOM修改的结果一次性的更新到页面上,从而有效的减少页面渲染的次数,减少修改DOM的重绘重排次数,提高渲染性能。
在每次数据发生变化前,虚拟DOM都会缓存一份,变化之时,现在的虚拟DOM会与缓存的虚拟DOM进行比较。在vue内部封装了diff算法,通过这个算法来进行比较,渲染时修改改变的变化,原先没有发生改变的通过原先的数据进行渲染。
2. 虚拟DOM的解析过程
- 首先对将要插入到文档中的 DOM 树结构进行分析
- 使用 js 对象将其表示出来,比如一个元素对象,包含 TagName、props 和 Children 这些属性
- 将这棵新的对象树和旧的对象树进行比较,记录下两棵树的的差异
- 最后将记录的有差异的地方应用到真正的 DOM 树中去
3. 为什么要用虚拟DOM
(1)保证性能下限,在不进行手动优化的情况下,提供过得去的性能
- 真实DOM∶ 生成HTML字符串+重建所有的DOM元素
- 虚拟DOM∶ 生成vNode + DOMDiff + 必要的dom更新
Virtual DOM的更新DOM的准备工作耗费更多的时间,也就是JS层面,相比于更多的DOM操作它的消费是极其便宜的。
尤雨溪在社区论坛中说道∶ 框架给你的保证是,你不需要手动优化的情况下,依然可以给你提供过得去的性能。
(2)跨平台
Virtual DOM本质上是JavaScript的对象,它可以很方便的跨平台操作,比如服务端渲染、uniapp等。
4. 虚拟DOM真的比真实DOM性能好吗
- 首次渲染大量DOM时,由于多了一层虚拟DOM的计算,会比innerHTML插入慢。
- 正如它能保证性能下限,在真实DOM操作的时候进行针对性的优化时,还是更快的。
5. DIFF算法的原理
在新老虚拟DOM对比时:
- 首先,对比节点本身,判断是否为同一节点,如果不为相同节点,则删除该节点重新创建节点进行替换
- 如果为相同节点,进行patchVnode,判断如何对该节点的子节点进行处理,先判断一方有子节点一方没有子节点的情况(如果新的children没有子节点,将旧的子节点移除)
- 比较如果都有子节点,则进行updateChildren,判断如何对这些新老节点的子节点进行操作(diff核心)。
- 匹配时,找到相同的子节点,递归比较子节点
在diff中,只对同层的子节点进行比较,放弃跨级的节点比较,使得时间复杂从O(n3)降低至O(n),也就是说,只有当新旧children都为多个子节点时才需要用核心的Diff算法进行同层级比较。
6. Vue中key的作用
vue 中 key 值的作用可以分为两种情况来考虑:
- v-if 中使用 key
- 由于 Vue 会尽可能高效地渲染元素,通常会复用已有元素而不是从头开始渲染。
- 因此当使用 v-if 来实现元素切换的时候,如果切换前后含有相同类型的元素,那么这个元素就会被复用。
- 如果是相同的 input 元素,那么切换前后用户的输入不会被清除掉,这样是不符合需求的。
- 因此可以通过使用 key 来唯一的标识一个元素,这个情况下,使用 key 的元素不会被复用。这个时候 key 的作用是用来标识一个独立的元素。
- v-for 中使用 key
- 用 v-for 更新已渲染过的元素列表时,它默认使用“就地复用”的策略。
- 如果数据项的顺序发生了改变,Vue 不会移动 DOM 元素来匹配数据项的顺序,而是简单复用此处的每个元素。
- 因此通过为每个列表项提供一个 key 值,来以便 Vue 跟踪元素的身份,从而高效的实现复用。这个时候 key 的作用是为了高效的更新渲染虚拟 DOM。
key 是 Vue 中 vnode 的唯一标记,通过这个 key,diff 操作可以更准确、更快速
- 更准确:因为带 key 就不是就地复用了,在 sameNode 函数a.key === b.key对比中可以避免就地复用的情况。所以会更加准确。
- 更快速:利用 key 的唯一性生成 map 对象来获取对应节点,比遍历方式更快
7. 为什么不建议用index作为key?
使用index 作为 key和没写基本上没区别,因为不管数组的顺序怎么颠倒,index 都是 0, 1, 2...这样排列,导致 Vue 会复用错误的旧子节点,做很多额外的工作。