Vue2组件更新流程(todo)
status: Idea Created time: March 15, 2024 6:52 PM
Vue2中有三个非常重要的类:Watcher、Observer和Dep。
Watcher:Vue2响应式中非常重要的一个类,管理依赖和状态的管理,watcher.update负责依赖的更新。
Observer:响应式对象,vm.$options.data会被其转换成响应式。本质上是通过Object.defineProperty(对于对象)和更改原型方法(对于数组)实现。
Dep:用于存储Watcher的一个结构,每个状态都有一个对应的dep。
整体流程:
在初始化时,vue2会对Component、watch和computed等创建Watcher实例(watcher),并通过
new Observer
将vm.$options.data
转换响应式对象(初始化data
在initData
函数中完成操作),同时每个状态都有一个对应的dep
对象,用于存放该状态的所有依赖(watcher
)。以组件为例,这里需要了解一个前提知识,vue无论是模板语法还是渲染函数,最后的目的都是为了生成
vnode
,而模板语法其实会被vue通过模板编译操作转换成渲染函数(render函数),render
函数执行的返回值就是vnode
,然后通过patch
新旧vnode
来更新DOM,从而达到更新视图的结果。在初始渲染组件时,vue会为组件创建一个watcher对象(执行
new Watcher
),在Watcher实例化的过程中,会将Dep.target
指向watcher
自身,之后执行一次组件render
函数,而在render
函数内可能会使用某些响应式状态,因此在执行render
函数中就自动会触发这些状态的getter
函数,在getter
函数中就会将Dep.target
(即当前组件watcher
对象)添加到observer
对应的dep
中当更新状态时,自然会触发状态的
setter
函数,此时就会通知该状态dep
中所有的watcher
,执行watcher.update()
进行更新,在这个例子中,会重新执行render
函数和patch
函数进行DOM更新。
具体代码:
首先,在组件初始化时,为组件创建一个watcher,每一个vue组件都有一个对应的watcher对象。
function mountComponent (
vm,
el,
hydrating
) {
vm.$el = el;
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode;
}
callHook(vm, 'beforeMount');
var updateComponent;
if (config.performance && mark) {
updateComponent = function () {
var vnode = vm._render(); // 执行render函数获得vnode,如果是模板语法,vue内部也会通过模板编译来将其转换成render函数
vm._update(vnode, hydrating); // vm._update函数内部其实也是通过patch来进行比对新旧vnode,从而进行更新
};
} else {
updateComponent = function () {
vm._update(vm._render(), hydrating);
};
}
// 每个vue组件都有对应的一个watcher对象
// 重点在这个实例化Watcher语句,参数传递了vm和updateComponent
new Watcher(vm, updateComponent, noop, {
before: function before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate');
}
}
}, true /* isRenderWatcher */);
// ...
return vm
}
之后主要看Watcher实例化的过程
export default class Watcher {
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
this.cb = cb
this.id = ++uid // uid是一个初始值为0的变量
this.active = true
this.dirty = this.lazy // for lazy watchers
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
// parsePath处理expOrFn是'obj.a'这种情况,例如在watch中就有这种写法
this.getter = parsePath(expOrFn)
}
// 最主要在于这条
this.value = this.lazy
? undefined
: this.get()
}
get () {
pushTarget(this) // 内部会执行 Dep.target = target
let value
const vm = this.vm
try {
// this,getter就是watcher初始化时传入的expOrFn参数,在这个例子中,它是updateComponent函数
// 也就是说,此时会调用一次render函数
value = this.getter.call(vm, vm)
} catch (e) {
// ...
} finally {
// ...
}
return value
}
}
也就是说,在初始化vue组件的watcher时,首先会将Dep.target指向watcher自身,然后执行一次vm.render进行渲染,同时也是让组件render引用的状态来收集watcher自身。
接下来看看vm.$options.data是如何变成