【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的核心组成部分,它们协同工作以提高框架性能。

相关推荐
IT_陈寒18 分钟前
React 性能优化:5个实战技巧让首屏加载提升50%,开发者亲测有效!
前端·人工智能·后端
rising start35 分钟前
前端基础一、HTML5
前端·html·html5
鬼谷中妖44 分钟前
JavaScript 循环与对象:深入理解 for、for...in、for...of、不可枚举属性与可迭代对象
前端
大厂码农老A1 小时前
你打的日志,正在拖垮你的系统:从P4小白到P7专家都是怎么打日志的?
java·前端·后端
im_AMBER1 小时前
CSS 01【基础语法学习】
前端·css·笔记·学习
DokiDoki之父1 小时前
前端速通—CSS篇
前端·css
pixle01 小时前
Web大屏适配终极方案:vw/vh + flex + clamp() 完美组合
前端·大屏适配·vw/vh·clamp·终极方案·web大屏
ssf19871 小时前
前后端分离项目前端页面开发远程调试代理解决跨域问题方法
前端
@PHARAOH1 小时前
WHAT - 前端性能指标(加载性能指标)
前端
尘世中一位迷途小书童1 小时前
🎨 SCSS 高级用法完全指南:从入门到精通
前端·css·开源