Vue源码学习 - 组件化(二) patch

Vue源码学习 - 组件化(二) patch

学习内容和文章内容来自 黄轶老师
黄轶老师的慕课网视频教程地址:《Vue.js2.0 源码揭秘》
黄轶老师拉钩教育教程地址:《Vue.js 3.0 核心源码解析》
这里分析的源码是Runtime + Compiler 的 Vue.js
调试代码在:node_modules\vue\dist\vue.esm.js 里添加
vue版本:Vue.js 2.5.17-beta

你越是认真生活,你的生活就会越美好——弗兰克·劳埃德·莱特
《人生果实》经典语录

点击回到 Vue源码学习完整目录

patch

通过前一章的分析我们知道,当我们通过createComponent 创建了组件 VNode,接下来会走到 vm._update,执行 vm.__patch__把 VNode 转换成真正的 DOM 节点。这个过程我们在前一章已经分析过了,但是针对一个普通的 VNode 节点,接下来我们来看看组件的 VNode 会有哪些不一样的地方。

重点

  • patch的整体流程: createComponent => 子组件初始化 => 子组件render => 子组件patch
  • activeInstance为当前激活的vm实例 vm.$vnode为组件的占位vnode vm._vnode为组件的渲染vnode
  • 嵌套组件的插入顺序 先子后父

debugger添加地方
在这里插入图片描述

patch 的过程会调用 createElm 创建元素节点,回顾一下 createElm的实现,它的定义在 src/core/vdom/patch.js 中:

function createElm (
  vnode,
  insertedVnodeQueue,
  parentElm,
  refElm,
  nested,
  ownerArray,
  index
) {
    
    
  // ...
  if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    
    
    return
  }
  // ...
}

在这里插入图片描述
src/App.vue

<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png">
    <HelloWorld msg="Welcome to Your Vue.js App"/>
  </div>
</template>

<script>
import HelloWorld from './components/HelloWorld.vue'

export default {
     
     
  name: 'App',
  components: {
     
     
    HelloWorld
  }
}
</script>

<style>
#app {
     
     
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

createComponent

我们删掉多余的代码,只保留关键的逻辑,这里会判断 createComponent(vnode, insertedVnodeQueue, parentElm, refElm)的返回值,如果为 true 则直接结束,那么接下来看一下 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)) {
    
    
      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
    }
  }
}

在这里插入图片描述

createComponent 函数中,首先对vnode.data 做了一些判断:

let i = vnode.data
if (isDef(i)) {
    
    
  // ...
  if (isDef(i = i.hook) && isDef(i = i.init)) {
    
    
    i(vnode, false /* hydrating */)
    // ...
  }
  // ..
}

如果 vnode 是一个组件 VNode,那么条件会满足,并且得到i就是 init 钩子函数,回顾上节我们在创建组件 VNode的时候合并钩子函数中就包含init 钩子函数,定义在 src/core/vdom/create-component.js 中:

init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
    
    
  if (
    vnode.componentInstance &&
    !vnode.componentInstance._isDestroyed &&
    vnode.data.keepAlive
  ) {
    
    
    // kept-alive components, treat as a patch
    const mountedNode: any = vnode // work around flow
    componentVNodeHooks.prepatch(mountedNode, mountedNode)
  } else {
    
    
  // 走到这里
    const child = vnode.componentInstance = createComponentInstanceForVnode(
      vnode,
      activeInstance
    )
    child.$mount(hydrating ? vnode.elm : undefined, hydrating)
  }
},

init 钩子函数执行也很简单,我们先不考虑 keepAlive的情况,它是通过 createComponentInstanceForVnode 创建一个 Vue 的实例,然后调用 $mount 方法挂载子组件, 先来看一下 createComponentInstanceForVnode的实现:

