從virtual-dom到多端渲染

前言:

  • 本文圍繞virtual-dom展開,vue/react藉助Virtual DOM 帶來了 分層設計
  • 不論是.vue文件仍是jsx文件都藉助virtual-dom來描述實際的dom結構,兩者都有一個render實現的過程
  • 什麼是渲染器,如何實現
  • 多端渲染帶來的可能性

image.png
渲染器示意圖javascript

1:模擬實現渲染器

所謂渲染器,簡單的說就是將 Virtual DOM 渲染成特定平臺下真實 DOM 的工具(就是一個函數,一般叫 render),渲染器的工做流程分爲兩個階段:mount 和 patch,若是舊的 VNode 存在,則會使用新的 VNode 與舊的 VNode 進行對比,試圖以最小的資源開銷完成 DOM 的更新,這個過程就叫 patch,或「打補丁」。若是舊的 VNode 不存在,則直接將新的 VNode 掛載成全新的 DOM,這個過程叫作 mount。html

1.1:渲染器

須要將基於實際框架(vue/react)描述的文檔結構用js對象來描述vue

不論是.vue文件仍是jsx文件和咱們在控制檯看到的實際dom結構仍是有必定距離的,固然具體的框架會有相應解析,在web層面固然都是咱們所熟悉的dom文檔結構。java

爲了簡化框架的解析過程。咱們目標落在實現由實際的dom結構到保存在內存中由js對象描述的virtual-domnode

  • dom結構能夠抽象成樹形數據結構
  • 實際打印dom結構
  • 模擬由virtual-dom到實際dom結構過程

1.1.1: 一個抽象virtual-dom的例子

// 一個ul-li列表能夠以下表示
<ul id='list'>
  <li class='item'>Item 1</li>
  <li class='item'>Item 2</li>
  <li class='item'>Item 3</li>
</ul>

// 樹形數據
var element = {
  tagName: 'ul', // 節點標籤名
  props: { // DOM的屬性,用一個對象存儲鍵值對
    id: 'list'
  },
  children: [ // 該節點的子節點
    {tagName: 'li', props: {class: 'item'}, children: ["Item 1"]},
    {tagName: 'li', props: {class: 'item'}, children: ["Item 2"]},
    {tagName: 'li', props: {class: 'item'}, children: ["Item 3"]},
  ]
}

這裏的element就是virtual-dom的樣子,只不過實際中從最外層標籤開始,結構比這複雜而已!react

1.1.2: BFS/DFS遍歷dom結構

爲了驗證dom文檔結構能夠抽象成以上的javascript對象,能夠實際的遍歷dom結構,以DFS遍歷樹形結構爲例,打印當前頁面的tagName,classList, 層級git

const DFS = function(node) {
    if (!node) {
        return
    }
    let deep = arguments[1] || 1
    console.log(`${node.nodeName}.${node.classList}  ${deep}`)
    if (!node.children.length) {
        return
    }
    Array.from(node.children).forEach((item) => DFS(item, deep + 1))
}
// 在body標籤上加了test id屬性
var aimNode = document.getElementById('test')
DFS(aimNode)

1.1.3: 由virtual-dom到真實的dom結構

Vue 的 render 方法是實例的一個私有方法,它用來把實例渲染成一個虛擬 Node即virtual-dom,體會一下和這裏render的區別github

  • 肯定基本的vNode類,web

    function Vnode (tagName, props, children) {
      this.tagName = tagName
      this.props = props
      this.children = children
    }
    // 添加render方法
    Vnode.prototype.render = function () {
      var el = document.createElement(this.tagName) // 根據tagName構建
      var props = this.props
    
      for (var propName in props) { // 設置節點的DOM屬性
        var propValue = props[propName]
        el.setAttribute(propName, propValue)
      }
    
      var children = this.children || []
    
      children.forEach(function (child) {
        var childEl = (child instanceof Vnode)
          ? child.render() // 若是子節點也是虛擬DOM,遞歸構建DOM節點
          : document.createTextNode(child) // 若是字符串,只構建文本節點
        el.appendChild(childEl)
      })
      return el
    }
    
    // 實例化ul,是一個virtual-dom對象
    var ul = new Vnode('ul', {id: 'list'}, [
      new Vnode('li', {class: 'item'}, ['Item 1']),
      new Vnode('li', {class: 'item'}, ['Item 2']),
      new Vnode('li', {class: 'item'}, ['Item 3'])
    ])
    // 掛載到body
    var ulRoot = ul.render()
    document.body.appendChild(ulRoot)

    2:render的過程

2.1: virtual-dom參與哪些流程

function render(vnode, container) {
// 獲取vnode
  const prevVNode = container.vnode
  if (prevVNode == null) {
    if (vnode) {
      // 沒有舊的 VNode,只有新的 VNode。使用 `mount` 函數掛載全新的 VNode
      mount(vnode, container)
      // 將新的 VNode 添加到 container.vnode 屬性下,這樣下一次渲染時舊的 VNode 就存在了
      container.vnode = vnode
    }
  } else {
    if (vnode) {
      // 有舊的 VNode,也有新的 VNode。則調用 `patch` 函數打補丁
      patch(prevVNode, vnode, container)
      // 更新 container.vnode
      container.vnode = vnode
    } else {
      // 有舊的 VNode 可是沒有新的 VNode,這說明應該移除 DOM,在瀏覽器中可使用 removeChild 函數。
      container.removeChild(prevVNode.el)
      container.vnode = null
    }
  }
}

