preface

This is the fourth chapter of VUe2.0 analysis. The difficulties of this article are all in patch, and there is no difficulty in component registration.

The article links

Vue parse: data

Vue parse: computed

Vue d. watch

Components are the heart of VUE, and there are two ways to register components: global and local, according to the official documentation. So let’s look at global registration first, so let’s do an example

<div id="app">
  <div>This is a div</div>
  <comp-a></comp-a>
</div>
<script>
  Vue.component('comp-a', {
    template: '
       
this is a child component
'
}) const vm = new Vue({ el: '#app',})
</script> Copy the code

For global registration, we first need to understand how methods are mounted during vUE initialization. In the core/instance/index.js file, vue. prototype has a large number of methods mounted by combining methods that can be guessed by method names.

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
Copy the code

InitGlobalAPI (Vue) is then called in core/index.js, which is defined in core/global-api/index.js, and it mounts a number of methods, namely static methods, on Vue. So what did it do? Let’s look at the core implementation of this example

export function initGlobalAPI (Vue: GlobalAPI) {

  Vue.options = Object.create(null)
  ASSET_TYPES.forEach(type= > {
    Vue.options[type + 's'] = Object.create(null)})// this is used to identify the "base" constructor to extend all plain-object
  // components with in Weex's multi-instance scenarios.
  Vue.options._base = Vue

  extend(Vue.options.components, builtInComponents)

  initExtend(Vue)
  initAssetRegisters(Vue)
}
Copy the code

The middle forEach loop looks directly at the result, and we give __proto__ of each existing object a null pointer. And extend to blend keep-alive into components. It is then registered through initAssetRegisters. The components in this method is

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

So in Vue it can be summed up as

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

Initialization of global registration

Let’s start with an example. Before new Vue, we used Vue.com Ponent to define global components, so Vue calls the static method Vue.component to initialize. This.options._base. Extend (definition). This.options._base is defined in initGlobalAPI as Vue. So here we call vue. extend to initialize the component. Ok, let’s go into vue. extend

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

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

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

  // cache constructor
  cachedCtors[SuperId] = Sub
  return Sub
}
Copy the code

This method is just as important as the init method, which is the creation of an instance of a component, but for this example we’ll just look at its core. And then summarize what it did.

  1. willVueAssigned toSuper
  2. A cache is made, for example if we use the same component twice in a parent component, then the return is that the cache does not need to create the instance method again
  3. To create theSubInstance method, the method’s__protoPoints to theVueTo point the constructor of a prototype at itself, standard prototype inheritance
  4. Merge option to the parent componentcomponentsAnd the incoming option of the child component, assigning the child component option;Pay attention to: of the parent componentassetsIs placed on the prototype of the child component, seemergeAssetsmethods

Also use Object.create 5. initialize props and computed 6. Initialize some other methods 7. Return the constructor

Vue.com Ponent completes execution and starts the new Vue process

As before, the new Vue initialization creates the render Watcher, which is the second argument passed in when the get function in the Watcher is called. The updateComponent method executes vm._update(vm._render(), hydrating). The method is divided into two steps. The first step is to execute the render function to generate a VNode. The second step is to render the vNode to the page via vm.update.

Vnode = render. Call (vm._renderProxy, vm.$createElement)

; (function anonymous () {
  with (this) {
    return _c(
      'div',
      { attrs: { id: 'app' } },
      [_c('div', [_v('This is a div.')]), _v(' '), _c('comp-a')].1)}})Copy the code

And then we do _c(comp-a). We talked about _c earlier, remember? In initRender, there is a declaration vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false). Obviously we’re calling createElement.

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

In this method, data is judged, and we only pass in the first two parameters, the first is VM, and the second is comp-a. Then the intermediate assignment step is not performed and the last value is false. When it is true. This function is triggered when we use the render function instead of the template, just so we know. Let’s look at the _createElement method. This method is in core/vnode/create-element.js

