MVVM
从 MVC 到 MVVM
view(pages)
<–>Controller(路由、控制器)
<–>Model(数据 json)
View(HTML)
<–>ViewModel(DOM监听、数据绑定)
<–>Model(数据 json)
MVC
Model
封装了一些逻辑和操作数据的接口View
视图用于展示页面Controller
作为控制器,通过调用接口对数据进行操作并返回给 View 层,承上启下的与 Model、View 双向交流MVVM
Model
数据层,只关心数据方面ViewModel
事件监听和数据绑定,起到双向绑定的作用,响应式更新数据View
视图展示页面
数据变化驱动视图更新,视图变化数据也会同时响应 如 V-Model,是一个双向的过程
Vue 源码
- Vue 作为 构造函数,通过在构造函数的原型上扩展方法
1
2
3function Vue(options) {
this._init(options) // Vue.prototype._init(options)
}
Vue 核心
compiler + reactive + runtime
compiler
编译器,将 template 编译成为 AST 抽象语法树 (以及 可执行函数的字符串形式 with(this){return ${code}}
)
reactive
响应式处理 data
runtime
虚拟 DOM(vnode) + diff 算法 + 真实 DOM 操作
Vue 初始化时做了什么
- 处理组件配置项
options
1 | Vue.prototype._init = function (options) { |
initLifecycle(vm)
- 初始化一些组件实例的关系属性:
1
2
3
4
5
6
7
8
9
10
11
12vm.$parent = parent
vm.$root = parent ? parent.$root : vm
vm.$children = []
vm.$refs = {}
vm._watcher = null
vm._inactive = null
vm._directInactive = false
vm._isMounted = false
vm._isDestroyed = false
vm._isBeingDestroyed = false
initEvents(vm)
初始化自定义事件
- 从父组件的配置项里拿到 监听器
1
const listeners = vm.$options._parentListeners
- 在
prototype
上定义 $on | $off | $emit 等方法
initRender(vm)
- 定义插槽
$slot
- 定义 _c | createElement() 方法
1 | vm.$slots = resolveSlots(options._renderChildren, renderContext) |
callHook(vm, 'beforeCreate')
执行生命周期钩子 beforeCreate()initInjections(vm)
初始化 injectinitState(vm)
初始化 state,响应式处理 props | data | computed | watch | methods ..
对props -> methods -> data -> computed -> watch
初始化,
- 并判重:key 不得与其他 key 重复;
- 将属性配置代理到 vue 实例上,允许通过 this.keyName 的形式访问
initProvide(vm)
初始化 providecallHook(vm, 'created')
执行生命周期钩子 created()查看是否有 el 选项,有则自动调用 $mount(),没有则需要 手动调用 $mount()
1
2
3new Vue({
// el: '#app',
}).$mount()
响应式原理
如何实现 vue 响应式
响应式核心是通过 Object.defineProperty() 拦截对数据属性的访问和设置来实现的
响应式数据分为两类:
对象: 循环遍历数据对象的所有属性,为其设置
getter/setter
方法,以达到拦截的目的如果其属性依旧为 对象时,则递归每一个
key
,为其设置getter/setter
this.key
访问数据时被getter
方法拦截,并调用dep.depend()
方法,,进行依赖收集,在 dep 中存储相关 watcherobj[key] = val
时,修改数据触发setter
方法,在其中对新旧值对比,需要改变时 将新值设为被观测对象(observe)并进行响应式处理,并调用dep.notify()
触发依赖通知 watcher 进行异步更新
数组:以数组原方法为原型创建新的对象,并在其中重写增强了 7 个可以直接改变数组的方法,通过拦截这 7 个方法进行数组操作
- 插入新数据时进行响应式处理,然后 dep 通知 watcher 去更新
- 删除数据时也是由 dep 通知更新
Data 响应式
- initData
1
2const keys = Object.keys(data)
observe(data, true /** rootData */) // new Observe(data)
Observe
1 | // 如果 observe 的是数组 |
Dep && Watcher
- Dep 的作用就是收集属性值的变化,用一个数组 subs 保存订阅者 (subs: Array
) 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20Object.defineProperty(obj, key, {
get: function () {
// dep 和 watcher 双向收集
dep.depend()
if (childOb) {
// 对嵌套对象进行 依赖收集
childOb.dep.depend()
// 数组
if (Array.isArray(value)) {
dependArray(value)
}
}
},
set: function () {
// 对新值 观测 响应式处理
childOb = !shallow && observe(newVal)
// 当响应式数据更新时,做依赖通知更新,即通知所有 watcher,执行 watcher.update 方法,进入异步更新阶段
dep.notify()
}
})1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22// dep.js
depend () {
if (Dep.target) {
// dep 放入 newDeps 数组 -> this.newDeps.push(dep)
// watcher 放入 dep 数组(subs) -> dep.addSub(this)
// 双向收集
Dep.target.addDep(this)
}
}
// watcher.js
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
// 将 dep 放到 watcher 中
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
// 将 watcher 自己放到 dep 中,双向收集
dep.addSub(this)
}
}
} - 数据发生变化时,通知 dep 所有
watcher
,执行 watcher.update(),见异步更新
1 | set: function () { |
computed
- 每个计算属性都 实例化一个
watcher
对象,一个 computed key 对应一个 watcher key - computed 的两种形式:function、包含一个 get 方法的对象,最终都会转化为 getter
- 初始化定义 computed 的时候,Object.defineProperty 方法将 computed 的属性代理到 vue,并定义
getter/setter
方法 - 访问 computed 时自动调用 getter,执行 createComputedGetter
createComputedGetter
方法调用 computed 回调函数执行,并将watcher.dirty = false
,将回调函数执行结果 作为缓存 保存在watcher.value
并返回- 当响应式数据更新时,遍历所有 watcher 并执行他们各自的 update 方法,将
watcher.dirty = true
,重新执行 computed 回调函数,计算新值,然后缓存到watcher.value
计算属性缓存原理:
每个 computed 对应一个 new Watcher,watcher 的作用就是调用 update 方法进行异步更新,而 update 只发生在 首次渲染以及后续视图更新
所以 首次渲染时,watcher 也会通知对应的 computed 更新数据,也就是执行计算属性的回调函数,并把结果返回
watcher 则是将其缓存到 watcher.value 上,并把 watcher.dirty 置为 false,后续对 computed 的访问都是将 watcher.value 的结果返回
只有下一次视图更新时,才会 watcher.dirty = true,并重新执行 computed 的回调函数,且继续缓存结果,dirty = false
异步更新
dep.notify()
此方法通知所有的 watcher,执行他们的watcher.update()
1
2
3
4
5
6
7
8notify () {
// 收集的 watcher
const subs = this.subs.slice()
// 遍历当前 dep 收集的所有 watcher,让这些 watcher 依次去执行自己的 update 方法
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}watcher.update()
根据 watcher 配置项( lazy | async | none )决定怎么走,一般情况是
queueWatcher(this)
将当前 watcher 放进 watcher 队列1
2
3
4
5
6
7
8
9
10
11
12
13
14
15update () {
if (this.lazy) {
// 懒执行,比如 computed
// 将 dirty 置为 true,在组件更新后,当响应式数据再次被更新时,执行 computed getter
// 重新执行 computed 回调函数,计算新值,然后缓存到 watcher.value
this.dirty = true
} else if (this.sync) {
// 同步执行
// this.$watch() 或者 watch 选项传递一个 sync 配置({ sync: true })
this.run()
} else {
// 放入 watcher 队列(一般都是走这个)
queueWatcher(this)
}
}
queueWatcher 方法中,将当前 watcher 放进 queueWatcher 队列后,调用 nextTick() 方法把 flushSchedulerQueue 函数传入 callbacks 数组,
queueWatcher
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22export function queueWatcher (watcher: Watcher) {
if (!flushing) {
// flushing = false 表示当前 watcher 队列没有在刷新,可以直接入队
queue.push(watcher)
} else {
// 当前 watcher 队列已经在刷新,watcher 入队需要特殊操作
// watcher 队列是有序的
// 找到 队列中比要插入的 watcher id 大1个的成员,从它的下一个位置插入队列
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// 即 this.$nextTick() | Vue.nextTick()
// 将回调函数 flushSchedulerQueue 放入 callbacks 数组
// 通过 pending 控制,向浏览器异步任务队列添加 flushCallBack 函数
nextTick(flushSchedulerQueue)
}flushSchedulerQueue 函数遍历 watcher 并执行 run()
nextTick()
1 | export function nextTick (cb?: Function, ctx?: Object) { |
flushCallbacks
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17function flushCallbacks () {
// 再次置为 false,下一个 flushCallbacks 可以进入异步微任务队列
// 异步微任务队列 只能存在一个 flushCallbacks 函数
pending = false
// 缓存 callbacks 所有
const copies = callbacks.slice(0)
// 清空 callbacks
callbacks.length = 0
// 执行 callbacks 中所有 函数
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks) // flushCallbacks() 执行 callbacks 中所有
}
如何实现的异步更新机制
- 核心是利用浏览器的异步任务队列来实现,首选两种微任务 (promise | MutationObserver),其次宏任务 (setImmediate | setTimeout)
- 响应式数据更新后,调用
dep.notify()
方法,通知 dep 中收集的 watcher 去执行自己的 update 方法 - watcher.update() 方法将当前 watcher 放入一个 watcher 队列 (全局的 queue 数组)
- 然后通过
nextTick(flushSchedulerQueue)
方法把这个 刷新 watcher 队列的方法(flushSchedulerQueue)放入一个全局的 callbacks 数组中 - 如果此时浏览器的异步任务队列中没有一个叫
flushCallbacks
的函数,则执行 timeFunc 函数,将 flushCallbacks 函数放入浏览器的异步任务队列(promise) - 如果已经存在
flushCallbacks
则等待执行完成,下一波再放入 - 这个
flushCallbacks
函数 负责执行 callbacks 数组中的所有flushSchedulerQueue
函数,也就是 刷新当前 watcher 队列并执行成员的 watcher.run 方法 - run 方法就是执行 组件更新函数(rendering watcher)new Watcher 时传入的 updateComponent 方法,进入组件更新;或者用户 watch 的回调函数
Vue 的 nextTick API 如何实现
Vue.nextTick()
或者 vm.$nextTick()
就做了两件事:
将传递的回调函数用 try catch 包裹(捕捉异常)后,放入 callbacks 数组
callbacks
数组收集了用户调用nextTick
传递的回调 或者是flushSchedulerQueue
函数(所有watcher.run()
)执行 timerFunc 函数,在浏览器的异步任务队列中 放入一个 刷新 callbacks 数组的函数
1
2
3
4const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks) // flushCallbacks() 执行 callbacks 中所有
}
Vue 在更新 DOM 是异步执行的,响应式数据发生变化时,通过一个 callbacks 数组收集 在同一个事件循环中的所有数据变更,然后在下一次事件循环当中执行
1 | vm.msg = 'new message' |
wen
<comp @click="handleClick"></comp>
中谁监听的 click 事件
- 组件上的事件监听由组件本身来监听,谁触发谁监听
- 最终会转化为:
this.$emit() | this.on('click', function handleClick() {})
的形式
provide/inject
原理
inject
组件主动从 祖代 中查找匹配key
的结果,并赋值给inject
中key
对应的值,最后得到一个result[key] = value
的对象返回1
2
3
4
5
6
7
8
9const provideKey = inject[key].from
let source = vm
while (source) {
if (source._provided && hasOwn(source._provided, provideKey)) {
result[key] = source._provided[provideKey]
break
}
source = source.$parent
}
computed
和methods
区别
methods
每次访问都会执行computed
通过 watcher 来实现的,对每个computed key
实例化一个watcher
,默认lazy
执行;computed
每次渲染只执行一次,并将watcher.dirty
置为false
,当再次渲染页面调用watcher.update()
方法时,会将watcher.dirty
重置为true
,并且执行一次computed
computed 有两种形式:
1
2
3
4
5
6
7
8
9
10// function
computed: {
show() { return 1 }
}
// 包含 getter 的对象
computed: {
showA: {
get: function () {} // 默认只有 getter,也可以定义 setter
}
}
- watch
- watch 实际上就是 每个 观察对象 调用
vm.$watch
方法,其中会实例化一个 Watcher 对象,并配置参数(deep, immediate)
1 | watch: { |
- computed | watch | methods 区别
- 使用场景
- methods 一般用于封装一些复杂的逻辑,同步异步处理
- computed 一般用于简单的同步逻辑,将处理后的数据返回,显示在模板中,以减轻模板的重量
- watch 一般用于需要监测数据变化时,执行异步或者开销较大的操作
computed | watch 都是通过 watcher 来实现的
computed
默认 lazy 执行,且不可更改,watch
可配置(deep | immediate)使用场景不同,
watch
通常用于异步操作,computed
同步多watch | methods 两种东西没有可比性,不过可以把 watch 中一些复杂的逻辑抽离到 methods 中,提高可读性
- is 属性
- 内联模板
inline-template