Skip to content

Latest commit

 

History

History
486 lines (397 loc) · 11.3 KB

chapter03.md

File metadata and controls

486 lines (397 loc) · 11.3 KB
order category tag
30
笔记
frontend
vue
vue3

第 3 章 Vue.js 3 的设计思路

3.1 声明式地描述 UI

Vue.js 3 的声明式解决方案

  • 使用与 HTML 标签 一致的方式描述DOM 元素
  • 使用与 HTML 标签 一致的方式描述属性
  • 使用:v-bind描述动态绑定的属性
  • 使用@v-on来描述事件
  • 使用与 HTML 标签 一致的方式描述层级结构

除了使用模板来声明式地描述 UI 之外,我们还可以用 JavaScript 对象来描述,如下所示:

const title = {
  // 标签名称
  tag: 'h1',
  // 标签属性
  props: {
    onClick: handler,
  },
  // 子节点
  children: [
    {
      tag: 'span',
    },
  ],
}

对应的 Vue.js 模板:

<h1 @click="handler"><span></span></h1>

如果要动态生成 h1- h6 标签的话,可以设置一个变量 tag: h${level},这时 JavaScript 的形式就比较灵活。

Vue.js 3 除了支持使用模板描述 UI 外, 还支持使用虚拟 DOM 描述 UI 。

import { h } from 'vue'

export default {
  render() {
    return h('h1', { onClick: handler }) // 虚拟 DOM
  },
}

等价于:

import { h } from 'vue'

export default {
  render() {
    return {
      tag: 'h1',
      props: {
        onClick: handler,
      },
    }
  },
}

上面的 h 函数是一个辅助创建虚拟 DOM 的工具函数。Vue.js 会根据组件的渲染函数的返回值拿到虚拟 DOM,然后把组件的内容渲染出来。 上面代码中的render函数就是渲染函数。

3.2 初识渲染器

虚拟 DOM 是 JavaScript 对象来描述真实 DOM 结构,然后通过渲染器将虚拟 DOM 渲染成真实 DOM。

渲染器非常重要,大家平时编写的 Vue.js 组件都是依赖渲染器来工作的。

假设我们有如下的虚拟 DOM:

const vnode = {
  tag: 'div',
  props: {
    onClick: () => alert('hello'),
  },
  children: 'click me',
}

接下来,编写一个渲染器,把上面的虚拟 DOM 渲染为真实 DOM:

function renderer(vnode, container) {
  // 使用vnode.tag作为标签名称创建 DOM
  const el = document.createElement(vnode.tag)
  // 遍历 vnode.props,将属性,事件添加到 DOM 元素
  for (const key in vnode.props) {
    if (/^on/.test(key)) {
      // 如果 key 以 on 开头,说明是事件
      el.addEventListener(
        key.substr(2).toLowerCase(), // 事件名称 onClick -> click
        vnode.props[key] // 事件处理函数
      )
    }
  }

  // 处理 children
  if (typeof vnode.children === 'string') {
    // 如果 children 是 字符串,说明它是文本子节点
    el.appendChild(document.createTextNode(vnode.children))
  } else if (Array.isArray(vnode.children)) {
    // 递归调用 renderer 函数,渲染子节点
    node.children.forEach(child => renderer(child, el))
  }

  // 将元素添加到挂载点下
  container.appendChild(el)
}

使用:

renderer(vnode, document.body)

::: note 执行效果

:::

::: normal-demo 渲染器示例

<div id="renderer-demo1"></div>
const vnode = {
  tag: 'div',
  props: {
    onClick: () => alert('hello'),
  },
  children: 'click me',
}

