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

这篇具有很好参考价值的文章主要介绍了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 } = n2
	
	if (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.type
	const componentOptions = vnode.type
	
	// 获取组件的渲染函数
	const { render } = componentOptions
	
	// 执行渲染函数,获取组件的渲染函数内容,即render返回的虚拟dom
	const subTree = render()
	
	// 最后调用 patch 函数来挂载组件所描述的内容,即 subTree
	patch(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.type
	const 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 函数来挂载组件所描述的内容,即 subTree
	patch(null, subTree, container, anchor)
}

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

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

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

function mountComponent(vnode, container, anchor) {

	// 通过vnode获取组件的选项对象,即vnode.type
	const 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 函数来挂载组件所描述的内容,即 subTree
		patch(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 = true
	
		p.then(() => {
			try {
				// 执行任务队列中的任务
				queue.forEach((job) => job())
			} finally{
				// 重置状态
				isFlushing = false
				queue.clear = 0
			}
		})
	} 
}

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

function mountComponent(vnode, container, anchor) {

	// 通过vnode获取组件的选项对象,即vnode.type
	const 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 函数来挂载组件所描述的内容,即 subTree
		patch(null, subTree, container, anchor)
	}, {
		// 指定该副作用函数的调度器为 queueJob 即可
		scheduler: queueJob
	})
	
	
}

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

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

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

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

function mountComponent(vnode, container, anchor) {

	// 通过vnode获取组件的选项对象,即vnode.type
	const 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 属性可以 用来区分组件的挂载和更新。文章来源地址https://www.toymoban.com/news/detail-650854.html

function mountComponent(vnode, container, anchor) {

	// 通过vnode获取组件的选项对象,即vnode.type
	const 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.注册生命周期

到了这里,关于vuejs 设计与实现 - 组件的实现原理的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • vuejs 设计与实现 - 双端diff算法

    我们介绍了简单 Diff 算法的实现原理。简单 Diff 算法利用虚拟节点的 key 属性,尽可能地复用 DOM元素,并通过移动 DOM的方式来完成更新,从而减少不断地创建和销毁 DOM 元素带来的性能开销。但是,简单 Diff 算法仍然存在很多缺陷,这些缺陷可以通过本章将要介绍的双端 Di

    2024年02月13日
    浏览(45)
  • vuejs 设计与实现 - 快速diff算法

    Vue.js 2 所采用的双端 Diff 算法。既然快速 Diff 算法如此高效,我们有必要了解它的思路。接下来,我们就着重讨论快速 Diff 算法的实现原理。 快速 Diff 算法借鉴了纯文本 Diff 算法中预处理的步骤。 案例: 旧的一组子节点:p-1、p-2、p-3。 新的一组子节点:p-1、p-4、p-2、p-3。 通过

    2024年02月13日
    浏览(34)
  • vuejs 设计与实现 - 渲染器 - 挂载与更新

    渲染器的核心功能:挂载与更新 1.2挂载子节点 (vnode.children) vnode.children可以是字符串类型的,也可以是数组类型的,如下: 可以看到,vnode.children 是一个数组,它的每一个元素都是一个独立的虚拟节点对象。这样就形成了树型结构,即虚拟DOM 树。 为了完成子节点的渲染,我们

    2024年02月13日
    浏览(34)
  • 【庖丁解牛】vue-element-admin前端CRUD通用操作组件详解,对,核心就是crud.js文件

    vue-element-admin 框架之所以能够快速定制应用,得益于其通配的CRUD操作,属性配置多样化且个性化,能够满足绝大部分开发需求,也方便了代码生成。 可以深入学习重点源文件是: src/components/Crud/crud.js ,一共 863 行代码,以及下图中其它四个vue组件,形成了对通用CRUD操作的高

    2024年01月18日
    浏览(59)
  • 前端Vue入门-day04-用vue实现组件通信

    (创作不易,感谢有你,你的支持,就是我前行的最大动力,如果看完对你有帮助,请留下您的足迹) 目录 组件的三大组成部分 注意点说明 组件的样式冲突 scoped data 是一个函数 组件通信 什么是组件通信 不同的组件关系 和 组件通信方案分类   父子通信流程图:  父 → 子

    2024年02月15日
    浏览(54)
  • backend.js:748 An error occurred in hook ‘getInspectorState‘ registered by plugin ‘org.vuejs.vue2-in

    问题 vue项目控制台操报错 backend.js:748 An error occurred in hook \\\'getInspectorState\\\' registered by plugin \\\'org.vuejs.vue2-internal\\\' with payload: 出现这个问题是你的Vue.js devtools版本搞错了 解决 重新下载插件 旧版本下载 确保禁用任何其他版本的 Vue 开发工具。一次只能启用一个版本。

    2024年02月16日
    浏览(42)
  • web前端期末大作业:网站设计与实现——咖啡网站HTML+CSS+JavaScript

    🎀 精彩专栏推荐👇🏻👇🏻👇🏻 ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 💂 作者主页: 【主页——🚀获取更多优质源码】 🎓 web前端期末大作业: 【📚毕设项目精品实战案例 (1000套) 】 🧡 程序员有趣的告白方式:【💌HTML七夕情人节表白网页制作

    2024年02月08日
    浏览(52)
  • React、Vue框架如何实现组件更新,原理是什么?

    原文合集地址如下,有需要的朋友可以关注 本文地址 合集地址 React 和 Vue 都是当今最流行的前端框架,它们都实现了组件化开发模式。为了优化性能,两者都采用了虚拟DOM技术。当组件状态发生改变时,它们会使用虚拟DOM进行局部渲染比对,只更新必要的DOM节点,从而避免重新渲染

    2024年02月17日
    浏览(53)
  • 前端Vue入门-day03-用Vue实现工程化、组件化开发

    (创作不易,感谢有你,你的支持,就是我前行的最大动力,如果看完对你有帮助,请留下您的足迹) 目录 生命周期 Vue 生命周期 和 生命周期的四个阶段  Vue 生命周期函数(钩子函数) 案例-create的应用 案例-mounted的应用 工程化开发 脚手架 Vue CLI 开发 Vue 的两种方式 基本介

    2024年02月15日
    浏览(59)
  • 不到200行用Vue实现类似Swiper.js的轮播组件

    大家在开发过程中,或多或少都会用到轮播图之类的组件,PC和Mobile上使用 Swiper.js ,小程序上使用swiper组件等。 本文将详细讲解如何用 Vue 一步步实现的类似 Swiper.js 的功能,无任何第三方依赖,干货满满。 在线预览:https://zyronon.github.io/douyin/ 项目源代码:https://github.com/

    2024年04月23日
    浏览(34)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包