[vue解析]当我们注册组件的时候,vue内部是如何运行的

605 阅读6分钟

前言

这是vue2.0解析的第四篇了,该篇文章的难点全在patch上了,组件注册并没有什么难度。

文章链接

vue解析:data

vue解析:computed

vue解析:watch

组件是vue的核心,从官方文档看组件有两种注册方式:全局和局部。那么我们先来看全局注册,先来一个例子

<div id="app">
  <div>这是一个div</div>
  <comp-a></comp-a>
</div>
<script>
  Vue.component('comp-a', {
    template: '<div>这个一个子组件</div>'
  })
  const vm = new Vue({
    el: '#app',
  })
</script>

关于全局注册,我们先要了解vue初始化过程中,是如何挂载方法的。在core/instance/index.js文件中,通过组合的方法在Vue.prototype上挂载了大量方法 通过方法名就能大概猜到是做什么的。

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

然后在core/index.js中调用了initGlobalAPI(Vue),该方法定义在core/global-api/index.js中,它在Vue上挂载了很多方法,也就是静态方法。 那么它到底做了什么呢?我们来看看关于本例子的核心实现

export function initGlobalAPI (Vue: GlobalAPI) {

  Vue.options = Object.create(null)
  ASSET_TYPES.forEach(type => {
    Vue.options[type + 's'] = Object.create(null)
    })

  // this is used to identify the "base" constructor to extend all plain-object
  // components with in Weex's multi-instance scenarios.
  Vue.options._base = Vue

  extend(Vue.options.components, builtInComponents)

  initExtend(Vue)
  initAssetRegisters(Vue)
}

中间这个forEach循环直接看结果,我们给每个现有对象的__proto__指向了空。并且通过extendkeep-alive混合到了components中。 然后通过initAssetRegisters进行注册。在这个方法中关于components就是

Vue['component'] = function(id, definition) {
  definition.name = definition.name || id
  definition = this.options._base.extend(definition)
  return definition
}

那么在Vue中就可以总结为

Vue.options = {
	components: {
		KeepAlive
	},
	directives: Object.create(null),
	filters: Object.create(null),
	_base: Vue
}
Vue.extend = function() {}
Vue.component = function(id, definition) {}

全局注册的初始化

首先看例子,在new Vue之前,我们使用Vue.component去定义了全局组件,那么Vue调用的就是初始化的静态方法Vue.component。而这个方法其实就一行核心代码 this.options._base.extend(definition)this.options._baseinitGlobalAPI中有定义就是Vue。那么这里我们就是调用的Vue.extend初始化了 组件,好,我们进Vue.extend看看

Vue.extend = function (extendOptions: Object): Function {
  extendOptions = extendOptions || {}
  const Super = this
  const SuperId = Super.cid
  const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
  if (cachedCtors[SuperId]) {
    return cachedCtors[SuperId]
  }
  const Sub = function VueComponent (options) {
    this._init(options)
  }
  Sub.prototype = Object.create(Super.prototype)
  Sub.prototype.constructor = Sub
  Sub.cid = cid++
  Sub.options = mergeOptions(
    Super.options,
    extendOptions
  )
  Sub['super'] = Super

  ASSET_TYPES.forEach(function (type) {
    Sub[type] = Super[type]
  })
  // enable recursive self-lookup
  if (name) {
    Sub.options.components[name] = Sub
  }

  Sub.superOptions = Super.options
  Sub.extendOptions = extendOptions
  Sub.sealedOptions = extend({}, Sub.options)

  // cache constructor
  cachedCtors[SuperId] = Sub
  return Sub
}

