Writing in the front
The article for reading notes to clone down Vue source code and debugging take ~~
Let’s take a look at how Vue goes from instance creation to DOM rendering:
For a basic rendering, the following analysis revolves around examples:
new Vue({
el: '#app'.data: {
msg: 'Hello Vue! ',}});Copy the code
So let’s see what new Vue() does.
import Vue from ‘vue’
When introducing Vue in a normal browser environment, SRC \platforms\web\ Runtime \index.js, SRC \platforms\web\entry-runtime-with-compiler.js, SRC \platforms\web\entry-runtime-with-compiler. They do some initialization of the Vue constructors and mount some static methods for specific environments.
In the first file, the initGlobalAPI method is called for the Vue constructor, which mounts some static methods and does some initialization for the Vue:
/* Execute */ when importing Vue
export function initGlobalAPI (Vue: GlobalAPI) {
// config
const configDef = {}
configDef.get = () = > config
if(process.env.NODE_ENV ! = ='production') {
configDef.set = () = > {
warn(
'Do not replace the Vue.config object, set individual fields instead.')}}Object.defineProperty(Vue, 'config', configDef)
// exposed util methods.
// NOTE: these are not considered part of the public API - avoid relying on
// them unless you are aware of the risk.
Vue.util = {
warn,
extend,
mergeOptions,
defineReactive
}
Vue.set = set
Vue.delete = del
Vue.nextTick = nextTick
// 2.6 explicit observable API
Vue.observable = <T>(obj: T): T= > {
observe(obj)
return obj
}
Vue.options = Object.create(null)
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)
initUse(Vue)
initMixin(Vue)
initExtend(Vue)
initAssetRegisters(Vue)
}
Copy the code
The second file mounts the __patch__ and $mount methods to Vue for all runtime environments:
// install platform patch function
// Assign patch to __patch__ in the Web environment
// Otherwise assign an empty function, since Vue does not have DOM objects when used on other platforms
// Patch is defined in SRC \platforms\web\ Runtime \patch.js
Vue.prototype.__patch__ = inBrowser ? patch : noop
// public mount method
// Public entry to the $mount mount method in different environments
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
) :Component {
// In the browser environment, get the DOM element corresponding to el
el = el && inBrowser ? query(el) : undefined
// Execute the real mount method
return mountComponent(this, el, hydrating)
}
Copy the code
The third file ADAPTS to the $mount method for web environments:
// Cache public $mount
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function () { / /... }
Copy the code
init
Vue’s constructor is defined in SRC \core\instance\index.js and is initialized before new is called. It initializes merge configuration, initializes lifecycle, initializes event center, initializes render, initializes Data, props, computed, watcher, and so on:
/* new Vue() from here */
function Vue (options) {
// The new call must be used
if(process.env.NODE_ENV ! = ='production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')}// Performs the initialization method, defined in initMixin
this._init(options)
}
// Mount the initializer _init and merge option to the vm.$option property
initMixin(Vue)
// block access to $data and $props, mount data operations on the prototype with $set, $delete, $watch methods
stateMixin(Vue)
// Mount event-related methods: $on, $once, $off, $emit
eventsMixin(Vue)
// Mount life-cycle methods: _update, $forceUpdate, $destroy
lifecycleMixin(Vue)
// Mount render methods: $nextTick, _render
renderMixin(Vue)
export default Vue
Copy the code
$mount
In initMixin, merge configuration and some initialization work is done, and finally $mount is called to enter the mount process:
// When we pass el, we actually call $mount() to start mounting
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
Copy the code
The $mount function generates the template and render functions, and finally calls the cached public mount function:
// Cache public $mount
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
) :Component {
// Query () is a wrapper around the element's DOM selector
// Returns the mounted root element (#app), or an empty div if none exists
el = el && query(el)
/* istanbul ignore if */
// Cannot be body or HTML
if (el === document.body || el === document.documentElement) { process.env.NODE_ENV ! = ='production' && warn(
`Do not mount Vue to <html> or <body> - mount to normal elements instead.`
)
return this
}
const options = this.$options
// Parse template/el and convert to render function
if(! options.render) {let template = options.template
// Whether template is used
if (template) {
if (typeof template === 'string') {
if (template.charAt(0) = = =The '#') {
// idToTemplate returns a string of child nodes to mount the root node
template = idToTemplate(template)
/* istanbul ignore if */
if(process.env.NODE_ENV ! = ='production' && !template) {
warn(
`Template element not found or is empty: ${options.template}`.this)}}}else if (template.nodeType) {
// If template is an element, take innerHTML directly
template = template.innerHTML
} else {
if(process.env.NODE_ENV ! = ='production') {
warn('invalid template option:' + template, this)}return this}}else if (el) {
// getOuterHTML returns the string of child nodes to mount the root node
template = getOuterHTML(el)
}
if (template) {
/* istanbul ignore if */
if(process.env.NODE_ENV ! = ='production' && config.performance && mark) {
mark('compile')}// Use template to generate the render function and mount it to option
const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV ! = ='production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
/* istanbul ignore if */
if(process.env.NODE_ENV ! = ='production' && config.performance && mark) {
mark('compile end')
measure(`vue The ${this._name} compile`.'compile'.'compile end')}}}// Finally, call the previously cached public $mount
return mount.call(this, el, hydrating)
}
Copy the code
The public mount ultimately calls the mountComponent method. In the previous example, we used _render to mount vnode and _update DOM:
vm._update(vm._render(), hydrating /* false */)
Copy the code
render
Let’s rewrite the example:
new Vue({
el: '#app'.data: {
msg: 'Hello'
},
render(createElement) {
return createElement('div', {
attrs: {
id: 'my-app'}},this.msg); }});Copy the code
_render is also a layer of cremation, which actually calls the render function:
// $createElement corresponds to our example, which is the first argument to the render function createElement
// _renderProxy can be thought of as a proxy for Vue instances, intercepting access to instance properties during rendering
vnode = render.call(vm._renderProxy, vm.$createElement)
Copy the code
$createElement is defined in initRender called in the initMixin method, which is divided into automatically compiled render calls and user manual calls:
// _c automatically compiles the createElement call to the render function
vm._c = (a, b, c, d) = > createElement(vm, a, b, c, d, false)
// The createElement call to the render function passed in by the user
vm.$createElement = (a, b, c, d) = > createElement(vm, a, b, c, d, true)
Copy the code
createElement
CreateElement provides a VNode node for the render function, returns a VNode object or array, and is really just a layer of currization of the real _createElement function to handle some arguments for it:
export function createElement (
context: Component,
tag: any, // Label name or component
data: any, // vNode data, such as attrs...
children: any, // Child node, tag, or array, which then needs to be normalized as a standard VNode array
normalizationType: any, // The type of the child node specification
alwaysNormalize: boolean
) :VNode | Array<VNode> {
if (Array.isArray(data) || isPrimitive(data)) {
// If the second argument is an array or a value of a common type
// it is directly children
/ / means we can use the createElement method like this: createElement method (' div ', this MSG | | [this MSG, 'hi'])
normalizationType = children
children = data
data = undefined
}
if (isTrue(alwaysNormalize)) {
// children represents the children of the current vNode, which can be of any type
// It next needs to be normalized as a standard VNode array
NormalizationType depends on whether the render function is automatically generated or manually called by the user
$createElement or vm._c
normalizationType = ALWAYS_NORMALIZE
}
// Actually call the _createElement method
return _createElement(context, tag, data, children, normalizationType)
}
Copy the code
The children specification essentially converts the children parameter into a VNode tree, and converts all nodes at all levels in children into VNode objects, including text nodes, comment nodes, etc.
Complete process for creating a VNode
export function _createElement (context: Component, tag? : string | Class<Component> |Function | Object.// Label name or componentdata? : VNodeData,// vNode data, such as attrs...children? : any,// Child node, tag, or array, which then needs to be normalized as a standard VNode arraynormalizationType? : number// The type of the child node specification
) :VNode | Array<VNode> {
// debugger
if (isDef(data) && isDef((data: any).__ob__)) {
// If reactive data is defined, a warning is issued and an empty vNode is returnedprocess.env.NODE_ENV ! = ='production' && warn(
`Avoid using observed data object as vnode data: The ${JSON.stringify(data)}\n` +
'Always create fresh vnode data objects in each render! ',
context
)
return createEmptyVNode()
}
// object syntax in v-bind
if (isDef(data) && isDef(data.is)) {
// According to the contents of the document
// <component v-bind:is="currentView"></component>
CreateElement (' Component ', {is: 'currentView'}, [...] )
// Indicates that a vNode is created by a component
tag = data.is
}
if(! tag) {// in case of component :is set to falsy value
// An empty vNode is returned because the tag attribute was not passed in correctly
return createEmptyVNode()
}
// 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
}
// normalizationType is already handled in the _createElement call
// Start normalizing the conversion of children
if (normalizationType === ALWAYS_NORMALIZE) {
// The user manually calls render
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
// Automatically compile render
children = simpleNormalizeChildren(children)
}
// Start creating a vnode
let vnode, ns
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
if (config.isReservedTag(tag)) {
// platform built-in elements
if(process.env.NODE_ENV ! = ='production' && isDef(data) && isDef(data.nativeOn)) {
warn(
`The .native modifier for v-on is only valid on components but it was used on <${tag}>. `,
context
)
}
// tag Creates a vNode instance directly if it is a built-in node (div, span, etc.)
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined.undefined, context
)
} else if((! data || ! data.pre) && isDef(Ctor = resolveAsset(context.$options,'components', tag))) {
// component
// tag Creates a vNode of component type if it is a registered component name
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
// Otherwise create a vnode with an unknown label
vnode = new VNode(
tag, data, children,
undefined.undefined, context
)
}
} else {
// direct component options / constructor
// Tag passes a component directly, creating a vNode of the component type
vnode = createComponent(tag, data, context, children)
}
if (Array.isArray(vnode)) {
return vnode
} else if (isDef(vnode)) {
if (isDef(ns)) applyNS(vnode, ns)
if (isDef(data)) registerDeepBindings(data)
return vnode
} else {
return createEmptyVNode()
}
}
Copy the code
_update (patch)
_update actually calls __patch__ to mount:
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
Copy the code
As mentioned earlier in import, patch is defined by platform, and the actual patch function is a function returned via the createPatchFunction closure:
export const patch: Function = createPatchFunction({ nodeOps, modules })
Copy the code
Patch flow for the example
Receiving parameters:
vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
Copy the code
Get parent node of el element
// The actual DOM element in vNode, #app
const oldElm = oldVnode.elm
// oldElm's parent, in this case
const parentElm = nodeOps.parentNode(oldElm)
Copy the code
Call createElm to convert the vNode generated by _render into a real DOM and insert it into the parent node of el:
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)
)
Copy the code
Delete old DOM elements from el:
if (isDef(parentElm)) {
Hello Vue!
// After insertion, the old root node is destroyed due to the render function
// Insert the DOM described by render (#my-app) and destroy the el template (# app)
removeVnodes([oldVnode], 0.0)}else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
Copy the code
CreateElm process
Receiving parameters:
createElm(
vnode,
insertedVnodeQueue,
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
Copy the code
Create root element
// Save the real DOM on the vnode.elm property
vnode.elm = vnode.ns // ns: namespace
? nodeOps.createElementNS(vnode.ns, tag)
// The key step is to call the encapsulated native createElement
CreateElement (tag); createElement(tag);
: nodeOps.createElement(tag, vnode)
Copy the code
Insert DOM into the page:
Call appendChid to insert the current DOM into the parent node
insert(parentElm, vnode.elm, refElm)
// If the current vNode has no tag attribute, it is a comment node or text node
if (isTrue(vnode.isComment)) {
vnode.elm = nodeOps.createComment(vnode.text)
// Create and insert a comment node
insert(parentElm, vnode.elm, refElm)
} else {
vnode.elm = nodeOps.createTextNode(vnode.text)
// Create and insert a text node
insert(parentElm, vnode.elm, refElm)
}
Copy the code
Before the DOM is inserted, createElm is called by the createChildren loop to generate the DOM for all the children of the current vNode and insert them into the parent of the current child:
// The vnode has already generated a real DOM, and needs to generate the vnode in its children as well
// createElm is called internally for vNode. children because children are vNodes
// If children is a text node, insert it directly into vnode.elm
createChildren(vnode, children, insertedVnodeQueue)
Copy the code
At this point the __Patch__ method call is complete, and _update(_render(), false) is done creating vNodes and patches.