從零開始建立你本身的Vue 3

Vue 3 內部原理講解,深刻理解 Vue 3,建立你本身的 Vue 3。
Deep Dive into Vue 3. Build Your Own Vue 3 From Scratch.html

本文完整內容可見,從零開始建立你本身的Vue 3vue

第1章 Vue 3總覽

你能學到什麼node

  • 瞭解 Vue 3 核心模塊的功能
  • 瞭解 Vue 3 總體的運行過程

核心模塊

Vue 3有三個核心模塊,分別是:react

  • 響應式(reactivity)模塊
  • 編譯器(compiler)模塊
  • 渲染器(renderer)模塊

響應式模塊

reactivity 模塊用來建立響應式對象,咱們能夠監聽這些對象的修改,當執使用了這些響應式對象的代碼執行時,他們就會被跟蹤,當響應式對象的值發生了變化時,這些被追蹤的代碼會從新執行。git

編譯器模塊

compiler 模塊是用來處理模板的,它把模板編譯成 render 函數,它能夠發生在瀏覽器的運行階段,但更多的是在Vue項目構建時進行編譯。github

渲染器模塊

renderer 模塊處理 VNode,把組件渲染到 web 頁上。它包含三個階段:web

  1. Render 階段,調用 render 函數並返回一個 VNode
  2. Mount 階段,render 接收 VNode,而後進行 JavaScript DOM 操做來建立 web 頁;
  3. Patch 階段,render 接收新舊兩個 VNode,比較兩者的不一樣,而後進行頁面的局部更新。

運行過程

  1. compiler 把 HTML 編譯成 render 函數;
  2. reactivity 模塊初始化 reactive 對象;
  3. renderer 模塊的 Render 階段,調用引用了 reactive 對象的 render 函數,這樣就監聽了響應式對象,render 函數返回 VNode
  4. renderer 模塊的 Monut 階段,用 VNode 生成真實的 DOM 並渲染到頁面上;
  5. 若是 reactive 對象發生了變化,將再次調用 render 函數建立新的 VNode,這時將進入 renderer 模塊的 Patch 階段,更新頁面的變化。

第2章 渲染機制

你能學到什麼算法

  • 瞭解 Virtual DOM 存在的意義
  • 瞭解 render 函數存在的必要性
  • 瞭解 tempalterender 的使用場景

Virtual DOM

Virtual DOM 是什麼

Virtual DOM 就是用 JavaScript 對象來描述真實的 DOM 節點。typescript

例如,有這樣一段 HTML:編程

<div id="div">
  <button @click="click">click</button>
</div>

用 Virtual DOM 來表示能夠是這樣的(爲何說能夠是這樣,由於這徹底取決於你的設計):

const vDom = {
  tag: 'div',
  id: 'div',
  children: [{
    tag: 'button',
    onClick: this.click
  }]
}

爲何要用 Virtual DOM

  1. 可跨平臺,Virtual DOM 使組件的渲染邏輯和真實 DOM 完全解耦,所以你能夠很方便的在不一樣環境使用它,例如,當你開發的不是面向瀏覽器的,而是 IOS 或 Android 或小程序,你能夠利用編寫本身的 render 函數,把 Virtual DOM 渲染成本身想要的東西,而不只僅是 DOM。
  2. 可程序式修改,Virtual DOM 提供了一種能夠經過編程的方式修改、檢查、克隆 DOM 結構的能力,你能夠在把 DOM 返回給渲染引擎以前,先利用基本的 JavaScript 來處理好。
  3. 提高性能,當頁面中有大量的 DOM 節點操做時,若是涉及到了瀏覽器的迴流和重繪,性能是十分糟糕的,就像第二條說的,在 DOM 返回給渲染引擎以前,咱們能夠先用 JavaScript 處理 Virtual DOM,最終才返回真實 DOM,極大減小回流和重繪次數。

render 函數

render 函數是什麼

首先咱們知道,當你在編寫 Vue 組件或頁面時,通常會提供一個 template 選項來寫 HTML 內容。根據Vue 3 總覽這一部份內容的介紹,Vue 會先走編譯階段,把 template 編譯成 render 函數,因此說最終的 DOM 必定是從 render 函數輸出的。所以 render 函數能夠用來代替 template,它返回的內容就是 VNode。直接使用 render 反而能夠省去 complier 過程。

爲何要提供 render 函數

