《Vue.js 设计与实现》读书笔记 - 第12章、组件的实现原理
第12章、组件的实现原理
12.1 渲染组件
在渲染器内部的实现看,一个组件是一个特殊类型的虚拟 DOM 节点。之前在 patch
我们判断了 VNode 的 type
值来处理,现在来处理类型为对象的情况。
// n1 旧node n2 新node container 容器
function patch(n1, n2, container, anchor) {
if (n1 && n1.type !== n2.type) {
unmount(n1)
n1 = null
}
const { type } = n2
if (typeof type === 'string') {
// ...
} else if (type === Text) {
// ...
} else if (type === Fragment) {
// ...
} else if (typeof type === 'object') {
// 组件
if (!n1) {
// 挂载
mountComponent(n2, container, anchor)
} else {
patchComponent(n1, n2, anchor)
}
}
}
其中 mountComponent
就是先通过组件的 render
函数获取对应的 vnode
然后再挂载。
function mountComponent(vnode, container, anchor) {
const componentOptinos = vnode.type
const { render } = componentOptinos
const subTree = render()
patch(null, subTree, container, anchor)
}
在渲染时使用组件类型:
const MyComponent = {
name: 'MyComponent',
render() {
return {
type: 'div',
children: 'Text',
}
},
}
const CompVNode = {
type: MyComponent,
}
renderer.render(CompVNode, document.querySelector('#app'))
12.2 组件状态与自更新
完成了组件的初始渲染,现在开始设计组件自身的状态。
我们在渲染时把组件的状态设置为响应式,并把渲染函数放在 effect
中执行,这样就实现了组件状态改变时重新渲染。同时指定 scheduler
来让渲染队列在一个微任务中执行并进行去重。
function mountComponent(vnode, container, anchor) {
const componentOptinos = vnode.type
const { render, data } = componentOptinos
const state = reactive(data()) // 让组件的数据变成响应式
// 为了让组件状态发生变化时能自动渲染
effect(
() => {
const subTree = render.call(state, state)
patch(null, subTree, container, anchor)
},
{
scheduler: queueJob,
}
)
}
const MyComponent = {
name: 'MyComponent',
data() {
return {
foo: 'hello world',
}
},
render() {
return {
type: 'div',
children: `foo = ${this.foo}`,
}
},
}
12.3 组件实例与组件的生命周期
当状态修改导致组件再次渲染时,patch
不应该还是挂载,所以我们需要维护一个实例,记录组件的状态,是否被挂载和上一次的虚拟 DOM 节点。
同时我们的组件有很多生命周期函数,我们需要在相应的时机调用对应的生命周期函数。
function mountComponent(vnode, container, anchor) {
const componentOptinos = vnode.type
const {
render,
data,
beforeCreate,
created,
beforeMount,
mounted,
beforeUpdate,
updated,
} = componentOptinos
beforeCreate && beforeCreate()
const state = reactive(data()) // 让组件的数据变成响应式
const instance = {
state,
isMounted: false,
subTree: null,
}
vnode.component = instance
created && created.call(state)
// 为了让组件状态发生变化时能自动渲染
effect(
() => {
const subTree = render.call(state, state)
if (!instance.isMounted) {
// 检测组件是否已经被挂载
beforeMount && beforeMount.call(state)
patch(null, subTree, container, anchor)
instance.isMounted = true
mounted && mounted.call(state)
} else {
beforeUpdate && beforeUpdate.call(state)
patch(instance.subTree, subTree, container, anchor)
updated && updated.call(state)
}
instance.subTree = subTree
},
{
scheduler: queueJob,
}
)
}
12.4 props 与组件的被动更新
在 Vue3 中要显示指定需要的属性,如果没有指定将会被存储到 attrs
对象中。
function mountComponent(vnode, container, anchor) {
const componentOptinos = vnode.type
const {
render,
data,
props: propsOption,
// ...
} = componentOptinos
beforeCreate && beforeCreate()
const state = reactive(data ? data() : {}) // 让组件的数据变成响应式
const [props, attrs] = resolveProps(propsOption, vnode.props)
const instance = {
state,
props: shallowReactive(props),
isMounted: false,
subTree: null,
}
// ...
}
function resolveProps(options, propsData) {
const props = {}
const attrs = {}
for (const key in propsData) {
if (key in options) {
props[key] = propsData[key]
} else {
attrs[key] = propsData[key]
}
}
return [props, attrs]
}
当父元素的数据发生变化时,父元素更新,导致子元素更新。在 patch
更新子元素时,由于存在旧节点,会调用 patchComponent
进行更新。在 patchComponent
中我们只需要更新组件属性。
function patchComponent(n1, n2, anchor) {
const instance = (n2.component = n1.component)
const { props } = instance
if (hasPropsChanged(n1.props, n2.props)) {
const [nextProps] = 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]
if (nextProps[key] !== prevProps[key]) return true
}
return false
}
但是这样仅仅在示例保存了 props
并不能在渲染函数中访问他们,所以需要封装一个渲染上下文对象,生命周期函数和渲染函数都绑定该对象。
function mountComponent(vnode, container, anchor) {
// ...
vnode.component = instance
const renderContext = new Proxy(instance, {
get(t, k, r) {
const { state, props } = t
if (state && k in state) {
return state[k]
} else if (k in props) {
return props[k]
} else {
console.error('不存在')
}
},
set(t, k, r) {
const { state, props } = t
if (state && k in state) {
state[k] = v
} else if (k in props) {
console.warn('不可以设置props的值')
} else {
console.error('不存在')
}
return true
},
})
created && created.call(renderContext)
// 为了让组件状态发生变化时能自动渲染
effect(() => {
const subTree = render.call(renderContext, renderContext)
// ...
})
}
12.5 setup 函数的作用与实现
setup
的返回值有两种情况
- 返回一个函数 作为组件的
render
函数 - 返回一个对象,该对象中包含的数据将暴露给模板使用
setup
函数接受两个参数,第一个参数是 props
数据对象,第二个参数是 setupContext
对象。
const Comp = {
props: {
foo: String,
},
setup(props, setupContext) {
props.foo // 访问 props 属性值
// expose 用于显式地对外暴露组件数据
const { slots, emit, attrs, expose } = setupContext
},
}
接下来在 mountComponent
中实现 setup
。
function mountComponent(vnode, container, anchor) {
const componentOptions = vnode.type
let {
render,
data,
setup,
// ...
} = componentOptions
// ...
// 暂时只有 attrs
const setupContext = { attrs }
const setupResult = setup(shallowReactive(instance.props), setupContext)
let setupState = null
if (typeof setupResult === 'function') {
if (render) {
console.warn('setup 返回渲染函数,render选项将被忽略')
}
render = setupResult
} else {
setupState = setupResult
}
vnode.component = instance
const renderContext = new Proxy(instance, {
get(t, k, r) {
const { state, props } = t
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, r) {
const { state, props } = t
if (state && k in state) {
state[k] = v
} else if (k in props) {
console.warn('不可以设置props的值')
} else if (setupState && k in setupState) {
setupState[k] = v
} else {
console.error('不存在')
}
return true
},
})
// ...
}
可以看到我们执行了 setup 并把结果放入了渲染上下文。
12.6 组件事件与 emit 的实现
emit
用来发射组件的自定义事件,本质上就是根据时间名称去 props
数据对象中寻找对用的事件处理函数并执行。
function mountComponent(vnode, container, anchor) {
// ...
function emit(event, ...payload) {
const eventName = `on${event[0].toUpperCase() + event.slice(1)}`
const handler = instance.props[eventName]
if (handler) {
handler(...payload)
} else {
console.warn(`${event} 事件不存在`)
}
}
const setupContext = { attrs, emit }
// ...
}
function resolveProps(options, propsData) {
const props = {}
const attrs = {}
for (const key in propsData) {
if (key in options || key.startsWith('on')) { // 事件不需要显示声明
props[key] = propsData[key]
} else {
attrs[key] = propsData[key]
}
}
return [props, attrs]
}
12.7 插槽的工作原理
在 vnode 中,插槽会被编译为渲染函数,如下:
// 子组件
const MyComponent = {
name: 'MyComponent',
render() {
return {
type: Fragment,
children: [
{
type: 'header',
children: [this.$slots.header()],
},
]
}
},
}
// 父组件
const vnode = {
type: MyComponent,
children: {
header() {
return {
type: 'h1',
children: '我是标题',
}
},
},
}
具体实现就是在子元素的渲染函数中,当他获取 $slots
的值,就把父元素传入的 children
返回。
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
if (k === '$slots') return slots
// ...
},
// ...
})
// ...
}
12.8 注册生命周期
在 setup
中通过 onMounted
等钩子函数可以注册该组件的生命周期函数。但是 onMounted
是如何知道是哪个组件的生命周期?
原理也很简单,和开始的收集依赖有点像,就是在全局保存当前正在执行 setup
的组件实例。
// 全局变量 保存当前在执行 setup 的实例
let currentInstance = null
function setCurrentInstance(instance) {
currentInstance = instance
}
// 以 onMounted 举例 会把函数添加到组件的 mounted 属性内
function onMounted(fn) {
if (currentInstance) {
currentInstance.mounted.push(fn)
} else {
console.error('onMounted 函数只能在 setup 中调用')
}
}
function mountComponent(vnode, container, anchor) {
// ...
const instance = {
// ...
// 在组件实例中添加 mounted 数组,用来存储通过 onMounted 函数注册的生命周期函数
mounted: [],
}
// ...
let setupState = null
if (setup) {
const setupContext = { attrs, emit, slots }
setCurrentInstance(instance)
const setupResult = setup(shallowReactive(instance.props), setupContext)
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,
}
)
}