Detailed explanation of Vue source code (I): preparations before generating Vue instances
1. Start with new Vue()
vue/src/core/index.js :
import Vue from './instance/index' // 1. Introduce Vue constructor import { initGlobalAPI } from './global-api/index' // 2. Introduce the dependency to initialize the global API import { isServerRendering } from 'core/util/env' import { FunctionalRenderContext } from 'core/vdom/create-functional-component' initGlobalAPI(Vue) // 3. Initialize global API Object.defineProperty(Vue.prototype, '$isServer', { get: isServerRendering }) Object.defineProperty(Vue.prototype, '$ssrContext', { get () { /* istanbul ignore next */ return this.$vnode && this.$vnode.ssrContext } }) // expose FunctionalRenderContext for ssr runtime helper installation Object.defineProperty(Vue, 'FunctionalRenderContext', { value: FunctionalRenderContext }) Vue.version = '__VERSION__' export default Vue
Since you start with new Vue(), you must first understand the constructor of Vue. As can be seen from the above code Note 1, Vue's constructor is in Vue / SRC / core / instance / index JS, the source code is as follows:
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) // 1. Vue instance initialization } initMixin(Vue) // 2 stateMixin(Vue) // 3 eventsMixin(Vue) // 4 lifecycleMixin(Vue) // 5 renderMixin(Vue) // 6 export default Vue
Note 1: when new Vue(), only one initialization work is performed this_ init(options) ; It is worth noting that after the constructor is defined, there is no call to new Vue at this time, that is, before the instance is created, the initialization at note 2 3 4 5 6 will be executed to initialize the global API. So far, the preparations are ready. When the Vue instance is generated by calling new Vue, this will be called_ init(options) . Next, explore what Vue did in turn before generating an instance.
1.1 initMixin (vue\src\core\instance\init.js)
let uid = 0 export function initMixin (Vue: Class<Component>) { Vue.prototype._init = function (options?: Object) { const vm: Component = this // 1. vm is this, the instance object of Vue // a uid vm._uid = uid++ // Each Vue instance object can be regarded as a component, and each component has one_ uid attribute to mark uniqueness let startTag, endTag /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { startTag = `vue-perf-start:${vm._uid}` endTag = `vue-perf-end:${vm._uid}` mark(startTag) } // a flag to avoid this being observed vm._isVue = true // merge options // Merge parameters. options are the parameters passed in when we call 'new Vue ({El:' app 'chuand,... Args})' // After the merge is completed, mount the merge results to the current 'Vue' instance 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) } else { vm.$options = mergeOptions( // After the merge is completed, mount the merge results to the current 'Vue' instance // This function will check whether the current Vue instance has an earlier function and the options options on its parent class and ancestor class, and can listen for changes and combine the options of the ancestor class, parent class and the current Vue instance resolveConstructorOptions(vm.constructor), options || {}, vm ) } /* istanbul ignore else */ if (process.env.NODE_ENV !== 'production') { initProxy(vm) } else { vm._renderProxy = vm } // expose real self vm._self = vm initLifecycle(vm) // 1. Initialize the declaration cycle initEvents(vm) initRender(vm) callHook(vm, 'beforeCreate') initInjections(vm) // resolve injections before data/props initState(vm) 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) } } }
1.1.1 initLifecycle
The above code marks each instance with a unique_ uid attribute, and then mark whether it is a Vue instance. After merging the parameters passed in by the user and Vue's own parameters, mount it to the $options attribute of Vue.
export function initLifecycle (vm: Component) { const options = vm.$options // locate first non-abstract parent // This annotation is very clear. It is to find the first non Abstract parent component of the current vue instance // When found, the current component will be merged into the ` $children 'array of the parent component // Thus, the parent-child relationship of components is established let parent = options.parent if (parent && !options.abstract) { 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 // These two ignore first. I don't know why vm._directInactive = false // These two ignore first. I don't know why vm._isMounted = false vm._isDestroyed = false vm._isBeingDestroyed = false }
As mentioned above, when initializing the declaration cycle, the parent-child relationship between the current component and other components will be described. If the parent component is found, the $root pointer will point to the parent component. If it is not found, it will point to the current Vue instance. Next, VM$ Children = [] initialize sub component list, VM$ Refs = {} initialize reference list, VM_ Watcher = null initializes the observer list. At this time, there is no observer, and data changes cannot be detected. VM_ Ismounted = false indicates that the current component has not been mounted to DOM, VM_ Isdestroyed = false indicates that the current component is not a destroyed instance, which is related to garbage collection, VM_ Isbeingdestroyed = false indicates whether the current component is destroying work.
At this point, the initialization of the declaration cycle has been completed.
1.1.2 initEvents
vue/src/core/instance/events.js :
1.2 stateMixin: status initialization
vue/src/core/instance/state.js :
export function stateMixin (Vue: Class<Component>) { // flow somehow has problems with directly declared definition object // when using Object.defineProperty, so we have to procedurally build up // the object here. const dataDef = {} dataDef.get = function () { return this._data } const propsDef = {} propsDef.get = function () { return this._props } if (process.env.NODE_ENV !== 'production') { dataDef.set = function () { warn( 'Avoid replacing instance root $data. ' + 'Use nested data properties instead.', this ) } propsDef.set = function () { warn(`$props is readonly.`, this) } } Object.defineProperty(Vue.prototype, '$data', dataDef) // 1 Object.defineProperty(Vue.prototype, '$props', propsDef) // 2 Vue.prototype.$set = set // 3 Vue.prototype.$delete = del // 4 Vue.prototype.$watch = function ( expOrFn: string | Function, cb: any, options?: Object ): Function { const vm: Component = this if (isPlainObject(cb)) { return createWatcher(vm, expOrFn, cb, options) } options = options || {} options.user = true const watcher = new Watcher(vm, expOrFn, cb, options) if (options.immediate) { try { cb.call(vm, watcher.value) } catch (error) { handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`) } } return function unwatchFn () { watcher.teardown() } } }
The above code initializes the state of the instance. In notes 1 and 2, two attributes of $data $props are added to the Vue prototype object respectively, and the values of these two attributes are the values of the current vm_ data _props property, and setting these two properties cannot be modified.
In note 3 and 4, set and delete methods are added to vm. There is no need to introduce what set and delete are. Vue objects also have Vue Set and Vue Both delete methods are derived from the function set below. Their functions are reflected in 1 and 2 of the code comments below:
The parameter target is an object or array, and the target has a__ ob__ Attribute. The source of this attribute is the constructor in the Observer class. One sentence is def (value, '_ob_', this). Value is the object to be observed, that is, the attribute in the data passed in when we write code, and then the data we pass in is actually managed__ ob__ This attribute is used. In the future, when we operate on the data in data or access the data in data, it will be proxied__ ob__ This property.
Then the $watcher method is mounted on the prototype object, and the return value of this method is a method to destroy the watcher. As for what the watcher is and its role, we'll talk about it later.
/** * Set a property on an object. Adds the new property and * triggers change notification if the property doesn't * already exist. */ export function set (target: Array<any> | Object, key: any, val: any): any { if (process.env.NODE_ENV !== 'production' && (isUndef(target) || isPrimitive(target)) ) { warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`) } if (Array.isArray(target) && isValidArrayIndex(key)) { target.length = Math.max(target.length, key) target.splice(key, 1, val) return val } if (key in target && !(key in Object.prototype)) { target[key] = val return val } const ob = (target: any).__ob__ if (target._isVue || (ob && ob.vmCount)) { process.env.NODE_ENV !== 'production' && warn( 'Avoid adding reactive properties to a Vue instance or its root $data ' + 'at runtime - declare it upfront in the data option.' ) return val } if (!ob) { target[key] = val return val } defineReactive(ob.value, key, val) // 1 ob.dep.notify() // 2 return val }
vue\src\core\util\lang.js :
/** * Define a property. */ export function def (obj: Object, key: string, val: any, enumerable?: boolean) { Object.defineProperty(obj, key, { value: val, enumerable: !!enumerable, writable: true, configurable: true }) }
1.2.3 watcher
vue/src/core/observer/watcher.js: it can be said to be one of the quintessence of Vue. It is based on observer mode and publish subscribe mode, and uses object. In ES5 specification The defineproperty interface realizes the observation of data and the automatic update of view. Let's analyze it bit by bit.
/* @flow */ import { warn, remove, isObject, parsePath, _Set as Set, handleError, noop } from '../util/index' import { traverse } from './traverse' import { queueWatcher } from './scheduler' import Dep, { pushTarget, popTarget } from './dep' import type { SimpleSet } from '../util/index' let uid = 0 /** * A watcher parses an expression, collects dependencies, * and fires callback when the expression value changes. * This is used for both the $watch() api and directives. */ export default class Watcher { vm: Component; expression: string; cb: Function; id: number; deep: boolean; user: boolean; lazy: boolean; sync: boolean; dirty: boolean; active: boolean; deps: Array<Dep>; newDeps: Array<Dep>; depIds: SimpleSet; newDepIds: SimpleSet; before: ?Function; getter: Function; value: any; constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean ) { this.vm = vm if (isRenderWatcher) { vm._watcher = this } vm._watchers.push(this) // options if (options) { this.deep = !!options.deep this.user = !!options.user this.lazy = !!options.lazy this.sync = !!options.sync this.before = options.before } else { this.deep = this.user = this.lazy = this.sync = false } this.cb = cb this.id = ++uid // uid for batching this.active = true this.dirty = this.lazy // for lazy watchers this.deps = [] this.newDeps = [] this.depIds = new Set() this.newDepIds = new Set() this.expression = process.env.NODE_ENV !== 'production' ? expOrFn.toString() : '' // parse expression for getter if (typeof expOrFn === 'function') { this.getter = expOrFn } else { this.getter = parsePath(expOrFn) if (!this.getter) { this.getter = noop process.env.NODE_ENV !== 'production' && warn( `Failed watching path: "${expOrFn}" ` + 'Watcher only accepts simple dot-delimited paths. ' + 'For full control, use a function instead.', vm ) } } this.value = this.lazy ? undefined : this.get() } /** * 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 "${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 } /** * Add a dependency to this directive. */ addDep (dep: Dep) { const id = dep.id if (!this.newDepIds.has(id)) { this.newDepIds.add(id) this.newDeps.push(dep) if (!this.depIds.has(id)) { dep.addSub(this) } } } /** * Clean up for dependency collection. */ cleanupDeps () { let i = this.deps.length while (i--) { const dep = this.deps[i] if (!this.newDepIds.has(dep.id)) { dep.removeSub(this) } } let tmp = this.depIds this.depIds = this.newDepIds this.newDepIds = tmp this.newDepIds.clear() tmp = this.deps this.deps = this.newDeps this.newDeps = tmp this.newDeps.length = 0 } /** * Subscriber interface. * Will be called when a dependency changes. */ update () { /* istanbul ignore else */ if (this.lazy) { this.dirty = true } else if (this.sync) { this.run() } else { queueWatcher(this) } } /** * Scheduler job interface. * Will be called by the scheduler. */ run () { if (this.active) { const value = this.get() if ( value !== this.value || // Deep watchers and watchers on Object/Arrays should fire even // when the value is the same, because the value may // have mutated. isObject(value) || this.deep ) { // set new value const oldValue = this.value this.value = value if (this.user) { try { this.cb.call(this.vm, value, oldValue) } catch (e) { handleError(e, this.vm, `callback for watcher "${this.expression}"`) } } else { this.cb.call(this.vm, value, oldValue) } } } } /** * Evaluate the value of the watcher. * This only gets called for lazy watchers. */ evaluate () { this.value = this.get() this.dirty = false } /** * Depend on all deps collected by this watcher. */ depend () { let i = this.deps.length while (i--) { this.deps[i].depend() } } /** * Remove self from all dependencies' subscriber list. */ teardown () { if (this.active) { // remove self from vm's watcher list // this is a somewhat expensive operation so we skip it // if the vm is being destroyed. if (!this.vm._isBeingDestroyed) { remove(this.vm._watchers, this) } let i = this.deps.length while (i--) { this.deps[i].removeSub(this) } this.active = false } } }
1.3 event initialization
In fact, some methods ($on $once $off $emit) are mounted on the Vue prototype object, and an event response system is implemented based on the publish subscribe mode, which is very similar to the eventEmitter in nodejs. This is the source of our common event bus mechanism.
Simply parse the following code:
$on is an event subscription. From its parameters (event: string | array < string >, FN: function), you can subscribe to multiple events at one time. They share a processing function, and then store all processing functions in VM in the form of key value pairs ({eventName: handler []})_ In the events object, wait for the event to be published. Once the event is published, it will go to the event handler list (handler []) according to the event type (eventName), read the handler and execute it.
$emit is the release of events. In the production environment, the case of event names (i.e. types) is converted. There is no need to distinguish the case of event names. Of course, we can't write them in such a rough way. Then cbs is the list of processing functions read according to the event name. const args = toArray(arguments, 1) is the parameter for processing the event. The function toArray removes the first parameter of the $emit function and finally passes it into our subscription function. Namely
vm.$ The final call result of emit ('render ',' a ', 124) code is VM_ All functions in the events ['render '] list run once with ('a', 123) as parameters.
$off is to delete the subscription function of the event from the subscription list. It provides two parameters (event?: string | array < string >, fn?: function). Both parameters are optional and cannot only wear the second parameter. If the argument list is empty, all subscribed events and event handling functions on the current vm will be deleted; If the second parameter is empty, the vm of the current vm_ All processing functions in events [event] will be cleared; If the second parameter fn is not empty, only vm_ Events [event] the fn function in the event handling list is deleted.
$once means that event processing is executed only once. If events are published multiple times, the processing function will be executed only once. This function is a little tricky. First create an on function, and then mount the event handling function fn to the function object. The function is also an object and can have its own properties. There is no doubt about this. There are only two sentences of code vm in the on function$ Off (event, on), let vm unsubscribe from the on function, which can ensure that the on function will not be executed in the future; Next sentence FN Apply (vm, arguments) calls FN, which ensures that FN is executed once. Hahaha, 666
This completes the initialization of the event.
export function eventsMixin (Vue: Class<Component>) { const hookRE = /^hook:/ Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component { const vm: Component = this if (Array.isArray(event)) { for (let i = 0, l = event.length; i < l; i++) { vm.$on(event[i], fn) } } else { (vm._events[event] || (vm._events[event] = [])).push(fn) // optimize hook:event cost by using a boolean flag marked at registration // instead of a hash lookup if (hookRE.test(event)) { vm._hasHookEvent = true } } return vm } Vue.prototype.$once = function (event: string, fn: Function): Component { const vm: Component = this function on () { vm.$off(event, on) fn.apply(vm, arguments) } on.fn = fn vm.$on(event, on) return vm } Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component { const vm: Component = this // all if (!arguments.length) { vm._events = Object.create(null) return vm } // array of events if (Array.isArray(event)) { for (let i = 0, l = event.length; i < l; i++) { vm.$off(event[i], fn) } return vm } // specific event const cbs = vm._events[event] if (!cbs) { return vm } if (!fn) { vm._events[event] = null return vm } // specific handler let cb let i = cbs.length while (i--) { cb = cbs[i] if (cb === fn || cb.fn === fn) { cbs.splice(i, 1) break } } return vm } Vue.prototype.$emit = function (event: string): Component { const vm: Component = this if (process.env.NODE_ENV !== 'production') { const lowerCaseEvent = event.toLowerCase() if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) { tip( `Event "${lowerCaseEvent}" is emitted in component ` + `${formatComponentName(vm)} but the handler is registered for "${event}". ` + `Note that HTML attributes are case-insensitive and you cannot use ` + `v-on to listen to camelCase events when using in-DOM templates. ` + `You should probably use "${hyphenate(event)}" instead of "${event}".` ) } } let cbs = vm._events[event] if (cbs) { cbs = cbs.length > 1 ? toArray(cbs) : cbs const args = toArray(arguments, 1) const info = `event handler for "${event}"` for (let i = 0, l = cbs.length; i < l; i++) { invokeWithErrorHandling(cbs[i], vm, args, vm, info) } } return vm } }
1.4 lifecycle mixin lifecycle initialization
The code is as follows. Three methods are added to the prototype object of Vue_ update $forceUpdate $destroy, take a look at what has been done in turn.
vm._update passed__ patch__ The function compiles the virtual node vnode into a real DOM In addition, the component update is also here to complete the transformation from the virtual node to the real DOM. After the parent component is updated, the child component will also be updated.
vm.$forceUpdate if there are observers on the current component, the component will be refined directly.
vm.$destroy destroys components. If the current component is going through the destruction process, it will return directly and wait for continued destruction. Otherwise, the beforeDestroy declaration cycle will be triggered and the current component will be marked as being destroyed. Then delete the current component from the parent component, destroy all watcher s, and destroy VM_ data__ ob__ , Mark the component status as destroyed, regenerate the real DOM, trigger the destroyed life cycle method, remove the events subscribed by the current component and the event handler, and clear the reference of the current component to the parent component.
vue/src/core/instance/lifecycle.js
export function lifecycleMixin (Vue: Class<Component>) { Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) { const vm: Component = this const prevEl = vm.$el const prevVnode = vm._vnode const restoreActiveInstance = setActiveInstance(vm) vm._vnode = vnode // Vue.prototype.__patch__ is injected in entry points // based on the rendering backend used. if (!prevVnode) { // initial render vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */) } else { // updates vm.$el = vm.__patch__(prevVnode, vnode) } restoreActiveInstance() // 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. } Vue.prototype.$forceUpdate = function () { const vm: Component = this if (vm._watcher) { vm._watcher.update() } } Vue.prototype.$destroy = function () { const vm: Component = this if (vm._isBeingDestroyed) { return } callHook(vm, 'beforeDestroy') vm._isBeingDestroyed = true // remove self from parent const parent = vm.$parent if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) { remove(parent.$children, vm) } // teardown watchers if (vm._watcher) { vm._watcher.teardown() } let i = vm._watchers.length while (i--) { vm._watchers[i].teardown() } // remove reference from data ob // frozen object may not have observer. if (vm._data.__ob__) { vm._data.__ob__.vmCount-- } // call the last hook... vm._isDestroyed = true // invoke destroy hooks on current rendered tree vm.__patch__(vm._vnode, null) // fire destroyed hook callHook(vm, 'destroyed') // turn off all instance listeners. vm.$off() // remove __vue__ reference if (vm.$el) { vm.$el.__vue__ = null } // release circular reference (#6759) if (vm.$vnode) { vm.$vnode.parent = null } } }
1.5 renderMixin
It also mounts some methods to Vue's prototype object.
Installrender helpers (Vue. Prototype) adds some methods required for template parsing and compilation to vm;
$nextTick is this we often use when writing code$ nextTick(), which returns a Promise instance p. we can access the data updated to the DOM element in the then function of p, or send it to this nextTick passes a callback function f, which can also access the data updated to the DOM element.
_ The render method generates a virtual node. See code below for details.
vue/src/core/instance/render.js
export function renderMixin (Vue: Class<Component>) { // install runtime convenience helpers installRenderHelpers(Vue.prototype) Vue.prototype.$nextTick = function (fn: Function) { return nextTick(fn, this) } Vue.prototype._render = function (): VNode { const vm: Component = this const { render, _parentVnode } = vm.$options if (_parentVnode) { vm.$scopedSlots = normalizeScopedSlots( _parentVnode.data.scopedSlots, vm.$slots, vm.$scopedSlots ) } // set parent vnode. this allows render functions to have access // to the data on the placeholder node. vm.$vnode = _parentVnode // render self let vnode try { // There's no need to maintain a stack because all render fns are called // separately from one another. Nested component's render fns are called // when parent component is patched. currentRenderingInstance = vm vnode = render.call(vm._renderProxy, vm.$createElement) } catch (e) { handleError(e, vm, `render`) // return error render result, // or previous vnode to prevent render error causing blank component /* istanbul ignore else */ if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) { try { vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e) } catch (e) { handleError(e, vm, `renderError`) vnode = vm._vnode } } else { vnode = vm._vnode } } finally { currentRenderingInstance = null } // if the returned array contains only a single node, allow it if (Array.isArray(vnode) && vnode.length === 1) { vnode = vnode[0] } // return empty vnode in case the render function errored out if (!(vnode instanceof VNode)) { if (process.env.NODE_ENV !== '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 } }