Vue 中提供的 render 函數是很是有用的,由於有些狀況用 template 來表達業務邏輯會必定程度受到限制,這種狀況你須要一種比較靈活的編程方式來表達底層的邏輯。

例如,當你有一個需求是大量的文本輸入框,這中需求你要寫的標籤並很少,可是卻揉了大量的交互邏輯,你須要在模板上添加大量的邏輯代碼(好比控制關聯標籤的顯示),然而,你的 JavaScript 代碼中也有大量的邏輯代碼。

render 函數的存在可讓你在一個地方寫業務邏輯,這時你就不用太多的去考慮標籤的問題了。

render 函數使用方法

有一段 template 以下:

template: '<div id="foo" @click="onClick">hello</div>'

Vue 3 中用 render 函數實現以下:

import { h } from 'vue'

render() {
 return h('div', {
   id: 'foo',
   onClick: this.onClick
 }, 'hello')
}

因爲這是純粹的 JavaScript,因此若是你須要實現 template 中相似 v-ifv-for 這樣的功能,直接經過三元表達式作到。

import { h } from 'vue'

render() {
 let nodeToReturn

 // v-if="ok"
 if(this.ok) {
   nodeToReturn = h('div', { 
     id: 'foo', 
     onClick: this.onClick 
   }, 'ok')
 } else {
   // v-for="item in list"
   const children = this.list.map(item => {
     return h('p', { 
       key: item.id 
     }, item.text)
   })

   nodeToReturn = h('div', {}, children)
 }
 return nodeToReturn
}

這就是 render 基本使用用法,就是 JavaScript 代碼而已。

render 函數使用場景

通常來講咱們用 tempate 能夠知足大多數場景來,可是你必定了解過 slot 這個東西,若是隻使用 tempate 你將沒法操做 slot 中的內容,若是你須要程序式地修改傳進來的 slot 內容,你就必須用到 render 函數了(這也是大多數使用 render 函數的場景)。

下面咱們用一個例子來講明。

好比咱們要實現這樣一個組件:實現層級縮進效果,即相似 HTML 中嵌套的 UL 標籤,看起來就像這樣:

level 1
  level 1-1
  level 1-2
    level 1-2-1
    level 1-2-2

咱們的模板是這樣寫的,實際上 Stack 組件就是給每個 slot 都增長一個左邊距:

<Stack size="10">
  <div>level 1</div>
  <Stack size="10">
    <div>level 1-1</div>
    <div>level 1-2</div>
    <Stack size="10">
      <div>level 1-2-1</div>
      <div>level 1-2-2</div>
    </Stack>
  </Stack>
</Stack>

如今咱們只用 template 是沒法實現這種效果的,衆所周知,template 只能把默認的 slot 渲染出來,它不能程序式處理 slot 的值。

咱們先用 template 來實現這個組件,stack.html

const Stack = {
  props: {
    size: [String, Number]
  },
  template: `
    <div class="stack">
      <slot></slot>
    </div>
  `
}

這樣因爲不能處理 slot 內容,那麼它的表現效果以下,並無層級縮進:

level 1
level 1-1
level 1-2
level 1-2-1
level 1-2-2

咱們如今嘗試用 render 函數實現 Stack 組件:

const { h } = Vue
const Stack = {
  props: {
    size: [String, Number]
  },
  render() {
    const slot = this.$slots.default
      ? this.$slots.default()
      : []

    return h('div', { class: 'stack' }, 
      // 這裏給每一項 slot 增長一個縮進 class
      slot.map(child => {
      return h('div', { class: `ml${this.$props.size}` }, [ child ])
    }))
  }
}

render 函數中咱們能夠經過 this.$slots 拿到插槽內容,經過 JavaScript 把它處理成任何咱們想要的東西,這裏咱們給每一項 slot 添加了一個 margin-left: 10px 縮進,看下效果:

level 1
  level 1-1
  level 1-2
    level 1-2-1
    level 1-2-2

完美,咱們實現了一個用 template 幾乎實現不了的功能。

原則:

  • 通常來講開發一些公共組件時纔會用到 render
  • 當你發現用 JavaScript 才能更好的表達你的邏輯時,那麼就用 render 函數
  • 平常開發的功能性組件使用 template,這樣更高效,且 template 更容易被 complier 優化

結束語

你可能會想,爲何不直接編譯成 VNode ,而要在中間加一層 render 呢?

