Vue源码剖析-虚拟DOM
- 课程回顾
- 虚拟DOM概念回顾
- 代码演示
- h函数用法
- h函数的返回结果-vnode
- 整体分析过程
- VNode的创建过程-createElement-上
- VNode的创建过程-createElement-下
- VNode的处理过程-update
- patch函数初始化
- patch函数的执行过程
- cbs对象
- patch函数内部的实现
- createElm函数
- patchVnode
课程回顾
- 虚拟DOM库-Snabbdom
- Vue.js响应式原理模拟实现
- Vue.js源码-响应式原理
虚拟DOM概念回顾
什么是虚拟DOM
- 虚拟DOM(Virtual DOM)是使用JavaScript对象描述真实的DOM
- Vue.js中的虚拟DOM借鉴Snabbdom,并添加了Vue.js的特性
- 例如:指令和组件机制
为什么要使用虚拟DOM
- 避免直接操作DOM,提高开发效率
- 作为一个中间层可以跨平台(服务端渲染)
- 虚拟DOM不一定可以提高性能
- 首次渲染的时候会增加开销
- 复杂视图情况下提升渲染性能
代码演示
- Vue中的h函数,相对于snanbbdom中的h函数,支持传入插槽和组件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="app">
<p ref="P1">{{msg}}</p>
</div>
<!-- 完整版本 -->
<script src="../../dist/vue.js"></script>
<script>
const vm = new Vue({
el: "#app",
render(h) {
const vnode = h("h1", { attrs: { id: "title" } }, this.msg);
console.log(vnode);
return vnode;
},
data: {
msg: "Hello Vue",
},
});
</script>
</body>
</html>
h函数用法
- vm.$creteElement(tag,data,children,normalizeChildren)
- tag:标签名或组件名称
- data:描述tag,可以设置DOM的属性或者标签的属性
- children:tag中的文本内容或者子节点
h函数的返回结果-vnode
- Vnode的核心属性
- tag
- data
- children
- text
- elm :记录vnode转换成的真实dom
- key: 复用当前的元素
整体分析过程
VNode的创建过程-createElement-上
- render函数内部的h函数就是createElement,有两种情况
- 把template模板转换成render函数时,render函数内部会使用_c
- 当直接写的render函数(不是template转变过来的),render函数内部使用$createElement
// 当把template模板转换成render函数时,render函数内部会使用_c
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.
// 当直接写的render函数(不是template转变过来的),render函数内部使用$createElement
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
createElement函数的作用是处理参数,,因为我们调用h参数的时候既可以传递两个参数,也可以传递三个参数,而真正创建Vnode是在_createElement函数中完成
// createElement函数的作用是处理参数,,因为我们调用h参数的时候既可以传递两个参数,也可以传递三个参数
// 而真正创建Vnode是在_createElement函数中完成
export function createElement (
context: Component, // vm实例
tag: any, // 标签
data: any, // 描述标签的数据
children: any, // 子节点
normalizationType: any,
alwaysNormalize: boolean
): VNode | Array<VNode> {
// 如果data是数组或者原始值,实际上data就是children
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children
children = data
data = undefined
}
// 用户传入的render函数,alwaysNormalize为true,模板编译转换成的render函数alwaysNormalize为false
if (isTrue(alwaysNormalize)) {
normalizationType = ALWAYS_NORMALIZE // 常量,值为2
}
return _createElement(context, tag, data, children, normalizationType)
}
VNode的创建过程-createElement-下
- _createElement函数分析
// createElement函数的作用是处理参数,,因为我们调用h参数的时候既可以传递两个参数,也可以传递三个参数
// 而真正创建Vnode是在_createElement函数中完成
// 正常情况下h函数只需要传tag/data/children这三个参数,normalizationType和alwaysNormalize是createElement内部的变量
export function createElement (
context: Component, // vm实例
tag: any, // 标签
data: any, // 描述标签的数据
children: any, // 子节点
normalizationType: any, // normalizationType不同,处理children的函数不同
alwaysNormalize: boolean
): VNode | Array<VNode> {
// 如果data是数组或者原始值,实际上data就是children
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children
children = data
data = undefined
}
// 用户传入的render函数,alwaysNormalize为true,模板编译转换成的render函数alwaysNormalize为false
if (isTrue(alwaysNormalize)) {
normalizationType = ALWAYS_NORMALIZE // 常量,值为2
}
return _createElement(context, tag, data, children, normalizationType)
}
// _createElement函数的作用是生成vnode
export function _createElement (
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
// data存在,且data._ob_属性(observer对象)存在,说明data是响应式的数据
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()
}
// object syntax in v-bind
// <component :is="currentTabComponent"></component>
if (isDef(data) && isDef(data.is)) {
// 如果data中有is属性,会记录到tag中来
// is属性的作用:动态组件
tag = data.is
}
if (!tag) {
// in case of component :is set to falsy value
// 如果tag是false,相当于把is指令设置为false,返回一个空的虚拟节点
return createEmptyVNode()
}
// warn against non-primitive key
if (process.env.NODE_ENV !== 'production' &&
// 如果data中有key属性,且不是原始值,此时报一个警告,key避免使用非原始值,应该适应字符串或者number类型值
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
)
}
}
// support single function children as default scoped slot
// 处理作用域插槽
if (Array.isArray(children) &&
typeof children[0] === 'function'
) {
data = data || {}
data.scopedSlots = { default: children[0] }
children.length = 0
}
// 下面的代码就是将children拍平,将多维数组转换成的一维数组
if (normalizationType === ALWAYS_NORMALIZE) {
// 当normalizationType的值为ALWAYS_NORMALIZE的时候,说明执行的是有用户传过来的render函数
// 将多维数组转换成一维数组
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
// 当normalizationType的值为SIMPLE_NORMALIZE的时候,说明执行的是tempalte编译转换成的render函数
// 将二维数组(如果children的元素存在是函数式组件的时候)转换成一维数组
children = simpleNormalizeChildren(children)
}
let vnode, ns
// tag是字符串
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对象
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
} else {
// direct component options / constructor
// 如果不是字符串,这说明是组件,创建组件对应的VNode对象
vnode = createComponent(tag, data, context, children)
}
if (Array.isArray(vnode)) {
// vnode是数组,直接返回
return vnode
} else if (isDef(vnode)) {
// 如果vnode已经初始化好了
if (isDef(ns)) applyNS(vnode, ns) // 处理vnode的命名空间
if (isDef(data)) registerDeepBindings(data)
return vnode
} else {
// 都不满足,返回一个空的注释节点
return createEmptyVNode()
}
}
VNode的处理过程-update
- vm._update(vm._render(), hydrating)=>vm._render()只是创建的VNode,将创建好的VNode交给update函数处理
- patch函数的作用:判断是否是首次渲染,调用vm._patch_()方法
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
const prevEl = vm.$el
const prevVnode = vm._vnode // vm实例的_vnode存储的是之前处理过的VNode对象,如果不存在该属性,说明是首次渲染
const restoreActiveInstance = setActiveInstance(vm)
vm._vnode = vnode
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
// 在src\platforms\web\runtime\index.js中注册了Vue.prototype.__patch__
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.
}
- 首先会判断preVnode(vm._vnode属性中保存)存在吗,如果存在,则说明不是首次渲染,如果存在,则说明是数据更新后的对比渲
- 首次渲染
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
- 数据更新后对比渲染
vm.$el = vm.__patch__(prevVnode, vnode)
- vm.__patch__方法会将虚拟节点转换后真实dom挂载到vm.$el属性上
patch函数初始化
- src\platforms\web\runtime\index.js
import { patch } from './patch'
Vue.prototype.__patch__ = inBrowser ? patch : noop
- 在src\platforms\web\runtime\patch.js中
- nodeOps是操作dom的各种api
- baseModules:与平台无关的模块,处理指令的
- 平台相关的模块attrs/klass/events/style等
- createPatchFunction创建patch函数的高阶函数
- 在src\core\vdom\patch.js中
patch函数的执行过程
cbs对象
- patch函数是执行createPatchFunction函数返回的函数=>柯里化
- 现返回patch函数之前,定义了cbs=>各种生命周期钩子对应的处理函数
const hooks = ['create', 'activate', 'update', 'remove', 'destroy']
// 定义cbs
for (i = 0; i < hooks.length; ++i) {
// cbs['update']=[]
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]])
}
}
}
patch函数内部的实现
- 全局变量insertedVnodeQueue=>新插入节点的队列,存储将来新插入VNode节点,存储这些节点的目的是将来把这些VNode节点对应的DOM元素挂载到DOM树上之后会触发这些VNode的insert钩子函数
- 如果老节点不是真实dom,且新老虚拟节点不是相同节点=>patchVnode(oldVnode, vnode, insertedVnodeQueue)
- 如果老节点oldVnode是 真实dom,将oldVnode 转换成虚拟dom=>oldVnode = emptyNodeAt(oldVnode),然后将新虚拟节点转换成真实dom,挂载到父节点上=>createElm(vnode,insertedVnodeQueue,parentElm)
createElm函数
- 作用;将虚拟dom转换成真实dom,并且挂载到dom树上
patchVnode
- 参考snabbdom