instructions

  • First of all, this article is to read vue.js source code carding article, the article is segmented carding, recording some of their own understanding and general process; More importantly, I want to understand how vue.js 3.0 works before it is released.

  • If you have never seen or touched the vue.js source code, we recommend you refer to the vue.js parsing articles listed below, because these articles explain the project in more detail, and this article is only a few demos of a feature point or API implementation, trying to briefly outline the process.

    • Line-by-line source code analysis – Highly recommended
    • Vue. Js source code analysis
    • Vue. Js source code analysis
  • If you have a clear understanding of the project catalog and entry, it is recommended to directly look at the code, which is more efficient (if you find it difficult to understand, come back to read others’ explanation and try to understand).

  • The code mentioned in this article is basically an abbreviated version, please refer to vue.js-2.5.17 for details.

  • Any omissions and mistakes are welcome to correct and exchange.

Vue constructor

/** * Vue constructor ** @param {*} options options */
function Vue(options) {
  if(process.env.NODE_ENV ! = ='production' && !(this instanceof Vue)) {
    warn('Vue is a constructor and should be called with the 'new' keyword ');
  }
  this._init(options);
}
Copy the code

We know that new Vue() will execute the Vue constructor, which in turn will execute _init(), so where does the _init method come from? The answer is that Vue adds this method during initialization. If you are not quite sure about initialization, you are advised to refer to the previous article on initialization: “Try reading the Vue source code” what was done before and after initialization ❓.

_init()

import config from '.. /config';
import { initProxy } from './proxy';
import { initState } from './state';
import { initRender } from './render';
import { initEvents } from './events';
import { mark, measure } from '.. /util/perf';
import { initLifecycle, callHook } from './lifecycle';
import { initProvide, initInjections } from './inject';
import { extend, mergeOptions, formatComponentName } from '.. /util/index';

let uid = 0;
export function initMixin(Vue: Class<Component>) {
  Vue.prototype._init = function(options? : Object) {
    const vm: Component = this; // The current Vue instance
    vm._uid = uid++; // Unique identifier of the current Vue instance

    / * * * * * * * * * * * * * * * * * * * * * * * * * * * * non-production environments for performance monitoring - start * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
    let startTag, endTag;
    if(process.env.NODE_ENV ! = ='production' && config.performance && mark) {
      startTag = `vue-perf-start:${vm._uid}`;
      endTag = `vue-perf-end:${vm._uid}`;
      mark(startTag);
    }

    vm._isVue = true; // a flag to prevent the object from being observed by the response system

    / * * * * * * * * * * * * * * * * * * to props, the data provided by the Vue, the methods and dealing with option to merge, * * * * * * * * * * * * * * * * * * /
    // _isComponent internal option: generated when Vue creates a component
    if (options && options._isComponent) {
      initInternalComponent(vm, options); // Optimize internal component instantiation because dynamic option merging is very slow and none of the internal component options require special handling.
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor), // parentVal
        options || {}, // childVal
        vm
      );
    }

    // Set the scope proxy for the render function. The purpose is to provide better prompts (for example, accessing non-existent attributes of the instance in the template will provide accurate error messages in non-production environments).
    if(process.env.NODE_ENV ! = ='production') {
      initProxy(vm);
    } else {
      vm._renderProxy = vm;
    }

    vm._self = vm; // Expose the real instance itself

    / * * * * * * * * * * * * * * * * * * * * * * * * * * * * to perform the initialization program and the early life cycle function called * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
    initLifecycle(vm); // Initialize the life cycle
    initEvents(vm); // Initialize the event
    initRender(vm); // Initialize render
    callHook(vm, 'beforeCreate'); // Call the lifecycle hook function -- beforeCreate
    initInjections(vm); // resolve injections before data/props
    initState(vm); Initialize initProps, initMethods, initData, initComputed, and initWatch
    initProvide(vm); // resolve provide after data/props
    callHook(vm, 'created'); // There is no mount operation at this point, so DOM is not accessible in Created, i.e. $el is not accessible

    / * * * * * * * * * * * * * * * * * * * * * * * * * * * * non-production environments for performance monitoring - end * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
    if(process.env.NODE_ENV ! = ='production' && config.performance && mark) {
      vm._name = formatComponentName(vm, false);
      mark(endTag);
      measure(`vue ${vm._name} init`, startTag, endTag);
    }

    / * * * * * * * * * * * * * * * * * * * * * * * * * * * * according to the mount point, mount function called * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
    if(vm.$options.el) { vm.$mount(vm.$options.el); }}; }Copy the code
  • According to the_initMethods can be roughly sorted out the following points:
    • ① Start the performance monitoring program (monitoring the execution time of (2), (3), and (4)) in a non-production environment.
    • (2) Combine the props, data, methods and other options provided by Vue.
    • ③ Set the scope proxy of the render function.
    • (4) Perform related initialization procedures and call initial life cycle functions.
    • ⑤ Call the mount function according to the mount point.

