前言
初始化模板代码
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>Examples</title>
<meta name="description" content="">
<meta name="keywords" content="">
<link href="" rel="stylesheet">
</head>
<body>
<div id="app">{{ name }}</div>
</body>
</html>
new Vue({
el: '#app',
data () {
return {
name: 'rookie'
}
}
})
紧接着我们上一篇章的分析,我们继续对Vue初始化的过程进行探讨,点击Vue初始化过程(1)可查看上一篇章。
updateComponent
updateComponent = () => { // 函数定义
vm._update(vm._render(), hydrating)
}
可以看到updateComponent函数由vm._update跟vm._render两个核心函数所构成。我们先看下_render函数,代码如下,源码戳这里
_render
Vue.prototype._render = function (): VNode {
const vm: Component = this
const { render, _parentVnode } = vm.$options // 从$options中取出render函数
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
}
}
首先,会从$options取到render,这个render函数其实就是在Vue初始化过程中,在执行Vue原型上的$$mount方法时,生成的render函数,并挂载到options对象上,具体代码如下:
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
接着,会执行vnode = render.call(vm._renderProxy, vm.$createElement),render函数最终返回vnode对象,其中vm._renderProxy是在Vue执行_init方法的时候进行赋值的
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
initProxy(vm) // development环境执行
} else {
vm._renderProxy = vm
}
而initPorxy方法定义如下:
initProxy = function initProxy (vm) {
if (hasProxy) {
// determine which proxy handler to use
const options = vm.$options
const handlers = options.render && options.render._withStripped
? getHandler
: hasHandler
vm._renderProxy = new Proxy(vm, handlers)
} else {
vm._renderProxy = vm
}
}
其实就是判断是否支持原生Proxy,如果支持则通过Proxy代理的方式赋值给vm._renderProxy,不然就直接用vm实例赋值。其实proxy代理对vm上的data数据进行了一些拦截和处理操作。
$createElment
render.call(vm._rnederProxy, vm.createElement的参数,源码戳这里
// 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)
- vm._c: 从模板中编译出来的render函数会使用这个函数进行传参
- vm.$createElment: 用户手写的render会使用这个函数进行传参 下面这行代码是否很熟悉
new Vue({
el: '#app',
render: h => h(app)
})
这里的h变量其实就是vm.$createElment这个函数,h(app)其实就是调用了createElment函数,可以结合Vue官方文档进行分析,实现如下:
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)
}
首先会判断data是否是数据还是基础数据类型,如果是的话,则表明没有传入data,相应的会对children、nomalizationType进行重新赋值,相当于参数的重载,最后又执行了_createElment方法
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()
}
}
可以看到,函数首先会对传入的children参数进行规范化处理(normalizeChildren和simpleNormalizeChildren),定义如下,源码:
/* @flow */
import VNode, { createTextVNode } from 'core/vdom/vnode'
import { isFalse, isTrue, isDef, isUndef, isPrimitive } from 'shared/util'
// The template compiler attempts to minimize the need for normalization by
// statically analyzing the template at compile time.
//
// For plain HTML markup, normalization can be completely skipped because the
// generated render function is guaranteed to return Array<VNode>. There are
// two cases where extra normalization is needed:
// 1. When the children contains components - because a functional component
// may return an Array instead of a single root. In this case, just a simple
// normalization is needed - if any child is an Array, we flatten the whole
// thing with Array.prototype.concat. It is guaranteed to be only 1-level deep
// because functional components already normalize their own children.
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
}
// 2. When the children contains constructs that always generated nested Arrays,
// e.g. <template>, <slot>, v-for, or when the children is provided by user
// with hand-written render functions / JSX. In such cases a full normalization
// is needed to cater to all possible types of children values.
export function normalizeChildren (children: any): ?Array<VNode> {
return isPrimitive(children)
? [createTextVNode(children)]
: Array.isArray(children)
? normalizeArrayChildren(children)
: undefined
}
function isTextNode (node): boolean {
return isDef(node) && isDef(node.text) && isFalse(node.isComment)
}
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
}
- simpleNormalizeChildren: 只对children数组进行了一层的降维,不会递归去遍历子数组中的子数组
- normalizeChildren
- 首先会对children进行判断,如果children是基本类型则返回包含一个文本VNode的数组,如果children是一个数组则会调用normalizeArrayChildren
- 初始化一个res数组,用来存放最后的VNode Array的结果,c变量为每一个子元素VNode,lastIndex是记录res最大的索引值,last就是res的最后一个元素,c和last的关系就是last是c的前一个元素,c是当前遍历的元素
- 知道了last和c之间的关系之后,下面的分析就一目了然了。先遍历children中的每一个元素,取出当前遍历的子元素并赋值给c,并把lastIndex设置为数组的最大索引值,把数组中最后的一个元素赋值给last,接下来主要分一下几种判断条件:
- 如果c是一个数组,并且数组长度大于1,此时递归调用normalizeChildren函数,并把结果赋值给c。接下来,如果last和c数组中的第一个元素c[0]两个都是文本节点的话,则将两个节点进行合并赋值给res[lastIndex],然后将c[0]的值删除(这是Vue一些细节上的处理),最后将c插入到res的尾部。
- 如果c是一个基本数据类型,并且last是一个文本节点的话,则直接将last.text和c合并后的文本VNode赋值给res[lastIndex],如果last不是一个文本节点的话,则直接新创建一个文本VNode,并插入到res尾部。
- 不满足以上两个情况的话:
- 如果last和c都是文本节点,则将两个文本合并之后创建的文本VNode赋值给res[lastIndex]
- 如果c是v-for循环生成的节点,并满足tag、key、nestedIndex都存在的情况,则执行c.key =
__vlist${nestedIndex}_${i}__, 接着将c插入到res的尾部中。
children规范化之后,对于我们的初始化代码来讲,我们会走到下面这个逻辑
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( // 创建VNode
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
}
首先会判断是不是一个原生的保留标签,然后就进行new VNode(),生成一个VNode实例并且返回。VNode其实就是一个对真实DOM的描述,大家可以自行点击VNode源码进行了解。
VNode
export default class VNode {
tag: string | void;
data: VNodeData | void;
children: ?Array<VNode>;
text: string | void;
elm: Node | void;
ns: string | void;
context: Component | void; // rendered in this component's scope
key: string | number | void;
componentOptions: VNodeComponentOptions | void;
componentInstance: Component | void; // component instance
parent: VNode | void; // component placeholder node
// strictly internal
raw: boolean; // contains raw HTML? (server only)
isStatic: boolean; // hoisted static node
isRootInsert: boolean; // necessary for enter transition check
isComment: boolean; // empty comment placeholder?
isCloned: boolean; // is a cloned node?
isOnce: boolean; // is a v-once node?
asyncFactory: Function | void; // async component factory function
asyncMeta: Object | void;
isAsyncPlaceholder: boolean;
ssrContext: Object | void;
fnContext: Component | void; // real context vm for functional nodes
fnOptions: ?ComponentOptions; // for SSR caching
devtoolsMeta: ?Object; // used to store functional render context for devtools
fnScopeId: ?string; // functional scope id support
constructor (
tag?: string,
data?: VNodeData,
children?: ?Array<VNode>,
text?: string,
elm?: Node,
context?: Component,
componentOptions?: VNodeComponentOptions,
asyncFactory?: Function
) {
this.tag = tag // 标签名
this.data = data // 传入的数据对象 包含 class id on 等属性,具体可参考Vue官方render函数
this.children = children // VNode 数组
this.text = text // 文本
this.elm = elm // 真实DOM
this.ns = undefined
this.context = context
this.fnContext = undefined
this.fnOptions = undefined
this.fnScopeId = undefined
this.key = data && data.key // 标记VNode的唯一性,用于后续组件更新的diff算法
this.componentOptions = componentOptions // 组件选项
this.componentInstance = undefined // 组件实例
this.parent = undefined // VNode占位符
this.raw = false
this.isStatic = false
this.isRootInsert = true
this.isComment = false // 是否是注释节点
this.isCloned = false
this.isOnce = false
this.asyncFactory = asyncFactory
this.asyncMeta = undefined
this.isAsyncPlaceholder = false
}
// DEPRECATED: alias for componentInstance for backwards compat.
/* istanbul ignore next */
get child (): Component | void {
return this.componentInstance
}
}
定义一个VNode如下
{
tag: 'div',
data: {
id: 'a',
class: 'b'
},
children: [
{
tag: 'span',
text: 'lxb'
}
]
}
则生成下面的DOM
<div id="a" class="b">
<span>lxb</span>
</div>
总结
这篇文章分析完了_render函数的执行,通过createElment函数内部的一系列判断之后生成VNode,VNode是一个用来描述真实DOM的JS对象,之后我们会继续分析VNode是如何映射成真实的DOM进而渲染视图的。文中如有不足或有误之处,望指出,共勉!!