重学Vue【createComponent函数做了什么?】

简介: 重学Vue源码,根据黄轶大佬的vue技术揭秘,逐个过一遍,巩固一下vue源码知识点,毕竟嚼碎了才是自己的,所有文章都同步在 公众号(道道里的前端栈) 和 github 上。

网络异常,图片无法展示
|

重学Vue源码,根据黄轶大佬的vue技术揭秘,逐个过一遍,巩固一下vue源码知识点,毕竟嚼碎了才是自己的,所有文章都同步在 公众号(道道里的前端栈)github 上。


正文


之前一个简单的 vue 是如何渲染到页面上的,从本篇开始,开始过下 vue 内部的组件化逻辑,组件化也是 vue 的一大特性,组件化就是把页面拆分成多个组件,每个组件依赖的资源都放在一起,组件之间是互相独立的,可复用可嵌套。本篇以 vue-cli 为例,分析一下组件初始化的过程:

import Vue from 'vue'
import App from './App.vue'
var app = new Vue({
  el: '#app',
  // 这里的 h 是 createElement 方法
  render: h => h(App)
})

它也是通过 render 函数去渲染的,和之前不一样的是,他传入的不是一个标签,而是一个 App 组件。


createElement


之前说 createElement 会调用一个 _createElement 方法,中间有一段:

if (typeof tag === 'string') {
  let Ctor
  ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
  if (config.isReservedTag(tag)) {
    // platform built-in elements
    vnode = new VNode(
      config.parsePlatformTagName(tag), data, children,
      undefined, undefined, context
    )
  } else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
    // component
    vnode = createComponent(Ctor, data, context, children, tag)
  } else {
    // unknown or unlisted namespaced elements
    // check at runtime because it may get assigned a namespace when its
    // parent normalizes children
    vnode = new VNode(
      tag, data, children,
      undefined, undefined, context
    )
  }
} else {
  // direct component options / constructor
  vnode = createComponent(tag, data, context, children)
}

如果 tag 是一个字符串(也就是一个html标签),就返回一个实例化的VNode节点,否则就通过 createComponent 方法创建一个组件VNode。createComponentsrc/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
  }
  const baseCtor = context.$options._base
  // plain options object: turn it into a constructor
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor)
  }
  // if at this stage it's not a constructor or an async component factory,
  // reject.
  if (typeof Ctor !== 'function') {
    if (process.env.NODE_ENV !== 'production') {
      warn(`Invalid Component definition: ${String(Ctor)}`, context)
    }
    return
  }
  // async component
  let asyncFactory
  if (isUndef(Ctor.cid)) {
    asyncFactory = Ctor
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context)
    if (Ctor === undefined) {
      // return a placeholder node for async component, which is rendered
      // as a comment node but preserves all the raw information for the node.
      // the information will be used for async server-rendering and hydration.
      return createAsyncPlaceholder(
        asyncFactory,
        data,
        context,
        children,
        tag
      )
    }
  }
  data = data || {}
  // resolve constructor options in case global mixins are applied after
  // component constructor creation
  resolveConstructorOptions(Ctor)
  // transform component v-model data into props & events
  if (isDef(data.model)) {
    transformModel(Ctor.options, data)
  }
  // extract props
  const propsData = extractPropsFromVNodeData(data, Ctor, tag)
  // functional component
  if (isTrue(Ctor.options.functional)) {
    return createFunctionalComponent(Ctor, propsData, data, context, children)
  }
  // extract listeners, since these needs to be treated as
  // child component listeners instead of DOM listeners
  const listeners = data.on
  // replace with listeners with .native modifier
  // so it gets processed during parent component patch.
  data.on = data.nativeOn
  if (isTrue(Ctor.options.abstract)) {
    // abstract components do not keep anything
    // other than props & listeners & slot
    // work around flow
    const slot = data.slot
    data = {}
    if (slot) {
      data.slot = slot
    }
  }
  // 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
  )
  // Weex specific: invoke recycle-list optimized @render function for
  // extracting cell-slot template.
  // https://github.com/Hanks10100/weex-native-directive/tree/master/component
  /* istanbul ignore if */
  if (__WEEX__ && isRecyclableComponent(vnode)) {
    return renderRecyclableComponentTemplate(vnode)
  }
  return vnode
}

