componentization

If you think it is good, please send me a Star at GitHub

Vue2.0 source code analysis: responsive principle (below) next: Vue2.0 source code analysis: componentization (below)

Due to the word limit of digging gold article, we had to split the top and the next two articles.

introduce

In the previous chapters, we have mentioned the concept of components many times. Components come up frequently in our daily development process and are one of the two cores of Vue: data-driven and componentized.

In this chapter, we will focus on the knowledge related to componentization. We will explore the mystery of componentization from the entry file main.js.

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'

Vue.config.productionTip = false

new Vue({
  router,
  store,
  render: h= > h(App)
}).$mount('#app')
Copy the code

$mount method

The code analysis

We already know that before the Vue will according to the different situation to mount the different methods of $mount, which bring the compiler version of $mount method is in the SRC/platforms/web/entry – the runtime – with – compiler. Js file being redefined, The code is as follows:

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
) :Component {
  el = el && query(el)

  /* istanbul ignore if */
  if (el === document.body || el === document.documentElement) { process.env.NODE_ENV ! = ='production' && warn(
      `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
    )
    return this
  }

  const options = this.$options
  // resolve template/el and convert to render function
  if(! options.render) {let template = options.template
    if (template) {
      if (typeof template === 'string') {
        if (template.charAt(0) = = =The '#') {
          template = idToTemplate(template)
          /* istanbul ignore if */
          if(process.env.NODE_ENV ! = ='production' && !template) {
            warn(
              `Template element not found or is empty: ${options.template}`.this)}}}else if (template.nodeType) {
        template = template.innerHTML
      } else {
        if(process.env.NODE_ENV ! = ='production') {
          warn('invalid template option:' + template, this)}return this}}else if (el) {
      template = getOuterHTML(el)
    }
    if (template) {
      /* istanbul ignore if */
      if(process.env.NODE_ENV ! = ='production' && config.performance && mark) {
        mark('compile')}const { render, staticRenderFns } = compileToFunctions(template, {
        outputSourceRange: process.env.NODE_ENV ! = ='production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns

      /* istanbul ignore if */
      if(process.env.NODE_ENV ! = ='production' && config.performance && mark) {
        mark('compile end')
        measure(`vue The ${this._name} compile`.'compile'.'compile end')}}}return mount.call(this, el, hydrating)
}
Copy the code

Prototype, then redefines the $mount method on vue. prototype. At the bottom of the latest $mount method, it also calls the cached original $mount method.

Then, where is the original $mount method is defined, it is in the SRC/core/platforms/web/runtime/index are defined in the js, its code is as follows:

Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
) :Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}
Copy the code

Now that we understand the differences between the two $mount methods, we will first examine the compiler version of the $mount method implementation, which does three main things: fetching the EL element, processing the template, and calling the original $mount method. We will follow these steps to analyze the code separately.

Code analysis:

  • Get el element: I remember in themain.jsIn the entry file we call$mountMethod is passed#appParameters?
import Vue from 'vue'
import App from './App.vue'
new Vue({
  render: h= > h(App)
}).$mount('#app')
Copy the code

When executing the $mount method, the first thing to do is to get the DOM element node to be mounted based on the passed EL element. It uses the query method to get the DOM element node, which looks like this:

export function query (el: string | Element) :Element {
  if (typeof el === 'string') {
    const selected = document.querySelector(el)
    if(! selected) { process.env.NODE_ENV ! = ='production' && warn(
        'Cannot find element: ' + el
      )
      return document.createElement('div')}return selected
  } else {
    return el
  }
}
Copy the code

We can see that in the query method, the el parameter is checked first, and if it is not a string, it is returned directly. If so, the DOM element is retrieved via Document. querySelector. If not, a div element is created and returned with an error message.

After looking at the code above, we might have a question: when is the el parameter not a string? The $mount method accepts a DOM element node directly, which means we can write something like this in the entry file:

import Vue from 'vue'
import App from './App.vue'
new Vue({
  render: h= > h(App)
}).$mount(document.querySelector('#app'))

Copy the code

In the official Vue documentation, we’ve no doubt seen a warning that elements provided by EL can only be used as mount points. Unlike Vue 1.x, all mount elements are replaced by the DOM generated by Vue. Therefore, mounting root instances to HTML or body is not recommended.

In the $mount method, we can also see code indicating that we cannot mount directly to HTML or body:

if (el === document.body || el === document.documentElement) { process.env.NODE_ENV ! = ='production' && warn(
    `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
  )
  return this
}
Copy the code

The $mount method replaces the contents of the mounted node. If you mount HTML or body directly, you may lose meta, link, or script.

  • To deal with the template: handlingtemplateis$mountThe core of the method, the process is relatively complex, more code, but the process is relatively clear. First of all torenderJudge if there isrenderThen it will not be processed againtemplateThis part of the logic is up for a userenderThe example is oursmain.jsEntry file:
import Vue from 'vue'
import App from './App.vue'
new Vue({
  render: h= > h(App)
}).$mount('#app')
Copy the code

Since the render option is provided when the root instance is created, make the $options.render condition true in the $mount method and go straight to the last step: call the original $mount method.

Note: Actually we use Vue – Cli scaffolding to create project, components in the $mount method execution, there have been a render function, this is because the Vue – loader has helped us to convert the template to render function. Therefore, for most cases, the template processing is not performed, and only a few special cases are performed.

After analyzing the branches that provide render options, let’s take a look at the logic for handling templates without render options. Let’s take a look at what happens to template, using the following code as an example:

export default {
  name: 'HelloWorld',
  data () {
    return {
      msg: 'Welcome to Your Vue.js App'}},template: `<div class="hello">{{ msg }}</div>`
}
Copy the code

Template and typeof template === ‘string’ are both true, so compileToFunctions(template,…) are compileToFunctions(template,…) The main step is to compile the template into the render function, which we’ll explain in more detail later. After rendering, assigning render to options.render is similar to providing a render function manually.

Typeof template === ‘string’ typeof template === ‘string’ typeof template === ‘string’

if (template.charAt(0) = = =The '#') {
  template = idToTemplate(template)
  /* istanbul ignore if */
  if(process.env.NODE_ENV ! = ='production' && !template) {
    warn(
      `Template element not found or is empty: ${options.template}`.this)}}Copy the code

This is because template can be passed directly as the ID of a DOM node, for example:

export default {
  template: '#main'
}
Copy the code

IdToTemplate: {idToTemplate: {idToTemplate: {idToTemplate: {idToTemplate: {idToTemplate: {idToTemplate: {idToTemplate: {idToTemplate: {idToTemplate: {idToTemplate:}}

const idToTemplate = cached(id= > {
  const el = query(id)
  return el && el.innerHTML
})
Copy the code

All this code does is query the DOM element by ID and return its innerHTML content.

So the second question is, why is there an else if branch logic like this?

else if (template.nodeType) {
  template = template.innerHTML
}
Copy the code

This is because template can accept a DOM element node directly in addition to strings, for example:

<div id="main">
  <div>dom</div>
</div>
Copy the code
export default {
  name: 'HelloWorld'.template: document.querySelector('#main')}Copy the code

Last question, what happens if I pass neither render nor template? In fact, it will eventually degrade to get the EL option as follows:

else if (el) {
  template = getOuterHTML(el)
}
Copy the code

If neither render nor template is provided, the el option is used in the last step, and the outerHTML of the DOM element is retrieved via el. The difference between innerHTML and outerHTML is as follows:

// Simulate a DOM element
const dom = `
      
dom
`
const innerHTML = '<div>dom</div>' const outerHTML = `
dom
`
` Copy the code
  • ** calls primitive
    m o u n t methods : Finally, let’s analyze Mount method ** : Finally, we analyze ‘
    mountThe final step of the method is to focus on analyzing the original (common)$mount ` method. Let’s review the implementation code for this method:
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
) :Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}
Copy the code

In this method, the process of dealing with el before with no difference, then we analysis the focus of the fallen to mountComponent method, this method is defined in SRC/core/instance/lifecycle. The js file, the code is as follows:

export function mountComponent (vm: Component, el: ? Element, hydrating? : boolean) :Component {
  vm.$el = el
  if(! vm.$options.render) { vm.$options.render = createEmptyVNodeif(process.env.NODE_ENV ! = ='production') {
      /* istanbul ignore if */
      if ((vm.$options.template && vm.$options.template.charAt(0)! = =The '#') ||
        vm.$options.el || el) {
        warn(
          'You are using the runtime-only build of Vue where the template ' +
          'compiler is not available. Either pre-compile the templates into ' +
          'render functions, or use the compiler-included build.',
          vm
        )
      } else {
        warn(
          'Failed to mount component: template or render function not defined.',
          vm
        )
      }
    }
  }
  callHook(vm, 'beforeMount')

  let updateComponent
  /* istanbul ignore if */
  if(process.env.NODE_ENV ! = ='production' && config.performance && mark) {
    updateComponent = () = > {
      const name = vm._name
      const id = vm._uid
      const startTag = `vue-perf-start:${id}`
      const endTag = `vue-perf-end:${id}`

      mark(startTag)
      const vnode = vm._render()
      mark(endTag)
      measure(`vue ${name} render`, startTag, endTag)

      mark(startTag)
      vm._update(vnode, hydrating)
      mark(endTag)
      measure(`vue ${name} patch`, startTag, endTag)
    }
  } else {
    updateComponent = () = > {
      vm._update(vm._render(), hydrating)
    }
  }

  // we set this to vm._watcher inside the watcher's constructor
  // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  // component's mounted hook), which relies on vm._watcher being already defined
  new Watcher(vm, updateComponent, noop, {
    before () {
      if(vm._isMounted && ! vm._isDestroyed) { callHook(vm,'beforeUpdate')}}},true /* isRenderWatcher */)
  hydrating = false

  // manually mounted instance, call mounted on self
  // mounted is called for render-created child components in its inserted hook
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')}return vm
}
Copy the code

The mountComponent method looks like a lot of code, but it doesn’t have to be complicated. It can be divided into three steps: callHook triggers the lifecycle function, define updateComponent, and define render Watcher.

  1. CallHook triggers the lifecycle function: This part is the easiest and only needs to be calledcallHookMethod to trigger the corresponding life cycle, inmountComponentThere are three places in the method that trigger the lifecycle:beforeMount.mountedandbeforeUpdate.
  2. Define updateComponentDefinition:updateComponentThe method we just have to look atelseBranch,ifThe branch mainly does the performance related thing, which is to open the browserperformanceWhen used.updateComponentMethod is calledvm._update()The main function of this method is to trigger component rerendering, whilevm._render()We’ve talked about that before.
  3. Define render WatcherIn:mountComponentMethod defines a renderWatcher, where renderWatcherThe second argument to theupdateComponent, this parameter will be used in renderingWatcherInstantiate and assign tothis.getterProperty, which is traversed when an update is distributedsubsTo perform an arrayupdateAnd then callthis.getter, which is called againupdateComponent, and then let the component re-render.

The flow chart

After analyzing the $mount method, we can get the following flow chart:

Render and renderProxy

After $mount, let’s take a look at render and the logic behind renderProxy. The main goal of this section is to understand what renderProxy does and how render works.

renderProxy

In the initMixin method we introduced earlier, we have the following code:

import { initProxy } from './proxy'
export default initMixin (Vue) {
  Vue.prototype._init = function () {
    // ...
    if(process.env.NODE_ENV ! = ='production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // ...}}Copy the code

InitProxy is defined in SRC/core/instance/proxy. A method of js file, the code is as follows:

let initProxy
initProxy = function initProxy (vm) {
  if (hasProxy) {
    // determine which proxy handler to use
    const options = vm.$options
    const handlers = options.render && options.render._withStripped
      ? getHandler
      : hasHandler
    vm._renderProxy = new Proxy(vm, handlers)
  } else {
    vm._renderProxy = vm
  }
}
Copy the code

Code analysis:

  • This method first determines whether the current environment supports nativeProxyCreate one if it is supportedProxyAgent, wherehasProxyIs abooleanValue, whose implementation logic is as follows:
const hasProxy = typeof Proxy! = ='undefined' && isNative(Proxy)
Copy the code
  • Then according to theoptions.renderandoptions.render._withStrippedTo select usegetHandlerorhasHandlerWhen usingvue-loaderparsing.vueFile when this timeoptions.render._withStrippedIs the true value, therefore selectedgetHandler. When choosing to usecompilerVersion of theVue.jsThe root instance in our entry file is defined like this:
import Vue from 'vue'
import App from './App'
new Vue({
  el: '#app'.components: { App },
  template: '<App/>'
})
Copy the code

For the root instance, options.render._withStripped is undefined, so use hasHandler. After figuring out when to use getHandler and hasHandler, we might have another question: What do getHandler and hasHandler do? How?

Before answering the first question, let’s take a look at the definitions of getHandler and hasHandler:

const allowedGlobals = makeMap(
  'Infinity,undefined,NaN,isFinite,isNaN,' +
  'parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,' +
  'Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,' +
  'require' // for Webpack/Browserify
)

const warnNonPresent = (target, key) = > {
  warn(
    `Property or method "${key}" is not defined on the instance but ` +
    'referenced during render. Make sure that this property is reactive, ' +
    'either in the data option, or for class-based components, by ' +
    'initializing the property. ' +
    'See: https://vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.',
    target
  )
}

const warnReservedPrefix = (target, key) = > {
  warn(
    `Property "${key}" must be accessed with "$data.${key}" because ` +
    'properties starting with "$" or "_" are not proxied in the Vue instance to ' +
    'prevent conflicts with Vue internals. ' +
    'See: https://vuejs.org/v2/api/#data',
    target
  )
}

const hasHandler = {
  has (target, key) {
    const has = key in target
    const isAllowed = allowedGlobals(key) ||
      (typeof key === 'string' && key.charAt(0) = = ='_' && !(key in target.$data))
    if(! has && ! isAllowed) {if (key in target.$data) warnReservedPrefix(target, key)
      else warnNonPresent(target, key)
    }
    returnhas || ! isAllowed } }const getHandler = {
  get (target, key) {
    if (typeof key === 'string' && !(key in target)) {
      if (key in target.$data) warnReservedPrefix(target, key)
      else warnNonPresent(target, key)
    }
    return target[key]
  }
}
Copy the code

As you can see, getHandler and hasHandler do pretty much the same thing, judging and processing illegal data during the rendering phase. In the case of warnNonPresent, it indicates that we are using undefined variables in the template; In the case of warnReservedPrefix, it tells us that we should not define variables that start with $or _, because it is easy to confuse them with internal attributes.

<template> {{msg1}} {{$age}} </template> <script> export default {data () {return {MSG: 'message', $age: 23 } } } </script>Copy the code

Then, our second question: how do getHandler and hasHandler trigger? This actually involves a little bit of ES6 Proxy knowledge, let’s use the following code as an example to illustrate:

const obj = {
  a: 1.b: 2.c: 3
}
const proxy = new Proxy(obj, {
  has (target, key) {
    console.log(key)
    return key in target
  },
  get (target, key) {
    console.log(key)
    return target[key]
  }
})

// Trigger getHandler, output a
proxy.a 

// Trigger hasHandler, output b c
with(proxy){
  const d = b + c
}
Copy the code

In the above code, we define a proxy proxy. When we access proxy.a, getHandler will be triggered based on the knowledge of proxy, so a will be output. When we access the proxy using with, any access to the attributes in the proxy triggers the hasHandler and therefore prints b and C.

Now that the code has been analyzed, we can summarize what initProxy does: judge and process invalid data during the render phase.

render

In the previous code, we encountered the following code in mountComponent:

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

In this section, we analyze and _render function implementation, it is in the SRC/core/instance/render js files are defined:

export function renderMixin (Vue) {
  Vue.prototype._render = function () :VNode {
    const vm: Component = this
    const { render, _parentVnode } = vm.$options
    / /... Omit code
    let vnode
    try {
      // There's no need to maintain a stack because all render fns are called
      // separately from one another. Nested component's render fns are called
      // when parent component is patched.
      currentRenderingInstance = vm
      vnode = render.call(vm._renderProxy, vm.$createElement)
    } catch (e) {
      handleError(e, vm, `render`)}/ /... Omit code
    vnode.parent = _parentVnode
    return vnode
  }
}
Copy the code

$options = $options = $options = $options = $options = $options = $options = $options = $options The most important step in the _render code is the call to the render. Call function, which returns VNode, which will be used later in the process.

When we call the render. Call method, in addition to passing our renderProxy, we pass a $createElement function, which is defined in the initRender method:

export function initRender (vm) {
  / /... Omit code
  vm._c = (a, b, c, d) = > createElement(vm, a, b, c, d, false)
  // normalization is always applied for the public version, used in
  // user-written render functions.
  vm.$createElement = (a, b, c, d) = > createElement(vm, a, b, c, d, true)
  / /... Omit code
}
Copy the code

We can see that the function definitions for vm.$createElement and vm._c are similar, with the only difference being that the last parameter passed when the createElement method is called is different. The $createElement and _c methods have similar definitions, but the scenarios are different. $createElement is usually used with the user-provided render, while _c is usually used with the template-generated Render.

Using the render function definition, we can rewrite the template example to render:

<template>
  <div id="app">
    {{msg}}
  </div>
</template>
<script>
export default () {
  data () {
    return {
      msg: 'message'
    }
  }
}
</script>
Copy the code

Render:

export default {
  data () {
    return {
      msg: 'message'}},render: ($createElement) {
    return  $createElement('div', {
      attrs: {
        id: 'app'}},this.message)
  }
}
Copy the code

In this section we look at the implementation of Render, and in the next section we’ll dive into the implementation of the createElement method.

createElement

In the previous section, we saw that the render function would call either $createElement or _c, and that they would end up calling the same createElement method with a slightly different last argument. In this section, we’ll take a closer look at the implementation logic of the createElement method.

CreateElement is defined in SRC /core/vdom/create-element.js.

const SIMPLE_NORMALIZE = 1
const ALWAYS_NORMALIZE = 2
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)
}
Copy the code

Before we look at the code, let’s take a look at where the last difference between the $createElement and _c methods occurs in createElement. We can guess from the last parameter name, for template compilation calls to _c, its alwaysNormalize is passed false, because _c is only used internally, so the parameter format for method calls is more formal, so we don’t need to normalize too much. $createElement is provided to the user. To make $createElement simple and useful, we must always normalize by allowing the user to pass in different forms of arguments to call $createElement, which results in user-written render.

Now that this analysis is complete, we know where the final difference between $createElement and _c is: children are simply normalized when called to _c, and children must always be normalized when called to $createElement.

Getting back to the point, we see that createElement is actually a wrapper around the _createElement method, which is designed to provide a function-like overloading of createElement (JavaScript doesn’t actually have this concept). The third parameter, data, is not passed.

// Do not pass data
createElement(this.'div'.'Hello, Vue'.1.false)
/ / transfer data
createElement(this.'div'.undefined.'Hello, Vue'.1.false)
Copy the code

When we do not pass data, we need to move the third and fourth arguments back one position, assign data to undefined, and pass the processed argument to _createElement. Let’s look at the _createElement method parameters:

  • context:VNodeCurrent up and down environment.
  • tag: tag, which can be normalHTMLElement tag, which can also beComponentComponents.
  • data:VNodeIs of typeVNodeData, can be in the root directoryflow/vnode.jsSee the specific definition in the file.
  • children:VNodeChild node of.
  • normalizationType:childrenCanonical type of child node.

The specific implementation code is as follows:

export function _createElement (context: Component, tag? : string | Class<Component> |Function | Object, data? : VNodeData, children? : any, normalizationType? : number) :VNode | Array<VNode> {
  / /... Omit code
  if(! tag) {// in case of component :is set to falsy value
    return createEmptyVNode()
  }
  if (normalizationType === ALWAYS_NORMALIZE) {
    children = normalizeChildren(children)
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    children = simpleNormalizeChildren(children)
  }
  let vnode, ns
  if (typeof tag === 'string') {
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    if (config.isReservedTag(tag)) {
      // platform built-in elements
      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))) {
      // 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)
  }
  / /... Omit code
}
Copy the code

The _createElement code may seem a bit heavy, but it does two things: normalize child nodes and create vNodes, which we’ll cover in more detail.

  • Normalize the child node: Because virtualDOMIs a tree structure, and each node should beVNodeType, butchildrenThe parameter is arbitrary, so if we have a child node, we need to normalize itVNodeType, if there are no child nodes, thenchildrenisundefined. As for how to normalize, it is throughnormalizationTypeParameter to achieve, wherenormalizationTypeThere are only three possible values:undefinedMeans that it is not normalized,1Stands for simple normalization,2Means always normalized. So let’s look at the period of zero1The case that it callssimpleNormalizeChildren, this method andnormalizeChildrenThe definition is in the same placesrc/core/vdom/helpers/normalize-children.jsIn the file, the code is as follows:
export function simpleNormalizeChildren (children: any) {
  for (let i = 0; i < children.length; i++) {
    if (Array.isArray(children[i])) {
      return Array.prototype.concat.apply([], children)
    }
  }
  return children
}
Copy the code

SimpleNormalizeChildren reduces a multidimensional array by one dimension, such as a two-dimensional array to a one-dimensional array and a three-dimensional array to a two-dimensional array, to facilitate subsequent traversal of children.

// The example is VNode
let children = ['VNode'['VNode'.'VNode'].'VNode']

// Simply normalize the child node
children = simpleNormalizeChildren(children)

// after normalization
console.log(children) // ['VNode', 'VNode', 'VNode', 'VNode']
Copy the code

Next we look at the case of 2, which calls normalizeChildren with the following code: normalizeChildren

export function normalizeChildren (children: any): ?Array<VNode> {
  return isPrimitive(children)
    ? [createTextVNode(children)]
    : Array.isArray(children)
      ? normalizeArrayChildren(children)
      : undefined
}
Copy the code

NormalizeChildren code is not very large or complex. When children is a base type value, it returns a VNode array of text nodes, createTextVNode, as we described earlier. If not, then check whether it is an array, if not, its children is undefined, if yes, call normalizeArrayChildren to normalize. Next, we will focus on the following normalizeArrayChildren implementation, which is defined in the same location as normalizeChildren, its implementation code is as follows:

function normalizeArrayChildren (children: any, nestedIndex? : string) :Array<VNode> {
  const res = []
  let i, c, lastIndex, last
  for (i = 0; i < children.length; i++) {
    c = children[i]
    if (isUndef(c) || typeof c === 'boolean') continue
    lastIndex = res.length - 1
    last = res[lastIndex]
    // nested
    if (Array.isArray(c)) {
      if (c.length > 0) {
        c = normalizeArrayChildren(c, `${nestedIndex || ' '}_${i}`)
        // merge adjacent text nodes
        if (isTextNode(c[0]) && isTextNode(last)) {
          res[lastIndex] = createTextVNode(last.text + (c[0]: any).text)
          c.shift()
        }
        res.push.apply(res, c)
      }
    } else if (isPrimitive(c)) {
      if (isTextNode(last)) {
        // merge adjacent text nodes
        // this is necessary for SSR hydration because text nodes are
        // essentially merged when rendered to HTML strings
        res[lastIndex] = createTextVNode(last.text + c)
      } else if(c ! = =' ') {
        // convert primitive to vnode
        res.push(createTextVNode(c))
      }
    } else {
      if (isTextNode(c) && isTextNode(last)) {
        // merge adjacent text nodes
        res[lastIndex] = createTextVNode(last.text + c.text)
      } else {
        // default key for nested array children (likely generated by v-for)
        if (isTrue(children._isVList) &&
          isDef(c.tag) &&
          isUndef(c.key) &&
          isDef(nestedIndex)) {
          c.key = `__vlist${nestedIndex}_${i}__ `
        }
        res.push(c)
      }
    }
  }
  return res
}
Copy the code

Although normalizeArrayChildren has a lot of code, it’s not that complicated to do, and we just need to focus on a few important logical branches of the traversal process.

  1. Traversal items are arrays: This is a slightly more complicated case and is commonv-fororslot“, nesting occursVNodeArray case if there is nestingVNodeThe case is recursively callednormalizeArrayChildren, let’s take the following example:
<template>
  <div id="app">
    <p>{{msg}}</p>
    <span v-for="(item, index) in list" :key="index">{{item}}</span>
  </div>
</template>
<script>
export default {
  name: 'App',
  data () {
    return {
      msg: 'message',
      list: [1, 2, 3]
    }
  }
}
</script>
Copy the code

When the render function of the App component executes, the children node of the App component will display a nested array of vNodes, as shown in the following code example:

const children = [
  [ { tag: 'p'},... ] , [[{tag: 'span'. }], [{tag: 'span'. }], [{tag: 'span'. }]]]Copy the code

By recursively calling the normalizeArrayChildren method, the nested array is processed into a one-dimensional array as follows:

const children = [
  [ { tag: 'p'},... ] [{tag: 'span'. }], [{tag: 'span'. }], [{tag: 'span'. }]]Copy the code
  1. Traversal item base type: When base type is invoked, encapsulatedcreateTextVNodeMethod to create a text node, andpushTo the result array.
  2. The traversal item is alreadyVNodeType: This is the simplest case, if it does not fall into either of the above, then the delegate itself is alreadyVNodeThat’s when we don’t have to do anything. RightpushIt goes into the result array.

In all three logical branches, istextNodes are judged, and this part of the code is mainly used to optimize text nodes: if there are two consecutive text nodes, merge them into one text node.

/ / before the merger
const children = [
  { text: 'Hello '. }, {text: 'Vue.js'. },]/ / after the merger
const children = [
  { text: 'Hello Vue.js'. }]Copy the code
  • Example Create a VNode: createVNodeThere are two main branches of node logic,tagforstringThe type andcomponentType, wherestringThere are several small logical judgment branches of the type. increateElementChapter, we focus on the type asstringThe branch. In this branch, first judgetagIs the tag name provided platform reserved tag (htmlorsvgIf yes, create the corresponding label directlyVNodeNode, if not, try to match in the global or local registered component. If the match succeeds, usecreateComponentTo create a component node, if there is no match, create an unknown labelVNodeNodes, such as:
<template>
  <div id="app">
    <div>{{msg}}</div>
    <hello-world :msg="msg" />
    <cms>12321321</cms>
  </div>
</template>
<script>
import HelloWorld from '@/components/HelloWorld.vue'
export default {
  name: 'App',
  data () {
    return {
      msg: 'message',
    }
  },
  components: {
    HelloWorld
  }
}
</script>
Copy the code

Tag is CMS, but it is not a platform reserved tag like div, nor is it a locally registered component like hello-world. It is an unknown tag. The parent node may provide a namespace for the child node during the createElement process. The verification of unknown tags occurs in the PATH phase, which will be described in the following sections.

createComponent

In the createElement section, we mentioned that the createComponent method is called in two places. In this section, we will examine the implementation logic of the createComponent method in detail.

CreateComponent is defined in the SRC /core/vmode/create-component.js file with the following code:

export function createComponent (
  Ctor: Class<Component> | Function | Object | void, data: ? VNodeData, context: Component, children: ?Array<VNode>, tag? : string) :VNode | Array<VNode> | void {
  / /... Omit the other
  const baseCtor = context.$options._base
  // plain options object: turn it into a constructor
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor)
  }

  data = data || {}

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

  // 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
  )
  / /... Omit the other
  return vnode
}
Copy the code

Because the createComponent method has many function points to implement, this is our simplified code. The code that has been simplified is component validation related, asynchronous component related, prosData related, abstract component related, and WEEX related.

When analyzing the createComponent method, we focused on two areas: constructing the subclass constructor and installing the component hook function. In this step, we only need to know that the third parameter passed to the VNode constructor is undefined, which means that the VNode has no children node. Because the value is undefined.

Code analysis:

  • Construct the subclass constructor: At the beginning of the code, the first pass$options._baseTake the base constructor, the base constructor is just bigVueConstructor of,$options._baseThe assignment process is ininitGlobalAPIThe value assigned during the execution of the function.
export function initGlobalAPI (Vue) {
  Vue.options._base = Vue
}
Copy the code

According to the rules we introduced earlier, the properties on options can be retrieved later by using $options, because the mergeOptions configuration is merged during the this._init method.

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

Let’s look at createComponent’s first parameter again, using the app.vue component as an example:

import HelloWorld from '@/components/HelloWorld.vue'
export default {
  name: 'App',
  data () {
    return {
      msg: 'message'.age: 23.list: [1.2.3]}},components: {
    HelloWorld
  }
}
Copy the code

We exported an object in the app. vue component, which defined three attributes of name, data and components, so the Ctor parameter should be this object, but when we actually debug, we found that there are more Ctor attributes than we expected. This is because vue-loader does some processing for us by default when processing.vue files. Here are the Ctor parameters for actual debugging of app.vue:

const Ctor = {
  beforeCreate: [function () {}].beforeDestroy: [function () {}].components: {
    HelloWorld
  },
  data () {
    return {
      msg: 'message'.age: 23.list: [1.2.3]}},name: 'App'.render: function () {},
  staticRenderFns: [].__file: './App.vue'._compiled: true
}
Copy the code

Next, let’s look at baseCtor. Extend. The global extend method is defined where we’ve shown it before, when initExtend is called in the initGlobalAPI method, InitExtend is defined in SRC /core/global-api/extend.js, and the code is as follows:

export function initExtend (Vue: GlobalAPI) {
  Vue.cid = 0
  let cid = 1

  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
  }
}
Copy the code

Let’s take a look at the core of the vue. extend method:

const Super = this
const Sub = function VueComponent (options) {
  this._init(options)
}
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
Copy the code

The extend method uses the classic parasitic combinatorial inheritance method to allow subclasses to inherit properties and methods from their parent. Before the prototype inherits, the this._init method is first called, the logic of which has already been mentioned and won’t be covered here. The Sub subclass has all the properties and methods of the Super superclass. For example:

const Super = function () {
  this.id = 1
  this.name = 'Super'
}
Super.prototype.say = function () {
  console.log('hello Super')}const Sub = function () {
  Super.call(this)
}
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub

const sub = new Sub()
console.log(sub.id)   / / 1
console.log(sub.name) // Super
sub.say()             // hellp Super
Copy the code

Let’s look at some more code:

const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
if (cachedCtors[SuperId]) {
  return cachedCtors[SuperId]
}
cachedCtors[SuperId] = Sub
Copy the code

The following code is used for caching: If we first introduced the header.vue component in the A.vue file, it would execute extend, and then we introduced header.vue in the B.vue file.

// A.vue
import MHeader from '@/components/header.vue'
export default {
  name: 'AComponent'.components: {
    MHeader
  }
}

// B.vue
import MHeader from '@/components/header.vue'
export default {
  name: 'BComponent'.components: {
    MHeader
  }
}

// header.vue extends only once.
Copy the code

Finally, after the inheritance is complete, it also handles props, computed, and various global API methods. The logic for this part is the same as what we mentioned before.

  • Install component hook functions: As we mentioned earlier,VueThe virtualDOMBorrowed from open source librariessnabbdomThe implementation of this libraryVNodeThe node is in different scenarios and provides corresponding hook functions to facilitate us to process the related logic. These hook functions are as follows:

These hook functions are also used in Vue and are defined as follows:

const componentVNodeHooks = {
  init: function () {},     // Triggered during initialization
  prepatch: function () {}, // Patch is triggered before
  insert: function () {},   // when inserted into the DOM
  destroy: function () {}   // Triggered before node removal
}
Copy the code

Let’s look at the definition of the installComponentHooks method:

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 } } }Copy the code

When the installComponentHooks method executes, we iterate over the properties of the hooks we defined, and then assign them to the parameters we passed. There is one thing worth noting: If you already have the same hook, the mergeHook method is executed to merge it.

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
}
Copy the code

