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

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.mzph.cn/news/36932.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

CentOS7.9 禁用22端口,使用其他端口替代

文章目录 业务场景操作步骤修改sshd配置文件修改SELinux开放给ssh使用的端口修改防火墙,开放新端口重启sshd生效 相关知识点介绍sshd服务SELinux服务firewall.service服务 业务场景 我们在某市实施交通信控平台项目,我们申请了一台服务器,用…

学习Vue:列表渲染(v-for)

在 Vue.js 中,实现动态列表的显示是非常常见的需求。为了达到这个目的,Vue 提供了 v-for 指令,它允许您迭代一个数组或对象,将其元素渲染为列表。然而,在使用 v-for 时,key 属性的设置也非常重要&#xff0…

微信小程序(原生)搜索功能实现

一、效果图 二、代码 wxml <van-searchvalue"{{ keyword }}"shape"round"background"#000"placeholder"请输入关键词"use-action-slotbind:change"onChange"bind:search"onSearch"bind:clear"onClear&q…

实践-CNN卷积层

实践-CNN卷积层 1 卷积层构造2 整体流程3 BatchNormalization效果4 参数对比5 测试效果 1 卷积层构造 2 整体流程 根据网络结构来写就可以了。 池化 拉平 训练一个网络需要2-3天的时间。用经典网络来&#xff0c;一些细节没有必要去扣。 损失函数&#xff1a; fit模型&…

运维监控学习笔记1

1、监控对象&#xff1a; 1、监控对象的理解&#xff1b;CPU是怎么工作的&#xff1b; 2、监控对象的指标&#xff1a;CPU使用率&#xff1b;上下文切换&#xff1b; 3、确定性能基准线&#xff1a;CPU负载多少才算高&#xff1b; 2、监控范围&#xff1a; 1、硬件监控&#x…

线性扫描寄存器分配算法介绍

线性扫描寄存器分配 文章目录 线性扫描寄存器分配1. 算法介绍2. 相关概念3. 算法的实现3.1 伪代码3.2 图示 参考文献 论文地址&#xff1a; Linear Scan Register Allocation ​ 我们描述了一种称为线性扫描的快速全局寄存器分配的新算法。该算法不基于图形着色&#xff0c;而…

echarts3d柱状图

//画立方体三个面 const CubeLeft echarts.graphic.extendShape({shape: {x: 0,y: 0,width: 9.5, //柱状图宽zWidth: 4, //阴影折角宽zHeight: 3, //阴影折角高},buildPath: function (ctx, shape) {const api shape.api;const xAxisPoint api.coord([shape.xValue, 0]);con…

陪诊小程序开发|陪诊陪护小程序让看病不再难

陪诊小程序通过与医疗机构的合作&#xff0c;整合了医疗资源&#xff0c;让用户能够更加方便地获得专业医疗服务。用户不再需要面对繁琐的挂号排队&#xff0c;只需通过小程序预约服务&#xff0c;便能够享受到合适的医疗资源。这使得用户的就医过程变得简单高效&#xff0c;并…

Redis使用规范及优化

缓存设计 缓存方案 普通缓存 查询数据时&#xff0c;先查找缓存&#xff0c;如果有延长缓存时间并返回。如果没有&#xff0c;再去查找数据库&#xff0c;将查询的数据再写到缓存&#xff0c;同时设置过期时间。如果是静态热点数据&#xff0c;可以不设置缓存失效时间。 冷…

IntelliJ最佳插件

基于 IntelliJ 平台的 JetBrains IDE 可能是当今最常见的 IDE 之一。它们的受欢迎程度在 JVM 语言社区中尤其明显&#xff0c;IntelliJ IDEA 仍然是大多数开发人员的首选 IDE。所有这一切都是在一些新竞争对手的出现和老竞争对手克服以前的缺点并重新加入竞争者的情况下实现的。…

【EI/SCOPUS检索】第三届计算机视觉、应用与算法国际学术会议(CVAA 2023)

