实现Vue-tiny-diff算法

前言

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

state -> vdom -> dom

vNode 层

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

{
  tag, props, children;
}

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

export function h(tag, props, children) {
  return {
    tag,
    props,
    children,
  };
}

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

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

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

let subTree = rootComponent.render(context);
mountElement(subTree, rootContainer);

mountElement 的实现逻辑

  1. 根据标签创建元素
  2. 更新属性
  3. 如果子节点为文本节点,直接创建, 若为数组,则递归创建
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来将挂载和更新两阶段分开

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 上

export function mountElement(vNode, container) {
  // ...
  let ele = (vNode.el = document.createElement(tag));
  // ...
}
  1. tag 变化的处理 ,这里用到了原生的replaceWith操作方法
if (newTree.tag !== oldTree.tag) {
  oldTree.el.replaceWith(document.createElement(newTree.tag));
}
  1. props 节点的处理
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 种情况

前三种比较简单

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)) {
    // ...
  }
}

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

// 暴力解法: 只对节点的长度作处理,不处理相同长度内的节点移位操作
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前端开发笔记,由于笔者能力有限,文章难免有疏漏之处,欢迎指正

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值