【Vue设计与实现】Vue.js 3 的 设计思路

前言

本章将从全局视角介绍 Vue.js 3.0 的设计思路,以及各个模块之间是如何协作的。

声明式的渲染UI

VueJS可以使用模板语法或JavaScript对象(或使用h函数将参数转化为JavaScript对象)来声明式

模板语法

JavaScript 复制代码
01 <h1 @click="handler"><span></span></h1>

JavaScript对象

JavaScript 复制代码
01 // h 标签的级别
02 let level = 3
03 const title = {
04   tag: `h${level}`, // h3 标签
05 }

h函数转化为JavaScript对象

h 函数是一个辅助创建虚拟 DOM 的工具函数,后续会将render()称为渲染函数,模板最终也会被编译器编译为渲染函数。

JavaScript 复制代码
01 import { h } from 'vue'
02
03 export default {
04   render() {
05     return h('h1', { onClick: handler }) // 虚拟 DOM
06   }
07 }

Vue源码中的 h 函数实现

初始渲染器

渲染器的作用就是把虚拟 DOM 渲染为真实 DOM。

实现一个简易的渲染器

JavaScript 复制代码
01 function renderer(vnode, container) {
02   // 使用 vnode.tag 作为标签名称创建 DOM 元素
03   const el = document.createElement(vnode.tag)
04   // 遍历 vnode.props,将属性、事件添加到 DOM 元素
05   for (const key in vnode.props) {
06     if (/^on/.test(key)) {
07       // 如果 key 以 on 开头,说明它是事件
08       el.addEventListener(
09         key.substr(2).toLowerCase(), // 事件名称 onClick ---> click
10         vnode.props[key] // 事件处理函数
11       )
12     }
13   }
14
15   // 处理 children
16   if (typeof vnode.children === 'string') {
17     // 如果 children 是字符串,说明它是元素的文本子节点
18     el.appendChild(document.createTextNode(vnode.children))
19   } else if (Array.isArray(vnode.children)) {
20     // 递归地调用 renderer 函数渲染子节点,使用当前元素 el 作为挂载点
21     vnode.children.forEach(child => renderer(child, el))
22   }
23
24   // 将元素添加到挂载点下
25   container.appendChild(el)
26 }

对于渲染器来说,它真正的难点在于需要精确地找到 vnode 对象的变更点并且只更新变更的内容。就上例来说,渲染器应该只更新元素对应改变的部分,而不需要再走一遍完整的创建元素的流程。

组件的本质

组件就是一组 DOM 元素的封装

我们可以定义一个JavaScript对象为MyComponent

JavaScript 复制代码
01 const MyComponent = function () {
02   return {
03     tag: 'div',
04     props: {
05       onClick: () => alert('hello')
06     },
07     children: 'click me'
08   }
09 }

在Vue源码中会判断vnode的tag是否是对象或是字符串,如果是字符串则直接作为标签渲染,如果是对象或是函数则进入组件渲染函数,组件渲染函数做的事情也就是递归调用渲染函数将标签拼装为组件。

JavaScript 复制代码
01 const vnode = {
02   tag: MyComponent
03 }
JavaScript 复制代码
01 function mountComponent(vnode, container) {
02   // vnode.tag 是组件对象,调用它的 render 函数得到组件要渲染的内容(虚拟 DOM)
03   const subtree = vnode.tag.render()
04   // 递归地调用 renderer 渲染 subtree
05   renderer(subtree, container)
06 }

模板的工作原理

以我们熟知的.vue 文件为例,一个 .vue 文件就是一个组件:

Vue 复制代码
01 <template>
02   <div @click="handler">
03     click me
04   </div>
05 </template>
06
07 <script>
08 export default {
09   data() {/* ... */},
10   methods: {
11     handler: () => {/* ... */}
12   }
13 }
14 </script>

其中 <template> 标签里的内容就是模板内容,编译器会把模板内容编译成渲染函数并添加到<\script> 组件对象上

JavaScript 复制代码
01 export default {
02   data() {/* ... */},
03   methods: {
04     handler: () => {/* ... */}
05   },
06   render() {
07     return h('div', { onClick: handler }, 'click me')
08   }
09 }

Vue.js的渲染过程

无论是使用模板还是直接手写渲染函数,对于一个组件来说,它要渲染的内容最终都是通过渲染函数产生的,然后渲染器再把渲染函数返回的虚拟 DOM 渲染为真实 DOM,这就是模板的工作原理,也是 Vue.js 渲染页面的流程。

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

组件的实现依赖于渲染器,模板的编译依赖于编译器

举个例子,组件中id往往是不易改变的,而class确实会经常改变的,如果我们能在模板编译时区分出哪些是会经常改变的内容,将这些信息提取出来,然后直接交给渲染器。

JavaScript 复制代码
01 <div id="foo" :class="cls"></div>

我们一眼就能看出其中 id="foo" 是永远不会变化的,而:class="cls" 是一个 v-bind 绑定,它是可能发生变化的。所以编译器能识别出哪些是静态属性,哪些是动态属性,在生成代码的时候完全可以附带这些信息:

JavaScript 复制代码
01 render() {
02   return {
03     tag: 'div',
04     props: {
05       id: 'foo',
06       class: cls
07     },
08     patchFlags: 1 // 假设数字 1 代表 class 是动态的
09   }
10 }

总结

本章介绍了声明式描述 UI 的概念,强调了Vue.js作为声明式框架的优势,通过模板和虚拟DOM实现UI描述。渲染器的基本原理是将虚拟DOM渲染为真实DOM,采用Diff算法实现高效更新。对组件的讨论揭示了其本质是虚拟DOM元素的封装,可以是函数或对象,渲染器通过执行组件的渲染函数获取内容,并递归渲染。最后,强调编译器和渲染器是Vue.js的核心组成部分,它们协同工作以提高框架性能。

相关推荐
大前端爱好者1 小时前
React 19 新特性详解
前端
随云6321 小时前
WebGL编程指南之着色器语言GLSL ES(入门GLSL ES这篇就够了)
前端·webgl
寻找09之夏2 小时前
【Vue3实战】:用导航守卫拦截未保存的编辑,提升用户体验
前端·vue.js
多多米10053 小时前
初学Vue(2)
前端·javascript·vue.js
柏箱3 小时前
PHP基本语法总结
开发语言·前端·html·php
新缸中之脑3 小时前
Llama 3.2 安卓手机安装教程
前端·人工智能·算法
hmz8563 小时前
最新网课搜题答案查询小程序源码/题库多接口微信小程序源码+自带流量主
前端·微信小程序·小程序
看到请催我学习3 小时前
内存缓存和硬盘缓存
开发语言·前端·javascript·vue.js·缓存·ecmascript
blaizeer4 小时前
深入理解 CSS 浮动(Float):详尽指南
前端·css
编程老船长4 小时前
网页设计基础 第一讲:软件分类介绍、工具选择与课程概览
前端