export function _createElement (context: Component, tag? : string | Class<Component> |Function | Object, data? : VNodeData, children? : any, normalizationType? : number) :VNode | Array<VNode> {
  if (typeof tag === 'string') {
    var Ctor;
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag);
    // If the label is reserved
    if (config.isReservedTag(tag)) {
      / / create a vnode
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined.undefined, context
      );
      // If data does not exist and components does exist, it is a component
    } else if((! data || ! data.pre) && isDef(Ctor = resolveAsset(context.$options,'components', tag))) {
      // component
      // Create a child component
      vnode = createComponent(Ctor, data, context, children, tag);
    } else {
      vnode = new VNode(
        tag, data, children,
        undefined.undefined, context ); }}else{ vnode = createComponent(tag, data, context, children); }}Copy the code

This method is very important to create the core method of Vnode, remove the judgment code, the core is the above logic

  1. tagThere are three cases of strings
    1. Reserve label creation directlyVnode
    2. Exists on instancecomponentsAnd,tagCan be incomponentsIs found in the component
    3. Unknown label CreationVnode
  2. tagInstead of a string, create a component

The resolveAsset(context.$options, ‘components’, tag) method checks if $options.components contains a tag and returns it. Obviously we can find this tag in components because we declared it via Vue. Then we go to the createComponent(Ctor, data, Context, Children, Tag) method. Let’s look at the arguments first. Ctor is the constructor. Data is undefined, Content is the current instance, children is undefined, and Tag is comp-a.

createComponent

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

  data = data || {}

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

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

  return vnode
}
Copy the code

In this method, we do some initialization of the options, mainly in the installComponentHooks method. Let’s go into this method.

const componentVNodeHooks = {
  init() {},
  prepatch() {},
  insert() {},
  destory(){}}const hooksToMerge = Object.keys(componentVNodeHooks)

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

Its main job is to put the methods in componentVNodeHooks into the hooks of Data, and if there are methods with the same name, merge them using the mergeHook strategy. The merge strategy is

(init1, init2) => {
 init1(),
 init2()
}
Copy the code

After all the work is done, we will start new Vnode. Instantiating a Vnode is very simple, just creating a lot of attributes. The key thing to remember is that the tag of Vnode is vue-component-1-comp-a.

vm._update

Create the parent component Vnode, you can try to debug it yourself, the process is similar. Here we’ll look directly at the update process and take a look at the formation of the incoming Vnode

vnode: {
  tag: 'div'.data: {
    attrs: {id: 'app'}},children: [
    Vnode // This is a div node
    Vnode // Empty text node
    Vnode // vue-component-1-comp-a]}Copy the code

Then we look at the _update method, which is in instance/ lifrCycle.js

Vue.prototype._update = function (vnode: VNode, hydrating? : boolean) {
 const vm: Component = this
 const prevEl = vm.$el
 const prevVnode = vm._vnode
 // Store the current VM instance
 const restoreActiveInstance = setActiveInstance(vm)
 vm._vnode = vnode
 if(! prevVnode) {// initial render
   vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)}else {
   // updates
   vm.$el = vm.__patch__(prevVnode, vnode)
 }
 // activeIntance becomes the previous instance
 restoreActiveInstance()
}
Copy the code

PrevVnode does not exist so call vm. __Patch__ (vm.$el, vnode, hydrating, false /* removeOnly */). Remember that the parameter vm.$el passed in is our

. Vnode is its virtual node.

Patch method

__patch__ is what, let’s come to the platform/runtime/index, js, with such a definition Vue. Prototype. __patch__ = inBrowser? Patch: noop, so on the browser side it is the patch method. Continue looking for the patch method, which is defined in patch.js in the same directory. It is the return value of a method, createPatchFunction, that passes in an object defined in core/ vDOM /patch, So the return value method of the createPatchFunction method is the method we will execute.

vueWhy would you do that?

Let’s look at the incoming object of the createPatchFunction method

  1. NodeOps defines a lot of primitivitynodeOperation method
  2. Modules includevueCustom properties and browser properties

In other words, Vue uses higher-order functions to separate the code, which is a good way to program. We should learn

Ok, patch, which I’m not going to post code on, is very long. We’re going to do it in a literal, pseudo-code kind of way

  • ifvnodeundefined
    • oldvnodeDefined, executeinvokeDestroyHook(oldVnode)
    • return
  • ifoldVnodeThere is no
    • performcreateElm(vnode, insertedVnodeQueue)
  • oldVnodeThere are
    • In the first place to judgeoldVnodeIs it a real node
    • oldvnodeNot really node, andsameVnode(oldVnode, vnode)
      • performpatchVnode
    • oldVnodeIt’s a real node that does a bunch of things

Roughly this judgment is enough, execute to the next level, the previous level will not be executed, look at the source code. So in our case, it jumps to oldvNode as the real node. Moving on, vue first creates an empty node on the real oldVnode as a virtual node, and then calls createElm, which is passed in as a VNode.

CreateElm Creates a node

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

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

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

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

  // Create child nodes recursively
  createChildren(vnode, children, insertedVnodeQueue)
  if (isDef(data)) {
    / / createhook execution
    invokeCreateHooks(vnode, insertedVnodeQueue)
  }
  // Insert the real node
  insert(parentElm, vnode.elm, refElm)
 
}

