//entry-runtime-with-compiler.js const mount = Vue.prototype.$mount Vue.prototype. $mount = function (//After getting the simple version of the mount function, overwrite it el?: string | Element, hydrating?: boolean ): Component { El = el & & query (el)//Find Mount Point el
/* istanbul ignore if */ if (el = document.body || el = document.documentElement) {//el cannot be body or document, because the following logic will directly replace the label of Mount Point process.env.NODE_ENV ! 'production' && warn( `Do not mount Vue to <html> or <body> - mount to normal elements instead.` ) returnthis }
const options = this.$options // resolve template/el and convert to render function If (! options.render) {//Determine if a render function is declared Let template = options.template//if not find template If (template) {//If there is a template, process it if (typeof template = 'string') { if (template.charAt(0) = '#') { template = idToTemplate(template) /* istanbul ignore if */ if (process.env.NODE_ENV ! 'production' && !template) { warn( `Template element not found or is empty: ${options.template}`, this ) } } } elseif (template.nodeType) { template = template.innerHTML } else { if (process.env.NODE_ENV ! 'production') { warn('invalid template option:' + template, this) } returnthis } } elseif (el) {//if not, assign template to the tag where el is located template = getOuterHTML(el) } If (template) {//Determine whether there is a template, if there is, generate a render function according to the template, which is also unique to the runtime version /* istanbul ignore if */ if (process.env.NODE_ENV ! 'production' && config.performance && mark) { mark('compile') }
/* istanbul ignore if */ if (process.env.NODE_ENV ! 'production' && config.performance && mark) { mark('compile end') measure(`vue ${this._name} compile`, 'compile', 'compile end') } } } Return mount.call (this, el, hydrating)//finally call the simple version of the mount function, defined in platform/web/runtime/index }
This code first determines whether the render function, if there is no judgment whether there is a template, and then generate the render function according to the template. In short, the mount stage of vue only recognizes the render function.
Let’s take a look at the last call to the mount function.
1 2 3 4 5 6 7 8
// platform/web/runtime/index.js Vue.prototype. $mount = function (//mount function, it will be called after the initialization action of new Vue is completed el?: string | Element, hydrating?: boolean ): Component { el = el && inBrowser ? query(el) : undefined returnmountComponent(this, el, hydrating) }
This step is very simple, find the Mount Point el and execute the mountComponent function.
// core/instance/lifecycle.js ExportfunctionmountComponent (//called in vm. $mount function) vm: Component, el: ?Element, hydrating?: boolean ): Component { vm.$el = el if (!vm.$options.render) { Vm. $options.render = createEmptyVNode//Determine whether there is a render function if (process.env.NODE_ENV ! 'production') { /* istanbul ignore if */ if ((vm.$options.template && vm.$options.template.charAt(0) ! '#') || vm.$options.el || el) { warn( 'You are using the runtime-only build of Vue where the template ' + 'compiler is not available. Either pre-compile the templates into ' + 'render functions, or use the compiler-included build.', vm ) } else { warn( 'Failed to mount component: template or render function not defined.', vm ) } } } callHook(vm, 'beforeMount')
let updateComponent /* istanbul ignore if */ if (process.env.NODE_ENV ! 'production' && config.performance && mark) { //开发环境且启用了一些性能埋点的设置 updateComponent = () => { const name = vm._name const id = vm._uid const startTag = `vue-perf-start:${id}` const endTag = `vue-perf-end:${id}`
mark(startTag) vm._update(vnode, hydrating) mark(endTag) measure(`vue ${name} patch`, startTag, endTag) } } else { UpdateComponent = () => { // define the updateComponent function, using the _update declared above, the first parameter is the component returned by _render, _render defined in core/instance/render.js vm._update(vm._render(), hydrating) } }
// we set this to vm._watcher inside the watcher's constructor // since the watcher's initial patch may call $forceUpdate (e.g. inside child // component's mounted hook), which relies on vm._watcher being already defined NewWatcher (vm, updateComponent, noop, {//Create a watcher and pass in the updateComponent defined just now, this watcher is a rendering watcher before () { if (vm._isMounted && !vm._isDestroyed) { callHook(vm, 'beforeUpdate') } } }, true/* isRenderWatcher */) hydrating = false
// manually mounted instance, call mounted on self // mounted is called for render-created child components in its inserted hook if (vm.$vnodenull) { vm._isMounted = true callHook(vm, 'mounted') } return vm }
In this step, the rendering Watcher will be created, so let’s take a look at what a Watcher is?
/** * 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 } elseif (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 } } }
We can see that the get method will be called in the constructor function, and this method will first pushTarget, which is the last blog响应式原理As mentioned in, assign this to a global variable Dep.target, then call the passed updateComponent function, which will call the render function, and the data [keyUsed] used in the rendering process will trigger its getter, thus adding the Dep.target to the subs of data [keyUesd].
_render
Let’s go back and look at the _render function called in the updateComponent function
It is mounted to the Vue prototype by the initRender function
Vue.prototype._render = function (): VNode { constvm: Component = this const { render, _parentVnode } = vm.$options
if (_parentVnode) { vm.$scopedSlots = normalizeScopedSlots( _parentVnode.data.scopedSlots, vm.$slots ) }
// 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 { Vnode = render.call (vm._renderProxy, vm. $createElement)//call render function, the context points to _renderProxy, the value is defined in init.js, in production is this, in other environments if the browser supports Proxy is Proxy, $createElement is defined in core\ vdom\ create-element.js } 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 } } // 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 instanceofVNode )) { // A template cannot have two root nodes at the same time 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 }
Where does _renderProxy value come from?
This code is defined _init function, which means that the proxy in the prod environment is itself
1 2 3 4 5
if (process.env.NODE_ENV ! 'production') { InitProxy (vm)//initialize _renderProxy } else { vm._renderProxy = vm }
Then why do you need to set up a layer of proxy under non-prod, and what does this layer of proxy do?
This problem does not show the source code. Simply put, the function of this layer of proxy is to prompt some errors, so there is no need for this layer of proxy under prod.
// wrapper function for providing a more flexible interface // without getting yelled at by flow exportfunctioncreateElement ( context: Component, tag: any, data: any, children: any, normalizationType: any, alwaysNormalize: boolean ): VNode | Array<VNode> { If (Array.isArray (data) | | isPrimitive (data )) { // This is to determine whether the third parameter is an array or Primitive. If it is, it means that the third parameter is actually children, and the following parameters are sequentially moved forward normalizationType = children children = data data = undefined } if (isTrue(alwaysNormalize)) { normalizationType = ALWAYS_NORMALIZE } return_createElement(context, tag, data, children, normalizationType) }
exportfunction_createElement ( context: Component, tag?: string | Class<Component> | Function | Object, data?: VNodeData, children?: any, normalizationType?: number ): VNode | Array<VNode> { If (isDef (data) & & isDef ((data: any). _ _ ob __)) { // vnode cannot be reactive returncreateEmptyVNode() } // object syntax in v-bind if (isDef(data) && isDef(data.is)) { tag = data.is } if (!tag) { // in case of component :is set to falsy value returncreateEmptyVNode() } // warn against non-primitive key if (process.env.NODE_ENV ! 'production' && isDef(data) && isDef(data.key) && !isPrimitive(data.key) ) { if (!__WEEX__ || !('@binding'in data.key)) { warn( 'Avoid using non-primitive value as key, ' + 'use string/number value instead.', context ) } } // support single function children as default scoped slot if (Array.isArray(children) && typeof children[0] = 'function' ) { data = data || {} data.scopedSlots = { default: children[0] } children.length = 0 } /* The difference between normalizeChildren and simpleNormalizeChildren is that the former will recursion to normalize, while the latter will only traverse the first layer. The normalizationType here is determined by the source of the render function. If the render function is user-defined, it is ALWAYS_NORMALIZE If it is compiled through template or the like, it is SIMPLE_NORMALIZE After this step, all children will be stored in a one-dimensional array */ if (normalizationType = ALWAYS_NORMALIZE) { children = normalizeChildren(children) } elseif (normalizationType = SIMPLE_NORMALIZE) { children = simpleNormalizeChildren(children) } let vnode, ns if (typeof tag = 'string') { letCtor ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag) //Determine whether the label of the current node is a native html reserved label if (config.isReservedTag(tag)) { // platform built-in elements vnode = newVNode( config.parsePlatformTagName(tag), data, children, undefined, undefined, context ) } elseif ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) { // component vnode = createComponent(Ctor, data, context, children, tag) } else { // unknown or unlisted namespaced elements // check at runtime because it may get assigned a namespace when its // parent normalizes children vnode = newVNode( tag, data, children, undefined, undefined, context ) } } else { // direct component options / constructor vnode = createComponent(tag, data, context, children) } if (Array.isArray(vnode)) { return vnode } elseif (isDef(vnode)) { if (isDef(ns)) applyNS(vnode, ns) if (isDef(data)) registerDeepBindings(data) return vnode } else { returncreateEmptyVNode() } }
_update
Above by _render function can get a vnode, get this vnode after we can continue to execute the incoming watcher updateComponent function, that is
// core/instance/lifecycle.js Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) { constvm: 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 ) { // determine if it is the first render // initial render //The first parameter of the first render is a real dom node vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false/* removeOnly */) //_patch方法定义在platform/web/runtime/index.js中 } else { // updates If it is update, the first parameter is vnode 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. }
/* Here, the function currying technique is used to effectively avoid various if-else The first parameter is the various operations of the native dom, and the second defines the various hook functions during the update process. These hook functions are composed of baseModules combined with platform-dependent platformModules */ exportconstpatch: Function = createPatchFunction({ nodeOps, modules })
This createPatchFunction is very complex, defines a large number of auxiliary functions, and finally returns the patch function. Here only shows the analysis of the patch function, and other auxiliary functions briefly mention its use.
returnfunctionpatch (oldVnode, vnode, hydrating, removeOnly) { if (isUndef(vnode)) { if (isDef(oldVnode)) invokeDestroyHook(oldVnode) return }
let isInitialPatch = false const insertedVnodeQueue = []
if (isUndef(oldVnode)) { // empty mount (likely as component), create new root element isInitialPatch = true createElm(vnode, insertedVnodeQueue) } else { const isRealElement = isDef(oldVnode.nodeType) if (!isRealElement && sameVnode(oldVnode, vnode)) { // patch existing root node patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly) } else { if (isRealElement) { // mounting to a real element // check if this is server-rendered content and if we can perform // a successful hydration. if (oldVnode.nodeType = 1 && oldVnode.hasAttribute(SSR_ATTR)) { oldVnode.removeAttribute(SSR_ATTR) hydrating = true } if (isTrue(hydrating)) { if (hydrate(oldVnode, vnode, insertedVnodeQueue)) { invokeInsertHook(vnode, insertedVnodeQueue, true) return oldVnode } elseif (process.env.NODE_ENV ! 'production') { warn( 'The client-side rendered virtual DOM tree is not matching ' + 'server-rendered content. This is likely caused by incorrect ' + 'HTML markup, for example nesting block-level elements inside ' + '<p>, or missing <tbody>. Bailing hydration and performing ' + 'full client-side render.' ) } } // either not server-rendered, or hydration failed. // create an empty node and replace it oldVnode = emptyNodeAt(oldVnode) }
// create new node createElm( vnode, insertedVnodeQueue, // extremely rare edge case: do not insert if old element is in a // leaving transition. Only happens when combining transition + // keep-alive + HOCs. (#4590) oldElm._leaveCb ? null : parentElm, nodeOps.nextSibling(oldElm) )
// update parent placeholder node element, recursively if (isDef(vnode.parent)) { let ancestor = vnode.parent const patchable = isPatchable(vnode) while (ancestor) { for (let i = 0; i < cbs.destroy.length; ++i) { cbs.destroy[i](ancestor) } ancestor.elm = vnode.elm if (patchable) { for (let i = 0; i < cbs.create.length; ++i) { cbs.create[i](emptyNode, ancestor) } // #6513 // invoke insert hooks that may have been merged by create hooks. // e.g. for directives that uses the "inserted" hook. const insert = ancestor.data.hook.insert if (insert.merged) { // start at index 1 to avoid re-invoking component mounted hook for (let i = 1; i < insert.fns.length; i++) { insert.fns[i]() } } } else { registerRef(ancestor) } ancestor = ancestor.parent } }
// destroy old node if (isDef(parentElm)) { removeVnodes(parentElm, [oldVnode], 0, 0) } elseif (isDef(oldVnode.tag)) { invokeDestroyHook(oldVnode) } } }