【Vue源码】- 实现渲染函数【template -> 渲染函数render -> 虚拟Dom】

459 阅读4分钟

MVVM框架中的渲染函数是会通过视图模板的编译建立的, 简单的说就是对视图模板进行解析并生成渲染函数。

目录

  1. 什么是虚拟Dom
  2. 什么是渲染函数
  3. DomDiff高效更新视图
  4. 【渲染函数】渲染模块使用渲染函数根据初始化数据生成虚拟Dom -> 利用虚拟Dom创建视图页面Html -> 数据模型一旦变化渲染函数将再次被调用生成新的虚拟Dom,然后做Dom Diff更新视图Html
  5. 【渲染函数】初始化数据 -> render(初始化数据) -> 初始化虚拟DOM -> 初始视图HTML -> 数据变化 -> render(变化数据) -> 新的虚拟DOM -> DOM Diff -> 更新视图HTML
  6. 【渲染函数】视图模板(template) -> 编译为渲染函数(render function) -> 转化为虚拟Dom
    1. 【渲染】template -> 渲染函数render -> 虚拟Dom

一、 什么是虚拟Dom

1) dom

image.png

Dom中节点众多,直接查询和更新Dom性能较差

2) 虚拟Dom

A way of representing the actual DOM with JavaScript Objects. 用JS对象重新表示实际的Dom

image.png

二、 什么是渲染函数

在Vue中我们通过将视图模板(template)编译为渲染函数(render function)再转化为虚拟Dom

image.png

三、 通过DomDiff高效更新视图

image.png

四、 实现渲染函数【关键】

在Vue中我们通过将视图模板(template)编译为渲染函数(render function)再转化为虚拟Dom

image.png

4.1) 渲染流程通常会分为三各部分:

image.png

  • RenderPhase : 渲染模块使用渲染函数根据初始化数据生成虚拟Dom
  • MountPhase : 利用虚拟Dom创建视图页面Html
  • PatchPhase:数据模型一旦变化渲染函数将再次被调用生成新的虚拟Dom,然后做Dom Diff更新视图Html
mount: function (container) {
    const dom = document.querySelector(container);
    const setupResult = config.setup();
    const render = config.render(setupResult);

    let isMounted = false;
    let prevSubTree;
    watchEffect(() => {
      if (!isMounted) {
        dom.innerHTML = "";
        // mount
        isMounted = true;
        const subTree = config.render(setupResult);
        prevSubTree = subTree;
        mountElement(subTree, dom);
      } else {
        // update
        const subTree = config.render(setupResult);
        diff(prevSubTree, subTree);
        prevSubTree = subTree;
      }
    });
  }
4.1.1) 1 Render Phase

渲染模块使用渲染函数根据初始化数据生成虚拟Dom

render(content) {
  return h("div", null, [
    h("div", null, String(content.state.message)),
    h(
      "button",
      {
        onClick: content.click,
      },
      "click"
    ),
  ]);
},

4.1.2) 2 Mount Phase

利用虚拟Dom创建视图页面Html

function mountElement(vnode, container) {
  // 渲染成真实的 dom 节点
  const el = (vnode.el = createElement(vnode.type));

  // 处理 props
  if (vnode.props) {
    for (const key in vnode.props) {
      const val = vnode.props[key];
      patchProp(vnode.el, key, null, val);
    }
  }

  // 要处理 children
  if (Array.isArray(vnode.children)) {
    vnode.children.forEach((v) => {
      mountElement(v, el);
    });
  } else {
    insert(createText(vnode.children), el);
  }

  // 插入到视图内
  insert(el, container);
}
4.1.3) 3 Patch Phase(Dom diff)

数据模型一旦变化渲染函数将再次被调用生成新的虚拟Dom,然后做Dom Diff更新视图Html

function patchProp(el, key, prevValue, nextValue) {
  // onClick
  // 1. 如果前面2个值是 on 的话
  // 2. 就认为它是一个事件
  // 3. on 后面的就是对应的事件名
  if (key.startsWith("on")) {
    const eventName = key.slice(2).toLocaleLowerCase();
    el.addEventListener(eventName, nextValue);
  } else {
    if (nextValue === null) {
      el.removeAttribute(key, nextValue);
    } else {
      el.setAttribute(key, nextValue);
    }
  }
}

通过DomDiff - 高效更新视图

image.png

image.png

function diff(v1, v2) {
  // 1. 如果 tag 都不一样的话,直接替换
  // 2. 如果 tag 一样的话
  //    1. 要检测 props 哪些有变化
  //    2. 要检测 children  -》 特别复杂的
  const { props: oldProps, children: oldChildren = [] } = v1;
  const { props: newProps, children: newChildren = [] } = v2;
  if (v1.tag !== v2.tag) {
    v1.replaceWith(createElement(v2.tag));
  } else {
    const el = (v2.el = v1.el);
    // 对比 props
    // 1. 新的节点不等于老节点的值 -> 直接赋值
    // 2. 把老节点里面新节点不存在的 key 都删除掉
    if (newProps) {
      Object.keys(newProps).forEach((key) => {
        if (newProps[key] !== oldProps[key]) {
          patchProp(el, key, oldProps[key], newProps[key]);
        }
      });

      // 遍历老节点 -》 新节点里面没有的话,那么都删除掉
      Object.keys(oldProps).forEach((key) => {
        if (!newProps[key]) {
          patchProp(el, key, oldProps[key], null);
        }
      });
    }
    // 对比 children

    // newChildren -> string
    // oldChildren -> string   oldChildren -> array

    // newChildren -> array
    // oldChildren -> string   oldChildren -> array
    if (typeof newChildren === "string") {
      if (typeof oldChildren === "string") {
        if (newChildren !== oldChildren) {
          setText(el, newChildren);
        }
      } else if (Array.isArray(oldChildren)) {
        // 把之前的元素都替换掉
        v1.el.textContent = newChildren;
      }
    } else if (Array.isArray(newChildren)) {
      if (typeof oldChildren === "string") {
        // 清空之前的数据
        n1.el.innerHTML = "";
        // 把所有的 children mount 出来
        newChildren.forEach((vnode) => {
          mountElement(vnode, el);
        });
      } else if (Array.isArray(oldChildren)) {
        // a, b, c, d, e -> new
        // a1,b1,c1,d1 -> old
        // 如果 new 的多的话,那么创建一个新的

        // a, b, c -> new
        // a1,b1,c1,d1 -> old
        // 如果 old 的多的话,那么把多的都删除掉
        const length = Math.min(newChildren.length, oldChildren.length);
        for (let i = 0; i < length; i++) {
          const oldVnode = oldChildren[i];
          const newVnode = newChildren[i];
          // 可以十分复杂
          diff(oldVnode, newVnode);
        }

        if (oldChildren.length > length) {
          // 说明老的节点多
          // 都删除掉
          for (let i = length; i < oldChildren.length; i++) {
            remove(oldChildren[i], el);
          }
        } else if (newChildren.length > length) {
          // 说明 new 的节点多
          // 那么需要创建对应的节点
          for (let i = length; i < newChildren.length; i++) {
            mountElement(newChildren[i], el);
          }
        }
      }
    }
  }
}

参考

总结

  • 虚拟Dom - 用JS对象重新表示实际的Dom

  • 【渲染函数过程】 渲染模块使用渲染函数根据初始化数据生成虚拟Dom -> 利用虚拟Dom创建视图页面Html -> 数据模型一旦变化渲染函数将再次被调用生成新的虚拟Dom,然后做Dom Diff更新视图Html

  • 【渲染函数过程】 初始化数据 -> render(初始化数据) -> 初始化虚拟DOM -> 初始视图HTML -> 数据变化 -> render(变化数据) -> 新的虚拟DOM -> DOM Diff -> 更新视图HTML

  • 【渲染函数】视图模板(template) -> 编译为渲染函数(render function) -> 转化为虚拟Dom

  • 【渲染】template -> 渲染函数render -> 虚拟Dom

  • MVVM框架中的渲染函数是会通过视图模板的编译建立的, 简单的说就是对视图模板进行解析并生成渲染函数。