Vue 源码(五)patch 过程(一)


highlight: tomorrow-night-eighties

theme: cyanosis

前言

Vue 的 patch 过程分为上下两篇

通过这篇文章可以了解如下内容

  • 父子组件的DOM树是如何建立关联的
  • mounted 生命周期的执行顺序

第一次渲染

当创建Render Watcher时会执行updateComponent函数 javascript updateComponent = () => { vm._update(vm._render(), hydrating) } updateComponent执行vm._render函数,获取 VNode,然后执行vm._update

javascript Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) { const vm: Component = this const prevEl = vm.$el // dom 节点 // 获取老的 VNode const prevVnode = vm._vnode // 设置 activeInstance 并返回一个匿名函数,匿名函数返回值是上一个 activeInstance 的值 const restoreActiveInstance = setActiveInstance(vm) // 当前 Vue 实例的 render 函数创建的 VNode vm._vnode = vnode // Vue.prototype.__patch__ is injected in entry points // based on the rendering backend used. if (!prevVnode) { // 设置 vm.$el // 首次渲染时走这里 vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */) } else { // 更新页面时走这里 vm.$el = vm.__patch__(prevVnode, vnode) } // 将 activeInstance 的值设置成上一个 vm 实例 restoreActiveInstance() if (prevEl) { prevEl.__vue__ = null } if (vm.$el) { vm.$el.__vue__ = vm } if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) { vm.$parent.$el = vm.$el } } vm._update函数先将变量activeInstance设置成当前的组件实例,然后执行vm.__patch__函数;

vm.__patch__函数的作用是创建、渲染、返回节点,并将vm.__patch__的返回值赋值给vm.$elvm.__patch__执行完成后将变量activeInstance设置成上一个组件实例,如果此时是根组件则为 null

接下来就重点介绍vm.__patch__,函数定义在/src/platforms/web/runtime/index.js javascript Vue.prototype.__patch__ = inBrowser ? patch : noop patch函数定义在src/platforms/web/runtime/patch.js javascript export const patch: Function = createPatchFunction({ nodeOps, modules }) 运用函数柯里化的方式,将平台特有API在这里区分,而不是在调用时通过if...else判断。

对于modules其实就是平台特有的一些操作,比如:attrclassstyleevent 等,还有核心的 directiveref,它们会向外暴露一些特有的方法

比如 directive,向外抛出了createupdatedestroy javascript export default { create: updateDirectives, update: updateDirectives, : function unbindDirectives (vnode: VNodeWithData) { updateDirectives(vnode, emptyNode) } }

createPatchFunction

接下来会执行createPatchFunction函数,函数定义在src/core/vdom/patch.js中,createPatchFunction函数内部定义了很多辅助方法,这些方法就暂时不一一列举了,等后面边用边看,先看下主要逻辑 ```javascript const hooks = ['create', 'activate', 'update', 'remove', 'destroy']

export function createPatchFunction (backend) { let i, j const cbs = {} const { modules, nodeOps } = backend // 将 modules 中导出的值都放到 cbs 中 for (i = 0; i < hooks.length; ++i) { cbs[hooks[i]] = [] for (j = 0; j < modules.length; ++j) { if (isDef(modules[j][hooks[i]])) { cbs[hooks[i]].push(modules[j][hooks[i]]) } } }

// ...

return function patch (oldVnode, vnode, hydrating, removeOnly) {}

} `` createPatchFunction函数最终的返回值是patch方法,也就是说vm.patch实际执行的是这里返回的patch方法,而在这之前会从modules中找到相应的方法,添加到cbs,最后cbs`的数据结构如下

bash cbs = { create: [fn1, fn2, ...], update: [fn1, fn2, ...], ... }

cbs中的这些方法在 patch 不同阶段调用,做相应的操作,比如创建指令等;cbs初始化完成之后会调用patch方法

```javascript return function patch (oldVnode, vnode, hydrating, removeOnly) { // 新节点不存在,老节点存在,销毁老节点 if (isUndef(vnode)) { if (isDef(oldVnode)) invokeDestroyHook(oldVnode) return }

let isInitialPatch = false
const insertedVnodeQueue = []
// VNode是组件的渲染VNode,并且是第一次渲染
if (isUndef(oldVnode)) {
  // 新节点存在,老节点不存在(首次渲染组件时会出现这种情况)
  isInitialPatch = true
  createElm(vnode, insertedVnodeQueue)
} else {
  // 判断 oldVnode 是否为真实节点
  const isRealElement = isDef(oldVnode.nodeType)
  // oldVnode 不是真实元素并且 oldVnode 和 VNode 是同一个节点,则执行 patchVnode
  if (!isRealElement && sameVnode(oldVnode, vnode)) {
    patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
  } else {
    // oldVnode 是真实节点
    if (isRealElement) {
      // 根据 oldVnode(此时 oldVnode 是真实节点) 创建一个 VNode
      oldVnode = emptyNodeAt(oldVnode)
    }

    // 获取节点的 真实元素
    const oldElm = oldVnode.elm
    // 获取 oldVnode 的 父节点
    const parentElm = nodeOps.parentNode(oldElm)

    // create new node
    createElm(
      vnode,
      insertedVnodeQueue,
      oldElm._leaveCb ? null : parentElm,
      nodeOps.nextSibling(oldElm)
    )

    // 更新 组件vnode 的 elm 并重新执行父组件的 cbs.create 和 insert hooks(不包含 mounted 钩子)
    if (isDef(vnode.parent)) {
      // ...
    }

    // destroy old node
    if (isDef(parentElm)) {
      removeVnodes([oldVnode], 0, 0)
    } else if (isDef(oldVnode.tag)) {
      invokeDestroyHook(oldVnode)
    }
  }
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm

} ```

首先 patch函数会判断新节点是否存在,如果新节点不存在,老节点存在,调用 destroy,销毁老节点;反之,判断是否存在老节点:

  • 如果不存在则调用createElm方法去创建并插入节点,比如组件的 渲染VNode 初次渲染时,会走这个逻辑。
  • 如果存在,判断老节点是否为真实节点;
    • 如果不是真实节点并且新旧节点相同,说明是更新过程,调用patchVnode函数;
    • 如果上述判断没有成立,继续判断老节点是不是真实节点,如果是,根据oldVnode创建一个VNode对象并赋值给oldVnode。然后获取oldVnode的真实节点和父节点 (如果oldVnodenull,则获取的真实节点和父节点都为null);
    • 接下来调用createElm方法去创建并插入节点,这里的createElm方法会传入父节点和oldVnode的相邻节点;比如根实例设置了el属性、或者更新过程中新旧节点不同都会走这个逻辑。等节点创建插入完成之后 删除老节点

patch函数的主要作用就是调用createElm创建并返回真实DOM

createElm

```javascript function createElm ( vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index ) { vnode.isRootInsert = !nested // for transition enter check if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) { return } // 获取 data 对象 const data = vnode.data // 获取 children const children = vnode.children // 节点标签 const tag = vnode.tag if (isDef(tag)) { if (process.env.NODE_ENV !== 'production') { // ... } // 创建 dom 节点 vnode.elm = vnode.ns ? nodeOps.createElementNS(vnode.ns, tag) : nodeOps.createElement(tag, vnode) setScope(vnode)

if (__WEEX__) {
    // ...
  } else {
    // 递归创建所有子节点,生成整颗 dom 树
    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)
}

} ```

createElm基于 VNode 创建整棵 DOM 树,并插入到父节点上。

createElm中会先调用createComponent方法,判断传入VNode的类型,如果传入的是组件占位符VNode,则执行创建组件的逻辑,返回true;如果传入的是普通VNode,返回false,通过createChildren创建子元素DOM;整个组件的DOM树创建完成之后将其插入传入的父节点中。

这里分别说一下普通标签和组件标签的 patch 过程

普通标签DOM创建过程

首先 createElm会调用createComponent方法,因为是一个普通VNode,返回false。接下来获取datatagchildren;然后判断tag是否为空:

  • 如果tag为空,并且vnode.isCommenttrue,说明是一个注释VNode,创建注释节点,插入到父节点中
  • 如果tag为空,并且vnode.isCommentfalse,说明是一个文本VNode,创建文本节点,插入到父节点中
  • 如果 tag不为空,调用nodeOps.createElement创建节点,并赋值给vnode.elm,然后调用createChildren创建子节点,将子节点插入到vnode.elm;子节点插入完成后将vnode.elm插入到父节点
createChildren

javascript function createChildren (vnode, children, insertedVnodeQueue) { if (Array.isArray(children)) { if (process.env.NODE_ENV !== 'production') { // 检测这组节点的 key 是否重复 checkDuplicateKeys(children) } for (let i = 0; i < children.length; ++i) { createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i) } } else if (isPrimitive(vnode.text)) { // 说明是文本节点,创建文本节点,并插入父节点 nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text))) } }

如果children是一个数组, 遍历children,调用createElm依次创建这些节点并插入父节点中。如果不是一个数组,并且是一个文本VNode的话,创建一个文本节点并插入到父节点

组件标签DOM创建过程

如果当前的VNode是组件占位符VNode,在 createElm 中调用createComponent方法,会执行创建组件的过程,返回true

先看下createComponent方法

createComponent

javascript function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) { let i = vnode.data if (isDef(i)) { // 判断组件实例是否已经存在, 并且组件被 keep-alive 包裹 const isReactivated = isDef(vnode.componentInstance) && i.keepAlive if (isDef(i = i.hook) && isDef(i = i.init)) { // 执行 组件的 init 钩子函数 i(vnode, false /* hydrating */) } if (isDef(vnode.componentInstance)) { // 将 渲染vnode 的 $el 属性赋值给 组件vnode 的 elm 属性 initComponent(vnode, insertedVnodeQueue) insert(parentElm, vnode.elm, refElm) if (isTrue(isReactivated)) { reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm) } return true } } }

在创建组件占位符VNode时,给vnode.data添加了几个钩子函数,其中就包含init钩子,所以createComponent会先判断vnode.data上有没有init钩子函数如果有则执行这个钩子函数,看下init的代码

javascript const componentVNodeHooks = { init (vnode: VNodeWithData, hydrating: boolean): ?boolean { if ( vnode.componentInstance && !vnode.componentInstance._isDestroyed && vnode.data.keepAlive ) { // ... } else { const child = vnode.componentInstance = createComponentInstanceForVnode( vnode, activeInstance ) child.$mount(hydrating ? vnode.elm : undefined, hydrating) } }, prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {}, insert (vnode: MountedComponentVNode) {}, destroy (vnode: MountedComponentVNode) {} }

init方法的if是针对于keep-alive的,这个后期会说,现在就看else的逻辑就行,else里面会执行createComponentInstanceForVnode方法,并传入vnodeactiveInstance(当前vm实例)。

javascript export function createComponentInstanceForVnode ( vnode: any, parent: any, // activeInstance in lifecycle state ): Component { // 给组件vue实例的 options 添加 _isComponent、_parentVnode、parent 属性 const options: InternalComponentOptions = { _isComponent: true, _parentVnode: vnode, // 组件占位符 VNode parent } const inlineTemplate = vnode.data.inlineTemplate if (isDef(inlineTemplate)) { options.render = inlineTemplate.render options.staticRenderFns = inlineTemplate.staticRenderFns } return new vnode.componentOptions.Ctor(options) }

createComponentInstanceForVnode方法首先会定义一个options对象,并添加属性

  • _isComponent:是否为组件
  • _parentVnode:组件的占位符VNode
  • parent:父组件实例

然后执行return new vnode.componentOptions.Ctor(options),在创建组件占位符VNode时,通过Vue.extend方法创建了一个子组件的构造函数,并将他存到了vnode.componentOptions.Ctor里面,所以这里会为子组件创建并返回一个Vue实例。也就是说他会执行一系列的初始化操作等

回到init方法,将createComponentInstanceForVnode方法返回的Vue实例赋值给vnode.componentInstance,也就是说 组件占位符VNode 的componentInstance属性指向子组件实例,创建了子组件实例后会执行child.$mount(hydrating ? vnode.elm : undefined, hydrating),调用 $mount 方法挂载子组件,在这个过程中会创建子组件的Render Watcher然后执行子组件的render函数创建组件的渲染VNode,并做依赖收集,接着执行_update函数、__patch__函数,就和上面的流程一样了。

javascript vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)

当子组件的挂载过程执行完成后,子组件Vue实例的$el属性就是子组件的 DOM树;回到createComponent方法,执行initComponent函数,initComponent函数内会执行vnode.elm = vnode.componentInstance.$el将子组件的DOM树赋值给组件占位符VNode 的elm属性,然后执行insert(parentElm, vnode.elm, refElm),将DOM树插入父元素中,并返回true,接着回到createElm中,因为createComponent返回true,所以不会继续向下执行。initComponent方法不是只有这一点逻辑,还会执行cbs中的钩子函数,这些逻辑在其他章节会说。

总结

父子组件的DOM树是如何建立关联的

当组件创建Render Watcher时,执行render函数获取组件的渲染VNode;然后执行_update函数,_update函数内执行patch函数创建节点并插入到DOM中;如果组件中有子组件,调用组件占位符VNode的init钩子函数,为子组件创建Vue实例,执行子组件的$mount方法创建Render Watcher,并对子组件执行上述流程;等子组件执行完成之后将子组件的DOM树挂载到组件占位符VNode的elm上,并将其插入到父元素中或相邻元素前后。这样父子组件的DOM树就关联起来了。

大体流程图如下

patch.jpg

mounted生命周期是如何执行的

假设有3个组件分别是,根组件、A组件、B组件,他们的关系是 根组件是A的父组件,AB的父组件

在执行根组件的 patch 过程中,会执行A组件的$mount方法,进而根组件 patch 过程停滞,先执行A的 patch 过程,B组件也是这个流程,A组件的 patch 过程停滞,执行B的 patch 过程,执行patch函数中有这样一段逻辑 javascript return function patch (oldVnode, vnode, hydrating, removeOnly) { let isInitialPatch = false const insertedVnodeQueue = [] if (isUndef(oldVnode)) { isInitialPatch = true createElm(vnode, insertedVnodeQueue) } else {} invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch) return vnode.elm }

创建一个isInitialPatch变量和一个insertedVnodeQueue数组,根据组件的渲染VNode创建DOM树时,传入的oldVnodenull,会将isInitialPatch设置成true,渲染VNode的DOM树创建完成后,会执行invokeInsertHook函数 javascript function invokeInsertHook (vnode, queue, initial) { if (isTrue(initial) && isDef(vnode.parent)) { // 每个组件的渲染 VNode 才会执行,vnode.parent 指向 组件VNode vnode.parent.data.pendingInsert = queue } else { // 执行 insertedVnodeQueue 中所有 VNode 的 insert hook for (let i = 0; i < queue.length; ++i) { queue[i].data.hook.insert(queue[i]) } } } 如果当前VNode是组件的渲染 VNode 并且没有老节点,invokeInsertHook会将insertedVnodeQueue添加到vnode.parent.data.pendingInsert里面,也就是组件占位符VNode的data.pendingInsert中。此时这个insertedVnodeQueue为空数组,因为B组件中没有子组件;到此B组件的DOM树已经创建完成,回到createComponent方法,继续执行A组件的 patch 过程。

createComponent函数会执行initComponent

javascript function initComponent (vnode, insertedVnodeQueue) { if (isDef(vnode.data.pendingInsert)) { insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert) vnode.data.pendingInsert = null } // 将 渲染vnode 的 $el 属性赋值给 组件vnode 的 elm 属性 vnode.elm = vnode.componentInstance.$el if (isPatchable(vnode)) { invokeCreateHooks(vnode, insertedVnodeQueue) setScope(vnode) } else { registerRef(vnode) insertedVnodeQueue.push(vnode) } } 此时的vnode参数是B组件的占位符VNode,将vnode.data.pendingInsert添加到insertedVnodeQueue里面;然后给B组件的占位符VNode设置elm属性;判断B组件是不是一个空组件,不管是不是都会执行insertedVnodeQueue.push(vnode);也就是说将B组件的占位符VNode添加到insertedVnodeQueue里面。执行到patch函数时,这里和B组件一样isInitialPatch也是true,所以执行invokeInsertHook时,会将insertedVnodeQueue赋值给A组件占位符VNode的data.pendingInsert

A组件的 patch 过程结束,回到根组件的 patch 过程中,继续执行createComponentcreateComponent内执行initComponent;将A组件占位符VNode的data.pendingInsert添加到insertedVnodeQueue里面;此时insertedVnodeQueue有两个元素分别是B组件的占位符VNode和A组件的占位符VNode;当根组件的patch函数执行invokeInsertHook时,就会走else逻辑,因为根组件的patch函数传入的oldVnode是有值的(真实节点,比如#app),所以isInitialPatchfalse,此时invokeInsertHook会执行insertedVnodeQueue中所有VNode的insert钩子函数;

```javascript const componentVNodeHooks = { init (vnode: VNodeWithData, hydrating: boolean): ?boolean {}, prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {},

insert (vnode: MountedComponentVNode) { const { context, componentInstance } = vnode if (!componentInstance.isMounted) { componentInstance.isMounted = true callHook(componentInstance, 'mounted') } if (vnode.data.keepAlive) {} }, destroy (vnode: MountedComponentVNode) {} } `` 在创建组件的占位符VNode时,会挂载一个insert钩子函数。这个钩子函数会给组件实例设置_isMountedtrue,代表已经挂载完成,然后执行组件的mounted`生命周期

当根组件的 patch 过程结束后,Render Watcher也创建完成了,回到mountComponent方法,mountComponent方法中有这样的逻辑 javascript if (vm.$vnode == null) { // 表示此组件已经挂载完成 vm._isMounted = true // 根组件的 mounted 函数 先子后父 callHook(vm, 'mounted') } 根组件实例的$vnode指向null,会执行根组件的mounted生命周期函数,并将根组件实例的_isMounted属性设置为true

结论

也就是说在整个 patch 过程中,会将组件占位符VNode收集起来,等 patch 过程结束时,执行所有VNode的insert钩子函数,顺序是先子后父,其实这里不光会收集组件占位符VNode,还会收集有指令的VNode,前提是指令绑定了insert钩子。

如果每次在组件 patch 结束后执行当前组件的insert钩子,由于还没有将组件的DOM树渲染到页面上,所以访问不到DOM节点;只有整个 patch 过程结束后,才能访问到,所以要收集起来统一执行。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值