目录
- 引言
- 1、从new Vue()入口开始:
- 2、this._init
- 3、挂载函数vm.$mount(vm.$options.el)
- 4、mountComponent函数
- 5、_render函数
- 6、createElement函数返回虚拟DOM
- 7、_update函数
- 8、patch函数
- 9、createElm函数
- 10、移除
引言
一般使用html,css和javascript直接将数据渲染到视图中,需要关心如何去操作dom,如果数据量比较大,那么操作过程将会繁琐且不好控制。vue将这些操作内置,开发过程中只需要关注数据的变化,视图的变化由vue框架内部实现。
将数据进行视图渲染的一个简单例子:
// html | |
<div id="app"> | |
{{ message }} | |
</div> | |
// js | |
var app = new Vue({ | |
el: '#app', | |
data: { | |
message: 'Hello Vue!' | |
}, | |
}) |
得到视图:
Hello Vue!
1、从new Vue()入口开始:
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) | |
} | |
initMixin(Vue) | |
stateMixin(Vue) | |
eventsMixin(Vue) | |
lifecycleMixin(Vue) | |
renderMixin(Vue) |
在实例化Vue的过程中,首先执行this._init方法;
2、this._init
this._init方法是在initMixin(Vue)时混入的
Vue.prototype._init = function (options?: Object) { | |
const vm: Component = this | |
// a uid | |
vm._uid = uid++ | |
let startTag, endTag | |
/* istanbul ignore if */ | |
if (process.env.NODE_ENV !== 'production' && config.performance && mark) { | |
startTag = `vue-perf-start:${vm._uid}` | |
endTag = `vue-perf-end:${vm._uid}` | |
mark(startTag) | |
} | |
// a flag to avoid this being observed | |
vm._isVue = true | |
// merge options | |
if (options && options._isComponent) { | |
// optimize internal component instantiation | |
// since dynamic options merging is pretty slow, and none of the | |
// internal component options needs special treatment. | |
initInternalComponent(vm, options) | |
} else { | |
vm.$options = mergeOptions( | |
resolveConstructorOptions(vm.constructor), | |
options || {}, | |
vm | |
) | |
} | |
/* istanbul ignore else */ | |
if (process.env.NODE_ENV !== 'production') { | |
initProxy(vm) | |
} else { | |
vm._renderProxy = vm | |
} | |
// expose real self | |
vm._self = vm | |
initLifecycle(vm) | |
initEvents(vm) | |
initRender(vm) | |
callHook(vm, 'beforeCreate') | |
initInjections(vm) // resolve injections before data/props | |
initState(vm) | |
initProvide(vm) // resolve provide after data/props | |
callHook(vm, 'created') | |
/* istanbul ignore if */ | |
if (process.env.NODE_ENV !== 'production' && config.performance && mark) { | |
vm._name = formatComponentName(vm, false) | |
mark(endTag) | |
measure(`vue ${vm._name} init`, startTag, endTag) | |
} | |
if (vm.$options.el) { | |
vm.$mount(vm.$options.el) | |
} | |
} |
其中进行生命周期、事件、render、数据等功能的初始化,最后执行了
vm.mount(vm.mount(vm.mount(vm.options.el)方法;
3、挂载函数vm.$mount(vm.$options.el)
该函数在entry-runtime-with-compiler.js函数中有定义
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) | |
} | |
function getOuterHTML (el: Element): string { | |
if (el.outerHTML) { | |
return el.outerHTML | |
} else { | |
const container = document.createElement('div') | |
container.appendChild(el.cloneNode(true)) | |
return container.innerHTML | |
} | |
} |
在没有render的条件下,当前存在template.nodeType,则通过template.innerHTML获得模板template,再通过compileToFunctions编译生成render函数。 运行结果如下:
function f() { | |
with (this) { | |
return _c("div", { attrs: { id: "app" } }, [_v(_s(message))]); | |
} | |
} |
获得render函数后,再执行缓存的mount,该方法在platforms/web/runtime/index.js中
Vue.prototype.$mount = function ( | |
el, | |
hydrating | |
) { | |
el = el && inBrowser ? query(el) : undefined; | |
return mountComponent(this, el, hydrating) | |
}; |
4、mountComponent函数
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 (e.g. 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 | |
} |
在当前函数中定义了updateComponent
函数,然后,通过new Watcher进行实例化将updateComponent
作为参数传入,在实例化的最后一步会执行回调函数然后执行updateComponent
,即执行vm._update(vm._render(), hydrating)
。
5、_render函数
该方法是在renderMixin(Vue)的时候混入的
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 | |
} |
其中vnode = render.call(vm._renderProxy, vm.$createElement)
函数以vm.$createElement
为参数执行render函数。以上例子是通过vue提供的编译方式编译生成的render函数:
function f() { | |
with (this) { | |
return _c("div", { attrs: { id: "app" } }, [_v(_s(message))]); | |
} | |
} |
也可以通过以下手写render的方式模拟
render: function (createElement) { | |
return createElement('div', { | |
attrs: { | |
id: 'app' | |
}, | |
}, this.message) | |
}, |
可以看出,在编译生成的例子中用到了_c
用来生成虚拟DOM,在手写render的例子中用vm.$createElement
生成虚拟DOM。_c
和vm.$createElement
都是在执行this._init的时候执行的initRender(vm)的时候挂载的。如下:
export function initRender (vm: Component) { | |
vm._vnode = null // the root of the child tree | |
vm._staticTrees = null // v-once cached trees | |
const options = vm.$options | |
const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree | |
const renderContext = parentVnode && parentVnode.context | |
vm.$slots = resolveSlots(options._renderChildren, renderContext) | |
vm.$scopedSlots = emptyObject | |
// bind the createElement fn to this instance | |
// so that we get proper render context inside it. | |
// args order: tag, data, children, normalizationType, alwaysNormalize | |
// internal version is used by render functions compiled from templates | |
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false) | |
// normalization is always applied for the public version, used in | |
// user-written render functions. | |
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true) | |
// $attrs & $listeners are exposed for easier HOC creation. | |
// they need to be reactive so that HOCs using them are always updated | |
const parentData = parentVnode && parentVnode.data | |
/* istanbul ignore else */ | |
if (process.env.NODE_ENV !== 'production') { | |
defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, () => { | |
!isUpdatingChildComponent && warn(`$attrs is readonly.`, vm) | |
}, true) | |
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, () => { | |
!isUpdatingChildComponent && warn(`$listeners is readonly.`, vm) | |
}, true) | |
} else { | |
defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true) | |
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true) | |
} | |
} |
其中都是调用了createElement方法,仅仅是最后一位参数不相同。
6、createElement函数返回虚拟DOM
// 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) | |
} |
是个包装函数,为了提供一个更加灵活的参数传入方式,最后执行_createElement
函数
export function _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__)) { | |
process.env.NODE_ENV !== 'production' && warn( | |
`Avoid using observed data object as vnode data: ${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)) { | |
tag = data.is | |
} | |
if (!tag) { | |
// in case of component :is set to falsy value | |
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 | |
} | |
if (normalizationType === ALWAYS_NORMALIZE) { | |
children = normalizeChildren(children) | |
} else if (normalizationType === SIMPLE_NORMALIZE) { | |
children = simpleNormalizeChildren(children) | |
} | |
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() | |
} | |
} |
在当前例子中,因为tab为'div',最后会执行到
vnode = new VNode( config.parsePlatformTagName(tag), data, children, undefined, undefined, context )
其中vnode是Vnode的实例,是用来描述当前节点的重要属性。
7、_update函数
该方法是在lifecycleMixin(Vue)的时候混入的
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) | |
} | |
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. | |
} |
如果有旧的虚拟dom,会执行vm.$el = vm.__patch__(prevVnode, vnode)
,这种场景是在数据发生变化的时候发生;
当前例子是首次渲染,所以会执行vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
,其中_patch函数在platforms/web/runtime/index.js中Vue.prototype.__patch__ = inBrowser ? patch : noop
。
8、patch函数
该方法在platforms/web/runtime/patch.js中
import * as nodeOps from 'web/runtime/node-ops' | |
import { createPatchFunction } from 'core/vdom/patch' | |
import baseModules from 'core/vdom/modules/index' | |
import platformModules from 'web/runtime/modules/index' | |
// the directive module should be applied last, after all | |
// built-in modules have been applied. | |
const modules = platformModules.concat(baseModules) | |
export const patch: Function = createPatchFunction({ nodeOps, modules }) |
将获取到nodeOps原生节点操作方法合集和模块操作方法作为createPatchFunction
的参数传入,该方法在core/vdom/patch.js中
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. | |
// 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([oldVnode], 0, 0) | |
} else if (isDef(oldVnode.tag)) { | |
invokeDestroyHook(oldVnode) | |
} | |
} | |
} | |
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch) | |
return vnode.elm | |
} | |
} |
在createPatchFunction
方法中,首先将modules
在hook
数组存在的方法合并成cbs对象,然后又定义了各种渲染需要的方法,最后,通过return function patch (oldVnode, vnode, hydrating, removeOnly)
返回真正需要执行的patch函数。其实在vue实例化执行过程中,只会进行一次Vue.prototype.__patch__ = inBrowser ? patch : noop
的求值,那么此时会通过闭包(函数科里化)的形式将modules, nodeOps和各种方法进行锁定,多次调用patch
也只会初始化一次,避免多次多用调用多次赋值。
通过条件判断,在当前例子中,会进入createElm
函数。
9、createElm函数
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__) { | |
// in Weex, the default insertion order is parent-first. | |
// List items can be optimized to use children-first insertion | |
// with append="tree". | |
const appendAsTree = isDef(data) && isTrue(data.appendAsTree) | |
if (!appendAsTree) { | |
if (isDef(data)) { | |
invokeCreateHooks(vnode, insertedVnodeQueue) | |
} | |
insert(parentElm, vnode.elm, refElm) | |
} | |
createChildren(vnode, children, insertedVnodeQueue) | |
if (appendAsTree) { | |
if (isDef(data)) { | |
invokeCreateHooks(vnode, insertedVnodeQueue) | |
} | |
insert(parentElm, vnode.elm, refElm) | |
} | |
} 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) | |
} | |
} |
在当前例子中,会执行createChildren(vnode, children, insertedVnodeQueue)
进行当前节点子元素的遍历,createChildren
中又递归的执行了createElm
函数,createElm
方法叶子节点顶端就是text文本,将执行:
vnode.elm = nodeOps.createTextNode(vnode.text); | |
insert(parentElm, vnode.elm, refElm); |
将生成的文本节点赋值给vnode.elm,并将其插入到父节点中,一层一层递归,子节点中的DOM节点始终将向父级插入,递归结束时,父节点将拥有一颗当前数据下的完整DOM树。 此时页面中的视图是:
{{message}}
Hello Vue!
还差最后一步,就是退出当前的createElm
函数回到patch
函数,执行最后的移除操作。
10、移除
// destroy old node | |
if (isDef(parentElm)) { | |
removeVnodes([oldVnode], 0, 0) | |
} else if (isDef(oldVnode.tag)) { | |
invokeDestroyHook(oldVnode) | |
} |
该方法将{{message}}从其父节点中移出。
小结:从数据到模板渲染,主要流程是将模板转换成render函数、将render函数转换成虚拟DOM以及将虚拟DOM进行视图渲染。