Note: Performance monitoring: Measure the Performance of Web pages and Web applications by allowing Web pages to access certain functions using the Web Performance API; Here is vuE-Mark, measure specific code implementation, but more details; What are the steps that are being monitored?

new Vue()

If you just look at the code, it might not be intuitive or easy to understand; Instead, use Demo to plug in breakpoints and see how each step works. That will give you a more intuitive understanding of how the code works.


      
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>vue.js DEMO</title>
    <script src=".. /.. /dist/vue.js"></script>
  </head>
  <body>
    <div id="app">
      <p>Calculated properties: {{messageTo}}</p>
      <p>Data attributes: {{message}}</p>
      <button @click="update">update</button>
      <item v-for="item in list" :msg="item" :key="item" @rm="remove(item)" />
    </div>

    <script>
      new Vue({
        el: '#app'.components: {
          item: {
            props: ['msg'].template: `
       
{{ msg }}
`
, created() { console.log('-- componentA --component lifecycle hook created-- '); }}},mixins: [ { created() { console.log('---created - mixins---'); }, methods: { remove(item) { console.log('Response Removed:', item); }}}].data: { message: 'hello vue.js'.list: ['hello,'.'the updated'.'vue.js'].obj: { a: 1.b: { c: 2.d: 3}}},computed: { messageTo() { return `The ${this.message}! ; `; }},watch: { message(val, oldVal) { console.log(val, oldVal, 'Message - Changed'); }},methods: { update() { this.message = `The ${this.list.join(' ')} ---- The ${Math.random()}`; }}});
</script> </body> </html> Copy the code

Enter the Vue constructor with options as shown in the breakpoint diagram:

Option merge processing

  • $options = mergeOptions; $options = mergeOptions; $options = mergeOptions;

  • ResolveConstructorOptions, the function is mainly judge whether there is a parent class constructor, if any parent to vm. The constructor. The options for handling return, if there is no direct return to the vm. The constructor. The options; Return vm.constructive. options directly from the Demo above.

  • Note: The initialization process performed above for vm.constructor.options results in:

    Vue.options = {
      components: {
        KeepAlive,
        Transition,
        TransitionGroup
      },
      directives: {
        model,
        show
      },
      filters: Object.create(null),
      _base: Vue
    };
    Copy the code
// _isComponent internal option: generated when Vue creates a component
if (options && options._isComponent) {
  initInternalComponent(vm, options); // Optimize internal component instantiation because dynamic option merging is very slow and none of the internal component options require special handling.
} else {
  vm.$options = mergeOptions(
    resolveConstructorOptions(vm.constructor), // parentVal
    options || {}, // childVal
    vm
  );
}
Copy the code

Based on the above analysis, the program enters the mergeOptions function. The following breakpoint diagram shows the input parameters of this function:

mergeOptions

Merge the two Option objects into a new Options for instantiation and inheritance of the core utility.

