共计 7295 个字符,预计需要花费 19 分钟才能阅读完成。
Vue- 虚构 DOM
一、模板转换成视图的过程
- Vue.js 通过编译将 template 模板转换成渲染函数 (h),执行渲染函数就能够失去一个虚构节点树。
- 在对 Model 进行操作的时候,会触发对应 Dep 中的 Watcher 对象。Watcher 对象会调用对应的 update 来批改视图。这个过程次要是将新旧虚构节点进行差别比照,而后依据比照后果进行 DOM 操作来更新视图。
二、Virtual DOM
1. 定义
Virtual DOM 其实就是一棵以 VNode 节点作为根底的树,用对象属性来形容节点,实际上它只是一层对实在 DOM 的形象。最终能够通过一系列操作使这棵树映射到实在环境上。
简略来说,能够把 Virtual DOM 了解为一个简略的 JS 对象,其中几个比拟重要的属性:
- tag 属性即这个 vnode 的标签属性
- data 属性蕴含了最初渲染成实在 dom 节点后,节点上的 class,attribute,style 以及绑定的事件
- children 属性是 vnode 的子节点
- text 属性是文本属性
- elm 属性为这个 vnode 对应的实在 dom 节点
- key 属性是 vnode 的标记,在 diff 过程中能够进步 diff 的效率,后文有解说
对于虚构 DOM,咱们来看一个简略的实例,就是下图所示的这个,具体的论述了模板 → 渲染函数 → 虚构 DOM 树 → 实在 DOM 的一个过程
2. 作用
虚构 DOM 的最终目标是将虚构节点渲染到视图上。然而如果间接应用虚构节点笼罩旧节点的话,会有很多不必要的 DOM 操作。例如,一个 ul 标签下很多个 li 标签,其中只有一个 li 有变动,这种状况下如果应用新的 ul 去代替旧的 ul, 因为这些不必要的 DOM 操作而造成了性能上的节约。
为了防止不必要的 DOM 操作,虚构 DOM 在虚构节点映射到视图的过程中,将虚构节点与上一次渲染视图所应用的旧虚构节点(oldVnode)做比照,找出真正须要更新的节点来进行 DOM 操作,从而防止操作其余无需改变的 DOM。
其实虚构 DOM 在 Vue.js 次要做了两件事:
** 提供与实在 DOM 节点所对应的虚构节点 vnode,
将虚构节点 vnode 和旧虚构节点 oldVnode 进行比照,而后更新视图 **
3. 劣势:
- 具备跨平台的劣势: 因为 Virtual DOM 是以 JavaScript 对象为根底而不依赖实在平台环境,所以使它具备了跨平台的能力,比如说浏览器平台、Weex、Node 等。
- 操作 DOM 慢,js 运行效率高咱们能够将 DOM 比照操作放在 JS 层,提高效率: 因为 DOM 操作的执行速度远不如 Javascript 的运算速度快,因而,把大量的 DOM 操作搬运到 Javascript 中,使用 patching 算法来计算出真正须要更新的节点,最大限度地缩小 DOM 操作,从而显著进步性能。
- 晋升渲染性能: Virtual DOM 的劣势不在于单次的操作,而是在大量、频繁的数据更新下,可能对视图进行正当、高效的更新。
三、diff 算法
Vue 的 diff 算法是基于 snabbdom 革新过去的,仅在同级的 vnode 间做 diff,递归地进行同级 vnode 的 diff,最终实现整个 DOM 树的更新。因为跨层级的操作是非常少的,忽略不计,这样工夫复杂度就从 O(n3) 变成 O(n)。
diff 算法包含几个步骤:
- 用 JavaScript 对象构造示意 DOM 树的构造;而后用这个树构建一个真正的 DOM 树,插到文档当中
- 当状态变更的时候,从新结构一棵新的对象树。而后用新的树和旧的树进行比拟,记录两棵树差别
- 把所记录的差别利用到所构建的真正的 DOM 树上,视图就更新了
四、实现代码
1. template 模板转换成渲染函数 (h)
const root = document.getElementById('root');
const oldVnode = h('ul', { id: 'container'},
h('li', { style: { backgroundColor: '#110000'}, key: 'A' }, 'A'),
h('li', { style: { backgroundColor: '#440000'}, key: 'B' }, 'B'),
h('li', { style: { backgroundColor: '#770000'}, key: 'C' }, 'C'),
);
const newVnode = h('ul', { id: 'newContainer'},
h('li', { style: { backgroundColor: '#440000'}, key: 'B' }, 'B1'),
h('li', { style: { backgroundColor: '#110000'}, key: 'A' }, 'A1'),
h('li', { style: { backgroundColor: '#AA0000'}, key: 'C' }, 'C1'),
// h('li', { style: { backgroundColor: '#AA0000'}, key: 'E' }, 'E1'),
);
mount(oldVnode, root);
setTimeout(() => {patch(oldVnode, newVnode);
}, 1000)
2. 通过渲染函数转化为 虚构 DOM 树
function h(type, props, ...children) {let key, newProps = {};
if (props) {if (props.key) {
key = props.key;
delete props.key;
}
for (let item in props) {if (props.hasOwnProperty(item)) {newProps[item] = props[item];
}
}
}
return vnode(type, key, props, children.map(child => {
// 解决文字节点
if (typeof child == 'string' || typeof child == 'number') {return vnode(undefined, undefined, undefined, undefined, child);
}
return child;
}))
}
// 生成 vnode 节点
function vnode(type, key, props = {}, children, text, domElement) {
return {type, key, props, children, text, domElement}
}
3. 渲染节点
1. 把虚构 DOM 节点封装成一个实在 DOM 节点
function createDOMElementFromVnode(vnode) {
let type = vnode.type;
let children = vnode.children;
if (type) {
// 一般节点 eg:div,span
vnode.domElement = document.createElement(vnode.type);
updateProperties(vnode);
if (Array.isArray(children)) {
children.map(child => {return vnode.domElement.appendChild(createDOMElementFromVnode(child))
})
}
} else {
// 文本节点
vnode.domElement = document.createTextNode(vnode.text);
}
return vnode.domElement;
}
2. 更新属性
function updateProperties(vnode, oldProps = {}) {
let newProps = vnode.props;
let domElement = vnode.domElement;
let oldStyle = oldProps.style || {};
let newStyle = newProps.style || {};
// 遍历老属性 (款式和属性),查看是否新属性中存在
for (let item in oldProps) {if (!newProps[item]) {delete domElement[item];
}
}
for (let item in oldStyle) {if (!newStyle[item]) {domElement.style[item] = "";
}
}
// 遍历新属性 款式独自赋值
for (let item in newProps) {if (item == 'style') {let styleObj = newProps[item];
for (let styleItem in styleObj) {domElement.style[styleItem] = styleObj[styleItem];
}
} else {domElement[item] = newProps[item];
}
}
}
3. 渲染节点
function mount(vnode, root) {root.appendChild(createDOMElementFromVnode(vnode));
}
4.patch 更新
1. 新老节点的更新
// 新老节点的更新
function patch(oldVnode, newVnode){
// 节点不同 间接替换
if(oldVnode.type != newVnode.type){return oldVnode.domElement.parentNode.replaceChild(createDOMElementFromVnode(newVnode),oldVnode.domElement);
}
// 如果新节点是文本节点,那么间接批改文本内容
if(newVnode.text){return oldVnode.domElement.textContent = newVnode.text;}
let domElement = newVnode.domElement = oldVnode.domElement;
updateProperties(newVnode, oldVnode.props);
let oldChildren = oldVnode.children;
let newChildren = newVnode.children;
if(oldChildren.length > 0 && newChildren.length>0){
// 新老节点都存在
updateChildren(domElement,oldChildren,newChildren);
}else if(oldChildren.length > 0){
// 老节点存在子节点,新的没有
oldVnode.domElement.innerHTML = '';
}else if(newChildren.length>0){
// 老节点不存在子节点,新的有
for(let i = 0;i< newChildren.length;i++){oldVnode.domElement.appendChild(createDOMElementFromVnode(newChildren[i]));
}
}
}
2. 新老节点都存在,比拟更新
// 新老节点都存在,比拟更新,次要是列表
function updateChildren(parentDOMElement,oldChildren,newChildren){
let oldStartIndex = 0;
let oldStartNode = oldChildren[0];
let oldEndIndex = oldChildren.length-1;
let oldEndNode = oldChildren[oldEndIndex];
let newStartIndex = 0;
let newStartNode = newChildren[0];
let newEndIndex = newChildren.length-1;
let newEndNode = newChildren[newEndIndex];
let oldKeyToIndexMap = createKeyToIndexMap(oldVnode.children);
while(oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex){if(!oldStartNode){
// 如果此节点不存在,间接下移
oldStartNode = oldChildren[++oldStartIndex];
}else if(!oldEndNode){oldEndNode = oldChildren[--oldEndIndex];
}else if(isSameVnode(oldStartNode,newStartNode)){
// 旧头节点 = 新头节点
patch(oldStartNode,newStartNode);
oldStartNode = oldChildren[++oldStartIndex];
newStartNode = newChildren[++newStartIndex];
}else if(isSameVnode(oldEndNode,newEndNode)){
// 旧尾节点 = 新尾节点
patch(oldEndNode,newEndNode);
oldEndNode = oldChildren[--oldEndIndex];
newEndNode = newChildren[--newEndIndex];
}else if(isSameVnode(oldStartNode,newEndNode)){
// 旧头节点 = 新尾节点
patch(oldStartNode,newEndNode);
parentDOMElement.insertBefore(oldStartNode.domElement,oldEndNode.domElement.nextSibling);
oldStartNode = oldChildren[++oldStartIndex];
newEndNode = newChildren[--newEndIndex];
}else if(isSameVnode(oldEndNode,newStartNode)){
// 旧尾节点 = 新头节点
patch(oldEndNode,newStartNode);
parentDOMElement.insertBefore(oldEndNode.domElement,oldStartNode.domElement);
oldEndNode = oldChildren[--oldEndIndex];
newStartNode = newChildren[++newStartIndex];
}else{let oldIndexByKey = oldKeyToIndexMap[newStartNode.key];
if(oldIndexByKey == null){
// 新元素间接创立
parentDOMElement.insertBefore(createDOMElementFromVnode(newStartNode),oldStartNode.domElement);
}else{let oldVnodeToMove = oldChildren[oldIndexByKey];
// console.log(oldKeyToIndexMap,oldIndexByKey,oldVnodeToMove, newStartNode);
if(oldVnodeToMove.type === newStartNode.type){patch(oldVnodeToMove,newStartNode);
oldChildren[oldIndexByKey] = undefined;
oldKeyToIndexMap = createKeyToIndexMap(oldVnode.children); // 避免 key 雷同
parentDOMElement.insertBefore(oldVnodeToMove.domElement,oldStartNode.domElement);
}else{
// key 雷同,type 不同,重建
parentDOMElement.insertBefore(createDOMElementFromVnode(newStartNode),oldStartNode.domElement);
}
}
newStartNode = newChildren[++newStartIndex];
}
}
// 解决残余的新节点
if(oldStartIndex > oldEndIndex){let beforeDOMElement = newChildren[newStartIndex+1] ? newChildren[newStartIndex+1].domElement:null;
for(let i = newStartIndex; i<= newEndIndex;i++){parentDOMElement.insertBefore(createDOMElementFromVnode(newChildren[i]),beforeDOMElement);
}
}
// 删除残余的旧节点
if(newStartIndex > newEndIndex){for(let i = oldStartIndex; i<= oldEndIndex;i++){parentDOMElement.removeChild(oldChildren[i].domElement)
}
}
}
// 获取 key 对应的地位 index
function createKeyToIndexMap(children) {let map = {};
for (let i = 0; i < children.length; i++) {if (children[i] && children[i].key) {map[children[i].key] = i;
}
}
return map;
}
// 是否是雷同的节点 类型雷同并且 key 雷同 key 可能为 null
function isSameVnode(oldVnode, newVnode) {return oldVnode.key === newVnode.key && oldVnode.type === newVnode.type;}