Vue解析

Vue响应式原理

  vue响应式也叫作数据双向绑定,大致原理阐述:
首先我们需要通过Object.defineProperty()方法把数据(data)设置为getter和setter的访问形式,这样我们就可以在数据被修改时在setter方法设置监视修改页面信息,也就是说每当数据被修改,就会触发对应的set方法,然后我们可以在set方法中去调用操作dom的方法。

  vue实现数据响应式,是通过数据劫持侦测数据变化,发布订阅模式进行依赖收集与视图更新,换句话说是Observe,Watcher以及Compile三者相互配

  1. Observe实现数据劫持,递归给对象属性,绑定setter和getter函数,属性改变时,通知订阅者
  2. Compile解析模板,把模板中变量换成数据,绑定更新函数,添加订阅者,收到通知就执行更新函数
  3. Watcher作为Observe和Compile中间的桥梁,订阅Observe属性变化的消息,触发Compile更新函数

发布订阅模式和观察者模式的区别

观察者模式

  观察者模式定义了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知,并自动更新。观察者模式属于行为型模式,行为型模式关注的是对象之间的通讯,观察者模式就是观察者和被观察者之间的通讯。

核心:

  1. 观察者(Watcher): 每个观察者必须有一个 update() 方法,当事件发生时,执行观察者的update()。观察者可以理解为发布/订阅模式的订阅者。
  2. 目标(Dependency依赖):可以理解为发布/订阅模式的发布者

订阅发布模式

  现在的发布订阅模式中,称为发布者的消息发送者不会将消息直接发送给订阅者,这意味着发布者和订阅者不知道彼此的存在。在发布者和订阅者之间存在第三个组件,称为调度中心或事件通道,它维持着发布者和订阅者之间的联系,过滤所有发布者传入的消息并相应地分发它们给订阅者。

核心:

  1. 订阅者
  2. 发布者
  3. 信号中心(事件中心)

VDom

  VDom顾名思义就是虚拟的dom对象,它本身就是⼀个 JavaScript 对象,利用JS对象来表示真实DOM的树结构,创建一个虚拟的DOm对象

好处

  1. 将真实元素节点抽象成 VNode,有效减少直接操作 dom 次数,从而提高程序性能
  2. 方便实现跨平台
  3. 不再依赖 HTML 解析器进行模版解析,可以进行更多的 AOT 工作提高运行时效率:通过模版 AOT 编译,Vue 的运行时体积可以进一步压缩,运行时效率可以进一步提升;
  4. 可以渲染到 DOM 以外的平台,实现 SSR、同构渲染这些高级特性,Weex等框架应用的就是这一特性。
  5. 无需手动操作DOM

缺点

  1. 无法进行极致优化,在一些性能要求极高的应用中虚拟DOM无法进行针对性的极致优化
  2. 首次渲染大量DOM时,由于多了一层虚拟DOM计算,会比innerHTML插入慢

nextTick()

  Vue.nextTick用于延迟执行一段代码,它接受2个参数(回调函数和执行回调函数的上下文环境),如果没有提供回调函数,那么将返回promise对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
// 源码
/**
* Defer a task to execute it asynchronously.
*/
export const nextTick = (function () {
const callbacks = []
let pending = false
let timerFunc

function nextTickHandler () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}

// the nextTick behavior leverages the microtask queue, which can be accessed
// via either native Promise.then or MutationObserver.
// MutationObserver has wider support, however it is seriously bugged in
// UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
// completely stops working after triggering a few times... so, if native
// Promise is available, we will use it:
/* istanbul ignore if */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
var p = Promise.resolve()
var logError = err => { console.error(err) }
timerFunc = () => {
p.then(nextTickHandler).catch(logError)
// in problematic UIWebViews, Promise.then doesn't completely break, but
// it can get stuck in a weird state where callbacks are pushed into the
// microtask queue but the queue isn't being flushed, until the browser
// needs to do some other work, e.g. handle a timer. Therefore we can
// "force" the microtask queue to be flushed by adding an empty timer.
if (isIOS) setTimeout(noop)
}
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
// use MutationObserver where native Promise is not available,
// e.g. PhantomJS, iOS7, Android 4.4
var counter = 1
var observer = new MutationObserver(nextTickHandler)
var textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
} else {
// fallback to setTimeout
/* istanbul ignore next */
timerFunc = () => {
setTimeout(nextTickHandler, 0)
}
}

