Write it out front (don’t look miss 100 million)

Recently has been reading Vue source code, also wrote a series of source code exploration articles.

However, received a lot of friends of the feedback are: the source code is obscure, often look at do not know what I am looking at, feel a lack of motivation, if you can point out the interview will be asked about the source code related interview questions, through the interview questions to see the source code, it is very good.

Seeing everyone’s feedback, I did not hesitate: arrange!!

Through three articles, I sorted out some Vue interview questions that are often asked in the interview of big factory. I answered them from the perspective of source code and abandoned the pure conceptual answer. I believe that the interviewer will definitely look at you with new eyes.

The source code is based on Vue2.6.11 version

What is the principle of responsive data?

Vue’s core API for implementing responsive data is Object.defineProperty.

In fact, by default, Vue will use object.defineProperty to redefine all attributes in data when initializing data. When the page gets the corresponding attribute, Vue will use object.defineProperty to redefine all attributes. A dependency collection (a watcher that collects the current component) notifies the dependent to update if the property changes.

Here, LET me show you a pictureVueThe process of implementing responsive data:

  • First, the first step is to initialize what the user passes indataThe data. This step corresponds to the source codesrc/core/instance/state.jsThe line 112
function initData (vm: Component{

  let data = vm.$options.data

  data = vm._data = typeof data === 'function'

    ? getData(data, vm)

    : data || {}

  if(! isPlainObject(data)) {

    // ...

  }

  // proxy data on instance

  const keys = Object.keys(data)

  const props = vm.$options.props

  const methods = vm.$options.methods

  let i = keys.length

  while (i--) {

   // ...

  }

  // observe data

  observe(data, true /* asRootData */)

}

Copy the code
  • The second step is to take the data and observe it, which is the first stepinitDataOf the last callobserveFunction. Corresponding in the sourcesrc/core/observer/index.jsThe line 110
/ * *

 * Attempt to create an observer instance for a value,

 * returns the new observer if successfully observed,

 * or the existing observer if the value already has one.

* /


export function observe (value: any, asRootData: ? boolean) :Observer | void {

  if(! isObject(value) || valueinstanceof VNode) {

    return

  }

  let ob: Observer | void

  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {

    ob = value.__ob__

  } else if (

    shouldObserve &&

! isServerRendering() &&

    (Array.isArray(value) || isPlainObject(value)) &&

    Object.isExtensible(value) &&

! value._isVue

  ) {

    ob = new Observer(value)

  }

  if (asRootData && ob) {

    ob.vmCount++

  }

  return ob

}

Copy the code

New Observer(value) creates an Observer instance to observe the data.

  • The third step is to implement the processing of the object. Corresponding sourcesrc/core/observer/index.js55.
/ * *

 * Observer class that is attached to each observed

 * object. Once attached, the observer converts the target

 * object's property keys into getter/setters that

 * collect dependencies and dispatch updates.

* /


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

    this.dep = new Dep()

    this.vmCount = 0

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

    }

  }

  // ...

}

Copy the code
  • The fourth step is to loop object property definitions in response to changes. Corresponding sourcesrc/core/observer/index.jsThe 135 lines.
/ * *

 * Define a reactive property on an Object.

* /


