Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vue源码-组件化 #3

Open
wozien opened this issue Aug 6, 2020 · 0 comments
Open

Vue源码-组件化 #3

wozien opened this issue Aug 6, 2020 · 0 comments
Labels

Comments

@wozien
Copy link
Owner

wozien commented Aug 6, 2020

组件化是Vue的核心概念,它让我们提取可复用的模版和脚本,在需要用到的地方插入对应的组件标签。那Vue是怎么把组件渲染成真实的DOM的呢,它和我们渲染普通的HTML有啥区别?我们在上篇文章中实例插入一个组件,通过源码分析组件怎么渲染?

组件虚拟节点

我们通过new Vue()插入一个组件节点:

// App.vue
<template>
  <div class="app">
    {{ msg }}
  </div>
</template>

<script>
export default {
  name: 'app',
  data() {
    return {
      msg: 'Hello Vue'
    };
  }
};
</script>

在入口文件挂载:

import Vue from 'vue';
import App from './App.vue';

new Vue({
  el: '#app',
  render: h => h(App)
});

从上篇分析new Vue()的主流程后,我们知道vue实例会调用vm.$mount()进行挂载。然后新建一个实例的渲染watcher,并执行下面的updateComponent函数:

updateComponent = () => {
  vm._update(vm._render(), hydrating)
}

该函数先调用vm._render()生成一个虚拟节点,很明显这是一个App组件节点。所以我们来看看_createElement()对组件节点是怎么创建的。它定义在src/core/vdom/create-element.js

if (typeof tag === 'string') {
  let Ctor
  // 获取tag的命名空间
  ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
  if (config.isReservedTag(tag)) {
    // 平台保留的标签
    if (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.nativeOn)) {
      warn(
        `The .native modifier for v-on is only valid on components but it was used on <${tag}>.`,
        context
      )
    }
    vnode = new VNode(
      config.parsePlatformTagName(tag), data, children,
      undefined, undefined, context
    )
  } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
    // 在vm的options的components选项查找,如果存在则是组件节点
    vnode = createComponent(Ctor, data, context, children, tag)
  } else {
    // 未知的元素
    vnode = new VNode(
      tag, data, children,
      undefined, undefined, context
    )
  }
} else {
  // direct component options / constructor
  vnode = createComponent(tag, data, context, children)
}

上面是创建虚拟节点的关键代码。因为tag是一个App组件的配置对象,所以调用createComponent()方法生成一个组件虚拟节点。这个方法定义在src/core/vdom/create-component.js:

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

  // _base的值为Vue构造函数
  const baseCtor = context.$options._base

  // 组件的配置项是对象,调用Vue.extend生成vue组件类
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor)
  }

  if (typeof Ctor !== 'function') {
    if (process.env.NODE_ENV !== 'production') {
      warn(`Invalid Component definition: ${String(Ctor)}`, context)
    }
    return
  }

  // 异步组件
  let asyncFactory
  if (isUndef(Ctor.cid)) {
    asyncFactory = Ctor
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
    if (Ctor === undefined) {
      // 异步组件一开始用空的注释节点做占位
      return createAsyncPlaceholder(
        asyncFactory,
        data,
        context,
        children,
        tag
      )
    }
  }

  data = data || {}

  resolveConstructorOptions(Ctor)

  // transform component v-model data into props & events
  // v-model的处理
  if (isDef(data.model)) {
    transformModel(Ctor.options, data)
  }

  // 从vnode的data中提取props数据
  const propsData = extractPropsFromVNodeData(data, Ctor, tag)

  // functional component
  // 函数组件处理
  if (isTrue(Ctor.options.functional)) {
    return createFunctionalComponent(Ctor, propsData, data, context, children)
  }

  const listeners = data.on
  data.on = data.nativeOn

  if (isTrue(Ctor.options.abstract)) {
    const slot = data.slot
    data = {}
    if (slot) {
      data.slot = slot
    }
  }

  // install component management hooks onto the placeholder node
  // 安装组件vnode的各种钩子
  installComponentHooks(data)
  // 返回组件的占位节点
  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
}

