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

相关推荐
迷雾漫步者1 小时前
Flutter组件————FloatingActionButton
前端·flutter·dart
向前看-2 小时前
验证码机制
前端·后端
燃先生._.3 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
XH华3 小时前
初识C语言之二维数组(下)
c语言·算法
南宫生4 小时前
力扣-图论-17【算法学习day.67】
java·学习·算法·leetcode·图论
不想当程序猿_4 小时前
【蓝桥杯每日一题】求和——前缀和
算法·前缀和·蓝桥杯
高山我梦口香糖4 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
m0_748235244 小时前
前端实现获取后端返回的文件流并下载
前端·状态模式
落魄君子4 小时前
GA-BP分类-遗传算法(Genetic Algorithm)和反向传播算法(Backpropagation)
算法·分类·数据挖掘