vue source code interpretation-component mechanism

Keywords: Javascript Vue Attribute JQuery angular

As the original jquery, angular user. Reaction and Vue are touched at the back. Gradually fell in love with vue. Keep an attitude of learning. Look at the source code. Sure enough, vegetables have to pay a price. Step by step debugging. My head aches. See where and remember where. A little bit of experience. Please point out the mistakes for me. Thank you. The last thing I saw was the vue component section.

Let's start with the simplest code to analyze the component mechanism:
<div id="app">
  <div>{{a}}</div>
  <input v-model="heihei">
  <button v-on:click="click1">
  click1
  </button>
  <my-component>
  <div slot='dudu'>111</div>
  <Child>{{a}}</Child>
  </my-component>
  <button @click.stop="click2">
  click2
  </button>
</div>
</body>
<script src="vue.js"></script>
<script type="text/javascript">
var Child = {
  template: '<div>A custom component!</div>'
} 
Vue.component('my-component', {
  name: 'my-component',
  template: '<div>A custom component!<Child></Child><slot></slot></div>',
  components: {
    Child:Child
  },
  created(){
    console.log(this);
  },
  mounted(){
    console.log(this);
  }
})
    new Vue({
  el: '#app',
  data: function () {
    return {
      heihei:{name:3333},
      a:1
    }
  },
  components: {
    Child:Child
  },
  created(){
  
  },
  methods: {
    click1: function(){
      console.log(this);
    },
    click2: function(){
      alert('click2')
    }
  }
})
</script>

We follow the browser's thinking line by line. When the script is executed. First executed

Vue.component('my-component', {
  name: 'my-component',
  template: '<div>A custom component!<Child></Child><slot></slot></div>',
  components: {
    Child:Child
  },
  created(){
    console.log(this);
  },
  mounted(){
    console.log(this);
  }
})

Let's see what this function has gone through:
When vue.js is initialized. InititGlobal API (vue) is called to hook the tool function Vue for vue.
After initAsset Registers (Vue) in initGlobal API (vue). Turn into

vue.component = function (
      id,
      definition
    ) {
      if (!definition) {
        return this.options[type + 's'][id]
      } else {
        /* istanbul ignore if */
        {
          if (type === 'component' && config.isReservedTag(id)) {
            warn(
              'Do not use built-in or reserved HTML elements as component ' +
              'id: ' + id
            );
          }
        }
        if (type === 'component' && isPlainObject(definition)) {
          definition.name = definition.name || id;
          definition = this.options._base.extend(definition);
        }
        if (type === 'directive' && typeof definition === 'function') {
          definition = { bind: definition, update: definition };
        }
        this.options[type + 's'][id] = definition;//Global components or instructions and filters. Uniform hanging vue.options Up. wait for init Use strategy to merge intrusion instances. For example use
        return definition
      }
    };

this.options._base is Vue.options._base = Vue in initGlobal API (vue);
so vue.component calls vue.extend. The source has been found. Let's take a good look at this vue.extend function. The code is as follows:

Vue.extend = function (extendOptions) {
    extendOptions = extendOptions || {};
    var Super = this;
    var SuperId = Super.cid;
    var cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {});
    if (cachedCtors[SuperId]) {
      return cachedCtors[SuperId]//If the component has been cached in extendOptions The top is taken out directly.
    }

    var name = extendOptions.name || Super.options.name;
    {
      if (!/^[a-zA-Z][\w-]*$/.test(name)) {
        warn(
          'Invalid component name: "' + name + '". Component names ' +
          'can only contain alphanumeric characters and the hyphen, ' +
          'and must start with a letter.'//Check component name
        );
      }
    }

    var Sub = function VueComponent (options) {
      this._init(options);
    };
    Sub.prototype = Object.create(Super.prototype);
    Sub.prototype.constructor = Sub;//take vue The method of hanging on the prototype Sub.prototype Medium, Sub Examples are also inherited. vue.prototype All attributes and methods above.
    Sub.cid = cid++;
    Sub.options = mergeOptions(
      Super.options,
      extendOptions//adopt vue Merge policy merges add-ons to new constructors
    );
    Sub['super'] = Super;Cache parent constructor

    // 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$1(Sub);
    }
    if (Sub.options.computed) {   //Handle props and computed Relevant Responsive Configuration Items
      initComputed$1(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. // Tool method for hanging vue on a new constructor
    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;//Cache Component Constructor on Extended Options
    return Sub
  };
}