export function mergeOptions(parent: Object, child: Object, vm? : Component) :Object {
  // Check whether the component name meets the requirement:
  // The qualified component name consists of plain characters and a hyphen (-), and must begin with a letter.
  / / testing whether the built-in tags (such as: slot) | | testing whether keep tags (HTML and SVG, etc.).
  if(process.env.NODE_ENV ! = ='production') {
    checkComponents(child);
  }

  // If child is a function, override child without its static property options;
  if (typeof child === 'function') {
    child = child.options;
  }

  / * * * * * * * * * * * * * * * * * * * * * * * * standardized treatment * * * * * * * * * * * * * * * * * * * * * * * * /
  normalizeProps(child, vm);
  normalizeInject(child, vm);
  normalizeDirectives(child);

  / * * * * * * * * * * * * * * * * * * * * * * * * extends/mixins recursive processing combination of * * * * * * * * * * * * * * * * * * * * * * * * /
  const extendsFrom = child.extends;
  if (extendsFrom) {
    parent = mergeOptions(parent, extendsFrom, vm);
  }
  if (child.mixins) {
    for (let i = 0, l = child.mixins.length; i < l; i++) { parent = mergeOptions(parent, child.mixins[i], vm); }}/ * * * * * * * * * * * * * * * * * * * * * * * * merge phase * * * * * * * * * * * * * * * * * * * * * * * * /
  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

Normalized processing

normalizeProps(child, vm);
normalizeInject(child, vm);
normalizeDirectives(child);
Copy the code

The above code is mainly for the normalization of Vue options. We know that Vue options can be written in a variety of ways, but ultimately all need to be processed into a unified format. The following is a comparison of the various forms with the normalization; The above code implementation is not discussed, but can be navigated directly to the code section.

  • Props:

    • There are several ways to write it:
      • props: ['size', 'myMessage']
      • props: { height: Number }
      • props: { height: { type: Number, default: 0 } }
    • After unified format processing, it is:
      • props: { size: { type: null }, myMessage: { type: null } }
      • props: { height: { type: Number } }
      • props: { height: { type: Number, default: 0 } }
  • Inject:

    • There are several ways to write it:
      • inject: ['foo'].
      • inject: { bar: 'foo' }
    • After unified format processing, it is:
      • inject: { foo: { from: 'foo' } }
      • inject: { bar: { from: 'foo' } }
  • Directives:

    • There are several ways to write it:
      • Directives: {foo: function() {console.log(' Custom directive: V-foo ')}
    • After unified format processing, it is:
      • directives: { foo: { bind: function() { console.log('v-foo'), update: function() { console.log('v-foo') } } }

Consolidation phase

At this point in the code execution, the actual merge will begin, and finally return the merged options.

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

In particular, Vue provides option merge policy functions for each option merge, and the Strats variable holds these functions. We will not expand each policy function separately here.

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

export function mergeDataOrFn(parentVal: any, childVal: any, vm? : Component): ?Function {
  // ...
}

// optionMergeStrategies: Object.create(null),
const strats = config.optionMergeStrategies;

// el/propsData merges policy functions
if(process.env.NODE_ENV ! = ='production') {
  strats.el = strats.propsData = function(parent, child, vm, key) {
    // ...
  };
}

// data merge policy function
strats.data = function(parentVal: any, childVal: any, vm? : Component): ?Function {
  // ...
};

// watch merges policy functions
strats.watch = function(parentVal: ? Object, childVal: ? Object, vm? : Component, key: string): ?Object {
  // ...
};

// props, methods, Inject, computed Merge policy functions
strats.props = strats.methods = strats.inject = strats.computed = function(parentVal: ? Object, childVal: ? Object, vm? : Component, key: string): ?Object {
  // ...
};

// provide merge policy functions
strats.provide = mergeDataOrFn;
Copy the code

Based on the above analysis, the mergeOptions function will return normalized and merged options. The following breakpoint diagram shows merged options:

Execute relevant initializers and call early life cycle functions

initLifecycle(vm); // Initialize the life cycle
initEvents(vm); // Initialize the event
initRender(vm); // Initialize render
callHook(vm, 'beforeCreate'); // Call the lifecycle hook function -- beforeCreate
initInjections(vm); // resolve injections before data/props
initState(vm); Initialize initProps, initMethods, initData, initComputed, and initWatch
initProvide(vm); // resolve provide after data/props
callHook(vm, 'created'); // There is no mount operation at this point, so DOM is not accessible in Created, i.e. $el is not accessible
Copy the code

initLifecycle

  • The following code is mainly done:
    • Find the first non-abstract parent
    • Adds the current instance to the parent instance$childrenIn the attribute
    • And sets the value of the current instance$parentFor the parent instance
    • Set some properties on the current instance
export function initLifecycle(vm: Component) {
  const options = vm.$options;
  Abstract component: it does not render a DOM element by itself and does not appear in the parent component chain. (eg keep-alive transition) */
  let parent = options.parent;
  if(parent && ! options.abstract) {// Loop to find the first non-abstract parent component
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent;
    }
    parent.$children.push(vm);
  }
  vm.$parent = parent;
  vm.$root = parent ? parent.$root : vm;

  vm.$children = [];
  vm.$refs = {};
  vm._watcher = null;
  vm._inactive = null;
  vm._directInactive = false;
  vm._isMounted = false;
  vm._isDestroyed = false;
  vm._isBeingDestroyed = false;
}
Copy the code

initEvents

export function initEvents(vm: Component) {
  // Add the '_events'' _hasHookEvent 'attribute to the current instance
  vm._events = Object.create(null);
  vm._hasHookEvent = false; // An event listener to determine whether lifecycle hooks exist
  const listeners = vm.$options._parentListeners; // Initializes the parent attach event
  if(listeners) { updateComponentListeners(vm, listeners); }}Copy the code

initRender

export function initRender(vm: Component) {
  vm._vnode = null; // the root of the child tree
  vm._staticTrees = null; // v-once cached trees

  / * * * * * * * * * * * * * * * * * * * * * * * * * * * parsing and processing slot * * * * * * * * * * * * * * * * * * * * * * * * * * /
  const options = vm.$options;
  const parentVnode = (vm.$vnode = options._parentVnode); // the placeholder node in parent tree
  const renderContext = parentVnode && parentVnode.context;
  vm.$slots = resolveSlots(options._renderChildren, renderContext);
  vm.$scopedSlots = emptyObject;

  / * * * * * * * * * * * * * * * * * * * * * * * * * * * packing createElement method () * * * * * * * * * * * * * * * * * * * * * * * * * * /
  // render: (createElement: () => VNode) => VNode createElement
  // Bind createElement FN to this instance to get the appropriate rendering context in it.
  // Args order: tags, data, child elements, normalizationType, alwaysNormalize Internal versions are used by the template compiled rendering function
  vm._c = (a, b, c, d) = > createElement(vm, a, b, c, d, false);
  // Normalization is always applied to the public version for user-written rendering functions.
  vm.$createElement = (a, b, c, d) = > createElement(vm, a, b, c, d, true);

  / * * * * * * * * * * * * * * * * * * * * * * * * * * * add $attrs / $listeners in instance * * * * * * * * * * * * * * * * * * * * * * * * * * /
  $listeners and $attrs are used for easier temporary creation. They need to be reactive so that the HOC that uses them is always updated
  const parentData = parentVnode && parentVnode.data;
  if(process.env.NODE_ENV ! = ='production') {
    // Define reactive attributes
    defineReactive(
      vm,
      '$attrs', (parentData && parentData.attrs) || emptyObject, () => { ! isUpdatingChildComponent && warn(`$attrs is readonly.`, vm);
      },
      true
    );
    defineReactive(
      vm,
      '$listeners', options._parentListeners || emptyObject, () => { ! isUpdatingChildComponent && warn(`$listeners is readonly.`, vm);
      },
      true
    );
  } else {
    defineReactive(
      vm,
      '$attrs',
      (parentData && parentData.attrs) || emptyObject,
      null.true
    );
    defineReactive(
      vm,
      '$listeners',
      options._parentListeners || emptyObject,
      null.true
    );
  }
  / * * * * * * * * * * * * * * * * * * * * * * * * * * * add $attrs / $listeners in instance * * * * * * * * * * * * * * * * * * * * * * * * * * /
}
Copy the code

callHook

export function callHook(vm: Component, hook: string) {
  pushTarget(); // To avoid using props data in some lifecycle hooks that result in collecting redundant dependencies #7573
  const handlers = vm.$options[hook];
  if (handlers) {
    // While merging options: The lifecycle hook options are merged into an array
    for (let i = 0, j = handlers.length; i < j; i++) {
      try {
        handlers[i].call(vm);
      } catch (e) {
        // Catch exceptions that may be thrown during the execution of lifecycle functions
        handleError(e, vm, `${hook} hook`); }}}// Determine if there is an event listener for lifecycle hooks, initialize it in initEvents, and trigger a response hook function if there is one
  if (vm._hasHookEvent) {
    vm.$emit('hook:' + hook);
  }
  popTarget();
}
Copy the code

As an additional note, you can use hook: plus lifecycle hook names to listen for the corresponding lifecycle of components

<child
  @hook:beforeCreate="handleChildBeforeCreate"
  @hook:created="handleChildCreated"
  @hook:mounted="handleChildMounted"
  @hook:Lifecycle hook name />
Copy the code

initInjections

export function initInjections(vm: Component) {
  const result = resolveInject(vm.$options.inject, vm); // Function: find the data provided by the parent component
  if (result) {
    // Provide and inject binding are not responsive.
    // This is intentional. However, if you pass in a listening object, the object's properties are still responsive.
    toggleObserving(false); // Turn off reactive detection
    Object.keys(result).forEach(key= > {
      // Define reactive attributes for each attribute and, in non-production environments, provide warnings.
      if(process.env.NODE_ENV ! = ='production') {
        defineReactive(vm, key, result[key], () => {
          warn(
            Avoid directly modifying the injected value, because the changes will be overwritten when the supplied component is re-rendered. Injection being modified:"${key}"`,
            vm
          );
        });
      } else{ defineReactive(vm, key, result[key]); }}); toggleObserving(true); // Enable reactive detection}}Copy the code

initState

/** * Initialize options such as props/ methods/ data/ computed/ watch/. * /
export function initState(vm: Component) {
  vm._watchers = [];
  const opts = vm.$options;
  if (opts.props) initProps(vm, opts.props);
  if (opts.methods) initMethods(vm, opts.methods);
  if (opts.data) {
    initData(vm);
  } else {
    observe((vm._data = {}), true /* asRootData */);
  }
  if (opts.computed) initComputed(vm, opts.computed);
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch);
  }
}
Copy the code

Note: the initialization sequence is simply shown here, and the internal initialization methods will be explored in building responsive systems. All you need to know is the initialization order: props => methods => data => computed => watch

initProvide

export function initProvide(vm: Component) {
  const provide = vm.$options.provide;
  if (provide) {
    vm._provided = typeof provide === 'function'? provide.call(vm) : provide; }}Copy the code

The above initialization part of the analysis, just a simple comb through its implementation process, if you want to do a more detailed understanding of its internal implementation, you can go to see the code implementation or the above mentioned source code parsing related articles.

Depending on the mount point, the mount function is called

If a mount point exists, the mount function is executed to render the component. How the mount function is implemented and how it is implemented will be sorted out later.

if (vm.$options.el) {
  vm.$mount(vm.$options.el);
}
Copy the code

Summary: The full text combs the implementationnew Vue()call_init()Method, followed by code execution to explore the internal implementation.


Following on from above -“Try reading Vue source code” before and after initialization ❓

Following -“Try reading the Vue source code” how responsive systems are built ❓To be continued…