1. import {compileToFunction} from './compiler/index.js';
  2. import { patch,createElm } from './vdom/patch';
  3. // 1.创建第一个虚拟节点
  4. let vm1 = new Vue({data:{name:'zf'}});
  5. let render1 = compileToFunction('<div>{{name}}</div>')
  6. let oldVnode = render1.call(vm1)
  7. // 2.创建第二个虚拟节点
  8. let vm2 = new Vue({data:{name:'jw'}});
  9. let render2 = compileToFunction('<p>{{name}}</p>');
  10. let newVnode = render2.call(vm2);
  11. // 3.通过第一个虚拟节点做首次渲染
  12. let el = createElm(oldVnode)
  13. document.body.appendChild(el);
  14. // 4.调用patch方法进行对比操作,尽量复用老的
  15. patch(oldVnode,newVnode);

我们想掌握vue中的diff算法就先构建出两个虚拟dom 之后做patch

【5】dom diff 解析 - 图1

一.基本Diff算法

1.比对标签

  1. // 如果标签不一致说明是两个不同元素
  2. if(oldVnode.tag !== vnode.tag){
  3. oldVnode.el.parentNode.replaceChild(createElm(vnode),oldVnode.el)
  4. }

在diff过程中会先比较标签是否一致,如果标签不一致用新的标签替换掉老的标签

  1. // 如果标签一致但是不存在则是文本节点
  2. if(!oldVnode.tag){
  3. if(oldVnode.text !== vnode.text){
  4. oldVnode.el.textContent = vnode.text;
  5. }
  6. }

如果标签一致,有可能都是文本节点,那就比较文本的内容即可

2.比对属性

  1. // 复用标签,并且更新属性
  2. let el = vnode.el = oldVnode.el;
  3. updateProperties(vnode,oldVnode.data);
  4. function updateProperties(vnode,oldProps={}) {
  5. let newProps = vnode.data || {};
  6. let el = vnode.el;
  7. // 比对样式
  8. let newStyle = newProps.style || {};
  9. let oldStyle = oldProps.style || {};
  10. for(let key in oldStyle){
  11. if(!newStyle[key]){
  12. el.style[key] = ''
  13. }
  14. }
  15. // 删除多余属性
  16. for(let key in oldProps){
  17. if(!newProps[key]){
  18. el.removeAttribute(key);
  19. }
  20. }
  21. for (let key in newProps) {
  22. if (key === 'style') {
  23. for (let styleName in newProps.style) {
  24. el.style[styleName] = newProps.style[styleName];
  25. }
  26. } else if (key === 'class') {
  27. el.className = newProps.class;
  28. } else {
  29. el.setAttribute(key, newProps[key]);
  30. }
  31. }
  32. }

当标签相同时,我们可以复用老的标签元素,并且进行属性的比对


3.比对子元素

  1. // 比较孩子节点
  2. let oldChildren = oldVnode.children || [];
  3. let newChildren = vnode.children || [];
  4. // 新老都有需要比对儿子
  5. if(oldChildren.length > 0 && newChildren.length > 0){
  6. // 老的有儿子新的没有清空即可
  7. }else if(oldChildren.length > 0 ){
  8. el.innerHTML = '';
  9. // 新的有儿子
  10. }else if(newChildren.length > 0){
  11. for(let i = 0 ; i < newChildren.length ;i++){
  12. let child = newChildren[i];
  13. el.appendChild(createElm(child));
  14. }
  15. }

这里要判断新老节点儿子的状况

  1. if (oldChildren.length > 0 && newChildren.length > 0) {
  2. updateChildren(el, oldChildren, newChildren)
  3. // 老的有儿子新的没有清空即可
  4. }

二.Diff中的优化策略