function initProps$1 (Comp) {
  var props = Comp.options.props;
  for (var key in props) {
    proxy(Comp.prototype, "_props", key);
  }
}

function initComputed$1 (Comp) {
  var computed = Comp.options.computed;
  for (var key in computed) {
    defineComputed(Comp.prototype, key, computed[key]);
  }
}

In general, vue.extend is a constructor that returns a new Vue with an additional configuration phase. In the function, the constructor is called Sub, which is initialized while waiting for render.
It is called by vue.component. Vue adds a global component my-component; at this point, vue.options.component is as follows:

The first three are built-in components of vue that are initialized at initgloabalapi.
At this point, the global component creation is complete. Global components are placed at the bottom. In future policy merges, it will be in the _proto_ of the component item in the subcomponent.

Look at the overall lifecycle of vue through recursive component creation rendering (understand how vue intelligently builds applications)

Above:

The official life cycle diagram of Vue is actually the life cycle of the components of vue. Along the new Vue() let's take a look at some of the stages of these life cycles

Vue.prototype._init = function (options) {
    var vm = this;
    // a uid
    vm._uid = uid$1++;

    var startTag, endTag;
    /* istanbul ignore if */
    if ("development" !== 'production' && config.performance && mark) {
      startTag = "vue-perf-init:" + (vm._uid);
      endTag = "vue-perf-end:" + (vm._uid);
      mark(startTag);
    }

    // a flag to avoid this being observed
    vm._isVue = true;
    // merge options
    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);//Internal component calls this shortcut
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),//Policy merge, each attribute has a corresponding merge rule
        options || {},
        vm
      );
    }
    /* istanbul ignore else */
    {
      initProxy(vm);//Attribute proxy, that is vm.xx = vm.data.xx
    }
    // expose real self
    vm._self = vm;
    initLifecycle(vm);//Initialize the life cycle state variables and establish the initial values of the child-parent relationship, such as $children, $parent.
    initEvents(vm);//Initialization events
    initRender(vm);//Initialize render core functions $createElement and $slots $scopedSlots, etc.
    callHook(vm, 'beforeCreate');
    initInjections(vm); // resolve injections before data/props
    initState(vm);//Using data hijacking as a response
    initProvide(vm); //resolve provide after data/props
    callHook(vm, 'created');

    /* istanbul ignore if */
    if ("development" !== 'production' && config.performance && mark) {
      vm._name = formatComponentName(vm, false);
      mark(endTag);
      measure(((vm._name) + " init"), startTag, endTag);
    }

    if (vm.$options.el) {
      vm.$mount(vm.$options.el);//Actively mount if there is el configuration phase. Compoile. render after triggering
    }
  };

Introducing the approximate _init function, let's move on to see how the program works. After completing vue.component(). Start executing new vue(), and create an instance.
Contrast the _init function. We know that it merges the incoming parameters separately. Initialize instance parameters. Create a response form for the response. Finally mount: vm.$mount(vm.$options.el);
To put it simply, mount. OK Let's go to the method and see what happened when we mounted it.

// public mount method
Vue$3.prototype.$mount = function (
  el,
  hydrating
) {
  el = el && inBrowser ? query(el) : undefined;
  return mountComponent(this, el, hydrating)
};

var mount = Vue$3.prototype.$mount//Cache mount to trigger render
Vue$3.prototype.$mount = function (//Core mount is used to construct render functions
  el,
  hydrating
) {
  el = el && query(el);

  /* istanbul ignore if */
  if (el === document.body || el === document.documentElement) {
    "development" !== 'production' && warn(
      "Do not mount Vue to <html> or <body> - mount to normal elements instead."//Detection, excluding non-mountable elements
    );
    return this
  }

  var options = this.$options;
  // resolve template/el and convert to render function
  if (!options.render) {
    var template = options.template;//If the input is template Template time.
    if (template) {
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          template = idToTemplate(template);
          /* istanbul ignore if */
          if ("development" !== 'production' && !template) {
            warn(
              ("Template element not found or is empty: " + (options.template)),
              this
            );
          }
        }
      } else if (template.nodeType) {
        template = template.innerHTML;//When the input is dom node
      } else {
        {
          warn('invalid template option:' + template, this);
        }
        return this
      }
    } else if (el) {
      template = getOuterHTML(el);//If it is one id,Such an initialization mount id=app,We'll get it. id=app Of html
    }
    if (template) {
      /* istanbul ignore if */
      if ("development" !== 'production' && config.performance && mark) {
        mark('compile');
      }

      var ref = compileToFunctions(template, {
        shouldDecodeNewlines: shouldDecodeNewlines,//core compile Function. Used to generate render Function. Let's not go into details here.
        delimiters: options.delimiters
      }, this);
      var render = ref.render;
      var staticRenderFns = ref.staticRenderFns;
      options.render = render;//mount render To example options Medium. To be called
      options.staticRenderFns = staticRenderFns;//Static elements are distinguished. Improving performance and subsequent virtualization dom When trees are compared, static nodes are not compared

      /* istanbul ignore if */
      if ("development" !== 'production' && config.performance && mark) {
        mark('compile end');
        measure(((this._name) + " compile"), 'compile', 'compile end');
      }
    }
  }
  return mount.call(this, el, hydrating)//Cached mount Call ready render
};

