虛擬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)
}

效果:
在這裏插入圖片描述

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章