1.在开头和结尾新增元素

  1. function isSameVnode(oldVnode,newVnode){
  2. // 如果两个人的标签和key 一样我认为是同一个节点 虚拟节点一样我就可以复用真实节点了
  3. return (oldVnode.tag === newVnode.tag) && (oldVnode.key === newVnode.key)
  4. }
  5. function updateChildren(parent, oldChildren, newChildren) {
  6. let oldStartIndex = 0;
  7. let oldStartVnode = oldChildren[0];
  8. let oldEndIndex = oldChildren.length - 1;
  9. let oldEndVnode = oldChildren[oldEndIndex];
  10. let newStartIndex = 0;
  11. let newStartVnode = newChildren[0];
  12. let newEndIndex = newChildren.length - 1;
  13. let newEndVnode = newChildren[newEndIndex];
  14. while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
  15. // 优化向后追加逻辑
  16. if(isSameVnode(oldStartVnode,newStartVnode)){
  17. patch(oldStartVnode,newStartVnode);
  18. oldStartVnode = oldChildren[++oldStartIndex];
  19. newStartVnode = newChildren[++newStartIndex];
  20. // 优化向前追加逻辑
  21. }else if(isSameVnode(oldEndVnode,newEndVnode)){
  22. patch(oldEndVnode,newEndVnode); // 比较孩子
  23. oldEndVnode = oldChildren[--oldEndIndex];
  24. newEndVnode = newChildren[--newEndIndex];
  25. }
  26. }
  27. if(newStartIndex <= newEndIndex){
  28. for(let i = newStartIndex ; i<=newEndIndex ;i++){
  29. let ele = newChildren[newEndIndex+1] == null? null:newChildren[newEndIndex+1].el;
  30. parent.insertBefore(createElm(newChildren[i]),ele);
  31. }
  32. }
  33. }

2.头移尾、尾移头

  1. // 头移动到尾部
  2. else if(isSameVnode(oldStartVnode,newEndVnode)){
  3. patch(oldStartVnode,newEndVnode);
  4. parent.insertBefore(oldStartVnode.el,oldEndVnode.el.nextSibling);
  5. oldStartVnode = oldChildren[++oldStartIndex];
  6. newEndVnode = newChildren[--newEndIndex]
  7. // 尾部移动到头部
  8. }else if(isSameVnode(oldEndVnode,newStartVnode)){
  9. patch(oldEndVnode,newStartVnode);
  10. parent.insertBefore(oldEndVnode.el,oldStartVnode.el);
  11. oldEndVnode = oldChildren[--oldEndIndex];
  12. newStartVnode = newChildren[++newStartIndex]
  13. }

以上四个条件对常见的dom操作进行了优化

3.暴力比对

  1. function makeIndexByKey(children) {
  2. let map = {};
  3. children.forEach((item, index) => {
  4. map[item.key] = index
  5. });
  6. return map;
  7. }
  8. let map = makeIndexByKey(oldChildren);

对所有的孩子元素进行编号

  1. let moveIndex = map[newStartVnode.key];
  2. if (moveIndex == undefined) { // 老的中没有将新元素插入
  3. parent.insertBefore(createElm(newStartVnode), oldStartVnode.el);
  4. } else { // 有的话做移动操作
  5. let moveVnode = oldChildren[moveIndex];
  6. oldChildren[moveIndex] = undefined;
  7. parent.insertBefore(moveVnode.el, oldStartVnode.el);
  8. patch(moveVnode, newStartVnode);
  9. }
  10. newStartVnode = newChildren[++newStartIndex]

用新的元素去老的中进行查找,如果找到则移动,找不到则直接插入

  1. if(oldStartIndex <= oldEndIndex){
  2. for(let i = oldStartIndex; i<=oldEndIndex;i++){
  3. let child = oldChildren[i];
  4. if(child != undefined){
  5. parent.removeChild(child.el)
  6. }
  7. }
  8. }

如果有剩余则直接删除

  1. if(!oldStartVnode){
  2. oldStartVnode = oldChildren[++oldStartIndex];
  3. }else if(!oldEndVnode){
  4. oldEndVnode = oldChildren[--oldEndIndex]
  5. }

在比对过程中,可能出现空值情况则直接跳过

三.更新操作

  1. Vue.prototype._update = function (vnode) {
  2. const vm = this;
  3. const prevVnode = vm._vnode; // 保留上一次的vnode
  4. vm._vnode = vnode;
  5. if(!prevVnode){
  6. vm.$el = patch(vm.$el,vnode); // 需要用虚拟节点创建出真实节点 替换掉 真实的$el
  7. // 我要通过虚拟节点 渲染出真实的dom
  8. }else{
  9. vm.$el = patch(prevVnode,vnode); // 更新时做diff操作
  10. }
  11. }