Vue 的渲染过程:
上一篇看了一下编译,编译之后返回了 render
函数,那么 render
又是如何生成 vnode 的呢?本文来看一下。
在 Vue 官方文档中介绍 render
函数 的第一个参数是 createElement
用来创建 VNode;
<div id="app">
{{ message }}
</div>
用 render
函数表示:
render:function(createElement){
return createElement('div',{
attrs:{
id:'app'
}
}, this.message)
}
再来看看 Vue 初始化的时候绑定在实例原型上的 _render
方法,在 src/core/instance/render.js
文件里:
//执行render函数生成vnode,以及异常处理
Vue.prototype._render = function (): VNode {
const vm: Component = this
const { render, _parentVnode } = vm.$options
//创建子组件的 vnode 执行normalizeScopedSlots
if (_parentVnode) {
vm.$scopedSlots = normalizeScopedSlots(
_parentVnode.data.scopedSlots,
vm.$slots,
vm.$scopedSlots
)
}
// 设置父vnode
vm.$vnode = _parentVnode
// render self
let vnode
try {
currentRenderingInstance = vm
// 执行 render 函数,生成 vnode
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e) {
handleError(e, vm, `render`)
// render函数出错
// 开发环境渲染错误信息,生产环境返回之前的 vnode,以防止渲染错误导致组件空白
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 (Array.isArray(vnode) && vnode.length === 1) {
vnode = vnode[0]
}
// 如果渲染函数出错,返回空vnode
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()
}
// 设置父节点
vnode.parent = _parentVnode
return vnode
}
这段代码主要是 vnode = render.call(vm._renderProxy, vm.$createElement)
这段代码拿到 vnode,可以看到传入的参数是 vm.$createElement
方法,这个方法是在 initRender
方法里面传入的 createElement
方法;最后的结果:vnode 是由 createElement
方法返回的;
那么 vnode 到底是什么东西呢?
虚拟DOM(Virtual DOM)这个概念的产生是因为浏览器中频繁的对真实 DOM 进行操作是会产生性能问题的;虚拟 DOM 是用一个原生 js 对象去描述一个真实 DOM 节点;
由于直接操作 DOM 性能低,但是 js 层的操作效率高,可以将 DOM 操作转化成对象操作,最终通过 diff 算法比对差异进行更新 DOM (减少了对真实 DOM 的操作)。虚拟 DOM 不依赖真实平台环境从而也可以实现跨平台;
在 Vue.js 中,虚拟 DOM 是⽤ VNode 这么⼀个 Class 去描述,它是定义在 src/core/vdom/vnode.js
中的:
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 //当前节点数据
this.children = children //当前节点的子节点
this.text = text //当前节点文本
this.elm = elm //当前虚拟节点对应的真实节点
this.ns = undefined //当前节点的名字空间
this.context = context //当前节点的编译作用域
this.fnContext = undefined //用于功能节点的真实上下文vm
this.fnOptions = undefined //SSR缓存
this.fnScopeId = undefined //功能范围id支持
this.key = data && data.key //节点的key属性,被当作节点的标志,用以优化
this.componentOptions = componentOptions //组件的option选项
this.componentInstance = undefined //当前节点对应的组件的实例
this.parent = undefined //当前节点的父节点
this.raw = false //是否为原生HTML或只是普通文本
this.isStatic = false //是否是静态节点
this.isRootInsert = true //是否作为根节点插入
this.isComment = false //是否是注释节点
this.isCloned = false //是否是克隆节点
this.isOnce = false //是否是v-once指令
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
}
}
这里有很多的属性,其中最重要的属性只有几个:tag、data、children 和 key。其余很多属性只是在 Vue 中为适用不同的场景,额外添加的。
createElement 方法
在 src/core/vdom/create-element.js
文件里:
export function createElement (
context: Component,
tag: any,
data: any,
children: any,
normalizationType: any,
alwaysNormalize: boolean
): VNode | Array<VNode> {
//data是数组或者原始类型,说明当前参数没有传递data,所以需要将参数重载
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children
children = data
data = undefined
}
//确定参数使用哪一个 normalization,根据render初始化的时候传入的boolean值决定
if (isTrue(alwaysNormalize)) {
normalizationType = ALWAYS_NORMALIZE
}
return _createElement(context, tag, data, children, normalizationType)
}
对于没有传递 data 的情况这里多说一下,因为很容易误解:第三个参数 data 是可以不传的
//没有传递data
creatElement(this, 'div', 'hello', 1, false);
|
|
//重载后
creatElement(this, 'div', undefined, 'hello', 1, false);
将 data 设置为 undefined,其他的参数向后移动一位,这样形参和实参就能一 一对应起来了;
createElement
⽅法实际上是对 _createElement
⽅法的封装,它允许传⼊的参数更加灵活,在处理这些参数后,调⽤真正创建 VNode 的函数 _createElement
;
_createElement
函数的入参:
context:VNode 当前上下文环境;
tag:标签,可以是正常的HTML元素标签,也可以是Component组件;
data:VNode的数据,其类型为VNodeData;
children:VNode的子节点;
normalizationType:children子节点规范化类型;
具体代码:
export function _createElement (
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
//data是响应式数据,创建空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()
}
//检测data中是否有is属性,是的话tag替换为is指向的内容,处理动态组件
if (isDef(data) && isDef(data.is)) {
tag = data.is
}
// tag如果为空,创建空虚拟节点
if (!tag) {
return createEmptyVNode()
}
// data 中的key如果定义了必须是string、number、boolean以及 symbol
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
)
}
}
// 支持单函数子函数作为默认作用域槽
if (Array.isArray(children) &&
typeof children[0] === 'function'
) {
data = data || {}
data.scopedSlots = { default: children[0] }
children.length = 0
}
//处理children的两种模式
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children)
}
let vnode, ns
//tag是string
if (typeof tag === 'string') {
let Ctor
//获取tag的名字空间
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
//判断是否是保留的标签
if (config.isReservedTag(tag)) {
// tag 上有.native修饰符,发出警告 :v-on的.native修饰符只在组件上有效,但在<${tag}>上使用过
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))) {
//如果tag对应的是组件名,创建组件
vnode = createComponent(Ctor, data, context, children, tag)
} else {
//未知或未列出的名称空间元素在运行时进行检查,因为当其父元素将子元素规范化时,可能会给它分配一个名称空间
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
} else {
//当 render: h => h(App) 时传入的是组件则tag为对象 走此逻辑
// direct component options / constructor
vnode = createComponent(tag, data, context, children)
}
//vnode是数组直接返回;
if (Array.isArray(vnode)) {
return vnode
} else if (isDef(vnode)) {
//不是数组但是不为空,如果有名字空间,则递归所有子节点应用该名字空间,
if (isDef(ns)) applyNS(vnode, ns)
//当在槽节点上使用像:style和:class这样的深度绑定时,必须确保父节点重新呈现
if (isDef(data)) registerDeepBindings(data)
return vnode
} else {
//为空则创建一个空vnode;
return createEmptyVNode()
}
}
通过检测 data 中是否有 is 属性,是的话 tag 替换为 is 指向的内容,处理动态组件;
_createElement
主要做两件事情:规范化子节点和创建 VNode 节点,接下来我们围绕这两个方面来详细介绍。
1、规范化子节点
虚拟 DOM 是一个树形结构,每一个节点都应该是 VNode 类型,但是 children 参数又是任意类型的,所以如果有子节点,我们需要把它进行规范化成 VNode 类型,如果没有子节点,那么 children 就是 undefined。下面就看看子节点的规范,在 src/core/vdom/helpers/normalize-children.js
文件中:
1、simpleNormalizeChildren
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
}
simpleNormalizeChildren ⽅法调⽤场景是 render 函数当函数是编译⽣成的。理论上编译⽣成的 children 都已经是 VNode 类型的,但这⾥有⼀个例外,就是 functional component 函数式组件 返回的是⼀个数组⽽不是⼀个根节点,所以会通过 Array.prototype.concat ⽅法把整个 children 数组降维,让它的深度只有⼀层。
2、normalizeChildren
export function normalizeChildren (children: any): ?Array<VNode> {
return isPrimitive(children)
? [createTextVNode(children)]
: Array.isArray(children)
? normalizeArrayChildren(children)
: undefined
}
normalizeChildren
⽅法的调⽤场景有 2 种,⼀个场景是 render 函数是⽤户⼿写的,当 children 只有⼀个节点的时候,Vue.js 从接⼝层⾯允许⽤户把 children 写成基础类型⽤来创建单 个简单的⽂本节点,这种情况会调⽤ createTextVNode
创建⼀个⽂本节点的 VNode;另⼀个场景是 当编译 template、slot 、 v-for
的时候会产⽣嵌套数组的情况,会调⽤ normalizeArrayChildren
⽅法, 接下来看⼀下它的实现:
function normalizeArrayChildren (children: any, nestedIndex?: string): Array<VNode> {
const res = []
let i, c, lastIndex, last
//遍历children 获取单个节点判断是不是数组是则递归
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}`)
// 合并相邻文本节点
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)) {
// 合并相邻的两个text
res[lastIndex] = createTextVNode(last.text + c)
} else if (c !== '') {
// 将primitive转换为vnode
res.push(createTextVNode(c))
}
} else {
if (isTextNode(c) && isTextNode(last)) {
//合并相邻文本节点
res[lastIndex] = createTextVNode(last.text + c.text)
} else {
// 嵌套数组子数组的默认键(可能是由v-for生成的)
if (isTrue(children._isVList) &&
isDef(c.tag) &&
isUndef(c.key) &&
isDef(nestedIndex)) {
c.key = `__vlist${nestedIndex}_${i}__`
}
res.push(c)
}
}
}
return res
}
nestedIndex 表示嵌套索引,循环 children 节点,判断每一个子节点是不是数组类型,是则递归调用 normalizeArrayChildren
方法;如果是基础类型调用 createTextVNode
方法转换成 VNode 类型,然后推到数组中去;如果已经是 VNode 类型了直接推进数组即可;
在遍历过程中,如果存在连续的两个 text 节点,则会把他们合并成一个 text 节点;
2、创建 VNode 节点
回到 createElement
函数,规范化 children 后,接下来会去创建⼀个 VNode 的实例:
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)
}
这⾥先对 tag 做判断,如果是 string 类型,则接着判断如果是内置的⼀些节点,则直接创建⼀个 普通 VNode,如果是为已注册的组件名,则通过 createComponent
创建⼀个组件类型的 VNode,否 则创建⼀个未知的标签的 VNode。 如果是 tag ⼀个 Component 类型,则直接调⽤ createComponent
创建⼀个组件类型的 VNode 节点;
3、创建组件节点
这里创建组件类型的 VNode 节点 在 src/core/vdom/create-component.js
文件中:
export function createComponent (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
//判断节点tag不存在直接返回
if (isUndef(Ctor)) {
return
}
//创建子类构造器函数
const baseCtor = context.$options._base
// 普通选项对象:将其转换为构造函数
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}
// 如果在此阶段它不是构造函数或异步组件工厂,则拒绝。
if (typeof Ctor !== 'function') {
if (process.env.NODE_ENV !== 'production') {
warn(`Invalid Component definition: ${String(Ctor)}`, context)
}
return
}
// 异步组件
let asyncFactory
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor
Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
if (Ctor === undefined) {
// 返回async组件的占位符节点,该节点呈现为注释节点,但保留该节点的所有原始信息。这些信息将用于异步服务器渲染和水合。
return createAsyncPlaceholder(
asyncFactory,
data,
context,
children,
tag
)
}
}
data = data || {}
// 解析构造函数选项,以防在创建组件构造函数后应用全局mixin
resolveConstructorOptions(Ctor)
// 将组件v-model 数据转换为props和events
if (isDef(data.model)) {
transformModel(Ctor.options, data)
}
//提取props
const propsData = extractPropsFromVNodeData(data, Ctor, tag)
// 功能组件
if (isTrue(Ctor.options.functional)) {
return createFunctionalComponent(Ctor, propsData, data, context, children)
}
// 提取侦听器,因为需要将它们视为子组件侦听器而不是DOM侦听器
const listeners = data.on
// 用带有.native修饰符的监听器替换,以便在父组件补丁期间处理它。
data.on = data.nativeOn
if (isTrue(Ctor.options.abstract)) {
// 抽象组件只保留 props & listeners & slot
const slot = data.slot
data = {}
if (slot) {
data.slot = slot
}
}
// 将组件管理钩子安装到占位符节点上
installComponentHooks(data)
// return a placeholder vnode
const name = Ctor.options.name || tag
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
// Weex specific: invoke recycle-list optimized @render function for
// extracting cell-slot template.
// https://github.com/Hanks10100/weex-native-directive/tree/master/component
if (__WEEX__ && isRecyclableComponent(vnode)) {
return renderRecyclableComponentTemplate(vnode)
}
return vnode
}
总共分为三个步骤:构建子类构造器函数、安装组件钩子函数、实例化 vnode;
1、构建子类构造器函数
const baseCtor = context.$options._base
// 普通选项对象:将其转换为构造函数
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}
这里的 context 从入参追溯,是 Vue 实例;之前的 initGlobalAPI
方法中有这样一段代码 Vue.options._base = Vue
,在参数合并的时候用 mergeOptions
方法把 options 扩展到了 vm.$options
上,这样 baseCtor 就是 Vue 实例本身;baseCtor.extend(Ctor)
实际上是 Vue.extend(Ctor)
;关于 Vue.extend
可以参考:(Vue2.x 源码 - 初始化:全局API);将 Ctor 构建成子类构造器函数;
2、安装组件钩子函数
installComponentHooks(data)
function installComponentHooks (data: VNodeData) {
const hooks = data.hook || (data.hook = {})
//遍历自定义的hooks
for (let i = 0; i < hooksToMerge.length; i++) {
const key = hooksToMerge[i]
const existing = hooks[key]
const toMerge = componentVNodeHooks[key]
if (existing !== toMerge && !(existing && existing._merged)) {
hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge
}
}
}
const componentVNodeHooks = {
//初始化时触发
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
// kept-alive components, treat as a patch
const mountedNode: any = vnode // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode)
} else {
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
},
//patch之前触发
prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
const options = vnode.componentOptions
const child = vnode.componentInstance = oldVnode.componentInstance
updateChildComponent(
child,
options.propsData, // updated props
options.listeners, // updated listeners
vnode, // new parent vnode
options.children // new children
)
},
//插入到DOM时触发
insert (vnode: MountedComponentVNode) {
const { context, componentInstance } = vnode
if (!componentInstance._isMounted) {
componentInstance._isMounted = true
callHook(componentInstance, 'mounted')
}
if (vnode.data.keepAlive) {
if (context._isMounted) {
// vue-router#1212
// During updates, a kept-alive component's child components may
// change, so directly walking the tree here may call activated hooks
// on incorrect children. Instead we push them into a queue which will
// be processed after the whole patch process ended.
queueActivatedComponent(componentInstance)
} else {
activateChildComponent(componentInstance, true /* direct */)
}
}
},
//节点移除之前触发
destroy (vnode: MountedComponentVNode) {
const { componentInstance } = vnode
if (!componentInstance._isDestroyed) {
if (!vnode.data.keepAlive) {
componentInstance.$destroy()
} else {
deactivateChildComponent(componentInstance, true /* direct */)
}
}
}
}
const hooksToMerge = Object.keys(componentVNodeHooks)
在installComponentHooks
方法执行的时候,遍历componentVNodeHooks
方法里定义的 hooks 对象的属性,然后会把这些 hooks 合并到 data.hook
上面,方便后面使用;如果有相同的 hook,则会执行mergeHook
方法来合并,mergeHook
方法的定义如下:
function mergeHook (f1: any, f2: any): Function {
const merged = (a, b) => {
f1(a, b)
f2(a, b)
}
merged._merged = true
return merged
}
合并就是把 data.hook
里面的钩子函数在componentVNodeHooks
里面定义的钩子函数后面执行;
3、实例化 VNode
const name = Ctor.options.name || tag
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
通过 new VNode
实例化⼀个 vnode 并返回。
总结:render
函数主要是调用 createElement
方法包装的 _createElement
方法,该对子节点进行规范化之后创建生成 vnode, 然后会调用 vm._update
⽅法,执⾏ patch
函数,将 vnode 渲染到真实 DOM 节点上去;