這是由於 VNode 自己包含的信息比較多,手寫太麻煩,也許你寫着寫着不自覺就封裝成了一個 helper 函數,h 函數就是這樣的,它把公用、靈活、複雜的邏輯封裝成函數,並交給運行時,使用這樣的函數將大大下降你的編寫成本。

知道了爲何要有 render 後,才須要去設計實現它,其實主要是實現 h 函數。

第3章 渲染器原理及其實現

你能學到什麼

  • 瞭解 Vue 3 中的 VNode
  • 瞭解 render 的具體渲染原理
  • 瞭解 diff 算法的做用
  • 實現 Vue 3 中渲染器功能

編譯器和渲染器 API初探

Complier 和 Renderer

Vue 3總覽章節中,咱們已經初步認識了編譯器(complier)和渲染器( renderer)的做用。

  • 編譯器是用來處理模板的,它把模板編譯成 render 函數
  • 渲染器處理 VNode,把組件渲染到 web 頁上

咱們有這樣一段 HTML:

<div id="div">
  <button @click="click">click</button>
</div>

編譯器會先把它處理成 render 函數,相似下面的代碼:

import { h } from 'vue'

render() {
  return h('div', {
    id: 'div',
  }, [
    h('button', {
      onClick: this.click
    }, 'click')
  ])
}

渲染器經過 render 函數獲取對應的 VNode,相似這樣:

const vDom = {
  tag: 'div',
  id: 'div',
  children: [{
    tag: 'button',
    onClick: this.click,
    text: 'click'
  }]
}

編譯器(Complier)真實場景

上面是一個很簡單的例子,實際上,Vue 3中的編譯器作了不少的優化工做,好比判斷你的節點是靜態的仍是動態的、緩存事件的綁定等等。因此若是你的組件用 template 實現的話,反而會被 Vue 優化。

咱們經過 Vue 3在線模板編譯系統 生成一段真實代碼:

import { createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock } from "vue"

export function render(_ctx, _cache) {
  return (_openBlock(), _createBlock("div", { id: "div" }, [
    _createVNode("button", { onClick: _ctx.click }, "click", 8 /* PROPS */, ["onClick"])
  ]))
}

// Check the console for the AST

能夠看到和咱們手寫的 render 函數仍是有比較大的差別。

設計 VNode

render 函數返回結果就是 h 函數執行的結果,所以 h 函數的輸出爲 VNode

因此須要先設計一下咱們的 VNode

用 VNode 描述 HTML

一個 html 標籤有它的標籤名、屬性、事件、樣式、子節點等諸多信息,這些內容都須要在 VNode 中體現。

<div id="div">
  div text
  <p>p text</p>
</div>
const elementVNode = {
  tag: 'div',
  props: {
    id: 'div'
  },
  text: 'div text',
  children: [{
    tag: 'p',
    props: null,
    text: 'p text'
  }]
}

上面的代碼顯示了 DOM 變成 VNode 的表現形式,VNode 各屬性解釋:

  • tag :表示 DOM 元素的標籤名,如 divspan
  • props:表示 DOM 元素上的屬性,如idclass
  • children:表示 DOM 元素的子節點
  • text:表示 DOM 元素的文本節點

這樣設計 VNode 徹底沒有問題(實際上 Vue 2 就是這樣設計的),可是 Vue 3 設計的 VNode 並不包含 text 屬性,而是直接用 children 代替,由於 text 本質也是 DOM 的子節點。

在保證語義講得通的狀況下儘量複用屬性,可使 VNode 對象更加輕量

基於此咱們把剛纔的 VNode 修改爲以下形式:

const elementVNode = {
  tag: 'div',
  props: {
    id: 'div'
  },
  children: [{
    tag: null,
    props: null,
    children: 'div text'
  }, {
    tag: 'p',
    props: null,
    children: 'p text'
  }]
}

用 VNode 描述抽象內容

什麼是抽象內容呢?組件就屬於抽象內容,好比下面這一段模板內容:

<div>
  <MyComponent></MyComponent>
</div>

MyComponent 是一個組件,咱們預期渲染出 MyComponent 組件全部的內容,而不是一個 MyComponent 標籤,這用 VNode 如何表示呢?

上一段內容咱們其實已經經過 tag 是否爲 null 來區分元素節點和文本節點了,那這裏咱們能夠經過 tag 是不是字符串判斷是標籤仍是組件呢?

