当页面越来越大,内容越来越多,那么对应的Vnode也会非常多,这时候就需要组件化,组件的好处是可以复用,然后可以将页面拆为多个部分,分别来构建。
12.1 渲染组件
这一节主要说的是:之前path函数中有处理的虚拟节点的类型中没有组件类,有string普通标签,Text文本标签,Fragment多组标签,Comment注释标签,type为Object是组件标签
我们改如何用设计组件的结构呢?组件对象有哪些描述,和拥有什么能力?
组件的本身是对页面内容的封装,它用来描述页面内容的一部分,因此一个组件必须包含一渲染函数,即render函数,有了render函数,渲染器就可以完成组件渲染了
.....
else if (typeof type === 'object' || typeof type === 'function') { // 如果是组件
// component
if (!n1) { // 当旧的Vnode不存在的时候
mountComponent(n2, container, anchor)
} else {
patchComponent(n1, n2, anchor)
}
.....
function MyFuncComp= {
name:"MyFuncComp",
data()
{},
render()
{
return { type: 'h1', children: props.title }
}
}
MyFuncComp.props = { // 组件的属性
title: String
}
const CompVNode = {
type: MyFuncComp,
props: {
title: 'A Big Title'
}
}
renderer.render(CompVNode, document.querySelector('#app'))
注意区分renderer.render和组件的render,组件的render是返回组件的虚拟DOM
那么这里我们总结一下Vnode中所有节点和子节点类型:
// ①组件类型
{
type: MyFuncComp,
props: {
title: 'A Small Title'
}
}
// ②多个子节点
{
type: Fragment,
children:[]
}
// ③普通标签类型
{
type: "xxxx",
props: {
title: 'A Small Title'
}
}
// ④文本类型
{
type: Text,
}
// ⑤注释类型
{
type: Comment,
}
// 子节点的类型,
{
children:null; //没有子节点
children:"xxx"; // 子节点为文本
children:[] // 子节点为一组
}
由以上的总结我们发现,
①节点类型有5种,而节点的子节点类型有三种,
②其中“多个子节点“” 文本类型,注释类型,都是没有props的,
③多个子节点类型的子节点一定是一个数组,
④子节只有一个节点的时候也是一个数组,而不是一个对象
⑤组件类型没有children子组件,他的Vnode是放在类型的对象中,
还要特别注意:区分Fragment和组件类型*
12.1 组件状态与自更新
这一章实现组件状态(组件的数)和组件渲染之间的关系
组件对象有一个data函数返回组件所以状态数据对象,而组件的render方法中应该可以用用this访问data中的数据,而data返回的对象也应该是响应式的,因为这是要是实现,当data中的数据改变,组件将重新渲染。但是不应该每次修改data都同步渲染,应该等所有操作结束再渲染一次,所以使用调度器,使用异步队列
// **任务缓存队列,用一个 Set 数据结构来表示,这样就可以自动对任务进行去重**
const queue = new Set()
// 一个标志,代表是否正在刷新任务队列
let isFlushing = false
//创建一个立即resolve的Promise 实例
const p= Promise.resolve()
// 调度器的主要函数,用来将一个任务添加到缓冲队列中,并开始刷新队列
function queueJob(job){
//将job添加到任务队列queue 中
queue.add(job)
//如果还没有开始刷新队列,则刷新之
if(!isFlushing)
{
//将该标志设置为true 以避免重复刷新
isFlushing= true
//在微任务中刷新缓冲队列
p.then(()=>{
try {
// 执行任务队列中的任务
queue.forEach(job => job())
}
finally {
// 重置状态
isFlushing = false
queue.length = 0
}
})
}
}
function mountComponent(vnode, container,anchor){
const componentOptions = vnode.type
const { render, data } = componentoptions
const state = reactive(data())
effect(()=>{
const subTree = render.call(ctx, ctx)
patch(null, subTree, container, anchor)
},{
//指定该副作用函数的调度器为queueJob 即可
scheduler: queueJob
})
}
注意:上面使用Set存储副作用函数,会自动去重,就不会收集单一代理对象多个相同副作用函数,不用多次执行,如果不去重,依然还是做不到节省多次渲染的开销
上面这个patch一直未null,我们应该让一个新旧组件对比,使用Diff复用新旧组件的DOM节点
12.3 组件实例与组件的声明周期
这一节讲得是组件实例,组件实例本质上是一个对象,包含与组件有关的状态信息,以及组件的声明周期,数据的响应式等等
function mountComponent(vnode, container, anchor) {
const componentOptions = vnode.type
//从组件选项对象中取得组件的生命周期函数
const {
render, data, beforeCreate, created, beforeMount, mounted, beforeUpdate,
updated
} = componentOptions
//在这里调用beforeCreate钩子
beforeCreate && beforeCreate()
const state = reactive(data()) // 生成响应式数据
const instance = {
state,
isMounted: false,// 是否已经被挂载过
subTree:null
}
vnode.comoponent = instance //将组件的实例挂到虚拟DOM上
//在这里调用created 钩子
created && created.call(state)
// 组件渲染,放到effect函数中进行追踪,当state数据改执行副作用函数
effect(() => {
const subTree = render.call(state, state)
if (!instance.isMounted) { // 如果是第一次挂载
//在这里调用beforeMount钩子
beforeMount && beforeMount.call(state)
patch(null, subTree, container, anchor)
instance.isMounted = true
//在这里调用 mounted钩子
mounted && mounted.call(state)
} else {
//在这里调用beforeUpdate钩子
beforeUpdate && beforeUpdate.call(state)
patch(instance.subTree, subTree, container, anchor)
//在这里调用updated钩子
updated && updated.call(state)
instance.subTree = subTree
}
},{
scheduler:queueJob
})
}
那么组件被挂载玩后 对应的虚拟DOM的结构是:
const Vnode={
type:MyComponent,
props:{}, //
comoponent:
{
state:ractive((MyComponent.data)),// 组件实例的状态响应式数据
isMounted:true ,// 是否已经被挂载过
subTree:MyComponent.render(),// 组件旧的虚拟dom
}
}
组件的选项对象结构是
const MyComponent= {
name:"MyComponent",// 组件的名字
props:{}, // 选项对象的props
data() // 组件的状态数据
{
return { foo:"xxx"}
},
render() //组件的render函数,返回组件的Vnode
{
return { type: 'div', children: "文本xxx" }
},
created() // 组件的生命周期函数
{
}
//....
}
注意区分组件的实例,和组件的选项对象,组件的选项对象,存储着组件的声明周期,而组件的实例,存储组件渲染以及组件响应式状态数据,以及被渲染的旧的组件虚拟DOM,
在实际的Vue中组件的生命周期函数选项对象是一个数组,因为有来着mixins的生命周期函数,
这里有一个问题,就是mountComponent是走没有旧Vnode这一个分支的,那么为什么组件会有已近被挂载呢?
else if (typeof type === 'object' || typeof type === 'function') {
if (!n1) { // 没有旧Vnode的情况下
mountComponent(n2, container, anchor);// 直接渲染组件
} else {
patchComponent(n1, n2, anchor)
}
}
12.4 props与组件的被动更新
这一节讲的是如何处理组件的props,对于一个组件来说他有两个Props,
(1)为组件传递props数据,即组件的Vnode.props对象 // 传入的属性
(2)组件选项对象中定义的props选项,即MyComput.props对象 // 组件需要接收的属性
这两个props处理原则是,只有当是需要接收的属性,和传入的属性一样,存储在props中,否则,传入的属性就是普通属性,存储在attrs中 ,也就是说props中是传入的属性,并且组件内也接收的属性,attrs是组件出入的属性,但是组件内部为接收的属性,
props会被使用shallowReactive给变成浅响应式数据,添加到组件实例上,props是父组件的数据,props改变会触发父组件重新渲染
在Vue3中,没有定义在MyComponent.props选项中的props数据将存储到attrs对象中
当父组件的中修改props,父组件的渲染函数会重新执行,渲染器发现父组件的subTree包含组件类型的虚拟节点,会调用patchCompnent函数完成子组件的更新,
由父组件更新引起子组件更新叫做子组件的被动更新
当子组件更新的时候,需要做的是:
(1)检测子组件是否正的需要更新,因为子组件的props可能是不变的
(2)如果需要更新,则更新子组件的props、slots等内容
function patchComponent(n1, n2, anchor) {
const instance = (n2.component = n1.component)
const { props } = instance
if (hasPropsChanged(n1.props, n2.props)) {
const [ nextProps, nex9tAttrs ] = resolveProps(n2.type.props, n2.props)
for (const k in nextProps) {
props[k] = nextProps[k]
}
for (const k in props) {
if (!(k in nextProps)) delete props[k]
}
}
}
function hasPropsChanged(
prevProps,
nextProps
) {
const nextKeys = Object.keys(nextProps)
if (nextKeys.length !== Object.keys(prevProps).length) {
return true
}
for (let i = 0; i < nextKeys.length; i++) {
const key = nextKeys[i]
return nextProps[key] !== prevProps[key]
}
return false
}
这里可以出一面试题:声明周期函数中this读取数据是先读取data和props中的数据?props中的变量可以和data中的变量重名吗?
由上面的代码可以知道,先读取data中的数据没有再读取props中的数据
12.5 setup函数的作用与实现
setup是vue3配合组合式aip,为用户提供一个地方,用于建立组合逻辑,创建响应式数据,创建通用函数,注册生命周期钩子等能力,组件的生命周期中setup只会在组件挂载的时候执行一次
这一章主要将了setup函数的两个不同类型的返回值和参数构成。
①setup返回值是 函数类型,返回的是一个Vnode,他会代替render中的Vnode,
②如果是返回的是一个对象,这个对象中的数据也是响应式的,和data,props中的数据一样,都会在声明周期函数的this中被访问的到,他们顺序是data=>props=>stataResult
setup函数有两个参数,第一个是props(浅代理只读),第二个是setupContext,setupContext使用由slots,emit,attrs,expose等构成的对象
12.6 组件事件与emit的实现
emit在setupContext中,也是setup()函数的参数,当我们在setup中执行emit的时候,自定义函数会被编译为on开头的属性,存放在组件的Vnode的Props中,emit实际就是在Props中找这个函数执行他,
注意之前实现的组件的Vnode的props只有,在Vnode.type中的Props也有声明的情况下才会被保留为最终的Props,也就是只有子组件想接收的Props属性才会在最终的Props中,其他的都会在attrs中,但是自定义事件这里,默认让他在最终的Props,也就是说事件属性,不会被Props过滤
12.7插槽的工作原理与实现
先观察一下Vue的插槽是怎么用的
<FancyButton>
Click me! <!-- 插槽内容 -->
<template #header>
</template>
<span style="color:red">Click me!</span>
<AwesomeIcon name="plus" />
</FancyButton>
模板
<button class="fancy-btn">
<slot></slot> <!-- 插槽出口 -->
</button>
插槽分为默认插槽,有名插槽
上面没有使用template
的都会收集到默认插槽,插槽中的内容可以是普通的html标签,也可以是组件
那么这样的组件的的Vnode是怎样的呢?
//父组件的渲染函数function render(){return{
type:MyComponent,
//组件的children 会被编译成一个对象
children:{
header(){
return { type:'h1',children:‘我是标题']
},
body(){
return { type:'section',children:‘我是内容]
footer(){
return { type:'p',children:‘我是注脚'}
12.1中说组件的Vnode没有children是错的,组件的children,就是插槽的Vnode,是对象
插槽就Vnode.children 他会被添加到组件对象实例中,并且也会放到声明周期函数created中,当声明周期函数created中this.$slots可以就会访问到组件children上的所有插槽的Vnode,
注意solts是不能在created函数中设置,而props和data,setupResult的值都可以修改
12.8 注册生命周期
这一节讲的是在setup()中写声明周期函数,将这些声明周期函数注册的组件上中,每个组件都有自己的setup和自己的生命周期函数,如何让每个函数能正确的注册到自己的组件上?
注意onMounted是全局函数,当在setup中执行的时候,其实是将函数传递到了组件的实例对象上
let currentInstance = null// 存储当前需要挂载的组件的实例对象
function onMounted(fn) {
if (currentInstance) {
currentInstance.mounted.push(fn)
}
}
// 将出入的Props分类
function resolveProps(options, propsData) {
const props = {}
const attrs = {}
for (const key in propsData) {
if ((options && key in options) || key.startsWith('on')) {
props[key] = propsData[key]
} else {
attrs[key] = propsData[key]
}
}
return [ props, attrs ]
}
function mountComponent(vnode, container, anchor) {
const isFunctional = typeof vnode.type === 'function' //虚拟组件的类型是不是函数
let componentOptions = vnode.type
if (isFunctional) { //如果是函数,返回的就是Vnode,依然变成对象类型
componentOptions = {
render: vnode.type, // 返回虚拟DOM
props: vnode.type.props // 构建想要从父组件获得的props
}
}
let { render, data, setup, beforeCreate, created, beforeMount, mounted, beforeUpdate, updated, props: propsOption } = componentOptions
beforeCreate && beforeCreate() // 声明周期
const state = data ? reactive(data()) : null // 组件的data数据变成响应式数据
const [props, attrs] = resolveProps(propsOption, vnode.props) // 将想要获得的Props和传入的做一个分类
const slots = vnode.children || {} // 获取组件的插槽的虚拟DOM
const instance = { // 组件实例
state, // 组件data的睡觉
props: shallowReactive(props), // 最终想要传入的数据+包含事件
isMounted: false,
subTree: null,
slots,
mounted: []
}
// event事件名称,payload事件传的参数
function emit(event, ...payload) {
const eventName = `on${event[0].toUpperCase() + event.slice(1)}`
const handler = instance.props[eventName];// 到最终的Props中的的属性中中
if (handler) {
handler(...payload) // 执行父组件的函数
} else {
console.error('事件不存在')
}
}
// setup
let setupState = null
if (setup) { //如果使用setup
const setupContext = { attrs, emit, slots } // setup的参数
const prevInstance = setCurrentInstance(instance) ;//将当前实例存给全局变量currentInstance ,返回以前的实例
const setupResult = setup(shallowReadonly(instance.props), setupContext);//执行setup
setCurrentInstance(prevInstance);// 旧的变新的,新的变旧的
if (typeof setupResult === 'function') { //如果返回的结果是函数就是返回的虚拟DOM
if (render) console.error('setup 函数返回渲染函数,render 选项将被忽略')
render = setupResult
} else { // 返回的是一个对象
setupState =setupResult
}
}
vnode.component = instance
const renderContext = new Proxy(instance, {
get(t, k, r) {
const { state, props, slots } = t
if (k === '$slots') return slots
if (state && k in state) {
return state[k]
} else if (k in props) {
return props[k]
} else if (setupState && k in setupState) {
return setupState[k]
} else {
console.error('不存在')
}
},
set (t, k, v, r) {
const { state, props } = t
if (state && k in state) {
state[k] = v
} else if (k in props) {
props[k] = v
} else if (setupState && k in setupState) {
setupState[k] = v
} else {
console.error('不存在')
}
}
})
// created
created && created.call(renderContext);// 这个声明周期中可以读取,组件的插槽的虚拟DOM,也可以读取data,props,setup中返回的响应式数据
effect(() => {
const subTree = render.call(renderContext, renderContext)
if (!instance.isMounted) {
beforeMount && beforeMount.call(renderContext)
patch(null, subTree, container, anchor)
instance.isMounted = true
mounted && mounted.call(renderContext)
instance.mounted && instance.mounted.forEach(hook => hook.call(renderContext))// 执行setup中定义的mounted函数
} else {
beforeUpdate && beforeUpdate.call(renderContext)
patch(instance.subTree, subTree, container, anchor)
updated && updated.call(renderContext)
}
instance.subTree = subTree
}, {
scheduler: queueJob
})
}
第十二章 组件的实现原理
(二十八)组件的挂载和更新为啥要单独处理?mountComponent的作用是?中的调度器是什么原理?
(1)因为组件的Vnode type是函数或者对象类型,不能直接获取props和children,因为他的Vnode存在于type值中,要用mountComponent和patchComponent单独处理。
(2)当没有旧Vnode的时候调用moutComponent渲染组件,他获取组件的render函数,data函数,获得Vnode和data,执行path进行渲染,Vnode的this执行data返回的对象
(3)mountComponent使用effct来代理,data会被代理,path会被代理,当data中的数据改变,会立即触发执行path,如果是连续更改data中的数据,则会造成性能问题,所以可以用scheduler来缓冲,等所有的数据改变在重执行path一次,
(二十九)组件的生命周期中更新是什么?为啥要在mountComponent中用instance.isMount来判断是不是要使用旧的Vnode?按理说mountComponent说明旧Vnode不存在啊?
(1)在初始化data中的数据之前就执行beforeCreate函数,之后执行created函数
执行完path之后执行mounted函数,当发现组件的实例的instance.isMount是以挂载后执行path后执行updated
(2)我想是虽然就Vnode不存在,但是如果instance.isMount以挂载,说明instance.subTree是存在的,这样比重新渲染要不耗费性能,主要要搞清楚组件的更新是在干什么,什么情况下会更新