See other chapters:
Instance method (or property) and static method
stay Vue (own) project structure In this article, we studied the construction process of vue project itself and learned that import Vue from 'core/index' is the core code of introducing vue. The first two lines of this file correspond to the instance method and static method (or global method) of vue. This article will take a look at these two parts together with you.
// vuev2.5.20/src/core/index.js // Return the Vue constructor and prepare the instance method import Vue from './instance/index' // Static method (or global method) import { initGlobalAPI } from './global-api/index' ...
Tip: vuev2.5.20 is only the name of the project vue cloned locally. It will be this project by default.
Instance method (instance/index.js)
instance/index. The content of JS is not complex, and the total code is less than 30 lines:
// src/core/instance/index.js all codes: 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' // Constructor 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) } // Initialize correlation. Every time new Vue() is called, it will be executed, which contains many initialization operations initMixin(Vue) // Status related. More accurate data correlation stateMixin(Vue) // Event related eventsMixin(Vue) // Life cycle related lifecycleMixin(Vue) // Render related renderMixin(Vue) export default Vue
First define the Vue constructor, and then call five methods, such as initmixin (), statemixin (), to add methods (or properties) to the Vue prototype. The following is a code snippet of initMixin(), stateMixin():
export function initMixin (Vue: Class<Component>) { Vue.prototype._init = function (options?: Object) { ... } }
export function stateMixin (Vue: Class<Component>) { ... Object.defineProperty(Vue.prototype, '$data', dataDef) Object.defineProperty(Vue.prototype, '$props', propsDef) Vue.prototype.$set = set Vue.prototype.$delete = del Vue.prototype.$watch = function ( expOrFn: string | Function, cb: any, options?: Object ): Function { ... } }
The remaining three methods are similar and add methods to Vue's prototype.
The following are the methods or properties defined on Vue's prototype in each method:
- initMixin(Vue) - _init()
- stateMixin(Vue) - $data,$props,$set(),$delete(),$watch()
- eventsMixin(Vue) - $on(),$once(),$off(),$emit()
- lifecycleMixin(Vue) - _update(),$forceUpdate(),$destroy()
- renderMixin(Vue) - $nextTick(),_render()
Next, we will analyze the implementation one by one.
initMixin (initialization related)
This method only defines a prototype method for Vue, that is_ init(). Each call to new Vue() is executed.
function Vue (options) { ... this._init(options) }
If you have seen the source code of jQuery, there is a similar method, init.
var jQuery = function( selector, context ) { // Return jQuery object return new jQuery.fn.init( selector, context, rootjQuery ); } jQuery.fn = jQuery.prototype = { constructor: jQuery, init: function( selector, context, rootjQuery ) { // A lot of logical processing ... } ... }
New jQuery is called every time jQuery(jqOptions) is executed fn. Init() has a lot of processing, so we can pass a variety of different types of parameters to jqOptions.
Similarly, new Vue() supports multiple parameters, so this_ init() also does many things:
// Core code Vue.prototype._init = function (options?: Object) { const vm: Component = this // Merge parameters if (options && options._isComponent) { initInternalComponent(vm, options) } else { vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ) } // Initialize lifecycle, initialize Events initLifecycle(vm) initEvents(vm) initRender(vm) // Trigger life hook: beforeCreate callHook(vm, 'beforeCreate') // resolve injections before data/props // The order we can use is: inject - > data / props - > provide initInjections(vm) initState(vm) initProvide(vm) // resolve provide after data/props callHook(vm, 'created') if (vm.$options.el) { // mount vm.$mount(vm.$options.el) } }
_ The init() method will merge parameters in turn, and then initialize the life cycle and events. The life cycle hooks beforeCreate and created will be triggered in turn, and finally mounted according to the conditions.
stateMixin (data related)
The methods (or properties) defined for the prototype of Vue in stateMixin(Vue) are: $data, $props, $set(), $delete(), $watch().
vm.$data,vm.$props
vm.$data to return the data object observed by the Vue instance.
vm.$props: returns the props object received by the current component.
vm.$set(),vm.$delete() and VM$ watch()
$set, $delete and $watch are all data related. Please see the background and principle analysis Detect data changes - [vue api principle]
eventsMixin (event related)
The prototype methods defined in eventsMixin(Vue) are: $on, $once, $off, $emit. They are registration events, registration events triggered only once, dissolution events, and user-defined events.
vm.$on()
For these four methods, $on() should be the core. Only after registering the event can the event be unbound or triggered.
The usage is as follows:
vm.$on('test', function (msg) { console.log(msg) }) vm.$emit('test', 'hi') // => "hi"
The following is the source code of $on(). The core function is to "collect events and corresponding callbacks":
Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component { const vm: Component = this // If it is an array, traverse the array and register in turn if (Array.isArray(event)) { for (let i = 0, l = event.length; i < l; i++) { vm.$on(event[i], fn) } } else { // Store the callback into the array corresponding to event (vm._events[event] || (vm._events[event] = [])).push(fn) ... } return vm }
vm.$once()
$once(), register the event that is triggered only once.
If you have seen the jQuery source code (event system), you can also guess that $once should be implemented based on $on:
Vue.prototype.$once = function (event: string, fn: Function): Component { const vm: Component = this // Agent for callback: log off the event first, and then trigger it function on () { vm.$off(event, on) fn.apply(vm, arguments) } on.fn = fn // Implemented on the basis of $on. Set a proxy for the callback vm.$on(event, on) return vm }
vm.$off()
$off(), unbind the event.
The logic is relatively simple: it supports the case that there is no parameter passed, the parameter passed is an array, unbinding the specified event, and unbinding the callback of the specified event. See the source code:
Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component { const vm: Component = this // If no parameters are provided, all event listeners are removed if (!arguments.length) { // Object.create(null) creates an empty object whose prototype is null to empty the event vm._events = Object.create(null) return vm } // Array, unbind the events in turn if (Array.isArray(event)) { for (let i = 0, l = event.length; i < l; i++) { vm.$off(event[i], fn) } return vm } // specific event // Unbind the specified event const cbs = vm._events[event] // No callback, return directly if (!cbs) { return vm } // If the callback of the event to be unbound is not specified, all callbacks of the event will be cleared directly if (!fn) { vm._events[event] = null return vm } // Callback specified by unbound event let cb let i = cbs.length while (i--) { cb = cbs[i] if (cb === fn || cb.fn === fn) { cbs.splice(i, 1) break } } return vm }
vm.$emit()
$emit(), trigger the event on the current instance.
Get the callback array and trigger callbacks in turn. See the source code:
Vue.prototype.$emit = function (event: string): Component { const vm: Component = this ... // Get callback array 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}"` // Call callbacks in turn for (let i = 0, l = cbs.length; i < l; i++) { // This method really calls the callback, which contains some error handling invokeWithErrorHandling(cbs[i], vm, args, vm, info) } } return vm }
Lifecycle mixin (lifecycle related)
The prototype methods defined for Vue in lifecycle mixin (Vue) are:_ update(),$forceUpdate(),$destroy().
vm.$forceUpdate()
Force the Vue instance to re render. Note that it only affects the instance itself and the subcomponents inserted into the slot content, not all subcomponents.
Surprisingly few codes:
// Full code for $forceUpdate() Vue.prototype.$forceUpdate = function () { const vm: Component = this if (vm._watcher) { vm._watcher.update() } }
The key should be VM_ watcher. update()
Through global search, VM_ The watcher assignment is in the watcher JS constructor (line {1}):
// src/core/observer/watcher.js export default class Watcher { vm: Component; ... constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean ) { this.vm = vm if (isRenderWatcher) { vm._watcher = this // {1} }
So we know what vm is_ The Watcher is a Watcher. And according to Detect data changes - [basic implementation] In the study of Watcher, we wrote such a code:
class Watcher{ ... // Receive notification when data changes, and then notify the outside world update(newVal, oldVal){ this.callback(newVal, oldVal) } }
Calling update() will notify the outside world. The outside world here may be the vm, and then the vm will do something, including re rendering.
vm.$destroy()
$destroy(), completely destroy an instance. Clean up its connection with other instances and unbind all its instructions and event listeners. See the source code:
Vue.prototype.$destroy = function () { const vm: Component = this // Prevent repeated calls if (vm._isBeingDestroyed) { return } // Trigger hook: beforeDestroy callHook(vm, 'beforeDestroy') vm._isBeingDestroyed = true // remove self from parent // Remove yourself from the parent element const parent = vm.$parent // There is a parent element & the parent element has not started to be deleted & is it not abstract? if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) { remove(parent.$children, vm) } // teardown watchers if (vm._watcher) { // teardown, cancel the observation. That is, delete yourself from all DEPs vm._watcher.teardown() } // Unbind the user through VM$ Monitor added by watch. 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 has been destroyed vm._isDestroyed = true // invoke destroy hooks on current rendered tree // Invokes the destroy hook on the currently rendered tree vm.__patch__(vm._vnode, null) // Call hook: destroyed callHook(vm, 'destroyed') // Unbind all events vm.$off() // remove __vue__ reference if (vm.$el) { vm.$el.__vue__ = null } // release circular reference (#6759) // Release circular reference (#6759) if (vm.$vnode) { vm.$vnode.parent = null } }
Tip: vm._watchers adds elements to Watcher's constructor, and in VM$ There is new Watcher() in the watch method, so it is speculated that VM_ The content in watchers comes from VM$ watch.
export default class Watcher { vm: Component; ... constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean ) { this.vm = vm if (isRenderWatcher) { vm._watcher = this } vm._watchers.push(this) ... } }
Vue.prototype.$watch = function ( expOrFn: string | Function, cb: any, options?: Object ): Function { const vm: Component = this ... const watcher = new Watcher(vm, expOrFn, cb, options) ... }
renderMixin (render related)
The prototype methods defined for Vue in renderMixin(Vue) are: $nextTick()_ render().
vm.$nextTick()
$nextTick(), delaying the callback until after the next DOM update cycle
For example:
new Vue({ // ... methods: { // ... example: function () { // Modify data this.message = 'changed' // The DOM has not been updated this.$nextTick(function () { // The DOM is now updated // `this ` is bound to the current instance this.doSomethingElse() }) } } })
According to the usage, we can guess that $nextTick() will save our callback first, and then trigger it at an appropriate time. See the source code:
Vue.prototype.$nextTick = function (fn: Function) { return nextTick(fn, this) }
The code should be in nextTick():
// next-tick.js export function nextTick (cb?: Function, ctx?: Object) { let _resolve // Encapsulate the callback method into an anonymous function and store it in the callbacks array callbacks.push(() => { if (cb) { try { cb.call(ctx) } catch (e) { handleError(e, ctx, 'nextTick') } } else if (_resolve) { _resolve(ctx) } }) //It feels like there's a secret {1} if (!pending) { pending = true timerFunc() } ... }
In nextTick(), we first save our callback, encapsulate the callback method into an anonymous function, and then store it in the callbacks array.
When is the callback triggered? I feel that there is a secret in the line {1}. It turns out that I guessed right. Please see the source code:
// next-tick. The relevant codes in JS are as follows: // Store callback function const callbacks = [] let pending = false // Execute callback functions in turn function flushCallbacks () { pending = false // Backup array callbacks. // Prevent callbacks from being modified when traversing callbacks const copies = callbacks.slice(0) // Empty array callbacks callbacks.length = 0 // Execute callbacks in turn for (let i = 0; i < copies.length; i++) { copies[i]() } } // Here we have an asynchronous delay wrapper using micro tasks. let timerFunc if (typeof Promise !== 'undefined' && isNative(Promise)) { const p = Promise.resolve() timerFunc = () => { p.then(flushCallbacks) // ios compatible processing Promise if (isIOS) setTimeout(noop) } isUsingMicroTask = true } else if (!isIE && typeof MutationObserver !== 'undefined' && ( isNative(MutationObserver) || // PhantomJS and iOS 7.x MutationObserver.toString() === '[object MutationObserverConstructor]' )) { // MutationObserver is also a micro task let counter = 1 const observer = new MutationObserver(flushCallbacks) const textNode = document.createTextNode(String(counter)) observer.observe(textNode, { characterData: true }) timerFunc = () => { counter = (counter + 1) % 2 textNode.data = String(counter) } isUsingMicroTask = true // window.setImmediate, this feature is non-standard. Please try not to use it in the production environment! // ie support, similar to setTimeout(fn, 0) } else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { timerFunc = () => { setImmediate(flushCallbacks) } } else { // Go back to setTimeout. timerFunc = () => { setTimeout(flushCallbacks, 0) } }
timerFunc is a function that selects different asynchronous method packages according to conditions. There are four asynchronous methods, promise Then and MutationObserver are micro tasks, and the latter two (setImmediate and setTimeout) are macro tasks.
The first is taken as an example:
timerFunc = () => { p.then(flushCallbacks) // ios compatible processing Promise if (isIOS) setTimeout(noop) }
When we execute timerFunc(), we will immediately execute p.then(flushCallbacks). The flushCallbacks() method will execute the callback functions in turn, but the flushCallbacks() method will only be triggered at the appropriate time (i.e. in the event loop).
Finally, we conclude: execute this$ Nexttick (FN) will enter nextTick(). First, we will encapsulate our callback function with an anonymous function. After throwing this anonymous function into callbacks, because there is no to-do (let pending = false), we will execute timerFunc().
Tip: you can see the introduction of event loop, micro task and macro task here
Static method (Global API / index. JS)
global-api/index. The total code of JS file is about 70 lines. Except for 20 lines imported through import, the rest are in the initGlobalAPI() method:
export function initGlobalAPI (Vue: GlobalAPI) { // config const configDef = {} configDef.get = () => config ... Object.defineProperty(Vue, 'config', configDef) // Exposed util methods. Note: These are not considered part of the public API - avoid dependencies Vue.util = { ... } // vm.$set is Vue Alias for set. // delete is similar to nextTick Vue.set = set Vue.delete = del Vue.nextTick = nextTick // 2.6 explicit observable API // Make an object responsive Vue.observable = <T>(obj: T): T => { observe(obj) return obj } Vue.options = Object.create(null) // ASSET_TYPES = [ 'component', 'directive', 'filter' ] ASSET_TYPES.forEach(type => { Vue.options[type + 's'] = Object.create(null) }) // this is used to identify the "base" constructor to extend all plain-object // components with in Weex's multi-instance scenarios. Vue.options._base = Vue extend(Vue.options.components, builtInComponents) // A method is defined: Vue use initUse(Vue) // A method is defined: Vue mixin() initMixin(Vue) // Initialize inheritance. A method is defined: Vue extned() initExtend(Vue) // Initialize asset registration. Three methods are defined: Vue component(),Vue.directive() and Vue filter() initAssetRegisters(Vue) }
Then we analyze the sequence of source code in turn.
Vue.set(),Vue.delete() and Vue nextTick()
-
Vue.set() is VM$ Alias for set()
-
Vue.delete() is VM$ Alias for delete
-
Vue.nextTick() is VM$ Alias for nexttick()
Vue.observable()
Few codes:
Vue.observable = <T>(obj: T): T => { observe(obj) return obj }
Look directly at the official website:
Usage: make an object responsive. Vue uses it internally to handle the objects returned by the data function.
The returned objects can be used directly in rendering functions and calculation properties, and the corresponding updates will be triggered when changes occur. It can also be used as a minimized cross component state memory for simple scenarios:
const state = Vue.observable({ count: 0 }) const Demo = { render(h) { return h('button', { on: { click: () => { state.count++ }} }, `count is: ${state.count}`) } }
initUse
// src/core/global-api/use.js export function initUse (Vue: GlobalAPI) { Vue.use = function (plugin: Function | Object) { // According to the plug-in const installedPlugins = (this._installedPlugins || (this._installedPlugins = [])) // If this plug-in is already installed, return directly to Vue if (installedPlugins.indexOf(plugin) > -1) { return this } // additional parameters // Additional parameters const args = toArray(arguments, 1) // this inserts the first bit args.unshift(this) // If the install attribute of the plug-in is a function, execute install if (typeof plugin.install === 'function') { plugin.install.apply(plugin, args) // If the plugin itself is a function, execute the plugin } else if (typeof plugin === 'function') { plugin.apply(null, args) } // Put the plug-in into the list of installed plug-ins installedPlugins.push(plugin) return this } }
Vue.use()
Only Vue is defined in initUse(Vue) Use () is the global method. According to the previous source code analysis, the function of this method should be to install plug-ins. Let's check the api to confirm.
Usage: install Vue JS plug-in. If the plug-in is an object, you must provide the install method. If the plug-in is a function, it will be used as the install method. When the install method is called, Vue is passed in as a parameter.
Tip: what exactly is a plug-in? Official website: plug ins are usually used to add global functions to Vue. There are no strict restrictions on the functional scope of plug-ins - there are generally the following types:
- Add a global method or property. For example: Vue custom element
- Add global resources: instructions / filters / transitions, etc. Such as Vue touch
- Add some component options through global blending. Such as Vue router
- Add Vue instance methods by adding them to Vue Implemented on prototype.
- A library that provides its own API and one or more of the functions mentioned above. Such as Vue router
In this way, plug-ins are very useful.
Tip: similar to jQuery, jQuery also provides methods (such as jQuery.extend = jQuery.fn.extend = function() {}) to extend core functions.
initMixin
export function initMixin (Vue: GlobalAPI) { Vue.mixin = function (mixin: Object) { this.options = mergeOptions(this.options, mixin) return this } }
Vue.mixin()
Only one global method Vue is defined in initMixin(Vue) mixin.
mixin is a little similar Object.assign The smell of. Let's take a look at the introduction on the official website:
Usage: register a blend globally, affecting all Vue instances created after registration. Not recommended for use in application code.
We are looking at its usage (from the official website):
// Inject a processor for the custom option 'myOption'. Vue.mixin({ created: function () { var myOption = this.$options.myOption if (myOption) { console.log(myOption) } } }) new Vue({ myOption: 'hello!' }) // => "hello!"
According to the usage and source code, we can infer that Vue Mixin is to merge our parameters into Vue (this.options). These parameters will be reflected when creating Vue instances later.
initExtend
Vue.extend()
Only Vue is defined in initExtend(Vue) Extend () is the global method.
The name of the extend method reminds me of Object.assign ,jQuery.extend. Let's take a look at how the api describes:
Usage: create a "subclass" using the underlying Vue constructor. A parameter is an object that contains component options.
I seem to have guessed wrong. According to the usage introduction, Vue The function of extend is more like that of Class in es6 extends . Directly analyze its source code to verify our guess again:
Vue.extend = function (extendOptions: Object): Function { extendOptions = extendOptions || {} // Super points to Vue const Super = this const SuperId = Super.cid // Cache related {1} const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {}) if (cachedCtors[SuperId]) { return cachedCtors[SuperId] } const name = extendOptions.name || Super.options.name ... // Define a subclass Sub const Sub = function VueComponent (options) { this._init(options) } // Take the prototype of Vue as the prototype of the new object, and set the new object as the prototype of Sub // Objects created with new Sub() can also access Vue Methods or properties in prototype Sub.prototype = Object.create(Super.prototype) Sub.prototype.constructor = Sub Sub.cid = cid++ // Merge parameters Sub.options = mergeOptions( Super.options, extendOptions ) // New super attribute, pointing to Vue Sub['super'] = Super if (Sub.options.props) { initProps(Sub) } if (Sub.options.computed) { initComputed(Sub) } // Allow further extension / mixing / plug-in use Sub.extend = Super.extend Sub.mixin = Super.mixin Sub.use = Super.use ... // Keep references to super options when extending. Sub.superOptions = Super.options Sub.extendOptions = extendOptions Sub.sealedOptions = extend({}, Sub.options) // Cache constructor cachedCtors[SuperId] = Sub // Return subclass Sub return Sub }
Execute Vue Extend () returns a subclass Sub that inherits the parent (Vue). In order to improve performance, a caching mechanism (line {1}) is added.
initAssetRegisters
Three global methods are defined in initAssetRegisters(Vue).
export function initAssetRegisters (Vue: GlobalAPI) { /** * Create asset registration methods. * Create asset registration method. */ ASSET_TYPES.forEach(type => { Vue[type] = function ( id: string, definition: Function | Object ): Function | Object | void { ... } }) }
ASSET_TYPES is an array:
export const ASSET_TYPES = [ 'component', 'directive', 'filter' ]
Tip: if you read jQuery, you will also find that there is a similar writing method, that is, when the logic of multiple methods is similar, they can be written together and appear concise.
Vue.component(),Vue.directive() and Vue filter()
It's a little difficult to read the source code directly (comments can be read later):
ASSET_TYPES.forEach(type => { Vue[type] = function ( id: string, definition: Function | Object ): Function | Object | void { // The second parameter is not passed, indicating that it is obtained. For example, get instructions if (!definition) { return this.options[type + 's'][id] } else { // Special handling of components if (type === 'component' && isPlainObject(definition)) { definition.name = definition.name || id definition = this.options._base.extend(definition) } // Special handling of instructions if (type === 'directive' && typeof definition === 'function') { definition = { bind: definition, update: definition } } // Corresponding to acquisition, this should be registration. For example, register instructions, components, or filters this.options[type + 's'][id] = definition return definition } } })
Let's look at the corresponding api:
- Vue. Filter (ID, [definition]) - register or get the global filter.
// register Vue.filter('my-filter', function (value) { // Returns the processed value }) // getter to return the registered filter var myFilter = Vue.filter('my-filter')
-
Vue.directive() - register or get global directives.
-
Vue.component() - register or get global components
The core is to register or obtain. It feels that the function is relatively simple. It is a bit similar to registering events and obtaining callback of events. Now let's look at the source code and the corresponding comments, and we will find that it is much easier to understand.
Tip: of course, components, instructions and filters are not as simple as callback, so we can guess that this is only responsible for registration and acquisition, but not how to take effect.
Method of distributing elsewhere
vm.$mount()
If the Vue instance does not receive the el option when instantiated, it is in the "unmounted" state and has no associated DOM elements. You can use VM$ Mount () manually mounts an unmounted instance.
Search the project for $mount and find that there are three files with Vue defined prototype.$ mount =:
- platforms\web\runtime\index.js
- platforms\weex\runtime\index.js
- platforms\web\entry-runtime-with-compiler.js
The first two files are different definitions of the $mount method based on different platforms (web or weex).
The last file is interesting. Please see the source code:
// entry-runtime-with-compiler.js // Function hijacking. Save original $mount const mount = Vue.prototype.$mount // {1} // Encapsulate the original $mount. The main work is to compile the template into rendering function Vue.prototype.$mount = function ( // {2} el?: string | Element, hydrating?: boolean ): Component { el = el && query(el) ... const options = this.$options // resolve template/el and convert to render function // Parse template / el and convert to render function if (!options.render) { ... } return mount.call(this, el, hydrating) }
stay Vue (own) project structure In the article, we know entry - runtime - with - compiler The file generated by JS is the full version (dist/vue.js). The full version contains the compiler, and we Template In this article, we also know the main function of the compiler and compile the template into a rendering function.
Therefore, we can infer that this $mount (line {2}) is to add the function of the compiler to the original $mount (line {1}).
See other chapters: