• Vue source code analysis and practice
  • Writer: Slow down

After introducing the Vue directory structure, we will introduce one of the core principles of Vue: the responsive principle. The content of the article mainly includes:

  • Vue entry file introduction
  • The process of new Vue
  • How is data initiated in response mode
  • Responsive processing
  • Some insights from the development process

Vue entry file introduction

1. src/core/index.js

import Vue from './instance/index'

initGlobalAPI(Vue); // Initialize some global apis

export default Vue
Copy the code

Following the entry from the last analysis, the core Vue constructor entry is actually SRC /core/index.js, where various static attributes (vue.config, vue.options, vue.util) are hung on Vue. Static methods (vue. set, vue. nextTick) and so on.

The static attribute is mainly a collection of configurations that affect all instances of Vue. For example, when vue.component is defined as a global component, it exists under the Vue.options.components configuration. When subsequent child components are instantiated, a mergeOption merge configuration is performed by vue. extend, and the sub. options of the child component are inherited from vue. options based on the merge strategy of the different configurations.

2. src/core/instance/index.js

import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '.. /util/index'

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)
}

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue
Copy the code

This module mainly does the following operations:

  • defineVueConstructor;
  • willVuePass in different module functions and inject different logic;

The process of new Vue

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options? :Object) {
    const vm: Component = this
    // a uid
    vm._uid = uid++ // Each vUE instance is initialized with a unique ID

    let startTag, endTag

    // a flag to avoid this being observed
    vm._isVue = true // It is used to identify the current vUE instance to prevent the VM from being intercepted as responsive data

    initLifecycle(vm) // Initialize the life cycle, define some variables, $children, $refs, etc
    initEvents(vm) // Initialize the event system, mainly for $ON, $emit implementation communication
    initRender(vm) // Initialize something related to render, such as vm.$createElement, which is used to write arguments to the render function
    callHook(vm, 'beforeCreate') // Trigger the beforeCreate hook
    initInjections(vm) // resolve injection before data/props // initialize injection relative
    initState(vm)/ / initialization data related, props, the methods, data, computed, watch
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')

    /* istanbul ignore if */
    if(process.env.NODE_ENV ! = ='production' && config.performance && mark) {
      vm._name = formatComponentName(vm, false)
      mark(endTag)
      measure(`vue ${vm._name} init`, startTag, endTag)
    }

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

_init = this._init = this._init = this._init = this._init = this._init = this._init = this._init = this._init

  • define_isVueProperty, used to identify the current VUE instance to avoid instance interception defined as responsive data
  • Initialize the life cycle, define some variables,children, children,children, refs, etc
  • Initialize the event system, mainly for on,on, on,emit implementation communication
  • Initialize something related to render, such as vm.$createElement, which is used to write arguments to the render function
  • Triggers the beforeCreate hook
  • Initialize theinjectInjection related
  • Initialization data related, props, the methods, data, computed, watch, etc.
  • Initialize theproviderelated
  • Set outcreatedhook

It is also easy to understand why the Lifecycle diagram of the Vue says that data is available in the Created lifecycle because initState initialdata operations are completed before the Created hook.

How is data initiated in response mode

After introducing the main process of New Vue, we will focus on how to define responsive data according to the data we normally define.

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

The above code is the internal execution process of initState, which initializes props, Methods, data, computed, and watch in sequence respectively. Reactive interception of data is mainly analyzed based on initData.

initData

function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}; // Initialize data. If the data passed in is function, execute data and assign the return value to _data
  // proxy data on instance
  // Proxy access to key
  const keys = Object.keys(data)
  let i = keys.length
  while (i--) {
    
  } else if(! isReserved(key)) { proxy(vm,`_data`, key)
    }
  }
  // observe data
  // Observe the data
  observe(data, true /* asRootData */)}Copy the code

In initData, the following steps are done:

  • Get the incomingdataConfig if passed asfunctionThe calldata.call(vm, vm)And assigns the returned value tovm._data;
  • After the first step of data proxy, our data is recorded in_dataProperty, then pass if access is requiredvm._data.xxxIt’s kind of redundant to access, soVueThrough the proxy, access can be overenabledvm.xxxIs actually accessedvm._data.xxx, simplifying developer coding;
  • right_dataforobserveObservation (core of responsive data);

Responsive processing

observe

export function observe (value: any, asRootData: ? boolean) :Observer | void {
  if(! isObject(value) || valueinstanceof VNode) {
    return
  }
  let ob: Observer | void
  ob = new Observer(value)
  return ob
}
Copy the code

To simplify the code, observing data is essentially generating an Observer instance of the object and observing the data during observer instantiation.

Observer

export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that have this object as root $data

  constructor (value: any) {
    this.value = value
    def(value, '__ob__'.this)
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }

  /** * Walk through all properties and convert them into * getter/setters. This method should only be called when * value  type is Object. */
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

  /** * Observe a list of Array items. */
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}
Copy the code

