什么是虚拟dom
用js对象来描述dom结构,而不是真实dom,称之为虚拟dom
虚拟dom的优点
- 跨平台
- 复杂视图的更新更快
- 避免直接dom操作,提高开发效率
render函数的参数h函数
h函数对应的是源码中的createElement函数
作用: 创建一个虚拟节点Vnode
h(tag, data, children)
一个可以传入四个参数
- 第一个参数可以是标签名称,一个字符串
'h1'
,或者组件对象 - 第二个参数data 是用来描述tag的内容的,可以设置标签的属性或者对应dom元素的属性,注册事件等
- 第三个参数可以是字符串或者是数组,字符串的话设置的是标签里的内容,数组的话设置的是标签内的子节点
vue中的h函数支持组件和插槽
Vnode的核心属性
- tag
- data
- children
- text
- elm 记录真实dom,当Vnode转化成真实dom的时候,会把真实dom记录到这个属性里面来
- key dom复用
虚拟dom创建的整体过程
- vm._init 初始化vue中的实例成员,最后调用了
vm.$mount()
- vm.$mount() 中调用了
mountCompoent()
- mountCompoent() 中创建了
Watcher
对象 - Watcher 中调用了
updateCompoent()
- updateCompoent()
- vm._update(vm._render(), hydrating)
- vm._render()
vnode = render.call(vm.renderProxy, vm.$createElement)
通过call来调用的,第一个参数就是改变内部的this的指向,第二个就是render函数的参数,就是h 函数vm.$createElement()
- h函数,render中调用,通过代码延时
- createElement(vm,a,b,c, true)
- _createElement(context, tag, data, children, normalizationType)
vm._createElement()
- vnode = new VNode(config.parsePlatformTagName(tag), data, chidlren, undefind, undefind, context)
- vm._render() 结束,返回vnode
- vm._update()
- 负责把虚拟dom, 渲染成真实dom
- 首次执行
vm.__patch__(vm.$el, vnode, hydrating, false)
- 数据更新
vm.__patch__(preVnode, vnode)
- vm.patch()
- patchVnode()
- updateChildren
vm._render() 之 _createElement()
render方法中调用了用户传过来的render函数(这里调用的是vm.$createElement()
)或者模板编译生成的render函数(这里调用的是vm._c()
),两者差异在于参数,最终调用了_createElement()
vm.$createElement()
函数对于第二个参数data和第三个参数children有没有传做出一些处理,然后把参数传入_createElement()
做出真正的创建虚拟dom的处理
1. 先判断data
是否传入并且data.__ob__
是否存在,是的话创建空节点并且发出警告,不需要响应式的data
2. 确认data中是否有is
属性,是的话,确认为自定义组件,赋值给tagtag = data.is
- 在用户传过来的render和模板转化过来的render 用不同的方法把传进来的children参数铺平
- 判断tag的类型
- tag是string的时候,不同情况下通过不同的方法创建vnode
- 是否是保留标签
- 是否是自定义组件
- 是普通字符串
- 否则就是组件 通过
createComponent
创建组件对应的VNode
对象
- tag是string的时候,不同情况下通过不同的方法创建vnode
- 根据得到的vnode判断,是直接返回vnode还是处理过后返回vnode,或者创建一个空的vnode返回
vm._update()
执行完_createElement()之后,返回一个VNode,通过vm._render()
返回
vm._update(vm._render(), hydrating) // _render生成虚拟dom _update调用patch 对比两个虚拟dom的差异并更新
被vm._update()当做参数接收,而vm._update()的作用就是把虚拟dom转换成真实dom,这个函数在首次渲染和数据更新的时候都会被调用,这个函数的核心就是调用vm.__patch__
这个方法
每次调用vm._update()
的时候,都会在vm._vnode
中存储最新的 vnode,当每次调用update函数的时候,会更具里面是否有值,来判断是否是首次渲染
vm.patch()
createElm
createElm()
作用就是
- 把vnode转换成dom元素,并且插入到dom树上,
- 把虚拟节点的children,转换成真实dom,并且插入到dom树上
- 触发一些相应的钩子函数(create,insert加入insertedVnodeQueue队列)
patchVnode
patchVnode
对比新旧vnode,以及新旧节点的子节点,找到差异,更新到真实dom,也就是执行diff算法
- 新节点没有文本节点的时候
- 新老节点的子节点是否都存在,如果都存在并且不相等的话调用updateChildren
- 新的有子节点,老的没有子节点
- 检查是否有重复的key
- 老节点有文本节点的话,清空老节点的文本节点
- 为当前dom 节点加入子节点
- 新的没有子节点,老的有子节点,删除老节点中的子节点,并且触发remove 和destory 钩子函数
- 老的有文本节点的话,置空
- 新老节点都有文本节点 修改文本
updateChildren
当新老节点都有子节点,并且新老节点是sameVnode的时候,会调用updateChildren
作用:对比新老子节点,找到差异,更新到dom树,如果没有发生变化的话会重用该节点
- 老的开始节点和新的开始节点是sameVnode的时候
- 直接将该vnode进行
patchVnode
- 获取下一组新老开始节点,就是他们的index值++ 并且重新给新老节点赋值
- 直接将该vnode进行
- 老的结束节点和新的结束节点是sameVnode的时候
- 直接将该vnode进行
patchVnode
- 获取下一组新老结束(end)节点,就是他们的index值-- 并且重新给新老节点赋值
- 直接将该vnode进行
- 老的开始节点和新的结束节点是sameVnode的时候(处理数组翻转的情况)
- 直接将该vnode进行
patchVnode
- 把老的开始节点移动到老的结束节点之后,就是移动到最后
- 获取下一组节点(老的++ 新的--)
- 直接将该vnode进行
- 老的结束节点和新的开始节点是sameVnode的时候(处理数组翻转的情况)
- 直接将该vnode进行
patchVnode
- 把老的结束节点移动到老的开始节点之前,就是移动到最前
- 获取下一组节点(老的-- 新的++)
- 直接将该vnode进行
- 如果情况都不满足,newStartVnode 依次和旧的节点比较
- 拿新的开始节点的key,去老节点数组中依次找相同key的老节点
- 优化了找的过程 先把老节点数组中的key和索引存储到了一个对象 oldKeyToIdx 中
- 如果新的开始节点有key属性,就去oldKeyToIdx中查找老节点的索引
- 如果新的开始节点没有key属性,就要去老节点数组中依次遍历找到相同老节点对应的索引
- 有key的话会快一点
- 如果没有找到的话 ,说明没有相同节点
- 创建新开始节点对应的dom对象,并插入到老的开始节点对应的dom元素前面
- 如果找到老节点, 取出要移动的老节点
- 如果找到的老节点和新的开始节点是sameVnode,执行patchVnode,并且将找到的旧节点移动到旧的开始节点之前
- 如果不是sameVnode,就是key相同,但是tag不同,是不同的元素,那么创建新元素,并插入到老的开始节点对应的dom元素前面
- 拿新的开始节点的key,去老节点数组中依次找相同key的老节点
结束之后index++ newStartVnode = newCh[++newStartIdx]
当一次节点对比结束时
- 当结束时 oldStartIdx > oldEndIdx, 旧节点遍历完,但是新节点还没有,说明新节点比老节点多,把剩下的新节点插入到老节点的后面
- 当结束时 newStartIdx > newEndIdx, 新节点遍历完,老节点数组还没有,删除老节点中的剩余节点