当一个Vue实例创建时,Vue会遍历data中的属性,用Object.defineProperty(Vue使用proxy)转换为getter/setter,并且在内部追踪相关依赖,在属性被访问和修改时通知变化。每个组件实例都有相应的watcher程序实例,他会在组件渲染的过程中把属性记录为依赖,之后依赖项的setter被调用时,会通知watcher重新计算,从而致使它关联的组件得以更新。
MVVM 双向绑定,达到数据变化 -> 试图更新;试图变化 -> 数据model变更
Vue是采用数据劫持结合发布者-订阅者模式的方式, 通过Object.defineProperty()来劫持各个属性的setter和getter,在数据变化时发布消息给订阅者,出发相应的监听回调。
由两个主要的部分组成
过程如下(Vue为例)
new Vue()
首先执行初始化,对data
执行相应化处理,这个过程发生在Observer
中data
中获取数据并初始化视图,这个过程发生在Compile
中updater
和watcher
,将来对应的数据变化时watcher
会调用更新函数data
的某个key
在一个视图中可能出现多次,所以每个key
都需要一个管家Dep
来管理多个watcher
data
中的数据一旦发生变化,会先找到对应的Dep
,通知所有watcher
执行更新函数MVVM
MVVM分为model、view、viewModel
Model和View并无关联,而是通过viewModel来进行联系的,Model和View之间有着数据绑定的关系,Model中的数据发生改变时会触发View层的更新,View中数据的变化也会更新到Model层中。
这种模式实现了Model和View的数据自动同步,因此开发者只需要专注于数据的维护,而不用操作DOM
MVC
MVC是Model、View和Controller的方式来组织代码结构,其中的View负责显示逻辑,Model负责业务数据以及数据操作。Model数据发生变化的时候会通知有关View层更新页面,Controller是View和Model之间的纽带,带用户页面发生交互的时候,COntroller中的事件触发器开始工作,通过调用Model层来完成Model的修改,Model再去更新View层
MVP
MVC中很多逻辑会在View层和Model层耦合,代码复杂的时候可能会造成代码的混乱。MVP模式和MVC的不同在于Presenter和Controller。Presenter来实现对View层和Model层的解耦,在MVC中Controller只知道Model中的接口,不知道View中的逻辑,然而在MVP模式中,View层的接口同样暴漏给了Presenter,因此可将Model层的变化和View的变化绑定在一起,实现View和Model的同步更新。
对于Computed
对于watch
总结
<button type="submit">
<slot>
Submit <!-- 默认内容 -->
</slot>
</button>
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
使用具名插槽的方式
<BaseLayout>
<template v-slot:header>
<!-- header 插槽的内容放这里 -->
</template>
</BaseLayout>
v-slot 有对应的简写 #,因此 可以简写为 。
<!-- <MyComponent> 的模板 -->
<div>
<slot :text="greetingMessage" :count="1"></slot>
</div>
在调用组件中调用slot 的数据
<MyComponent v-slot="slotProps">
{{ slotProps.text }} {{ slotProps.count }}
</MyComponent>
具名作用域插槽
<MyComponent>
<template #header="headerProps">
{{ headerProps }}
</template>
</MyComponent>
v-model 实际上是一个语法糖,如:
<input v-model="searchText" />
实际上相当于:
<input
:value="searchText"
@input="searchText = $event.target.value"
/>
应用在组件上就变成:
<CustomInput
:value="searchText"
@update:value="newValue => searchText = newValue"
/>
实际上是通过prop
和$.emit
来实现的
<!-- CustomInput.vue -->
<template>
<input
:value="modelValue"
@input="$emit('update:value', $event.target.value)"
/>
</template>
Vue组件可能存在多个实例,如果使用对象形式定义data,会导致它们共用一个data对象,状态变化会影响所有组件实例,这是不合理的,采用函数的形式,在initData时会将其作为工厂函数返回新的data对象,有效避免多实例之间状态污染的问题。
(1)编码阶段
另外还有keep-alive
独有的生命周期
(1)父子组件间通信
(2)兄弟组件间通信
任意组件之间
https://abc.com/#/vue
hash模式的主要原理是onhashchange()事件window.onhashchange = function(event) {
console.log(event.newURL, oldURL)
}
使用onhashchange()事件的好处是在页面的hash值发生变化的时候不用向后端发送请求
这种模式需要后台支持,如果访问到不存在的页面则会返回404
vuex 是专为vue.js 应用开发的状态管理模式,每一个Vuex应用的核心是store(仓库)。
export default createStore({
state: {
count: 1
},
getters: {
twoCount(state, getters) {
return state.count * 2
}
},
mutations: {
addCount(state, payload = 1) {
state.count += payload
}
},
actions: {
addCountAsync(context, payload = 1) {
context.commit("addCount", payload)
}
},
modules: {
a: moduleA
}
})
this.$store.state.count
获取到count的值,当然,通过this获取的方式非常的麻烦,不方便多个属性获取,所以还可以通过辅助函数mapState
获取 computed: {
...mapState(["count"])
},
// 或者
computed: {
...mapState({
count: state => state.count
})
},
// 或者
computed: {
...mapState({
countAlias: "count"
})
},
state
,可以获取到state中的值,也可以接受其他的getters
作为第二个参数。在组件中可以通过this.$store.getters.twoCount
来调用,它的辅助函数是mapGetters
。 computed: {
...mapGetters(["twoCount"])
},
// 或者
computed: {
...mapGetters({
twoCount: "twoCount"
})
},
state
,第二个参数的用户自行传入的参数payload
(其他名字也可以),需要注意的是不建议在组件中直接使用mutation中的函数,需要使用store.commit
操作mutation中的函数。或者可以使用辅助函数mapMutations
在组件中使用,例如: methods: {
// // 本质上也是映射为 this.$store.commit('addCount')
...mapMUtations(["addCount"])
},
context.commit
来提交一个mutation,也可以使用context.state
和context.getters
获取state和getters的值,但它并不是store实例本体 actions: {
addCountAsync(context, payload = 1) {
setTimeout(() => {
context.commit("addCount", payload)
}, 1000);
}
},
action 通过store.dispatch
触发,在组件中使用this.$store.dispatch("addCountAsync")
触发,在action中可以调用异步函数或者调用接口数据,可以分发多重mutation,它的辅助函数是mapActions
methods: {
...mapActions(["addCountAsync"])
}
action是异步函数,意味着还可以在函数中嵌套进Promise函数
actions: {
actionA ({ commit }) {
return new Promise((resolve, reject) => {
setTimeout(() => {
commit('someMutation')
resolve()
}, 1000)
})
}
}
可以使用then
来接收action函数的结果
store.dispatch('actionA').then(() => {
// ...
})
甚至可以嵌套它的action函数进来
const moduleA = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... },
getters: { ... }
}
const moduleB = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... }
}
const store = createStore({
modules: {
a: moduleA,
b: moduleB
}
})
store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态
模块中的使用方法和根目录中的使用方法一致,对于模块内部的 mutation 和 getter,接收的第一个参数是模块的局部状态对象。对于模块内部的 action,局部状态通过 context.state 暴露出来,根节点状态则为 context.rootState,对于模块内部的 getter,根节点状态会作为第三个参数暴露出来:
const moduleA = {
// ...
getters: {
sumWithRootCount (state, getters, rootState) {
return state.count + rootState.count
}
}
}
module 的命名空间
默认情况下,模块内部的 action 和 mutation 仍然是注册在全局命名空间的——这样使得多个模块能够对同一个 action 或 mutation 作出响应。也就是说可以和根一样的调用,会自动匹配Model中的action 或 mutation。Getter 同样也默认注册在全局命名空间。
为了具有更高的封装度和复用性,你可以通过添加 namespaced: true 的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名,例如:
const store = createStore({
modules: {
account: {
namespaced: true,
// 模块内容(module assets)
state: () => ({ ... }), // 模块内的状态已经是嵌套的了,使用 `namespaced` 属性不会对其产生影响
getters: {
isAdmin () { ... } // -> getters['account/isAdmin']
},
actions: {
login () { ... } // -> dispatch('account/login')
},
mutations: {
login () { ... } // -> commit('account/login')
},
// 嵌套模块
modules: {
// 继承父模块的命名空间
myPage: {
state: () => ({ ... }),
getters: {
profile () { ... } // -> getters['account/profile']
}
},
// 进一步嵌套命名空间
posts: {
namespaced: true,
state: () => ({ ... }),
getters: {
popular () { ... } // -> getters['account/posts/popular']
}
}
}
}
}
})
在组件中使用如下:
computed: {
...mapState({
a: state => state.some.nested.module.a,
b: state => state.some.nested.module.b
}),
...mapGetters([
'some/nested/module/someGetter', // -> this['some/nested/module/someGetter']
'some/nested/module/someOtherGetter', // -> this['some/nested/module/someOtherGetter']
])
},
methods: {
...mapActions([
'some/nested/module/foo', // -> this['some/nested/module/foo']()
'some/nested/module/bar' // -> this['some/nested/module/bar']()
])
}
使用vuex的整体流程
在vue3 中使用 vuex
首先引用 useStore
钩子函数, 等价于this.$store
import { useStore } from 'vuex'
export default {
setup () {
const store = useStore()
}
}
import { computed } from 'vue'
import { useStore } from 'vuex'
export default {
setup () {
const store = useStore()
return {
// 在 computed 函数中访问 state
count: computed(() => store.state.count),
// 在 computed 函数中访问 getter
double: computed(() => store.getters.double)
}
}
}
import { useStore } from 'vuex'
export default {
setup () {
const store = useStore()
return {
// 使用 mutation
increment: () => store.commit('increment'),
// 使用 action
asyncIncrement: () => store.dispatch('asyncIncrement')
}
}
}
(1)什么是diff算法
diff 算法是一种通过同层的树节点进行比较高效的算法
有两个特点:
diff 不是vue特有的,在很多场景下都有应用,在vue中作用是用来虚拟DOM和真实DOM之间的vnode节点比较
(2)比较方式
diff 的整体策略为:深度优先,同级比较
1.比较只会在同层级比较,不会跨层级比较
2.比较的过程中,循环从两边向中间靠拢
以下是vue通过diff算法更新的例子:
新旧vnode
节点如下图所示:
第一次循环后,发现旧节点D与新节点D相同,直接复用D作为diff的第一个真实的节点,同时旧节点的endIndex
移动到C,新节点的startIndex
u移动到C
第二次循环后,同样是旧节点的末尾和新节点的开头(都是 C)相同,同理,diff 后创建了 C 的真实节点插入到第一次创建的 D 节点后面。同时旧节点的 endIndex 移动到了 B,新节点的 startIndex 移动到了 E
第三次循环中,发现E没有找到,这时候只能直接创建新的真实节点 E,插入到第二次创建的 C 节点之后。同时新节点的 startIndex 移动到了 A。旧节点的 startIndex 和 endIndex 都保持不动
第四次循环中,发现了新旧节点的开头(都是 A)相同,于是 diff 后创建了 A 的真实节点,插入到前一次创建的 E 节点后面。同时旧节点的 startIndex 移动到了 B,新节点的startIndex 移动到了 B
第五次循环中,情形同第四次循环一样,因此 diff 后创建了 B 真实节点 插入到前一次创建的 A 节点后面。同时旧节点的 startIndex移动到了 C,新节点的 startIndex 移动到了 F
新节点的 startIndex 已经大于 endIndex 了,需要创建 newStartIdx 和 newEndIdx 之间的所有节点,也就是节点F,直接创建 F 节点对应的真实节点放到 B 节点后面
(1)监测机制的改变
(2)对象式的组件声明方式
语法API
回顾vue2,每个组件实例都对应一个watcher,他会在组件渲染的过程中把用到的数据property记录为依赖,当依赖发生改变,触发setter,则会通知watcher,从而使关联的组件重新渲染
设想一下,假如一个组件有大量的静态节点,如:
<template>
<div id="content">
<p class="text">静态文本</p>
<p class="text">静态文本</p>
<p class="text">{{ message }}</p>
<p class="text">静态文本</p>
...
<p class="text">静态文本</p>
</div>
</template>
可以看到组件的内部只有一个动态节点,剩余的都是一些静态节点,所以这里的很多diff算法和遍历都是不需要的,造成性能浪费
因此,vue3 在编译阶段,主要做了以下优化:
(1)diff优化
vue3 相比于 vue2 在diff算法中增加了静态标记,在会发生变化的地方增加一个 flag 标记
下图这里,已经标记静态节点的p标签在diff过程中则不会比较,把性能进一步提高
静态提升
Vue3中对不参与更新的元素,会做静态提升,只会被创建一次,在渲染时直接复用,提升为一个常量,不参与 diff 运算
事件监听缓存
默认情况下,绑定事件行为被视为动态绑定,每次都会去追踪它的变化,开启缓存后下次diff算法直接用
SSR优化
当静态内容大到一定量级时候,会用createStaticVNode方法在客户端去生成一个static node,这些静态node,会被直接innerHtml,就不需要创建对象,然后根据对象渲染
div>
<div>
<span>你好</span>
</div>
... // 很多个静态属性
<div>
<span>{{ message }}</span>
</div>
</div>
编译后
import { mergeProps as _mergeProps } from "vue"
import { ssrRenderAttrs as _ssrRenderAttrs, ssrInterpolate as _ssrInterpolate } from "@vue/server-renderer"
export function ssrRender(_ctx, _push, _parent, _attrs, $props, $setup, $data, $options) {
const _cssVars = { style: { color: _ctx.color }}
_push(`${
_ssrRenderAttrs(_mergeProps(_attrs, _cssVars))
}>你好...你好${
_ssrInterpolate(_ctx.message)
}`)
}
2、源码体积
相比Vue2,Vue3整体体积变小了,除了移出一些不常用的API,再重要的是Tree shanking
任何一个函数,如ref、reavtived、computed等,仅仅在用到的时候才打包,没用到的模块都被摇掉,打包的整体体积变小
import { computed, defineComponent, ref } from 'vue';
export default defineComponent({
setup(props, context) {
const age = ref(18)
let state = reactive({
name: 'test'
})
const readOnlyAge = computed(() => age.value++) // 19
return {
age,
state,
readOnlyAge
}
}
});
3、响应式系统
vue2中采用 defineProperty来劫持整个对象,然后进行深度遍历所有属性,给每个属性添加getter和setter,实现响应式
vue3采用proxy重写了响应式系统,因为proxy可以对整个对象进行监听,所以不需要深度遍历
- 可以监听动态属性的添加
- 可以监听到数组的索引和数组length属性
- 可以监听删除属性
虚拟DOM
什么是虚拟DOM
虚拟DOM实际上是一层对真实ODM的抽象,用JavaScript对象来描述节点,最终通过一系列操作使这个树映射到真实的DOM上
这JavaScript 对象中,虚拟DOM表现为一个Object对象,并且至少包含标签名(tag
)、属性(attrs
)和子元素对象(children
)三个属性,创建虚拟DOM就是为了更好的将模拟的节点渲染到页面视图中,所以虚拟DOM
对象节点和真实DOM
节点属性一一对应
例如,在真实的DOM中,标签如下:
<div id="app">
<p class="p">节点内容</p>
<h3>{{ foo }}</h3>
</div>
实例化DOM
const app = new Vue({
el:"#app",
data:{
foo:"foo"
}
})
当然我们可以根据基本属性生成如下虚拟DOM,类似于:
{
tag: "div",
attr: {
id: "app"
},
children: [
{
tag: "p",
attr: {
class: "p"
},
children: "节点内容"
},
{
tag: "h3",
attr: {
},
children: foo
}
]
}
观察render
生成的虚拟DOM:
(function anonymous(
) {
with(this){return _c('div',{attrs:{"id":"app"}},[_c('p',{staticClass:"p"},
[_v("节点内容")]),_v(" "),_c('h3',[_v(_s(foo))])])}})
通过vNode
vue可以对这颗抽象树进行创建节点、删除节点以及修改节点的操作,经过diff
计算做出需要修改的最小单位,再去更新视图,减少DOM操作,提高性能
为什么需要虚拟DOM
DOM是很慢的,其元素非常庞大,页面性能的问题,大部分都是由DOM操作引起的
真实的DOM节点包含很多的属性,控制台打印直观感受一下:
由此可见,如果每次都直接去操作DOM的代价是非常昂贵的,平凡操作还会出现页面卡顿,影响用户体验
虚拟DOM的技术不仅仅可以用在网页上,最大的优势在于抽象了原本的抽象过程,实现跨平台的能力,目标平台可以是安卓和IOS的原生组件,也可以是小程序,也可以是各种GUI
如何实现虚拟DOM
首先可以看看vue中的vnode的结构
源码位置: src/core/vdom/vnode.js
export default class VNode {
tag: string | void;
data: VNodeData | void;
children: ?Array<VNode>;
text: string | void;
elm: Node | void;
ns: string | void;
context: Component | void; // rendered in this component's scope
functionalContext: Component | void; // only for functional component root nodes
key: string | number | void;
componentOptions: VNodeComponentOptions | void;
componentInstance: Component | void; // component instance
parent: VNode | void; // component placeholder node
raw: boolean; // contains raw HTML? (server only)
isStatic: boolean; // hoisted static node
isRootInsert: boolean; // necessary for enter transition check
isComment: boolean; // empty comment placeholder?
isCloned: boolean; // is a cloned node?
isOnce: boolean; // is a v-once node?
constructor (
tag?: string,
data?: VNodeData,
children?: ?Array<VNode>,
text?: string,
elm?: Node,
context?: Component,
componentOptions?: VNodeComponentOptions
) {
/*当前节点的标签名*/
this.tag = tag
/*当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息*/
this.data = data
/*当前节点的子节点,是一个数组*/
this.children = children
/*当前节点的文本*/
this.text = text
/*当前虚拟节点对应的真实dom节点*/
this.elm = elm
/*当前节点的名字空间*/
this.ns = undefined
/*编译作用域*/
this.context = context
/*函数化组件作用域*/
this.functionalContext = undefined
/*节点的key属性,被当作节点的标志,用以优化*/
this.key = data && data.key
/*组件的option选项*/
this.componentOptions = componentOptions
/*当前节点对应的组件的实例*/
this.componentInstance = undefined
/*当前节点的父节点*/
this.parent = undefined
/*简而言之就是是否为原生HTML或只是普通文本,innerHTML的时候为true,textContent的时候为false*/
this.raw = false
/*静态节点标志*/
this.isStatic = false
/*是否作为跟节点插入*/
this.isRootInsert = true
/*是否为注释节点*/
this.isComment = false
/*是否为克隆节点*/
this.isCloned = false
/*是否有v-once指令*/
this.isOnce = false
}
// DEPRECATED: alias for componentInstance for backwards compat.
/* istanbul ignore next https://github.com/answershuto/learnVue*/
get child (): Component | void {
return this.componentInstance
}
}
代码中
- 所有对象的
context
选项都指向了vue实例
elm
属性则指向了其相对应的真实DOM节点