function createChildren (vnode, children, insertedVnodeQueue) {
  if (Array.isArray(children)) {
    for (let i = 0; i < children.length; ++i) {
      createElm(children[i], insertedVnodeQueue, vnode.elm, null.true, children, i)
    }
  } else if (isPrimitive(vnode.text)) {
    nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))
  }
}
Copy the code

In createElm, first vue creates the element node of vNode, literally. CreateChildren is then called, passing in vNode and children, and createElm is called again, which becomes children[I]. Then it is obvious that when vNode is COMP-A, we will perform the actual component creation.

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
  let i = vnode.data
  if (isDef(i)) {
    const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
    if (isDef(i = i.hook) && isDef(i = i.init)) {
      i(vnode, false /* hydrating */)}if (isDef(vnode.componentInstance)) {
      initComponent(vnode, insertedVnodeQueue)
      insert(parentElm, vnode.elm, refElm)
      if (isTrue(isReactivated)) {
        reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
      }
      return true}}}Copy the code

We already know that vnode.data contains the initialization function we need, so we will execute it here. When we reach I (vnode, false /* hydrating */), we will execute vnode.init. There are three main steps in the init method

  1. performprepatchthishook
  2. Initialize the child component instance
  3. $mountMount child components

Once executed, we have vnode.componentInstance. And then there’s the initComponent and in this method it does the invokeCreateHooks which are the create methods in modules that are dom dependent. AppendChild (elm) nodeops.appendChild (parent, elm)

What is rendering at this point?

Take a look at the element. Yes, the station node comp-a. So when do you render the real nodes?

<div id="app">
 <div>This is a div</div>
 <comp-a></comp-a>
</div>
Copy the code

Back to createElm, createChildren is finished. So we’re done recursively creating children. Look at the code

// Create child nodes recursively
createChildren(vnode, children, insertedVnodeQueue)
if (isDef(data)) {
 / / createhook execution
 invokeCreateHooks(vnode, insertedVnodeQueue)
}
// Insert the real node
insert(parentElm, vnode.elm, refElm)
Copy the code

The parent component’s invokeCreateHooks method, which is also a set of hook functions, are then executed for the actual insert. At this point we have vnode.elm, which contains the children node. Look at the element after execution

<body>
  <div id="app">
    <div>This is a div</div>
    <comp-a></comp-a>
  </div>
  <div id="app">
    <div>This is a div</div>
    <div>This is a child component</div>
  </div>
</body>
Copy the code

Yes, it’s rendered, but there are two, it’s easy, just delete the last one. Obviously after createElm is over, we’re going back to Patch

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

To delete the node, perform insert. This insert is a component of vNode. data, where we will execute the component mounted hook and set instance Mounted to end. The mounted hook function of the vue instance is executed.

Local registration

Let’s look at what’s different about local registration.

<div id="app">
  <div>This is a div</div>
  <comp-a></comp-a>
</div>
<script>
  const componentA = {
    template: '
       
this is a child component
'
} const vm = new Vue({ el: '#app'.components: { 'comp-a': componentA } })
</script> Copy the code

ResolveAsset (context.$options, ‘components’, tag); vm.$options (context.$options, ‘components’, tag); So when we run the createComponent method, we’ll run this code

if (isObject(Ctor)) {
 Ctor = baseCtor.extend(Ctor)
}
Copy the code

When we execute the extend method, we pass options with no name, only Vue. Extend. So this code doesn’t execute either

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

There is no difference later, you can go through the process of render to patch as I did above.