这个方法一开始先获取context.$options._base,也就是Vue构造函数。然后调用Vue.extend()返回组件的构造器。它定义在src/core/global-api/extend.js

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 name = extendOptions.name || Super.options.name
  if (process.env.NODE_ENV !== 'production' && name) {
    validateComponentName(name)
  }

  const Sub = function VueComponent (options) {
    this._init(options)
  }
  // 继承Vue原型的方法
  Sub.prototype = Object.create(Super.prototype)
  Sub.prototype.constructor = Sub
  Sub.cid = cid++

  // 合并配置,组件构造器也拥有Vue.options全局配置
  Sub.options = mergeOptions(
    Super.options,
    extendOptions
  )
  Sub['super'] = Super

  // 把组件props和computed挂载原型上,避免每次实例话组件的时候
  // 都在实例上代理一遍
  if (Sub.options.props) {
    initProps(Sub)
  }
  if (Sub.options.computed) {
    initComputed(Sub)
  }

  // 各种Vue静态方法的继承
  Sub.extend = Super.extend
  Sub.mixin = Super.mixin
  Sub.use = Super.use

  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)

  cachedCtors[SuperId] = Sub
  return Sub
}

这个方法一开始先缓存组件构造器到组件配置对象的extendOptions._Ctor中,在后面再用到该组件,直接返回组件构造器。然后定义组件构造器:

const Sub = function VueComponent (options) {
  this._init(options)
}

所以新建一个组件实际入口也是vm._init()方法。然后指定原型为Vue.prototype让它拥有Vue原型方法。接下来处理组件的option配置,它调用mergeOptions()方法把Vue.options上的属性合并到组件构造器的options,比如平时我们全局注册的组件,就是在这步进行合并,让我们组件内直接使用而不用声明。最后,把Vue上面的静态方法也赋值到组件构造器,最后返回这个构造器。

回到createComponent()方法,如果创建组件配置不是一个对象,证明是一个异步组件,所以下面就是对异步工厂函数的处理:

let asyncFactory
if (isUndef(Ctor.cid)) {
  asyncFactory = Ctor
  Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
  if (Ctor === undefined) {
    // 异步组件一开始用空的注释节点做占位
    // return a placeholder node for async component, which is rendered
    return createAsyncPlaceholder(
      asyncFactory,
      data,
      context,
      children,
      tag
    )
  }
}

里面的实现细节我们之后再分析。接下来是处理v-modelprops等,我们暂且跳过。然后到了下面这个方法:

installComponentHooks(data)  // 安装钩子

这个方法主要是安装组件vnode在接下来patch过程需要调用的钩子函数。它的定义:

const componentVNodeHooks = {
  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 // 组件占位符所在的vm
      )
      child.$mount(hydrating ? vnode.elm : undefined, hydrating)
    }
  },

  prepatch(oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
    // ...
  },

  insert (vnode: MountedComponentVNode) {
    // ...
  },

  destroy (vnode: MountedComponentVNode) {
    // ..
  }
}

const hooksToMerge = Object.keys(componentVNodeHooks)

// 合并对应阶段的钩子
function mergeHook (f1: any, f2: any): Function {
  const merged = (a, b) => {
    // flow complains about extra args which is why we use any
    f1(a, b)
    f2(a, b)
  }
  merged._merged = true
  return merged
}

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对象定义的各种阶段钩子合并到了我们新建的组件vnode的hook属性上面。至此我们组件vnode就创建完成并返回,输出下返回的结果:

创建组件实例

现在我们已经得到组件的虚拟DOM,正确的说是组件占位vnode。因为它在渲染后会被组件对应的DOM替换,比如我们例子的<App/>组件。

执行完render后调用Vue实例的vm._update()方法,该方法里面会走下面的方法:

if (!prevVnode) {
  // initial render
  // 首次挂载
  vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
}

接下来的过程上一篇一样。会进行patch()方法并执行oldvnode为真实DOM的条件逻辑,不同的是createElm()方法里面会执行到下面代码为true并返回,因为我们此时vnode为组件虚拟节点:

// 组件节点的处理
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
  return
}

来看下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))) {
      // 调用组件vnode的init钩子
      i(vnode, false /* hydrating */);
    }

    if (isDef(vnode.componentInstance)) {
      initComponent(vnode, insertedVnodeQueue);
      // 把组件对应的DOM替换组件占位符
      insert(parentElm, vnode.elm, refElm);
      if (isTrue(isReactivated)) {
        reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
      }
      return true;
    }
  }
}

