vuejs 设计与实现 - 组件的实现原理

news/2024/11/9 0:33:19/

1.渲染组件

如果是组件则:vnode .type的值是一个对象。如下:

const vnode = {type: MyComponent,}

为了让渲染器能处理组件类型的虚拟节点,我们还需要在patch函数中对组件类型的虚拟节点进行处理,如下:

function patch(n1, n2, container, anchor) {if(!n1 && n1.type !== n2.type) {unmount(n1)n1 = nill}const { type } = n2if (typeof type === 'string') {} else if (typeof type === 'object') {// 组件if (!n1) {// 挂载组件
+			mountComponent(n2, container,anchor )} else {// 更新组件
+			patchComponent(n1, n2, anchor)}}
}

一个组件必须包含一个渲染函数,即render函数,并且渲染函数的返回值应该是虚拟dom。如下:

const MyComponent = {name: 'MyComponent',render() {return {type: 'div',children: '我是文本'}}
}

有了基本的结构,渲染器就能完成组件的渲染。渲染器中真正完成组件的渲染的是mountComponent函数。实现如下:

function mountComponent(vnode, container, anchor) {// 通过vnode获取组件的选项对象,即vnode.typeconst componentOptions = vnode.type// 获取组件的渲染函数const { render } = componentOptions// 执行渲染函数,获取组件的渲染函数内容,即render返回的虚拟domconst subTree = render()// 最后调用 patch 函数来挂载组件所描述的内容,即 subTreepatch(null, subTree, container, anchor)
}

2.组件状态与自更新

为组件设计自身的状态:data
我们用data函数来定义组件自身的状态。

