1.渲染组件
从用户角度来看,一个有状态组件其实是一个对象:
const MyComponent = {
name:"MyComponent",
data(){
return {foo:1}
}
}
从渲染器内部实现来看,组件是特殊类型的vnode,对普通标签而言,如果是div,type值为div:
const vnode = {
type:"div"
}
如果是片段,type值为Fragment,如果是文本节点,type是text,如果是组件,type为组件的选项对象。组件还会有一个渲染函数render,返回虚拟dom。渲染器就可以根据render函数的返回值渲染出组件。
const MyComponent = {
name:"MyComponent",
data(){
return {foo:1}
},
render(){
return {
type:"div",
children:"文本内容"
}
}
}
2.组件状态与自更新
通过组件的data函数定义组件状态,实现组件初始化:
function mountComponent(vnode,container,anchor){
const options = vnode.type,{render,data} = options;
//调用data函数得到原始数据并使用reactive函数包装为响应式数据。
const state = reactive(data());
//调用render函数时,将this设置为state,render内部就可以通过this访问组件自身的状态数据
//为了能在组件自身状态变化时更新组件,我们将渲染任务包裹在effect中
effect(()=>{
const subTree = render.call(state,state);
patch(null,subTree,container,anchor)
})
}
此外我们希望,如果以同步的方式多次修改响应式数据,最终视图只会变化一次。我们实现一个调度器,当副作用函数需要执行时,我们把它缓存到微任务队列中,执行栈清空后再去取出执行。
const queue = new Set();
let isFlushing = false;
const p = Promise.resolve();
function queueJob(job){
queue.add(job);
if(!isFlusing){
isFlusing = true;
p.then(()=>{
try{
queue.foreach(job => job());
}finally{
isFlusing = false;
queue.clear = 0;
}
})
}
}
3.组件实例与组件的生命周期
组件实例是一个状态集合,也是一个对象,它维护着组件运行时所有信息,例如生命周期函数、组件渲染的子树、组件是否被挂载、组件自身状态等:
//组件实例
const instance = {
//组件自身状态数据
state,
//组件是否被挂载
isMounted:false,
subTree:null
}
我们可以在恰当的时机调用对应的生命周期钩子:
function mountComponent() {
const options = vnode.type;
const {render,data,beforeCreate,created,beforeMount,mounted,beforeUpdate,updated} = options;
//调用beforeCreate
beforeCreate && beforeCreate();
const state = reactive(data());
const instance = {
state,
isMounted:false,
subTree:null
}
vnode.component = instance;
//调用created
created && created.call(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()
}else{
//调用beforeUpdate
beforeUpdate && beforeUpdate.call(state)
patch(instance,subTree,container,anchor)
//调用updated
updated && updated(state)
}
instance.subTree = subTree
})
}
4.props和组件的被动更新
组件的props会出现在两个地方,一个是在组件template模板中:
<MyComponent title="a title" :other="val"/>
另一个是在props属性中:
const MyComponent = {
name:"MyComponent",
props:{
title:String
}
}
如果一个属性出现在template模板中,却没有出现在props对象中,它就会被储存在attrs对象中,其处理过程很简单,遍历template模板和props对象,再把符合条件的属性添加到attrs即可:
function resolveProps(options,propsData){
const props = {},attrs = {};
for(const key in propsData){
if(key in options){
props[key] = propsData[key]
}else{
attrs[key] = propsData[key]
}
}
return {props,attrs}
}
props实际上是父组件的数据,当props变化时,会触发父组件的更新,而渲染器发现父组件的subTree中含有组件类型的虚拟子节点,就会完成子组件的更新。这种由父组件更新引起子组件更新叫做子组件的被动更新。当子组件被动更新时,我们要判断子组件的props是否发生改变,如果发生改变则更新子组件的props和slots。
5.setup的作用与实现
setup用于配合组合式API,用于建立组合逻辑、创建响应式数据、创建通用函数、注册生命周期钩子,它的返回值有两种情况: 1.返回一个函数,该函数作为组件的render函数; 2.返回一个对象,对象中的数据暴露给模板使用; setup函数接受两个参数,一个是props,另一个是setupContext(包含与组件接口相关的重要数据,如slots、emit、attrs等)。
function mountComponent() {
const options = vnode.type;
//从组件选项中取出setup函数
const {render,data,setup,beforeCreate,created,beforeMount,mounted,beforeUpdate,updated} = options;
//调用beforeCreate
beforeCreate && beforeCreate();
const state =data ? reactive(data()) : null;
const instance = {
state,
isMounted:false,
subTree:null,
props:shallowReactive(props)
}
const setupContext = {attrs,emit,slots};
//调用setup函数,将只读版本的props作为第一个参数传递,避免用户修改props
const setupResult = setup(shallowReadonly(instance.props),setupContext);
//setupState用来储存由setup返回的数据
let setupState = null;
//如果setup的返回值是函数,则将其作为渲染函数
if(typeof setupResult === "function"){
//如果已经有render则报告冲突
if(render){
console.error('setup函数返回渲染函数,render将被忽略')
}
render = setupResult
}else{
setupState = setupResult;
}
vnode.component = instance;
//调用created
created && created.call(state);
//创建上下文对象,即代理组件实例
const renderContext = new Proxy(instace,{
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,v,r){
const {state,props} = t;
if(state && k in state){
state[k] = v
}else if(k in props){
console.warn(`Attempting to mutate prop "${k}"`.props are readonly)
}else if(setupState && k in setupState){
setupState[k] = v;
}else {
console.error("不存在")
}
}
})
}
6.组件事件与emit的实现
emit用来发射组件的自定义事件,其本质是根据事件名称去props数据对象中寻找对应的事件处理函数并执行:
function emit(event,...payload){
const eventName = `on${event[0].toUpperCase() + event.slice(1)}`;
const handler = instance.props[eventName]
if(handler){
handler(...payload)
}else{
console.eror('事件不存在')
}
}
7.插槽的工作原理及实现
组件的插槽指组件会预留槽位,槽位具体要渲染的内容由用户指定,如以下MyComponent模板所示:
<template>
<header>
<slot name="header"/>
</header>
<div>
<slot name="body"/>
</div>
<footer>
<slot name="footer"/>
</footer>
</template>
在父组件中使用MyComponent组件时,可以根据插槽名字插入指定内容。