Vue 数据驱动
Vue 数据驱动
数据驱动包括:
- 视图是由数据驱动生成的,对视图的修改,不会直接操作 DOM,而是通过修改数据。
- 数据更新驱动视图变化
首次渲染 DOM 概览
new Vue() 初始化
Vue 实际上是一个类,类在 Javascript 中是用 Function
来实现的。
Vue(options) : 实例化 Vue
// src\core\instance\index.ts
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'
import type { GlobalAPI } from 'types/global-api'
function Vue(options) {
if (__DEV__ && !(this instanceof Vue)) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
//@ts-expect-error Vue has function type
initMixin(Vue)
//@ts-expect-error Vue has function type
stateMixin(Vue)
//@ts-expect-error Vue has function type
eventsMixin(Vue)
//@ts-expect-error Vue has function type
lifecycleMixin(Vue)
//@ts-expect-error Vue has function type
renderMixin(Vue)
export default Vue as unknown as GlobalAPI
使用 new
关键字初始化 Vue 之后,会调用 this._init
方法,实际调用为挂载在 Vue 原型上的 _init
方法。
Vue.prototype._init
方法是在执行 initMixin(Vue)
方法是定义的。主要进行合并配置、初始化生命周期、初始化事件中心、初始化渲染、初始化 inject
、 初始化 props
/ methods
/ data
/ computed
/ watch
、初始化 provide
等。
在初始化之后,检测有 el
属性, 则调用 vm.$mount
方法挂载 vm
,将模板渲染成最终的 DOM 。
Vue.prototype._init(options) : 初始化 Vue
// src\core\instance\init.ts
export function initMixin(Vue: typeof Component) {
Vue.prototype._init = function (options?: Record<string, any>) {
const vm: Component = this
// a uid
vm._uid = uid++
let startTag, endTag
/* istanbul ignore if */
if (__DEV__ && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}
// a flag to mark this as a Vue instance without having to do instanceof
// check
vm._isVue = true
// avoid instances from being observed
vm.__v_skip = true
// effect scope
vm._scope = new EffectScope(true /* detached */)
vm._scope._vm = 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 as any)
} else {
// 合并配置
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor as any),
options || {},
vm
)
}
/* istanbul ignore else */
if (__DEV__) {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
initLifecycle(vm) // 初始化生命周期
initEvents(vm) // 初始化事件中心
initRender(vm) // 初始化渲染
callHook(vm, 'beforeCreate', undefined, false /* setContext */)
initInjections(vm) // 初始化 inject - resolve injections before data/props
initState(vm) // 初始化 props 、 methods 、 data 、 computed 、 watch
initProvide(vm) // 初始化 provide - resolve provide after data/props
callHook(vm, 'created')
/* istanbul ignore if */
if (__DEV__ && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${vm._name} init`, startTag, endTag)
}
// 如果有 el 属性, 则调用 vm.$mount 方法挂载 vm
// 目的是将模板渲染程最终的 DOM
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
$mount : Vue 实例挂载(Vue.prototype.$mount)
使用 new Vue(options)
进行初始化之后,检测有 el
属性, 则调用 vm.$mount
方法挂载 vm
Vue 在初始化过程中,会对挂载在原型上的 $mount
方法进行重写。首先使用 mount
缓存原型上的 $mount
方法,然后再重新定义该方法。
- 获取
el
属性对应的 DOM 节点,进行判断。Vue 不能挂载到<body>
、<html>
根节点上。 - 判断是否定义
render
方法。如果没有,则会把el
或者template
字符串转换成render
方法,进行编译。- 在 Vue 2.x 中,所有 Vue 组件的渲染都需要
render
方法,包括单文件组件(.vue
)、el
或者template
属性,最终都会转换成render
方法。 - 通过调用
compileToFunctions
方法实现编译成render
。
- 在 Vue 2.x 中,所有 Vue 组件的渲染都需要
- 完成编译之后,调用
mount
(即:缓存原型上的$mount
方法)
重写 Vue.prototype.$mount : Vue 实例挂载
// src\platforms\web\runtime-with-compiler.ts
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) {
__DEV__ &&
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 (__DEV__ && !template) {
warn(
`Template element not found or is empty: ${options.template}`,
this
)
}
}
} else if (template.nodeType) {
template = template.innerHTML
} else {
if (__DEV__) {
warn('invalid template option:' + template, this)
}
return this
}
} else if (el) {
// @ts-expect-error
template = getOuterHTML(el)
}
if (template) {
/* istanbul ignore if */
if (__DEV__ && config.performance && mark) {
mark('compile')
}
const { render, staticRenderFns } = compileToFunctions(
template,
{
outputSourceRange: __DEV__,
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
},
this
)
options.render = render
options.staticRenderFns = staticRenderFns
/* istanbul ignore if */
if (__DEV__ && config.performance && mark) {
mark('compile end')
measure(`vue ${this._name} compile`, 'compile', 'compile end')
}
}
}
return mount.call(this, el, hydrating)
}
公共 $mount
方法支持传入两个参数
el?: string | Element
: 表示挂载的元素。可以是字符串(在浏览器环境下,会调用query
方法转换为 DOM 对象),也可以是 DOM 对象。hydrating?: boolean
: 与服务器渲染有关。
公共 $mount
方法实际上会调用 mountComponent
方法
mountComponent
核心是实例化一个渲染Watcher
。在Watcher
回调函数中,调用updateComponent
方法。Watcher
主要作用如下:- 初始化时,执行回调函数
- 当
vm
实例中监测的数据发生变化时,执行回调函数
updateComponent
方法中,调用vm._render
方法生成 Virtual DOM ,最终调用vm._update
更新 DOM 。- 最后,判断为根节点时,设置
vm._isMounted = true
,表示该实例已挂载。同时,执行mounted
生命周期钩子函数。vm.$vnode == null
判断是否为根节点。vm.$vnode
为 Vue 实例的父级 Virtual DOM,vm.$vnode
为null
则表示根 Vue 实例 。
公共 Vue.prototype.$mount : Vue 实例挂载
// src\platforms\web\runtime\index.ts
// public mount method
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
// src\core\instance\lifecycle.ts
export function mountComponent(
vm: Component,
el: Element | null | undefined,
hydrating?: boolean
): Component {
vm.$el = el
if (!vm.$options.render) {
// @ts-expect-error invalid type
vm.$options.render = createEmptyVNode
if (__DEV__) {
/* 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 (__DEV__ && 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)
}
}
const watcherOptions: WatcherOptions = {
before() {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}
if (__DEV__) {
watcherOptions.onTrack = e => callHook(vm, 'renderTracked', [e])
watcherOptions.onTrigger = e => callHook(vm, 'renderTriggered', [e])
}
// 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,
watcherOptions,
true /* isRenderWatcher */
)
hydrating = false
// flush buffer for flush: "pre" watchers queued in setup()
const preWatchers = vm._preWatchers
if (preWatchers) {
for (let i = 0; i < preWatchers.length; i++) {
preWatchers[i].run()
}
}
// 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
}
_render : 渲染 Virtual DOM (Vue.prototype._render)
vm._render
Vue.prototype._render
是在执行 renderMixin
方法时定义的,是 Vue 实例的一个私有方法,主要用于将实例渲染成一个 Virtual DOM 。
在开发过程中,主要使用 template
模板,template
模板在 Vue 实例挂载($mount
)时,会被编译成 render
方法。
在 Vue API render
渲染函数中,接收一个 createElement
方法作为第一个参数用来创建 VNode 。其中,createElement
就是 Vue.prototype._render
函数中 vnode = render.call(vm._renderProxy, vm.$createElement)
中的 vm.$createElement
方法。
Vue.prototype._render : 将实例渲染成一个 Virtual DOM
// src\core\instance\render.ts
Vue.prototype._render = function (): VNode {
const vm: Component = this
const { render, _parentVnode } = vm.$options
if (_parentVnode && vm._isMounted) {
vm.$scopedSlots = normalizeScopedSlots(
vm.$parent!,
_parentVnode.data!.scopedSlots,
vm.$slots,
vm.$scopedSlots
)
if (vm._slotsProxy) {
syncSetupSlots(vm._slotsProxy, 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.
setCurrentInstance(vm)
currentRenderingInstance = vm
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e: any) {
handleError(e, vm, `render`)
// return error render result,
// or previous vnode to prevent render error causing blank component
/* istanbul ignore else */
if (__DEV__ && vm.$options.renderError) {
try {
vnode = vm.$options.renderError.call(
vm._renderProxy,
vm.$createElement,
e
)
} catch (e: any) {
handleError(e, vm, `renderError`)
vnode = vm._vnode
}
} else {
vnode = vm._vnode
}
} finally {
currentRenderingInstance = null
setCurrentInstance()
}
// if the returned array contains only a single node, allow it
if (isArray(vnode) && vnode.length === 1) {
vnode = vnode[0]
}
// return empty vnode in case the render function errored out
if (!(vnode instanceof VNode)) {
if (__DEV__ && 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
}
在执行 initRender
方法时,定义了 vm._c
和 vm.$createElement
,两者都调用了 createElement
方法。
vm._c
: 是将模板编译成的render
函数使用的vm.$createElement
: 是用户手写render
方法使用的
vm._c
与 vm.$createElement
两者都调用了 createElement
方法。
initRender : 初始化渲染,定义 vm.$createElement 用于编译成 render 函数
// src\core\instance\render.ts
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 as Component)
vm.$slots = resolveSlots(options._renderChildren, renderContext)
vm.$scopedSlots = parentVnode
? normalizeScopedSlots(
vm.$parent!,
parentVnode.data!.scopedSlots,
vm.$slots
)
: 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
// @ts-expect-error
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.
// @ts-expect-error
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 (__DEV__) {
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
)
}
}
Vue API - render
Vue API render
渲染函数,接收一个 createElement
方法作为第一个参数用来创建 VNode。
- 如果组件是一个函数组件,渲染函数还会接收一个额外的 context 参数,为没有实例的函数组件提供上下文信息。
- Vue 选项中的
render
函数若存在,则 Vue 构造函数不会从template
选项或通过el
选项指定的挂载元素中提取出的 HTML 模板编译渲染函数。
Vue API - render 示例
var getChildrenTextContent = function (children) {
return children
.map(function (node) {
return node.children ? getChildrenTextContent(node.children) : node.text
})
.join('')
}
Vue.component('anchored-heading', {
render: function (createElement) {
var headingId = getChildrenTextContent(this.$slots.default)
.toLowerCase()
.replace(/\W+/g, '-')
.replace(/(^-|-$)/g, '')
return createElement('h' + this.level, [
createElement(
'a',
{
attrs: {
name: headingId,
href: '#' + headingId,
},
},
this.$slots.default
),
])
},
props: {
level: {
type: Number,
required: true,
},
},
})
Virtual DOM
Virtual DOM 其实是一棵以 JavaScript 对象(VNode 节点)作为基础的树,用对象属性来描述节点,实际上它只是一层对真实 DOM 的抽象。最终可以通过一系列操作使这棵树映射到真实环境上。
在 Vue.js 中,Virtual DOM 是用 VNode Class 描述。实际上 Vue.js 中 Virtual DOM 是借鉴了一个开源库 snabbdom 的实现,然后加入了一些 Vue.js 特色的东西。
Virtual DOM 映射到真实的 DOM 实际上要经历 VNode 的 create
、 diff
、 patch
等过程。在 Vue.js 中,VNode 的 create
是通过 createElement
方法创建的。
Class VNode : 描述 Virtual DOM
// src\core\vdom\vnode.ts
export default class VNode {
tag?: string
data: VNodeData | undefined
children?: Array<VNode> | null
text?: string
elm: Node | undefined
ns?: string
context?: Component // rendered in this component's scope
key: string | number | undefined
componentOptions?: VNodeComponentOptions
componentInstance?: Component // component instance
parent: VNode | undefined | null // 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 // async component factory function
asyncMeta: Object | void
isAsyncPlaceholder: boolean
ssrContext?: Object | void
fnContext: Component | void // real context vm for functional nodes
fnOptions?: ComponentOptions | null // for SSR caching
devtoolsMeta?: Object | null // used to store functional render context for devtools
fnScopeId?: string | null // functional scope id support
isComponentRootElement?: boolean | null // for SSR directives
constructor(
tag?: string,
data?: VNodeData,
children?: Array<VNode> | null,
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
this.fnOptions = undefined
this.fnScopeId = undefined
this.key = data && data.key
this.componentOptions = componentOptions
this.componentInstance = undefined
this.parent = undefined
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
}
}
createElement : 创建 Virtual DOM
createElement
是在调用 Vue.prototype._render
时,执行 render.call(vm._renderProxy, vm.$createElement)
,通过 vm.$createElement
调用执行。
vm.$createElement
是在执行 initRender
方法中定义的,实际调用 createElement
方法。
// normalization is always applied for the public version, used in
// user-written render functions.
// @ts-expect-error
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
其中,createElement
方法实际上是对 _createElement
方法的封装,它允许传入的参数更加灵活,在处理这些参数后,调用真正创建 VNode 的函数 _createElement
createElement: 对 _createElement 方法的封装,创建 VNode
// src\core\vdom\create-element.ts
// 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 (isArray(data) || isPrimitive(data)) {
normalizationType = children
children = data
data = undefined
}
if (isTrue(alwaysNormalize)) {
normalizationType = ALWAYS_NORMALIZE
}
return _createElement(context, tag, data, children, normalizationType)
}
_createElement(context, tag, data, children, normalizationType)
方法参数
context: Component
: 表示 VNode 的上下文环境tag?: string | Component | Function | Object
: 表示标签data?: VNodeData
: 表示 VNode 的数据children?: any
: 表示当前 VNode 的子节点normalizationType?: number
: 表示子节点规范的类型。类型不同,调用规范方法也不同,主要是参考render
函数是编译生成的还是用户手写的。
_createElement : 创建 VNode
// src\core\vdom\create-element.ts
export function _createElement(
context: Component,
tag?: string | Component | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
if (isDef(data) && isDef((data as any).__ob__)) {
__DEV__ &&
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 (__DEV__ && isDef(data) && isDef(data.key) && !isPrimitive(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 (isArray(children) && isFunction(children[0])) {
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 (
__DEV__ &&
isDef(data) &&
isDef(data.nativeOn) &&
data.tag !== 'component'
) {
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 as any, data, context, children)
}
if (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 规范化
根据 normalizationType
的不同,会调用 normalizeChildren(children)
或 simpleNormalizeChildren(children)
方法,将 children
规范为 VNode 类型
simpleNormalizeChildren
函数,调用场景为通过编译生成的render
函数。理论上,通过编译生成的children
都已经是 VNode 类型,但是,通过function component
函数式组件返回的是数组而不是一个根节点,所以需要通过Array.prototype.concat
将整个children
数组扁平化,使其深度只有一层。normalizeChildren
函数,调用场景如下:用户手写的
render
函数 : 当children
只有一个节点的时候, Vue.js 从接口层面允许用户把children
写成基础类型,用来创建单个简单的文本节点,这种情况只会调用createTextVNode
创建一个文本节点的 VNode。编译
slot
、v-for
时,产生嵌套数组的情况 : 调用normalizeArrayChildren(children, nestedIndex)
方法遍历children
,获取单个节点c
后,对c
的类型进行判断。- 如果是基础类型,则通过
createTextVNode(children)
方法转换成 VNode 类型。 - 如果是数组类型,则递归调用
normalizeArrayChildren(children, nestedIndex)
方法。如果children
是一个列表并且列表还存在嵌套的情况,则根据nestedIndex
去更新它的key
。 - 否则,已经是 VNode 类型。
normalizeArrayChildren(children, nestedIndex)
方法接收 2 个参数:children
: 表示要规范的子节点。nestedIndex
: 表示嵌套的索引。
- 如果是基础类型,则通过
simpleNormalizeChildren 方法 、 normalizeChildren 方法
// src\core\vdom\helpers\normalize-children.ts
// 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 (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> | undefined {
return isPrimitive(children)
? [createTextVNode(children)]
: isArray(children)
? normalizeArrayChildren(children)
: undefined
}
function normalizeArrayChildren(
children: any,
nestedIndex?: string
): Array<VNode> {
const res: VNode[] = []
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 (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].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
}
创建 VNode
通过调用 normalizeChildren(children)
或 simpleNormalizeChildren(children)
方法,将 children
规范为 VNode 类型后,会创建一个 VNode 实例。对 tag
进行判断,进行相关逻辑处理:
tag
为string
类型- 如果是内置的一些节点,则直接创建一个普通 VNode
- 如果是为已注册的组件名,则通过
createComponent
创建一个组件类型的 VNode - 否则,创建一个未知的标签的 VNode
tag
为一个Component
类型,直接调用createComponent
创建一个组件类型的 VNode 节点
_update : 将 VNode 渲染真实 DOM (Vue.prototype._update)
Vue.prototype._update
是在执行 lifecycleMixin
方法时定义的,是 Vue 实例的一个私有方法,主要用于将 Virtual DOM 渲染真实 DOM 。
Vue.prototype._update
的核心就是调用 vm.__patch__
方法。主要在首次渲染与数据更新的时候进行调用。
Vue.prototype._update
// src\core\instance\lifecycle.ts
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.
}
Vue.prototype.__patch__
是在 Vue 初始化的过程中定义的。该方法在不同平台上,定义也不同。
- 在服务端渲染中,无真实的浏览器 DOM 环境,则不需要将 VNode 转换为 DOM,则为
noop
(空函数)。 - 在浏览器端渲染中,则指向
patch
,调用createPatchFunction
方法的返回值(patch
方法)。
// src\platforms\web\runtime\index.ts
import { patch } from './patch'
Vue.prototype.__patch__ = inBrowser ? patch : noop
// ========================================
// ========================================
// src\platforms\web\runtime\patch.ts
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 })
createPatchFunction(backend)
内部定义了一系列的辅助方法,最终返回了一个 patch
方法,该方法赋值给了 vm._update
函数里调用的 vm.__patch__
。
createPatchFunction(backend)
方法传入一个对象,包含 nodeOps
参数和 modules
参数。
nodeOps
: 封装了一系列 “平台 DOM” 操作的方法modules
: 定义了 “平台” 的一些模块钩子函数的实现
patch
方法与平台相关,在 Web 和 Weex 环境中,将虚拟 DOM 映射到 “平台 DOM” 的方法是不同的,并且对 “DOM” 包括的属性模块创建和更新也不相同,因此,每个平台都有对应的 nodeOps
和 modules
。
patch(oldVnode, vnode, hydrating, removeOnly)
:
oldVnode
: 表示旧的 VNode 节点,也可以不存在或者是一个 DOM 对象vnode
: 表示执行_render
后返回的 VNode 的节点hydrating
: 表示是否是服务端渲染removeOnly
: 供transition-group
使用
createPatchFunction
// src\core\vdom\patch.ts
export function createPatchFunction(backend) {
let i, j
const cbs: any = {}
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: any[] = []
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 (__DEV__) {
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
}
}
首次渲染场景
var app = new Vue({
el: '#app',
render: function (createElement) {
return createElement('div', {
attrs: {
id: 'app'
},
}, this.message)
},
data: {
message: 'Hello Vue!'
}
})
在首次渲染时,执行 Vue.prototype._update
方法,调用 vm.__patch__
方法如下:
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
vm.$el
: 对应示例中,id
为app
的 DOM 对象。相关于 Vue 项目中 index.html 模板中写的<div id="app">
。vm.$el
赋值在公共$mount
方法执行时调用mountComponent
中处理的vnode
: 对应的是调用vm._render()
函数的返回值hydrating
: 在非服务端渲染情况下为false
removeOnly
为false
传入 patch(oldVnode, vnode, hydrating, removeOnly)
方法的 oldVnode
实际上是一个 DOM container 。则,在patch
方法中 isRealElement
为 true
,并通过 emptyNodeAt
方法把 oldVnode
转换成 VNode 对象,再调用 createElm
方法 。
createElm
: 通过虚拟节点创建真实的 DOM 并插入到它的父节点中。主要实现逻辑如下:
- 调用
createComponent
方法,创建子组件。在首次渲染时,返回为false
。 - 判断
vnode
是否包含tag
。- 包含,则对
tag
的合法性在非生产环境下进行校验,校验是否为一个合法标签。 - 不包含,则可能是一个注释或者纯文本节点,可以直接插入父元素中。
- 包含,则对
- 再调用 “平台 DOM” 的操作创建一个占位符元素。
- 再调用
createChildren
方法创建子元素:遍历子虚拟节点,递归调用createElm
。(在遍历过程中会把vnode.elm
作为父容器的 DOM 节点占位符传入。) - 再调用
invokeCreateHooks
方法执行所有的create
钩子,并把 vnode push 到insertedVnodeQueue
(插入顺序队列) 中。 - 再调用
insert
方法把 DOM 插入到父节点中,因为是递归调用,子元素会优先调用insert
方法。所以,整个vnode
树节点插入顺序是 先子后父 。insert
方法调用nodeOps
(封装了一系列 “平台 DOM” 操作的方法) 把子节点插入到父节点中。其实是调用原生 DOM 的 API 进行 DOM 操作。
patch 方法中 createElm 辅助方法
function createElm(
vnode,
insertedVnodeQueue,
parentElm?: any,
refElm?: any,
nested?: any,
ownerArray?: any,
index?: any
) {
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 (__DEV__) {
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)
createChildren(vnode, children, insertedVnodeQueue)
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
insert(parentElm, vnode.elm, refElm)
if (__DEV__ && 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)
}
}
function createChildren(vnode, children, insertedVnodeQueue) {
if (isArray(children)) {
if (__DEV__) {
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)))
}
}
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)
}
}
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)
}
}
}
// ========================================
// ========================================
// src\platforms\web\runtime\node-ops.ts
export function insertBefore(
parentNode: Node,
newNode: Node,
referenceNode: Node
) {
parentNode.insertBefore(newNode, referenceNode)
}
export function appendChild(node: Node, child: Node) {
node.appendChild(child)
}