function renderer(vnode, container) {
  // 使用vnode.tag作为标签名称创建 DOM
  const el = window.document.createElement(vnode.tag)
  // 遍历 vnode.props,将属性,事件添加到 DOM 元素
  for (const key in vnode.props) {
    if (/^on/.test(key)) {
      // 如果 key 以 on 开头,说明是事件
      el.addEventListener(
        key.substr(2).toLowerCase(), // 事件名称 onClick -> click
        vnode.props[key] // 事件处理函数
      )
    }
  }

  // 处理 children
  if (typeof vnode.children === 'string') {
    // 如果 children 是 字符串,说明它是文本子节点
    el.appendChild(window.document.createTextNode(vnode.children))
  } else if (Array.isArray(vnode.children)) {
    // 递归调用 renderer 函数,渲染子节点
    node.children.forEach(child => renderer(child, el))
  }

  // 将元素添加到挂载点下
  container.appendChild(el)
}

renderer(vnode, window.document.querySelector('#renderer-demo1'))

:::

3.3 组件的本质

一句话总结:组件就是一组 DOM 元素的封装

这组 DOM 元素就是组件要渲染的内容,因此我们可以定义一个函数来代表组件,而函数的返回值就代表组件要渲染的内容:

const MyComponent = function () {
  return {
    tag: 'div',
    props: {
      onClick: () => alert('hello'),
    },
    children: 'click me',
  }
}

用虚拟 DOM 来描述组件:

const vnode = {
  tag: MyComponent,
}

就想 tag: 'div' 一样,用 tag: MyComponent 来描述组件。为了能渲染组件,修改前面的 renderer 函数:

function renderer(vnode, container) {
  if (typeof vnode.tag === 'string') {
    // 说明 vnode 描述的是元素
    mountElement(vnode, container)
  } else if if (typeof vnode.tag === 'function') {
    // 说明 vnode 描述的是组件
    mountComponent(vnode, container)
  }
}

function mountElement(vnode, container) {
  // 使用vnode.tag作为标签名称创建 DOM
  const el = document.createElement(vnode.tag)
  // 遍历 vnode.props,将属性,事件添加到 DOM 元素
  for (const key in vnode.props) {
    if (/^on/.test(key)) {
      // 如果 key 以 on 开头,说明是事件
      el.addEventListener(
        key.substr(2).toLowerCase(), // 事件名称 onClick -> click
        vnode.props[key] // 事件处理函数
      )
    }
  }

  // 处理 children
  if (typeof vnode.children === 'string') {
    // 如果 children 是 字符串,说明它是文本子节点
    el.appendChild(document.createTextNode(vnode.children))
  } else if (Array.isArray(vnode.children)) {
    // 递归调用 renderer 函数,渲染子节点
    node.children.forEach((child) => renderer(child, el))
  }

  // 将元素添加到挂载点下
  container.appendChild(el)
}

function mountComponent(vnode, container) {
  //调用组件函数,获取组件要渲染的内容(虚拟DOM)
  const subtree = vnode.tag()
  // 递归调用 renderer 渲染 虚拟 DOM
  renderer(subtree, container)
}

组件也不一定是函数,也可以是对象:

const MyComponent2 = {
  render() {
    return {
      tag: 'div',
      props: {
        onClick: () => alert('hello'),
      },
      children: 'click me',
    }
  },
}

对应的 renderer 函数修改:

function renderer(vnode, container) {
  if (typeof vnode.tag === 'string') {
    // 说明 vnode 描述的是元素
    mountElement(vnode, container)
  } else if (typeof vnode.tag === 'object') {
    // 说明 vnode 描述的是组件
    mountComponent(vnode, container)
  }
}

接着修改 mountComponent 函数:

function mountComponent(vnode, container) {
  //调用组件函数,获取组件要渲染的内容(虚拟DOM)
  const subtree = vnode.tag.render()
  // 递归调用 renderer 渲染 虚拟 DOM
  renderer(subtree, container)
}

下面为改造后的示例:

::: note 示例运行结果

:::

::: normal-demo 组件示例

<div id="component-function-demo"></div>
<div id="component-object-demo"></div>
// 函数组件
const MyComponent = function () {
  return {
    tag: 'div',
    props: {
      onClick: () => alert('hello'),
    },
    children: 'click me',
  }
}

// 对象组件
const MyComponent2 = {
  render() {
    return {
      tag: 'div',
      props: {
        onClick: () => alert('hello'),
      },
      children: 'click me',
    }
  },
}