export function createComponentInstanceForVnode (
  vnode: any, // we know it's MountedComponentVNode but flow doesn't
  parent: any, // activeInstance in lifecycle state
): Component {
    
    
  const options: InternalComponentOptions = {
    
    
    _isComponent: true,
    _parentVnode: vnode,
    parent
  }
  // check inline-template render functions
  const inlineTemplate = vnode.data.inlineTemplate
  if (isDef(inlineTemplate)) {
    
    
    options.render = inlineTemplate.render
    options.staticRenderFns = inlineTemplate.staticRenderFns
  }
  return new vnode.componentOptions.Ctor(options)
}

在这里插入图片描述
createComponentInstanceForVnode函数构造的一个内部组件的参数,然后执行 new vnode.componentOptions.Ctor(options)。这里的vnode.componentOptions.Ctor 对应的就是子组件的构造函数
我们上一节分析了它实际上是继承于 Vue 的一个构造器 Sub,相当于 new Sub(options)这里有几个关键参数要注意几个点,
_isComponenttrue表示它是一个组件,
parent 表示当前激活的组件实例(注意,这里比较有意思的是如何拿到组件实例,后面会介绍。
在这里插入图片描述

所以子组件的实例化实际上就是在这个时机执行的,并且它会执行实例的_init方法,这个过程有一些和之前不同的地方需要挑出来说,代码在src/core/instance/init.js 中:

Vue.prototype._init = function (options?: Object) {
    
    
  const vm: Component = this
  // merge options
  if (options && options._isComponent) {
    
    
    // optimize internal component instantiation
    // since dynamic options merging is pretty slow, and none of the
    // internal component options needs special treatment.
    initInternalComponent(vm, options)
  } else {
    
    
    vm.$options = mergeOptions(
      resolveConstructorOptions(vm.constructor),
      options || {
    
    },
      vm
    )
  }
  // ...
  if (vm.$options.el) {
    
    
    vm.$mount(vm.$options.el)
  } 
}

在这里插入图片描述

这里首先是合并 options 的过程有变化,_isComponenttrue,所以走到了 initInternalComponent过程,这个函数的实现也简单看一下:

export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
    
    
  const opts = vm.$options = Object.create(vm.constructor.options)
  // doing this because it's faster than dynamic enumeration.
  const parentVnode = options._parentVnode
  opts.parent = options.parent
  opts._parentVnode = parentVnode

  const vnodeComponentOptions = parentVnode.componentOptions
  opts.propsData = vnodeComponentOptions.propsData
  opts._parentListeners = vnodeComponentOptions.listeners
  opts._renderChildren = vnodeComponentOptions.children
  opts._componentTag = vnodeComponentOptions.tag

  if (options.render) {
    
    
    opts.render = options.render
    opts.staticRenderFns = options.staticRenderFns
  }
}

在这里插入图片描述

这个过程我们重点记住以下几个点即可:
opts.parent = options.parent
opts._parentVnode = parentVnode
它们是把之前我们通过createComponentInstanceForVnode 函数传入的几个参数合并到内部的选项 $options 里了。

initLifecycle(vm)方法 单步调试时 也可以看看
在这里插入图片描述

在这里插入图片描述

再来看一下 _init函数最后执行的代码:

if (vm.$options.el) {
    
    
   vm.$mount(vm.$options.el)
}

在这里插入图片描述

在这里插入图片描述
由于组件初始化的时候是不传 el 的,因此组件是自己接管了$mount 的过程,这个过程的主要流程在上一章介绍过了,回到组件init的过程,componentVNodeHooksinit 钩子函数,在完成实例化的_init后,接着会执行
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
这里 hydrating 为 true 一般是服务端渲染的情况,我们只考虑客户端渲染,所以这里$mount相当于执行 child.$mount(undefined, false),它最终会调用 mountComponent方法,进而执行 vm._render()方法:

mountComponent方法会走到vm._update(vm._render(), hydrating)方法
在这里插入图片描述

Vue.prototype._render = function (): VNode {
    
    
  const vm: Component = this
  const {
    
     render, _parentVnode } = vm.$options

  
  // set parent vnode. this allows render functions to have access
  // to the data on the placeholder node.
  vm.$vnode = _parentVnode
  // render self
  let vnode
  try {
    
    
    vnode = render.call(vm._renderProxy, vm.$createElement)
  } catch (e) {
    
    
    // ...
  }
  // set parent
  vnode.parent = _parentVnode
  return vnode
}

