实现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前端开发笔记,由于笔者能力有限,文章难免有疏漏之处,欢迎指正

相关推荐
Martin -Tang21 分钟前
vite和webpack的区别
前端·webpack·node.js·vite
迷途小码农零零发22 分钟前
解锁微前端的优秀库
前端
王解1 小时前
webpack loader全解析,从入门到精通(10)
前端·webpack·node.js
老码沉思录1 小时前
写给初学者的React Native 全栈开发实战班
javascript·react native·react.js
我不当帕鲁谁当帕鲁1 小时前
arcgis for js实现FeatureLayer图层弹窗展示所有field字段
前端·javascript·arcgis
那一抹阳光多灿烂1 小时前
工程化实战内功修炼测试题
前端·javascript
爱吃生蚝的于勒1 小时前
C语言内存函数
c语言·开发语言·数据结构·c++·学习·算法
放逐者-保持本心,方可放逐2 小时前
微信小程序=》基础=》常见问题=》性能总结
前端·微信小程序·小程序·前端框架
毋若成4 小时前
前端三大组件之CSS,三大选择器,游戏网页仿写
前端·css
红中马喽4 小时前
JS学习日记(webAPI—DOM)
开发语言·前端·javascript·笔记·vscode·学习