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

先从初始渲染看起

当创建Render Watcher时会执行updateComponent函数

updateComponent = () => {
  vm._update(vm._render(), hydrating)
}
复制代码

updateComponent执行vm._render函数,获取 VNode,然后执行vm._update

  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

Vue.prototype.__patch__ = inBrowser ? patch : noop
复制代码

patch函数定义在src/platforms/web/runtime/patch.js

export const patch: Function = createPatchFunction({ nodeOps, modules })
复制代码

运用函数柯里化的方式,将平台特有API在这里区分,而不是在调用时通过if...else判断。

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

比如 directive,向外抛出了createupdatedestroy

export default {
  create: updateDirectives,
  update: updateDirectives,
  : function unbindDirectives (vnode: VNodeWithData) {
    updateDirectives(vnode, emptyNode)
  }
}
复制代码

createPatchFunction

接下来会执行createPatchFunction函数,函数定义在src/core/vdom/patch.js中,createPatchFunction函数内部定义了很多辅助方法,这些方法就暂时不一一列举了,等后面边用边看,先看下主要逻辑

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的数据结构如下

cbs = {
    create: [fn1, fn2, ...],
    update: [fn1, fn2, ...],
    ...
}
复制代码

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

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属性、或者更新过程中新旧节点不同都会走这个逻辑。等节点创建插入完成之后 删除老节点

createElm

  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,继续执行createElm

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

普通标签

首先 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
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的话,创建一个文本节点并插入到父节点

组件标签创建过程

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

先看下createComponent方法

createComponent
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的代码

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实例)。

export function createComponentInstanceForVnode (
  vnode: any,
  parent: any, // activeInstance in lifecycle state
): Component {
  // 给组件vue实例的 options 添加 _isComponent、_parentVnode、parent 属性
  const options: InternalComponentOptions = {
    _isComponent: true,
    _parentVnode: 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__函数,就和上面的流程一样了。

当子组件的挂载过程执行完成后,子组件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中的钩子函数,这些逻辑在其他章节会说。

总结

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

mounted生命周期是如何执行的

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

在执行根组件的 patch 过程中,会执行A组件的$mount方法,进而根组件patch过程停滞,先执行A的patch过程,B组件也是这个流程,A组件的patch过程停滞,执行B的patch过程,执行patch函数中有这样一段逻辑

  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函数

  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])
      }
    }
  }
复制代码

invokeInsertHook中会将insertedVnodeQueue添加到vnode.parent.data.pendingInsert里面,也就是组件占位符VNode的data.pendingInsert中。此时这个insertedVnodeQueue为空数组,因为B组件中没有子组件;到此B组件的DOM树已经创建完成,回到createComponent方法,继续执行A组件的patch过程。

createComponent函数会执行initComponent

  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是有值的,所以isInitialPatchfalse,此时invokeInsertHook会执行insertedVnodeQueue中所有VNode的insert钩子函数;

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方法中有这样的逻辑

  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过程结束后,才能访问到,所以要收集起来统一执行。

大体流程图如下

patch.jpg

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享