【Ts重構Vue】01-如何建立虛擬節點

如何建立虛擬節點

React/Vue都用到了虛擬DOM,圍繞虛擬DOM,本篇主要解決下面3個問題。vue

爲何要使用虛擬DOM? 如何定義(建立)虛擬dom呢? 虛擬DOM如何映射爲真實DOM?node

咱們的編碼目標是下面的demo可以成功渲染。web

let vm = new Vue({
  el: '#app',
  render (h) {
    return h('h1', 'Hello Vue!')
  }
})
複製代碼

爲何要使用虛擬節點

將下列代碼拷貝至瀏覽器中運行:算法

let d = document.createElement('div')
for(let key in d) console.log(key)
複製代碼

咱們會發現,真實dom上有很是多的屬性,經過自定義虛擬dom可以有效節省空間。小程序

另外,真實dom的重排重繪是很是消耗性能的,應該儘可能少修改,藉助虛擬DOM的diff算法,可以有效提高性能。瀏覽器

最重要的是,當前有很是多的跨端開發需求,如原生、web、小程序等等,藉助虛擬DOM有助於跨端開發,一段代碼到處運行。bash

建立虛擬DOM

VNode必備屬性只有tag/data/children/text/elm,其餘屬性爲vue功能須要,如componetOptions/componentInstance只在組件節點中才被使用。app

export class VNode {
  tag?: string
  data?: VNodeData
  children?: Array<VNode>
  text?: string
  elm?: Node

  context?: Vue
  componentOptions?: VueOptions
  componentInstance?: Vue
  parent?: VNode
  key?: string | number
  constructor(
    tag?: string,
    data?: VNodeData,
    children?: Array<VNode>,
    text?: string,
    elm?: Node,
    context?: Vue,
    componentOptions?: VueOptions
  ) {
    this.tag = tag
    this.data = data || ({} as VNodeData)
    this.children = children
    this.text = text
    this.elm = elm
    this.context = context || bindContenxt
    this.componentOptions = componentOptions
  }
}

複製代碼

在vue-render方法中,此處h即爲建立虛擬節點的函數。dom

new Vue({
  render (h) {
    return h('h1', 'hello world')
  }
})
複製代碼

咱們知道真實DOM的節點類型很是多,如Element、Attr、Comment、Document、DocumentFragment、Text等,而VNode,只作4種形式:組件節點、子節點(children屬性不爲空)、文本節點、註釋節點。異步

h爲重載函數,根據參數不一樣生成不一樣類型的vnode:

  1. 子節點

子節點類型,其tag和children屬性不爲空,其text屬性爲空。

v1 = h('h1', [h('', 'hello world')])

{
  children: [
    {
      children: undefined, 
      data: {},
      elm: undefined,
      tag: undefined,
      text: 'hello world'
    }
  ],
  data: {},
  elm: undefined,
  tag: "h1",
  text: undefined,
}

複製代碼
  1. 文本節點

文本節點類型,其tag和children屬性爲空,其text屬性不爲空。

v2 = h('', 'hello world')               
{
  children: undefined, 
  data: {},
  elm: undefined,
  tag: undefined,
  text: 'hello world'
}
複製代碼
  1. 註釋節點

文本節點類型,其tag屬性爲!,children屬性爲空,其text屬性不爲空。

v3 = h('!', 'hello comment')

{
  children: undefined, 
  data: {},
  elm: undefined,
  tag: '!',
  text: 'hello world'
}
複製代碼
  1. 組件節點

組件節點類型,其componentOptions屬性不爲空。

v4 = h('button-count', [])

{
  children: undefined
  componentInstance: Proxy {$refs: {…}, $options: {…}}
  componentOptions: {Ctor: ƒ, propsData: undefined, children: Array(1), tag: "button-counter"}
  data: {on: undefined, hook: {…}}
  elm: button
  tag: "vue-component-1-button-counter"
  text: undefined
}
複製代碼

經過屬性狀態劃分爲4種類型,在進行diff算法時,針對不一樣的類型將進行不一樣的處理,如組件節點會調用createComponentInstanceForVnode進行初始化。

虛擬DOM如何映射爲真實DOM?

咱們建立了本身的虛擬DOM,接下來,將虛擬DOM映射爲真實DOM,將Hello Vue渲染至瀏覽器。

映射過程有一個很是重要的方法patch,patch接收新舊節點,執行diff算法。

  1. 若是兩個節點爲sameVnode關係,則調用patchVnode
  2. 不然,直接刪除舊的節點,添加新的節點

webMethods.append的本質是執行parentElm.appendChild(createElm(vnode))

