vue3-runtime(二) patch

121 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第16天,点击查看活动详情

这期讲重渲染后生成的新VNode树,和旧树对比去更新dom视图,也就是广义的diff算法,patch函数

patch流程

新节点不存在:删除旧节点

比对:

类型不同 :删除旧节点,挂载新节点

相同类型:若旧节点不存在则渲染新节点,存在则更新

[属性不同:替换更新] [子节点不同:patchChildren]

patch.png

改造render函数

使之拥有旧节点值,便于比对

export function render(vnode, container) {//传入新生成的vnode
    // 获取容器上的旧vnode
  const prevVNode = container._vnode;
    //没有新节点,说明删了
  if (!vnode) {
    if (prevVNode) {
      unmount(prevVNode);
    }
  } else {
      // 两者都有,比对
    patch(prevVNode, vnode, container);
  }
    // 渲染后 将vode存放在容器上
  container._vnode = vnode;
}

简单流程

function patch(n1, n2, container, anchor) {
  if (n1 && !isSameVNodeType(n1, n2)) {
    unmount(n1);
    n1 = null;
  }
    // 先不看anchor 
  const { shapeFlag } = n2;
  if (shapeFlag & ShapeFlags.ELEMENT) {
      // 元素节点 比较
    processElement(n1, n2, container, anchor);
  } else if (shapeFlag & ShapeFlags.TEXT) {
    processText(n1, n2, container, anchor);
  } else if (shapeFlag & ShapeFlags.FRAGMENT) {
    processFragment(n1, n2, container, anchor);
  } else if (shapeFlag & ShapeFlags.COMPONENT) {
    processComponent(n1, n2, container, anchor);
  }
}

function processElement(n1, n2, container, anchor) {
    // 旧节点不存在,则渲染新节点
  if (n1 == null) {
    mountElement(n2, container, anchor);
  } else {
      //存在,则替换更新
    patchElement(n1, n2);
  }
}
// 替换更新属性 子节点
function patchElement(n1, n2) {
  n2.el = n1.el;
  patchProps(n2.el, n1.props, n2.props);
  patchChildren(n1, n2, n2.el);
}

patchProps

和上一章的渲染属性类似,只是这次加入比较区别

传入的属性是对象,需要遍历新传入的对象属性,和旧的值比较;再遍历旧的值,如果其中在新对象中不存在,则要去元素中把相应的属性给删掉

export function patchProps(el, oldProps, newProps) {
  if (oldProps === newProps) {
    return;
  }
  // 可能为null,null就不能赋值默认参数了,undefined才有效
  oldProps = oldProps || {};
  newProps = newProps || {};
  // 对象,所以赋值新属性覆盖,去掉老属性中新属性没的属性。
  for (const key in newProps) {
    if (key === 'key') {
      continue;
    }
    const prev = oldProps[key];
    const next = newProps[key];
    if (prev !== next) {
      patchDomProp(el, key, prev, next);
    }
  }
  for (const key in oldProps) {
    if (key !== 'key' && !(key in newProps)) {
      patchDomProp(el, key, oldProps[key], null);
    }
  }
}

const domPropsRE = /[A-Z]|^(value|checked|selected|muted|disabled)$/;
function patchDomProp(el, key, prev, next) {
  switch (key) {
    case 'class':
      // 暂时认为class就是字符串
      // next可能为null,会变成'null',因此要设成''
      el.className = next || '';
      break;
    case 'style':
      // style为对象,如果样式删除
      if (!next) {
        el.removeAttribute('style');
      } else {
        //  对象,所以赋值新样式覆盖,去掉老样式中新样式没的属性。
        for (const styleName in next) {
          el.style[styleName] = next[styleName];
        }
        if (prev) {
          for (const styleName in prev) {
            if (next[styleName] == null) {
              el.style[styleName] = '';
            }
          }
        }
      }
      break;
    default:
      if (/^on[^a-z]/.test(key)) {
        // 事件
        if (prev !== next) {
          const eventName = key.slice(2).toLowerCase();
          if (prev) {
            el.removeEventListener(eventName, prev);
          }
          if (next) {
            el.addEventListener(eventName, next);
          }
        }
      } else if (domPropsRE.test(key)) {
        if (next === '' && typeof el[key] === 'boolean') {
          next = true;
        }
        el[key] = next;
      } else {
        // 例如自定义属性{custom: ''},应该用setAttribute设置为<input custom />
        // 而{custom: null},应用removeAttribute设置为<input />
        if (next == null || next === false) {
          el.removeAttribute(key);
        } else {
          el.setAttribute(key, next);
        }
      }
      break;
  }
}