return function queueNextTick (cb?: Function, ctx?: Object) {
let _resolve
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
pending = true
timerFunc()
}
if (!cb && typeof Promise !== 'undefined') {
return new Promise((resolve, reject) => {
_resolve = resolve
})
}
}
})()


生命周期

  1. beforeCreate:在实例被完成创建出来,el和data都没有初始化,不能访问data、method,一般在这个阶段不进行操作。
  2. created:vue实例中的data、method已被初始化,属性也被绑定,但是此时还是虚拟dom,真是dom还没生成,$el 还不可用。
  3. beforeMount:此时模板已经编译完成,但还没有被渲染至页面中(即为虚拟dom加载为真实dom),此时el存在则会显示el。
  4. Mounted:此时模板已经被渲染成真实DOM,用户已经可以看到渲染完成的页面,页面的数据也是通过双向绑定显示data中的数据。
  5. beforeUpdate:重新渲染之前触发,然后vue的虚拟dom机制会重新构建虚拟dom与上一次的虚拟dom树利用diff算法进行对比之后重新渲染。
  6. updated:数据已经更改完成,dom也重新render完成。
  7. beforeDestroy:销毁前执行($destroy方法被调用的时候就会执行),一般在这里善后:清除计时器、清除非指令绑定的事件等等…’)
  8. destroyed:销毁后 (Dom元素存在,只是不再受vue控制),卸载watcher,事件监听,子组件。

vue-router

  1. VueRouter :路由器类,根据路由请求在路由视图中动态渲染选中的组件
  2. router-link :路由链接组件,声明用以提交路由请求的用户接口
  3. router-view:路由视图组件,负责动态渲染路由选中的组件

路由传参

动态路由匹配
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 路由
const router = new VueRouter({
routes: [
// 动态路径参数 以冒号开头
{ path: '/user/:id', component: User }
]
})

// 传递参数
// 1. 声明式
<router-link :to="/user/1"> 跳转到匹配路由 </router-link>

// 2. 编程式
this.$router.push({
path: '/child/${id}',
})

Url传参方式
  1. 通过params显式传参
1
2
3
4
5
6
7
8
9
10
11
12
13
// 路由
const routes = [{
path: '/child/:id',
name: 'Child',
component: () => import('@/components/Child')
}]
// 传递参数
this.$router.push({
path: '/child/foo',
})
// 接受参数
this.$route.params.id === foo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 还可以配置多个参数
const routes = [{
path: '/user/:name/hobby/:id',
component: UserComponent
}]

// 传递参数:这里 username 对应[:name], userHobby 对应[:id]
// 其它字段必须完全一致,否则无法匹配
this.$router.push({
path: '/user/userName/hobby/userHobby'
})
// 接收参数
this.$route.params.name === userName
this.$route.params.id === userHobby
  1. 通过params隐式传参
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 路由
const routes = [{
path: '/child',
name: 'Child',
component: () => import('@/components/Child')
}]
// 传递参数
this.$router.push({
name: 'Child',
params: {
id: 1
}
})
// 接收参数
this.$route.params.id === 1
  1. 通过query传递参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 路由
const routes = [{
path: '/child',
name: 'Child',
component: () => import('@/components/Child')
}]
// 传递参数(通过 name 或者 path 来匹配路由)
this.$router.push({
path: '/child',
query: {
id: 1
}
})
// 接收参数
this.$route.query.id === 1