第三届计算机视觉、应用与算法国际学术会议&#xff08;CVAA 2023) The 3rd International Conference on Computer Vision, Application and Algorithm 2023年第三届计算机视觉、应用与算法国际学术会议&#xff08;CVAA 2023&#xff09;主要围绕计算机视觉、计算机应用、计…

PPT颜色又丑又乱怎么办?

一、设计一套PPT时&#xff0c;可以从这5个方面进行设计 二、PPT颜色 &#xff08;一&#xff09;、PPT常用颜色分类 一个ppt需要主色、辅助色、字体色、背景色即可。 &#xff08;二&#xff09;、搭建PPT色彩系统 设计ppt时&#xff0c;根据如下几个步骤&#xff0c;依次选…

Arduino驱动红外二氧化碳传感器(气体传感器篇)

目录 1、传感器特性 2、驱动程序 红外激光传感器是将成熟的红外吸收气体检测技术与精密光路设计、精良电路设计紧密结合而制作出的高性能传感器,具有高灵敏度、高分辨率、低功耗,响应快、抗水汽干扰、不中毒、稳定性高、使用寿命长等特点。本篇博文使用Arduino驱动红外二氧…

Android学习之路(2) 设置视图

一、设置视图宽高 ​ 在Android开发中&#xff0c;可以使用LayoutParams类来设置视图&#xff08;View&#xff09;的宽度和高度。LayoutParams是一个用于布局的参数类&#xff0c;用于指定视图在父容器中的位置和大小。 ​ 下面是设置视图宽度和高度的示例代码&#xff1a; …

Win10基于 Anaconda 配置 Deeplabcut 环境

最近需要做动物行为学分析的相关研究&#xff0c;同时由于合作者只有 Windows 系统&#xff0c;于是只好在 Windows 中配置环境。说实话还真的是挺折磨的。。。 一、下载 Anaconda 可以通过清华源下载 Anaconda&#xff1a;https://mirrors.tuna.tsinghua.edu.cn/anaconda/ar…

算法leetcode|70. 爬楼梯(rust重拳出击)

文章目录 70. 爬楼梯&#xff1a;样例 1&#xff1a;样例 2&#xff1a;提示&#xff1a; 分析&#xff1a;题解&#xff1a;rust&#xff1a;go&#xff1a;c&#xff1a;python&#xff1a;java&#xff1a; 70. 爬楼梯&#xff1a; 假设你正在爬楼梯。需要 n 阶你才能到达楼…

奥威BI数据可视化工具:报表就是平台,随时自助分析

别的数据可视化工具&#xff0c;报表就只是报表&#xff0c;而奥威BI数据可视化工具&#xff0c;一张报表就约等于一个平台&#xff0c;可随时展开多维动态自助分析&#xff0c;按需分析&#xff0c;立得数据信息。 奥威BI是一款多维立体分析数据的数据可视化工具。它可以帮助…

电脑xinput1_3.dll丢失的解决方法?哪个解决方法更简单

最近在打开软件或者游戏的时候&#xff0c;电脑提示xinput1_3.dll文件丢失的错误。这个问题导致我无法运行某些游戏和应用程序。通过一番尝试和研究&#xff0c;我找到了一些修复xinput1_3.dll文件丢失的方法&#xff0c;并在此分享给大家。 首先&#xff0c;我了解到xinput1_3…

如何使用PHP编写爬虫程序

在互联网时代&#xff0c;信息就像一条无休无止的河流&#xff0c;源源不断地涌出来。有时候我们需要从Web上抓取一些数据&#xff0c;以便分析或者做其他用途。这时候&#xff0c;爬虫程序就显得尤为重要。爬虫程序&#xff0c;顾名思义&#xff0c;就是用来自动化地获取Web页…

NSI45030AT1G LED驱动器方案为汽车外部及内部照明恒流稳流器(CCR)方案

关于线性恒流调节器&#xff08;CCR&#xff09;&#xff1a;是一种用于控制电流的稳定输出。它通常由一个功率晶体管和一个参考电流源组成。CCR的工作原理是通过不断调节功率晶体管的导通时间来维持输出电流的恒定。当输出电流超过设定值时&#xff0c;CCR会减少功率晶体管的导…