The core of the $mount method is actually the render function that prepares the component. One of the core methods here is:

var ref = compileToFunctions(template, {
        shouldDecodeNewlines: shouldDecodeNewlines,//core compile Function. Used to generate render Function. Let's not go into details here.
        delimiters: options.delimiters
      }, this);

Two main things are done in the compileToFunctions function:
1: Compoile the template (parse by label, generate ast (abstract grammar tree)
2: Generate render function grammar using generate(ast, options)

Let's look at the render function generated by the last instance:

No mistake, that's how it feels. The render function generated is stored in options, waiting for invocation

OK Start calling.

mount.call(this, el, hydrating)=>mountComponent(this, el, hydrating)=>updateComponent = function () {
      vm._update(vm._render(), hydrating);
    };=>vm._watcher = new Watcher(vm, updateComponent, noop);

new Watcher actively calls updateComponent to touch dependencies (for variables in data referenced in pages if listening) and formally calls render functions. Now that it's all said. Let's look at the render function:

Vue.prototype._render = function () {
    var vm = this;
    var ref = vm.$options;
    var render = ref.render;
    var staticRenderFns = ref.staticRenderFns;
    var _parentVnode = ref._parentVnode;

    if (vm._isMounted) {
      // clone slot nodes on re-renders
      for (var key in vm.$slots) {
        vm.$slots[key] = cloneVNodes(vm.$slots[key]);
      }
    }

    vm.$scopedSlots = (_parentVnode && _parentVnode.data.scopedSlots) || emptyObject;

    if (staticRenderFns && !vm._staticTrees) {
      vm._staticTrees = [];
    }
    // set parent vnode. this allows render functions to have access
    // to the data on the placeholder node.
    vm.$vnode = _parentVnode;
    // render self
    var vnode;
    try {
      vnode = render.call(vm._renderProxy, vm.$createElement);//Core function, call render
    } catch (e) {
      handleError(e, vm, "render function");
      // return error render result,
      // or previous vnode to prevent render error causing blank component
      /* istanbul ignore else */
      {
        vnode = vm.$options.renderError
          ? vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)
          : vm._vnode;
      }
    }
    // return empty vnode in case the render function errored out
    if (!(vnode instanceof VNode)) {
      if ("development" !== 'production' && Array.isArray(vnode)) {
        warn(
          'Multiple root nodes returned from render function. Render function ' +
          'should return a single root node.',
          vm
        );
      }
      vnode = createEmptyVNode();
    }
    // set parent
    vnode.parent = _parentVnode;
    return vnode
  };

_ render() indirectly calls vnode = render.call(vm._renderProxy, vm.$createElement);

Then it combines the render function. Look what happened. vm.$createElement is the core function for creating virtual dom.

Continue to look at the core build virtual dom functions:

function createElement (
  context,
  tag,
  data,
  children,//children is all the child elements under this element
  normalizationType,
  alwaysNormalize
) {
  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)
}

function _createElement (
  context,
  tag,
  data,
  children,
  normalizationType
) {
  if (isDef(data) && isDef((data).__ob__)) {
    "development" !== 'production' && warn(
      "Avoid using observed data object as vnode data: " + (JSON.stringify(data)) + "\n" +
      'Always create fresh vnode data objects in each render!',
      context
    );
    return createEmptyVNode()
  }
  if (!tag) {
    // in case of component :is set to falsy value
    return createEmptyVNode()
  }
  // 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);
  }
  var vnode, ns;
  if (typeof tag === 'string') {
    var Ctor;
    ns = config.getTagNamespace(tag);
    if (config.isReservedTag(tag)) {
      // platform built-in elements
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      );
    } else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {If it's a component, take the component's construction parameters from the context
      // 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);
  }
  if (vnode !== undefined) {
    if (ns) { applyNS(vnode, ns); }
    return vnode
  } else {
    return createEmptyVNode()
  }
}