我们只保留关键部分的代码,这里的_parentVnode 就是当前组件的父 VNode,而 render函数生成的 vnode当前组件的渲染 vnodevnode 的 parent 指向了 _parentVnode,也就是 vm.$vnode,它们是一种父子的关系。
在这里插入图片描述
我们知道在执行完 vm._render生成 VNode 后,接下来就要执行 vm._update 去渲染 VNode 了。来看一下组件渲染的过程中有哪些需要注意的,vm._update 的定义在 src/core/instance/lifecycle.js 中:

export let activeInstance: any = null
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    
    
  const vm: Component = this
  const prevEl = vm.$el
  const prevVnode = vm._vnode
  const prevActiveInstance = activeInstance
  activeInstance = 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)
  }
  activeInstance = prevActiveInstance
  // 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.
}

_update 过程中有几个关键的代码,首先 vm._vnode = vnode的逻辑,这个 vnode 是通过 vm._render() 返回的组件渲染 VNodevm._vnodevm.$vnode 的关系就是一种父子关系,用代码表达就是 vm._vnode.parent === vm.$vnode。还有一段比较有意思的代码:

export let activeInstance: any = null
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    
    
    // ...
    const prevActiveInstance = activeInstance
    activeInstance = vm
    if (!prevVnode) {
    
    
      // initial render
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
    
    
      // updates
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    activeInstance = prevActiveInstance
    // ...
}

这个activeInstance作用就是保持当前上下文的 Vue 实例,它是在lifecycle 模块的全局变量,定义是 export let activeInstance: any = null,并且在之前我们调用 createComponentInstanceForVnode 方法的时候从lifecycle模块获取,并且作为参数传入的。

PS:
activeInstance为当前激活的vm实例 vm.$vnode为组件的占位vnode vm._vnode为组件的渲染vnode

因为实际上 JavaScript 是一个单线程Vue 整个初始化是一个深度遍历的过程,在实例化子组件的过程中,它需要知道当前上下文的 Vue 实例是什么,并把它作为子组件的父 Vue 实例
之前我们提到过对子组件的实例化过程先会调用initInternalComponent(vm, options)合并 options,把 parent 存储在 vm.$options 中,在 $mount 之前会调用initLifecycle(vm)方法:

export function initLifecycle (vm: Component) {
    
    
  const options = vm.$options

  // locate first non-abstract parent
  let parent = options.parent
  if (parent && !options.abstract) {
    
    
    while (parent.$options.abstract && parent.$parent) {
    
    
      parent = parent.$parent
    }
    parent.$children.push(vm)
  }

  vm.$parent = parent
  // ...
}

可以看到 vm.$parent 就是用来保留当前 vm 的父实例,并且通过 parent.$children.push(vm)来把当前的 vm存储到父实例的 $children 中。

vm._update 的过程中,把当前的 vm 赋值给 activeInstance,同时通过 const prevActiveInstance = activeInstanceprevActiveInstance 保留上一次的 activeInstance

实际上,prevActiveInstance当前的 vm 是一个父子关系,当一个vm实例完成它的所有子树的 patch或者 update过程后,activeInstance 会回到它的父实例,这样就完美地保证了 createComponentInstanceForVnode 整个深度遍历过程中,我们在实例化子组件的时候能传入当前子组件的父 Vue 实例,并在_init 的过程中,通过 vm.$parent 把这个父子关系保留。

那么回到_update,最后就是调用__patch__渲染VNode了。

vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
 
function patch (oldVnode, vnode, hydrating, removeOnly) {
    
    
  // ...
  let isInitialPatch = false
  const insertedVnodeQueue = []

  if (isUndef(oldVnode)) {
    
    
    // empty mount (likely as component), create new root element
    isInitialPatch = true
    createElm(vnode, insertedVnodeQueue)
  } else {
    
    
    // ...
  }
  // ...
}