const MyComponent = {name: 'MyComponent',data() {return {foo: 'dd'}},render() {return {type: 'div',children: `foo 的值是: ${this.foo}` // 在渲染函数内使用组件状态}}
}

我们约定用户必须使用data函数来定义组件自身的状态,同时可以在渲染函数中通过this访问data函数返回的状态数据:

function mountComponent(vnode, container, anchor) {// 通过vnode获取组件的选项对象,即vnode.typeconst componentOptions = vnode.type// 获取组件的渲染函数
+	const { render, data } = componentOptions+	//调用 data 函数得到原始数据,并调用 reactive 函数将其包装为响应式数据
+  const state = reactive(data())// 执行渲染函数,获取组件的渲染函数内容,即render返回的虚拟dom
+	// 调用 render 函数时,将其 this 设置为 state,从而 render 函数内部可以通过 this 访问组件自身状态数据
+	const subTree = render.call(state,state)// 最后调用 patch 函数来挂载组件所描述的内容,即 subTreepatch(null, subTree, container, anchor)
}

实现组件自身状态的初始化需要两个步骤:

    1. 通过组件的选项对象取得 data 函数并执行,然后调用 reactive 函数将 data 函数返回的状态包装为响应式数据;
    1. 在调用 render 函数时,将其 this 的指向设置为响应式数据 state,同时将 state 作为 render 函数的第一个参数传递。

当组件自身状态发生变化时,我们需要有能力触发组件更新,即 组件的自更新。为此,我们需要将整个渲染任务包装到一个effect中,如下:

function mountComponent(vnode, container, anchor) {// 通过vnode获取组件的选项对象,即vnode.typeconst componentOptions = vnode.type// 获取组件的渲染函数const { render, data } = componentOptions//调用 data 函数得到原始数据,并调用 reactive 函数将其包装为响应式数据const state = reactive(data())+	// 将组件的 render 函数调用包装到 effect 内
+	effect(() => {// 执行渲染函数,获取组件的渲染函数内容,即render返回的虚拟dom// 调用 render 函数时,将其 this 设置为 state,从而 render 函数内部可以通过 this 访问组件自身状态数据const subTree = render.call(state,state)// 最后调用 patch 函数来挂载组件所描述的内容,即 subTreepatch(null, subTree, container, anchor)
+	})}

将组件的 render 函数调用包装到 effect 内,这样一旦组件自身响应式数据发生变化,组件就会自动重新 执行渲染函数,从而完成更新。但是,由于effect的执行是同步的,因此放响应式数据发生变化时,与之关联的副作用函数会同步执 行。
换句话说,如果多次修改响应式数据的值,将会导致渲染函数执 行多次,这实际上是没有必要的。因此,我们需要设计一个机制,以 使得无论对响应式数据进行多少次修改,副作用函数都只会重新执行 一次。为此,我们需要实现一个调度器,当副作用函数需要重新执行 时,我们不会立即执行它,而是将它缓冲到一个微任务队列中,等到 执行栈清空后,再将它从微任务队列中取出并执行。有了缓存机制,我们就有机会对任务进行去重,从而避免多次执行副作用函数带来的性能开销。 具体实现如下:

// 任务缓存队列,用一个 Set 数据结构来表示,这样就可以自动对任务进行去重
const queue = new Set()// 一个标志,代表是否正在刷新任务队列
let isFlushing = false// 创建一个立即 resolve 的 Promise 实例
const p = Promiser.resolve()// 调度器的主要函数,用来将一个任务添加到缓冲队列中,并开始刷新队列
function queueJob(job) {queue.add(job)// 如果还没有开始刷新队列,则刷新之if (!isFlushing) {isFlushing = truep.then(() => {try {// 执行任务队列中的任务queue.forEach((job) => job())} finally{// 重置状态isFlushing = falsequeue.clear = 0}})} 
}

上面是调度器的最小实现,本质上利用了微任务的异步执行机 制,实现对副作用函数的缓冲。其中 queueJob 函数是调度器最主要 的函数,用来将一个任务或副作用函数添加到缓冲队列中,并开始刷 新队列。有了 queueJob 函数之后,我们可以在创建渲染副作用时使 用它,

function mountComponent(vnode, container, anchor) {// 通过vnode获取组件的选项对象,即vnode.typeconst componentOptions = vnode.type// 获取组件的渲染函数const { render, data } = componentOptions//调用 data 函数得到原始数据,并调用 reactive 函数将其包装为响应式数据const state = reactive(data())// 将组件的 render 函数调用包装到 effect 内effect(() => {// 执行渲染函数,获取组件的渲染函数内容,即render返回的虚拟dom// 调用 render 函数时,将其 this 设置为 state,从而 render 函数内部可以通过 this 访问组件自身状态数据const subTree = render.call(state,state)// 最后调用 patch 函数来挂载组件所描述的内容,即 subTreepatch(null, subTree, container, anchor)}, {// 指定该副作用函数的调度器为 queueJob 即可scheduler: queueJob})}

这样,当响应式数据发生变化时,副作用函数不会立即同步执行,而是会被 queueJob 函数调度,最后在一个微任务中执行。

不过,上面这段代码存在缺陷。可以看到,我们在 effect 函数内调用 patch 函数完成渲染时,第一个参数总是 null。这意味着,每次更新发生时都会进行全新的挂载,而不会打补丁,这是不正确的。正确的做法是:每次更新时,都拿新的 subTree 与上一次组件所渲染的 subTree 进行打补丁。为此,我们需要实现组件实例,用它来维护组件整个生命周期的状态,这样渲染器才能够在正确的时机执行合适的操作。

3.组件实例与组件的生命周期

组件实例本质上是一个状态集合(对象)。
引入组件实例

function mountComponent(vnode, container, anchor) {// 通过vnode获取组件的选项对象,即vnode.typeconst componentOptions = vnode.type// 获取组件的渲染函数const { render, data } = componentOptions//调用 data 函数得到原始数据,并调用 reactive 函数将其包装为响应式数据const state = reactive(data())+	const instance = {
+		state, // 组件自身的状态数据,即 data
+		isMounted: false, //  一个布尔值,用来表示组件是否已经被挂载,初始值为 false
+		subTree: null // 组件所渲染的内容,即子树(subTree)
+	}// 将组件实例设置到 vnode 上,用于后续更新
+	vnode.component = instance// 将组件的 render 函数调用包装到 effect 内effect(() => {// 执行渲染函数,获取组件的渲染函数内容,即render返回的虚拟dom// 调用 render 函数时,将其 this 设置为 state,从而 render 函数内部可以通过 this 访问组件自身状态数据const subTree = render.call(state,state)// // 检查组件是否已经被挂载
+		if (!isMounted) {// 初次挂载,调用 patch 函数第一个参数传递 null
+			patch(null, subTree, container, anchor)+			// 将组件实例的isMounted设置为true,这样当更新发生时就不会再次进行挂载操作。而是执行更新
+			instance.isMounted  = true
+		} else {
+			// 当isMounted为true时,说明组件已经挂载了,只需要完成自更新即可
+			patch(instance.subTree,subTree, conatiner, anchor)
+		}// 最后调用 patch 函数来挂载组件所描述的内容,即 subTree
+		patch(null, subTree, container, anchor)
+     // 更新组件实例的子树
+ 		instance.subTree = subTree}, {// 指定该副作用函数的调度器为 queueJob 即可scheduler: queueJob})}

在上面这段代码中,我们使用一个对象来表示组件实例,该对象有三个属性。

  • state:组件自身的状态数据,即 data。
  • isMounted:一个布尔值,用来表示组件是否被挂载。
  • subTree:存储组件的渲染函数返回的虚拟 DOM,即组件的子树 (subTree)。

在上面的实现中,组件实例的 instance.isMounted 属性可以 用来区分组件的挂载和更新。

function mountComponent(vnode, container, anchor) {// 通过vnode获取组件的选项对象,即vnode.typeconst componentOptions = vnode.type// 从组件选项对象中取得组件的生命周期函数
+	const { render, data, beforeCreate, created, beforeMount,
mounted, beforeUpdate, updated } = componentOptions// 在这里调用beforeCreate钩子beforeMount && beforeMount()const state = reactive(data())const instance = {state,isMounted: false,subTree: null}	vnode.component = instance// 在这里调用 created 钩子created && created(state)effect(() => {const subTree = render.call(state, state)if (!instance.isMounted) {beforeMount && beforeMount.call(state)}}) 	}

4.props与组件的被动更新

5.setup函数的作用与实现

6.组件事件与emit的实现

7.插槽的工作原理与实现

8.注册生命周期


http://www.ppmy.cn/news/1031469.html

相关文章

opsForHash() 与 opsForValue 请问有什么区别?

&#x1f449;&#xff1a;&#x1f517;官方API参考手册 如图&#xff0c;opsForHash()返回HashOperations<K,HK,HV>但是 opsForValue()返回ValueOperations<K,V>… 区别就是opsForHash的返回值泛型中有K,HK,HV,其中K是Redis指定的某个数据库里面某一个关键字(由…

本地Linux 部署 Dashy 并远程访问教程

文章目录 简介1. 安装Dashy2. 安装cpolar3.配置公网访问地址4. 固定域名访问 转载自cpolar极点云文章&#xff1a;本地Linux 部署 Dashy 并远程访问 简介 Dashy 是一个开源的自托管的导航页配置服务&#xff0c;具有易于使用的可视化编辑器、状态检查、小工具和主题等功能。你…

记一次Kafka重复消费解决过程

起因&#xff1a;车联网项目开发&#xff0c;车辆发生故障需要给三个系统推送消息&#xff0c;故障上报较为频繁&#xff0c;所以为了不阻塞主流程&#xff0c;采用了使用kafka。消费方负责推送并保存推送记录&#xff0c;但在一次压测中发现&#xff0c;实际只发生了10次故障&…

基于SpringBoot实现MySQL备份与还原

基于SpringBoot实现MySQL备份与还原&#xff0c;需求是在页面上对所有的平台数据执行备份和恢复操作&#xff0c;那么就需要使用代码去调用MySQL备份和恢复的指令&#xff0c;下面是具体实现步骤&#xff1b; MySQL备份表设计 CREATE TABLE IF NOT EXISTS mysql_backups (id …

adb对安卓app进行抓包(ip连接设备)

adb对安卓app进行抓包&#xff08;ip连接设备&#xff09; 一&#xff0c;首先将安卓设备的开发者模式打开&#xff0c;提示允许adb调试 二&#xff0c;自己的笔记本要和安卓设备在同一个网段下&#xff08;同连一个WiFi就可以了&#xff09; 三&#xff0c;在笔记本上根据i…

21款美规奔驰GLS450更换中规高配主机,汉化操作更简单

很多平行进口的奔驰GLS都有这么一个问题&#xff0c;原车的地图在国内定位不了&#xff0c;语音交互功能也识别不了中文&#xff0c;原厂记录仪也减少了&#xff0c;使用起来也是很不方便的。 可以实现以下功能&#xff1a; ①中国地图 ②语音小助手&#xff08;你好&#xf…

【Terraform学习】本地变量(Terraform配置语言学习)

背景&#xff1a; 关于如何在机器上拉terraform代码&#xff0c;初始化就不重复了&#xff0c;需要的可以查看前面的文章&#xff1a; 【Terraform学习】Terraform-AWS部署快速入门&#xff08;快速入门&#xff09;_向往风的男子的博客-CSDN博客 使用本地变量命名资源 将每…

《图解HTTP》——HTTP协议详解

目录 一、HTTP协议概述&#x1f3b9; 二、HTTP请求消息&#x1f966; 三、HTTP报文&#x1f420; 四、HTTP 协议瓶颈&#x1f512; 五、HTTP协议相关技术补充&#x1f381; 六、利用telnet观察http协议通讯过程&#x1f4a1; 一、HTTP协议概述&#x1f3b9; HTTP是一个属…