2.1.1: 掛載mount

模擬由vnode到實際dom的過程,見前文render方法算法

2.1.2 :更新patch

3:多端渲染

3.1:render能夠不走向dom

前面的例子是Virtual DOM渲染爲 Web 平臺的真實 DOM,因爲面向瀏覽器,渲染器內部須要調用瀏覽器提供的 DOM 編程接口

  • document.createElement
  • el.appendChild
  • document.body.appendChild

爲了實現多端渲染,render方法不須要再強依賴DOM 編程接口

相應的操做節點的接口由具體平臺暴露,知足相似與dom節點的增刪改查

節點:能夠理解成對應平臺的展現單元,如web端展現的是dom

function specialRenderer(options) {
  const {
    hanlde: {
      createElement: platformCreateElement,
      appendChild: platformAppendChild,
      insertBefore: platformInsertBefore,
      removeChild: platformRemoveChild,
      parentNode: platformParentNode,
      nextSibling: platformNextSibling,
      querySelector: platformQuerySelector
    }
  } = options
}

Vue3 提供了一個叫作 @vue/runtime-test 的包,其做用是方便開發者在無 DOM 環境時有能力對組件的渲染內容進行測試。

3.2:Taro多端實現猜測

Taro官方文檔

Taro 是一套遵循 React 語法規範的 多端開發 解決方案。現現在市面上端的形態多種多樣,Web、React-Native、微信小程序等各類端大行其道,當業務要求同時在不一樣的端都要求有所表現的時候,針對不一樣的端去編寫多套代碼的成本顯然很是高,這時候只編寫一套代碼就可以適配到多端的能力就顯得極爲須要。

使用 Taro,咱們能夠只書寫一套代碼,再經過 Taro 的編譯工具,將源代碼分別編譯出能夠在不一樣端(微信/百度/支付寶/字節跳動/QQ小程序、快應用、H五、React-Native 等)運行的代碼。
image.png

Taro多端實現猜測

  • 基於Taro的UI描述,dom結構仍是會藉助virtual-dom描述
  • 要實現web,多家小程序的編譯實現,應該是在render的階段判斷具體環境,提供相似操做的dom(平臺元素)的接口
  • 如今回顧 React 的 Learn Once, Write Anywhere 口號,實際上強調的就是它對各類不一樣渲染層的支持

3.3:多端渲染具體實現

too simple sometimes naive

基於渲染層的一點認識開始去調研市面上多端渲染框架的原理,結果實現截然不同。

探討:基於react的Taro是如何實現一套代碼,多端運行?

  • 前面的討論其實並非沒有意義,只不過render落腳點不同,基於UI層面的描述是能夠實現的
  • 可是沒有考慮到的問題包括
  • Framewrok —— 通俗來講,完成一個 App 應用交互任務所需規範,例如生命週期(onLoad、onShow)、模塊化與數據管理等。
  • Library —— 能夠理解就是「方法封裝集合」
  • 待補充。。。

3.3.1標準的 Language統一

image.png

業務代碼統一約束,藉助babel輸出多端代碼

3.3.2:Framework/Library處理

  • 業界處理思想不一
  • Chameleon:在各個端運行時分別實現了 Framework 統一,在各個端儘可能使用原有框架,方便利用其生態,這樣不少組件能夠直接用起來。(folk?)

    • 能夠藉助babel - babel/polyfill基於語法和api的處理加以理解
  • taro: 挑選了微信小程序的組件庫和 API 來做爲 Taro 的運行時標準

4 :總結

image.png

再次回顧一下這張圖,基本思想就是藉助Virtual DOM 帶來了 分層設計,每一步的單獨處理均可以自成一家之言。框架多端編譯的概念層出不窮,

站在開發者的角度知道背後實際是作了哪些改動就能夠根據本身的興趣選擇方向。

文中對一些方法和操做作了簡化,目的在與梳理流程,知識點包括不限於:

  • 將Dom結構描述爲js對象--生成virtual-dom
  • 由js對象生成實際的dom結構-- render渲染器
  • 將更新的js對象與以前展開diff比較 -- 實現diff算法
  • 比較結果patch應用到實際的dom樹上更新Dom結構 -- 差量更新
  • 實現自定義render函數對接多平臺
  • 目前實現多端渲染的思路

5:參考閱讀

VirtualDOM和基本DFS:https://zhuanlan.zhihu.com/p/64187708

babel-runtime使用與性能優化

渲染器解讀:http://hcysun.me/vue-design/zh/renderer-advanced.html

跨端框架架構解讀:http://www.javashuo.com/article/p-rsrhhecn-mq.html

框架和庫的區別:https://zhuanlan.zhihu.com/p/26078359

相關文章
相關標籤/搜索