VUE3源码学习笔记-第三部分:组件的实现原理

139 阅读4分钟

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组件时,可以根据插槽名字插入指定内容。