Vue源码分析—组件化(二)

patch

当我们通过createComponent创建了组件VNode,接下来会走到vm._update,执行vm.__patch__去把VNode转换成真正的DOM节点。但是针对一个普通的VNode节点,接下来我们来看看组件的VNode会有哪些不一样的地方。
patch的过程会调用createElm创建元素节点,回顾一下createElm的实现,它的定义在src/core/vdom/patch.js中:

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

createComponent

我们删掉多余的代码,只保留关键的逻辑,这里会判断createComponent(vnode, insertedVnodeQueue, parentElm, refElm)的返回值,如果为true则直接结束,那么接下来看一下createComponent方法的实现:

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
    }
  }
}

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.parentopts._parentVnode = parentVnode,它们是把之前我们通过createComponentInstanceForVnode函数传入的几个参数合并到内部的选项$options里了。
再来看一下_init函数最后执行的代码:

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

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

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当前组件的渲染vnodevnodeparent指向了_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模块获取,并且作为参数传入的。因为实际上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,在遍历的过程中,如果遇到子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
    }
  }
}

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

总结

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容