const elementVNode = {
  tag: 'div',
  props: null,
  children: [{
    tag: MyComponent,
    props: null
  }]
}

理論上是能夠的,Vue 2 中就是經過 tag 來判斷的,具體過程以下,能夠在這裏看源碼

  1. VNode.tag 若是不是字符串,則建立組件類型的 VNode
  2. VNode.tag 是字符串

    1. 如果內置的 htmlsvg 標籤,則建立正常的 VNode
    2. 如果屬於某個組件的 id,則建立組件類型的 VNode
    3. 未知或沒有命名空間的組件,直接建立 VNode

以上這些判斷都是在掛載(或 patch)階段進行的,換句話說,一個 VNode 表示的內容須要在代碼運行階段才知道。這就帶來了兩個難題:沒法從 AOT 的層面優化、開發者沒法手動優化。

若是能夠提早知道 VNode 類型,那麼就能夠對其進行優化,因此這裏咱們能夠定義好一套用來判斷 VNode 類型的規則,隨即是用 FLAG = 1 這樣的數字表示仍是其它方法。

區分 VNode 類型

這裏咱們給 VNode 增長一個字段 shapeFlag(這是爲了和 Vue 3 保持一致),它是一個枚舉類型變量,具體以下:

export const enum ShapeFlags {
  // html 或 svg 標籤
  ELEMENT = 1,
  // 函數式組件
  FUNCTIONAL_COMPONENT = 1 << 1,
  // 普通有狀態組件
  STATEFUL_COMPONENT = 1 << 2,
  // 子節點是純文本
  TEXT_CHILDREN = 1 << 3,
  // 子節點是數組
  ARRAY_CHILDREN = 1 << 4,
  // 子節點是 slots
  SLOTS_CHILDREN = 1 << 5,
  // Portal
  PORTAL = 1 << 6,
  // Suspense
  SUSPENSE = 1 << 7,
  // 須要被keepAlive的有狀態組件
  COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8,
  // 已經被keepAlive的有狀態組件
  COMPONENT_KEPT_ALIVE = 1 << 9,
  // 有狀態組件和函數式組件都是「組件」,用 COMPONENT 表示
  COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT
}

如今咱們能夠修改咱們的 VNode 以下:

const elementVNode = {
  shapeFlag: ShapeFlags.ELEMENT,
  tag: 'div',
  props: null,
  children: [{
    shapeFlag: ShapeFlags.COMPONENT,
    tag: MyComponent,
    props: null
  }]
}

shapeFlag 如何用來判斷 VNode 類型呢?按位運算便可。

const isComponent = vnode.shapeFlag & ShapeFlags.COMPONENT

熟悉一下按位運算。

  • a & b:對於每個比特位,只有兩個操做數相應的比特位都是1時,結果才爲1,不然爲0。
  • a | b:對於每個比特位,當兩個操做數相應的比特位至少有一個1時,結果爲1,不然爲0。

咱們把 ShapeFlags 對應的值列出來,以下:

ShapeFlags 操做 bitmap
ELEMENT 0000000001
FUNCTIONAL_COMPONENT 1 << 1 0000000010
STATEFUL_COMPONENT 1 << 2 0000000100
TEXT_CHILDREN 1 << 3 0000001000
ARRAY_CHILDREN 1 << 4 0000010000
SLOTS_CHILDREN 1 << 5 0000100000
PORTAL 1 << 6 0001000000
SUSPENSE 1 << 7 0010000000
COMPONENT_SHOULD_KEEP_ALIVE 1 << 8 0100000000
COMPONENT_KEPT_ALIVE 1 << 9 1000000000
COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT

根據上表展現的基本 flags 值能夠很容易地得出下表:

ShapeFlags bitmap
COMPONENT 00000001 10

區分 children 的類型

上面咱們已經看到了 children 能夠是數組或純文本,但真實場景多是:

  • null
  • 純文本
  • 數組

這裏咱們能夠增長一個 ChildrenShapeFlags 的變量表示 children 的類型,可是基於以前的設計原則,咱們徹底能夠用 ShapeFlags 來表示,那麼同一個 ShapeFlags 如何既用來表示 VNode 的類型,又用來表示其 children 的類型呢?

仍然是按位運算,咱們經過 JavaScript 代碼判斷 children 類型,而後和當前 VNode 進行按位或運算便可。