这里又回到了本节开始的过程,之前分析过负责渲染成 DOM 的函数是 createElm,注意这里我们只传了 2 个参数,所以对应的 parentElmundefined。我们再来看看它的定义:

function createElm (
  vnode,
  insertedVnodeQueue,
  parentElm,
  refElm,
  nested,
  ownerArray,
  index
) {
    
    
  // ...
  if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    
    
    return
  }

  const data = vnode.data
  const children = vnode.children
  const tag = vnode.tag
  if (isDef(tag)) {
    
    
    // ...

    vnode.elm = vnode.ns
      ? nodeOps.createElementNS(vnode.ns, tag)
      : nodeOps.createElement(tag, vnode)
    setScope(vnode)

    /* istanbul ignore if */
    if (__WEEX__) {
    
    
      // ...
    } else {
    
    
      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)
  }
}

注意,这里我们传入的vnode组件渲染的 vnode,也就是我们之前说的vm._vnode
如果组件的根节点是个普通元素,那么 vm._vnode 也是普通的 vnode,这里 createComponent(vnode, insertedVnodeQueue, parentElm, refElm)的返回值是false

接下来的过程就和我们上一章一样了,先创建一个父节点占位符,然后再遍历所有子 VNode 递归调用 createElm(createElm方法渲染生成DOM节点),在遍历的过程中,如果遇到子 VNode是一个组件的 VNode,则重复本节开始的过程,这样通过一个递归的方式就可以完整地构建了整个组件树

由于我们这个时候传入的parentElm是空,所以对组件的插入,在createComponent有这么一段逻辑:

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
    
    
  let i = vnode.data
  if (isDef(i)) {
    
    
    // ....
    if (isDef(i = i.hook) && isDef(i = i.init)) {
    
    
      i(vnode, false /* hydrating */)
    }
    // ...
    if (isDef(vnode.componentInstance)) {
    
    
      initComponent(vnode, insertedVnodeQueue)
      insert(parentElm, vnode.elm, refElm)
      if (isTrue(isReactivated)) {
    
    
        reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
      }
      return true
    }
  }
}
 function insert (parent, elm, ref$$1) {
    
    
   if (isDef(parent)) {
    
    
     if (isDef(ref$$1)) {
    
    
       if (nodeOps.parentNode(ref$$1) === parent) {
    
    
         nodeOps.insertBefore(parent, elm, ref$$1);
       }
     } else {
    
    
       nodeOps.appendChild(parent, elm);
     }
   }
 }

在这里插入图片描述

function insertBefore (parentNode, newNode, referenceNode) {
    
    
  parentNode.insertBefore(newNode, referenceNode);
}

在这里插入图片描述
先将HelloWorld组件插入到ID为app的Dom节点上 然后再将ID为app的Dom节点插入到父节点上 也就是body节点上

在完成组件的整个 patch 过程后,最后执行insert(parentElm, vnode.elm, refElm)完成组件的 DOM 插入,如果组件 patch 过程中又创建了子组件,那么DOM 的插入顺序先子后父

重点

  • patch的整体流程: createComponent => 子组件初始化 => 子组件render => 子组件patch
  • activeInstance为当前激活的vm实例 vm.$vnode为组件的占位vnode vm._vnode为组件的渲染vnode
  • 嵌套组件的插入顺序 先子后父

总结

那么到此,一个组件的 VNode如何创建、初始化、渲染的过程也就介绍完毕了。在对组件化的实现有一个大概了解后,接下来我们来介绍一下这其中的一些细节。
我们知道编写一个组件实际上是编写一个 JavaScript 对象,对象的描述就是各种配置,之前我们提到在_init的最初阶段执行的就是merge options 的逻辑,那么下一节我们从源码角度来分析合并配置的过程。

Vue源码学习 - 组件化(一) createComponent
Vue源码学习 - 组件化(三) 配置合并

点击回到 Vue源码学习完整目录


谢谢你阅读到了最后~
期待你关注、收藏、评论、点赞~
让我们一起 变得更强

猜你喜欢

转载自blog.csdn.net/weixin_42752574/article/details/113444596