Vue.js source code learning-data driven
For example, the following uses template string syntax to render the data as
< div > {{ message }} </div >
Copy code
var app = new Vue({
el : '#app' ,
data : {
message : 'Hello Vue!' ,
},
})
Copy code
Will eventually be rendered on the page
1. What happened to new Vue
Let's analyze first
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)
}
Copy code
You can know from the code
Look at the following piece of code, which is the end of the method.
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
Copy code
At the end of initialization, it is detected that if there is
2. the implementation of Vue instance mounting
Let's take a look at the source code first, in
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el)
/* istanbul ignore if */
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
//resolve template/el and convert to render function
if (!options.render) {
let template = options.template
if (template) {
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
)
}
}
} else if (template.nodeType) {
template = template.innerHTML
} else {
if (process.env.NODE_ENV !== 'production' ) {
warn( 'invalid template option:' + template, this )
}
return this
}
} else if (el) {
template = getOuterHTML(el)
}
if (template) {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark( 'compile' )
}
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 ${ this ._name} compile` , 'compile' , 'compile end' )
}
}
}
return mount.call( this , el, hydrating)
}
Copy code
This code first caches the
On the original prototype
//public mount method
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser? query(el): undefined
return mountComponent( this , el, hydrating)
}
Copy code
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
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)
const vnode = vm._render()
mark(endTag)
measure( `vue ${name} render` , startTag, endTag)
mark(startTag)
vm._update(vnode, hydrating)
mark(endTag)
measure( `vue ${name} patch` , startTag, endTag)
}
} else {
updateComponent = () => {
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 (eg inside child
//component's mounted hook), which relies on vm._watcher being already defined
new Watcher(
vm,
updateComponent,
noop,
{
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.$vnode == null ) {
vm._isMounted = true
callHook(vm, 'mounted' )
}
return vm
}
Copy code
The core is to define first
Set when the function is finally judged to be the root node
3. render
Let's take a look
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
}
Copy code
The most important thing is the call of the render method, as follows:
vnode = render.call (vm._renderProxy, vm. $ createElement) copying the code
In the code above
and so
4. createElement
//wrapper function for providing a more flexible interface
//without getting yelled at by flow
export function createElement (
context: Component,
tag: any,
data: any,
children: any,
normalizationType: any,
alwaysNormalize: boolean
): VNode | Array < VNode > {
if ( Array .isArray(data) || isPrimitive(data)) {
normalizationType = children
children = data
data = undefined
}
if (isTrue(alwaysNormalize)) {
normalizationType = ALWAYS_NORMALIZE
}
return _createElement(context, tag, data, children, normalizationType)
}
Copy code
export function _createElement (
context: Component,
tag?: string | Class<Component> | Function | Object ,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array < VNode > {
//...
//Normalization of children
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children)
}
//Creation of 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
)
}
vnode = new VNode(
config.parsePlatformTagName(tag),
data,
children,
undefined ,
undefined ,
context
)
} else if (
(!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 = new VNode(tag, data, children, undefined , undefined , context)
}
} else {
//direct component options/constructor
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 code
Actually,
4.1. Standardization of children
Every
export function simpleNormalizeChildren ( children: any ) {
for ( let i = 0 ; i <children.length; i++) {
if ( Array .isArray(children[i])) {
return Array .prototype.concat.apply([], children)
}
}
return children
}
export function normalizeChildren ( children: any ):? Array < VNode > {
return isPrimitive(children)
? [createTextVNode(children)]
: Array .isArray(children)
? normalizeArrayChildren(children)
: undefined
}
Copy code
function normalizeArrayChildren (
children: any,
nestedIndex?: string
): Array < VNode > {
const res = []
let i, c, lastIndex, last
for (i = 0 ; i <children.length; i++) {
c = children[i]
if (isUndef(c) || typeof c === 'boolean' ) continue
lastIndex = res.length- 1
last = res[lastIndex]
//nested
if ( Array .isArray(c)) {
if (c.length> 0 ) {
c = normalizeArrayChildren(c, ` ${nestedIndex || '' } _ ${i} ` )
//merge adjacent text nodes
if (isTextNode(c[ 0 ]) && isTextNode(last)) {
res[lastIndex] = createTextVNode(last.text + (c[ 0 ]: any).text)
c.shift()
}
res.push.apply(res, c)
}
} else if (isPrimitive(c)) {
if (isTextNode(last)) {
//merge adjacent text nodes
//this is necessary for SSR hydration because text nodes are
//essentially merged when rendered to HTML strings
res[lastIndex] = createTextVNode(last.text + c)
} else if (c !== '' ) {
//convert primitive to vnode
res.push(createTextVNode(c))
}
} else {
if (isTextNode(c) && isTextNode(last)) {
//merge adjacent text nodes
res[lastIndex] = createTextVNode(last.text + c.text)
} else {
//default key for nested array children (likely generated by v-for)
if (
isTrue(children._isVList) &&
isDef(c.tag) &&
isUndef(c.key) &&
isDef(nestedIndex)
) {
c.key = `__vlist ${nestedIndex} _ ${i} __`
}
res.push(c)
}
}
}
return res
}
Copy code
After right
4.2, the creation of 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
)
}
vnode = new VNode(
config.parsePlatformTagName(tag),
data,
children,
undefined ,
undefined ,
context
)
} else if (
(!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 = new VNode(tag, data, children, undefined , undefined , context)
}
} else {
//direct component options/constructor
vnode = createComponent(tag, data, context, children)
}
Copy code
Right here first
Well, here we know
5. update
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)
}
//...
}
Copy code
The core is to call
Vue.prototype .__ patch__ = inBrowser patch:? Noop copy the code
So in the browser rendering, it points to
Export const Patch: Function = createPatchFunction ({nodeOps, modules})
copy the code
The definition of the method is to call
export function createPatchFunction ( backend ) {
let i, j
const cbs = {}
const {modules, nodeOps} = backend
for (i = 0 ; i <hooks.length; ++i) {
cbs[hooks[i]] = []
for (j = 0 ; j <modules.length; ++j) {
if (isDef(modules[j][hooks[i]])) {
cbs[hooks[i]].push(modules[j][hooks[i]])
}
}
}
//...
return function patch ( 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
} else if (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)
}
//replacing existing element
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
//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.
//eg 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([oldVnode], 0 , 0 )
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
}
Copy code
Back to
The following uses an example to analyze its execution logic.
var app = new Vue({
el : '#app' ,
render : function ( createElement ) {
return createElement( 'div' , { attrs : { id : 'app' } }, this .message)
},
data : { message : 'Hello Vue!' },
})
Copy code
in
Initial the render//
vm. $ EL Patch __ .__ = vm (vm. $ EL, the vnode, Hydrating, false /* removeonly */ )
Copy the code
In execution
Then back to
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
} else if (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)
}
//replacing existing element
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
//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)
)
}
Copy code
Due to our incoming
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
if (isDef(vnode.elm) && isDef(ownerArray)) {
//This vnode was used in a previous render!
//now it's used as a new node, overwriting its elm would cause
//potential patch errors down the road when it's used as an insertion
//reference node. Instead, we clone the node on-demand before creating
//associated DOM element for it.
vnode = ownerArray[index] = cloneVNode(vnode)
}
vnode.isRootInsert = !nested //for transition enter check
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
const data = vnode.data
const children = vnode.children
const tag = vnode.tag
if (isDef(tag)) {
if (process.env.NODE_ENV !== 'production' ) {
if (data && data.pre) {
creatingElmInVPre++
}
if (isUnknownElement(vnode, creatingElmInVPre)) {
warn(
'Unknown custom element: <' +
tag +
'>-did you ' +
'register the component correctly? For recursive components,' +
'make sure to provide the "name" option.' ,
vnode.context
)
}
}
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
setScope(vnode)
/* istanbul ignore if */
if (__WEEX__) {
//...
} else {
createChildren(vnode, children, insertedVnodeQueue)
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
insert(parentElm, vnode.elm, refElm)
}
if (process.env.NODE_ENV !== 'production' && data && data.pre) {
creatingElmInVPre--
}
} else if (isTrue(vnode.isComment)) {
vnode.elm = nodeOps.createComment(vnode.text)
insert(parentElm, vnode.elm, refElm)
} else {
vnode.elm = nodeOps.createTextNode(vnode.text)
insert(parentElm, vnode.elm, refElm)
}
}
Copy code
vnode.elm = vnode.ns ? nodeOps.createElementNS(vnode.ns, tag) : nodeOps.createElement(tag, vnode) setScope(vnode) Copy code
Next call
createChildren(vnode, children, insertedVnodeQueue)
function createChildren ( vnode, children, insertedVnodeQueue ) {
if ( Array .isArray(children)) {
if (process.env.NODE_ENV !== 'production' ) {
checkDuplicateKeys(children)
}
for ( let i = 0 ; i <children.length; ++i) {
createElm(
children[i],
insertedVnodeQueue,
vnode.elm,
null ,
true ,
children,
i
)
}
} else if (isPrimitive(vnode.text)) {
nodeOps.appendChild(vnode.elm, nodeOps.createTextNode( String (vnode.text)))
}
}
Copy code
Then call
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
function invokeCreateHooks ( vnode, insertedVnodeQueue ) {
for ( let i = 0 ; i <cbs.create.length; ++i) {
cbs.create[i](emptyNode, vnode)
}
i = vnode.data.hook //Reuse variable
if (isDef(i)) {
if (isDef(i.create)) i.create(emptyNode, vnode)
if (isDef(i.insert)) insertedVnodeQueue.push(vnode )
}
}
Copy code
Last call
insert(parentElm, vnode.elm, refElm)
function insert ( parent, elm, ref ) {
if (isDef(parent)) {
if (isDef(ref)) {
if (nodeOps.parentNode(ref) === parent) {
nodeOps.insertBefore(parent, elm, ref)
}
} else {
nodeOps.appendChild(parent, elm)
}
}
}
Copy code
export function insertBefore (
parentNode: Node,
newNode: Node,
referenceNode: Node
) {
parentNode.insertBefore(newNode, referenceNode)
}
export function appendChild ( node: Node, child: Node ) {
node.appendChild(child)
}
Copy code
in
Back again
Finally, according to the previous recursion
6. summary
So far, how to render the template and data from the main line into the final
This article only analyzes the most basic and simplest scenarios. In practice, the page will be divided into many components, so the process of componentization will be analyzed in the next article.