new Vue(..)之后总共有两个大的步骤,第一步是调用vm._init完成组件的各种准备(初始化)工作,然后是开始结合数据与模板实现页面的渲染。vue引入了虚拟DOM技术,这里页面渲染分为两步,将模板和数据(转为了render函数)转为虚拟DOM树,而后再将虚拟DOM树同步到界面上。上一小节已经分析过创建虚拟DOM树的过程,现在我们来看看虚拟DOM是如何同步到界面上的。
updateComponent = () => {
vm._update(vm._render(), hydrating) // hydrating: ssr相关 忽略
}
new Watcher(vm, updateComponent, noop, {
before () { /*...*/ }
}, true /* isRenderWatcher */)export function lifecycleMixin (Vue: Class<Component>) {
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
const prevEl = vm.$el
const prevVnode = vm._vnode
const restoreActiveInstance = setActiveInstance(vm)
vm._vnode = vnode
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
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
}
// if parent is an HOC, update its $el as well
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el
}
// updated hook is called by the scheduler to ensure that children are
// updated in a parent's updated hook.
}
//...
}scheduler进行调度的。udpated hook,先父后子(flushSchedulerQueue -> callUpdatedHooks)❓❓❓ 下面重点看下patch方法
这里的核心逻辑在snabbdom源码分析中说过,参考snabbdom@3.5.1 源码分析第三篇。
export function createPatchFunction (backend) {
//...
return function patch (oldVnode, vnode, hydrating, removeOnly) {
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
//...
if (isUndef(oldVnode)) {
//...createElm
} else {
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
//... patchVnode
} else {
//... createElm
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
}总共分为四种情况,见下面分析。
直接调用invokeDestroyHook触发oldVnode销毁逻辑
function invokeDestroyHook (vnode) {
let i, j
const data = vnode.data
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.destroy)) i(vnode)
for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode)
}
if (isDef(i = vnode.children)) {
for (j = 0; j < vnode.children.length; ++j) {
invokeDestroyHook(vnode.children[j])
}
}
}const componentVNodeHooks = {
//... init、insert、prepatch
destroy (vnode: MountedComponentVNode) {
const { componentInstance } = vnode
if (!componentInstance._isDestroyed) {
if (!vnode.data.keepAlive) {
componentInstance.$destroy()
} else {
deactivateChildComponent(componentInstance, true /* direct */)
}
}
}
}beforeDestroy,并设置正在销毁_isBeingDestroyed标识watcher(在mountComponent创建的,用来渲染组件的)watchers(在initState -> initWatcher中创建的)_isDestroyedvm.$el.__vue__ = null 结论:需要清理一切需要清理的,并且所有的属性应该都是统一在一个地方声明,确保删除的时候没有遗漏。
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 watchers
if (vm._watcher) {
vm._watcher.teardown()
}
let i = vm._watchers.length
while (i--) {
vm._watchers[i].teardown()
}
// 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
}
}keepAlive场景下,看到这里涉及 _inactive 的使用
function deactivateChildComponent (vm, direct) {
if (direct) {
vm._directInactive = true;
if (isInInactiveTree(vm)) {
return
}
}
if (!vm._inactive) {
vm._inactive = true;
for (var i = 0; i < vm.$children.length; i++) {
deactivateChildComponent(vm.$children[i]);
}
callHook(vm, 'deactivated');
}
}
这种情况暂时不会真正挂载界面上,因为没有提供挂载点(肯定需要oldVnode存在的)
这种情况就两个步骤
// empty mount (likely as component), create new root element
isInitialPatch = true // 关键
createElm(vnode, insertedVnodeQueue) // 注意:没有提供挂载点:parentElm
// isInitialPatch为true时,会延迟insert hooks执行,指导真正挂载到界面上。
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch) // isInitialPatch: truefunction invokeInsertHook (vnode, queue, initial) {
// delay insert hooks for component root nodes, invoke them after the
// element is really inserted
if (isTrue(initial) && isDef(vnode.parent)) {
vnode.parent.data.pendingInsert = queue
} else {
for (let i = 0; i < queue.length; ++i) {
queue[i].data.hook.insert(queue[i])
}
}
}isInitialPatch情况,延迟当前创建的root component的insert hooks。其中vnode.data.hook.insert如下:
const componentVNodeHooks = {
insert (vnode: MountedComponentVNode) {
const { context, componentInstance } = vnode
if (!componentInstance._isMounted) {
componentInstance._isMounted = true
callHook(componentInstance, 'mounted')
}
if (vnode.data.keepAlive) {
if (context._isMounted) {
// vue-router#1212
// During updates, a kept-alive component's child components may
// change, so directly walking the tree here may call activated hooks
// on incorrect children. Instead we push them into a queue which will
// be processed after the whole patch process ended.
queueActivatedComponent(componentInstance)
} else {
activateChildComponent(componentInstance, true /* direct */)
}
}
},
//...
}mounted生命周期,并设置_isMounted标识组件已经挂载。keepAlive场景,暂不考虑,后面可能单独小节会说。
// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch) // isInitialPatch: falsesameVnode:没什么好说的,看到针对异步加载情况单独加了判断。
function sameVnode (a, b) {
return (
a.key === b.key && (
(
a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)
) || (
isTrue(a.isAsyncPlaceholder) &&
a.asyncFactory === b.asyncFactory &&
isUndef(b.asyncFactory.error)
)
)
)
}patchVnode很重要,后面会单独分析,其目的就是对两个vnode进行对比
// replacing existing element const oldElm = oldVnode.elm const parentElm = nodeOps.parentNode(oldElm)
// create new node // 第三个参数:issue##4590 createElm(vnode, insertedVnodeQueue, oldElm.\_leaveCb ? null : parentElm, nodeOps.nextSibling(oldElm))
// update parent placeholder node element, recursively // ...
// destroy old node if (isDef(parentElm)) { removeVnodes([oldVnode], 0, 0) } else if (isDef(oldVnode.tag)) { invokeDestroyHook(oldVnode) }
```javascriptremoveVnodes删除oldVnode.elm,否则直接调用invokeDestroyHook下面单独说一下 removeVnodes
div,也可能是组件(如todo-item))rm()function removeVnodes (vnodes, startIdx, endIdx) {
for (; startIdx <= endIdx; ++startIdx) {
const ch = vnodes[startIdx]
if (isDef(ch)) {
if (isDef(ch.tag)) {
removeAndInvokeRemoveHook(ch)
invokeDestroyHook(ch)
} else { // Text node
removeNode(ch.elm)
}
}
}
}removeAndInvokeRemoveHook区分了普通节点和组件节点,如果是普通节点如div则直接removeNode移除就好,如果是组件节点(在创建虚拟DOM章节说过,组件标签本身也有关联的虚拟DOM,这里的组件节点就是这个虚拟DOM,并不代表组件的实际渲染内容)则需要触发组件的remove相关的钩子并且递归删除组件的实际内容(实际上就是组件实际渲染内容的根节点,vm._vnode,vm._render方法中执行render函数创建组件的虚拟DOM树,并将根节点保存到vm._vnode。)
function removeAndInvokeRemoveHook (vnode, rm) {
if (isDef(rm) || isDef(vnode.data)) {
let i
const listeners = cbs.remove.length + 1
if (isDef(rm)) {
// we have a recursively passed down rm callback
// increase the listeners count
rm.listeners += listeners
} else {
// directly removing
rm = createRmCb(vnode.elm, listeners)
}
// recursively invoke hooks on child component root node
if (isDef(i = vnode.componentInstance) && isDef(i = i._vnode) && isDef(i.data)) {
removeAndInvokeRemoveHook(i, rm)
}
for (i = 0; i < cbs.remove.length; ++i) {
cbs.remove[i](vnode, rm)
}
if (isDef(i = vnode.data.hook) && isDef(i = i.remove)) {
i(vnode, rm)
} else {
rm()
}
} else {
removeNode(vnode.elm)
}
}function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index) {
if (isDef(vnode.elm) && isDef(ownerArray)) {
// This vnode was used in a previous render!
// now it's used as a new node, overwriting its elm would cause
// potential patch errors down the road when it's used as an insertion
// reference node. Instead, we clone the node on-demand before creating
// associated DOM element for it.
vnode = ownerArray[index] = cloneVNode(vnode)
}
vnode.isRootInsert = !nested // for transition enter check
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
const data = vnode.data
const children = vnode.children
const tag = vnode.tag
if (isDef(tag)) {
vnode.elm = nodeOps.createElement(tag, vnode)
setScope(vnode)
createChildren(vnode, children, insertedVnodeQueue)
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
insert(parentElm, vnode.elm, refElm)
} else if (isTrue(vnode.isComment)) {
vnode.elm = nodeOps.createComment(vnode.text)
insert(parentElm, vnode.elm, refElm)
} else {
vnode.elm = nodeOps.createTextNode(vnode.text)
insert(parentElm, vnode.elm, refElm)
}
}invokeInsertHooks时会用到。(cbs.create是数组是因为会存在多个模块需要处理该元素(主体是模块),而vnode.data.hook.create只是用来处理自身的(主体是自己))组件在更新的时候也要重新创建? 不浪费性能吗?
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */)
}
// after calling the init hook, if the vnode is a child component
// it should've created a child instance and mounted it. the child
// component also has set the placeholder vnode's elm.
// in that case we can just return the element and be done.
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}createComponentInstanceForVnode创建子组件实例,而后调用$mount进行子组件的渲染。(和我们之前的文章new Vue() 整体流程对应上了是不是,整个过程两个大的步骤:实例初始化 + 渲染) // inline hooks to be invoked on component VNodes during patch
const componentVNodeHooks = {
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
if (vnode.componentInstance && !vnode.componentInstance._isDestroyed && vnode.data.keepAlive) {
// keepAlive 场景,暂忽略
} else {
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
}, //... insert、prepatch、destroy}```patch的第二个场景vnode && !oldVnode会创建一个游离组件,组件等待挂载 ,还没有想到这种场景,记个TODO ❎function initComponent (vnode, insertedVnodeQueue) { if (isDef(vnode.data.pendingInsert)) { //... 创建的游离组件情况(暂时没有挂载) } vnode.elm = vnode.componentInstance.$el if (isPatchable(vnode)) { invokeCreateHooks(vnode, insertedVnodeQueue) setScope(vnode) } else { // empty component root. // skip all element-related modules except for ref (#3455) registerRef(vnode) // make sure to invoke the insert hook insertedVnodeQueue.push(vnode) }}```- insert:document.insertBefore/appendChild
- keepAlive场景下,reactivateComponent,不展开。这两个方法和snabbdom中的实现几乎完全一致,可以参考,下面重点说下patchVnode差异部分。
returnreturn这里主要区别是针对组件vnode的处理:updateChildComponent
在之前创建的组件实例中,组件vue实例是保存在vnode.componentInstance中,重新渲染组件实例并不会重新创建,还是复用之前的,但是由于属性值、事件等都可能发生了变化,因此需要更新。虽然组件实例不会重新创建,但是组件标签本身关联的vnode还是会重新创建(新的vnode),并且在_render -> componentComponent 会获取最新的componentOptions,保存到vnode.componentOptions。
因此这里就是将新的vnode.componentOptions更新到oldVnode.componentInstance中。
关注一下props,vm._props是响应式对象(initProps中增强的),这里和initProps调用validateProp之前先设置了toggleObserving(false),考虑到validateProp中的有效调用是getPropDefaultValue,难道是针对它?记个TODO ❎,有专门的提交添加此处的代码,见commit#d6bef795
注意:由于这里给vm._props重新赋值了,因此组件中computed、watch、渲染watcher等订阅的观察者都会触发。
export function updateChildComponent (
vm: Component, propsData: ?Object, listeners: ?Object,
parentVnode: MountedComponentVNode, renderChildren: ?Array<VNode>) {
//... slot 相关,暂且忽略,后面可能小节分析
vm.$options._parentVnode = parentVnode
vm.$vnode = parentVnode // update vm's placeholder node without re-render
if (vm._vnode) { // update child tree's parent
vm._vnode.parent = parentVnode
}
vm.$options._renderChildren = renderChildren
// update $attrs and $listeners hash
// these are also reactive so they may trigger child update if the child
// used them during render
vm.$attrs = parentVnode.data.attrs || emptyObject
vm.$listeners = listeners || emptyObject
// 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)
}
toggleObserving(true)
// keep a copy of raw propsData
vm.$options.propsData = propsData
}
// update listeners
listeners = listeners || emptyObject
const oldListeners = vm.$options._parentListeners
vm.$options._parentListeners = listeners
updateComponentListeners(vm, listeners, oldListeners)
//... slot相关 暂且忽略
}先发一版,后面会以一个有父子组件的demo,并记录执行状态,来更新第五节和本节的内容,并给出更多图示,比如父子组件实例,vnode的相互引用关系。