来看下这个方法都做了什么事情,它有几个参数,第一个可以传入组件、方法、对象和空,第二个是一个节点数据,第三个是当前上下文实例,第四个是一个子的数组vnode,最后是一个标签。

上面的代码简化一下,其实只有三步:构造子类构造函数,安装组件钩子函数**,实例化vnode**。


构造子类构造函数

const baseCtor = context.$options._base
// plain options object: turn it into a constructor
if (isObject(Ctor)) {
  Ctor = baseCtor.extend(Ctor)
}

一般情况下编写组件都会 import 一个组件,然后用 components 去声明它,然后页面上才可以使用:

import HelloWorld from './components/HelloWorld'
export default {
  name: 'app',
  components: {
    HelloWorld
  }
}

export 的是一个对象,所以 createComponent 里的代码逻辑会执行到 baseCtor.extend(Ctor),这里 baseCtor 实际上就是 Vue,它来自哪里呢?从 ~/vue/src/core/global-api/index.js 里可以看到一个 Vue.options._base = Vue,还记得vue在初始化的时候有一个合并操作么:

vm.$options = mergeOptions(
  resolveConstructorOptions(vm.constructor),
  options || {},
  vm
)

它就是把上面的 Vue.options._baseoptions 合并到了一起,放在了 vm.$options 上,所以 context.$options._base 拿到的就是 Vue,也就是说这个 baseCtor 其实就是一个 Vue。后面如果 Ctor 是一个对象,就执行 baseCtor.extend(Ctor),也就是 Vue.extend(),把 Ctor 转化为一个新的构造器。接着看下 Vue.extend 的定义:

/**
   * Class inheritance
   */
  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)
    }
    Sub.prototype = Object.create(Super.prototype)
    Sub.prototype.constructor = Sub
    Sub.cid = cid++
    Sub.options = mergeOptions(
      Super.options,
      extendOptions
    )
    Sub['super'] = Super
    // For props and computed properties, we define the proxy getters on
    // the Vue instances at extension time, on the extended prototype. This
    // avoids Object.defineProperty calls for each instance created.
    if (Sub.options.props) {
      initProps(Sub)
    }
    if (Sub.options.computed) {
      initComputed(Sub)
    }
    // allow further extension/mixin/plugin usage
    Sub.extend = Super.extend
    Sub.mixin = Super.mixin
    Sub.use = Super.use
    // create asset registers, so extended classes
    // can have their private assets too.
    ASSET_TYPES.forEach(function (type) {
      Sub[type] = Super[type]
    })
    // enable recursive self-lookup
    if (name) {
      Sub.options.components[name] = Sub
    }
    // keep a reference to the super options at extension time.
    // later at instantiation we can check if Super's options have
    // been updated.
    Sub.superOptions = Super.options
    Sub.extendOptions = extendOptions
    Sub.sealedOptions = extend({}, Sub.options)
    // cache constructor
    cachedCtors[SuperId] = Sub
    return Sub
  }

可以看到,传入一个对象可以返回一个函数(Vue的子类)。这里注意一下 const Super = this,这个 this 不是指的 vm,而是当前的 Vue。 后面把传进来的 name 做了一层校验 validateComponentName,判断这个组件名称是否合法,校验规则在 src/core/util/options.js

export function validateComponentName (name: string) {
  if (!/^[a-zA-Z][\w-]*$/.test(name)) {
    warn(
      'Invalid component name: "' + name + '". Component names ' +
      'can only contain alphanumeric characters and the hyphen, ' +
      'and must start with a letter.'
    )
  }
  if (isBuiltInTag(name) || config.isReservedTag(name)) {
    warn(
      'Do not use built-in or reserved HTML elements as component ' +
      'id: ' + name
    )
  }
}