export function defineReactive (

  obj: Object,

  key: string,

  val: any,

customSetter? :? Function,

shallow? : boolean

{

  const dep = new Dep()



  const property = Object.getOwnPropertyDescriptor(obj, key)

  if (property && property.configurable === false) {

    return

  }



  // cater for pre-defined getter/setters

  const getter = property && property.get

  const setter = property && property.set

  if((! getter || setter) &&arguments.length === 2) {

    val = obj[key]

  }



  letchildOb = ! shallow && observe(val)

  Object.defineProperty(obj, key, {

    enumerabletrue.

    configurabletrue.

    getfunction reactiveGetter ({

      const value = getter ? getter.call(obj) : val

      if (Dep.target) {

        dep.depend()  // Collect dependencies

        // ...

      }

      return value

    },

    setfunction reactiveSetter (newVal{

      // ...

      dep.notify()  // Notify dependencies to be updated

    }

  })

}

Copy the code
  • The fifth step is actually usingdefineReactiveIn the methodObject.definePropertyRedefine the data. ingetThrough thedep.depend()Collect dependencies. When the data changes, the update operation of the intercepting property passessetIn thedep.notify()Notifies dependencies to update.

How does Vue detect array changes?

There are two core points in Vue for detecting array changes:

  • First, we override the array methods by using function hijacking
  • VuedataIn the array, the prototype chain was rewritten. Refers to the self – defined array prototype method, so that when the array is calledapi, you can notify the dependency updates. If the array contains a reference type, the array reference type is observed again.

Here is a flowchart to illustrate:

So step 1 and step 2 and the last question what is the principle of responsive data? It’s the same thing. I won’t expand it.

  • The first step is also to initialize the data passed in by the user. Corresponding sourcesrc/core/instance/state.jsThe 112 – lineinitDataFunction.
  • The second step is to look at the data. Corresponding sourcesrc/core/observer/index.jsThe 124 lines.
  • The third step is to point the array’s stereotype method to the overridden stereotype. Corresponding sourcesrc/core/observer/index.js49.
if (hasProto) {

  protoAugment(value, arrayMethods)

else {

  // ...

}

Copy the code

The protoAugment method:

/ * *

 * Augment a target Object or Array by intercepting

 * the prototype chain using __proto__

* /


function protoAugment (target, src: Object{

  /* eslint-disable no-proto */

  target.__proto__ = src

  /* eslint-enable no-proto */

}

Copy the code
  • The fourth step is a two-step operation. The first is to rewrite the array of the prototype method, corresponding to the source codesrc/core/observer/array.js.
/ *

 * not type checking this file because flow doesn't play well with

 * dynamically accessing methods on Array prototype

* /




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



const arrayProto = Array.prototype

export const arrayMethods = Object.create(arrayProto)



const methodsToPatch = [  // The array methods listed here can be called to change the original array

  'push'.

  'pop'.

  'shift'.

  'unshift'.

  'splice'.

  'sort'.

  'reverse'

]



/ * *

 * Intercept mutating methods and emit events

* /


methodsToPatch.forEach(function (method{  // Override the prototype method

  // cache original method

  const original = arrayProto[method]  // Call the original array method

  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)  // Perform in-depth monitoring

    // notify change

    ob.dep.notify()  // Manually notify the view of the update after calling the array method

    return result

  })

})



Copy the code

The second step is to call the observeArray method on the array:

// src/core/observer/index.js line:74

/ * *

 * 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

So what you’re doing is you’re going through the array, calling the observe method for each of the entries, and you’re doing a deep observation.

whyVueAsynchronous rendering?

Let’s start with a question: if Vue does not use asynchronous updates, does it override the current component every time data is updated?

The answer is yes, for performance reasons, the view will be updated asynchronously after this round of data update.

Let me show you a pictureVueThe process of asynchronous update:

  • Step 1 Calldep.notify()noticewatcherPerform the update operation. Corresponding sourcesrc/core/observer/dep.jsIn line 37.
notify () {  // Notify dependency updates

  // stabilize the subscriber list first

  const subs = this.subs.slice()

  if(process.env.NODE_ENV ! = ='production' && !config.async) {

    // subs aren't sorted in scheduler if not running async

    // we need to sort them now to make sure they fire in correct

    // order

    subs.sort((a, b) = > a.id - b.id)

  }

  for (let i = 0, l = subs.length; i < l; i++) {

    subs[i].update()  // The update method in the dependency

  }

}

Copy the code
  • The second step is the same as the first stepnotifyMethod, traversalsubs, the implementation ofsubs[i].update()Method, which is called sequentiallywatchertheupdateMethods. Corresponding sourcesrc/core/observer/watcher.jsThe line 164
/ * *

 * Subscriber interface.

 * Will be called when a dependency changes.

* /


update () {

  /* istanbul ignore else */

  if (this.lazy) {  // Calculate the properties

    this.dirty = true

  } else if (this.sync) {  / / synchronize watcher

    this.run()

  } else {

    queueWatcher(this)  // Put watcher in a queue for batch updates when data changes

  }

}

Copy the code
  • The third step is executionupdateIn the functionqueueWatcherMethods. Corresponding sourcesrc/core/observer/scheduler.jsThe 164 lines.


/ * *

 * Push a watcher into the watcher queue.

 * Jobs with duplicate IDs will be skipped unless it's

 * pushed when the queue is being flushed.

* /


export function queueWatcher (watcher: Watcher{

  const id = watcher.id  // Filter the watcher. Multiple properties may depend on the same watcher

  if (has[id] == null) {

    has[id] = true

    if(! flushing) {

      queue.push(watcher)  // Put watcher in the queue

    } else {

      // if already flushing, splice the watcher based on its id

      // if already past its id, it will be run next immediately.

      let i = queue.length - 1

      while (i > index && queue[i].id > watcher.id) {

        i--

      }

      queue.splice(i + 1.0, watcher)

    }

    // queue the flush

    if(! waiting) {

      waiting = true



      if(process.env.NODE_ENV ! = ='production' && !config.async) {

        flushSchedulerQueue()

        return

      }

      nextTick(flushSchedulerQueue)  // call the nextTick method to refresh the watcher queue in the nextTick

    }

  }

}

Copy the code
  • The fourth step is executionnextTick(flushSchedulerQueue)Method, next onetickThe refreshwatcherThe queue

Talk about thenextTickThe implementation principle of?

By default, vue.js pushes the Watcher object into a queue every time a setter is triggered. On the next tick, pull out the entire queue and run it again.

Because the browser platform currently does not implement the nextTick method, the weee. js source code uses promises, setTimeout, and setImmediate to create an event in a microtask. The purpose is to execute the event after the current call stack finishes (not necessarily immediately).

The nextTick method uses macrotasks and microtasks to define an asynchronous method. Calling nextTick multiple times will put the method in the queue, using this asynchronous method to clear the current queue.

So this nextTick method is asynchronous.

Let’s look at it in a picturenextTickThe implementation of the:

  • First of all, it callsnextTickAnd the incomingcb. Corresponding sourcesrc/core/util/next-tick.jsThe 87 lines.
export function nextTick (cb? : Function, ctx? : Object{

  let _resolve

  callbacks.push((a)= > {

    if (cb) {

      try {

        cb.call(ctx)

      } catch (e) {

        handleError(e, ctx, 'nextTick')

      }

    } else if (_resolve) {

      _resolve(ctx)

    }

  })

  if(! pending) {

    pending = true

    timerFunc()

  }

  // $flow-disable-line

  if(! cb &&typeof Promise! = ='undefined') {

    return new Promise(resolve= > {

      _resolve = resolve

    })

  }

}

Copy the code
  • I’m going to define onecallbacksArrays are used to storenextTick, nexttickBefore dealing with these callbacks, allcbIt’s going to be therecallbacksIn the array.
  • The next step is to calltimerFuncFunction. Corresponding sourcesrc/core/util/next-tick.js33.
let timerFunc



if (typeof Promise! = ='undefined' && isNative(Promise)) {

  timerFunc = (a)= > {

    // ...

  }

  isUsingMicroTask = true

else if(! isIE &&typeofMutationObserver ! = ='undefined' && (

  isNative(MutationObserver) ||

  // PhantomJS and iOS 7.x

  MutationObserver.toString() === '[object MutationObserverConstructor]'

)) {



  timerFunc = (a)= > {

    // ...

  }

  isUsingMicroTask = true

else if (typeofsetImmediate ! = ='undefined' && isNative(setImmediate)) {

  timerFunc = (a)= > {

    setImmediate(flushCallbacks)

  }

else {

  // Fallback to setTimeout.

  timerFunc = (a)= > {

    setTimeout(flushCallbacks, 0)

  }

}

Copy the code

TimerFunc value logic:

1. We know that there are two kinds of asynchronous tasks, microTask is better than macroTask, so Promise is preferred. So here we first determine whether a Promise is supported by the browser.

2. Macrotask is not supported. Macrotask will determine whether the browser supports MutationObserver and setImmediate.

3. If neither is supported, use setTimeout only. This also shows that setTimeout is the worst performance in macroTask.

NextTick the if (! “Pending” is a statement that allows the logic of the if statement to be executed only once, which in fact indicates whether there are events in the callbacks waiting for execution.

The main logic for the flushCallbacks function is to set pending to false and empty the callbacks array, then iterate through the callbacks array and execute each function in the array.

  • nextTickThe last step corresponds to:
if(! cb &&typeof Promise! = ='undefined') {

  return new Promise(resolve= > {

    _resolve = resolve

  })

}

Copy the code

In this case, if we call nextTick with no callback passed and the browser supports promises, we return a Promise instance and assign resolve to _resolve. Back to the code at the beginning of nextTick:

let _resolve

callbacks.push((a)= > {

  if (cb) {

    try {

      cb.call(ctx)

    } catch (e) {

      handleError(e, ctx, 'nextTick')

    }

  } else if (_resolve) {

    _resolve(ctx)

  }

})

Copy the code

When we execute the callbacks function and find no CB but _resolve, we execute the resolve function on the Promise object we returned earlier.

Do you knowVueIn thecomputedHow does it work?

Here’s a conclusion: The computed property computed is essentially computed Watcher, which has a cache.

A picture of the computed implementation:

  • The first is when the component is instantiatedinitComputedMethods. Corresponding sourcesrc/core/instance/state.jsThe 169 lines.
const computedWatcherOptions = { lazytrue }



function initComputed (vm: Component, computed: Object{

  // $flow-disable-line

  const watchers = vm._computedWatchers = Object.create(null)

  // computed properties are just getters during SSR

  const isSSR = isServerRendering()



  for (const key in computed) {

    const userDef = computed[key]

    const getter = typeof userDef === 'function' ? userDef : userDef.get

    if(process.env.NODE_ENV ! = ='production' && getter == null) {

      warn(

        `Getter is missing for computed property "${key}". `.

        vm

      )

    }



    if(! isSSR) {

      // create internal watcher for the computed property.

      watchers[key] = new Watcher(

        vm,

        getter || noop,

        noop,

        computedWatcherOptions

      )

    }



    // component-defined computed properties are already defined on the

    // component prototype. We only need to define computed properties defined

    // at instantiation here.

    if(! (keyin vm)) {

      defineComputed(vm, key, userDef)

    } else if(process.env.NODE_ENV ! = ='production') {

      if (key in vm.$data) {

        warn(`The computed property "${key}" is already defined in data.`, vm)

      } else if (vm.$options.props && key in vm.$options.props) {

        warn(`The computed property "${key}" is already defined as a prop.`, vm)

      }

    }

  }

}

Copy the code

The initComputed function takes a computed object and then walks through each of the computed properties. Determine that a computedWatcher instance is created for watchers[key] if it is not rendered on the server (the corresponding value is vm._computedWatchers[key]). Then call the defineComputed method over each evaluated property, passing in the component stereotype, evaluated property, and corresponding value.

  • defineComputedDefined in source codesrc/core/instance/state.jsLine 210.
// src/core/instance/state.js

export function defineComputed(

  target: any,

  key: string,

  userDef: Object | Function

{

  constshouldCache = ! isServerRendering();

  if (typeof userDef === "function") {

    sharedPropertyDefinition.get = shouldCache

      ? createComputedGetter(key)

      : createGetterInvoker(userDef);

    sharedPropertyDefinition.set = noop;

  } else {

    sharedPropertyDefinition.get = userDef.get

? shouldCache && userDef.cache ! = =false

        ? createComputedGetter(key)

        : createGetterInvoker(userDef.get)

      : noop;

    sharedPropertyDefinition.set = userDef.set || noop;

  }

  if (

process.env.NODE_ENV ! = ="production" &&

    sharedPropertyDefinition.set === noop

  ) {

    sharedPropertyDefinition.set = function ({

      warn(

        `Computed property "${key}" was assigned to but it has no setter.`.

        this

      );

    };

  }

  Object.defineProperty(target, key, sharedPropertyDefinition);

}

Copy the code

We first defined shouldCache to indicate whether or not we want to cache values. UserDef is then treated separately as a function or an object. Here’s a sharedPropertyDefinition, let’s look at its definition:

// src/core/instance/state.js

const sharedPropertyDefinition = {

  enumerabletrue.

  configurabletrue.

  get: noop,

  set: noop,

};

Copy the code

SharedPropertyDefinition is just a property descriptor.

Back to the defineComputed function. If userDef is a function, the getter is defined to the return value of the createComputedGetter(key) call.

Because shouldCache is true

The getter is also the return value of the createComputedGetter(key) call, and the setter is either userDef.set or empty, if userDef is an object that is not rendered on the server and cache is not specified to be false.

So the defineComputed function defines getters and setters, and then calls object.defineProperty at the end to add a getter/setter to the evaluated property, This getter is fired when we access the evaluated property.

Setters for computed properties are actually rarely used unless we specify a set function for computed.

  • Whether it isuserDefWhether it is a function or an object, it will be called eventuallycreateComputedGetterFunction, let’s seecreateComputedGetterDefinition:
function createComputedGetter(key{

  return function computedGetter({

    const watcher = this._computedWatchers && this._computedWatchers[key];

    if (watcher) {

      if (watcher.dirty) {

        watcher.evaluate();

      }

      if (Dep.target) {

        watcher.depend();

      }

      return watcher.value;

    }

  };

}

Copy the code

We know that this getter is triggered when the computed property is accessed, and that’s when the computedGetter is executed.

ComputedGetter first takes a computedWatcher created during the previous component instantiation through this._computedWatchers[key] and assigns it to Watcher.

ComputedWatcherOptions, the fourth argument passed to new Watcher, has a value of true for lazy, which corresponds to the value of true for dirty in Watcher’s constructor. In computedGetter, if dirty is false (that is, the value of the dependency has not changed), it is not re-evaluated. That is, computed is cached.

Then there are two if judgments, first calling the evaluate function:

/ * *

 * Evaluate the value of the watcher.

 * This only gets called for lazy watchers.

* /


evaluate () {

  this.value = this.get()

  this.dirty = false

}

Copy the code

First call this.get() and assign its return value to this.value. Let’s look at get:

// src/core/observer/watcher.js

/ * *

 * Evaluate the getter, and re-collect dependencies.

* /


get () {

  pushTarget(this)

  let value

  const vm = this.vm

  try {

    value = this.getter.call(vm, vm)

  } catch (e) {

    if (this.user) {

      handleError(e, vm, `getter for watcher "The ${this.expression}"`)

    } else {

      throw e

    }

  } finally {

    // "touch" every property so they are all tracked as

    // dependencies for deep watching

    if (this.deep) {

      traverse(value)

    }

    popTarget()

    this.cleanupDeps()

  }

  return value

}

Copy the code

The first step in get is to call pushTarget and pass in the computed Watcher:

// src/core/observer/dep.js

export function pushTarget(target: ? Watcher{

  targetStack.push(target);

  Dep.target = target;

}

Copy the code

You can see that computed Watcher is pushed to the targetStack and dep. target is set to computed Watcher. The original value of dep. target is render Watcher because it is in the render phase. Go back to the get function, and then call this.getter.

Back to the evaluate function:

evaluate () {

  this.value = this.get()

  this.dirty = false

}

Copy the code

After executing the get function, set dirty to false.

Back to computedGetter, and then down to another if, we execute the depend function:

// src/core/observer/watcher.js

/ * *

 * Depend on all deps collected by this watcher.

* /


depend () {

  let i = this.deps.length

  while (i--) {

    this.deps[i].depend()

  }

}

Copy the code

The logic here is to make dep. target, which is rendering Watcher, subscribe to this. Dep, which is the Dep instance created when we instantiated Computed Watcher, The render Watcher is saved in the subs of this.dep.

The execution of theevaluatedependAfter the function,computedGetterThe function ends withevaluateThe return value of the property is returned, which is the final calculated value of the computed property, and the page is rendered.