Previous: Vue2.x source code - initialization: initMixin(Vue)
This article mainly looks at the four blends of stateMixin, eventsMixin, lifecycle mixin and renderMixin.
These methods are mainly used to mount some methods on the prototype of Vue instances:
1. stateMixin: data related $data, $props, $set, $del, $watch
2. eventsMixin: events $on, $off, $once, $emit
3. Lifecycle mixin: component and instance update_ Update, $forceUpdate, lifecycle $destroy
4. renderMixin: shorthand rendering tool function installRenderHelpers, component rendering $nextTick_ render
1, stateMixin(Vue)
In the src/core/instance/state.js file
export function stateMixin (Vue: Class<Component>) { //Use the get methods of dataDef and propsDef to obtain the data and props on the instance const dataDef = {} dataDef.get = function () { return this._data } const propsDef = {} propsDef.get = function () { return this._props } //Setting the set method in the development environment throws a warning message 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) } } //Responsively bind data and props to $data and $props, Object.defineProperty(Vue.prototype, '$data', dataDef) Object.defineProperty(Vue.prototype, '$props', propsDef) //Set $set $delete Vue.prototype.$set = set Vue.prototype.$delete = del 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() } } }
First, define dataDef and propsDef to set the get and set methods of response $data and $props; Then add $set, $delete, $watch methods to the instance;
Note: the instance root $data cannot be replaced; props is read-only and cannot be modified.
let obj = { a:1 } this.$data = obj ///This direct assignment to $data will cause a warning (the default copy of the object is a deep copy, that is, a reference)
watch
The watch method is mainly to create a watcher object, which involves the principle of response; For convenience, I'll take out the $watch code separately
//Set $watch Vue.prototype.$watch = function ( expOrFn: string | Function, //User manual monitoring cb: any, //Callback function after listening for changes options?: Object //parameter ): Function { const vm: Component = this //If cb is an object, execute createwatch, obtain the handler attribute in the cb object, and continue to recursively execute $watch until cb is not an object if (isPlainObject(cb)) { return createWatcher(vm, expOrFn, cb, options) } options = options || {} //It is marked as the user watcher, and is used to listen to the watch function when assigning a new value to data options.user = true //Add a watcher for expOrFn const watcher = new Watcher(vm, expOrFn, cb, options) //immediate==true execute cb callback immediately if (options.immediate) { try { cb.call(vm, watcher.value) } catch (error) { handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`) } } //Cancel observation function return function unwatchFn () { //Removes the current watcher from the subscriber list of all dependencies watcher.teardown() } } function createWatcher ( vm: Component, expOrFn: string | Function, handler: any, options?: Object ) { if (isPlainObject(handler)) { options = handler handler = handler.handler } if (typeof handler === 'string') { handler = vm[handler] } return vm.$watch(expOrFn, handler, options) }
createWatcher determines whether the handler is an object. If yes, mount the handler to options. If not, take out the handler attribute of the object; If the handler is a string, find the handler from the vm to assign a value; Finally, execute the $watch method on the instance;
The Watcher involved in watch will be described in detail later. I won't elaborate here.
set,del
The set and del methods are in the src/core/observer/index file
export function set (target: Array<any> | Object, key: any, val: any): any { //Development environment, undefined, original object if (process.env.NODE_ENV !== 'production' && (isUndef(target) || isPrimitive(target)) ) { warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`) } //Is target an array and key a valid array index if (Array.isArray(target) && isValidArrayIndex(key)) { //Compare key and target.length to reassign the array length target.length = Math.max(target.length, key) //Replace the value of the key position target.splice(key, 1, val) return val } //If the key already exists in the target or the key belongs to the original attribute of the Object, it is assigned directly if (key in target && !(key in Object.prototype)) { target[key] = val return val } //Avoid adding a responsiveness attribute to the Vue instance or its root $data at run time -- declare it in advance in the data option //Get the observe instance. You can refer to the observe instance object const ob = (target: any).__ob__ //isVue is true, indicating that target is a Vue instance //The vmCount attribute exists, indicating that it is root $data 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 } //Add as a responsive property and notify data updates defineReactive(ob.value, key, val) ob.dep.notify() return val }
set is used to add the val attribute value of the key attribute name to the target in response;
export function del (target: Array<any> | Object, key: any) { //Development environment, undefined, original object if (process.env.NODE_ENV !== 'production' && (isUndef(target) || isPrimitive(target)) ) { warn(`Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`) } //Is target an array and key a valid array index if (Array.isArray(target) && isValidArrayIndex(key)) { //Directly delete the value of the key position target.splice(key, 1) return } //Avoid deleting the attribute on the Vue instance, or set its root $data to null const ob = (target: any).__ob__ if (target._isVue || (ob && ob.vmCount)) { process.env.NODE_ENV !== 'production' && warn( 'Avoid deleting properties on a Vue instance or its root $data ' + '- just set it to null.' ) return } //If there is no key in the target, this attribute is returned directly if (!hasOwn(target, key)) { return } //delete attribute and notify data update delete target[key] if (!ob) { return } ob.dep.notify() }
The del method deletes the attribute of the key attribute name on the target object.
set and del methods cannot operate on properties on Vue instance objects and properties of root data objects ($data).
2, eventsMixin(Vue)
In the src/core/instance/events.js file
export function eventsMixin (Vue: Class<Component>) { const hookRE = /^hook:/ //In a publish subscribe relationship, one plays the role of subscription Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component { const vm: Component = this //The event is an array, and the $on method is called repeatedly until it is found that the event is not an array if (Array.isArray(event)) { for (let i = 0, l = event.length; i < l; i++) { vm.$on(event[i], fn) } } else { //Check under the constructor of vue_ Does the events object have a current event //If it does not exist, create an array. If it does exist, add the callback fn of the subscription to the array_ The current event property of events (vm._events[event] || (vm._events[event] = [])).push(fn) // Check whether the current event starts with hook:, if so, set the current vue_ The status of hasHookEvent is true if (hookRE.test(event)) { vm._hasHookEvent = true } } return vm } //It is used to listen to a custom event, but it is triggered only once. Remove the listener after the first trigger Vue.prototype.$once = function (event: string, fn: Function): Component { const vm: Component = this //An on method is provided internally, function on () { //Calling $off removes the event listener vm.$off(event, on) //this of fn points to vm fn.apply(vm, arguments) } //The passed in parameter FN is replaced by on in $on. When the user manually unloads, vm.$off('xxx',fn) cannot find the FN event. Here, FN is passed into on.fn; //$off will judge internally that if the fn attribute of an item in the callback function list is the same as fn, the event can be successfully removed on.fn = fn //Execute on method vm.$on(event, on) return vm } //Remove custom event listener Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component { const vm: Component = this // When no parameters are received, it means that all event listeners in the vue constructor should be unloaded if (!arguments.length) { vm._events = Object.create(null) return vm } // If the event is an array, $off is called in a loop if (Array.isArray(event)) { for (let i = 0, l = event.length; i < l; i++) { vm.$off(event[i], fn) } return vm } // If the current event does not exist, it will be returned directly const cbs = vm._events[event] if (!cbs) { return vm } //If there is no callback function, remove the current event directly if (!fn) { vm._events[event] = null return vm } // If there is a callback, check the current subscription array, delete the current callback function, and exit let cb let i = cbs.length while (i--) { cb = cbs[i] if (cb === fn || cb.fn === fn) { cbs.splice(i, 1) break } } return vm } //In a publish subscribe relationship, one acts as a publisher Vue.prototype.$emit = function (event: string): Component { const vm: Component = this if (process.env.NODE_ENV !== 'production') { const lowerCaseEvent = event.toLowerCase() //Check whether there is a problem with the naming of the event (v-on cannot be used to listen to hump events, and whether it already exists on the instance) 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 there is a callback for the current event to be published, publish the event to the subscribed event in turn 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 } } //src/core/util/error.js export function invokeWithErrorHandling ( handler: Function, context: any, args: null | any[], vm: any, info: string ) { let res try { //The event callback function is bound to the vm res = args ? handler.apply(context, args) : handler.call(context) if (res && !res._isVue && isPromise(res) && !res._handled) { res.catch(e => handleError(e, vm, info + ` (Promise/async)`)) // Avoid triggering capture multiple times during nested calls res._handled = true } } catch (e) { handleError(e, vm, info) } return res }
On events subscribe to events, emit triggers events, and off deletes events. Once is essentially an on event, but once can only be triggered once.
3, Lifecycle mixin (Vue)
In the src/core/instance/lifecycle.js file
export function lifecycleMixin (Vue: Class<Component>) { //_ The update method is used to update the component content and is also the entry of the patch Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) { const vm: Component = this //$el and $el of elements before cache update_ vnode const prevEl = vm.$el const prevVnode = vm._vnode //The current instance is set as active const restoreActiveInstance = setActiveInstance(vm) //Pass in the latest vnode vm._vnode = vnode // prevVnode does not exist, indicating that it is the first rendering // __ patch__ It is mainly to convert vnode into dom and render it in the view if (!prevVnode) { // First rendering vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */) } else { // Render again (update) vm.$el = vm.__patch__(prevVnode, vnode) } //Call the method in return to return the original active instance restoreActiveInstance() // Previous el additions_ vue_ Property and set to null if (prevEl) { prevEl.__vue__ = null } //__ vue__ Point to the Vue instance received when updating if (vm.$el) { vm.$el.__vue__ = vm } // If the parent $parent of the current instance is a high-level component, its $el is also updated if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) { vm.$parent.$el = vm.$el } } // $forceUpdate method, calling the instance's_ watcher, force the update once and trigger the updated life cycle Vue.prototype.$forceUpdate = function () { const vm: Component = this if (vm._watcher) { vm._watcher.update() } } //Completely destroy an instance. Clean up its connection with other instances and unbind all its instructions and event listeners //Trigger beforeDestroy and destroyed lifecycles Vue.prototype.$destroy = function () { const vm: Component = this //If already started, interrupt to avoid repeated destruction if (vm._isBeingDestroyed) { return } callHook(vm, 'beforeDestroy') vm._isBeingDestroyed = true // Removes the current object from the parent node const parent = vm.$parent if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) { remove(parent.$children, vm) } // Unloads the watcher object on the current instance if (vm._watcher) { vm._watcher.teardown() } let i = vm._watchers.length while (i--) { vm._watchers[i].teardown() } // Removing a reference from a frozen object may not have an observer if (vm._data.__ob__) { vm._data.__ob__.vmCount-- } vm._isDestroyed = true // Invokes the destroy hook on the currently rendered tree vm.__patch__(vm._vnode, null) callHook(vm, 'destroyed') // Close all instance listeners vm.$off() // Related properties are not set to null if (vm.$el) { vm.$el.__vue__ = null } if (vm.$vnode) { vm.$vnode.parent = null } } }
_ update mainly uses__ patch__ Method passes in different parameters to realize rendering and updating; The setActiveInstance method is used here, which is mainly used to cache the last instance object and return the current instance object__ patch__ Return to the previous instance object after completion, which is generally used for direct nesting of components.
4, renderMixin(Vue)
In the src/core/instance/render.js file
export function renderMixin (Vue: Class<Component>) { // Mount the shorthand rendering tool functions on the instance. These are runtime code installRenderHelpers(Vue.prototype) //Delay the callback until after the next DOM update cycle Vue.prototype.$nextTick = function (fn: Function) { return nextTick(fn, this) } //Execute the render function to generate vnode and exception handling Vue.prototype._render = function (): VNode { const vm: Component = this const { render, _parentVnode } = vm.$options //Create the vnode of the subcomponent and execute normalizeScopedSlots if (_parentVnode) { vm.$scopedSlots = normalizeScopedSlots( _parentVnode.data.scopedSlots, vm.$slots, vm.$scopedSlots ) } // Set parent vnode vm.$vnode = _parentVnode // render self let vnode try { currentRenderingInstance = vm // Execute the render function to generate vnode vnode = render.call(vm._renderProxy, vm.$createElement) } catch (e) { handleError(e, vm, `render`) // Error in render function // The development environment renders an error message, and the production environment returns the previous vnode to prevent rendering errors from leading to blank components 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 one node, convert it if (Array.isArray(vnode) && vnode.length === 1) { vnode = vnode[0] } // If the rendering function fails, an empty vnode is returned 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 node vnode.parent = _parentVnode return vnode } }
Execute the render function to generate vnodes. If the execution fails, an incorrect rendering result or previous vnodes will be returned to prevent rendering errors from causing blank components; If the render function does not generate vnode, an empty vnode is returned;
This is mainly responsible for the production of vnode, and then some exception handling methods.
The createElement method will be described in detail later, but there is no more description here.
installRenderHelpers
export function installRenderHelpers (target: any) { target._o = markOnce target._n = toNumber target._s = toString target._l = renderList target._t = renderSlot target._q = looseEqual target._i = looseIndexOf target._m = renderStatic target._f = resolveFilter target._k = checkKeyCodes target._b = bindObjectProps target._v = createTextVNode target._e = createEmptyVNode target._u = resolveScopedSlots target._g = bindObjectListeners target._d = bindDynamicKeys target._p = prependModifier }
Mount the short function name of common rendering tool functions on the Vue.prototype prototype.
nextTick
In the src/core/util/next-tick.js file
const callbacks = [] let pending = false export function nextTick (cb?: Function, ctx?: Object) { let _resolve // callback is an array defined above that stores the following methods callbacks.push(() => { // If the callback function exists, it is bound to cb.call(ctx) on the vue instance_ resolve(ctx) if (cb) { try { cb.call(ctx) } catch (e) { handleError(e, ctx, 'nextTick') } } else if (_resolve) { _resolve(ctx) } }) //padding status bit if (!pending) { pending = true timerFunc() } // If the callback function does not exist and the current environment supports Promise, a Promise object is returned and assigned to_ resolve if (!cb && typeof Promise !== 'undefined') { return new Promise(resolve => { _resolve = resolve }) } } let timerFunc //Does the current environment support promise if (typeof Promise !== 'undefined' && isNative(Promise)) { const p = Promise.resolve() timerFunc = () => { p.then(flushCallbacks) // Set an empty counter to force the refresh of the micro task queue if (isIOS) setTimeout(noop) } isUsingMicroTask = true } else if (!isIE && typeof MutationObserver !== 'undefined' && ( isNative(MutationObserver) || // PhantomJS and iOS 7.x MutationObserver.toString() === '[object MutationObserverConstructor]' )) { // Use MutationObserver where native Promise is not available 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 } else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { timerFunc = () => { setImmediate(flushCallbacks) } } else { timerFunc = () => { setTimeout(flushCallbacks, 0) } } // Empty the padding flag, cache the data in the copies, empty the callbacks array, and traverse and execute each method function flushCallbacks () { pending = false const copies = callbacks.slice(0) callbacks.length = 0 for (let i = 0; i < copies.length; i++) { copies[i]() } }
The callback function is cached in the callbacks array, then the timerFunc method is invoked. TimerFunc will invoke the flushCallbacks method according to different environments through different means to execute every cache in callbacks.
The core idea of nextTick is to process tasks through asynchronous methods; vue will give priority to use promise.then, MutationObserver and setImmediate according to the current environment. If they are not supported, set timeout will be used to delay the function until the DOM is updated. (the reason is that the macro task consumes more than the micro task, so the micro task is used first, and the macro task with the largest consumption is used last)