实现Vue-tiny-diff算法

前言

前面我们实现了基本的数据更新到视图渲染的逻辑,但是这种方式(innerHTML)是极其低效的, 因此,我们相应引入 dom 和 diff 算法, 数据到视图的过程变为:

state -> vdom -> dom

vNode 层

所谓 vNode, 就是一个表示 dom 结构的轻量对象

js 复制代码
{
  tag, props, children;
}

为了方便创建, 引入创建一个创建节点的方法h

js 复制代码
export function h(tag, props, children) {
  return {
    tag,
    props,
    children,
  };
}

我们需要修改 render 函数, 让其返回一个创建好的 vNode(vTree)

js 复制代码
render(context) {
    return h(
      'div',
      {
        id: 'id-1',
        class: 'class-1'
      },
      [h('p', null, String(context.value)), h('p', null, String(context.value))]
    )
  },

接下来对返回的 vTree 挂载到真实的节点

js 复制代码
let subTree = rootComponent.render(context);
mountElement(subTree, rootContainer);

mountElement 的实现逻辑

  1. 根据标签创建元素
  2. 更新属性
  3. 如果子节点为文本节点,直接创建, 若为数组,则递归创建
js 复制代码
export function mountComponent(vnode, container) {
  const { tag, props, children } = vnode;
  // tag
  let ele = document.createElement(tag);
  // props
  for (const key in props) {
    if (Object.hasOwnProperty.call(props, key)) {
      const value = props[key];
      ele.setAttribute(key, value);
    }
  }
  /* children
        1. string
        2. object
    */
  if (typeof children === "string") {
    const textNode = document.createTextNode(children);
    ele.appendChild(textNode);
  } else if (isArray(children)) {
    children.forEach((vnode) => {
      mountComponent(vnode, ele);
    });
  }
  container.appendChild(ele);
}

function isArray(ele) {
  return typeof ele.sort === "function";
}

diff 算法

除了第一次挂载需要生成所有节点以外, 新的更新是在旧的基础上"缝缝补补", 这个差量更新的过程交给我们的 diff 算法

我们用一个变量isMounted来将挂载和更新两阶段分开

js 复制代码
export default function createApp(rootComponent) {
  return {
    mount(rootContainer) {
      let context = rootComponent.setup();
      let isMounted = false;
      let oldSubTree;
      effectWatch(() => {
        if (!isMounted) {
          isMounted = true;
          let subTree = (oldSubTree = rootComponent.render(context));
          mountElement(subTree, rootContainer);
        } else {
          let newSubTree = rootComponent.render(context);
          diff(newSubTree, oldSubTree);
          oldSubTree = newSubTree;
        }
      });
    },
  };
}

接下来我们就可以处理diff的逻辑了, 需要分别对tag,props,children的变更做处理,

因为 diff 的郭恒要对真实的 dom 节点进行操作, 在 mounted 过程中将 dom 渲染完成后,我们需要将其挂载到对应的 vNode 上

js 复制代码
export function mountElement(vNode, container) {
  // ...
  let ele = (vNode.el = document.createElement(tag));
  // ...
}
  1. tag 变化的处理 ,这里用到了原生的replaceWith操作方法
js 复制代码
if (newTree.tag !== oldTree.tag) {
  oldTree.el.replaceWith(document.createElement(newTree.tag));
}
  1. props 节点的处理
js 复制代码
newTree.el = oldTree.el;
// props, 对比两个对象, 各自遍历一遍,找出各自不同的地方
let { props: newProps } = newTree;
let { props: oldProps } = oldTree;
if (newProps && oldProps) {
  Object.keys(newProps).forEach((key) => {
    // 同时存在,意味着需要更新节点
    let newVal = newProps[key];
    if (Object.hasOwnProperty.call(oldProps, key)) {
      let oldVal = oldProps[key];
      if (newVal !== oldVal) {
        newTree.el.setAttribute(key, newVal);
      }
    } else {
      // 旧的不存在, 创建
      newTree.el.setAttribute(key, newVal);
    }
  });
}
// 移除已不存在的旧节点
if (oldProps) {
  Object.keys(oldProps).forEach((key) => {
    if (!Object.hasOwnProperty.call(newProps, key)) {
      newTree.el.removeAttribute(key);
    }
  });
}

当然, 为了演示, 这里的处理过程比较简单,

  1. children 的处理

chilren 的处理相对比较麻烦,为了简化, 目前根据 children 的类型区分

即: newChildren[string, array] * oldChildren[array, string] = 4 种情况

前三种比较简单

js 复制代码
let { children: oldChildren } = oldTree;
let { children: newChildren } = newTree;
if (typeof newChildren === "string") {
  if (typeof oldChildren === "string") {
    if (newChildren !== oldChildren) {
      newTree.el.textContent = newChildren;
    }
  } else if (isArray(oldChildren)) {
    newTree.el.textContent = newChildren;
  }
} else if (isArray(newChildren)) {
  if (typeof oldChildren === "string") {
    newTree.el.textContent = ``;
    mountElement(newTree, newTree.el);
  } else if (Array.isArray(oldChildren)) {
    // ...
  }
}

下面分析两者都是数组的情况, 为了简化, 只对节点的长度作处理,不处理相同长度内的节点移位操作

js 复制代码
// 暴力解法: 只对节点的长度作处理,不处理相同长度内的节点移位操作
const length = Math.min(newChildren.length, oldChildren.length);
// 更新相同长度的部分
for (var index = 0; index < length; index++) {
  let newTree = newChildren[index];
  let oldTree = oldChildren[index];
  diff(newTree, oldTree);
}
// 创建
if (newChildren.length > oldChildren.length) {
  for (let index = length; index < newChildren.length; index++) {
    const newVNode = newChildren[index];
    mountElement(newVNode, newTree.el);
  }
}
// 删除
if (oldChildren.length > newChildren.length) {
  for (let index = length; index < oldChildren.length; index++) {
    const vNode = oldChildren[index];
    vNode.el.remove(); // 节点移除自身
  }
}

本文首发于个人 Github前端开发笔记,由于笔者能力有限,文章难免有疏漏之处,欢迎指正

相关推荐
GDAL3 分钟前
HTML 中的 Canvas 样式设置全解
javascript
m0_528723819 分钟前
HTML中,title和h1标签的区别是什么?
前端·html
Dark_programmer9 分钟前
html - - - - - modal弹窗出现时,页面怎么能限制滚动
前端·html
MZWeiei14 分钟前
PTA:运用顺序表实现多项式相加
算法
GDAL15 分钟前
HTML Canvas clip 深入全面讲解
前端·javascript·canvas
禾苗种树16 分钟前
在 Vue 3 中使用 ECharts 制作多 Y 轴折线图时,若希望 **Y 轴颜色自动匹配折线颜色**且无需手动干预,可以通过以下步骤实现:
前端·vue.js·echarts
GISer_Jing21 分钟前
Javascript排序算法(冒泡排序、快速排序、选择排序、堆排序、插入排序、希尔排序)详解
javascript·算法·排序算法
cookies_s_s21 分钟前
Linux--进程(进程虚拟地址空间、页表、进程控制、实现简易shell)
linux·运维·服务器·数据结构·c++·算法·哈希算法
贵州数擎科技有限公司37 分钟前
使用 Three.js 实现流光特效
前端·webgl
JustHappy39 分钟前
「我们一起做组件库🌻」做个面包屑🥖,Vue的依赖注入实战💉(VersakitUI开发实录)
前端·javascript·github