It's not hard to see how vue constructs virtual dom. Recursively call createElement to generate virtual DOM tree. When a child is a component or a common element. Do different things. Here we are concerned about. When a child element is a component. This is called here.
vnode = createComponent(tag, data, context, children);
Careful people can see what this function does. Simply put, this function takes out the component's construction parameters and places them on the component Options of the element. For subsequent creation of real dom. Mark the element as a component. Recursive initialization.
Skip these heavy ones. Let's look directly at what the final virtual dom generated by our html looks like. As follows:

Let's see what our my-component looks like:

The parameters needed to initialize the component are stored on componentOptios.

After the virtual dom is built. vue enters the update stage:
At this stage, the vue determines whether the element was previously present or not. Is it rendered for the first time? If it's the first time. Then create it directly. If there is no previous ovnode, the difference is significant. Minimize updates. Look at the specific functions:

nction lifecycleMixin (Vue) {
  Vue.prototype._update = function (vnode, hydrating) {
    var vm = this;
    if (vm._isMounted) {
      callHook(vm, 'beforeUpdate');
    }
    var prevEl = vm.$el;
    var prevVnode = vm._vnode;//Fetch cached virtual dom
    var prevActiveInstance = activeInstance;
    activeInstance = vm;
    vm._vnode = vnode;//Cache the current vnode for the next update
    // Vue.prototype.__patch__ is injected in entry points
    // based on the rendering backend used.
    if (!prevVnode) {//If it's rendered for the first time. Direct creation
      // initial render
      vm.$el = vm.__patch__(
        vm.$el, vnode, hydrating, false /* removeOnly */,
        vm.$options._parentElm,
        vm.$options._refElm
      );
    } else {
      // updates
      vm.$el = vm.__patch__(prevVnode, vnode);//Update. There will be differences.
    }
    activeInstance = prevActiveInstance;
    // update __vue__ reference
    if (prevEl) {
      prevEl.__vue__ = null;
    }
    if (vm.$el) {
      vm.$el.__vue__ = vm;
    }
    // if parent is an HOC, update its $el as well
    if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
      vm.$parent.$el = vm.$el;
    }
    // updated hook is called by the scheduler to ensure that children are
    // updated in a parent's updated hook.
  };

_ We won't look at the patch_ function. Let's take a look at it.


  return function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
    if (isUndef(vnode)) {
      if (isDef(oldVnode)) { invokeDestroyHook(oldVnode); }
      return
    }

    var isInitialPatch = false;
    var insertedVnodeQueue = [];

    if (isUndef(oldVnode)) {
      // empty mount (likely as component), create new root element
      isInitialPatch = true;//If it's rendered for the first time. Direct creation
      createElm(vnode, insertedVnodeQueue, parentElm, refElm);
    } else {
      var isRealElement = isDef(oldVnode.nodeType);
      if (!isRealElement && sameVnode(oldVnode, vnode)) {//If updated and virtualized dom Similarly, there is a similar algorithm. such as tag,key Must be consistent. Only then will I go. diff compare
        // patch existing root node
        patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly);
      } else {
        if (isRealElement) {
          // mounting to a real element
          // check if this is server-rendered content and if we can perform
          // a successful hydration.
          if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
            oldVnode.removeAttribute(SSR_ATTR);
            hydrating = true;
          }
          if (isTrue(hydrating)) {
            if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
              invokeInsertHook(vnode, insertedVnodeQueue, true);
              return oldVnode
            } else {
              warn(
                'The client-side rendered virtual DOM tree is not matching ' +
                'server-rendered content. This is likely caused by incorrect ' +
                'HTML markup, for example nesting block-level elements inside ' +
                '<p>, or missing <tbody>. Bailing hydration and performing ' +
                'full client-side render.'
              );
            }
          }
          // either not server-rendered, or hydration failed.
          // create an empty node and replace it
          oldVnode = emptyNodeAt(oldVnode);
        }
        // replacing existing element
        var oldElm = oldVnode.elm;
        var parentElm$1 = nodeOps.parentNode(oldElm);
        createElm(
          vnode,
          insertedVnodeQueue,
          // extremely rare edge case: do not insert if old element is in a
          // leaving transition. Only happens when combining transition +
          // keep-alive + HOCs. (#4590)
          oldElm._leaveCb ? null : parentElm$1,
          nodeOps.nextSibling(oldElm)
        );

        if (isDef(vnode.parent)) {
          // component root element replaced.
          // update parent placeholder node element, recursively
          var ancestor = vnode.parent;
          while (ancestor) {
            ancestor.elm = vnode.elm;
            ancestor = ancestor.parent;
          }
          if (isPatchable(vnode)) {
            for (var i = 0; i < cbs.create.length; ++i) {
              cbs.create[i](emptyNode, vnode.parent);
            }
          }
        }

        if (isDef(parentElm$1)) {
          removeVnodes(parentElm$1, [oldVnode], 0, 0);
        } else if (isDef(oldVnode.tag)) {
          invokeDestroyHook(oldVnode);
        }
      }
    }

    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
    return vnode.elm
  }

