当前位置: 首页 > news >正文

《Vuejs设计与实现》第 12 章(组件实现原理 下)

目录

12.5 setup函数的使用与实现

12.6 组件事件与 emit 的实现

12.7 插槽的工作原理与实现

12.8 注册生命周期

12.9 小结


12.5 setup函数的使用与实现

Vue3 引入了新的组件选项—— setup 函数,区别于 Vue2 的组件选项。
setup 函数是组合式 API 的核心,提供一处创建组合逻辑、创建响应式数据、定义通用函数和注册生命周期钩子等。它仅在组件挂载时执行一次,有两种可能的返回类型:

它可以返回一个函数作为组件的渲染函数:

const Comp = {setup() {// setup 函数返回一个作为组件的渲染函数的函数return () => {return { type: 'div', children: 'hello' }}}
}

这种情况适用于当组件不使用模板表示其渲染内容。如果组件使用模板,那么 setup 不能再返回函数,否则会与模板编译的渲染函数冲突。

它也可以返回一个对象,该对象中的数据将供模板使用:

const Comp = {setup() {const count = ref(0)// 返回的对象中的数据会暴露给渲染函数return {count}},render() {// 通过 this 可以访问 setup 暴露的响应式数据return { type: 'div', children: `count is: ${this.count}` }}
}

上述代码,通过 this,我们可以在 render 函数中访问由 setup 暴露的数据。

setup 函数接收两个参数:props 数据对象和一个称为 setupContext 的对象,如下所示:

const Comp = {props: {foo: String},setup(props, setupContext) {props.foo // 访问传入的 props 数据const { slots, emit, attrs, expose } = setupContext}
}

setup 函数可以通过其第一个参数获取外部传递的 props 数据对象。
同时,接收第二个参数 setupContext 对象,其中包含与组件接口相关的数据和方法,例如 slots(插槽)、emit(发射自定义事件)、attrs(属性)、expose(暴露组件数据,仍在设计讨论中)等。
在 Vue3 中,我们更推崇组合式 API,故 setup 函数应与 Vue.js 2 中的其他组件选项,如 data、watch、methods 等分开使用,避免混用带来的语义和理解困扰。
现在,我们将基于上述功能实现 setup 组件选项:

function mountComponent(vnode, container, anchor) {const componentOptions = vnode.type// 从组件选项中取出 setup 函数let { render, data, setup /* 省略其他选项 */ } = componentOptionsbeforeCreate && beforeCreate()const state = data ? reactive(data()) : nullconst [props, attrs] = resolveProps(propsOption, vnode.props)const instance = {state,props: shallowReactive(props),isMounted: false,subTree: null}// setupContext,由于我们还没有讲解 emit 和 slots,所以暂时只需要 attrsconst setupContext = { attrs }// 调用 setup 函数,将只读版本的 props 作为第一个参数传递,避免用户意外地修改 props 的值,// 将 setupContext 作为第二个参数传递const setupResult = setup(shallowReadonly(instance.props), setupContext)// setupState 用来存储由 setup 返回的数据let setupState = null// 如果 setup 函数的返回值是函数,则将其作为渲染函数if (typeof setupResult === 'function') {// 报告冲突if (render) console.error('setup 函数返回渲染函数,render 选项将被忽略')// 将 setupResult 作为渲染函数render = setupResult} else {// 如果 setup 的返回值不是函数,则作为数据状态赋值给 setupStatesetupState = setupResult}vnode.component = instanceconst renderContext = new Proxy(instance, {get(t, k, r) {const { state, props } = tif (state && k in state) {return state[k]} else if (k in props) {return props[k]} else if (setupState && k in setupState) {// 渲染上下文需要增加对 setupState 的支持return setupState[k]} else {console.error('不存在')}},set(t, k, v, r) {const { state, props } = tif (state && k in state) {state[k] = v} else if (k in props) {console.warn(`Attempting to mutate prop "${k}". Props are readonly.`)} else if (setupState && k in setupState) {// 渲染上下文需要增加对 setupState 的支持setupState[k] = v} else {console.error('不存在')}}})// 省略部分代码
}

上述代码,实现了最简的 setup 函数。
这里需要注意的是,setupContext 是一个对象,仅包含 attrs;
我们通过检测 setup 函数的返回值类型来确定处理方式。
如果返回函数,直接将其作为组件的渲染函数。并确保组件选项中没有现有的 render 选项,否则打印警告。
renderContext 需要正确处理 setupState,因为 setup 函数返回的数据状态也应暴露到渲染环境。

12.6 组件事件与 emit 的实现

在 Vue.js 中,我们可以使用 emit 函数来发射组件的自定义事件。例如:

const MyComponent = {name: 'MyComponent',setup(props, { emit }) {// 发射 change 事件,并向事件处理函数传递两个参数emit('change', 1, 2)return () => {return //...}}
}

当使用这个组件时,我们可以监听由 emit 函数发射的自定义事件,例如:

<MyComponent @change="handler" />
const CompVNode = {type: MyComponent,props: {onChange: handler}
}

在这里,我们可以看到自定义事件 change 已经被编译成一个名为 onChange 的属性,并存储在 props 数据对象中。
这是 Vue.js 的默认约定,但如果自己做框架,可以根据需要进行修改。
实现 emit 的过程中,本质上就是寻找与事件名对应的处理函数并执行。如下代码所示:

function mountComponent(vnode, container, anchor) {// 省略部分代码const instance = {state,props: shallowReactive(props),isMounted: false,subTree: null,}// 定义 emit 函数,它接收两个参数// event: 事件名称// payload: 传递给事件处理函数的参数function emit(event, ...payload) {// 根据约定对事件名称进行处理,例如 change --> onChangeconst eventName = `on${event[0].toUpperCase() + event.slice(1)}`// 根据处理后的事件名称去 props 中寻找对应的事件处理函数const handler = instance.props[eventName]if (handler) {// 调用事件处理函数并传递参数handler(...payload)} else {console.error('事件不存在')}}// 将 emit 函数添加到 setupContext 中,用户可以通过 setupContext 取得 emit 函数const setupContext = { attrs, emit }// 省略部分代码
}

上述代码,我们首先定义了一个 emit 函数,并将其添加到 setupContext 对象中,这样用户就可以在 setup 函数中使用 emit。
当 emit 函数被调用时,我们根据命名约定在 props 数据对象中找到对应的事件处理函数。然后,执行这个处理函数并将参数传递给它。
但需要注意的是,我们之前在讨论 props 的时候提到过,没有明确声明为 props 的属性会被存储在 attrs 中。
这意味着我们如果声明,就不能在 instance.props 中找到任何以 on 开头的属性,即使它们是事件处理器。为了解决这个问题,我们需要对处理 props 的方法做特殊处理:

function resolveProps(options, propsData) {const props = {}const attrs = {}for (const key in propsData) {// 如果属性以'on'开头,无论是否显式声明,都将其添加到props中,而不是添加到attrs中if (key in options || key.startsWith('on')) {props[key] = propsData[key]} else {attrs[key] = propsData[key]}}return [ props, attrs ]
}

上述代码,我们检查 propsData 的键是否以 'on' 开头,如果是,我们认为该属性是组件的自定义事件,即使组件没有显式声明它为 props,我们也将其添加到最后解析的 props 数据对象中,而不是添加到 attrs 对象中。

12.7 插槽的工作原理与实现

插槽是组件的重要特性,它允许用户插入自定义内容到组件中的预留位置。例如,我们有一个名为 MyComponent 的组件,其模板如下:

<template><header><slot name="header" /></header><div><slot name="body" /></div><footer><slot name="footer" /></footer>
</template>

当我们在父组件中使用 MyComponent 组件时,可以根据插槽名称插入自定义内容:

<MyComponent><template #header><h1>我是标题</h1></template><template #body><section>我是内容</section></template><template #footer><p>我是注脚</p></template>
</MyComponent>

这段父组件的模板编译成的渲染函数如下:

function render() {return {type: MyComponent,children: {header() { return { type: 'h1', children: '我是标题' } },body() { return { type: 'section', children: '我是内容' } },footer() { return { type: 'p', children: '我是注脚' } }}}
}

这里,插槽内容被编译为插槽函数,这些函数返回的就是插槽的具体内容。
而在 MyComponent 组件的模板中,插槽内容则通过调用插槽函数来获得。其编译结果为:

function render() {return [{ type: 'header', children: [this.$slots.header()] },{ type: 'body', children: [this.$slots.body()] },{ type: 'footer', children: [this.$slots.footer()] }]
}

可以看到,渲染插槽的过程实际上就是调用插槽函数并渲染其返回的内容,这与 React 中的 render props 模式非常类似。
在实现插槽时,我们需要依赖于 setupContext 中的 slots 对象,如下所示:

function mountComponent(vnode, container, anchor) {// 省略部分代码// 直接使用编译好的 vnode.children 对象作为 slots 对象即可const slots = vnode.children || {}// 将 slots 对象添加到 setupContext 中const setupContext = { attrs, emit, slots }
}

 

在这里,我们直接将编译好的 vnode.children 对象用作 slots 对象,并将其添加到 setupContext 中。
为了能在 render 函数和生命周期钩子函数中通过 this.slots访问插槽内容,我们还需要在renderContext中特别处理slots访问插槽内容,我们还需要在renderContext中特别处理slots 属性:

function mountComponent(vnode, container, anchor) {// 省略部分代码const slots = vnode.children || {}const instance = {state,props: shallowReactive(props),isMounted: false,subTree: null,// 将插槽添加到组件实例上slots,}// 省略部分代码const renderContext = new Proxy(instance, {get(t, k, r) {const { state, props, slots } = t// 当 k 的值为 $slots 时,直接返回组件实例上的 slotsif (k === '$slots') return slots// 省略部分代码},set(t, k, v, r) {// 省略部分代码},})// 省略部分代码
}

在这里,我们为渲染上下文的代理对象 get 方法添加了一个特殊的处理:如果读取的属性是 slots,那么我们直接返回组件实例上的slots对象,这样用户就可以通过this.slots,那么我们直接返回组件实例上的slots对象,这样用户就可以通过this.slots 来访问插槽内容了。

12.8 注册生命周期

Vue3 引入了一些组合式 API,用来注册生命周期钩子函数,例如 onMounted,onUpdated 等。以下是它们的用法:

import { onMounted } from 'vue'const MyComponent = {setup() {onMounted(() => {console.log('mounted 1')})// 可以注册多个onMounted(() => {console.log('mounted 2')})}
}

在 setup 函数中,你可以通过多次调用 onMounted 函数来注册多个 mounted 生命周期钩子函数。它们会在组件被挂载后执行。
这里的关键在于:在A组件的 setup 函数中调用 onMounted 会将该钩子函数注册到 A 组件上;而在 B 组件的 setup 函数中调用 onMounted 则会将钩子函数注册到 B 组件上。这一功能是如何实现的呢?
要实现这个,我们需要一个全局变量 currentInstance 来存储当前的组件实例。在执行组件的 setup 函数前,我们先设置 currentInstance 为当前组件实例,这样就能关联起通过 onMounted 函数注册的钩子函数与当前组件实例。
首先,我们创建一个全局变量 currentInstance 和一个设置该变量的函数 setCurrentInstance。

// 全局变量,存储当前正在被初始化的组件实例
let currentInstance = null// 该方法接收组件实例作为参数,并将该实例设置为 currentInstance
function setCurrentInstance(instance) {currentInstance = instance
}

接着,我们修改 mounteComponent 函数,设置 currentInstance,并在 setup 函数执行完毕后重置 currentInstance:

function mountComponent(vnode, container, anchor) {// 省略部分代码const instance = {state,props: shallowReactive(props),isMounted: false,subTree: null,slots,// 在组件实例中添加 mounted 数组,用来存储通过 onMounted 函数注册的生命周期钩子函数mounted: [],}// 省略部分代码// setupconst setupContext = { attrs, emit, slots }// 在调用 setup 函数之前,设置当前组件实例setCurrentInstance(instance)// 执行 setup 函数const setupResult = setup(shallowReadonly(instance.props), setupContext)// 在 setup 函数执行完毕之后,重置当前组件实例setCurrentInstance(null)// 省略部分代码
}

接下来实现 onMounted 函数:

function onMounted(fn) {if (currentInstance) {// 将生命周期函数添加到 instance.mounted 数组中currentInstance.mounted.push(fn)} else {console.error('onMounted 函数只能在 setup 中调用')}
}

最后一步,需要在合适的时机调用这些注册到 instance.mounted 数组中的生命周期钩子函数:

function mountComponent(vnode, container, anchor) {// 省略部分代码effect(() => {const subTree = render.call(renderContext, renderContext)if (!instance.isMounted) {// 省略部分代码// 遍历 instance.mounted 数组并逐个执行即可instance.mounted && instance.mounted.forEach(hook => hook.call(renderContext))} else {// 省略部分代码}instance.subTree = subTree}, {scheduler: queueJob})
}

我们只需要在合适的时机遍历 instance.mounted 数组,并逐个执行该数组内的生命周期钩子函数即可。
除 mounted 生命周期钩子以外的其他生命周期钩子,其实现原理与此相同。

12.9 小结

本章首先介绍了如何借助虚拟节点来描述组件,其中,vnode.type 属性用于存储组件对象,通过这个属性,渲染器能够判断它是否是组件。如果是,渲染器将通过 mountComponent 和 patchComponent 完成组件的挂载和更新。
然后,我们探讨了组件的自更新机制。在组件挂载阶段,我们为组件创建了一个用于渲染其内容的副作用函数,该函数与组件自身的响应式数据建立了关联。当响应式数据变化时,触发重新执行副作用函数,从而重新渲染组件。为了避免额外的性能开销,我们使用了一个自定义调度器,将渲染副作用函数缓存到微任务队列中,实现渲染任务的去重。
接下来,我们介绍了组件实例,这是一个包含组件运行过程中状态的对象,例如是否已挂载、响应式数据以及渲染内容等。有了组件实例,我们就可以根据实例的状态来决定是进行新的挂载还是进行补丁操作。
此外,我们讨论了组件的 props 和被动更新。引起子组件更新的副作用更新被称为子组件的被动更新。我们也介绍了渲染上下文(renderContext),这实际上是组件实例的代理对象,我们通过它来访问组件实例的数据。
我们进一步讨论了 setup 函数,它是组合式 API 的核心,我们避免将它与 Vue.js 2 中的传统组件选项混用。setup 函数的返回值可以是函数或数据对象,前者作为组件的渲染函数,后者会暴露到渲染上下文中。
我们还讨论了 emit 函数,它包含在 setupContext 对象中,用于发射组件的自定义事件。经过编译后,组件绑定的事件会以 onXxx 的形式存储到 props 对象中。当 emit 函数执行时,会在 props 对象中寻找对应的事件处理函数并执行。
我们还探讨了组件的插槽机制,这是借鉴了 Web Component 中的 <slot> 标签。插槽内容会被编译为插槽函数,返回值即为填充的内容。<slot> 标签会被编译为插槽函数的调用,通过执行相应的插槽函数,我们能得到填充内容的虚拟 DOM,最后将该内容渲染到槽位中。
最后,我们介绍了 onMounted 等生命周期钩子函数的注册方法。注册的生命周期函数会被添加到当前组件实例的 instance.mounted 数组中。为了追踪当前初始化的组件实例,我们定义了全局变量 currentInstance 和相应的 setCurrentInstance 函数。

http://www.lryc.cn/news/604285.html

相关文章:

  • 量子图灵机 Quantum Turing Machine, QTM
  • 【从基础到实战】STL string 学习笔记(上)
  • 如何在出售Windows11/10/8/7前彻底清除电脑数据
  • Python 使用 asyncio 包处理并 发(使用asyncio包编写服务器)
  • Linux的小程序——进度条
  • 重生之我在10天内卷赢C++ - DAY 1
  • 红绿多空策略
  • 华为昇腾×绿算全闪存缓存释放澎湃潜能
  • 【C++详解】深入解析多态 虚函数、虚函数重写、纯虚函数和抽象类、多态原理、重载/重写/隐藏的对⽐
  • 基于 Hadoop 生态圈的数据仓库实践 —— OLAP 与数据可视化(六)
  • ‌CASE WHEN THEN ELSE END‌
  • 分布式系统:一致性
  • Linux常用基础命令
  • 【大语言模型入门】—— Transformer 如何工作:Transformer 架构的详细探索
  • 【C语言】指针深度剖析(一)
  • LeetCode 11 - 盛最多水的容器
  • VUE进阶案例
  • RabbitMQ 消息持久化的三大支柱 (With Spring Boot)
  • Hyperchain账本数据存储机制详解
  • C++:stack与queue的使用
  • AI应用:电路板设计
  • [mcp: JSON-RPC 2.0 规范]
  • Excel文件批量加密工具
  • 【LeetCode 随笔】
  • flask使用celery通过数据库定时
  • 【C语言进阶】题目练习
  • 深入理解 Qt 元对象系统 (Meta-Object System)
  • 最新优茗导航系统源码/全开源版本/精美UI/带后台/附教程
  • Linux定时器和时间管理源码相关总结
  • 进阶向:Manus AI与多语言手写识别