咱們增長以下函數用來專門處理子節點類型,這和 Vue 3 中的處理一致:

function normalizeChildren(vnode, children) {
  let type = 0
  if (children == null) {
    children = null
  } else if (Array.isArray(children)) {
    type = ShapeFlags.ARRAY_CHILDREN
  } else if (typeof children === 'string') {
    children = String(children)
    type = ShapeFlags.TEXT_CHILDREN
  }
  vnode.shapeFlag |= type
}

這樣咱們就能夠直接經過 shapeFlag 同時判斷 VNode 及其 children 類型了。

爲何 children 也須要標識呢?緣由只有一個: 爲了 patch 過程的優化

定義 VNode

至此,咱們能夠定義 VNode 結構以下:

export interface VNodeProps {
  [key: string]: any
}
export interface VNode {
  // _isVNode 是 VNode 對象
  _isVNode: true
  // el VNode 對應的真實 DOM
  el: Element | null
  shapeFlag: ShapeFlags.ELEMENT,
  tag: | string | Component | null,
  props: VNodeProps | null,
  children: string | Array<VNode>
}

實際上,Vue 3 中對 VNode 的定義要複雜的多,這裏就不去細看了。

生成 VNode 的 h 函數

基本的 h 函數

首先咱們實現一個最簡單的 h 函數,能夠是這樣的,接收三個參數:

  • tag 標籤名
  • props DOM 上的屬性
  • children 子節點

咱們新建一個文件 h.ts,內容以下:

function h(tag, props, children){
  return {
    tag,
    props,
    children
  }
}

咱們用以下的 VNode 來表示 <div class="red"><span>hello</span></div>

import { h } from './h'
const vdom = h('div', {
  class: 'red'
}, [
  h('span', null, 'hello')
])

看一下實際輸出內容:

const vdom = {
  "tag": "div",
  "props": {
    "class": "red"
  },
  "children": [
    {
      "tag": "span",
      "props": null,
      "children": "hello"
    }
  ]
}

基本符合預期,可是這裏有同窗可能又要問了:「這個 vdom 和寫的 h 函數沒什麼不一樣,爲何不直接寫 VNode?」

這是由於咱們如今的 h 函數所作的僅僅就是返回傳入的參數,實際上根據咱們對 VNode 的定義,還缺乏一些字段,不過你也能夠直接寫 VNode,但這樣會增長大量的額外工做。

完整的 h 函數

如今咱們補全 h 函數,添加 _isVNodeelshapeFlag 字段。

function h(tag, props = null, children = null) {
  return {
    _isVNode: true,
    el: null,
    shapeFlag: null,
    tag,
    props,
    children
  }
}

這裏的 _isVNode 永遠爲 trueel 不是在建立 VNode 的時候賦值,因此不用處理,咱們主要處理 shapeFlag,實際上 shapeFlag 有 10 種類型,咱們這裏只實現一個最簡單的判斷:

function h(tag, props = null, children = null) {
  let shapeFlag = null
  // 這裏爲了簡化,直接這樣判斷
  if (typeof tag === 'string') {
    shapeFlag = ShapeFlags.ELEMENT
  } else if(typeof tag === 'object'){
    shapeFlag = ShapeFlags.STATEFUL_COMPONENT
  } else if(typeof tag === 'function'){
    shapeFlag = ShapeFlags.FUNCTIONAL_COMPONENT
  }

  return {
    _isVNode: true,
    el: null,
    shapeFlag,
    tag,
    props,
    children
  }
}

如今咱們須要處理一下 children 的類型了,VNode 章節中咱們講過其判斷邏輯,那麼 h 函數如今完整邏輯以下:

function h(tag, props = null, children = null) {
  let shapeFlag = null
  // 這裏爲了簡化,直接這樣判斷
  if (typeof tag === 'string') {
    shapeFlag = ShapeFlags.ELEMENT
  } else if(typeof tag === 'object'){
    shapeFlag = ShapeFlags.STATEFUL_COMPONENT
  } else if(typeof tag === 'function'){
    shapeFlag = ShapeFlags.FUNCTIONAL_COMPONENT
  }

  const vnode = {
    _isVNode: true,
    el: null,
    shapeFlag,
    tag,
    props,
    children
  }
  normalizeChildren(vnode, vnode.children)
  return vnode
}

