对象的高效修改与查询--位运算

102 阅读4分钟

在看虚拟节点一节中,可以看到虚拟节点是涉及到4种类型的;

背景:由虚拟节点开始🤔

一种 我们直接调用h函数创建虚拟节点

function h(type, props?, children?) {
    return createVNode(type, props, children)
}

function createVNode(type, props, children) {
    var vnode = {
        type: type,
        props: props,
        children: children,
    };
    return vnode;
}
// 调用h创建虚拟节点
h(
    'div', // type
    {id: 'root', class: ['red', 'blue']}, // porps
    [
          h("p", { class: 'red'}, "hi"),
          h("p", { class: 'blue'}, "mini-vue")
     ] // children
     // chilren也可能是简单的文本 比方<div id="root">这里是chilrend就是普通文本</div>
)

第二种 创建app时传入根组件也会被直接转换成虚拟节点-》component -> vnode

// 其次 createApp时传入【根组件】时也会直接转换成虚拟节点
function createApp (rootComponent) {
    return {
        // rootContainer根容器
        mount(rootContainer) {
            // 把所有的东西转换成vnode 后续素有的逻辑操作都会基于 vnode 做处理
            // component -> vnode
            const vnode = createVNode(rootComponent)
            render(vnode, rootContainer)
        }
    }
}
export const App = {
  // 平时我们写的是.vue类的文件 <template></template> 这是需要有编译功能的这里暂时不考虑这个 
  // 我们用render函数‘ template最终其实也会被编译成render函数被其处理
  render() {
    return h('div',{
      id: 'root',
      class: ['red', 'blue']
    },
    "hello world"
    )
  },
  setup() {
    return {
      msg: 'mini-vue 哈哈哈'
    }
  },
}

那么此时createVNode中的type类型就会有两种 一种是普通的div即普通element其typeof为string; 第二种传入的是一个App带有状态的component其typeof为object

接着考虑其children 有可能是普通的一段文本string; 也可能是数组array需要继续patch递归

因此我们就写出了如下的代码

function patch(vnode, container) {
    // 去处理组件
    // 判断虚拟节点类型 preocessComponent / processElement
    // 判断是不是element
    if (typeof vnode.type === "string") {
      processElement(vnode, container);
    } else if (isObject(vnode.type)) {
      processComponent(vnode, container)
    }

}

function mountElement(vnode: any, container: any) {
  // 看vnode.ts中 vnode = { type, props, chidren }
  // const el = document.createElement('div') // type
  // el.setAttribute('id', 'root') // props
  // // chidren 涉及两种类型 一种string类型; 一种array类型
  // el.textContent = 'hi mini-vue'

  // document.body.appendChild(el);

  const { type, props, children } = vnode
  // 尝试将el存储起来 有多个h会有多个el; 但是只有type为div,id为root的才是render的得到的第一层里的el会被最终$el读取
  const el = (vnode.el =  document.createElement(type)) // type
  for (const key in props) {
    const val = props[key]
    el.setAttribute(key, val)
  }
  if (typeof children === 'string') {
    el.textContent = children
  } else if (Array.isArray(children)) {
    mountChildren(vnode, el)
  }

  container.appendChild(el)

}

由于我们已经知道虚拟节点创建到渲染,type和children会涉及4中元素,这时为了逻辑统一,我们一般会这样写

一、有限属性--对象属性的修改和读取

const shapeFlags = {
    ELEMENT: 0,
    STATEFUl_COMPONENT: 0,
    TEXT_CHILDREN: 0,
    ARRAY_CHILDREN: 0
}
// 将shapeFlags添加到vnode上
function createVNode(type, props, children) {
    var vnode = {
        type: type,
        props: props,
        children: children,
        shapeFlags: shapeFlags, // 这里是一个对象
    };
    function getShapsFlags(type) {
        typeof type === 'string' ? ShapeFlags.ELEMENT = 1 : ShapeFlags.STATEFUl_COMPONENT = 1
        return ShapeFlags
    }
    typeof children === 'string' ? shapeFlags.TEXT_CHILDREN = 1 : shapeFlags.ARRAY_CHILDREN = 1
    return vnode;
}

1. 设值 修改