function patch(oldVnode: VNode, vnode: VNode) {
  let parentElm = webMethods.parentNode(oldVnode.elm)

  if (isSameVnode(oldVnode, vnode)) {
    patchNode(oldVnode, vnode)
  } else {
    webMethods.remove(parentElm, oldVnode.elm)
    webMethods.append(parentElm, createElm(vnode))
  }

  return parentElm
}
複製代碼

createElm須要根據虛擬節點的類型進行不一樣的處理,同時它會將生成好的真實DOM掛載在vnode.elm屬性之上,方便對真實dom進行操做。

function createElm(vnode: VNode): Node {
  // 組件節點
  if (createComponent(vnode)) {
    return vnode.elm
  }

  if (vnode.tag === '!') {
    // 註釋節點
    vnode.elm = webMethods.createComment(vnode.text!)
  } else if (!vnode.tag) {
    // 文本節點
    vnode.elm = webMethods.createText(vnode.text!)
  } else {
    // 子節點
    vnode.elm = webMethods.createElement(vnode.tag!)
  }

  return vnode.elm
}
複製代碼

接着對相同虛擬節點(sameVNode)進行比較,根據children屬性分狀況處理,如updateChilden(比較子節點),removeChildren(刪除子節點),insertChildren(添加子節點),setTextContent(修改文本的內容)。

function patchNode(oldVnode: VNode, vnode: VNode) {
  let i: any
  const data = vnode.data,
    oldCh = oldVnode.children,
    ch = vnode.children,
    elm = (vnode.elm = oldVnode.elm!)

  if (oldVnode === vnode) return

  if (oldCh) {
    // 子節點
    if (ch) {
      if (ch === oldCh) return
      updateChildren(elm!, oldCh, ch)
    } else {
      removeChildren(elm!, oldCh, 0, oldCh.length - 1)
      webMethods.setTextContent(elm!, vnode.text!)
    }
  } else {
    // 文本節點
    if (ch) {
      webMethods.setTextContent(elm, '')
      insertChildren(elm!, null, ch, 0, ch.length - 1)
    } else {
      webMethods.setTextContent(elm!, vnode.text!)
    }
  }
}
複製代碼

最終經過不斷遞歸,比較完全部虛擬DOM。

Vue虛擬DOM處理的流程

回顧咱們的DEMO,咱們須要頁面可以渲染出<h1>Hello Vue!</h1>

let vm = new Vue({
  el: '#app',
  render (h) {
    return h('h1', 'Hello Vue!')
  }
})
複製代碼

初始化vue實例後,調用render函數會返回vnode,而el指向的根節點會被初始化爲oldVnode,即:

oldVnode = {
  tag: 'DIV'
  elm: //指向真實dom
}
vnode = {
  tag: 'h1',
  ele: undefined,
  children: [
    {
      tag: '',
      text: 'hello world'
    }
  ]
}
複製代碼

接着執行patch(oldVnode, vnode),對節點進行比較,完成渲染。

簡易代碼

咱們根據上面的流程實現下功能吧。

補充說明下方法:h爲生成VNode的函數,createNodeAt將真實DOM轉爲虛擬DOM,patch是進行映射的核心函數。

class Vue {
  constructor (options) {
    this.$options = options
    this._vnode = null

    if(options.el) {
      this.$mount(options.el)
    }
  },
  _render () {
    return this.$options.render.call(this, h)
  },
  _update (vnode) {
    let oldVnode = this._vnode
    this._vnode = vnode

    patch(oldVnode, vnode)
  }
  $mount (el) {
    this._vnode = createNodeAt(documeng.querySelector(options.el))
    this._update(this._render())
  }
}
複製代碼

ps: 還沒有驗證(運行)上述代碼,後期將進行驗證。

總結

虛擬DOM的diff算法可能沒有表述清楚,推薦直接看snabbdom。基於虛擬DOM技術進行跨平臺開發的方案有:ReactNative、Weex、taro等,還沒有學習故不作敘述。

槓精一下

虛擬DOM究竟提高了多少性能?(www.zhihu.com/question/31…

虛擬DOM的起源?(juejin.im/post/5d085c…

虛擬DOM的diff算法?

系列文章

【Ts重構Vue】00-Ts重構Vue前言

【Ts重構Vue】01-如何建立虛擬節點

【Ts重構Vue】02-數據如何驅動視圖變化

【Ts重構Vue】03-如何給真實DOM設置樣式

【Ts重構Vue】04-異步渲染

【Ts重構Vue】05-實現computed和watch功能

相關文章
相關標籤/搜索