从零开始建立你本身的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

相关文章
相关标签/搜索