function normalizeChildren(vnode, children) {
  let type = 0
  if (children == null) {
    children = null
  } else if (Array.isArray(children)) {
    type = ShapeFlags.ARRAY_CHILDREN
  } else if (typeof children === 'object') {
    type = ShapeFlags.SLOTS_CHILDREN
  } else if (typeof children === 'string') {
    children = String(children)
    type = ShapeFlags.TEXT_CHILDREN
  }
  vnode.shapeFlag |= type
}

如今咱們從新寫一個測試代碼看一下 h 函數輸入結果:

import { h } from './h'
const MyComponent = {
  render() {}
}
const vdom = h('div', {
  class: 'red'
}, [
  h('p', null, 'hello'),
  h('p', null, null),
  h(MyComponent)
])

console.log(vdom);
// vdom:
// {
//   _isVNode: true,
//   el: null,
//   shapeFlag: 17,
//   tag: 'div',
//   props: { class: 'red' },
//   children: [
//     {
//       _isVNode: true,
//       el: null,
//       shapeFlag: 9,
//       tag: 'p',
//       props: null,
//       children: 'hello'
//     },
//     {
//       _isVNode: true,
//       el: null,
//       shapeFlag: 1,
//       tag: 'p',
//       props: null,
//       children: null
//     },
//     {
//       _isVNode: true,
//       el: null,
//       shapeFlag: 4,
//       tag: [Object],
//       props: null,
//       children: null
//     }
//   ]
// }

至此已經完成了 h 函數的基本設計,能夠獲得想要的 VNode 了,下一步就是把 VNode 渲染到頁面上。

渲染 VNode 的 mount 函數

獲得 VNode 以後,咱們須要把它渲染到頁面上,這就是渲染器的 Mount 階段。

mount 函數基本原理

首先,新建一個 render.ts 文件,用來處理掛載相關代碼。

mount 函數應該是這樣,接收一個 VNode 做爲參數,並把生成的 DOM 放進指定的容器 container 中,實現以下:

function mount(vnode, container) {
  const el = document.createElement(vnode.tag)
  contianer.appendChild(el);
}

這就是掛載所作的核心事情,不過這裏咱們還缺乏具體要實現的內容:

  1. 根據不一樣 shapeFlag 生成不一樣 DOM
  2. 設置 DOM 的屬性
  3. DOM 子節點的處理
  4. 生成 DOM 後需將其賦值給 vnode.el

解決 VNode 的類型問題

這裏咱們須要先了解一下普通有狀態組件和函數式組件分別是什麼,如下僅作理解用。

  • 普通有狀態組件
const MyComponent = {
  render() {
    return h('div', null, 'stateful component')
  }
}
  • 函數式組件
function MyFunctionalComponent() {
  return h('div', null, 'function component')
}

咱們根據 vnode.shapeFlag 的值來對各類類型 VNode 進行渲染操做。

function mount(vnode, container) {
  if (vnode.tag === null) {
    mountTextElement(vnode, container)
  } else if (vnode.shapeFlag & ShapeFlags.ELEMENT) {
    mountElement(vnode, container)
  } else if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
    mountStatefulComponent(vnode, container)
  } else if (vnode.shapeFlag & ShapeFlags.FUNCTIONAL_COMPONENT) {
    mountFunctionalComponent(vnode, container)
  }
}

function mountTextElement(vnode, container) {
  ...
}

function mountElement(vnode, container) {
  ...
}

function mountStatefulComponent(vnode, container) {
  ...
}

function mountFunctionalComponent(vnode, container) {
  ...
}
渲染文本節點
function mountTextElement(vnode, container) {
  const el = document.createTextNode(vnode.children)
  container.appendChild(el)
}
渲染標籤節點
function mountElement(vnode, container) {
  const el = document.createElement(vnode.tag)
  container.appendChild(el)
}
渲染普通有狀態組件

普通有狀態組件就是一個對象,經過 render 返回其 VNode, 所以其渲染方法以下:

function mountStatefulComponent(vnode, container) {
  const instance = vnode.tag
  instance.$vnode = instance.render()
  mount(instance.$vnode, container)
  instance.$el = vnode.el = instance.$vnode.el
}
渲染函數式組件

函數式組件的 tag 爲一個函數,返回值爲 VNode,所以其渲染方法以下:

function mountFunctionalComponent(vnode, container){
  const $vnode = vnode.tag()
  mount($vnode, container)
  vnode.el = $vnode.el
}

