组件标识
vnode的type是一个对象就说明是组件,组件是对页面内容的封装,是用来描述页面内容的一部分。因此,一个组件必须包含一个render函数,并且渲染函数的返回值应该是vnode
js
const MyComponent = {
// 组件名称,可选
name: 'MyComponent',
// 组件的渲染函数,其返回值必须为虚拟 DOM
render() {
// 返回虚拟 DOM
return {
type: 'div',
children: `我是文本内容`
}
}
}
// 该 vnode 用来描述组件,type 属性存储组件的选项对象
const CompVNode = {
type: MyComponent
// ...
}
组件渲染
在patch添加判断type是组件的逻辑,如果没有oldN就挂载组件
js
function patch(oldN, newN, container, anchor) {
if (oldN && oldN.type !== newN.type) {
unmount(oldN)
oldN = null
}
const { type } = newN
if (typeof type === 'string') {
// 作为普通元素处理
} else if (type === Text) {
// 作为文本节点处理
} else if (type === Fragment) {
// 作为片段处理
} else if (typeof type === 'object') {
// vnode.type 的值是选项对象,作为组件来处理
if (!oldN) {
// 挂载组件
mountComponent(newN, container, anchor)
}
}
思路:
- 通过vnode.type拿到组件的选项
- 因为组件的render是返回页面的vdom,调用patch挂载真实dom
js
function mountComponent(vnode, container, anchor) {
// 通过 vnode 获取组件的选项对象,即 vnode.type
const componentOptions = vnode.type
// 获取组件的渲染函数 render
const { render } = componentOptions
// 执行渲染函数,获取组件要渲染的内容,即 render 函数返回的虚拟 DOM
const subTree = render()
// 最后调用 patch 函数来挂载组件所描述的内容,即 subTree
patch(null, subTree, container, anchor)
}
结果:
组件data与更新
在组件定义data函数,同时在render函数中通过this访问由data函数返回的状态数据
js
const MyComponent = {
name: 'MyComponent',
// 用 data 函数来定义组件自身的状态
data() {
return {
foo: 'hello world'
}
},
render() {
return {
type: 'div',
children: `foo 的值是: ${this.foo}` // 在渲染函数内使用组件状态
}
}
}
思路:
- 通过componentOptions拿到组件的data函数并执行拿到数据
- 把拿到的数据传入reactive得到响应数据
- 因为需要修改页面数据后重新执行render函数,所以将render跟patch传入effect,effect一开始会自执行一次,render再通过call将this执行state响应数据,这样render的this.foo就变成state.foo
diff
function mountComponent(vnode, container, anchor) {
// 通过 vnode 获取组件的选项对象,即 vnode.type
const componentOptions = vnode.type
// 获取组件的渲染函数 render
- const { render } = componentOptions
+ const { render, data } = componentOptions
+ // 调用 data 函数得到原始数据,并调用 reactive 函数将其包装为响应式数据
+ const state = reactive(data())
- // 执行渲染函数,获取组件要渲染的内容,即 render 函数返回的虚拟 DOM
- const subTree = render()
- // 最后调用 patch 函数来挂载组件所描述的内容,即 subTree
- patch(null, subTree, container, anchor)
+ // 将组件的 render 函数调用包装到 effect 内
+ effect(() => {
+ // 调用 render 函数时,将其 this 设置为 state,
+ // 从而 render 函数内部可以通过 this 访问组件自身状态数据
+ const subTree = render.call(state, state)
+ // 最后调用 patch 函数来挂载组件所描述的内容,即 subTree
+ patch(null, subTree, container, anchor)
+ })
}
异步更新
由于effect的执行是同步的,因此当响应式数据发生变化时,与之关联的副作用函数会同步执行,所以需要无论对响应式数据进行多少次修改,副作用函数都只会重新执行一次
思路:
- 定义存放任务set队列、是否正在刷新队列标识、微任务变量
- queueJob函数作为调度器,当修改响应数据时,如果effect函数第二个参数有传scheduler参数就会将effect队列中的effect依次传给scheduler
- 当多个effect传给queueJob会保存到任务set队列,isFlushing第一次为false,把isFlushing设置为true、执行微任务的then函数,后续执行queueJob只会执行queue.add(job),当数据修改完成后,执行微任务队列中的 queue.forEach(job => job()),此时queue是多次添加后的数据
js
// 任务缓存队列,用一个 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.clear = 0
}
})
}
}
diff
function mountComponent(vnode, container, anchor) {
// 通过 vnode 获取组件的选项对象,即 vnode.type
const componentOptions = vnode.type
// 获取组件的渲染函数 render
const { render, data } = componentOptions
// 调用 data 函数得到原始数据,并调用 reactive 函数将其包装为响应式数据
const state = reactive(data())
// 将组件的 render 函数调用包装到 effect 内
effect(() => {
// 调用 render 函数时,将其 this 设置为 state,
// 从而 render 函数内部可以通过 this 访问组件自身状态数据
const subTree = render.call(state, state)
// 最后调用 patch 函数来挂载组件所描述的内容,即 subTree
patch(null, subTree, container, anchor)
},
+ {
+ // 指定该副作用函数的调度器为 queueJob 即可
+ scheduler: queueJob
+ })
}
组件实例
组件实例本质上就是一个状态集合(或一个对象),它维护着组件运行过程中的所有信息,例如注册到组件的生命周期函数、组件渲染的子树(subTree)、组件是否已经被挂载、组件自身的状态 (data)
思路:
- 在挂载组件时定义instance对象,instance中有state(组件自身的状态数据)、isMounted(组件是否已经被挂载)、subTree(组件所渲染的内容)
- 将instance设置在vnode的component上
- 在effect中判断isMounted,为false时,patch函数第一个参数传null,并将isMounted设置为true,这样当更新发生时就不会再次进行挂载操作
- 为true时,patch函数第一个参数传instance.subTree也就是上次vnode,与新的vnode进行比较更新
- 最终将subTree赋值给instance.subTree
diff
function mountComponent(vnode, container, anchor) {
// 通过 vnode 获取组件的选项对象,即 vnode.type
const componentOptions = vnode.type
// 获取组件的渲染函数 render
const { render, data } = componentOptions
// 调用 data 函数得到原始数据,并调用 reactive 函数将其包装为响应式数据
const state = reactive(data())
+ // 定义组件实例,一个组件实例本质上就是一个对象,它包含与组件有关的状态信息
+ const instance = {
+ // 组件自身的状态数据,即 data
+ state,
+ // 一个布尔值,用来表示组件是否已经被挂载,初始值为 false
+ isMounted: false,
+ // 组件所渲染的内容,即子树(subTree)
+ subTree: null
+ }
+ // 将组件实例设置到 vnode 上,用于后续更新
+ vnode.component = instance
// 将组件的 render 函数调用包装到 effect 内
effect(() => {
// 调用 render 函数时,将其 this 设置为 state,
// 从而 render 函数内部可以通过 this 访问组件自身状态数据
const subTree = render.call(state, state)
+ if (!instance.isMounted) {
+ // 初次挂载,调用 patch 函数第一个参数传递 null
+ patch(null, subTree, container, anchor)
+ // 重点:将组件实例的 isMounted 设置为 true,这样当更新发生时就不会再次进行挂载操作,
+ // 而是会执行更新
+ instance.isMounted = true
+ } else {
+ // 当 isMounted 为 true 时,说明组件已经被挂载,只需要完成自更新即可,
+ // 所以在调用 patch 函数时,第一个参数为组件上一次渲染的子树,
+ // 意思是,使用新的子树与上一次渲染的子树进行打补丁操作
+ patch(instance.subTree, subTree, container, anchor)
+ }
+ // 更新组件实例的子树
+ instance.subTree = subTree
}, {
// 指定该副作用函数的调度器为 queueJob 即可
scheduler: queueJob
})
}
组件生命周期
思路:
- 在组件选项拿到生命周期函数
- 在data、instance之前调用beforeCreate,所以beforeCreate拿不到data的数据与组件实例
- 在data、instance后,执行effect函数前,执行created并将this指向state,因为create函数会用到this
- instance.isMounted为false时说明是要挂载,所以在patch之前执行beforeMount,并将this指向state
- instance.isMounted为true时说明是要更新,所以在patch之前执行updated,并将this指向state
diff
function mountComponent(vnode, container, anchor) {
// 通过 vnode 获取组件的选项对象,即 vnode.type
const componentOptions = vnode.type
+ // 从组件选项对象中取得组件的生命周期函数
+ const { render, data, beforeCreate, created, beforeMount, mounted, beforeUpdate, updated } = componentOptions
+ // 在这里调用 beforeCreate 钩子
+ beforeCreate && beforeCreate()
// 调用 data 函数得到原始数据,并调用 reactive 函数将其包装为响应式数据
const state = reactive(data())
// 定义组件实例,一个组件实例本质上就是一个对象,它包含与组件有关的状态信息
const instance = {
// 组件自身的状态数据,即 data
state,
// 一个布尔值,用来表示组件是否已经被挂载,初始值为 false
isMounted: false,
// 组件所渲染的内容,即子树(subTree)
subTree: null
}
// 将组件实例设置到 vnode 上,用于后续更新
vnode.component = instance
+ // 在这里调用 created 钩子
+ created && created.call(state)
// 将组件的 render 函数调用包装到 effect 内
effect(() => {
// 调用 render 函数时,将其 this 设置为 state,
// 从而 render 函数内部可以通过 this 访问组件自身状态数据
const subTree = render.call(state, state)
console.log(subTree)
if (!instance.isMounted) {
+ // 在这里调用 beforeMount 钩子
+ beforeMount && beforeMount.call(state)
// 初次挂载,调用 patch 函数第一个参数传递 null
patch(null, subTree, container, anchor)
// 重点:将组件实例的 isMounted 设置为 true,这样当更新发生时就不会再次进行挂载操作,
// 而是会执行更新
instance.isMounted = true
+ // 在这里调用 mounted 钩子
+ mounted && mounted.call(state)
} else {
+ // 在这里调用 beforeUpdate 钩子
+ beforeUpdate && beforeUpdate.call(state)
// 当 isMounted 为 true 时,说明组件已经被挂载,只需要完成自更新即可,
// 所以在调用 patch 函数时,第一个参数为组件上一次渲染的子树,
// 意思是,使用新的子树与上一次渲染的子树进行打补丁操作
patch(instance.subTree, subTree, container, anchor)
+ // 在这里调用 updated 钩子
+ updated && updated.call(state)
}
// 更新组件实例的子树
instance.subTree = subTree
}, {
// 指定该副作用函数的调度器为 queueJob 即可
scheduler: queueJob
})
}
结果:
组件props
对于一个组件来说,有两部分关于 props 的内容我们需要关心:
- 为组件传递的 props 数据,即组件的 vnode.props 对象;
- 组件选项对象中定义的 props 选项,即 MyComponent.props 对象。
js
const MyComponent = {
name: 'MyComponent',
// 组件接收名为 title 的 props,并且该 props 的类型为 String
props: {
title: String
},
render() {
return {
type: 'div',
children: `count is: ${this.title}` // 访问 props 数据
}
}
}
const vnode = {
type: MyComponent,
props: {
title: 'A big Title',
other: this.val
}
}
思路:
- 拿到props,转换成浅响应数据定义在组件实例上
diff
function mountComponent(vnode, container, anchor) {
const componentOptions = vnode.type
+ // 从组件选项对象中取出 props 定义,即 propsOption
+ const { render, data, props: propsOption /* 其他省略 */ } = componentOptions
beforeCreate && beforeCreate()
const state = reactive(data())
+ // 调用 resolveProps 函数解析出最终的 props 数据与 attrs 数据
+ const [props, attrs] = resolveProps(propsOption, vnode.props)
const instance = {
state,
+ // 将解析出的 props 数据包装为 shallowReactive 并定义到组件实例上
+ props: shallowReactive(props),
isMounted: false,
subTree: null
}
vnode.component = instance
// 省略部分代码
}
- 遍历vnode的prop,再判断vnode中的props是否在组件的props中
- 如果在组件的props中就说明是合法的props,存到props对象中
- 否则就存到attrs对象中
js
// resolveProps 函数用于解析组件 props 和 attrs 数据
function resolveProps(options, propsData) {
const props = {}
const attrs = {}
// 遍历为组件传递的 props 数据
for (const key in propsData) {
if (key in options) {
// 如果为组件传递的 props 数据在组件自身的 props 选项中有定义,则将其视为合法的 props
props[key] = propsData[key]
} else {
// 否则将其作为 attrs
attrs[key] = propsData[key]
}
}
// 最后返回 props 与 attrs 数据
return [props, attrs]
}
父组件props修改
当vnode的props发生改变时,需要做的是:
- 检测子组件是否真的需要更新,因为子组件的 props 可能是不变的
- 如果需要更新,则更新子组件的 props、slots 等内容
js
// 父组件要渲染的内容
const vnode = {
type: MyComponent,
props: {
title: 'A Big Title'
}
}
// 父组件要渲染的内容
const vnode = {
type: MyComponent,
props: {
title: 'A Small Title'
}
}
思路:
当组件发生改变会执行patchComponent
diff
function patch(oldN, newN, container, anchor) {
else if (typeof type === 'object') {
// 如果 newN.type 的值的类型是对象,则它描述的是组件
if (!oldN) {
mountComponent(newN, container, anchor)
+ } else {
+ patchComponent(oldN, newN, anchor)
+ }
}
}
- 将旧vnode的实例赋值给新vnode
- 比较新旧vnode的props,如果需要更新就resolveProps,比较新组件定义的props跟父组件传入的props,获取到新的合法props
- 遍历新的合法props更新旧vnode的props
- 遍历旧vnode的props,判断是否在新vnode的props中,不存在说明不存在,需要删除
js
function patchComponent(oldN, newN, anchor) {
// 获取组件实例,即 oldN.component,同时让新的组件虚拟节点 newN.component也指向组件实例
const instance = newN.component = oldN.component
// 获取当前的 props 数据
const { props } = instance
// 调用 hasPropsChanged 检测为子组件传递的 props 是否发生变化,如果没有变化,则不需要更新
if (hasPropsChanged(oldN.props, newN.props)) {
// 调用 resolveProps 函数重新获取 props 数据
const [nextProps] = resolveProps(newN.type.props, newN.props)
// 更新 props
for (const k in nextProps) {
props[k] = nextProps[k]
}
// 删除不存在的 props
for (const k in props) {
if (!(k in nextProps)) delete props[k]
}
}
}
- 比较组件新props与组件旧props的长度,不一样说明需要更新props,返回true
- 组件新旧props长度一样,但值不一样说明需要更新,返回true
- 最终返回false,不需要更新
js
function hasPropsChanged(prevProps, nextProps) {
const nextKeys = Object.keys(nextProps)
const prevKeys = Object.keys(prevProps)
// 如果新旧 props 的数量变了,则说明有变化
if (nextKeys.length !== prevKeys.length) return true
for (let i = 0; i < nextKeys.length; i++) {
const key = nextKeys[i]
if (nextProps[key] !== prevProps[key]) return true
}
return false
}
- 给instance做一层proxy代理renderContext
- 生命周期的this指向renderContext
- 当访问的数据不在state中,就尝试访问props
- 当修改数据在state中,就修改state,如果在props中就报错,props是只读的
js
function mountComponent(vnode, container, anchor) {
// 省略部分代码
const instance = {
state,
props: shallowReactive(props),
isMounted: false,
subTree: null
}
vnode.component = instance
// 创建渲染上下文对象,本质上是组件实例的代理
const renderContext = new Proxy(instance, {
get(target, key, receiver) {
// 取得组件自身状态与 props 数据
const { state, props } = target
if (state && key in state) {
return state[key]
} else if (key in props) { //// 如果组件自身没有该数据,则尝试从props 中读取
return props[key]
} else {
console.error('不存在')
}
},
set(target, key, value, receiver) {
const { state, props } = target
if (state && key in state) {
state[key] = value
} else if (key in props) {
console.warn(`Attempting to mutate prop "${k}". Propsare readonly.`)
} else {
console.error('不存在')
}
}
})
// 生命周期函数调用时要绑定渲染上下文对象
created && created.call(renderContext)
// 省略部分代码
}
结果:
js
const vnode = {
type: MyComponent,
props: {
title: 'A Big Title'
}
}
const vnode1 = {
type: MyComponent,
props: {
title: 'A Small Title'
}
}
renderer.render(vnode, document.querySelector('#app'))
setTimeout(() => {
renderer.render(vnode1, document.querySelector('#app'))
},1000)
- 组件挂载执行mountComponent
- 将render的this指向renderContext,在render中数据都从renderContext访问
- 组件更新执行patchComponent
- instance的props是代理对象,当props的key被修改会触发effect,此时effect中instance.isMounted为true执行patch重新渲染
setup 函数的作用与实现
返回一个函数
该函数将作为组件的 render 函数
js
const Comp = {
setup() {
// setup 函数可以返回一个函数,该函数将作为组件的渲染函数
return () => {
return { type: 'div', children: 'hello' }
}
}
}
返回一个对象
该对象中包含的数据将暴露给模板使用
js
const Comp = {
setup() {
const count = ref(0)
// 返回一个对象,对象中的数据会暴露到渲染函数中
return {
count
}
},
render() {
// 通过 this 可以访问 setup 暴露出来的响应式数据
return { type: 'div', children: `count is: ${this.count}` }
}
}
setup 函数接收两个参数
第一个参数取得外部为组件传递的 props 数据对象,第二个参数是组件接口相关的数据和方法
js
const Comp = {
props: {
foo: String
},
setup(props, setupContext) {
props.foo // 访问传入的 props 数据
// setupContext 中包含与组件接口相关的重要数据
const { slots, emit, attrs, expose } = setupContext
// ...
}
}
思路:
- 首先从组件选项拿到setup
- setup第一个参数是通过resolveProps函数解析出最终的props并且只读,第二个参数是setupContext对象,暂时只有attrs
- setup函数返回值有对象与函数两种情况,如果是函数并且组件选项的render不存在就赋值给render作为渲染函数,如果是对象就作为数据
- 因为setup函数的返回值是对象作为数据的情况,需要在renderContext的get中判断,如果setupState存在并且key在setupState上,就返回setupState[k],在set中判断,如果setupState存在并且key在setupState上,就将最新的值赋值给setupState[key]
diff
function mountComponent(vnode, container, anchor) {
const componentOptions = vnode.type
+ // 从组件选项中取出 setup 函数
+ let { render, data, setup, /* 省略其他选项 */ } = componentOptions
beforeCreate && beforeCreate()
const state = data ? reactive(data()) : null
const [props, attrs] = resolveProps(propsOption, vnode.props)
const instance = {
state,
props: shallowReactive(props),
isMounted: false,
subTree: null
}
+ // setupContext,由于我们还没有讲解 emit 和 slots,所以暂时只需要attrs
+ const 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 的返回值不是函数,则作为数据状态赋值给 setupState
+ setupState = setupResult
+ }
vnode.component = instance
const renderContext = new Proxy(instance, {
get(target, key, receiver) {
// 取得组件自身状态与 props 数据
const { state, props } = target
if (state && key in state) {
return state[key]
} else if (key in props) { //// 如果组件自身没有该数据,则尝试从props 中读取
return props[key]
+ } else if (setupState && key in setupState) {
+ // 渲染上下文需要增加对 setupState 的支持
+ return setupState[key]
+ } else {
console.error('不存在')
}
},
set(target, key, value, receiver) {
const { state, props } = target
if (state && key in state) {
state[key] = value
} else if (key in props) {
console.warn(`Attempting to mutate prop "${key}". Propsare readonly.`)
+ } else if (setupState && key in setupState) {
+ // 渲染上下文需要增加对 setupState 的支持
+ setupState[key] = value
+ } else {
console.error('不存在')
}
}
})
// 省略部分代码
}
结果:
- setup返回一个函数
js
const Comp = {
setup() {
// setup 函数可以返回一个函数,该函数将作为组件的渲染函数
return () => {
return { type: 'div', children: 'hello' }
}
}
}
// 父组件要渲染的内容
const vnode = {
type: Comp,
}
renderer.render(vnode, document.querySelector('#app'))
2. setup返回一个对象
js
const Comp = {
setup() {
// 返回一个对象,对象中的数据会暴露到渲染函数中
return {
count: 1
}
},
render() {
// 通过 this 可以访问 setup 暴露出来的响应式数据
return { type: 'div', children: `count is: ${this.count}` }
}
}
// 父组件要渲染的内容
const vnode = {
type: Comp,
}
renderer.render(vnode, document.querySelector('#app'))
3. setup函数参数
js
const Comp = {
props: {
title: String
},
setup(props, setupContext) {
return () => ({ type: 'div', children: `title is: ${props.title}` })
}
}
// 父组件要渲染的内容
const vnode = {
type: Comp,
props: {
title: 'A Big Title'
}
}
renderer.render(vnode, document.querySelector('#app'))
组件事件与 emit 的实现
在组件通过@注入自定义事件
html
<MyComponent @change="handler" />
对应的vnode
js
const CompVNode = {
type: MyComponent,
props: {
onChange: handler
}
}
在组件setup用emit发射事件
js
const MyComponent = {
name: 'MyComponent',
setup(props, { emit }) {
// 发射 change 事件,并传递给事件处理函数两个参数
emit('change', 1, 2)
return () => {
return // ...
}
}
}
思路:
- emit函数第一个参数是自定义事件的名称字符串,将名称转换成原生的事件名称格式
- 第二个参数是传递的参数使用...收集,根据转换后的事件名称去props中寻找对应的事件处理函数,事件处理函数存在就将payload传入
diff
function mountComponent(vnode, container, anchor) {
// 省略部分代码
const instance = {
state,
props: shallowReactive(props),
isMounted: false,
subTree: null
}
+ // 定义 emit 函数,它接收两个参数
+ // event: 事件名称
+ // payload: 传递给事件处理函数的参数
+ function emit(event, ...payload) {
+ // 根据约定对事件名称进行处理,例如 change --> onChange
+ const 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 }
+ const setupContext = { attrs, emit }
// 省略部分代码
}
将on开头的自定义事件也放入props对象中
diff
function resolveProps(options, propsData) {
const props = {}
const attrs = {}
for (const key in propsData) {
- if (key in options) {
+ // 以字符串 on 开头的 props,无论是否显式地声明,都将其添加到 props数据中,而不是添加到 attrs 中
+ if (key in options || key.startsWith('on')) {
props[key] = propsData[key]
} else {
attrs[key] = propsData[key]
}
}
return [props, attrs]
}
结果:
js
const MyComponent = {
name: 'MyComponent',
props: {
title: String
},
setup(props, { emit }) {
// 发射 change 事件,并传递给事件处理函数两个参数
emit('input', 1, 2)
return () => {
return {
type: 'div',
children: `count is: ${props.title}` // 访问 props 数据
}
}
}
}
function handler(...num) {
console.log(num)
}
// 父组件要渲染的内容
const vnode = {
type: MyComponent,
props: {
title: 'A Big Title',
onInput: handler
}
}
renderer.render(vnode, document.querySelector('#app'))
插槽的工作原理与实现
MyComponent 组件
html
<template>
<header><slot name="header" /></header>
<div>
<slot name="body" />
</div>
<footer><slot name="footer" /></footer>
</template>
MyComponent 组件对应的render函数
js
function render() {
return [
{
type: 'header',
children: [this.$slots.header()]
},
{
type: 'body',
children: [this.$slots.body()]
},
{
type: 'footer',
children: [this.$slots.footer()]
}
]
}
父组件
html
<MyComponent>
<template #header>
<h1>我是标题</h1>
</template>
<template #body>
<section>我是内容</section>
</template>
<template #footer>
<p>我是注脚</p>
</template>
</MyComponent>
父组件对应的render函数,父组件的children是一个对象,key为插槽名称,value为插槽渲染内容,方便后续子组件通过this.$slots访问到
js
// 父组件的渲染函数
function render() {
return {
type: MyComponent,
// 组件的 children 会被编译成一个对象
children: {
header() {
return { type: 'h1', children: '我是标题' }
},
body() {
return { type: 'section', children: '我是内容' }
},
footer() {
return { type: 'p', children: '我是注脚' }
}
}
}
}
思路:
- 父组件插槽的内容放在children,通过vnode.children获得插槽内容
- 将插槽内容添加setupContext,方便在setup函数拿到
- 在执行组件render时,通过访问this.$slots指向父组件的children
diff
function mountComponent(vnode, container, anchor) {
// 省略部分代码
+ // 直接使用编译好的 vnode.children 对象作为 slots 对象即可
+ const slots = vnode.children || {}
const instance = {
state,
props: shallowReactive(props),
isMounted: false,
subTree: null,
// 将插槽添加到组件实例上
slots
}
+ // 将 slots 对象添加到 setupContext 中
+ const setupContext = { attrs, emit, slots }
const renderContext = new Proxy(instance, {
get(t, k, r) {
const { state, props, slots } = t
// 当 k 的值为 $slots 时,直接返回组件实例上的 slots
+ if (k === '$slots') return slots
// 省略部分代码
},
set(t, k, v, r) {
// 省略部分代码
}
})
// 省略部分代码
}
结果:
- 在setup函数成功拿到slots对象
js
const MyComponent = {
name: 'MyComponent',
props: {
title: String
},
setup(props, { emit, slots }) {
return () => {
return {
type: Fragment,
children: [
{
type: 'header',
children: [slots.header()]
},
{
type: 'body',
children: [slots.body()]
},
{
type: 'footer',
children: [slots.footer()]
}
]
}
}
},
}
// 父组件要渲染的内容
const vnode = {
type: MyComponent,
// 组件的 children 会被编译成一个对象
children: {
header() {
return { type: 'h1', children: '我是标题' }
},
body() {
return { type: 'section', children: '我是内容' }
},
footer() {
return { type: 'p', children: '我是注脚' }
}
}
}
renderer.render(vnode, document.querySelector('#app'))
2. 在子组件的render成功拿到this.$slots
js
const MyComponent = {
name: 'MyComponent',
props: {
title: String
},
render() {
return {
type:Fragment,
children:[
{
type: 'header',
children: [this.$slots.header()]
},
{
type: 'body',
children: [this.$slots.body()]
},
{
type: 'footer',
children: [this.$slots.footer()]
}
]
}
}
}
// 父组件要渲染的内容
const vnode = {
type: MyComponent,
// 组件的 children 会被编译成一个对象
children: {
header() {
return { type: 'h1', children: '我是标题' }
},
body() {
return { type: 'section', children: '我是内容' }
},
footer() {
return { type: 'p', children: '我是注脚' }
}
}
}
renderer.render(vnode, document.querySelector('#app'))
注册生命周期
setup函数中可以注册多个生命周期函数
js
const MyComponent = {
setup() {
onMounted(() => {
console.log('mounted 1')
})
// 可以注册多个
onMounted(() => {
console.log('mounted 2')
})
// ...
}
}
思路:
生命周期是外部定义,在setup函数中调用的函数,如果多个组件一起调用生命周期函数会分不清,需要定义当前组件实例,在调用setup前保存当前组件,调用后重置null
js
// 全局变量,存储当前正在被初始化的组件实例
let currentInstance = null
// 该方法接收组件实例作为参数,并将该实例设置为 currentInstance
function setCurrentInstance(instance) {
currentInstance = instance
}
onMounted接受一个函数参数,调用onMounted就将函数参数传入当前组件实例的mounted数组
js
function onMounted(fn) {
if (currentInstance) {
// 将生命周期函数添加到 instance.mounted 数组中
currentInstance.mounted.push(fn)
} else {
console.error('onMounted 函数只能在 setup 中调用')
}
}
在effect中遍历instance的mounted
diff
function mountComponent(vnode, container, anchor) {
// 省略部分代码
const instance = {
state,
props: shallowReactive(props),
isMounted: false,
subTree: null,
slots,
+ // 在组件实例中添加 mounted 数组,用来存储通过 onMounted 函数注册的生命周期钩子函数
+ mounted: []
}
// 省略部分代码
// setup
const setupContext = { attrs, emit, slots }
+ // 在调用 setup 函数之前,设置当前组件实例
+ setCurrentInstance(instance)
// 执行 setup 函数
const setupResult = setup(shallowReadonly(instance.props), setupContext)
+ // 在 setup 函数执行完毕之后,重置当前组件实例
+ setCurrentInstance(null)
// 省略部分代码
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
})
}
结果:
js
const MyComponent = {
name: 'MyComponent',
props: {
title: String
},
setup(props, { emit, slots }) {
onMounted(() => {
console.log('mounted 1')
})
// 可以注册多个
onMounted(() => {
console.log('mounted 2')
})
},
render() {
return {
type: Fragment,
children: [
{
type: 'header',
children: [this.$slots.header()]
},
{
type: 'body',
children: [this.$slots.body()]
},
{
type: 'footer',
children: [this.$slots.footer()]
}
]
}
}
}
// 父组件要渲染的内容
const vnode = {
type: MyComponent,
// 组件的 children 会被编译成一个对象
children: {
header() {
return { type: 'h1', children: '我是标题' }
},
body() {
return { type: 'section', children: '我是内容' }
},
footer() {
return { type: 'p', children: '我是注脚' }
}
}
}
renderer.render(vnode, document.querySelector('#app'))