Let’s take the following code as an example:

/ / before the merger
const hooks = {
  init: function () {
    console.log('init hook 1')}}const vnode = {
  data: {
    hook: {
      init: function () {
        console.log('init hook 2')}}}}/ / merge
mergeHook()

/ / after the merger
const vnode = {
  data: {
    hook: {
      init: () = > {
        init1(),
        init2()
      }
    }
  }
}
Copy the code

In the createComponent section, we explained that components do mergeOptions configuration merge. To better understand the path process, we will introduce mergeOptions configuration merge strategy first in the following sections.

Merge strategy

In this section, merge strategy is explained in three steps: background of configuration merge, scenario of configuration merge, and merge strategy.

background

We might wonder, why do configuration merges? This is because there are some default configurations in the Vue, which allow us to provide some custom configurations during initialization for the purpose of customizing individual requirements in different scenarios. If you look at some of the best open source libraries and frameworks, the design philosophy is almost always similar.

To illustrate the background of configuration merge:

Vue.mixin({
  created () {
    console.log('global created mixin')
  },
  mounted () {
    console.log('global mounted mixin')}})Copy the code

If we use vue. mixin to globally mix two lifecycle configurations created and Mounted, then in our application both lifecycle configurations are reflected in each instance, whether it is the root instance or the component instance. However, root instances or component instances can also have their own Created or Mounted configurations, which can cause unexpected problems if configuration consolidation is not done properly.

scenario

There are more than one or two scenarios for configuration consolidation, and we mainly introduce the following four scenarios:

  • vue-loader: We mentioned earlier when we use.vueFile form for development when due.vueBelongs to a special file extension,webpackCannot be identified natively, so a correspondingloaderTo parse it, it’s justvue-loader. Suppose we write the followingHelloWorld.vueComponent and then import it somewhere else.
// HelloWorld.vue
export default {
  name: 'HelloWorld',
  data () {
    return {
      msg: 'hello, world'}}}// App.vue
import HelloWorld from '@/components/HelloWorld.vue'
export default {
  name: 'App'.components: {
    HelloWorld
  }
}
Copy the code

In the helloWorld.vue file, we only provided name and data configuration options, but when we actually debug the HelloWorld component instance, we found that there are many additional properties because vue-loader added them by default.

const HelloWorld = {
  beforeCreate: [function () {}].beforeDestroy: [function () {}].name: 'HelloWorld',
  data () {
    return {
      msg: 'hello, world'}},... }Copy the code

We can see that vue-loader adds beforeCreate and beforeDestroy by default. This situation must be combined if our component also provides these two configurations.

  • extend: In the last section we introducedcreateComponentWe know that the child component inherits largeVueSome properties or methods on, assuming we registered a component globally.