const vnode1 = {
  tag: MyComponent,
}

const vnode2 = {
  tag: MyComponent2,
}

// 渲染函数
function renderer(vnode, container) {
  if (typeof vnode.tag === 'string') {
    // 说明 vnode 描述的是元素
    mountElement(vnode, container)
  } else if (typeof vnode.tag === 'function') {
    // 说明 vnode 描述的是组件
    mountFunctionalComponent(vnode, container)
  } else if (typeof vnode.tag === 'object') {
    // 说明 vnode 描述的是组件
    mountObjectComponent(vnode, container)
  }
}

function mountElement(vnode, container) {
  // 使用vnode.tag作为标签名称创建 DOM
  const el = window.document.createElement(vnode.tag)
  // 遍历 vnode.props,将属性,事件添加到 DOM 元素
  for (const key in vnode.props) {
    if (/^on/.test(key)) {
      // 如果 key 以 on 开头,说明是事件
      el.addEventListener(
        key.substr(2).toLowerCase(), // 事件名称 onClick -> click
        vnode.props[key] // 事件处理函数
      )
    }
  }

  // 处理 children
  if (typeof vnode.children === 'string') {
    // 如果 children 是 字符串,说明它是文本子节点
    el.appendChild(window.document.createTextNode(vnode.children))
  } else if (Array.isArray(vnode.children)) {
    // 递归调用 renderer 函数,渲染子节点
    node.children.forEach(child => renderer(child, el))
  }

  // 将元素添加到挂载点下
  container.appendChild(el)
}

function mountFunctionalComponent(vnode, container) {
  //调用组件函数,获取组件要渲染的内容(虚拟DOM)
  const subtree = vnode.tag()
  // 递归调用 renderer 渲染 虚拟 DOM
  renderer(subtree, container)
}

function mountObjectComponent(vnode, container) {
  //调用组件函数,获取组件要渲染的内容(虚拟DOM)
  const subtree = vnode.tag.render()
  // 递归调用 renderer 渲染 虚拟 DOM
  renderer(subtree, container)
}

// 使用
renderer(vnode1, window.document.querySelector('#component-function-demo'))
renderer(vnode2, window.document.querySelector('#component-object-demo'))

:::

3.4 模板的工作原理

无论是手续虚拟 DOM (渲染函数)还是使用模板,都属于声明式地描述 UI,Vue.js 同时支持 ,这是因为 Vue.js 框架中有编译器

编译器的作用就是将模板编译为渲染函数,比如:

<div @click="handler">
  click me
</div>

编译器生成的渲染函数:

render() {
  return h('div', { onClick: handler }, 'click me')
}

对应 .vue 文件来说:

<template>
  <div @click="handler">click me</div>
</template>

<script>
export default {
  data() {
    /* ... */
  },
  methods: {
    handler: () => {
      /* ... */
    },
  },
}
</script>

将被编译为:

export default {
  data() {
    /* ... */
  },
  methods: {
    handler: () => {
      /* ... */
    },
  },
  render() {
    return h('div', { onClick: handler }, 'click me')
  },
}

无论是使用模板还是直接手写渲染函数,对于一个组件来说,它要渲染的内容最终都是通过渲染函数产生的。

3.5 Vue.js 是各个模块组成的有机整体

Vue.js 中编译器渲染器会配合工作,以提升性能。

比如,编译器有分析动态内容的能力,能识别哪些是静态属性,哪些是动态属性,生成代码时附带这些信息,供渲染器使用,比如 patchFlags

3.6 总结

  • Vue.js 是声明式框架,可以用模板方式描述,也可以用渲染函数(虚拟 DOM)来描述
  • 渲染器的作用是把虚拟 DOM 对象渲染为真实 DOM 元素
  • 组件的本质是一组虚拟 DOM 的封装,它可以是一个返回虚拟 DOM 的函数,也可以是一个对象
  • Vue.js 的模板 会被编译器编译为渲染函数