設置 DOM 屬性

這裏爲了簡化,這裏咱們假設 props 的每一項都是 DOM 的 attribute,因此咱們能夠這樣作:

function mountElement(vnode, container) {
  const el = document.createElement(vnode.tag)
  if(vnode.props){
    for(const key in vnode.props){
      const value = vnode.props[key]
      el.setAttribute(key, value)
    }
  }
  container.appendChild(el);
}

實際上,Vue 3 中 props 是一個扁平化的結構,它同時包含了 propertyattributeevent listener等,每一項都須要單獨處理,以下:

props: {
  id: 'div',
  class: 'red',
  key: 'key1',
  onClick: this.onClick
}

簡單解釋 propertyattribute 的區別就是:attribute 是 DOM 自帶的屬性,如:idclassproperty 是自定義的屬性名,如:keydata-xxx

渲染子節點

咱們知道 children 能夠是字符串或數組,所以實現方法以下:

function mountElement(vnode, container) {
  const el = document.createElement(vnode.tag)

  // props
  if(vnode.props){
    for(const key in vnode.props){
      const value = vnode.props[key]
      el.setAttribute(key, value)
    }
  }

  // children
  if(vnode.children){
    if(typeof vnode.children === 'string'){
      el.textContent = vnode.children
    }else{
      vnode.children.forEach(child => {
        mount(child, el)
      })
    }
  }

  container.appendChild(el);
}

關聯 VNode 及其 DOM

這個只須要增長一行代碼便可,其它函數相似:

function mountTextElement(vnode, container) {
  const el = document.createTextNode(vnode.children)
  vnode.el = el // (*)
  container.appendChild(el)
}

完整實現

如今咱們實現了渲染器 mount 全部的功能,完整代碼以下:

function mount(vnode, container) {
  if (vnode.tag === null) {
    mountTextElement(vnode, container)
  } else if (vnode.shapeFlag & ShapeFlags.ELEMENT) {
    mountElement(vnode, container)
  } else if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
    mountStatefulComponent(vnode, container)
  } else if (vnode.shapeFlag & ShapeFlags.FUNCTIONAL_COMPONENT) {
    mountFunctionalComponent(vnode, container)
  }
}

function mountTextElement(vnode, container) {
  const el = document.createTextNode(vnode.children)
  vnode.el = el
  container.appendChild(el)
}

function mountElement(vnode, container) {
  const el = document.createElement(vnode.tag)

  // props
  if(vnode.props){
    for(const key in vnode.props){
      const value = vnode.props[key]
      el.setAttribute(key, value)
    }
  }

  // children
  if(vnode.children){
    if(typeof vnode.children === 'string'){
      el.textContent = vnode.children
    }else{
      vnode.children.forEach(child => {
        mount(child, el)
      })
    }
  }

  vnode.el = el
  container.appendChild(el)
}

function mountStatefulComponent(vnode, container) {
  const instance = vnode.tag
  instance.$vnode = instance.render()
  mount(instance.$vnode, container)
  instance.$el = vnode.el = instance.$vnode.el
}

function mountFunctionalComponent(vnode, container){
  const $vnode = vnode.tag()
  mount($vnode, container)
  vnode.el = $vnode.el
}

完整示例

如今咱們能夠檢驗一下寫的是否正確,新建 vdom.html,添加以下代碼,並在瀏覽器中打開:

import { h } from './h'
import { mount } from './render'

const MyComponent = {
  render() {
    return h('div', null, 'stateful component')
  }
}
function MyFunctionalComponent() {
  return h('div', null, 'function component')
}

const vdom = h('div', {
  class: 'red'
}, [
  h('p', null, 'text children'),
  h('p', null, null),
  h(MyComponent),
  h(MyFunctionalComponent)
])

console.log(vdom);
mount(vdom, document.querySelector("#app"))

瀏覽器渲染結果,全部內容均正常顯示。

至此,咱們已經瞭解了 Vue 3 的基本渲染原理,並實現了一個簡易版本的渲染器。

未完待續~(因爲內容太多,後續將不在本文繼續增長)

第4章 Vue 3響應式原理及實現

你能學到什麼

  • 瞭解 reactive 設計理念
  • 開發獨立的響應式庫

詳見 Vue 3響應式原理及實現

本文完整內容可見,build-your-own-vue-next

相關文章
相關標籤/搜索