关键字: diff算法 key更高效 v-for中的index作为key的问题 虚拟dom给真实dom打补丁的过程 v-show v-if 初始数据请求放在created还是mounted里
接上文先补充一下初识数据渲染放在哪个生命周期,一般的话created和mounted都可以,而且如果请求少的话也不会出什么问题,
然后也看了很多分析说放在created的话如果请求过多会较长时间白屏(那这里也有小伙伴说是异步请求不会消耗很长时间,但是我认为的话异步请求只是在http异步请求里面执行代码,完成之后回调会进入事件队列,那假如说请求过多,在js引擎线程的同步事件执行完成之后异步请求还没完成的话,不也会出现长时间白屏影响渲染吗,所以异步请求并不是不要事件呀,只是用其他线程来执行代码)
,那如果又放在mounted里面的话,都知道mounted之前会执行render函数渲染,所以这个异步的时机就很随机了,如果是在渲染完成之后请求了数据,所以就可能会闪屏,因为当watcher被通知数据状态改变的时候,就会重新渲染render,那如果是在渲染之前就拿到了数据,那么也不会再次渲染了,
所以呢我觉得还是都可以吧,如果说数据是依赖dom的话当然在渲染的时候请求会好一些,但是created在某些程度来讲还是会更早拿到数据的,这些也是我在项目中和一些技术文章里面总结出来的,如果有不对的,欢迎大家来讨论。
再提一下v-show和v-if
因为这里的话就是项目里面常见的登录注册页面,这两个页面是同一个组件,但是就有一些注册里面的input框在登录里面是没有的,比如说再次输入密码这样的,那么这里一般会用v-show来控制隐藏,那么这里就会有问题,因为v-show虽然隐藏了,但他的站位还在,相当于visibility:hidden,所以如果方法不做处理的话,再次输入密码的input框会传入一个空值,那在登录里面方法就会报错,这里最简单的方法就是用v-if,改变dom结构,触发重排(v-show是重绘)v-if相当于display:none,就不会再传空值,如果用v-show的话,加一个当v-show为false时的方法直接return也可解决,
所以区别在于初始渲染是vdisplay:none不会渲染,但是后续每一次改变值都会触发重排,而v-show第一次就会渲染,但后续只是重绘而已,比较节省性能吧。
言归正传,从watcher说一下新的vnode生成和oldvnode进行运算并且给真实dom打补丁的过程,这里就涉及diff算法,diff算法基于两点
1相同组件的dom结构是一样的,不同组件的dom结构不同
2同一层级的一组节点,他们可以通过唯一的key值进行区分
基于以上这两点假设,使得虚拟DOM的Diff算法的复杂度从O(n^3)降到了O(n)
上一张图
那用key值的话会减少很多不必要的操作比如这个
在原来的dom上插入f,如果diff算法没有key值的话,那么在检测到f的时候就会用f来替换c,c替换d,d替换e,在插入e
但如果有key的话,就会直接插入,少了很多额外的操作
那就从上面这张图开始读源码吧,当setter通知所用watcher时,那么就开始打补丁了
那么就会有两种情况出现,当dom的结构和之前一样和不一样的时候,所以先会有一个判断
(出于习惯就在代码里写注释了)
function patch (oldVnode, vnode) {
// some code
if (sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode) //当结构一致时执行patchvnode函数
} else {//当结构不一样时执行的操作就简单多了,将之前的节点替换掉就行
const oEl = oldVnode.el // 当前oldVnode对应的真实元素节点
let parentEle = api.parentNode(oEl) // 父元素
createEle(vnode) // 根据Vnode生成新元素
if (parentEle !== null) {//判断父元素是否为空
api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl)) // 将新元素添加进父元素
api.removeChild(parentEle, oldVnode.el) // 移除以前的旧元素节点
oldVnode = null //解除全局变量的引用,回收内存
}
}
// some code
return vnode //最后更新虚拟dom
}
那么接下来就分析结构一致的情况,那么什么情况下结构是一致的呢,看一下samevnode源码
function sameVnode (a, b) {
return (
a.key === b.key && // key值一致
a.tag === b.tag && // 标签名一致
a.isComment === b.isComment && // 是否都为注释节点
// 是否都定义了data,data包含一些具体信息,例如onclick , style
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b) // 当标签是<input>的时候,type必须相同
)
}
那这些都一致了的话我们就要看他们的内容(文本)和子节点了
patchVnode (oldVnode, vnode) {
const el = vnode.el = oldVnode.el
let i, oldCh = oldVnode.children, ch = vnode.children
if (oldVnode === vnode) return //如果两个节点完全一样就直接return
if (oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text) {
api.setTextContent(el, vnode.text) //如果是文本节点当两个节点的文本不为空并且不相等是,直接替换
}else {
updateEle(el, vnode, oldVnode)
if (oldCh && ch && oldCh !== ch) {
updateChildren(el, oldCh, ch)
}else if (ch){
createEle(vnode) //create el's children dom
}else if (oldCh){
api.removeChildren(el)
}
}
}
当文本内容不同是直接替换,之后做了如下事情
- 找到对应的真实dom,称为
el
- 判断
Vnode
和oldVnode
是否指向同一个对象,如果是,那么直接return
- 如果他们都有文本节点并且不相等,那么将
el
的文本节点设置为Vnode
的文本节点。 - 如果
oldVnode
有子节点而Vnode
没有,则删除el
的子节点 - 如果
oldVnode
没有子节点而Vnode
有,则将Vnode
的子节点真实化之后添加到el
- 如果两者都有子节点,则执行
updateChildren
函数比较子节点,这一步很重要
现在看一看重要的updateChildren,代码量较大,就写在注释里了
updateChildren (parentElm, oldCh, newCh) {
let oldStartIdx = 0, newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
let oldKeyToIdx
let idxInOld
let elmToMove
let before //四个指针
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { //四个指针往中间走,
//当开始和结束指针位置相反时,表示有一个vnode已经走完了跳出循环
if (oldStartVnode == null) { // 后面用key值时会将已经移动的vnode用null替换,所以这里
//如果遇到了只要将指针往后面移动就行
oldStartVnode = oldCh[++oldStartIdx]
}else if (oldEndVnode == null) {
oldEndVnode = oldCh[--oldEndIdx]
}else if (newStartVnode == null) {
newStartVnode = newCh[++newStartIdx]
}else if (newEndVnode == null) {
newEndVnode = newCh[--newEndIdx]
}else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
//头指针和头指针比较如果相同调用patchVnode()这里会有递归知道把后面的更新完才会return,
//最简单的情况是两个内容相同直接return,之后两个头指针往中间走
}else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
//同理之后两个尾指针往中间走
}else if (sameVnode(oldStartVnode, newEndVnode)) {
patchVnode(oldStartVnode, newEndVnode)
api.insertBefore(parentElm, oldStartVnode.el, api.nextSibling(oldEndVnode.el))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
//这里的比较会很大的提升性能,将oldvnode的头指针和newnode的尾指针调用patchvonde(),这里和上面
//也是一样的,不同的是这里会在真实dom上将返回的vnode从之前的位置到oldEndVnode指针对应的节点之前,
//之后相同的key值的指针往前面走
}else if (sameVnode(oldEndVnode, newStartVnode)) {
patchVnode(oldEndVnode, newStartVnode)
api.insertBefore(parentElm, oldEndVnode.el, oldStartVnode.el)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
//这里会在真实dom上将返回的vnode插入到oldStartVnode指针对应的节点之前
}else {
// 当前面所用情况都没有时就使用key比较,那newvnode的当前头索引在oldvnode的key值里面找
if (oldKeyToIdx === undefined) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
// 有key生成index表
}
idxInOld = oldKeyToIdx[newStartVnode.key]
if (!idxInOld) {
api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
newStartVnode = newCh[++newStartIdx]
//如果新的vnode中的key值在oldvode中不存在,则真实dom的在oldStartVnode前插入现在的newvnode节点,
//指针向后
}
else {
elmToMove = oldCh[idxInOld]
if (elmToMove.sel !== newStartVnode.sel) {
api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
}else {
patchVnode(elmToMove, newStartVnode)
oldCh[idxInOld] = null
api.insertBefore(parentElm, elmToMove.el, oldStartVnode.el)
}
newStartVnode = newCh[++newStartIdx]
}
//如果存在则在真实dom的oldStartVnode前插入该节点,并将oldvnode的值设置为null,索引后移
}
}
if (oldStartIdx > oldEndIdx) {
before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].el
addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx)
//如果oldvnode里面头索引大于尾索引,说明newVnode里面还有节点,这时需要将newvnode里头索引到尾索引
//的node添加到真实dom中
}else if (newStartIdx > newEndIdx) {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
//反之说明需要将oldvnode中多余的值删掉
}
下面是从别处看到的例子
最后再用一张图来进行总结吧,组件的diff较为简单,而元素的话就有递归了
这里在补充一个小的问题就是为什么不推荐v-for的时候用index作为key,是因为如果对list进行操作的话会导致元素显示的混乱,比如将list[2]这个元素删掉,那么之前的list[3]的key就会是现在的list[2]的索引,那之后的显示也会乱掉