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

1.渲染组件

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

javascript 复制代码
const vnode = {
	 type: MyComponent,
	 
}

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

javascript 复制代码
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。如下:

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

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

javascript 复制代码
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函数来定义组件自身的状态。

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

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

javascript 复制代码
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中,如下:

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

javascript 复制代码
// 任务缓存队列,用一个 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 函数之后,我们可以在创建渲染副作用时使 用它,

javascript 复制代码
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.组件实例与组件的生命周期

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

javascript 复制代码
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 属性可以 用来区分组件的挂载和更新。

javascript 复制代码
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.注册生命周期

相关推荐
Fan_web7 分钟前
jQuery——事件委托
开发语言·前端·javascript·css·jquery
安冬的码畜日常8 分钟前
【CSS in Depth 2 精译_044】第七章 响应式设计概述
前端·css·css3·html5·响应式设计·响应式
莹雨潇潇1 小时前
Docker 快速入门(Ubuntu版)
java·前端·docker·容器
Jiaberrr1 小时前
Element UI教程:如何将Radio单选框的圆框改为方框
前端·javascript·vue.js·ui·elementui
Tiffany_Ho2 小时前
【TypeScript】知识点梳理(三)
前端·typescript
安冬的码畜日常3 小时前
【D3.js in Action 3 精译_029】3.5 给 D3 条形图加注图表标签(上)
开发语言·前端·javascript·信息可视化·数据可视化·d3.js
太阳花ˉ3 小时前
html+css+js实现step进度条效果
javascript·css·html
小白学习日记3 小时前
【复习】HTML常用标签<table>
前端·html
程序员大金4 小时前
基于SpringBoot+Vue+MySQL的装修公司管理系统
vue.js·spring boot·mysql