一,框架设计概览
1. 权衡的艺术
1.1 命令式和声明式
命令式框架的一大特点就是关注过程 .
自然语言描述能够与代码产生一一对应的关系,代码本身描述的是"做事的过程", 这符合我们的逻辑直觉。
声明式框架更加关注结果.
1.2 性能与可维护的权衡
1.3 虚拟 DOM 的性能如何
1.4 运行时和编译时
- 纯运行时的
- 运行时 + 编译时的
- 纯编译时的
纯运行时的框架,由于它没有编译过程,因此我们没办法分析用户提供的内容。
纯编译时的框架,可以分析用户内容,但是这种做法有损灵活性。
2. 框架设计的核心要素
2.1 提升用户的开发体验
2.2 控制框架代码的体积
2.3 框架要做到良好的 Tree-Shaking
2.4 框架应该输出怎样的构建产物
2.5 特性开关
2.6 错误处理
2.7 良好的 TypeScript 类型支持
3. Vue.js3 的设计思路
3.1 声明式地描述 UI
Vue.js 是一个声明式的 UI 框架,意思是说用户在使用 Vue.js3 开发时是声明式地描述 UI 的。
除了上面这种使用模板来声明式地描述 UI 之外,我们还可以用 JavaScript 对象来描述。
那么,使用模板和 JavaScript 对象描述 UI 有何不同呢?答案是:使用 JavaScript 对象描述 UI 更加灵活。
import { h } from 'vue'
const r1 = {
render() {
return h('h1', { onClick: handler })
}
}
const r2 = {
render() {
return {
tag: 'h1',
props: { onClick: handlerClick }
}
}
}
这里有必要解释一下什么是组件的渲染函数. 一个组件要渲染的内容时通过渲染函数来描述的,也就是上面代码中的 render 函数,Vue.js 会根据组件的 render 函数的返回值拿到虚拟 DOM, 然后就可以把组件的内容渲染出来了。
3.2 初识渲染器
现在我们已经了解了什么是虚拟 DOM, 它其实就是用 JavaScript 对象来描述真实的 DOM 结构。
渲染器的作用就是把虚拟 DOM 渲染为真实 DOM.
const vnode = {
tag: 'div',
props: {
onClick: () => alert('hello')
},
children: 'click me'
}
function renderer(vnode, container) {
// 使用 vnode.tag 作为标签名创建 DOM 元素
const el = document.createElement(vnode.tag)
// 遍历 vnode.props, 将属性,事件添加到 DOM 元素
for (const key in vnode.props) {
// 如果 key 以 on 开头说明它是事件
if (/^on/.test(key)) {
el.addEventListener(
// 事件名称 onClick ---> click
key.substring(2).toLowerCase(),
// 事件处理函数
vnode.props[key])
}
}
// 处理children
if (typeof vnode.children === 'string') {
// 如果children是字符串,说明它是元素的文本子节点
el.appendChild(document.createTextNode(vnode.children))
} else if (Array.isArray(vnode.children)) {
// 递归地调用render 函数渲染子节点,使用当前元素 el 作为挂载点
vnode.children.forEach(child => renderer(child, el))
}
// 将元素添加到挂载点下
container.appendChild(el)
}
renderer(vnode, document.body)
怎么样,是不是感觉渲染器并没有想象得那么神秘?其实不然,别忘了我们现在所做的还仅仅是创建节点,渲染器的精髓都在更新节点的阶段。
但无论如何,希望大家明白,渲染器的工作原理其实很简单,归根结底,都是使用一些我们熟悉的 DOM 操作 API 来完成渲染工作。
3.3 组件的本质
其实虚拟 DOM 除了能够描述真实 DOM 之外,还能够描述组件。但是组件并不是真实的 DOM 元素。那么如何使用虚拟 DOM 来描述呢?想要弄明白这个问题,就需要先搞清楚组件的本质是什么。
一句话总结:组件就是一组 DOM 元素的封装, 这组 DOM 元素就是组件要渲染的内容,因此我们可以定义一个函数来代表组件,而函数的返回值值就代表组件要渲染的内容。
const MyComponent = function () {
return {
tag: 'div',
props: {
onClick: () => alert('hello')
},
children: 'click me'
}
}
const MyComponentObject = {
render() {
return {
tag: 'div',
props: {
onClick: () => alert('hello')
},
children: 'click me'
}
}
}
可以看到,组件的返回值也是虚拟 DOM,它代表组件要渲染的内容。搞清楚了组件的本质,我们就可以定义用虚拟 DOM 来描述组件了。很简单,我们可以让虚拟 DOM 对象中的 tag 属性来存储组件函数
const vnode2 = {
tag: MyComponent,
// tag: MyComponentWithObject,
}
为了能够渲染组件,需要渲染器的支持。修改前面提到的 renderer 函数。
function renderer2(vnode, container) {
if (typeof vnode2.tag === 'string') {
// 说明 vnode 描述的是标签元素
mountElement(vnode, container)
} else if (typeof vnode2.tag === 'function') {
// 说明 vnode 描述的是组件
mountComponent(vnode, container)
} else if (typeof vnode2.tag === 'object') {
// 说明 vnode 描述的是组件
mountComponentWithObject(vnode, container)
}
}
function mountElement(vnode, container) {
// 使用 vnode.tag 作为标签名创建 DOM 元素
const el = document.createElement(vnode.tag)
// 遍历 vnode.props, 将属性,事件添加到 DOM 元素
for (const key in vnode.props) {
// 如果 key 以 on 开头说明它是事件
if (/^on/.test(key)) {
el.addEventListener(
// 事件名称 onClick ---> click
key.substring(2).toLowerCase(),
// 事件处理函数
vnode.props[key])
}
}
// 处理children
if (typeof vnode.children === 'string') {
// 如果children是字符串,说明它是元素的文本子节点
el.appendChild(document.createTextNode(vnode.children))
} else if (Array.isArray(vnode.children)) {
// 递归地调用render 函数渲染子节点,使用当前元素 el 作为挂载点
vnode.children.forEach(child => renderer(child, el))
}
// 将元素添加到挂载点下
container.appendChild(el)
}
function mountComponent(vnode, container) {
// 调用组件函数,获取要渲染的内容(虚拟DOM)
const subtree = vnode.tag()
// 递归地调用 renderer 函数渲染 subtee
renderer2(subtree, container)
}
function mountComponentWithObject(vnode, container) {
// vnode.tag是组件对象,调用它的render函数得到组件要渲染的内容(虚拟DOM)
const subtree = vnode.tag.render()
// 递归地调用 renderer 函数渲染 subtee
renderer2(subtree, container)
}
3.4 模板的工作原理
无论是手写虚拟 DOM(渲染函数)还是使用模板,都属于声明地描述 UI, 并且 Vue.js 同时支持这两种描述 UI 的方式。上文中我们讲解了虚拟 DOM 是如何渲染成真实的 DOM 的,那么模板是如何工作的呢?这就要提到 Vue.js 框架中的另外一个重要组成部分:编译器。
编译器和渲染器一样,只是一段程序而已,不过它们的工作内容不同。编译器的作用其实就是将模板编译为渲染函数
<div @click="handler">
click me
</div>
对于编译器来说,模板就是一个普通的字符串,它会分析该字符串并生成一个功能与之相同的渲染函数
render(){
return h('div', { onClick: handler }, 'click me')
}
以我们熟悉的 .vue 文件为例,一个 .vue 文件就是一个组件
<template>
<div @click="handler">click me</div>
</template>
<script lang="ts" setup>
function handler(){
alert('click me')
}
</script>
其中 <template>
标签里的内容就是模板内容,编译器会把模板内容编译成渲染函数并添加到<script>
标签块的组件对象上
export default {
data() { },
methods: {},
render() {
return h('div', { onClick: handler }, 'click me')
}
}
所以,无论是使用模板还是直接手写渲染函数,对于一个组件来说,它要渲染的内容最终都是通过渲染函数产生的,然后渲染器再把渲染函数返回的虚拟 DOM 渲染为真实 DOM,这就是模板的工作原理,也是 Vue.js 渲染页面的流程。
3.5 Vue.js 是各个模块组成的有机整体
如前所述,组件的实现依赖于渲染器,模板的编译依赖于编译器,并且编译后生成的代码是根据渲染器和虚拟 DOM 的设计决定的,因此 Vue.js 的各个模块之间是互相关联、互相制约的,共同构成一个有机整体。
标签:Vue,DOM,渲染,js,vnode,tag,组件,第一章,children From: https://www.cnblogs.com/sanhuai/p/17378601.htmlpatchFlags