这个方法会先调用vnode上面的init钩子函数,init钩子主要是创建组件实例,并且得到组件真实DOM节点。我们看下init钩子的定义。在定义我们前面创建组件vnode的地方:

const componentVNodeHooks = {
  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 // 组件占位符所在的vm
      )
      child.$mount(hydrating ? vnode.elm : undefined, hydrating)
    }
  },

  //...
}

这个方法createComponentInstanceForVnode创建组件的实例。再看这个方法实现之前,现在说下activeInstance变量的意义。它是当前Vue或者组件实例,作为一个全局变量,主要维护组件树对应的实例对象的父子关系。在vm._update()方法里面处理:

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

   // ..  
}

setActiveInstance()方法定义:

export function setActiveInstance(vm: Component) {
  const prevActiveInstance = activeInstance
  activeInstance = vm
  return () => {
    activeInstance = prevActiveInstance
  }
}

很简单,先把之前的实例对象存起来,再设置成最新调用update的实例。在patch完后就恢复原来的实例。

现在来看下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, // 组件的占位符vnode
    parent               // 组件的父实例vm
  }
  const inlineTemplate = vnode.data.inlineTemplate
  if (isDef(inlineTemplate)) {
    options.render = inlineTemplate.render
    options.staticRenderFns = inlineTemplate.staticRenderFns
  }
  return new vnode.componentOptions.Ctor(options) // 调用vm._init()方法
}

这个方法主要通过new vnode.componentOptions.Ctor(options)创建一个组件实例,然后调用vm._init()方法回到我们之前创建Vue实例的逻辑,但是有两个不同的地方。一个是合并配置,它会走下面的处理:

if (options && options._isComponent) {
  // 组件实例的配置合并
  initInternalComponent(vm, options)
} else {
  // .. 不走这
}

因为组件构造器已经在extend的时候合并了Vue的全局属性,没必要再合并一次。最后组件实例不会走下面方式的挂载:

// 使用new Vue进行挂载,组件的挂载不会走这
if (vm.$options.el) {
  vm.$mount(vm.$options.el);
}

组件的挂载定义在init钩子,如下:

child.$mount(hydrating ? vnode.elm : undefined, hydrating)

创建组件真实的DOM

接下来就走挂载的逻辑,这个和前面Vue实例的挂载一样。最后也是会走到vm.__patch__()方法,不同的是vm.$elundefined。所以在patch函数里面,它会执行下面条件逻辑:

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

这个逻辑就是调用createElm()方法把App组件对应的DOM虚拟节点转为真实的DOM,并且存储在vnode.elm上。方法我们上一篇分析过,这里不再累赘。这我们例子中也就是下面对应的DOM:

  <div class="app">
    {{ msg }}
  </div>

patch()返回vnode.elm作为vm.__patch__()的结果存在vm.$el。这里的vm就是我们App组件实例。也就是我们我们已经得到App组件的真实DOM,并存在vm.$el上面,也就是App组件虚拟节点的vnode.componentInstance.$el

执行完App组件实例的updateComponents()函数,流程回到App虚拟节点的init()钩子。然后执行下面的代码:

if (isDef(vnode.componentInstance)) {
  initComponent(vnode, insertedVnodeQueue)
  // 把组件对应的DOM替换组件占位符
  insert(parentElm, vnode.elm, refElm)
  if (isTrue(isReactivated)) {
    reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
  }
  return true
}

通过initComponent方法会把组件对应的真实DOM赋值给vnode.elm,注意这是vnode是组件App的占位虚拟节点,然后调用insert()方法把组件插入DOM中。至此,我们App组件就渲染完成:

总结

其实,Vue组件化的渲染过程是一个深度遍历的过程。假如我们例子中的App组件中包含其他组件,也会先创建对应占位vnode。 在App实例调用update的时候创建包含组件的实例,创建组件的真实DOM并插入到App组件的DOM中,如此反复递归。最后new Vue获取最终的DOM节点插入真实DOM环境。

通过上面的分析,我们可以知道组件DOM插入是先子后父的,并且它满足树的深度优先遍历。

@wozien wozien added the vue label Sep 15, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant