vuejs设计与实现 12-14 组件化
Vue组件化
1. 组件化
组件化
- 渲染器主要负责将虚拟DOM渲染为真实DOM;只需要使用虚拟DOM来描述最终呈现的内容即可。当编写比较复杂的页面时,用来描述页面结构的虚拟DOM的代码量会变得越来越多,或者说页面模板会变得越来越大,这时就需要组件化的能力。
- 有了组件,则可以将一个大的页面拆分为多个部分,每一个部分都可以作为单独的组件,这些组件共同组成完整的页面。组件化的实现同样需要渲染器的支持。
- 渲染组件
- 一个有状态组件就是一个选项对象,从渲染器内部实现来看,一个组件则是一个特殊类型的虚拟DOM节点;如为了描述普通标签,用虚拟节点的vnode.type来存储标签名称;为了描述片段,虚拟节点的vnode.type属性值为Fragment;为了描述文本,虚拟节点的vnode.type属性值为Text;
- 渲染器会使用虚拟节点的type属性区分类型,对于不同类型的节点,需要采用不同的处理方法来完成挂载和更新。
- 为了使用虚拟节点来描述组件,可以用虚拟节点的vnode.type属性来存储组件的选项对象;为了让渲染器能够处理组件类型的虚拟节点,还需要在patch函数中对组件类型的虚拟节点进行处理,即将该阻尼节点作为组件的描述来看待,并调用mountComponent和patchComponent函数来完成组件的挂载和更新。
- 组件本身是对页面内容的封装,它用来描述页面内容的一部分,因此一个组件必须包含一个渲染函数,即render函数,并且渲染函数的返回值应该是虚拟DOM;换句话说,组件的渲染函数就是用来描述组件所渲染内容的接口;
- 组件状态与自更新
- 实现组件自身状态的初始化需要两个步骤:1.通过组件的选项对象取得data函数并执行,然后调用reactive函数将data函数返回的状态包装为响应式数据;2.在调用render函数时,将其this的指向设置为响应式数据state,同时将state作为render函数的第一个参数传递;
- 当自身状态发生变化时,需要有能力触发组件更新,即组件的自更新;为此,需要将整个渲染任务包装到一个effect中;一旦组件自身的响应式数据发生变化,组件就会自动重新执行渲染函数,从而完成更新。但是由于effect的执行是同步的,因此当响应式数据发生变化时,与之关联的副作用函数会同步执行。换句话说,如果多次修改响应式数据的值,将会导致渲染函数执行多次,这实际上是没有必要的。因此,需要设计一个机制,以使得无论对响应式数据进行多少次修改,副作用函数都只会执行一次。为此,需要实现一个调度器,当副作用函数需要重新执行时,不会立即执行它,而是将其缓冲到一个微任务队列中,等到执行栈清空后,再将它从微任务队列中取出并执行;有了缓存机制,有机会对任务进行去重,从而避免多次执行副作用函数带来的性能开销,使用Set数据结构来缓存任务队列,自动对任务进行去重。调度器,本质上是利用了微任务的异步执行机制,实现对副作用函数的缓冲。
- 组件实例与组件的生命周期
- 组件实例本质上是一个状态集合或一个对象,它维护着组件运行过程中的所有信息,如注册到组件的生命周期函数、组件渲染的子树subTree、组件是否已经被挂载、组件自身的状态data等;
- 首先从组件的选项对象中取得注册到组件上的生命周期函数,然后在合适的时机调用它们,这就是组件生命周期的实现原理;但由于可能存在多个同样的生命周期钩子,如来自mixins的生命周期钩子函数,因此需要将组件生命周期钩子序列化为一个数组。
- props与组件的被动更新
- 为组件传递的props数据,即组件的vnode.props对象;
- 组件选项对象中定义的props选项,即MyComponent.props对象;
- 将组件选项中定义的MyComponent.props对象和为组件传递的vnode.props对象相结合,最终解析出组件在渲染时需要使用的props和attrs数据;这里需要注意两点:1.在vue.js中,没有定义在MyComponnet.props选项中的props数据将存储到attrs对象中;2.默认值、类型校验等也是围绕MyComponent.props及vnode.props这两个对象展开的;
- props数据变化问题:props本质上是父组件的数据,当props发生变化时,会触发父组件重新渲染;
- patchComponent函数用来完成子组件的更新,由父组件自更新引起的子组件更新叫做子组件的被动更新;当子组件发生被动更新时,我们需要做的是:监测子组件是否真的需要更新,因为子组件的props可能是不变的;如果需要更新,则更新子组件的props、slots等内容;
- 子组件被动更新的最小实现:需要将组件实例添加到新的组件vnode对象上,即n2.component=n1.component;否则下次更新时将无法取得组件实例;instance.props对象本身是浅响应的,因此在更新组件的props时,只需要设置instance.props对象下的属性值即可触发组件重新渲染;
- setup函数的作用与实现
- setup函数主要用于配合组合式API,为用户提供一个地方,用于建立组合逻辑、创建响应式数据、创建通用函数、注册生命周期钩子等能力;在组件的整个生命周期中,setup函数只会在被挂载时执行一次,它的返回值可以有两种情况:1.返回一个函数,该函数将作为组件的render函数;2.返回一个对象,该对象中包含的数据将暴露给模板使用;
- setup函数暴露的数据可以在渲染函数中通过this来访问;
- setup函数接收2个参数,第一个参数是props数据对象,第二个参数是setupContext对象;
- 组件事件与emit的实现
- emit用来发射组件的自定义事件;
- 发射自定义事件的本质是根据事件名称去props数据对象中寻找对应的事件处理函数并执行;
- 插槽的工作原理与实现
- 组件的插槽指组件会预留一个槽位,该槽位具体要渲染的内容由用户插入;
- 组件模板中的插槽内容会被编译为插槽函数,而插槽函数的返回值就是具体的插槽内容;
- 渲染插槽内容的过程,就是调用插槽函数并渲染由其返回的内容的过程;
- 生命周期
- 通过onMounted注册的生命周期函数会被注册到当前组件实例的instance.mounted数组中,为了维护当前正在初始化的组件实例,定义了全局变量currentInstance,以及用来设置该变量的setCurrentInstance函数;
- 异步组件
- 异步组件,以异步的方式加载并渲染一个组件,这在代码分割、服务端下发组件等场景中尤为重要。
- 异步组件本质上是通过封装手段来实现友好的用户接口,从而降低用户层面的使用复杂度;
// 同步
import App from 'App.vue';
createApp(App).mount('#app');
// 异步,使用动态导入语句import()来加载组件,它会返回一个Promise实例,组件加载成功后,会调用createApp函数完成挂载,这样就实现了以异步的方式来渲染页面;
const loader = () => import('App.vue');
loader().then(App => {
createApp(App).mount('#app');
})
// 单个组件的异步加载渲染
<template>
<CompA />
<component :is="asyncComp" />
</template>
<script>
import {shallowRef} from 'vue';
import CompA from 'CompA.vue';
export default {
components: {CompA},
setup() {
const asyncComp = shallowRef(null);
// 异步加载CompB组件
import('CompB.vue').then(CompB => asyncComp.value=CompB);
return {
asyncComp
}
}
}
</script>
- 封装defineAsyncComponent来定义异步组件,并直接使用components组件选项来注册它;defineAsyncComponent函数本质上是一个高阶组件,它的返回值是一个包装组件;包装组件会根据加载器的状态来决定渲染什么内容,如果加载器成功地加载了组件,则渲染被加载的组件,否则渲染一个占位内容;通常占位是一个注释节点,组件没有被加载成功时,页面会渲染一个注释节点来占位,也可使用空文本节点来占位;
- 异步组件通常以网络请求的形式进行加载,当加载组件的时间超过了指定时长后,会触发超时错误,如果用户配置了Error组件,则会渲染该组件;
- 异步加载的组件受网络影响较大,加载过程可能很慢,也可能很快;慢的时候是否可以考虑Loading组件来提供更好的用户体验;网络好的情况下,需要延迟展示Loading,防止闪烁情况;如200ms没有完成加载,才展示Loading组件,对于200ms内完成加载的情况,避免了闪烁问题的出现;
- 重试,指的是当加载出错时,有能力重新发起加载组件的请求;提供开箱即用的重试机制会提升用户的开发体验;异步组件加载失败后的重试机制,与请求服务端接口失败后的重试机制一样;
- 假设调用fetch函数会发送HTTP请求,且该请求会在1s后失败;为了实现失败后的重试,需要封装一个load函数
// load函数内部调用了fetch函数来发送请求,并得到一个Promise实例,接着添加catch语句块来捕获该实例的错误;当捕获到错误后,要么抛出错误,要么返回一个新的Promise实例,并把该实例的resolve和reject方法暴露给用户,让用户来决定下一步该怎么做;这里将新的Promise实例的resolve和reject分别封装为retry函数和fail函数,并将它们作为onError回调函数的参数;这样用户就可以在错误发生时主动选择重试或直接抛出错误;
function load(onError){
const p = fetch();
return p.catch(err=>{
return new Promise((resolve, reject) => {
const retry = () => resolve(load(onError))
const fail = () => reject(err);
onError(retry, fail)
})
})
}
- 函数式组件
- 函数式组件,允许使用一个普通函数定义组件,并使用该函数的返回值作为组件要渲染的内容;
- 函数式组件特点:无状态、编写简单且直观;
- 一个函数式组件,本质上就是一个普通函数,该函数的返回值是虚拟DOM;
- 在用户接口层面,一个函数式组件就是一个返回虚拟DOM的函数;
- 函数式组件没有自身状态,但它仍然可以接收由外部传入的props,为了给函数式组件定义props,需要在组件函数上添加静态的props属性;
- 在有状态组件的基础上,实现函数式组件将变得简单,因为挂载组件的逻辑可以复用mountComponent函数;在patch函数内部,通过检测vnode.type类型来判断组件的类型:如果vnode.type是一个对象,则它是一个有状态组件,且vnode.type是组件选项对象;如果vnode.type是一个函数,则它是一个函数式组件;无论是函数式组件还是有状态组件都可以通过mountComponent函数来完成挂载,也都可以通过patchComponent函数来完成更新;
- 在mountedComponent函数内检查组件的类型,如果是函数式组件,则直接将组件函数作为组件选项对象的render选项,并将组件函数的静态props属性作为组件的props选项即可,其他逻辑不变;严谨考虑,通过isFunctional变量实现选择性地执行初始化逻辑,因为对于函数式组件来说,无须初始化data及生命周期钩子;
- 内建组件和模块
- KeepAlive组件本质,缓存管理,再加上特殊的挂载和卸载逻辑;
- KeepAlive组件的实现需要渲染器层面的支持,这是因为KeepAlive组件在卸载时并不会真的卸载,否则就无法维持组件的当前状态了。将被KeepAlive组件从原容器搬到另一个隐藏的容器,实现假卸载;当搬运到隐藏容器中的组件需要再次挂载时,也不执行真正的挂载逻辑,而是该组件从隐藏容器中再搬运到原容器,这个过程对应到组件的生命周期,即activated和deactivated;
- 卸载一个被KeepAlive的组件时,并不会真的被卸载,而会被移动到一个隐藏容器中(失活);当重新挂载该组件时,也不是真正的挂载,而是从隐藏容器中取出,再放回原容器中,即页面中(激活);
- KeepAlive组件本身并不会渲染额外的内容,它的渲染函数最小只返回需要被KeepAlive的组件,把这个需要被KeepAlive的组件称为内部组件,KeepAlive组件会对内部组件进行操作,主要是在内部组件的vnode对象上添加一些标记属性,以便渲染器能够据此执行特定的逻辑,这些标记属性包括shouldKeepAlive,keepAliveInstance,keepAlive;
- KeepAlive组件支持props:include,显示地配置应该被缓存的组件;exclude显示地配置不应该被缓存组件;max,缓存设置最大容量;
- Teleport组件:可以将指定内容渲染到特定容器中,而不受DOM层级的限制;to属性指定渲染目标如to="body"
- Transition组件:当DOM元素被挂载时,将动效添加到该DOM元素上;当DOM元素被卸载时,不要立即卸载DOM元素,而是等到附加到该DOM上的动效执行完成后再卸载它;
- 过渡效果,本质上是一个DOM元素在两种状态间的切换,浏览器会根据过渡效果自行完成DOM元素的过渡;这里的过渡指的是持续时长、运动曲线、要过渡的属性等;实现动效的过程分为多个阶段,如beforeEnter、enter、leave等;
参考&感谢各路大神
1. vue.js设计与实现-霍春阳
宝剑锋从磨砺出,梅花香自苦寒来。