1 数据更新视图
2 跨平台
因为使用了
Virtual DOM
,Vue.js 具有了跨平台能力
Virtual DOM
只是js
对象,是如何调用不同平台的 api 的?
- 依赖于适配层:将不同平台的 api 封装在内,以同样的接口对外暴露
2.1 举个栗子
- 提供
nodeOps
对象做适配,根据platform
区分不同平台来执行当前平台对应的 api,而对外提供了一致的接口,供Virtual DOM
来调用。
const nodeOps = {
setTextContent(text) {
if(platform === 'weex') {
node.parentNode.setAttr('value', text);
} else if(platform === 'web') {
node.textContent = text;
}
},
parentNode() {
// ...
},
removeChild() {
// ...
},
nextSibling() {
// ...
},
insertBefore() {
// ...
}
}
3 patch 过程使用到的 API
- 这些
api
在patch
的过程中使用,调用nodeOps
中的相应函数来操作平台
3.1 insert
- 用来在
parent
这个父节点下插入一个子节点,如果指定了ref
则插入到ref
这个子节点前面
function insert(parent, elm, ref) {
if(parent) {
if(ref) {
if(ref.parentNode === parent) {
nodeOps.insertBefore(parent, elm, ref);
}
} else {
nodeOps.appendChild(parent, elm);
}
}
}
3.2 createElm
- 用来新建一个节点,
tag
存放创建一个标签节点,否则创建一个文本节点
function createELm(vnode, parentElm, refElm) {
if(vnode.tag) {
insert(parentElm, nodeOps.createElement(vnode.tag), ref);
} else {
insert(parentElm, nodeOps.createElement(vnode.text), ref);
}
}
3.3 addVnodes
- 用来批量调用
createElm
新建节点
function addVnodes(parentElm, refElm, vnodes, startIdx, endIdx) {
for(; startIdx <= endIdx; ++startIdx) {
createElm(vnodes[startIdx], parentElm, refElm);
}
}
3.4 removeNode
- 用来移除一个节点
function removeNode(el) {
const parent = nodeOps.parentNode(el);
if(parent) {
nodeOps.removeChild(parent, el);
}
}
3.5 removeVnodes
- 用来批量调用
removeNode
移除节点
function removeVnodes(parentElm, vnodes, startIdx, endIdx) {
for(; startIdx <= endIdx; ++startIdx) {
const ch = vnodes[startIdx];
if(ch) {
removeNode(cn.elm);
}
}
}
4 patch
4.1 diff 算法
用
diff
算法可以比对出两棵树的差异
diff
算法是通过同层的树节点进行比较 --> 时间复杂度只有O(n)
,是一种相当高效的算法
4.2 patch 的过程
- patch 的主要功能:比对两个
Vnode
节点,将差异更新到视图上
function patch(oldVnode, vnode, parentElm) {
if(!oldVnode) {
addVnodes(parentElm, null, vnode, 0, vnode.length - 1);
} else if(!vnode) {
removeVnodes(parentElm, oldVnode, 0, oldVnode.length - 1);
} else {
if(sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode);
} else {
removeVnodes(parentElm, oldVnode, 0, oldVnode.length - 1);
addVnodes(parentElm, null, vnode, 0, vnode.length - 1);
}
}
}
1. 在 oldVnode 不存在时
- 相当于新的
Vnode
替代原本没有的节点,所以直接用addVnodes
将这些节点批量添加到parentElm
上
if(!oldVnode) {
addVnodes(parentElm, null, vnode, 0, vnode.length - 1);
}
2. 在 Vnode(新Vnode节点)不存在时
- 相当于要把旧的节点删除,所以直接用
removeVnodes
进行批量的节点删除
else if(!vnode) {
removeVnodes(parentElm, oldVnode, 0, oldVnode.length - 1);
}
3. 当 oldVnode 与 Vnode 都存在时
- 需要判断它们是否属于
sameVnode
,如果是则进行patchVnode
操作,否则,删除旧节点,增加新节点
if(sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode);
} else {
removeVnodes(parentElm, oldVnode, 0, oldVnode.length - 1);
addVnodes(parentElm, null, vnode, 0, vnode.length - 1);
}
5 sameVnode
- 只有当
key
、tag
、isComment
(是否为注释节点)、data
同时定义(不定义),同时满足当标签类型为input
的时候type
相同即可。
function sameVnode() {
return (
a.key === b.key &&
a.tag === b.tag &&
a.isComment === b.isComment &&
(!!a.data) === (!!b.data) &&
sameInputType(a, b)
)
}
function sameInputType(a, b) {
if(a.tag !== 'input') return true;
let i;
const typeA = (i = a.data) && (i = i.attrs) && i.type;
const typeB = (i = b.data) && (i = i.attrs) && i.type;
return typeA === typeB;
}
6 patchVnode
patchVnode
是在符合sameVnode
的条件下触发的,会进行比对
function patchVnode(oldVnode, vnode) {
if(oldVnode === vnode) return
if(vnode.isStatic && oldVnode.isStatic && vnode.key === oldVnode.key)
{
vnode.elm = oldnode.elm;
vnode.componentInstance = oldVnode.componentInstance;
retirn;
}
if(vnode.text) {
nodeOps.setTextContent(elm, vnode.text);
}
if(oldCh && ch && (oldCh !== ch)) {
updateChildren(elm, oldCh, ch);
} else if(ch) {
if(oldVnode.text) nodeOps.setTextContent(elm, '');
addVnodes(elm, null, ch, 0, ch.length - 1);
} else if(oldCh) {
removeVnodes(elm, oldCh, 0, oldCh.length - 1);
} else if(oldVnode.text) {
nodeOps.setTextContent(elm, '');
}
}
6.1 在新旧 Vnode 节点相同时,直接 return
if(oldVnode === vnode) return
6.2 当新旧 Vnode 节点都是 isStatic(静态的),并且 key相同时
- 将
componentInstance
与elm
从旧 Vnode 节点“拿过来”,跳过比对的过程
if(vnode.isStatic && oldVnode.isStatic && vnode.key === oldVnode.key) {
vnode.elm = oldnode.elm;
vnode.componentInstance = oldVnode.componentInstance;
retirn;
}
6.3 当新 Vnode 节点是文本节点时,直接用 setTextContent
来设置 text--nodeOps
是适配层
if(vnode.text) {
nodeOps.setTextContent(elm, vnode.text);
}
6.4 当新 Vnode 节点是非文本节点时
-
当
oldCh
与ch
都存在且不相同时,使用UpdateChildren
函数来更新子节点 -
当只有
ch
存在时,如果旧节点是文本节点则先将节点的文本清除,然后将ch
批量插入到节点elm
下 -
当只有
oldCh
存在时,说明需要将旧节点通过removeVnodes
全部清除 -
当只有旧节点是文本节点时,清除其节点文本内容
if(oldCh && ch && (oldCh !== ch)) {
updateChildren(elm, oldCh, ch);
} else if(ch) {
if(oldVnode.text) nodeOps.setTextContent(elm, '');
addVnodes(elm, null, ch, 0, ch.length - 1);
} else if(oldCh) {
removeVnodes(elm, oldCh, 0, oldCh.length - 1);
} else if(oldVnode.text) {
nodeOps.setTextContent(elm, '');
}
7 updateChildren
7.1 定义新旧节点两边的索引:oldStartIdx
、newStartIdx
、oldEndIdx
、newEndIdx
- 同时定义指向这几个索引对应的 Vnode 节点:
oldStartVnode
、newStartVnode
、oldEndVnode
、newEndVnode
7.2 while循环:oldStartIdx
、newStartIdx
、oldEndIdx
、newEndIdx
会逐渐向中间靠拢
1 当 oldStartVnode
或者 oldEndVnode
不存在时,oldStartIdx
与 oldEndIdx
继续向中间靠拢,并更新对应的 oldStartVnode
与 oldEndVnode
的指向
2 将 oldStartIdx
、newStartIdx
、oldEndIdx
、newEndIdx
两两比对的过程
一共会出现
2*2=4
种情况
-
oldStartVnode
与newStartVnode
符合sameVnode
时,直接patchVnode
,同时oldStartIdx
与newStartIdx
向后移一位 -
oldEndVnode
与newEndVnode
符合sameVnode
时,同样进行patchVnode
操作并将oldEndIdx
与newEndIdx
向前移动一位 -
oldStartVnode
与newEndVnode
符合sameVnode
时,将oldStartVnode.elm
直接移动到oldEndVnode.elm
这个节点后面,然后oldStartIdx
向后移动一位,newEndIdx
向前移动一位 -
oldEndVnode
与newStartVnode
符合sameVnode
时,将oldEndVnode.elm
直接移动到oldStartVnode.elm
这个节点后面,然后oldEndIdx
向前移动一位,newStartIdx
向后移动一位
3 其他情况下
-
处理节点得到一个
key
与index
索引对应的 map 表oldKeyToIdx
-
可以根据某一个 key 值,快速地从
oldKeyToIdx
中获取相同 key 节点的索引idxInOld
,然后找到相同的节点 -
如果没有找到相同的节点,则通过
createElm
创建一个新节点,并将newStartIdx
向后移一位 -
如果找到了节点,同时也符合
sameVnode
,则将这两个节点进行patchVnode
,将该位置的旧节点赋值为 undefined,同时将newStartVnode.elm
插入到oldStartVnode.elm
前面,newStartIdx
后移一位 -
如果不符合
sameVnode
,只能创建一个新节点插入到parentElm
的子节点中,newStartIdx
后移一位
7.3 当 while 循环结束后,如果 oldStartIdx
> oldEndIdx
,调用 addVnodes
将新节点多的节点插入
7.4 如果满足 newStartIdx
> newEndIdx
,调用 removeVnodes
将多的旧节点批量删除
function updateChildren(parentElm, oldCh, newCh) {
// 新旧节点 两边的索引及节点
let oldStartIdx = 0;
let newStartIdx = 0;
let oldEndIdx = oldCh.length - 1;
let newEndIdx = newCh.length - 1;
let oldStartVnode = oldCh[0];
let newStartVnode = newCh[0];
let oldEndVnode = oldCh[oldEndIdx];
let newEndVnode = newCh[newEndIdx];
let oldKeyToIdx, idxInOld, elmToMove, refElm;
// 在while循环中,新旧节点两边的索引会逐渐向中间靠拢
while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if(!oldStartVnode) {
oldStartVnode = oldCh[++oldStartIdx];
} else if(!oldEndVnode) {
oldEndVnode = oldCh[--oldEndIdx];
} else if(sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode);
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
} else if(sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
} else if(sameVnode(oldStartVnode, newEndVnode)) {
patchVnode(oldStartVnode, newEndVnode);
nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm));// 第三个参数什么意思?第一个参数是什么
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
} else if(sameVnode(oldEndVnode, newStartVnode)) {
patchVnode(oldEndVnode, newStartVnode);
nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);//不懂too
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
} else {
let elmToMove = oldCh[idxInOld];
if(!oldKeyToIdx) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
if(!idxInOld) {
createElm(newStartVnode, parentElm);
newStartVnode = newCh[++newStartIdx];
} else {
elmToMove = oldCh[idxInOld];
if (sameVnode(elmToMove, newStartVnode)) {
patchVnode(elmToMove, newStartVnode);
oldCh[idxInOld] = undefined;
nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm);
newStartVnode = newCh[++newStartIdx];
} else {
createElm(newStartVnode, parentElm);
newStartVnode = newCh[++newStartIdx];
}
}
}
}
if(oldStartIdx > oldEndIdx) {
refElm = (newCh[newEndIdx + 1]) ? newCh[newEndIdx + 1].elm : null;
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx);
} else if(newStartIdx > newEndIdx) {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
}
}
// 产生key与index索引对应的一个map表
/**
* [
* {xx: xx, key: 'key0'},
* {xx: xx, key: 'key1'},
* {xx: xx, key: 'key2'}
* ]
* {
* key0: 0,
* key1: 1,
* key2: 2
* }
*/
createKeyToOldIdx(children, beginIdx, endIdx) {
let i, key;
const map = {};
for(i = beginIdx; i <= endIdx; ++i) {
key = children[i].key;
if(isDef(key)) map[key] = i;
}
return map;
}