The core of the patch method is createElm: it's important to understand this function as follows

function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested) {
    vnode.isRootInsert = !nested; // for transition enter check
    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {//Identify whether it is a component based on previously saved component options. If so. Let's go into this logic.
      return
    }

    var data = vnode.data;
    var children = vnode.children;
    var tag = vnode.tag;
    if (isDef(tag)) {
      {
        if (data && data.pre) {
          inPre++;
        }
        if (
          !inPre &&
          !vnode.ns &&
          !(config.ignoredElements.length && config.ignoredElements.indexOf(tag) > -1) &&
          config.isUnknownElement(tag)
        ) {
          warn(
            'Unknown custom element: <' + tag + '> - did you ' +
            'register the component correctly? For recursive components, ' +
            'make sure to provide the "name" option.',
            vnode.context
          );
        }
      }
      vnode.elm = vnode.ns
        ? nodeOps.createElementNS(vnode.ns, tag)
        : nodeOps.createElement(tag, vnode);
      setScope(vnode);

      /* istanbul ignore if */
      {
        createChildren(vnode, children, insertedVnodeQueue);
        if (isDef(data)) {
          invokeCreateHooks(vnode, insertedVnodeQueue);
        }
        insert(parentElm, vnode.elm, refElm);
      }

      if ("development" !== 'production' && data && data.pre) {
        inPre--;
      }
    } else if (isTrue(vnode.isComment)) {
      vnode.elm = nodeOps.createComment(vnode.text);
      insert(parentElm, vnode.elm, refElm);
    } else {
      vnode.elm = nodeOps.createTextNode(vnode.text);
      insert(parentElm, vnode.elm, refElm);
    }
  }

Let's focus on our component parts first. When children is a component element, it is obvious that createComponent (vnode, inserted VnodeQueue, parentElm, refElm) is called.

var componentVNodeHooks = {
  init: function init (
    vnode,
    hydrating,
    parentElm,
    refElm
  ) {
    if (!vnode.componentInstance || vnode.componentInstance._isDestroyed) {
      var child = vnode.componentInstance = createComponentInstanceForVnode(
        vnode,
        activeInstance,
        parentElm,//The _init method within the component is invoked to create the subcomponent recursively. Formally enter the life cycle of subcomponents
        refElm
      );
      child.$mount(hydrating ? vnode.elm : undefined, hydrating);Triggers the mounting of subcomponents. Start the compilation and assembly of subcomponents render. Again/Until the subcomponents are fully rendered. Start again creelem Next child
    } else if (vnode.data.keepAlive) {
      // kept-alive components, treat as a patch
      var mountedNode = vnode; // work around flow
      componentVNodeHooks.prepatch(mountedNode, mountedNode);
    }
  },

This is the core part of recursively creating subcomponents.

Summary: Write this vue for the first time. Failed. Cutting module is not detailed enough. Component mechanisms feel like they use a lot of things. This side is too big. I don't know if I should go into details or...

Overall: vue creates a virtual DOM when comp, if the element is a component. Prepare the component's construction parameters. Including template and data, etc. Elements in a component such as slot and child are placed under the child of the component element. Elements in components for subsequent content distribution are also compiled within the scope of parent components. Look at the render () function. Then when vue needs to turn virtual DOM into real dom. When encountering component elements. Start recursive initialization. Until the component compile,render is built. Start building the next element. Finally, add it to the real id=app. And delete the old ones. Ha-ha. Write freely

Posted by snorky on Sun, 23 Jun 2019 17:22:25 -0700