上文介绍了什么是vnode
,虚拟dom
就是用一个原生的js
对象来描述dom
,那么vnode
是如何渲染成真实的dom
,本文不涉及组件渲染成真实的dom
,主要关注以下几点
vnode
是如何渲染成真实的dom
vnode
是何时渲染成真实的dom
本文demo
基础代码来自于源码,便于单步调试分析源码执行流程
// index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="app">
<h1>{{foo}}</h1>
<div>
<p>{{bar}}</p>
<h3>{{other}}</h3>
</div>
</div>
</body>
<script src="./dist/app.bundle.js"></script> //打包后的js
// index.js
new Vue({
render(h) {
return h('h1', { class: 'p' }, [this.msg, h('span', [h('a', 'game'), h('a', 'over')])])
},
data() {
return {
msg: 'hello'
}
}
}).$mount('#app')
demo中多次调用
h
方法生成vnode
,h方法也就是我们前面文章提到的createElement
方法,这个方法中调用了_createElement
方法。因为这里不包含组件,所以最终会调用new VNode(...)
方法生成需要渲染的vnode
.
我们在createElm
(后面介绍)方法中打一个断点,参考图(1),看看我们需要渲染的vnode
究竟是什么?
vnode
可以看到
vnode
的结构,主要关注以下三个属性
tag
: 指定渲染何种类型的html
节点,没有的话可能是注释节点或者是文本节点,上面的例子中有vnode
对应三个文本节点text
: 文本节点的内容children
:虚拟节点的子节点,这个很重要,反应了真实的dom
节点间的层级关系。从图(2)和图(3)中可以看到vnode
准确的的反映了真实的dom
结构,所以接下来我们会重点分析图(3)是如何转换成图(2)的。以及Vue
在源码实现的过程中值得学习的地方。
vm._update
从上文分析我们得知,无论时用户手写render
还是编译生成的render
函数最终都会生成vnode
,作为_update
方法的参数。
// src/core/instance/lifecycle.js 方法中了省略部分代码
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
...
let updateComponent
/* istanbul ignore if */
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
...
return vm
}
初次渲染时会生成渲染
watcher
,在创建该实例的过程中会执行updateComponent
方法,从而执行vm._update方法
.
// src/core/instance/lifecycle.js
export let activeInstance: any = null
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.
}
export function setActiveInstance(vm: Component) {
const prevActiveInstance = activeInstance
activeInstance = vm
return () => {
activeInstance = prevActiveInstance
}
}
重点关注以下几件事
vm._vnode = vnode
指定_vnode
为要渲染的vnode
setActiveInstance(vm)
方法中指定模块内的全局变量activeInstance
为当前实例vm
,并返回一个函数restoreActiveInstance
,这里利用了闭包实现。
因为是初次渲染,所以执行
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
下面我看看_patch
方法
__patch__
在src/platform/web/runtime/index.js
中定义了__patch__
方法
// install platform patch function
import { patch } from './patch'
Vue.prototype.__patch__ = inBrowser ? patch : noop
在src/platform/web/runtime/patch.js
/* @flow */
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
里面封装了原生的dom
操作,modules
目录下的文件与attr
,props
,events
相关。
export const patch: Function = createPatchFunction({ nodeOps, modules })
在src/core/vdom/patch.js
中定义了createPatchFunction
。该方法内部定义了很多方法,其中返回了patch
方法,这里的实现借助了js
闭包。不用每次patch
的过程中进行与平台有关的处理。
export function createPatchFunction (backend) {
...
createElm() {
}
return return function patch (oldVnode, vnode, hydrating, removeOnly) {
...
}
...
}
patch
方法中有四个参数,其中hydrating
和服务端有关。
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 ' +
', or missing
. 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
}
实例中的patch
过程主要分为以下几步
patch (oldVnode, vnode, hydrating, removeOnly)
-
oldVnode
是el: div#app
,此时调用oldVnode = emptyNodeAt(oldVnode)
将真实的dom
元素div#app
转化成虚拟dom
并且生成的oldVnode
的elm
属性为div#app
-
获取div#app
的父元素,实例中也就是body
const parentElm = nodeOps.parentNode(oldElm)
-
创建真实的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)
)
五. createElm
方法
这个方法中有重要一点,调用createElement
(这里不考虑createElementNS
)方法,生成真实的dom
,并赋值给vnode.elm
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
setScope(vnode)
从这里我们可以看到,vnode
描述这真实的dom
,vnode.elm
是根据vnode
生成的真实dom
.
在createElm
中会调用createChildren方法
,如果有子节点会创建好子节点并插入到父节点。这里使用了递归,也就是所有的子节点全部插入各自的父节点后,生成的整个dom
树才会插入到 父节点,示例中是body
.
分析图 (3)的vnode
插入流程.
首先将生成的整个vnode
插入
createElm(
vnode,
insertedVnodeQueue, //不用关注
parentElm, //body
refElm //不用关注
)
在插入的过程中由于tag===h1
的children
不为空
- 遍历
children
,调用createElm
方法将文本节点hello
插入到父节点h1
中
- 在插入
tag ==== 'span'
的vnode
节点的过程中,由于该vnode
的children
也不为空,所以会将children
中的每个child
插入到vnode:{tag:'span'}.elm
中,然后将span
插入到h1中
。最后将h1
插入到body
中。
最关键的是明白以下几点
- 会根据每个
vnode
对象生成真实的dom
元素,并赋值给vnode
对象的.elm
属性
- 在将
vnode.elm
插入到parentElm
(真实的父dom
节点)前,会调用createChildren
方法,将每个vnode
对应的真实dom
插入到对应的parentElm
中,最终完成整个虚拟dom
对应的真实dom
树的渲染,并将整个虚拟dom
对应的真实dom
树插入到body
中。
你可能感兴趣的:(vue源码笔记)