接着后面定义了一个子构造函数 Sub,调用了 _init,和Vue的 _init 很像,接着做了一层原型继承,也就是将这个 _init 变成 Vue.protype._init,后面把自身的 options 和 Vue的 options 合并,然后对自身的 propscomputed 也做了一层初始化,后面把全局的一些静态方法都赋值给 Sub,目的就是让 Sub 拥有和Vue一样的能力。

Vue.extend 的好处是,如果很多地方用同一个组价的时候,其实调用 Vue.extend 方法传入的对象是一样的,就只会执行一次:

const Super = this
const SuperId = Super.cid
const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
if (cachedCtors[SuperId]) {
  return cachedCtors[SuperId]
}

因为每次进来其实已经定义过了 _Ctor 了,并且已经有值了,并且 SuperId 是有的,所以直接返回这个构造器(如果SuperId一致,说明它们的父构造器是同一个)。


安装组件钩子

// if at this stage it's not a constructor or an async component factory,
// reject.
if (typeof Ctor !== 'function') {
  if (process.env.NODE_ENV !== 'production') {
    warn(`Invalid Component definition: ${String(Ctor)}`, context)
  }
  return
}

接着看,如果这个构造器不是一个方法,就报一个错,说组件定义不合法,后面是一个对异步组件的操作,跳过,后面:

// resolve constructor options in case global mixins are applied after
// component constructor creation
resolveConstructorOptions(Ctor)

options 重新计算,因为可能被全局mixin影响,跳过,后面对 v-modelpropsDataoptions 和 自定义事件 的处理跳过,到 installComponentHooks,安装组件钩子开始。之前说到 patch 的过程中会暴露一堆钩子函数, 这里就是安装这些钩子:

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

它遍历了 hooksToMerge,来看下是个啥:

// inline hooks to be invoked on component VNodes during 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
      )
      child.$mount(hydrating ? vnode.elm : undefined, hydrating)
    }
  },
  prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
    const options = vnode.componentOptions
    const child = vnode.componentInstance = oldVnode.componentInstance
    updateChildComponent(
      child,
      options.propsData, // updated props
      options.listeners, // updated listeners
      vnode, // new parent vnode
      options.children // new children
    )
  },
  insert (vnode: MountedComponentVNode) {
    const { context, componentInstance } = vnode
    if (!componentInstance._isMounted) {
      componentInstance._isMounted = true
      callHook(componentInstance, 'mounted')
    }
    if (vnode.data.keepAlive) {
      if (context._isMounted) {
        // vue-router#1212
        // During updates, a kept-alive component's child components may
        // change, so directly walking the tree here may call activated hooks
        // on incorrect children. Instead we push them into a queue which will
        // be processed after the whole patch process ended.
        queueActivatedComponent(componentInstance)
      } else {
        activateChildComponent(componentInstance, true /* direct */)
      }
    }
  },
  destroy (vnode: MountedComponentVNode) {
    const { componentInstance } = vnode
    if (!componentInstance._isDestroyed) {
      if (!vnode.data.keepAlive) {
        componentInstance.$destroy()
      } else {
        deactivateChildComponent(componentInstance, true /* direct */)
      }
    }
  }
}
const hooksToMerge = Object.keys(componentVNodeHooks)

也就是遍历了 initprepatchinsertdestroy。也就是说组件默认会有这四个钩子,然后遍历钩子,把钩子合并到 data.hook 上,来看下具体的合并过程:

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
}

很简单就是依次执行就好了。


生成vnode

// 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
)

最后就是生成一个 vnode,这个和之前的不太一样,它的名字会打一个标识 vue-component-,后面就是传入了一些参数,回顾一下 vnode 是如何定义参数的:

//...
constructor (
  tag?: string,
  data?: VNodeData,
  children?: ?Array<VNode>,
  text?: string,
  elm?: Node,
  context?: Component,
  componentOptions?: VNodeComponentOptions,
  asyncFactory?: Function
) {
  //...
}