完整的导航解析流程

  1. 导航被触发
  2. 在失活的组件里调用beforeRouteLeave 守卫
  3. 调用全局的beforeEnch守卫
  4. 在重用的组件中调用beforeRouteUpdate守卫
  5. 在路由组件配置中调用BeforEnter
  6. 解析异步路由组件
  7. 在激活的组件中调用beforeRouteEnter
  8. 调用全局的bbeforeResolve
  9. 导航被确认
  10. 调用全局的afterEach钩子
  11. 触发DOM更新
  12. 调用beforeRouteEnter守卫中传给next的回调函数,创建好的组件实例会作为回调函数的参数传入

vuex

&emsp;&emsp;相当于一个公共仓库,保存着所有组件都能共用的数据

  1. state:保存所有数据,以对象的方式导出
  2. mutations:保存所有方法,用来改变state的数据
  3. actions:暴露给用户使用,借此触发mutations中的方法,保存数据(可执行异步操作)
  4. gettings:获取数据

Vue3新特性

  1. compositionAPI(组合式API),代码组织更方便了, 逻辑复用更方便了 非常利于维护!
  2. 检测类型 ( 类型推导 Typescript 支持 )
  3. ES6 的 proxy 替代了 ES5 的 Object.defineProperty
  4. template 模板可以有多个根元素

compositionAPI

  1. setup 中不能使用 this, this 指向 undefined
  2. reactive函数:将复杂类型数据, 转换成响应式数据
  3. ref 函数:对传入的数据(一般简单数据类型),包裹一层对象, 转换成响应式。
  4. toRef 函数:使用 toRef函数 , 将 reactive 函数的响应式对象中的指定属性转换为响应式数据
  5. toRefs 函数:对一个 响应式对象 的所有内部属性, 都做响应式处理, 保证展开或者解构出的数据也是响应式的( 一般配合 reactive 使用)
  6. computed 函数:computed 函数调用时, 要接收一个处理函数, 处理函数中, 需要返回计算属性的值

keep-alive

&emsp;&emsp;keep-alive是用来缓存组件的,提供了两个activated与deactivated。

  1. include - 逗号分隔字符串或正则表达式或一个数组来表示。只有名称匹配的组件会被缓存。
  2. exclude - 逗号分隔字符串或正则表达式或一个数组来表示。任何名称匹配的组件都不会被缓存。
  3. max - 数字。最多可以缓存多少组件实例。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
// 源码
export default {
name: 'keep-alive',
abstract: true,

props: {
include: [String, RegExp, Array],
exclude: [String, RegExp, Array],
max: [String, Number]
},

created () {
this.cache = Object.create(null)
this.keys = []
},

destroyed () {
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys)
}
},

mounted () {
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
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
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
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
}

vnode.data.keepAlive = true
}
return vnode || (slot && slot[0])
}
}

为什么访问data属性不需要带data

&emsp;&emsp;vue中访问属性代理 this.data.xxx 转换 this.xxx 的实现

template预编译

&emsp;&emsp;当vue进行打包时,会直接把组件中的模板转换为render函数,这叫做模板预编译

好处: 运行时就不再需要编译模板了,提高了运行效率,打包结果中不再需要vue的编译代码,减少了打包体积

diff算法

&emsp;&emsp;diff算法就是进行虚拟节点对比,并返回一个patch对象,用来存储两个节点不同的地方,最后用patch记录的消息去局部更新Dom。

特点: 同级比较,循环从两边向中间比较

v-if 和v-show

v-show

v-show严格意义来说其实是条件隐藏,直接在页面初始化的时候将DOM(对象模型)元素也初始化,因为它就是将它所在的元素添加一个display属性为none,如果条件符合就显示。

v-if

v-if严格意义来说就是条件判断,符合就加载DOM(对象模型)元素,不符合就不显示。

区别

  1. v-if有更高的切换性能,比如说需要判断多个条件时,就使用if。
  2. 如果需要频繁的切换,选择v-show,因为show是动态的改变样式,不需要增删DOM(对象模型)元素,大项目推荐使用show,能极大减少浏览器后期的操作性能。

v-for和v-if的优先级

  1. 在vue2中,v-for的优先级是高于v-if,如果把它们放在一起,每次循环都会遍历整个列表,造成资源浪费。
  2. 在vue3中v-if的优先级高于v-for,会报错