该方法其实和init方法一样重要,是组件的创建实例方法,但关于本例子我们也只看其核心。然后总结它干了些啥。

  1. Vue赋值给了Super
  2. 做了一个缓存的处理,比如我们在一个父组件中使用两次同一个组件,那么之后返回的就是缓存不需要再次创建实例方法
  3. 创建了Sub实例方法,将该方法的__proto指向了Vue的原型,将原型的构造函数指向它自己,标准的原型继承
  4. 合并选项,将父组件中的components和子组件的传入选项合并到一起,赋值给子组件选项;注意:父组件的assets是被放在子组件的原型上的,具体可以去看mergeAssets方法 也是使用了Object.create
  5. 初始化propscomputed
  6. 初始化一些其他方法
  7. 返回构造函数

那么Vue.component就执行结束了,之后开始new Vue的流程

和之前一样,new Vue初始化会创建渲染Watcher,在调用watcher里的get函数的时候,就是调用传入的第二个参数。就是updateComponent方法, 就是会执行vm._update(vm._render(), hydrating)。该方法分为两步,第一步执行render函数生成vnode。第二步通过vm.updatevnode渲染到页面。

那么直接看vm._render的核心,vnode = render.call(vm._renderProxy, vm.$createElement),在这里打个断点,就能拿到我们例子的匿名执行函数

;(function anonymous () {
  with (this) {
    return _c(
      'div',
      { attrs: { id: 'app' } },
      [_c('div', [_v('这是一个div')]), _v(' '), _c('comp-a')],
      1
    )
  }
})

之后我们看执行_c(comp-a)。之前我们提到过_c,还记得吗?在initRender中,有这么一个声明vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)。 很显然我们这时候调用的就是createElement

export function createElement (
  context: Component,
  tag: any,
  data: any,
  children: any,
  normalizationType: any,
  alwaysNormalize: boolean
): VNode | Array<VNode> {
  if (Array.isArray(data) || isPrimitive(data)) {
    normalizationType = children
    children = data
    data = undefined
  }
  if (isTrue(alwaysNormalize)) {
    normalizationType = ALWAYS_NORMALIZE
  }
  return _createElement(context, tag, data, children, normalizationType)
}

在这个方法中,判断了data,我们这里只传入了前两个参数,第一个是vm,第二个就是comp-a。那么就不会执行中间的赋值步骤,而且最后一个值为false。什么时候它为true。 当我们使用render函数而不是template的时候,就会触发这个函数了,现在知道就好。我们来看_createElement方法。该方法在core/vnode/create-element.js

export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
  if (typeof tag === 'string') {
    var Ctor;
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag);
    // 如果是保留标签
    if (config.isReservedTag(tag)) {
      // 创建 vnode
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      );
      // 如果data不存在,且 components存在,是组件
    } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      // component
      // 创建子组件
      vnode = createComponent(Ctor, data, context, children, tag);
    } else {
      vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
      );
     }
  } else {
    vnode = createComponent(tag, data, context, children);
  }
}

这个方法非常重要是用来创建Vnode的核心方法,删除判断代码,核心就是上面的逻辑

  1. tag是字符串分三种情况
    1. 保留标签直接创建Vnode
    2. 实例上存在components,且tag能在components中找到,是组件
    3. 未知标签 创建Vnode
  2. tag不是字符串,创建组件

根据上面的例子,我们会走走resolveAsset(context.$options, 'components', tag)这个方法,这个方法功能就是找$options.components里面 是否有tag这个值并且返回它。显然我们能在components里面找到这个tag,因为我们通过Vue声明了。然后我们就会进入createComponent(Ctor, data, context, children, tag) 方法。先来看看各个参数,Ctor是构造函数。data没有定义,content是当前实例,children未定义,tagcomp-a

createComponent

export function createComponent (
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {

  data = data || {}

  // install component management hooks onto the placeholder node
  installComponentHooks(data)

  // return a placeholder vnode
  const name = Ctor.options.name || tag
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefined, undefined, undefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )

  return vnode
}

在这个方法中前面还是做一些选项的初始化工作,主要在installComponentHooks方法,我们进入这个方法看看。

const componentVNodeHooks = {
  init() {},
  prepatch() {},
  insert() {},
  destory() {}
}