import HelloWorld from '@/components/HelloWorld.vue'
Vue.component('HelloWorld', HelloWorld)
Copy the code

When we register components in other components, components on the large Vue should be properly configured and merged with components in the component.

  • mixin: In the frontConfiguration Merge BackgroundSection, we useVue.mixinThe global is mixed with two lifecycle configurations, which belong tomixinConfigure the scope of the merge, for example, within another componentmixinBlend in the scene:
/ / a mixin definition
const sayMixin = {
  created () {
    console.log('hello mixin created')
  },
  mounted () {
    console.log('hello mixin mounted')}}// Components introduce mixins
export default {
  name: 'App'.mixins: [sayMixin],
  created () {
    console.log('app component created')
  },
  mounted () {
    console.log('app component mounted')}}Copy the code

When providing mixins selection in the app.vue component, configuration merge is also required because sayMixin we define also provides both Created and Mounted lifecycle configurations. Since mixins accept an array option, if we pass multiple mixins that have already been defined, and those mixins may exist that provide the same configuration, configuration merge is also required.

Note: The vue. mixin global API method internally calls mergeOptions to blend in, which is defined in the previous initGlobalAPI section and implemented as follows:

import { mergeOptions } from '.. /util/index'

export function initMixin (Vue: GlobalAPI) {
  Vue.mixin = function (mixin: Object) {
    this.options = mergeOptions(this.options, mixin)
    return this}}Copy the code
  • this._initStrictly speaking, this is not a configuration merge scenario, but should be a means of configuration merge. For the first typevue-loaderAnd the secondextendThey will be there if necessarythis._initConfiguration merge, for example, is called in the constructor when the child component is instantiatedthis._init:
const Sub = function VueComponent (options) {
  this._init(options)
}

Vue.prototype._init = function () {
  / /... Omit the other
  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
    )
  }
  / /... Omit the other
}
Copy the code

Merge strategy

SRC /core/util/options.js file. It looks like this:

export function mergeOptions (
  parent: Object,
  child: Object, vm? : Component) :Object {
  if(process.env.NODE_ENV ! = ='production') {
    checkComponents(child)
  }

  if (typeof child === 'function') {
    child = child.options
  }

  normalizeProps(child, vm)
  normalizeInject(child, vm)
  normalizeDirectives(child)

  // Apply extends and mixins on the child options,
  // but only if it is a raw options object that isn't
  // the result of another mergeOptions call.
  // Only merged options has the _base property.
  if(! child._base) {if (child.extends) {
      parent = mergeOptions(parent, child.extends, vm)
    }
    if (child.mixins) {
      for (let i = 0, l = child.mixins.length; i < l; i++) {
        parent = mergeOptions(parent, child.mixins[i], vm)
      }
    }
  }

  const options = {}
  let key
  for (key in parent) {
    mergeField(key)
  }
  for (key in child) {
    if(! hasOwn(parent, key)) { mergeField(key) } }function mergeField (key) {
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options
}
Copy the code

Let’s ignore the rest of the mergeOptions method and look at the core of mergeField. In this method, it calls the strats policy method based on different keys and assigns the combined configuration to options. The definition of each key of the Strats policy object will be described in the following sections.

Default Merge Policy

In the mergeField method, we see that defaultStrat default merge policy is used when there is no corresponding policy method for the passed key, which is defined as follows:

const defaultStrat = function (parentVal: any, childVal: any) :any {
  return childVal === undefined
    ? parentVal
    : childVal
}
Copy the code

The code for defaultStrat’s default merge policy is very simple: simply overwrite existing values, for example:

const defaultStrat = function (parentVal, childVal) {
  return childVal === undefined
    ? parentVal
    : childVal
}
const parent = {
  age: 23.name: 'parent'.sex: 1
}
const child = {
  age: undefined.name: 'child'.address: 'guangzhou'
}
function mergeOptions (parent, child) {
  let options = {}
  for (const key in parent) {
    mergeField(key)
  }
  for (const key in child) {
    if(! parent.hasOwnProperty(key)) { mergeField(key) } }function mergeField (key) {
    options[key] = defaultStrat(parent[key], child[key])
  }
  return options
}
const $options = mergeOptions(parent, child)
console.log($options) // {age: 23, name: 'child', sex: 1, address: 'guangzhou'}
Copy the code

In this case, both age and name exist in the parent and child objects. The child.age value is undefined, so the parent. Since the value of child.name is not undefined, the value of child.name is taken last, which also applies to the merge of the address attribute.

Note: if you want to for a select modify its default merger strategy, you can use the Vue. Config. OptionMergeStrategies to configuration, such as:

// Customize the merge strategy selected by the el, take only the second parameter.
import Vue from 'vue'
Vue.config.optionMergeStrategies.el = (toVal, fromVal) {
  return fromVal
}
Copy the code

El and propsData merge

For the merge of el and propsData attributes, the default merge policy is used in Vue, which is defined as follows:

const strats = config.optionMergeStrategies
if(process.env.NODE_ENV ! = ='production') {
  strats.el = strats.propsData = function (parent, child, vm, key) {
    / /... Omit the other
    return defaultStrat(parent, child)
  }
}
Copy the code

For the el and propsData options, the reason for using the default merge strategy is simple, because only one copy is allowed for EL and propsData.

Life cycle hooks merge

For lifecycle hook functions, they are merged using the mergeHook method. The strats policy object defines the hooks property as follows:

export const LIFECYCLE_HOOKS = [
  'beforeCreate'.'created'.'beforeMount'.'mounted'.'beforeUpdate'.'updated'.'beforeDestroy'.'destroyed'.'activated'.'deactivated'.'errorCaptured'.'serverPrefetch'
]

LIFECYCLE_HOOKS.forEach(hook= > {
  strats[hook] = mergeHook
})
Copy the code

Let’s take a look at how mergeHook is implemented. The code is as follows:

function mergeHook (
  parentVal: ?Array<Function>,
  childVal: ?Function|?Array<Function>
): ?Array<Function> {
  const res = childVal
    ? parentVal
      ? parentVal.concat(childVal)
      : Array.isArray(childVal)
        ? childVal
        : [childVal]
    : parentVal
  return res
    ? dedupeHooks(res)
    : res
}

function dedupeHooks (hooks) {
  const res = []
  for (let i = 0; i < hooks.length; i++) {
    if (res.indexOf(hooks[i]) === -1) {
      res.push(hooks[i])
    }
  }
  return res
}
Copy the code

We can see that in the mergeHook method, it uses three levels of ternary operation to determine whether there is childVal. If not, it directly returns parentVal. ParentVal = parentVal; parentVal = parentVal; parentVal = parentVal; If not, we need to check whether childVal is an array. If not, we return it as an array. If it is already an array, we return it.

Finally, we determine the RES, and then we call dedupeHooks. This method simply strips the array of duplicates. Finally, we write several cases according to the above logic to illustrate.

/ / a
const parentVal = [function created1 () {}]
const childVal = undefined
const res = [function created1 () {}]

/ / 2
const parentVal = [function created1 () {}]
const childVal = [function created2 () {}]
const res = [function created1 () {}, function created2 () {}]

/ / is three
const parentVal = undefined
const childVal = [function created2 () {}]
const res = [function created2 () {}]
Copy the code

Let’s look at a more specific scenario:

// mixin.js
export const sayMixin = {
  created () {
    console.log('say mixin created')}}export const helloMixin = {
  created () {
    console.log('hello mixin created')}}// App.vue
export default {
  name: 'App',
  created () {
    console.log('component created')}}// Order of execution
// say mixin created
// hello mixin created
// component created
Copy the code

Code analysis: We can see that the Created lifecycle functions in mixins take precedence over the created lifecycle functions provided by the component itself because the extends and Mixins options are handled first before the parent and Child properties are traversed. Taking mixins as an example, it first iterates through the mixins array we provided, then merges these configurations into the parent according to the rules, and finally merges its own configurations into the corresponding location after iterating through the child’s attributes. In the example we provided, Self-provided created is appended to the end of the array using the array concat method. When a component triggers the Created life cycle, it is called in array order.

if(! child._base) {if (child.extends) {
    parent = mergeOptions(parent, child.extends, vm)
  }
  if (child.mixins) {
    for (let i = 0, l = child.mixins.length; i < l; i++) {
      parent = mergeOptions(parent, child.mixins[i], vm)
    }
  }
}
Copy the code

Merge data and provide

For data and provide, they are eventually merged using mergeDataOrFn, except for the special data option, which requires a separate layer. Their properties on the Strats policy object are defined as follows:

strats.data = function (parentVal: any, childVal: any, vm? : Component): ?Function {
  if(! vm) {if (childVal && typeofchildVal ! = ='function') { process.env.NODE_ENV ! = ='production' && warn(
        'The "data" option should be a function ' +
        'that returns a per-instance value in component ' +
        'definitions.',
        vm
      )

      return parentVal
    }
    return mergeDataOrFn(parentVal, childVal)
  }

  return mergeDataOrFn(parentVal, childVal, vm)
}
strats.provide = mergeDataOrFn
Copy the code

In the wrapper function that merges data, childVal is checked and returns an error message if it is not a function type. If so, call the mergeDataOrFn method to merge. Next, let’s look at the implementation logic of the mergeDataOrFn method:

export function mergeDataOrFn (parentVal: any, childVal: any, vm? : Component): ?Function {
  if(! vm) {// in a Vue.extend merge, both should be functions
    if(! childVal) {return parentVal
    }
    if(! parentVal) {return childVal
    }
    // when parentVal & childVal are both present,
    // we need to return a function that returns the
    // merged result of both functions... no need to
    // check if parentVal is a function here because
    // it has to be a function to pass previous merges.
    return function mergedDataFn () {
      return mergeData(
        typeof childVal === 'function' ? childVal.call(this.this) : childVal,
        typeof parentVal === 'function' ? parentVal.call(this.this) : parentVal
      )
    }
  } else {
    return function mergedInstanceDataFn () {
      // instance merge
      const instanceData = typeof childVal === 'function'
        ? childVal.call(vm, vm)
        : childVal
      const defaultData = typeof parentVal === 'function'
        ? parentVal.call(vm, vm)
        : parentVal
      if (instanceData) {
        return mergeData(instanceData, defaultData)
      } else {
        return defaultData
      }
    }
  }
}
Copy the code

In the mergeDataOrFn method, we can see that it is separated by VM, but the idea of merging the two is the same: if parentVal and childVal are function types, call the function separately and then merge the objects they return, which is mainly the case for data merge. For provide, it does not need to be a function type, so mergeData is used to merge directly. The reason to distinguish between VMS is to deal with compatibility provide, when passed, because this property is defined at the parent level and therefore belongs to the parent rather than the current component VM.

Finally, let’s look at the mergeData method implementation code:

function mergeData (to: Object.from:?Object) :Object {
  if (!from) return to
  let key, toVal, fromVal

  const keys = hasSymbol
    ? Reflect.ownKeys(from)
    : Object.keys(from)

  for (let i = 0; i < keys.length; i++) {
    key = keys[i]
    // in case the object is already observed...
    if (key === '__ob__') continue
    toVal = to[key]
    fromVal = from[key]
    if(! hasOwn(to, key)) { set(to, key, fromVal) }else if( toVal ! == fromVal && isPlainObject(toVal) && isPlainObject(fromVal) ) { mergeData(toVal, fromVal) } }return to
}
Copy the code

MergeData does almost the same thing as the extend method mentioned earlier, except that because of all the properties in data (including those of nested objects), we need to use set to handle them responsively. Set method is Vue. Set or enclosing $set method of ontology, it defined in SRC/core/observer/index. The js file, before we mentioned in the responsive chapter.

Components, directives and filters are combined

The combination of components, directives, and filters is the same mergeAssets method, and the strats policy object defines these properties as follows:

const ASSET_TYPES = [
  'component'.'directive'.'filter'
]
ASSET_TYPES.forEach(function (type) {
  strats[type + 's'] = mergeAssets
})
Copy the code

Next, let’s look at the specific definition of mergeAssets:

function mergeAssets (
  parentVal: ?Object.1
  childVal: ?Object, vm? : Component, key: string) :Object {
  const res = Object.create(parentVal || null)
  if(childVal) { process.env.NODE_ENV ! = ='production' && assertObjectType(key, childVal, vm)
    return extend(res, childVal)
  } else {
    return res
  }
}
Copy the code

The mergeAssets method is not very much code and the logic is very clear. First, create a res prototype with parentVal. If childVal does not have one, return the RES prototype directly. If so, extend all properties on childVal to the RES stereotype using extend. It is important to note that extend is not vue. extend or this.$extend as we mentioned earlier. It is a method defined in SRC /shared/utils.js, and its code is as follows:

export function extend (to: Object, _from: ?Object) :Object {
  for (const key in _from) {
    to[key] = _from[key]
  }
  return to
}
Copy the code

Let’s write a simple example to illustrate the extend method:

const obj1 = {
  name: 'AAA'.age: 23
}
const obj2 = {
  sex: 'male'.address: 'guangzhou'
}
const extendObj = extend(obj1, obj2)
console.log(extendObj) / / {name: "AAA", the age: 23, sex: 'male', address: 'guangzhou'}
Copy the code

After introducing the Extend method, we return to the mergeAssets method, which we also illustrate:

// main.js
import Vue from 'vue'
import HelloWorld from '@/components/HelloWorld.vue'
Vue.component('HelloWorld', HelloWorld)

// App.vue
import Test from '@/components/test.vue'
export default {
  name: 'App'.components: {
    Test
  }
}
Copy the code

In the main.js entry, we define a global HelloWorld component, and in app. vue we define a local Test component. When the code is run into mergeAssets, the parameters are as follows:

const parentVal = {
  HelloWorld: function VueComponent () {... },KeepAlive: {... },Transition: {... },TransitionGroup: {...}
}
const childVal = {
  Test: function VueComponent () {...}
}
Copy the code

Since both parentVal and childVal have values, the extend method is called with the following res before and after the call:

/ / before invoking it
const res = {
  __proto__: {
    HelloWorld: function VueComponent () {... },KeepAlive: {... },Transition: {... },TransitionGroup: {... }}}// extend is called
const res = {
  Test: function VueComponent () {... },__proto__: {
    HelloWorld: function VueComponent () {... },KeepAlive: {... },Transition: {... },TransitionGroup: {... }}}Copy the code

Suppose we use both components in the app.vue component as follows:

<template>
  <div>
    <test />
    <hello-world />
  </div>
</template>
Copy the code

During app.vue component rendering, when compiled to
, it looks for the component in its components option and immediately finds test.vue in its properties. Then compiled into “hello world” / >, this property is not to be found in the object itself, according to the rules of the prototype chain will go upstairs for prototype, and then found the HelloWorld in __proto__. Vue component, two components smoothly is parsed and rendering.

For the other two options directives and filters, they have the same processing logic as components.

Watch merger

For the Watch option, the merge method used is defined separately, and its properties on the Strats policy object are defined as follows:

strats.watch = function (
  parentVal: ?Object,
  childVal: ?Object, vm? : Component, key: string): ?Object {
  // work around Firefox's Object.prototype.watch...
  if (parentVal === nativeWatch) parentVal = undefined
  if (childVal === nativeWatch) childVal = undefined
  /* istanbul ignore if */
  if(! childVal)return Object.create(parentVal || null)
  if(process.env.NODE_ENV ! = ='production') {
    assertObjectType(key, childVal, vm)
  }
  if(! parentVal)return childVal
  const ret = {}
  extend(ret, parentVal)
  for (const key in childVal) {
    let parent = ret[key]
    const child = childVal[key]
    if (parent && !Array.isArray(parent)) {
      parent = [parent]
    }
    ret[key] = parent
      ? parent.concat(child)
      : Array.isArray(child) ? child : [child]
  }
  return ret
}
Copy the code

If parentVal does not exist, return the prototype created by parentVal. If parentVal does not exist, return childVal. Note that since this is its own configuration, there is no need to create and create a prototype like parentVal. When parentVal and childVal are present, all properties on parentVal are first extended to the RET object, and the property keys of childVal are then traversed. If the parent value is not an array during the traversal, it is manually processed as an array and the child is appended to the end of the array using the array concat method. The above code analysis can be illustrated by using the following example:

/ / a
const parentVal = {
  msg: function () {
    console.log('parent watch msg')}}const childVal = undefined
const ret = {
  __proto__: {
    msg: function () {
      console.log('parent watch msg')}}}/ / 2
const parentVal = undefined
const childVal = {
  msg: function () {
    console.log('child watch msg')}}const ret = {
  msg: function () {
    console.log('child watch msg')}}/ / is three
const parentVal = {
  msg: function () {
    console.log('parent watch msg')}}const childVal = {
  msg: function () {
    console.log('child watch msg')}}const ret = {
  msg: [
    function () {
      console.log('parent watch msg')},function () {
      console.log('child watch msg')}}]Copy the code

As with hooks, if a watch is provided in a mixins that is the same as its own component, the watch in the mixins is executed first, then the watch in its own component is executed.

Merge props, Methods, Inject, and computed

Props, Methods, Inject, and computed are a little different from the previous configurations, which have one thing in common: they don’t allow the same attributes, such as the attributes we provide on methods, no matter where they come from; we just merge all the attributes together.

Let’s look at the definition of these attributes on strats policy objects:

strats.props =
strats.methods =
strats.inject =
strats.computed = function (
  parentVal: ?Object,
  childVal: ?Object, vm? : Component, key: string): ?Object {
  if(childVal && process.env.NODE_ENV ! = ='production') {
    assertObjectType(key, childVal, vm)
  }
  if(! parentVal)return childVal
  const ret = Object.create(null)
  extend(ret, parentVal)
  if (childVal) extend(ret, childVal)
  return ret
}
Copy the code

As you can see, the code isn’t too complicated in its implementation, just using the extend method to merge object properties. When parentVal is not present, return childVal directly. There is no need to create and return a prototype, for the reasons mentioned above. If parentVal has one, create a prototype and extend all properties on parentVal to the RET object using extend. ChildVal is evaluated, and if there is one, extend to RET. If not, return. The above code analysis, we illustrate:

const parentVal = {
  age: 23.name: 'AAA'
}
const parentVal = {
  address: 'guangzhou'
}
const ret = {
  age: 23.name: 'AAA'.address: 'guangzhou'
}
Copy the code

Vue2.0 source code analysis: responsive principle (below) next: Vue2.0 source code analysis: componentization (below)

Due to the word limit of digging gold article, we had to split the top and the next two articles.