数据驱动
概念
数据驱动指的是:
视图是由数据驱动生成的,我们对视图的修改,不会直接操作 DOM,而是通过修改数据。
只关心数据的修改会让代码的逻辑变的非常清晰,因为 DOM 变成了数据的映射,我们所有的逻辑都是对数据的修改,而不用碰触 DOM,这样的代码非常利于维护。
new Vue
过程
Vue 类的定义入口:src/core/instance/index.js
// 创建了 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)
}
// 1. Vue 初始化,原型添加 _init 函数
initMixin(Vue)
// 2. 定义状态,原型添加 $set,$delete,$watch 函数
stateMixin(Vue)
// 3. 定义事件,原型添加 $on,$once,$off,$emit
eventsMixin(Vue)
// 4. 定义生命周期函数,原型添加 _update,$forceUpdate,$destroy
lifecycleMixin(Vue)
// 5. 定义渲染函数,原型添加 $nextTick,_render 函数
renderMixin(Vue)
Vue
只能通过 new
关键字初始化,然后调用 this._init(options)
。
this._init
做了什么
_init
方法是在调用 initMixin(Vue)
方法时挂载在 Vue
的原型上的,入口:src/core/instance/init.js
// 1. uid +1
vm._uid = uid++
// 2. a flag to avoid this being observed
vm._isVue = true
// 3. 合并配置
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
)
}
// 4. 将 vm(this) 挂载在私有变量_self 上
vm._self = vm
// 5. 初始化一些变量:生命周期(里面初始化一些生命周期要用到的私有变量)、事件、渲染
initLifecycle(vm)
initEvents(vm)
initRender(vm)
// 6. 调用钩子函数 beforeCreate
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
// 7. 初始化配置 $options 上的属性状态:props,methods,data,computed,watch
initState(vm)
initProvide(vm) // resolve provide after data/props
//8. 调用钩子函数 created
callHook(vm, 'created')
// 9. 初始化的最后,如果有 el 属性则调用 vm.$mount 方法挂载 vm
// 挂载的目标就是把模板渲染成最终的 DOM
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
vm.$mount
做了什么
入口:src/platforms/web/entry-runtime-with-compiler.js
在这个文件里重新定义了 $mount
原型方法(编译版才有,runtime-only 版本没有)
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean // 与服务端渲染相关
): Component {
// ...
// 调用 compileToFunctions 进行在线编译,转换成 render 方法
const { render, staticRenderFns } = compileToFunctions(template, {
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
return mount.call(this, el, hydrating)
}
第一次定义的入口:src/platforms/web/runtime/index.js
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
// hydrating 和服务端渲染相关,在浏览器环境下不需要传第二个参数
return mountComponent(this, el, hydrating)
}
mountComponent
函数入口:src/core/instance/lifecycle.js
拆解为以下步骤:
// 1. 挂载 el 元素到 $el
vm.$el = el
// 2. 执行钩子函数 beforeMount
callHook(vm, 'beforeMount')
// 3. 定义 updateComponent 函数,这个函数先执行了 vm._render() 方法生成虚拟 Node(VNode),再调用 vm._update 更新 DOM
let updateComponent
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
// 4. 实例化一个 渲染 Watcher (重点)
// Watch 作用:1.初始化的时候会执行回调函数 2. 当 vm 实例中的监测的数据发生变化的时候执行回调函数
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
if (vm.$vnode == null) {
// 判断为根节点的时候设置 vm._isMounted 为 true,表示这个实例已经挂载了
vm._isMounted = true
// 5. 执行钩子函数 mounted
callHook(vm, 'mounted')
}
因此接下来我们看 vm._render
方法做了什么。
vm._render
做了什么
入口:src/core/instance/render.js
_render 方法是实例的一个私有方法,把实例渲染成一个虚拟 Node。
Vue.prototype._render = function (): VNode {
const vm: Component = this
const { render, _parentVnode } = vm.$options
// reset _rendered flag on slots for duplicate slot check
if (process.env.NODE_ENV !== 'production') {
for (const key in vm.$slots) {
// $flow-disable-line
vm.$slots[key]._rendered = false
}
}
if (_parentVnode) {
vm.$scopedSlots = _parentVnode.data.scopedSlots || emptyObject
}
// 设置父节点,使得渲染函数可以取得父节点的 data 数据
vm.$vnode = _parentVnode
// render self
let vnode
try {
// render 方法的调用
// vm.$createElement 创建元素,返回 VNode : https://cn.vuejs.org/v2/guide/render-function.html#createElement-%E5%8F%82%E6%95%B0
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') {
if (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
}
} else {
vnode = vm._vnode
}
}
// 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(虚拟 DOM)
在调试控制台通过 document.createElement
创建的原生 DOM 元素,可以看到这个 DOM 元素上的属性和方法繁多,非常庞大。如下图所示。
Virtual DOM 是使用原生 JS 对象去描述一个 DOM 节点。比起创建一个原生 DOM 元素来说,代价要小得多。
// 例如
// DOM 是这样:
<div class="test">
<p>123</p>
</div>
// VNode 将真实的 DOM 的数据抽取出来,以对象的形式模拟树形结构。
var Vnode = {
tag: 'div',
data:{
class: 'test'
},
children: [
{ tag: 'p', text: '123' }
]
};
Vue.js
中定义了一个 VNode 类。
入口:src/core/vdom/vnode.js
export default class VNode {
tag: string | void; // 当前节点的标签名
data: VNodeData | void; // 当前节点的数据对象。VNodeData 定义入口: types/vnode.d.ts
children: ?Array<VNode>; // 数组类型,包含了当前节点的子节点
text: string | void; // 当前节点的文本,一般文本节点或注释节点会有该属性
elm: Node | void; // 当前虚拟节点对应的真实的dom节点
ns: string | void; // 当前节点的 namespace 命名空间
context: Component | void; // 当前节点的编译作用域
key: string | number | void; // 节点的key属性,用于作为节点的标识,有利于patch的优化
componentOptions: VNodeComponentOptions | void; // 创建组件实例时会用到的选项信息
componentInstance: Component | void; // 当前节点对应的组件实例
parent: VNode | void; // 当前节点的父节点
// strictly internal
raw: boolean; // 判断是否为HTML或普通文本,innerHTML的时候为true,innerText的时候为false (仅服务端)
isStatic: boolean; // 是否为静态节点
isRootInsert: boolean; // 是否作为根节点插入,被<transition>包裹的节点,该属性的值为false
isComment: boolean; // 当前节点是否是注释节点
isCloned: boolean; // 当前节点是否为克隆节点
isOnce: boolean; // 当前节点是否有v-once(只渲染一次)指令
asyncFactory: Function | void; // 异步组件工厂函数
asyncMeta: Object | void;
isAsyncPlaceholder: boolean;
ssrContext: Object | void;
fnContext: Component | void; // 函数化组件的作用域
fnOptions: ?ComponentOptions; // SSR 缓存
devtoolsMeta: ?Object; // 用于存储 devtools 的渲染函数上下文
fnScopeId: ?string; // 提供的函数作用域 Id
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
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: 向后兼容的 componentInstance 的别名。
/* istanbul ignore next */
get child (): Component | void {
return this.componentInstance
}
}
Vue.js
中 Virtual DOM
的实现借鉴了一个开源库:snabbdom
由于 VNode 只是用来映射到真实 DOM 的渲染,不需要包含操作 DOM 的方法,因此它是非常轻量和简单的。
上面只是说了 Virtual DOM 类的数据结构,那么它要如何映射到真实的 DOM?
映射经历了三个过程:create(创建)、diff(比较)、patch(打补丁)
VNode 在 Vue.js 中如何创建
入口:src/core/vdom/create-element.js
// 0 : 不需要进行规范化
const SIMPLE_NORMALIZE = 1 // 1:只需要简单的规范化处理,将 children 数组打平一层并且都规范为 VNode 类型
const ALWAYS_NORMALIZE = 2 // 2:完全规范化,将一个 N 层的 children 完全打平为一维数组且规范为 VNode 类型
function createElement (
context: Component,
tag: any,
data: any,
children: any,
normalizationType: any,
alwaysNormalize: boolean
): VNode | Array<VNode> {
// 若 data 是数组 || data 是四种基本类型(string/number/symbol/boolean)
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
函数将参数处理完成后,调用 _createElement
进行 VNode 真正的创建。
function _createElement (
context: Component, // VNode 的上下文环境,是 Component 类型
tag?: string | Class<Component> | Function | Object, // 表示标签
data?: VNodeData, // 表示 VNode 的数据,它是一个 VNodeData 类型
children?: any, // children 表示当前 VNode 的子节点,它是任意类型的,它将被规范为标准的 VNode 数组
normalizationType?: number // 表示子节点规范的类型
): VNode | Array<VNode> {
// isDef 判断参数是否 defined (!undefined&&!null)
// 并且data的__ob__已经定义(表示已经被 observed,上面绑定了 Oberver 对象)
// 则创建一个空节点
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()
}
// is: 用于动态组件且基于 DOM 内模板的限制来工作。https://cn.vuejs.org/v2/api/#is
if (isDef(data) && isDef(data.is)) {
tag = data.is
}
if (!tag) {
// 万一动态组件的 :is 被设为假值,则返回一个没有内容的注释节点
return createEmptyVNode()
}
// 警告:当 key 值非基本类型(string/number/symbol/boolean)
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
}
if (normalizationType === ALWAYS_NORMALIZE) {
// 完全规范化(将 children 完全打成一维)
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
// 简单规范化(将 children 降一个维度)
children = simpleNormalizeChildren(children)
}
let vnode, ns
// 如果 tag 标签是 string 类型的,即 div/p/span 之类的。
if (typeof tag === 'string') {
let Ctor
// 定义标签名的命名空间
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
// 判断 tag 标签是否是 HTML 的保留标签
if (config.isReservedTag(tag)) {
// 则直接创建一个普通 VNode 节点
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
} else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// 判断 tag 是否为自定义组件标签
// Ctor 为组件的构造类
// 创建一个组件节点
vnode = createComponent(Ctor, data, context, children, tag)
} else {
// 未知命名空间的元素
// 也要兜底地创建一个节点
// 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 {
// tag 不是字符串时直接创建一个组件节点
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 {
// 如果vnode没有成功创建则创建空节点
return createEmptyVNode()
}
}
什么是 children 规范化
if (normalizationType === ALWAYS_NORMALIZE) {
// 完全规范化(将 children 完全打成一维)
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
// 简单规范化(将 children 降一个维度)
children = simpleNormalizeChildren(children)
}
问题: 为什么在这里要对 children 子节点们进行规范?
解答:
VNode 实际上是一个树状结构,每一个 VNode 可能会有若干个子节点,这些子节点也应该是 VNode 结构。
但是在 createElement 函数中, children 的类型是 any,也就是说可以是任意类型的。
因此, 必须将其规范为 VNode 类型。
那再来看normalizeChildren
和simpleNormalizeChildren
分别做了什么事。
函数入口:src/core/vdom/helpers/normalize-children.js
// 代码中对需要两个规范化做出的解释:
// 模板编译器试图在编译时静态分析模板来最大程度地减少规范化的次数。
// 对于纯 HTML 标记,可以完全跳过规范化,因为可以保证所生成的渲染函数返回的数组元素都是是 VNode 类型。
// 但这里有两个额外的场景是需要规范化的:
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. 当子节点始终包含嵌套的数组(多维数组)时 -> 两种情况:
// a. 当编译 <template>, <slot>, v-for 时会产生多维数组
// b. 当用户自己手写渲染函数/JSX,子节点可能是多维数组
// 以上两种情况都需要通过完全规范化来适配所有可能出现的情况
function normalizeChildren (children: any): ?Array<VNode> {
// 1. 如果 children 是基本类型,则创建文本节点
// 2. 如果是一个数组,则调用 normalizeArrayChildren() 函数
// 3. 否则返回 undefined
return isPrimitive(children)
? [createTextVNode(children)]
: Array.isArray(children)
? normalizeArrayChildren(children)
: undefined
}
/**
* normalizeArrayChildren
* @param {*} children 表示要规范的子节点
* @param {*} nestedIndex 表示嵌套的索引, 因为 children 内部的单个 child 也可能是一个数组
*/
function normalizeArrayChildren (children: any, nestedIndex?: string): Array<VNode> {
const res = [] // 存放最后展平的 VNode 数组
let i, c, lastIndex, last
// 遍历 children
for (i = 0; i < children.length; i++) {
// 获得单个节点 c
c = children[i]
// 若 c 节点为 undefined 或 boolean 类型,则下一个
if (isUndef(c) || typeof c === 'boolean') continue
lastIndex = res.length - 1
last = res[lastIndex]
// 如果是一个数组类型
if (Array.isArray(c)) {
if (c.length > 0) {
// 递归调用 normalizeArrayChildren
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)) { // 若是基本类型
// 如果已部分展平的 VNode 数组 res 的最后一个节点是文本节点
//(即存在两个连续的 text 节点,会把它们合并成一个 text 节点)
if (isTextNode(last)) {
// 合并相邻的文本节点
// 这在服务端渲染里是很有必要的,因为服务端渲染为 html 字符串类型时,文本节点为字符串类型,本质上就需要合并
res[lastIndex] = createTextVNode(last.text + c)
} else if (c !== '') { // 若未空字符串
// 将 c 转换为文本节点
res.push(createTextVNode(c))
}
} else {
// 如果当前节点和已存入的最后一个节点都是文本节点
if (isTextNode(c) && isTextNode(last)) {
// 合并相邻的文本节点
res[lastIndex] = createTextVNode(last.text + c.text)
} else {
// 可能是由 v-for 生成的带有默认 key 的嵌套数组子节点
if (isTrue(children._isVList) &&
isDef(c.tag) &&
isUndef(c.key) &&
isDef(nestedIndex)) {
c.key = `__vlist${nestedIndex}_${i}__`
}
res.push(c)
}
}
}
return res
}
vm._update
做了什么
入口:src/core/instance/lifecycle.js
_update 方法是实例的一个私有方法,它会在(1)首次渲染,(2)数据更新时调用,这个方法的作用是把 VNode 渲染成真实的 DOM。
这里先对首次渲染部分进行分析,数据更新在分析响应式原理时再看。
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
const prevEl = vm.$el
const prevVnode = vm._vnode
const prevActiveInstance = activeInstance
activeInstance = vm
vm._vnode = vnode
// Vue.prototype.__patch__ 方法是根据使用的渲染后端不同在入口处引入的方法不相同
// 即不同的平台,例如在 web 和 weex 上的定义不一样
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
activeInstance = prevActiveInstance
// 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.
}
从上面的源码看,核心方法就是 __patch__
,这个方法也是整个Virtual DOM构建真实DOM最核心的方法。
Vue.prototype.__patch__
函数功能: 主要完成了新的虚拟节点和旧的虚拟节点的diff
过程,根据两者的比较结果最小单位地修改视图。经过patch
过程之后生成真实的 DOM 节点并完成视图更新的工作。
入口:src/platforms/web/runtime/index.js
Vue.prototype.__patch__ = inBrowser ? patch : noop
可以看到,在非浏览器环境(服务端渲染)中,是一个空函数。
原因:在服务端渲染中,没有真实的浏览器 DOM 环境,不需要把 VNode 最终转换成 DOM。
在浏览器环境中,是一个 patch
方法。
patch
函数定义的入口:src/platforms/web/runtime/patch.js
import * as nodeOps from 'web/runtime/node-ops' // 封装了一系列 DOM 操作的方法
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
定义的入口: src/core/vdom/patch.js
const hooks = ['create', 'activate', 'update', 'remove', 'destroy']
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]])
}
}
}
// 定义一系列辅助方法
// ...
/**
* 最终返回了一个 patch 方法
* @param {*} oldVnode 表示旧的 VNode 节点, 可以不存在或者是一个 DOM 对象
* @param {*} vnode 表示执行 _render 后返回的 VNode 的节点
* @param {*} hydrating 表示是否是服务端渲染
* @param {*} removeOnly 给 transition-group 用
*/
return function patch (oldVnode, vnode, hydrating, removeOnly) {
// 执行 _render 后返回的 vnode 不存在
if (isUndef(vnode)) {
// 旧的 VNode 节点存在则销毁旧的 VNode 的钩子
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue = []
// oldVnode 未定义的时候
if (isUndef(oldVnode)) {
// 说明是一个空的 mount(可能是组件),创建一个新的 root 元素
// 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)) {
// 是位置处在同一个节点则直接调用 patchVnode 函数修改现有节点
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.
// 当旧的 VNode 是服务端渲染的元素,hydrating 标记为 true
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
oldVnode.removeAttribute(SSR_ATTR)
hydrating = true
}
// oldVnode 是服务端渲染的元素
if (isTrue(hydrating)) {
// hydrate 函数只支持浏览器,则可以将 oldVnode 节点理解为真实 DOM 节点
// 将渲染得到的新节点 vnode 合并进 DOM 节点 oldVnode
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 是真实节点但上面的操作又不是服务器端渲染或是合并到真实DOM失败,
// 则创建一个空节点替换 oldVnode
oldVnode = emptyNodeAt(oldVnode)
}
// replacing existing element
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
// create new node
// 虚拟节点创建真实 DOM 并插入到它的父节点中
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
// 递归地更新父节点 element
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)) {
// 调用 destroy 钩子
invokeDestroyHook(oldVnode)
}
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
}