渲染器学习之后就应该学习组件化了,渲染器就是为了把组件渲染出来,把虚拟DOM渲染为真实DOM
1、渲染组件
从用户的角度来看,一个有状态的组件就是一个选项对象。但是,从渲染器内部来看,一个组件则是一个特殊类型的虚拟DOM节点。例如,描述普通标签,通过虚拟节点vnode.type属性来存储标签,如下代码所示:
const vnode = {
type:'div'
// ...
}
渲染组件需要处理type类型为对象时的情况,因为组件的本质是对象。需要对patch函数更新,代码如下:
function patch(n1, n2, container, anchor) {
if (n1 && n1.type !== n2.type) {
unmount(n1)
n1 = null
}
const { type } = n2
if (typeof type === 'string') {
if (!n1) {
mountElement(n2, container, anchor)
} else {
patchElement(n1, n2)
}
} else if (type === Text) {
if (!n1) {
const el = n2.el = createText(n2.children)
insert(el, container)
} else {
const el = n2.el = n1.el
if (n2.children !== n1.children) {
setText(el, n2.children)
}
}
} else if (type === Fragment) {
if (!n1) {
n2.children.forEach(c => patch(null, c, container))
} else {
patchChildren(n1, n2, container)
}
} else if (typeof type === 'object' || typeof type === 'function') {
// component
if (!n1) {
mountComponent(n2, container, anchor)
} else {
patchComponent(n1, n2, anchor)
}
}
}
组件本身是对页面内容的封装,用来描述页面内容的一部分,因此一个组件必须包含一个render函数,并且返回值应该是虚拟DOM,代码如下:
const MyComponent = {
// 组件名称,可选
name: 'MyComponent',
// 组件的渲染函数,其返回值必须为虚拟 DOM
render() {
// 返回虚拟 DOM
return {
type: 'div',
children: `我是文本内容`
}
}
}
有基本结构之后就可以完成渲染,代码如下:
// 用来描述组件的 VNode 对象,type 属性值为组件的选项对象
const CompVNode = {
type: MyComponent
}
// 调用渲染器来渲染组件
renderer.render(CompVNode, document.querySelector('#app'))
渲染器内部执行挂载组件,实现如下
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)
}
2、组件状态与自更新
与用户约定data()表示组件的状态,同时可以在渲染函数中通过this访问由data函数返回状态数据。
const MyComponent = {
name: 'MyComponent',
// 用 data 函数来定义组件自身的状态
data() {
return {
foo: 'hello world'
}
},
render() {
return {
type: 'div',
children: `foo 的值是: ${this.foo}` // 在渲染函数内使用组件状态
}
}
}
组件状态初始化:
1.通过组件的选项对象获取data并执行,通过reactive把data函数的返回状态包装为响应式数据
2.在调用render函数时,将this的指向设置为响应式数据state,同时将state作为render函数的第一个参数传递。
代码如下:
function mountComponent(vnode, container, anchor) {
const componentOptions = vnode.type
const { render, data } = componentOptions
// 调用 data 函数得到原始数据,并调用 reactive 函数将其包装为响应式数据
const state = reactive(data())
// 调用 render 函数时,将其 this 设置为 state,
// 从而 render 函数内部可以通过 this 访问组件自身状态数据
const subTree = render.call(state, state)
patch(null, subTree, container, anchor)
}
3、组件实例与组件的生命周期
组件实例本质上就是一个状态集合(或一个对象),负责维护组件运行过程中的所有信息
如:组件生命周期函数、组件渲染的子树、组件是否已经被挂载、组件自身的状态等
function patchComponent(n1, n2, anchor) {
const instance = (n2.component = n1.component)
const { props } = instance
if (hasPropsChanged(n1.props, n2.props)) {
const [ nextProps, nextAttrs ] = 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
}
const p = Promise.resolve()
const queue = new Set()
let isFlushing = false
function queueJob(job) {
queue.add(job)
if (!isFlushing) {
isFlushing = true
p.then(() => {
try {
queue.forEach(jon => job())
} finally {
isFlushing = false
}
})
}
}
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) {
componentOptions = {
render: vnode.type,
props: vnode.type.props
}
}
let { render, data, setup, beforeCreate, created, beforeMount, mounted, beforeUpdate, updated, props: propsOption } = componentOptions
beforeCreate && beforeCreate()
const state = data ? reactive(data()) : null
const [props, attrs] = resolveProps(propsOption, vnode.props)
//slots
const slots = vnode.children || {}
const instance = {
state,
props: shallowReactive(props),
isMounted: false,
subTree: null,
slots,
mounted: []
}
//发送自定义事件部分
function emit(event, ...payload) {
const eventName = `on${event[0].toUpperCase() + event.slice(1)}`
const handler = instance.props[eventName]
if (handler) {
handler(...payload)
} else {
console.error('事件不存在')
}
}
// setup
let setupState = null
if (setup) {
const setupContext = { attrs, emit, slots }
const prevInstance = setCurrentInstance(instance)
const setupResult = setup(shallowReadonly(instance.props), setupContext)
setCurrentInstance(prevInstance)
if (typeof setupResult === 'function') {
if (render) console.error('setup 函数返回渲染函数,render 选项将被忽略')
render = setupResult
} else {
setupState = setupContext
}
}
vnode.component = instance
//slots
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)
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))
} else {
beforeUpdate && beforeUpdate.call(renderContext)
patch(instance.subTree, subTree, container, anchor)
updated && updated.call(renderContext)
}
instance.subTree = subTree
}, {
scheduler: queueJob
})
}
主要三部分:
state:组件自身状态,即data
isMounted:表示组件是否挂载
subTree:存储组件的渲染函数返回的虚拟DOM,即组件的子树(subTree)
4、props与组件被动更新
props重要的两部分:
1、为组件传递的props数据,即vnode.props对象
2、组件选项对象中定义的props选项,即。props对象
这部分代码如上面mountComponent函数所示
其中解析组件在渲染时使用的props和attrs数据的注意事项:
1、Vue.js中,没有定义在组件上的props选项中的props数据存储到attrs对象中
2、上述实现没有包含默认值、类型校验等内容的处理
在props数据发生变化时,会触发父组件重新渲染,渲染器会发现父组件的subtree包含组件类型的虚拟节点,调用patch操作,patchComponent函数进行子组件更新操作。
当子组件发生被动更新时需要:
1、检测子组件是否真的需要更新,因为子组件的props可能是不变的
2、如果需要更新,则更新子组件的props、slots等内容
5、setup函数的作用与实现
setup函数是Vue.js 3中组件选项,主要用于组合式API中,转换响应式、注册生命周期钩子函数
setup函数接收的两个参数:
1.props:取得外部为组件传递的props数据对象
2.setupContext:与组件接口相关的数据与方法
slots:插槽
emit:发送自定义事件
attrs:没显示声明为props的都在这里面
expose:暴露组件数据
setup代码实现,见上面代码中setup注释部分
6、emit的实现
自定义事件会被编译为属性存储到props
emit接收的两个参数:
1.event:事件名称
2.playload:传递给事件处理函数的参数
主要原理,实现emit函数并添加到setupContext中,检测是否以on字符串开头,如果是则认为是自定义事件,具体代码在上面emit注释部分
7、插槽的工作原理与实现
插槽:在组件中预留位置,具体渲染内容由用户决定
实现原理:插槽内容会被编译为插槽函数,放到setupContext中,拦截renderContext的get方法,遇到$slots直接返回slots对象,代码如上注释部分
8、注册生命周期
不同组件,不同的生命周期,需要分配currentIstance去判断当前生命周期属于哪个组件,每当初始化组件并执行setup函数之前,先将currentIstance设置为当前组件,再执行setup函数就可以判断哪个组件正在被初始化,代码如上
总结
1、学习了如何使用虚拟节点描述组件,通过type存储组件对象,通过mountComponent挂载组件,patchComponent更新组件
2、组件自更新,触发副作用函数重新执行,缓冲到微任务队列中
3、组件实例,本质上是对象,包含了组件运行过程中的状态,判断标识确定进行挂载还是打补丁
4、props与组件的被动更新,副作用自更新引起的子组件的更新叫被动更新,渲染上下文
5、setup函数,组合式API写法包含props,setupContext
6、emit函数包含在setupContext中,通过v-on指令为组件编译后以onXX存储到props中
7、slots,会被编译为插槽函数调用,通过执行插槽函数得到外部想槽为填充的部分
8、生命周期部分,通过currentInstance判断是哪个组件处于生命周期的哪个阶段