实现mini-vue -- runtime-core模块(三)ShapeFlags + 位运算区分组件类型

257 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第4天,点击查看活动详情

本篇文章会对之前的代码进行重构,主要是重构区分组件类型的逻辑,组件的类型目前已实现的有Component组件类型、Element原生DOM类型,并且子组件也分为TEXT_CHILDREN,即纯文本的子组件,和ARRAY_CHILDREN,数组类型的子组件

由于组件类型的判断将会经常用到,那么实现一个有效且高性能的方式去区分组件类型很有必要,这就是本节要讲的ShapeFlags的作用,可以使用对象的方式去实现,但是为了性能更高,会采用位运算的方式实现

1. 实现 ShapeFlags 进行重构,提升性能

目前我们区分vnode.type是直接通过判断语句中使用typeofisObject去区分vnode的类型的,这种方式有点偏向底层了,可读性不够高,如果能够实现一种机制,给每一个vnode加上一个标识属性,标识它是什么类型的话就好了

这就是ShapeFlags的意义,可以给vnode添加一个shapeFlag属性,根据该属性去判断,提高代码可读性

1.1 使用对象方式实现

可以维护一个ShapeFlags对象,类似下面这样

const ShapeFlags = {
  element: 0,
  stateful_component: 0,
  text_children: 0,
  array_children: 0,
};

初始值是全都为0,当确定了一个vnode的类型的时候,就将对应的flag置为1,比如有一个vnodeElement类型的,那么就可以像这样子判断:

// 设置 shapeFlag
vnode.shapeFlag.element = 1;

// 判断
if (vnode.shapeFlag.element) {
  // 处理 Element 类型的逻辑
}

但其实这种对象的方式不够高效,为了让性能更加高,我们会采用位运算的方式实现


1.2 使用位运算方式实现

可以定义一个枚举,里面存放vnode的类型,但是是通过位运算的方式实现,比如下面这样:

// src/shared/shapeFlags.ts
export const enum ShapeFlags {
  ELEMENT = 1,
  STATEFUL_COMPONENT = 1 << 1,
  TEXT_CHILDREN = 1 << 2,
  ARRAY_CHILDREN = 1 << 3,
}

然后就可以在创建vnode的时候根据type去初始化它的shapeFlag

vnode.shapeFlag = ShapeFlags.ELEMENT;

要判断的时候可以通过与运算**&**去处理

if (vnode.shpaeFlag & ShapeFlags.ELEMENT) {
  // do something...
}

// ELEMENT 0001 & 0001 === 0001 判断通过
// STATEFUL_COMPONENT 0010 & 0001 === 0000 判断不通过

而如果是要添加新的类型,比如已知子vnodeELEMENT类型,并且已知是它里面没有新的子节点了,只有纯文本,也就是TEXT_CHILDREN类型,那么就可以通过或运算**|**的方式去添加

vnode.shapeFlag |= ShapeFlags.TEXT_CHILDREN;

// 0001 | 0100 === 0101 --> 表明既是 ELEMENT 又是 TEXT_CHILDREN 类型

if (vnode.shapeFlag & ShapeFlags.ELEMENT) {
}
// 0101 & 0001 === 0001 --> ELEMENT

if (vnode.shapeFlag & ShapeFlags.TEXT_CHILDREN) {
}
// 0101 & 0100 === 0100 --> TEXT_CHILDREN

那么现在有了ShapeFlags枚举,我们就可以用它去替换vnode类型判断的代码以及新增初始化vnode.shapeFlag的代码了

1.2.1 初始化 vnode.shapeFlag

在创建vnode的时候给vnode添加shapeFlag属性定义

export function createVNode(type, props?, children?) {
  const vnode = {
    type,
    props,
    children,
    shapeFlag: getShapeFlag(type),
    el: null,
  };

  return vnode;
}

function getShapeFlag(type) {
  return typeof type === 'string'
    ? ShapeFlags.ELEMENT
    : ShapeFlags.STATEFUL_COMPONENT;
}

以及需要根据children是文本还是数组去添加shapeFlag的类型

export function createVNode(type, props?, children?) {
  const vnode = {
    type,
    props,
    children,
    shapeFlag: getShapeFlag(type),
    el: null,
  };

+  // 根据 children 的类型添加 vnode 的类型 -- 是 TEXT_CHILDREN 还是 ARRAY_CHILDREN
+  if (typeof children === 'string') {
+    vnode.shapeFlag |= ShapeFlags.TEXT_CHILDREN;
+  } else if (Array.isArray(children)) {
+    vnode.shapeFlag |= ShapeFlags.ARRAY_CHILDREN;
+  }

  return vnode;
}

1.2.2 重构 vnode 类型判断代码

找出原来的判断vnode类型的代码,用vnode.shapeFlag属性去重构

首先是renderer.tspatch函数中有用到,会根据vnodeComponent类型还是Element类型去走不同的分支处理

export function patch(vnode, container) {
- const { type } = vnode;
+ const { type, shapeFlag } = vnode;

- if (typeof type === 'string') {
+ if (shapeFlag & ShapeFlags.ELEMENT) {
    // 真实 DOM
    processElement(vnode, container);
- } else if (isObject(type)) {
+ } else if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
    // 处理 component 类型
    processComponent(vnode, container);
  }
}

然后是mountElement中对children的判断

function mountElement(vnode: any, container: any) {
  // 将 DOM 对象挂载到 vnode 上 从而让组件实例能够访问到
  const el = (vnode.el = document.createElement(vnode.type));
- const { children } = vnode;
+ const { children, shapeFlag } = vnode;

- if (typeof children === 'string') {
+ if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
    el.textContent = children;
- } else if (Array.isArray(children)) {
+ } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
    mountChildren(children, el);
  }

  // props
  const { props } = vnode;
  for (const [key, value] of Object.entries(props)) {
    el.setAttribute(key, value);
  }

  container.append(el);
}

重构完后再build看看,如果不影响之前的功能说明重构没问题啦!