Components are basically the standard of modern Web development, and in Vue they are one of its core cornerstones.

Vue components in this aspect of the design is also very careful, developers can be said to use the cost is very low, let’s take a look, and learn the skills and good design ideas.

The text analysis


We need to understand componentized development first. There is a diagram on the Vue website, which simply and figuratively describes the core idea:

During development, we split the page into components, which are stacked together like piles of wood to form a tree. So this is the core idea of componentized development.

At this point, we can understand the next component of the front end: a functional independent module.

There are several core points:

  • The module
    • Component must be a module (independent)
      • In fact, it can be considered as a combination of multiple modules (logical module JS, view module CSS, structural module HTML).
    • The purpose of modules is to divide and conquer and decouple
  • independent
    • Independence means pursuing reuse
    • Independence means composability (nesting)
    • The modules themselves are independent, but here more emphasis is placed on functional independence
  • function
    • Emphasize completeness, which is the foundation of functionality
    • Emphasize functionality, i.e. specific things that can be done, very specific (tables, navigation, etc.)

Components in Vue, with a good start… , and the recommended accompanying single-file component… (PERSONALLY, I like this organization very much.)

Let’s take a look at Vue components using a component example:

import Vue from 'vue'
import App from './App.vue'
const vm = new Vue({
  render (h) {
    return h(App)
Copy the code

App.vue is one of the above single-file components, which looks like this:

  <div id="app">
    <div @click="show = ! show">Toggle</div>
    <p v-if="show">{{ msg }}</p>
export default {
  data () {
    return {
      msg: 'Hello World! '.show: false}}}</script>
<style lang="stylus">
  font-family Avenir, Helvetica, Arial, sans-serif
  -webkit-font-smoothing antialiased
  -moz-osx-font-smoothing grayscale
  text-align center
  color #2c3e50
  margin-top 60px
Copy the code

Here, too, is a closer sense of what a component looks like in Vue: template + script logic + style. In the logical part, we use the same initialization part that we used in the lifecycle analysis: processing of some configuration items (data, methods, computed, Watch, provide, Inject, and so on).

Of course, there are a lot of other configuration items in Vue, details can refer to the official documentation, not detailed here.


Based on our example and our analysis in the lifecycle article, after Vue applies mount, it calls the render() function to get vDOM data, and we know that h is the instance’s $createElement, and that the App parameter is a component we defined.

Back to the source code, the createElement implementation is available at… Here’s a quick look:

import { createComponent } from './create-component'
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
  / / _createElement directly
  return _createElement(context, tag, data, children, normalizationType)
export function _createElement (context: Component, tag? : string | Class<Component> |Function | Object, data? : VNodeData, children? : any, normalizationType? : number) :VNode | Array<VNode> {
  if(isDef(data) && isDef((data: any).__ob__)) { process.env.NODE_ENV ! = ='production' && warn(
      `Avoid using observed data object as vnode data: The ${JSON.stringify(data)}\n` +
      'Always create fresh vnode data objects in each render! ',
    return createEmptyVNode()
  // object syntax in v-bind
  if (isDef(data) && isDef( {
    tag =
  if(! tag) {// in case of component :is set to falsy value
    return createEmptyVNode()
  // warn against non-primitive key
  if(process.env.NODE_ENV ! = ='production'&& isDef(data) && isDef(data.key) && ! isPrimitive(data.key) ) {if(! __WEEX__ || ! ('@binding' in data.key)) {
        'Avoid using non-primitive value as key, ' +
        'use string/number value instead.',
  // support single function children as default scoped slot
  if (Array.isArray(children) &&
    typeof children[0= = ='function'
  ) {
    data = data || {}
    data.scopedSlots = { default: children[0] }
    children.length = 0
  if (normalizationType === ALWAYS_NORMALIZE) {
    children = normalizeChildren(children)
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    children = simpleNormalizeChildren(children)
  let vnode, ns
  if (typeof tag === 'string') {
    // String
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    if (config.isReservedTag(tag)) {
      // Built-in elements, which in Web terms are normal HTML elements
      // platform built-in elements
      if(process.env.NODE_ENV ! = ='production'&& isDef(data) && isDef(data.nativeOn) && data.tag ! = ='component') {
          `The .native modifier for v-on is only valid on components but it was used on <${tag}>. `,
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined.undefined, context
    } else if((! data || ! data.pre) && isDef(Ctor = resolveAsset(context.$options,'components', tag))) {
      // component
      // Component scenario
      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 {
    // This is definitely the component scenario where our Case will enter
    // direct component options / constructor
    vnode = createComponent(tag, data, context, children)
  if (Array.isArray(vnode)) {
    return vnode
  } else if (isDef(vnode)) {
    if (isDef(ns)) applyNS(vnode, ns)
    if (isDef(data)) registerDeepBindings(data)
    return vnode
  } else {
    return createEmptyVNode()
Copy the code

The next focus seems to be createComponent, from…

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)) {
  // Vue
  const baseCtor = context.$options._base
  // plain options object: turn it into a constructor
  // Our scenario, since it is a normal object, will call vue.extend to become 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 (typeofCtor ! = ='function') {
    if(process.env.NODE_ENV ! = ='production') {
      warn(`Invalid Component definition: The ${String(Ctor)}`, context)
  // async component
  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
      // 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(
  data = data || {}
  // resolve constructor options in case global mixins are applied after
  // component constructor creation
  // 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
  // There was a bit of abstraction involved earlier
  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
  // Installing component hooks is important!!
  // return a placeholder vnode
  const name = || tag
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? ` -${name}` : ' '}`,
    data, undefined.undefined.undefined, context,
    { Ctor, propsData, listeners, tag, children },
  // Weex specific: invoke recycle-list optimized @render function for
  // extracting cell-slot template.
  /* istanbul ignore if */
  if (__WEEX__ && isRecyclableComponent(vnode)) {
    return renderRecyclableComponentTemplate(vnode)
  return vnode
Copy the code

Take a close look at this important install hooks

const componentVNodeHooks = {
  init (vnode: VNodeWithData, hydrating: boolean): ? boolean {// ...
  prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
    // ...
  insert (vnode: MountedComponentVNode) {
    // ...
  destroy (vnode: MountedComponentVNode) {
    // ...}}const hooksToMerge = Object.keys(componentVNodeHooks)
// Install component hooks
function installComponentHooks (data: VNodeData) {
  const hooks = data.hook || (data.hook = {})
  Init prepatch insert destroy
  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)) {/ / the mergeHook
      hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge
function mergeHook (f1: any, f2: any) :Function {
  // Returns a new function called f1 f2 in order
  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
Copy the code

ComponentVNodeHooks init Prepatch Insert Destroy on vNode data.hook

And here’s another trick, mergeHook uses the closure feature to make it possible to merge function execution.

Render () executes patch logic and createElm…

function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index) {
  // ...
  if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
  // ...
Copy the code


function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
  let i =
  if (isDef(i)) {
    const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
    // Important: call the init hook in
    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.
    // init hook creates component instance and mounts
    // Now componentInstance has been created
    if (isDef(vnode.componentInstance)) {
      // Initialize the component
      initComponent(vnode, insertedVnodeQueue)
      // Insert elements
      insert(parentElm, vnode.elm, refElm)
      if (isTrue(isReactivated)) {
        reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
      return true}}}function initComponent (vnode, insertedVnodeQueue) {
  if (isDef( {
    insertedVnodeQueue.push.apply(insertedVnodeQueue, = null
  vnode.elm = vnode.componentInstance.$el
  if (isPatchable(vnode)) {
    // Trigger CREATE hooks
    invokeCreateHooks(vnode, insertedVnodeQueue)
  } else {
    // empty component root.
    // skip all element-related modules except for ref (#3455)
    // make sure to invoke the insert hook
function invokeCreateHooks (vnode, insertedVnodeQueue) {
  // Emphasis: the CREATE hook in CBS
  for (let i = 0; i < cbs.create.length; ++i) {
    cbs.create[i](emptyNode, vnode)
  // It's built in vNode
  i = // Reuse variable
  if (isDef(i)) {
    / / create hooks
    if (isDef(i.create)) i.create(emptyNode, vnode)
    // Important: Insert hook, not called immediately but put in queue
    if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
function insert (parent, elm, ref) {
  if (isDef(parent)) {
    if (isDef(ref)) {
      if (nodeOps.parentNode(ref) === parent) {
        nodeOps.insertBefore(parent, elm, ref)
    } else {
      nodeOps.appendChild(parent, elm)
Copy the code

The above analysis has a few key points to focus on:

  • What the init hook does
  • Where did CBS hook come from and what did it probably do
  • Why is the insertion hook queued instead of executed immediately

What the init hook does

Back to installing hooks, we know that componentVNodeHooks define what init hooks need to do…

init (vnode: VNodeWithData, hydrating: boolean): ? boolean {if( vnode.componentInstance && ! vnode.componentInstance._isDestroyed && ) {/ / ignore
    // kept-alive components, treat as a patch
    const mountedNode: any = vnode // work around flow
    componentVNodeHooks.prepatch(mountedNode, mountedNode)
  } else {
    // Create a component instance for vNode
    const child = vnode.componentInstance = createComponentInstanceForVnode(
    // Mount the component instance
    child.$mount(hydrating ? vnode.elm : undefined, hydrating)
Copy the code

And this createComponentInstanceForVnode logic is as follows

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

We already know that the constructor is a subclass of Vue, so the initialization process is basically a Vue initialization process; In The init hook, The component instance is immediately called $mount to mount The component. All of this logic has already been analyzed in The lifecycle analysis, so you can see vue-the Good Parts: life cycle.

Where did CBS hook come from and what did it probably do

So where do the hooks in CBS come from? This needs to go back to patch…

const hooks = ['create'.'activate'.'update'.'remove'.'destroy']
export function createPatchFunction (backend) {
  let i, j
  const cbs = {}
  const { modules, nodeOps } = backend
  for (i = 0; i < hooks.length; ++i) {
    cbs[hooks[i]] = []
    for (j = 0; j < modules.length; ++j) {
      if (isDef(modules[j][hooks[i]])) {
  // ...
Copy the code

At the top of createPatchFunction, when executed, CBS is assigned based on the configuration in the modules passed in. Here we don’t need to see what all modules do, we can pick two to see roughly what they might do: one comes from the core directive… And the other is style from the platform Web…

// directives.js
export default {
  // Hooks are used to create update and destroy
  create: updateDirectives,
  update: updateDirectives,
  destroy: function unbindDirectives (vnode: VNodeWithData) {
    updateDirectives(vnode, emptyNode)
function updateDirectives (oldVnode: VNodeWithData, vnode: VNodeWithData) {
  if ( || {
    _update(oldVnode, vnode)
function _update (oldVnode, vnode) {
  // Update instruction information based on old and new VNode information
  const isCreate = oldVnode === emptyNode
  const isDestroy = vnode === emptyNode
  const oldDirs = normalizeDirectives(, oldVnode.context)
  const newDirs = normalizeDirectives(, vnode.context)
  const dirsWithInsert = []
  const dirsWithPostpatch = []
  let key, oldDir, dir
  for (key in newDirs) {
    oldDir = oldDirs[key]
    dir = newDirs[key]
    if(! oldDir) {// The instruction bind hook
      // new directive, bind
      callHook(dir, 'bind', vnode, oldVnode)
      if (dir.def && dir.def.inserted) {
    } else {
      // existing directive, update
      dir.oldValue = oldDir.value
      dir.oldArg = oldDir.arg
      // The directive update hook
      callHook(dir, 'update', vnode, oldVnode)
      if (dir.def && dir.def.componentUpdated) {
  if (dirsWithInsert.length) {
    const callInsert = () = > {
      for (let i = 0; i < dirsWithInsert.length; i++) {
        // Directive inserted hook
        callHook(dirsWithInsert[i], 'inserted', vnode, oldVnode)
    if (isCreate) {
      mergeVNodeHook(vnode, 'insert', callInsert)
    } else {
  if (dirsWithPostpatch.length) {
    mergeVNodeHook(vnode, 'postpatch'.() = > {
      for (let i = 0; i < dirsWithPostpatch.length; i++) {
        // Directive componentUpdated hook
        callHook(dirsWithPostpatch[i], 'componentUpdated', vnode, oldVnode)
  if(! isCreate) {for (key in oldDirs) {
      if(! newDirs[key]) {// no longer present, unbind
        // Command unbind hook
        callHook(oldDirs[key], 'unbind', oldVnode, oldVnode, isDestroy)
Copy the code

As you can see, it is basically a hook function that calls instructions for each cycle according to various conditions. The core is also the idea of life cycle.

// style.js
export default {
  create: updateStyle,
  update: updateStyle
function updateStyle (oldVnode: VNodeWithData, vnode: VNodeWithData) {
  const data =
  const oldData =
  if (isUndef(data.staticStyle) && isUndef( &&
    isUndef(oldData.staticStyle) && isUndef(
  ) {
  let cur, name
  const el: any = vnode.elm
  const oldStaticStyle: any = oldData.staticStyle
  const oldStyleBinding: any = oldData.normalizedStyle || || {}
  // if static style exists, stylebinding already merged into it when doing normalizeStyleData
  const oldStyle = oldStaticStyle || oldStyleBinding
  const style = normalizeStyleBinding( || {}
  // store normalized style under a different key for next diff
  // make sure to clone it if it's reactive, since the user likely wants
  // to mutate it. = isDef(style.__ob__)
    ? extend({}, style)
    : style
  const newStyle = getStyle(vnode, true)
  for (name in oldStyle) {
    if (isUndef(newStyle[name])) {
      setProp(el, name, ' ')}}for (name in newStyle) {
    cur = newStyle[name]
    if(cur ! == oldStyle[name]) {// ie9 setting to null has no effect, must use empty string
      setProp(el, name, cur == null ? ' ' : cur)
Copy the code

The general logic is to compare the new style with the old style to reset the style of the element.

This is a good way to dynamically extend capabilities at run time.

Why is the insertion hook queued instead of executed immediately

That’s because you need to ensure that the insert hook is the one that performs the insert after the element has actually been inserted into the DOM. This situation mainly occurs when the child component acts as the root node and is rendered for the first time. In this case, the actual DOM element itself is one, so it needs to wait until the initComponent of the parent component is inserted into the queue of the parent component patch, and then execute it.

This logic in the final phase of patch… It calls invokeInsertHook, which is related:

function invokeInsertHook (vnode, queue, initial) {
  // delay insert hooks for component root nodes, invoke them after the
  // element is really inserted
  // As we explained above
  if (isTrue(initial) && isDef(vnode.parent)) { = queue
  } else {
    // Call vNode's data.hook. Insert hook directly at other times
    for (let i = 0; i < queue.length; ++i) {
Copy the code

This is back to our setup hook logic. What does the insert hook do?…

insert (vnode: MountedComponentVNode) {
  const { context, componentInstance } = vnode
  if(! componentInstance._isMounted) {// It is not mounted
    componentInstance._isMounted = true
    // Invoke the component instance's mounted hook
    callHook(componentInstance, 'mounted')}// Keep alive
  if ( {
    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.
    } else {
      activateChildComponent(componentInstance, true /* direct */)}}}Copy the code

The child component (the instance of App component in our scenario) has not yet called the mounted hook, so it calls the mounted hook directly to complete the call to the mounted lifecycle hook.

The mounted lifecycle hook of the Vue instance is called after the patch of the original Vue instance is completed.

At this point, the entire initialization and mount process is basically complete, so here’s a review of the whole process:

  • The root instance CREATE phase is complete
  • Mount phase of the root instance
    • render
      • The child vNode component creates and installs hooks
    • patch
      • Encounter common elements
        • Creating a DOM element
      • In the component
        • Create child component instances (via init hook) & mount
        • Trigger child component mounted hooks (via insert hooks)
    • Mounted hook of the root instance is triggered

When a component is destroyed, it basically starts from updating the component, then patch it, and then triggers the destroy hook…

destroy (vnode: MountedComponentVNode) {
  const { componentInstance } = vnode
  if(! componentInstance._isDestroyed) {if(! { componentInstance.$destroy() }else {
      deactivateChildComponent(componentInstance, true /* direct */)}}}Copy the code

The rest is consistent with The destruction logic covered in The Vue-The Good Parts: Life Cycle article.


As we shared at the beginning about componentization and components, and throughout the history of the front end itself, componentization has been a best practice for development.

The core reason is that component-based development gives us the greatest benefit of all: divide and conquer, and the benefits of divide and conquer: split and isolate complexity.

Of course, there are many other benefits:

  • High cohesion, low coupling (via component specification constraints, such as props, Events, etc.)
    • Easy to develop and test
    • To facilitate collaborative
  • reuse
    • Available everywhere
  • Easy extension

With this, the ultimate goal of improving development efficiency and maintainability is achieved.


From the above analysis, we also have a better understanding of how componentization is implemented in Vue. Components all inherit from Vue, so they all have basically the same configuration, lifecycle, and API.

So aside from a deeper understanding of the components, and the whole point, what else can we learn from the Vue implementation?

Component design

In Vue component is designed according to class, although for the user, the more time you write is a common object, into a pair of configuration items, but in the Vue internal processing, or the way the extend of the transformation to a constructor, and instantiated, this is a classic inheritance thinking.

The configuration items we know about the Vue component include lifecycle hooks (create related, mount related, Update related, destroy related), props, data, methods, computed, watch, There are also DOM related el, Template, and Render. These options are also the most common part of everyday life, so we need to understand and understand the implementation and function behind them.

In addition, Vue components also contain resources related to… , combination related… And other… These configuration items, are also commonly used, interested in their own internal implementation and find out the essence of their implementation.

$props, $data, $EL, $attrs, $watch, $mount(), $destroy(), $on(), $off(), $emit(), $once(), etc. It can also be seen that the name starts with $, which is very standard. You can refer to the official website for more information.

There are also very useful dynamic components and asynchronous components, designed very friendly…

Plug-in thinking

The organization of modules, that is, modules passed into createPatchFunction. We also analyzed two modules examples above. It can be seen that, with the help of patch hooks designed by us at the VDOM level, we split many functions into modules, and each module does its own thing according to the timing of the hooks. Here you can also find that this is probably the use of plug-in thinking, plug-in thinking itself is a manifestation of the microkernel architecture. This point is also consistent with Vue’s overall design philosophy: an incremental framework.

Therefore, Vue basically follows its own design concept from some internal designs to the whole ecological construction, which is a very important practice and persistence and worth our thinking.

Other small Tips

  • Life cycle hooks (VDOM, directive) for different scenarios that appear again and again, see VUe-the Good Parts: Life cycle
  • MergeHook, implemented with closures, turns calls that might have required an array implementation into a single function call niubility
  • Vue.extend implementation, classic inheritance implementation…
  • How the Vue resolveConstructorOptions (considering inheritance)…

The team number of Didi front-end technology team has been online, and we have synchronized certain recruitment information. We will continue to add more positions, and interested students can chat with us.