new Vue()到底干了什么?🚀
前言
本文不会对Vue源码进行过多细致的解读,主要串联起new Vue()之后,八大生命周期
是如何触发的,以及Vue是如何进行dom更新渲染的。
注:本文结合Vue源码与Vue.js技术揭秘整理而来;为了缩减篇幅,文中代码均有删减
入口文件
new Vue()之后调用 _init 方法,开始相关初始化工作
function Vue(options)
this._init(options)
}
// 以下几个方法皆为在Vue原型链上挂载相应的方法
initMixin(Vue) // _init
stateMixin(Vue) // $data、$props、$set、$delete、$watch
eventsMixin(Vue) // $on、$once、$off、$emit
lifecycleMixin(Vue) // _update、$forceUpdate、$destroy
renderMixin(Vue) // $nextick、_render
beforeCreate & created
Vue.prototype._init = function (options?: Object) {
// ...
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
// ...
}
可以看到 beforeCreate
和 created
的钩子调用是在 initState
的前后。那么显然 beforeCreate
的钩子函数中就不能获取到 props
、data
中定义的值,也不能调用 methods
中定义的函数。
在这两个钩子函数执行的时候,并没有渲染 DOM,所以我们也不能够访问 DOM,一般来说,如果组件在加载的时候需要和后端有交互,放在这俩个钩子函数执行都可以,如果是需要访问 props
、data
等数据的话,就需要使用 created
钩子函数。之后我们会介绍 vue-router 和 vuex 的时候会发现它们都混合了 beforeCreate
钩子函数
initState
initState
的作用是初始化 props
、data
、methods
、watch
、computed
等属性
export function initState(vm: Component) {
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
// Composition API
initSetup(vm)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
const ob = observe((vm._data = {}))
ob && ob.vmCount++
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
initData
initData主要调用observe方法,该方法会生成一个__ob__
实例挂载到对象数据上,并递归的将对象属性变为响应式
function initData(vm: Component) {
let data: any = vm.$options.data
data = vm._data = isFunction(data) ? getData(data, vm) : data || {};
// ...
// observe data
const ob = observe(data)
ob && ob.vmCount++
}
constructor(public value: any, public shallow = false, public mock = false) {
// this.value = value
this.dep = mock ? mockDep : new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (isArray(value)) {
// ...
} else {
const keys = Object.keys(value)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
defineReactive(value, key, NO_INITIAL_VALUE, undefined, shallow, mock)
}
}
}
export function defineReactive(
obj: object,
key: string,
val?: any,
customSetter?: Function | null,
shallow?: boolean,
mock?: boolean
) {
const dep = new Dep();
// ...
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
}
return isRef(value) && !shallow ? value.value : value
},
set: function reactiveSetter(newVal) {
const value = getter ? getter.call(obj) : val
if (!hasChanged(value, newVal)) {
return
}
// ...
val = newVal
dep.notify()
}
})
return dep
}
create阶段流程总结
_init() -> beforeCreate
-> initState() -> initData() -> observe() -> defineReactive() -> getter、setter -> created
数据准备阶段完成,准备进行依赖收集。
beforeMount & mounted
前置api $mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
还是在Vue.prototype._init
里面,执行完created
生命周期后执行$mount
进行组件挂载。
callHook(vm, 'created')
...
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
顾名思义,beforeMount
钩子函数发生在 mount
,也就是 DOM 挂载之前,它的调用时机是在 mountComponent
函数中,定义在 src/core/instance/lifecycle.ts 中:
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
// ...
callHook(vm, 'beforeMount')
// 根据初始化还是更新,选择不同的函数
let updateComponent
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
// 挂载
updateComponent = () => {
const vnode = vm._render()
vm._update(vnode, hydrating)
}
} else {
// 更新
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
其中vm._render()
用于生成当前组件的vnode
,同时触发模板上的依赖收集。
在执行 vm._render()
函数渲染 VNode 之前,执行了 beforeMount
钩子函数,在执行完 vm._update()
把 vnode patch 到真实 DOM 后,执行 mounted
钩子。
注意:这里对 mounted
钩子函数执行有一个判断逻辑,vm.$vnode
如果为 null
,则表明这不是一次组件的初始化过程,而是我们通过外部 new Vue
初始化过程。那么对于组件,它的 mounted
时机在哪儿呢?
为了精简内容,具体看Vue.js技术揭秘中的解释。
mount阶段中的_update
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
const prevEl = vm.$el
const prevVnode = vm._vnode
vm._vnode = vnode
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
restoreActiveInstance()
// update __vue__ reference
if (prevEl) {
prevEl.__vue__ = null
}
if (vm.$el) {
vm.$el.__vue__ = vm
}
}
return function patch(oldVnode, vnode, hydrating, removeOnly) {
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue: any[] = []
if (isUndef(oldVnode)) {
// empty mount (likely as component), create new root element
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
} else {
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
这里的__patch__
就是patch
,用于生成虚拟DOM树,执行后会返回真实dom节点。然后在_update里通过vm.$el
进行挂载,完成挂载后触发mounted
生命周期。
初始化Wacher实例
constructor(
vm: Component | null,
expOrFn: string | (() => any),
cb: Function,
options?: WatcherOptions | null,
isRenderWatcher?: boolean
) {
if (isRenderWatcher) {
vm._watcher = this
}
// ...
// parse expression for getter
if (isFunction(expOrFn)) {
this.getter = expOrFn
} else {
// ...
}
this.value = this.lazy ? undefined : this.get()
}
这里的expOrFn
就是mount中的updateComponent
,从而执行vm._update(vm._render(), hydrating)
,生成虚拟dom,并挂载真实dom。
update阶段中的_update
当响应式数据更新后,会调用数据对象挂载的__ob__.dep.notify()
方法,进而触发以下操作(update):
wacher.update()
-> watcher.run()
-> watcher.get()
-> getter执行(mount中的updateComponent)
,进而重新进行依赖收集。
mount阶段流程总结
vm.$mount() -> beforeMount
-> new Watcher() -> watcher.get() -> updateComponent() -> vm._render() -> vm._update() -> vm.$el = patch() -> mounted
beforeUpdate & updated
更新阶段从某个响应式数据发生变化的时候开始,它的setter函数会通知闭包中的Dep,Dep则会调用它管理的所有Watch对象,触发Watch对象的update方法:
update() {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
Vue异步执行dom更新,将watcher放进队列里,从而触发queueWatcher:
export function queueWatcher(watcher: Watcher) {
const id = watcher.id
if (has[id] != null) {
return
}
has[id] = true
if (!flushing) {
queue.push(watcher)
}
// queue the flush
if (!waiting) {
waiting = true
nextTick(flushSchedulerQueue)
}
}
下一个事件循环(nexTick),清空队列,执行flushSchedulerQueue
函数,它的定义在 src/core/observer/scheduler.ts 中:
function flushSchedulerQueue () {
// ...
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
watcher.before() // 触发beforeUpdate生命周期
}
id = watcher.id
has[id] = null
watcher.run()
// ...
}
// 获取到 updatedQueue
callUpdatedHooks(updatedQueue)
}
function callUpdatedHooks (queue) {
let i = queue.length
while (i--) {
const watcher = queue[i]
const vm = watcher.vm
if (vm._watcher === watcher && vm._isMounted) {
callHook(vm, 'updated')
}
}
}
同时更新阶段watcher
会执行mounted
阶段传进去的options(watcher.before
),从而触发beforeUpdate
生命周期
const watcherOptions: WatcherOptions = {
before() {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}
同时,mountComponent
中实例化Watcher,还把当前 wathcer
实例 push 到 vm._watchers
中,vm._watcher
是专门用来监听 vm
上数据变化然后重新渲染的,所以它是一个渲染相关的 watcher
,因此在 callUpdatedHooks
函数中,只有 vm._watcher
的回调执行完毕后,才会执行 updated
钩子函数。
重点:一个组件实例只有一个视图watcher,配合nextick机制,组件在一次事件循环中,无论绑定的数据更新多少遍,始终只有一个watcher生效。
dom树如何更新
我们现在知道了模板编译后会生成render
函数,render函数执行后会触发模板(视图)上的依赖收集,同时生成组件的vnode。因此当相应的数据变化的时候,会触发wacher调用get()
方法,从而触发updateComponent
->_update
(diff算法)。
但是我们缺少了一点,那就是父组件通过props
传进来的数据是怎样添加响应式并进行子组件的重新渲染的?
function initProps(vm: Component, propsOptions: Object) {
const propsData = vm.$options.propsData || {}
// ...
for (const key in propsOptions) {
keys.push(key)
const value = validateProp(key, propsOptions, propsData, vm)
// ...
defineReactive(props, key, value)
// ...
}
组件初始化时,将props初始化为响应式数据。
子组件更新
首先,prop
数据的值变化在父组件,我们知道在父组件的 render
过程中会访问到这个 prop
数据,所以当 prop
数据变化一定会触发父组件的重新渲染,那么重新渲染是如何更新子组件对应的 prop
的值呢?
在父组件重新渲染的最后,会执行 patch
过程,进而执行 patchVnode
函数,patchVnode
通常是一个递归过程,当它遇到组件 vnode
的时候,会执行组件更新过程的 prepatch
钩子函数,在 src/core/vdom/patch.ts 中:
function patchVnode (
oldVnode,
vnode,
insertedVnodeQueue,
ownerArray,
index,
removeOnly
) {
// ...
let i
const data = vnode.data
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode)
}
// ...
}
这里的i
,就是prepatch。
prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
const options = vnode.componentOptions
const child = vnode.componentInstance = oldVnode.componentInstance
updateChildComponent(
child,
options.propsData, // updated props
options.listeners, // updated listeners
vnode, // new parent vnode
options.children // new children
)
}
这里调用了updateChildComponent,在这个方法里会对子组件的props
进行更新,从而触发setter
-> watcher.get()
-> _update()
-> patch()、diff
等
export function updateChildComponent(
vm: Component,
propsData: Record<string, any> | null | undefined,
listeners: Record<string, Function | Array<Function>> | undefined,
parentVnode: MountedComponentVNode,
renderChildren?: Array<VNode> | null
) {
// ...
// update props
if (propsData && vm.$options.props) {
toggleObserving(false)
const props = vm._props
const propKeys = vm.$options._propKeys || []
for (let i = 0; i < propKeys.length; i++) {
const key = propKeys[i]
const propOptions: any = vm.$options.props // wtf flow?
props[key] = validateProp(key, propOptions, propsData, vm)
}
// keep a copy of raw propsData
vm.$options.propsData = propsData
}
}
注意:
其实子组件的重新渲染有 2 种情况,一个是 prop
值被修改,另一个是对象类型的 prop
内部属性的变化。
先来看一下 prop
值被修改的情况,当执行 props[key] = validateProp(key, propOptions, propsData, vm)
更新子组件 prop
的时候,会触发 prop
的 setter
过程,只要在渲染子组件的时候访问过这个 prop
值,那么根据响应式原理,就会触发子组件的重新渲染。
再来看一下当对象类型的 prop
的内部属性发生变化的时候,这个时候其实并没有触发子组件 prop
的更新。但是在子组件的渲染过程中,访问过这个对象 prop
,所以这个对象 prop
在触发 getter
的时候会把子组件的 render watcher
收集到依赖中,然后当我们在父组件更新这个对象 prop
的某个属性的时候,会触发 setter
过程,也就会通知子组件 render watcher
的 update
,进而触发子组件的重新渲染。
以上就是当父组件 props
更新,触发子组件重新渲染的 2 种情况。
更新阶段流程总结
响应式数据更新(props、data) -> wacher.update() -> queueWatcher() -> flushSchedulerQueue() -> beforeUpdate
-> watcher.run() -> watcher.get() -> updateComponent() -> vm._render() -> vm._update() -> vm.$el = patch() -> callUpdatedHooks() -> updated
beforeDestroy & destroyed
顾名思义,beforeDestroy
和 destroyed
钩子函数的执行时机在组件销毁的阶段,最终会调用 $destroy
方法,它的定义在 src/core/instance/lifecycle.ts 中:
Vue.prototype.$destroy = function () {
const vm: Component = this
if (vm._isBeingDestroyed) {
return
}
callHook(vm, 'beforeDestroy')
vm._isBeingDestroyed = true
// remove self from parent
const parent = vm.$parent
if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
remove(parent.$children, vm)
}
// teardown scope. this includes both the render watcher and other
// watchers created
vm._scope.stop()
// remove reference from data ob
// frozen object may not have observer.
if (vm._data.__ob__) {
vm._data.__ob__.vmCount--
}
// call the last hook...
vm._isDestroyed = true
// invoke destroy hooks on current rendered tree
vm.__patch__(vm._vnode, null)
// fire destroyed hook
callHook(vm, 'destroyed')
// turn off all instance listeners.
vm.$off()
// remove __vue__ reference
if (vm.$el) {
vm.$el.__vue__ = null
}
// release circular reference (#6759)
if (vm.$vnode) {
vm.$vnode.parent = null
}
}
流程总结
beforeCreate -> created:
_init() -> beforeCreate
-> initState() -> initData() -> observe() -> defineReactive() -> getter、setter -> created
beforeMount -> mounted
vm.$mount() -> beforeMount
-> new Watcher() -> watcher.get() -> updateComponent() -> vm._render() -> vm._update() -> vm.$el = patch() -> mounted
beforeUpdate -> updated
响应式数据更新(props、data) -> wacher.update() -> queueWatcher() -> flushSchedulerQueue() -> beforeUpdate
-> watcher.run() -> watcher.get() -> updateComponent() -> vm._render() -> vm._update() -> vm.$el = patch() -> callUpdatedHooks() -> updated
beforeDestroy -> destroyed
vm.$destroy() -> beforeDestroy
-> 节点卸载、数据清除、移除虚拟dom等 -> destroyed
本来想简短的把整个流程讲完,奈何内容实在太多,也是一次对Vue生命周期探索的有趣体验吧!
纸上得来终觉浅 绝知此事要躬行
参考文献:
转载自:https://juejin.cn/post/7235591859669827640