patchChildren

新节点若是文本节点,和旧类型一样则更新内容,不一样则 unmount旧节点

新节点若是数组节点,旧节点是文本则删除渲染新,一样类型则继续patch,旧节点不存在则挂载

新节点若不存在,删除旧节点

patchChildren.jpg vue 源码的 patchChildren 结构

if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
  if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
    unmountChildren(c1);
  }
  if (c2 !== c1) {
    container.textContent = c2;
  }
} else {
  // c2 is array or null

  if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
    // c1 was array

    if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
      // c2 is array
      // patchArrayChildren()
    } else {
      // c2 is null
      unmountChildren(c1);
    }
  } else {
    // c1 was text or null

    if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
      container.textContent = '';
    }
    if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
      mountChildren(c2, container, anchor);
    }
  }
}

其中 unmount代码,删除不是简单 remove就行,因为 fragment加了两个空白文本节点

function unmount(vnode) {
  const { shapeFlag, el } = vnode;
  if (shapeFlag & ShapeFlags.COMPONENT) {
    unmountComponent(vnode);
  } else if (shapeFlag & ShapeFlags.FRAGMENT) {
    unmountFragment(vnode);
  } else {
    el.parentNode.removeChild(el);
  }
}
function unmountFragment(vnode) {
  // eslint-disable-next-line prefer-const
  // el 首节点,详情看下面的内容
  let { el: cur, anchor: end } = vnode;
  while (cur !== end) {
    const next = cur.nextSibling;
    cur.parentNode.removeChild(cur);
    cur = next;
  }
  end.parentNode.removeChild(end);
}

patchArrayChildren

下期讲

Fragment 的问题

render(
  h('ul', null, [
    h('li', null, 'first'),
    h(Fragment, null, []),
    h('li', null, 'last'),
  ]),
  document.body
);
setTimeout(() => {
  render(
    h('ul', null, [
      h('li', null, 'first'),
      h(Fragment, null, [h('li', null, 'middle')]),
      h('li', null, 'last'),
    ]),
    document.body
  );
}, 2000);

middle 被放在了最后面。 原因是在 mountElement 中,使用了 container.appendChild

所以要添加 anchor属性

anchor 是 Fragment 的专有属性

在 Fragment 位置 前后加入空的文本节点,占位,保证不会把元素插错位置

function processFragment(n1, n2, container, anchor) {
  const fragmentStartAnchor = (n2.el = n1
    ? n1.el
    : document.createTextNode(''));
  const fragmentEndAnchor = (n2.anchor = n1
    ? n1.anchor
    : document.createTextNode(''));
    // fragment没有el,新搞el作为第一个空白文本节点,anchor表示尾节点
  if (n1 == null) {
    // 表示新节点多了个fragment,插入两个空白文本节点占位,一开始anchor为空,相当于appendChild
    container.insertBefore(fragmentStartAnchor, anchor);
    container.insertBefore(fragmentEndAnchor, anchor);
    // 这样子 挂载时就插入到 第二个空白节点前,就不会插入到父元素最后的元素去了
    mountChildren(n2.children, container, fragmentEndAnchor);
  } else {
    patchChildren(n1, n2, container, fragmentEndAnchor);
  }
}

function mountChildren(children, container, anchor) {
  children.forEach((child) => {
    patch(null, child, container, anchor);
      // 因为n1 == null,走 mountElement(n2, container, anchor);
  });
}

还有就是 旧节点换成fragment节点时(或fragment换成普通节点),anchor的位置很重要

h1, "" h1 "", h1
h1, h2, h1
anchor等于最后一个h1,把h2插入h1前。
function patch(n1, n2, container, anchor) {
  if (n1 && !isSameVNodeType(n1, n2)) {
    // n1被卸载后,n2将会创建,因此anchor至关重要。需要将它设置为n1的下一个兄弟节点
    anchor = (n1.anchor || n1.el).nextSibling;
。。。
  }
    。。。
}
function processFragment(n1, n2, container, anchor) {
  const fragmentEndAnchor = (n2.anchor = n1
    ? n1.anchor
    : document.createTextNode(''));
  if (n1 == null) {
。。。
  } else {
    patchChildren(n1, n2, container, fragmentEndAnchor);
  }
}

function patchChildren(n1, n2, container, anchor) {
      if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
        mountChildren(c2, container, anchor);
      }
}