const hooksToMerge = Object.keys(componentVNodeHooks)

function installComponentHooks (data: VNodeData) {
  const hooks = data.hook || (data.hook = {})
  for (let i = 0; i < hooksToMerge.length; i++) {
    const key = hooksToMerge[i]
    const existing = hooks[key]
    const toMerge = componentVNodeHooks[key]
    if (existing !== toMerge && !(existing && existing._merged)) {
      hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge
    }
  }
}

它的主要工作是将componentVNodeHooks中的方法放到datahooks中,如果有同名方法,将使用mergeHook策略合并。合并策略为

(init1, init2) => {
 init1(),
 init2()
}

好准备工作都完成后,就开始new Vnode,实例化Vnode很简单,就是创建了很多属性,关键要记住我们现在的tagVnodetagvue-component-1-comp-a

vm._update

接下来其实还有创建父组件Vnode的过程,可以自己试着debug一下,过程是类似的。这边我们直接看update的过程,并且看一下传入的Vnode的构成

vnode: {
  tag: 'div',
  data: {
    attrs: {id: 'app'}
  },
  children: [
    Vnode // 这是一个div节点
    Vnode // 空文本节点
    Vnode // vue-component-1-comp-a
  ]
}

然后我们看_update方法,它在instance/lifrcycle.js

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
 const vm: Component = this
 const prevEl = vm.$el
 const prevVnode = vm._vnode
 // 存储当前的vm实例
 const restoreActiveInstance = setActiveInstance(vm)
 vm._vnode = vnode
 if (!prevVnode) {
   // initial render
   vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
 } else {
   // updates
   vm.$el = vm.__patch__(prevVnode, vnode)
 }
 // activeIntance 变成了上一个实例
 restoreActiveInstance()
}

这里我们也只看核心在当前vnode下,它不存在prevVnode节点,所以调用vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)。先想想传入的参数vm.$el就是我们的<div id='app'></div>vnode是它的虚拟节点。

patch 方法

__patch__是什么,让我们来到platform/runtime/index.js,其中有这么一句定义 Vue.prototype.__patch__ = inBrowser ? patch : noop,所以在浏览器端它就是patch方法。 继续找这个patch方法,它被定义在同目录下的patch.js中,它是一个方法的返回值,createPatchFunction,这个方法传入了一个对象,它被定义在core/vdom/patch,所以 createPatchFunction方法的返回值方法,才是我们将要执行的方法。

vue为什么要这么做?

让我们看看createPatchFunction方法的传入对象

  1. nodeOps 它定义了非常多的原生node操作方法
  2. modules 包含vue自定义的属性和浏览器属性

也就是说,vue使用高阶函数将代码抽离了,这是一种很好的编程方式。应该学习

好,下面我们来看patch,这个我不贴代码了,它非常长。我们使用文字来描述,类似伪代码的形式

  • 如果vnode未定义
    • oldvnode定义了,执行invokeDestroyHook(oldVnode)
    • return
  • 如果oldVnode不存在
    • 执行createElm(vnode, insertedVnodeQueue)
  • oldVnode存在
    • 首先判断oldVnode是否是真实节点
    • oldvnode不是真是节点,且sameVnode(oldVnode, vnode)
      • 执行patchVnode
    • oldVnode是真实节点,做一系列操作

大致这么判断就够了,执行到下一层级,前一层级就不会执行了,对照源码看。那么我们例子的情况,会跳到oldvnode为真实节点。接着往下执行,vue首先会对真实的oldVnode创建一个空节点为虚拟节点,然后调用 createElm,这时候传入的vnode

createElm 创建节点