children 为空,也就是 组件vnode的children是个空,然后 text 是空,elm 也是空,componentOptions{ Ctor, propsData, listeners, tag, children },最后返回这个vnode。

至此组件创建完毕。


捋一遍:子组件的构造器生成 baseCtor,它是继承于 Vue 的,返回一个组件构造器,其次组件vnode的data有一堆 hook,它会合并到组件的 data.hook 上,最后就是生成一个组件vnode,它没有 children,多了一个 componentOptions

目录
相关文章
|
2月前
|
JavaScript
vue使用iconfont图标
vue使用iconfont图标
148 1
|
7天前
|
移动开发 JavaScript API
Vue Router 核心原理
Vue Router 是 Vue.js 的官方路由管理器,用于实现单页面应用(SPA)的路由功能。其核心原理包括路由配置、监听浏览器事件和组件渲染等。通过定义路径与组件的映射关系,Vue Router 将用户访问的路径与对应的组件关联,支持哈希和历史模式监听 URL 变化,确保页面导航时正确渲染组件。
|
11天前
|
监控 JavaScript 前端开发
ry-vue-flowable-xg:震撼来袭!这款基于 Vue 和 Flowable 的企业级工程项目管理项目,你绝不能错过
基于 Vue 和 Flowable 的企业级工程项目管理平台,免费开源且高度定制化。它覆盖投标管理、进度控制、财务核算等全流程需求,提供流程设计、部署、监控和任务管理等功能,适用于企业办公、生产制造、金融服务等多个场景,助力企业提升效率与竞争力。
65 12
|
7天前
|
JavaScript 前端开发 开发者
Vue中的class和style绑定
在 Vue 中,class 和 style 绑定是基于数据驱动视图的强大功能。通过 class 绑定,可以动态更新元素的 class 属性,支持对象和数组语法,适用于普通元素和组件。style 绑定则允许以对象或数组形式动态设置内联样式,Vue 会根据数据变化自动更新 DOM。
|
7天前
|
JavaScript 前端开发 数据安全/隐私保护
Vue Router 简介
Vue Router 是 Vue.js 官方的路由管理库,用于构建单页面应用(SPA)。它将不同页面映射到对应组件,支持嵌套路由、路由参数和导航守卫等功能,简化复杂前端应用的开发。主要特性包括路由映射、嵌套路由、路由参数、导航守卫和路由懒加载,提升性能和开发效率。安装命令:`npm install vue-router`。
|
28天前
|
JavaScript 安全 API
iframe嵌入页面实现免登录思路(以vue为例)
通过上述步骤,可以在Vue.js项目中通过 `iframe`实现不同应用间的免登录功能。利用Token传递和消息传递机制,可以确保安全、高效地在主应用和子应用间共享登录状态。这种方法在实际项目中具有广泛的应用前景,能够显著提升用户体验。
55 8
|
29天前
|
存储 设计模式 JavaScript
Vue 组件化开发:构建高质量应用的核心
本文深入探讨了 Vue.js 组件化开发的核心概念与最佳实践。
75 1
|
2月前
|
JavaScript 关系型数据库 MySQL
基于VUE的校园二手交易平台系统设计与实现毕业设计论文模板
基于Vue的校园二手交易平台是一款专为校园用户设计的在线交易系统,提供简洁高效、安全可靠的二手商品买卖环境。平台利用Vue框架的响应式数据绑定和组件化特性,实现用户友好的界面,方便商品浏览、发布与管理。该系统采用Node.js、MySQL及B/S架构,确保稳定性和多功能模块设计,涵盖管理员和用户功能模块,促进物品循环使用,降低开销,提升环保意识,助力绿色校园文化建设。
|
3月前
|
JavaScript API 开发者
Vue是如何进行组件化的
Vue是如何进行组件化的
|
3月前
|
JavaScript 前端开发 开发者
Vue是如何进行组件化的
Vue是如何进行组件化的

热门文章

最新文章

相关实验场景

更多