之前写过博文Vue的首次渲染过程,在文章中提到,渲染dom的时候会调用vm.render()
函数生成VNode
,之后会调用vm._update(vnode, ...)
来渲染页面。
这篇文章我们从两个方面入手
vm.render()
是如何生成虚拟DOM的之前我们准备了首次渲染的调试代码,下面我们new Vue
中补充一些参数,去观察Vue是如何进行关于虚拟DOM的操作
<body>
<div id="app">div>
body>
<script>
const vm = new Vue({
el: '#app',
render(h){
const vnode = h(
'h1',
{
attrs: { id: 'title' }
},
this.msg
)
return vnode
},
data: {
msg: 'Hello Vue'
}
})
script>
上面的代码可以看出h()
函数主要生成虚拟DOM,他有多种传递参数的方式,在分析源码的过程再去做讨论
h()
返回的值一个VNode对象,下面记录下VNode的核心属性:
vm.render()
是如何生成虚拟DOM的此函数是在core/instance/render.js
中定义,这个方法的核心是调用了用户传递过来的render
const { render, _parentVnode } = vm.$options
// vm._renderProxy就是vue实例,观察我们传入的options.render,观察到:vm.$createElement就是我们定义的h函数
vnode = render.call(vm._renderProxy, vm.$createElement)
下面分析vm.$createElement
此函数是在core/instance/render.js
中定义,这个函数就一句话,核心作用就是对手写的render函数进行渲染。它上面还有个_c方法看起来和$createElement很像,这个方法是对编译生成的render 进行渲染的方法
// 对编译生成的render 进行渲染的方法
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
// 对手写 render 函数进行渲染的方法
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
发现$createElement
返回了 createElement
,下面看createElement
内部实现
此函数在/core/vdom/create-element.js
中定义, 当前这个函数主要是处理我们传递过来的参数, h函数可以接受参数的方式可以参考官方文档,这里就不说了。 处理完参数后,这个函数最终返回_createElement
,
export function createElement (
context: Component,
tag: any,
data: any,
children: any,
normalizationType: any,
alwaysNormalize: boolean
): VNode | Array<VNode> {
// 如果data是数组或者是原始值的时候
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children
// data复制给children
children = data
data = undefined
}
// 如果调用的是用户传入的render函数
if (isTrue(alwaysNormalize)) {
// normalizationType = 2
normalizationType = ALWAYS_NORMALIZE
}
return _createElement(context, tag, data, children, normalizationType)
}
export function _createElement (
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
// 1.-------------- 对传入的值做判断 --------------
/** 如果data不为空并且data中有__ob__的属性,说明data是响应式数据。开发环境报警告⚠️ ,返回空的VNode节点 */
// data中又is属性,会把is记录到tag里面
//
if (isDef(data) && isDef(data.is)) {
tag = data.is
}
/* 如果设置:is="fasle" 返回空的VNode节点*/
/** 如果data中有key,并且key不是原始值, 开发环境警告⚠️ */
/** 省略了一段处理插槽的代码 */
// 2.--------------处理传入children--------------
if (normalizationType === ALWAYS_NORMALIZE) {
// 返回一维数组,处理用户手写的render
// 判断 children 的类型,如果是原始值的话转换成 VNode 的数组
// 如果是数组的话,继续处理数组中的元素
// 如果数组中的子元素又是数组(slot template),递归处理
// 如果连续两个节点都是字符串会合并文本节点
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
// 把二维数组,转换成一维数组
// 如果 children 中有函数组件的话,函数组件会返回数组形式
// 这时候 children 就是一个二维数组,只需要把二维数组转换为一维数组
children = simpleNormalizeChildren(children)
}
// 3.--------------下面的代码开始创建vnode对象-------------
let vnode, ns
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
// tag是否是html中的保留标签
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
// 查找自定义组件构造函数的声明
// 根据 Ctor 创建组件的 VNode
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()
}
}
通过上面的分析,我们已经知道render
方法是如何工作的了,执行完createElement
后,我们拿到了VNode对象,回到 vm._update(vm._render(), hydrating)
。继续向下执行
这个方法在core/instance/lifecycle.js
中定义,作用是把VNode渲染成真实DOM,首次渲染会调用,数据更新也会调用。这个方法的核心是调用vm.__patch__
方法
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
const prevEl = vm.$el
const prevVnode = vm._vnode // 这里面存的是我们上一次处理的VNode
const restoreActiveInstance = setActiveInstance(vm)
vm._vnode = vnode
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
if (!prevVnode) {
// 如果不存在 说明是首次渲染
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// 数据更新的额时候调用
vm.$el = vm.__patch__(prevVnode, vnode)
}
...
}
vm.__patch__
/platforms/web/runtime/index.js
中定义,就一句话import { patch } from './patch'
// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop
patch
函数, 发现他是createPatchFunction
函数返回的
// nodeOps存储的是封装的一些dom操作
import * as nodeOps from 'web/runtime/node-ops'
import { createPatchFunction } from 'core/vdom/patch'
// 存储的是处理指令和ref的模块
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
,看一下patch
函数式如何初始化的在/core/vdom/patch.js
下找到createPatchFunction
。这个函数里面的内容很多,是一个高阶函数,返回了patch
函数进行函数柯里化,这里我们主要看patch函数的初始化过程
const hooks = ['create', 'activate', 'update', 'remove', 'destroy']
export function createPatchFunction (backend){
let i, j
const cbs = {}
/* 1.-----------------------------解构参数-----------------------------*/
// modules 节点的属性/事件/样式的操作
// nodeOps 节点操作
const { modules, nodeOps } = backend
/* 2.----------------------------钩子函数存到cbs里---------------------- */
for (i = 0; i < hooks.length; ++i) {
cbs[hooks[i]] = []
for (j = 0; j < modules.length; ++j) {
if (isDef(modules[j][hooks[i]])) {
// 如果模块中定义了对应的钩子函数
// 最后的形式为:cbs['update'] = [updateAttrs, updateClass, update...]
cbs[hooks[i]].push(modules[j][hooks[i]])
}
}
}
/*3.--------------------------初始化patch-----------------------------*/
return function patch(oldVnode, vnode, hydrating, removeOnly){
...
}
}
patch
,下面看patch里面做了些什么这个函数的核心功能是:对比两个 VNode 的差异,把差异更新到真实 DOM。如果是首次渲染的话,会把真实 DOM 先转换成 VNode
return function patch(oldVnode,vnode,hydrating,removeOnly){
/* 1.vnode不存在,但是oldVnode存在,执行Destory钩子函数 */
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
/* 2.声明两个变量,一个标记当前dom是否挂载到了dom树上,一个存储钩子函数*/
let isInitialPatch = false
const insertedVnodeQueue = []
/* 3. 判断oldVnode是否存在, 不存在就不会渲染到页面,只是创建新的dom存到内存中 */
if(isUndef(oldVnode)){
isInitialPatch = true
// 把vnode转成真实dom
createElm(vnode, insertedVnodeQueue)
}else{
// ① 判断oldVnode是否是dom元素
const isRealElement = isDef(oldVnode.nodeType)
// ② 如果oldVnode不是dom元素,并且和 vnode是 sameVnode
if(!isRealElement && sameVnode(oldVnode, vnode)){
// 开始执行diff, 并把差异更新到dom中去
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
}else{
... // 这地方有关于ssr的操作,不是本文的重点,省略了
// 如果第一个参数是真实DOM,把oldVnode 转成vnode节点
if (isRealElement) {
// 把oldVnode 转成vnode节点,存到oldVnode中
oldVnode = emptyNodeAt(oldVnode)
}
}
// ③ 找到oldVnode对应的父节点
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
// ④ vnode转成真实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)
)
// ⑤ 判断oldVNode中的获取的parentElm是否存在
if (isDef(parentElm)) {
// 把老节点移除,并触发相关的钩子函数
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
// ⑥ 通过isInitialPatch判断当前对应的dom元素是否挂载到dom树上,如果有:触发insertedVnodeQueue里面定义的所有钩子函数
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
}
patch函数看起来比较复杂,在这个地方加个简单的流程总结
path函数调用结束后,我们虚拟dom的更新其实已经完成了。但是我们发现patch 里面调用了两个关键函数,patchVnode
和 createElm
。这两个函数一个diff vnode,一个是把vnode转成真实dom,然后挂载到dom树上。
因为文章过长影响观感,对于这两个函数,单独开篇分析