function createElm (
  vnode,
  insertedVnodeQueue,
  parentElm,
  refElm,
  nested,
  ownerArray,
  index
) {

  // 组件vnode
  if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    return
  }

  const data = vnode.data
  const children = vnode.children
  const tag = vnode.tag

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

  // 递归创建子节点
  createChildren(vnode, children, insertedVnodeQueue)
  if (isDef(data)) {
    // 执行createhook
    invokeCreateHooks(vnode, insertedVnodeQueue)
  }
  // 插入真实节点
  insert(parentElm, vnode.elm, refElm)
 
}

function createChildren (vnode, children, insertedVnodeQueue) {
  if (Array.isArray(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)))
  }
}

createElm中,首先vue创建了vnode的元素节点,真实的。然后调用createChildren,传入的是vnodechildren,在这个方法中将再次调用createElm,传入的会变成children[i]。那么很显然当 vnodecomp-a的时候,我们将执行真正的组件创建。

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 */)
    }
    if (isDef(vnode.componentInstance)) {
      initComponent(vnode, insertedVnodeQueue)
      insert(parentElm, vnode.elm, refElm)
      if (isTrue(isReactivated)) {
        reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
      }
      return true
    }
  }
}

在之前我们已经知道vnode.data里面有我们需要的初始化函数,所以这里我们将执行它,当执行到i(vnode, false /* hydrating */)的时候,就是执行了vnode.init。在init方法中主要进行三步

  1. 执行prepatch这个hook
  2. 初始化子组件实例
  3. $mount挂载子组件

执行完成之后,我们就拿到了vnode.componentInstance。然后就是initComponent在这里方法里面会执行 invokeCreateHooks该方法就是执行的modules里面的create方法,都是dom相关的。 执行完这个就是insert方法,这个方法核心也是一句nodeOps.appendChild(parent, elm)

这时候渲染的是什么?

看一看元素, 没错是站位节点comp-a。那么什么时候渲染真实节点?

<div id="app">
 <div>这是一个div</div>
 <comp-a></comp-a>
</div>

回到createElm,这时候createChildren执行完了。也就是递归创建子节点结束了。看代码

// 递归创建子节点
createChildren(vnode, children, insertedVnodeQueue)
if (isDef(data)) {
 // 执行createhook
 invokeCreateHooks(vnode, insertedVnodeQueue)
}
// 插入真实节点
insert(parentElm, vnode.elm, refElm)

这时候执行父组件的invokeCreateHooks方法,也是一系列的钩子函数,执行完成后才是真正的insert。 这时候我们有vnode.elm,其中包含children节点。执行完成后看元素

<body>
  <div id="app">
    <div>这是一个div</div>
    <comp-a></comp-a>
  </div>
  <div id="app">
    <div>这是一个div</div>
    <div>这个一个子组件</div>
  </div>
</body>

没错渲染完成了,但是有两个,这很简单,删除上一个就行了。很显然createElm结束后,我们还要回到patch

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

删除节点,执行insert。这个insert是组件的,也就是vnode.data里面的,这里我们将执行组件的mounted钩子,设置实例mounted结束。最后回到vm._update执行vue实例的mounted钩子函数 最后执行完毕。

局部注册

我们来看下局部注册有什么不同。

<div id="app">
  <div>这是一个div</div>
  <comp-a></comp-a>
</div>
<script>
  const componentA = {
    template: '<div>这个一个子组件</div>'
  }
  const vm = new Vue({
    el: '#app',
    components: {
      'comp-a': componentA
    }
  })
</script>

前面的都没什么不同,而且在resolveAsset(context.$options, 'components', tag)代码中, vm.$options也能拿到该tag,但是他不在原型中,且是一个对象,所以当我们运行到createComponent 方法,我们会运行这段代码

if (isObject(Ctor)) {
 Ctor = baseCtor.extend(Ctor)
}

然后在执行extend方法的时候,我们传入的options是没有name的,只有在Vue.extend下才有。 所以这段代码也不会执行

if (name) {
   Sub.options.components[name] = Sub
 }

后面没什么区别,可以按照我上面的方法走一遍renderpatch的流程。