虚拟DOM如何新建和渲染

上一篇描述了什么是虚拟DOM。
在React和Vue中,虚拟DOM的创建都是由模板或者JSX来完成的。但是由模板变成render或者JSX完成虚拟DOM的创建都是由webpack的loader来完成。
我们现在就用原生的方法去完成虚拟DOM是如何去新建和渲染的。

如何新建

假设我们要生成下面这样一个虚拟DOM

<div id="test">
   <p>节点1</p>
</div> 

1.我们新建一个"vdom.js"文件,新建createElement函数,这个函数就是用来创建虚拟DOM。
思路:
1、DOM一般由三部分组成:1.标签 2.标签属性 3.子节点
2、我们创建一个函数,传入三个参数:tag,data,children
3.我们判断tag是什么类型的,并记录,可分为HTML,COMPONENT,TEXT等。我们这次只说HTML和TEXT
4.我们判断children是什么类型的,并记录,可分为EMPTY(无),SINGLE(单个),MULTIPE(多个)
5.如果children为文本,我们创建children为文本标签
6.以对象的形式返回这些数据
代码:

//虚拟DOM的类型
const vnodeType = {
  HTML: 'HTML',
  TEXT: 'TEXT',
  COMPONENT: 'COMPONENT'
}
//子节点的类型
const childType = {
  EMPTY: 'EMPTY',
  SINGLE: 'SINGLE',
  MULTIPLE: 'MULTIPLE'
}
// 创建虚拟DOM
// 三个参数 tag(标签名),data:属性值,children: 子节点,默认是null
function createElement(tag, data, children=null) {
  // 记录vnode的类型
  let flag
  // 如果是string,如'div',我们就认为是普通节点HTML
  if (typeof tag === 'string') {
    flag = vnodeType.HTML
  } else if (typeof tag === 'function') {
    // 如果是function,我们就认为是组件
    flag = vnodeType.COMPONENT
  } else {
    // 其他的默认是文本类型
    flag = vnodeType.TEXT
  }
  // 记录字节点类型
  let childrenFlag
  // 如果为空,说明没有字节点
  if (children === null) {
    childrenFlag = childType.EMPTY
  } else if (Array.isArray(children)) {
    // 如果它是一个数组,根据长度判断,如果为0,认为没有子节点,否则认为有多个节点
    const lenght = children.length
    if (lenght === 0) {
      childrenFlag = childType.EMPTY
    } else {
      childrenFlag = childType.MULTIPLE
    }
  } else {
    // 其他情况,都默认是文本
    childrenFlag = childType.SINGLE
    children = createTextVnode(children + '')
  }

  // 返回虚拟DOM
  return {
    flag, //vnode的类型
    tag, // 标签,div ,文本没有tag,组件就是一个函数
    data, // 属性
    children, // 子节的
    childrenFlag,// 子节点类型
    el: null
  }
}

//新建文本类型的vnode
function createTextVnode (text) {
  //文本节点的tag 为null,且它没有子节点
  return {
    flag: vnodeType.TEXT,
    tag: null,
    data: null,
    children: text,
    childrenFlag: childType.EMPTY
  }
}

2.我们在"index.html"中调用以上函数
代码:

<body>
  <!-- <div id="test">
    <p>节点1</p>
  </div> -->
  <script src="./vdom.js"></script>
  <script>
    let div = createElement('div', {id: 'test'}, [
      createElement('p', {}, '节点1')
    ])
    console.log(JSON.stringify(div, null, 2))
  </script>
</body>

3、我们控制台打印出来
在这里插入图片描述
结果以对象的形式展示出来了,最终完成了虚拟DOM的新建。

如何渲染

1.渲染分为首次渲染和非首次渲染
我们将上述虚拟节点变得复杂一些。我们新增了多个子节点,每个子节点含有key属性及其他属性。

let vnode = createElement('div', {id: 'test'}, [
      createElement('p', {key: 'a', style: {color: 'blue'}}, '节点1'),
      createElement('p', {key: 'b', '@click': () => {alert('节点2')}}, '节点2'),
      createElement('p', {key: 'c', 'class':'item-header' }, '节点3'),
      createElement('p', {key: 'd'}, '节点4'),
    ])
// 执行渲染函数
render(vnode, document.getElementById('app'))

现在我们需要将它渲染到页面上。
思路:
1.我们在“vdom.js”中新建一个render函数,参数为要渲染的虚拟节点和要渲染到哪个节点中的元素(盒子)
2.判断它是首次渲染,还是非首次渲染。(我们现在只写首次渲染)
3.调用首次渲染函数mount函数,将要渲染的虚拟节点和盒子传入
4.新建mount函数,判断要渲染的vnode的类型,如果是节点类型,调用mountElement函数,如果是文本类型,调用mountText函数。
5.新建mountElement函数,根据vnode的tag新建一个虚拟dom,并将dom赋值给el
6.遍历data,渲染属性
7.判断vnode的子节点类型,如果是单个节点,直接调用mount函数递归,参数为子节点和dom,如果是多节点,遍历子节点递归
8.将dom添加盒子中
9.新建mountText函数,创建一个文本节点dom,并记录el
10.将dom添加盒子中
这样我们就完成了首次渲染
代码:

function render(vnode, container) {
  // 首次渲染
  mount(vnode, container)
}

// 首次渲染
function mount(vnode, container) {
  // 得到vnode的类型
  const { flag } = vnode
  // 如果是节点类型,执行mountElement
  if (flag === vnodeType.HTML) {
    mountElement(vnode, container)
  } else if (flag === vnodeType.TEXT) {
    // 如果是文本类型,执行mountText
    mountText(vnode, container)
  }
}

function mountElement(vnode, container) {
  // 根据tag新建dom元素
  const dom = document.createElement(vnode.tag)
  // 赋值给el
  vnode.el = dom
  // 结构出 data, children, childrenFlag
  const { data, children, childrenFlag } = vnode
  // 挂载属性
  if (data) {
    for (let key in data) {
      // 传入4个参数,当前节点,属性名, 上个属性值,这此属性值,因为是首次渲染,所以perv为null
      patchData(dom, key, null, data[key])
    }
  }
  // 挂载子节点
  if (childrenFlag !== childType.EMPTY) {
    if (childrenFlag === childType.SINGLE) {
      // 递归
      mount(children, dom)
    } else if (childrenFlag === childType.MULTIPLE) {
      for(let i = 0; i < children.length; i++) {
        // 递归
        mount(children[i], dom)
      }
    }
  }
  // 添加到 container 中
  container.appendChild(dom)
}
function patchData(el, key, perv, next) {
  switch(key) {
    case 'style':
      for (let i in next) {
        el.style[i] = next[i]
      }
    break
    case 'class':
      el.className = next
    break
    default:
      if (key[0] === '@') {
        el.addEventListener(key.slice(1), next)
      } else {
        el.setAttribute(key, next)
      }
  }
}
function mountText(vnode, container) {
  // 创建文本节点
  const dom = document.createTextNode(vnode.children)
  // 记录el
  vnode.el = dom
  // 添加到 container 中
  container.appendChild(dom)
}

效果:
在这里插入图片描述

在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值