// 如果此时进来的虚拟节点的type是stateful_component
shapeFlags.STATEFUl_COMPONENT = 1
// 如果此时进来的虚拟节点的type是stateful_component; 且children是array_children
shapeFlags.STATEFUl_COMPONENT = 1
shapeFlags.ARRAY_CHILDREN = 1

2. 读取

function patch(vnode, container) {
    const { shapeFlags } = vnode
    if (shapeFlags.ELEMENT) {
      processElement(vnode, container);
    } else if (shapeFlags.STATEFUl_COMPONENT) {
      processComponent(vnode, container)
    }
}
function mountElement(vnode: any, container: any) {
   // ……
  const { type, props, children, shapeFlags } = vnode
  // 其他……
  if (shapeFlags.TEXT_CHILDREN) {
    el.textContent = children
  } else if (shapeFlags.ARRAY_CHILDREN) {
    mountChildren(vnode, el)
  }
  container.appendChild(el)
}

二、 更高效的方式--位运算

// 0000 -> 默认
// 0001 -> element类型
// 0010 -> stateful_component类型
// 0100 -> text_children类型
// 1000 -> array_children类型
// 此时 如果节点是element类型,chilren是array_children类型;那就是
// -> 1001

1. 位运算的学习

1.1 按位与:&

um1 & num2:转为二进制,按位相与 同1则1, 符号位参与运算

&&运算符我们都知道,只有两个都为真,结果才为真。 &道理是一样的,只有两个数的值为1时,才返回1。例如1和3的按位与操作:

    0001 
&   0011 
--------- 
    0001

1.2 ## 按位或 |

~num:转换为二进制按位取反 0变1,1变0, 符号位参与运算

与||操作符的道理也是一样的,只要两个数中有一个数为1,结果就为1,其他则为0。


			    0001
			 |  0011
			---------
			    0011

2. 改造上述

所以我们要设置值时

2.1 设值 修改

用|同为0才是0

0001 -> 1001
那就是 00011000
    ------
      1001
``

### 2.2 查找
用&同为1才是1
```ts
1001 -〉 查找是否有0001
         1001
     &   0001
        ------
         0001

三、位运算重构上述

1. 设值 修改


// `num<<n`:将 `num` 转为二进制,左移 `n` 位(当然,输出的结果是十进制的)
const enum ShapeFlags {
    ELEMENT = 1, // 
    STATEFUl_COMPONENT = 1 << 1, // 0010
    TEXT_CHILDREN = 1 << 2, // 0100
    ARRAY_CHILDREN = 1 << 3, // 1000
}
// 将shapeFlags添加到vnode上
function createVNode(type, props, children) {
    var vnode = {
        type: type,
        props: props,
        children: children,
        shapeFlag: getShapsFlag(type), // 这里就是一个普通的二进制数值
    };
    
    function getShapsFlag(type) {
        return typeof type === 'string' ? ShapeFlags.ELEMENT : ShapeFlags.STATEFUl_COMPONENT
    }
    
    if (typeof children === 'string') {
         // 比方vnode.shapeFlags经过getShapsFlag变成了0001;0001 | 0100 就是0101
         vnode.shapeFlag |= ShapeFlags.TEXT_CHILDREN 
        // 即 vnode.shapeFlags = vnode.shapeFlags | ShapeFlags.TEXT_CHILDREN 的简写
    } else {
        vnode.shapeFlag |= ShapeFlags.ARRAY_CHILDREN 
    }

    return vnode;
}

2. 读取

function patch(vnode, container) {
    const { shapeFlag } = vnode // 比方0101 查找是否有0001 用&
    if (shapeFlag & ShapeFlags.ELEMENT) {
      processElement(vnode, container);
    } else if (shapeFlag & ShapeFlags.STATEFUl_COMPONENT) {
      processComponent(vnode, container)
    }
}
function mountElement(vnode: any, container: any) {
   // ……
  const { type, props, children, shapeFlag } = vnode
  // 其他……
  if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
    el.textContent = children
  } else if (shapeFlag & shapeFlags.ARRAY_CHILDREN) {
    mountChildren(vnode, el)
  }
  container.appendChild(el)
}

总结,位运算设置和查找更为高效,但同时损失了可读性; 性能和可读性有时会是相冲的;这时要自己根据情况进行取舍