Based on the above code, the Observer does the following when instantiating:

  • Defines an unenumerable object on the data__ob__.__ob__Points to the currentobserverThe instance
  • Check if it is an array, if so, modify it__proto__Point to the intercepted array method andobserveArrayIterate over the array and make a recursive observation of each item in the array
  • If it is an object, it passeswalkIterates through each property of the object, according toVue2.xThe core of the APIObject.definePropertyInterception attributeget.setMethod, which is passed later when the property is evaluatedgetDepends on collection, and when setting a value, passessetTrigger the update.

__ob__

At first glance, I never understood the main purpose of this attribute. It turns out that this property is very useful:

  • Can be used to determine whether the current property object is already responsive data
  • Due to modular writing problems, through__ob__To get the current when intercepting array methodsobserverExample to observe the new data

Array response

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

const arrayProto = Array.prototype
// Create a new object based on array. prototype, arraymethods.__proto__ = arrayProto
export const arrayMethods = Object.create(arrayProto) 

const methodsToPatch = [
  'push'.'pop'.'shift'.'unshift'.'splice'.'sort'.'reverse'
];// The set of methods that will change the original array

/** * Intercept mutating methods and emit events */
// Hijack the method that will change the array itself, without affecting the execution of the original array operation at the same time, convenient to observe the new contents of the array
methodsToPatch.forEach(function (method) {
  // cache original method
  const original = arrayProto[method]; // Get the array
  def(arrayMethods, method, function mutator (. args) {
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // notify change
    ob.dep.notify()
    return result
  })
})
Copy the code

The main logical process of the above code is as follows:

  • Prototype, and create a new object based on that prototype object. The __proto__ of that object points to array. prototype, which means that when I access the properties of that object, If you can’t find it, you’ll follow the prototype chain to Array.prototype

  • MethodsToPatch means a patched method with seven operations that modify the contents of the array itself, so you need to hijack these array operations to get new data and observe it. What about this.__ob__?

    • Here’s an example:

      const vm = new Vue({
      	data () {
              return {
                  arr: [1.2.3]}}}); vm.arr.push(4);
      Copy the code

      The code above, when pushing on vm.arr, actually goes to the method above. This in the function refers to vm.arr, which is the array object, and we defined __ob__ for value earlier. ObserveArray is called observeArray to observe the new properties of push, unshift, and splice

Object response

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function, shallow? : boolean) {
  const dep = new Dep()
  Object.defineProperty(obj, key, {
    enumerable: true.configurable: true.get: function reactiveGetter () {
      dep.depend();
      return val
    },
    set: function reactiveSetter (newVal) {
      if (newVal === value) {
        return} val = newVal dep.notify(); }})}Copy the code

A simple implementation of object responsive data is shown above, that is, when we value properties, we collect dependencies with DEP. depend, and when we set values, we update dependencies with dep.notify. The more specific process of dependency collection and triggering updates is covered in a later section.

Some insights from the development process

  • When data defines data, it starts with _ or $if you do not want to expose the data to the outside world. In this way, the data defined is not defined on the instance for proxy access.

    const vm = new Vue({
        data () {
            return {
                _count: 0}}}); vm._count// undefined;
    vm._data._count / / 0
    Copy the code

    And the reason for that is

    • Agreed to_.$The attributes at the beginning are private and cannot be accessed directly from the outside.
    • To avoid contact withVueSome internal implementations are in conflict, such as_router._route, and internally defined private variables_eventsAnd so on, sodataIn order to_.$Initial variables are not proxied to the instance;
  • Try to cache property values during development

    const vm = new Vue({ data () { return { count: 0 } }, mounted () { console.time('time'); for (let i = 0; i < 1000000; i++) { this.count++; } console.log(this.count); console.timeEnd('time'); // time: 160.903076171875 ms}});Copy the code
    const vm = new Vue({ data () { return { count: 0 } }, mounted () { let { count } = this; console.time('time'); for (let i = 0; i < 1000000; i++) { count++; } console.log(count); console.timeEnd('time'); // time: 2.114990234375 ms}});Copy the code

    The main reason for the above gap is that Vue intercepts the attributes in data through Object.defineProperty, which makes various internal judgments and logic processing, so in the loop of Demo1, every value will enter the logic of GET. Performance problems occur. However, in Demo2, only the value operation is performed at the beginning and cached, and the subsequent operations are the cached value. Therefore, the problem of Demo1 is avoided, and the time difference is obviously compared.

conclusion

This article mainly introduces the entry files of Vue, and then introduces the process of new Vue, initialization life cycle, event system, Render correlation, state correlation (props, Methods, data, computed, watch), etc. And how Vue2. X implements responsive data based on Object.defineProperty according to initData. This chapter is followed by two lessons learned during the development process. The space is limited and more content is ignored, if there is not enough specific welcome to point out.