Vue3 组件渲染原理

106 阅读5分钟

Text-Fragment

除了元素虚拟节点之外,Vue3中还有很多其他类型的虚拟节点,这里我们先来说下Text和Fragment的实现

export const Text = Symbol('Text')
export const Fragment = Symbol('Fragment')

文本类型

renderer.render(h(Text,'jw handsome'),document.getElementById('app'))
const patch = (n1,n2,container,anchor?) => {
    // 初始化和diff算法都在这里喲
    if(n1 == n2){return }
    if(n1 && !isSameVNodeType(n1,n2)){ // 有n1 是n1和n2不是同一个节点
        unmount(n1)
        n1 = null
    }
    const {type,shapeFlag} = n2;
    switch(type){
        case Text:
            processText(n1,n2,container); // 处理文本
            break;
        case Fragment:
            processFragment(n1,n2,container); // 处理fragment
            break;
        default:
            if(shapeFlag & ShapeFlags.ELEMENT){ 
                processElement(n1,n2,container,anchor); // 之前处理元素的逻辑
            }
    }
}
const processText = (n1,n2,container)=>{
    if(n1 == null){
        hostInsert((n2.el = hostCreateText(n2.children)),container)
    }else{
        const el = n2.el = n1.el;
        if(n2.children !== n1.children){
            hostSetText(el,n2.children)
        }
    }
}

Fragment类型

renderer.render(h(Fragment,[h(Text,'hello'),h(Text,'jw')]),document.getElementById('app'))
const processFragment = (n1,n2,container)=>{
    if(n1 == null){ 
        mountChildren(n2.children,container);
    }else{
        patchChildren(n1,n2,container);
    }
}

为了让Vue3支持多根节点模板,Vue.js 提供Fragment来实现,核心就是一个无意义的标签包裹多个节点。

同时这里要处理下卸载的逻辑,如果是fragment则删除子元素

const unmount = (vnode) =>{
    if(vnode.type === Fragment){
        return unmountChildren(vnode.children)
    }
    hostRemove(vnode.el)
}

组件渲染挂载流程

组件需要提供一个render函数,渲染函数需要返回虚拟DOM

const VueComponent = {
    data(){
        return {age:13} 
    },
    render(){
        return h('p',[h(Text,"I'm Jiang sir"),h('span',this.age+'')])
    }
}
createRenderer(renderOptions).render(h(VueComponent),document.getElementById('app'))

添加组件类型

h方法中传入一个对象说明要渲染的是一个组件。(后续还有其他可能)

export const createVNode = (type,props,children = null)=>{
    const shapeFlag = isString(type)  
        ? ShapeFlags.ELEMENT: isObject(type)
        ? ShapeFlags.STATEFUL_COMPONENT:0;
    // ... 稍后可以根据类型来进行组件的挂载
}

组件的渲染

const patch = (n1,n2,container,anchor?) => {
    // 初始化和diff算法都在这里喲
    if(n1 == n2){return }
    if(n1 && !isSameVNodeType(n1,n2)){ // 有n1 是n1和n2不是同一个节点
        unmount(n1)
        n1 = null
    }
    const {type,shapeFlag} = n2;
    switch(type){
        // ...
        default:
            if(shapeFlag & ShapeFlags.ELEMENT){
                processElement(n1,n2,container,anchor)
            }else if(shapeFlag & ShapeFlags.COMPONENT){
                processComponent(n1,n2,container,anchor)
            }
    }
}
const mountComponent = (n2,container,anchor)=>{
    const {render,data=()=>({})} = n2.type;
    const state = reactive(data())
    const instance = {
        state, // 组件的状态
        isMounted:false, // 组件是否挂载
        subTree:null, // 子树
        update:null,
        vnode:n2
    }
    const componentUpdateFn = ()=>{
        if(!instance.isMounted){
            const subTree = render.call(state,state);
            patch(null,subTree,container,anchor);
            instance.subTree = subTree
            instance.isMounted = true;
        }else{
            const subTree = render.call(state,state);
            patch(instance.subTree,subTree,container,anchor)
            instance.subTree = subTree
        }
    }
    const effect = new ReactiveEffect(componentUpdateFn)
    const update = instance.update = effect.run.bind(effect);
    update();
}
const processComponent = (n1,n2,container,anchor)=>{
    if(n1 == null){
        mountComponent(n2,container,anchor);
    }else{
        // 组件更新逻辑
    }
}

组件异步渲染

修改调度方法,将更新方法压入到队列中

const effect = new ReactiveEffect(
    componentUpdateFn,
    ()=>queueJob(instance.update) 
);
const update = instance.update = effect.run.bind(effect);

1
2
3
4
5

批处理操作scheduler.js

const queue = [];
let isFlushing = false;
const resolvedPromise = Promise.resolve()
export function queueJob(job){
    if(!queue.includes(job)){
        queue.push(job);
    }
    if(!isFlushing){
        isFlushing = true;
        resolvedPromise.then(()=>{
            isFlushing = false;
            let copy = queue.slice(0)
            queue.length = 0; // 这里要先清空,防止在执行过程中在加入新的job
            for(let i = 0; i < queue.length;i++){
                let job = queue[i];
                job();
            }
            copy.length = 0;
        })
    }
}

组件Props、Attrs实现

PropsAttrs关系是:没有定义在component.props中的属性将存储到attrs对象中

let {createRenderer,h,render,Text,Fragment} = VueRuntimeDOM
const VueComponent  = { 
    data(){
        return {name:'zf',age:13}
    },
    props:{
        address:String,
    },
    render(){ 
        return h('p',[`${this.name}今年${this.age}岁了`,`${this.address}`,`${this.$attrs.a}${this.$attrs.b}`]);
    }
}
render(h(VueComponent,{address:'霍营',a:1,b:2}),app);

initProps

const mountComponent = (vnode,container,anchor) =>{
    let {data=()=>({}),render,props:propsOptions = {}} = vnode.type; // 这个就是用户写的内容
    const state = reactive(data()); // pinia 源码就是 reactive({})  作为组件的状态
    const instance = { // 组件的实例
        state,
        vnode,  // vue2的源码中组件的虚拟节点叫$vnode  渲染的内容叫_vnode
        subTree:null, // vnode组件的虚拟节点   subTree渲染的组件内容
        isMounted:false,
        update:null,
        propsOptions,
        attrs:{},
        props:{}
    }
    vnode.component = instance
    initProps(instance,vnode.props);
}

componentProps.ts

export function initProps(instance,rawProps){
    const props = {};
    const attrs = {};
    const options = instance.propsOptions || {}; // 获取组件用户的配置
    if(rawProps){
        for(let key in rawProps){
            const value = rawProps[key];
            if( key in options){
                props[key] = value;
            }else{
                attrs[key] = value
            }
        }
    }
    instance.props = reactive(props); // 这里应该用shallowReactive,遵循单向数据流原则
    instance.attrs = attrs;
}

属性代理

const publicPropertiesMap = {
    $attrs:i=> i.attrs
}
const mountComponent = (vnode,container,anchor) =>{
	// ...
    const instance = { // 组件的实例
        // ...
        proxy:null
    }
    vnode.component = instance
    initProps(instance,vnode.props);
    instance.proxy =  new Proxy(instance,{
        get(target,key){
            const {state,props} = target;
            if(state && hasOwn(state,key)){
                return state[key];
            }else if(hasOwn(props,key)){
                return props[key];
            }
            const publicGetter = publicPropertiesMap[key];
            if(publicGetter){
                return publicGetter(instance)
            }
        },
        set(target,key,value){
            const {state,props} = target;
            if(state && hasOwn(state,key)){
                state[key] = value;
                return true;
            }else if(hasOwn(props,key)){
                console.warn(`Attempting to mutate prop "${key}". Props are readonly.`)
                return false;
            } 
            return true;
        }
    });
}

组件流程整合

const mountComponent = (vnode,container,anchor) =>{
    // 1) 创建实例
    const instance = vnode.component = createComponentInstance(vnode);
    // 2) 给实例赋值
    setupComponent(instance)
    // 3) 创建渲染effect及更新
    setupRenderEffect(instance,container,anchor);
}

1)创建组件实例

component.ts

export function createComponentInstance(vnode){
    const instance = { // 组件的实例
        data:null,
        vnode,  // vue2的源码中组件的虚拟节点叫$vnode  渲染的内容叫_vnode
        subTree:null, // vnode组件的虚拟节点   subTree渲染的组件内容
        isMounted:false,
        update:null,
        attrs:{},
        props:{},
        proxy:null,
        propsOptions:vnode.type.props
    }
    return instance
}

2)设置组件属性

const publicPropertiesMap = {
    $attrs:i=> i.attrs
}
const PublicInstanceProxyHandlers = {
    get(target,key){
        const {data,props} = target;
        if(data && hasOwn(data,key)){
            return data[key];
        }else if(hasOwn(props,key)){
            return props[key];
        }
        const publicGetter = publicPropertiesMap[key];
        if(publicGetter){
            return publicGetter(target)
        }
    },
    set(target,key,value){
        const {data,props} = target;
        if(data && hasOwn(data,key)){
            data[key] = value;
            return true;
        }else if(hasOwn(props,key)){
            console.warn(`Attempting to mutate prop "${key}". Props are readonly.`)
            return false;
        } 
        return true;
    }
}
export function setupComponent(instance){
    const {props,type} = instance.vnode;
    initProps(instance,props);
    instance.proxy = new Proxy(instance,PublicInstanceProxyHandlers);
    const data = type.data;
    if(data){
        if(!isFunction(data)) return console.warn('The data option must be a function.')
        instance.data = reactive(data.call(instance.proxy))
    }
    instance.render = type.render
}

3)渲染effect

 const setupRenderEffect = (instance,container,anchor) =>{
     const {render} = instance
     const componentUpdateFn = () =>{ // 区分是初始化 还是要更新
         if(!instance.isMounted){ // 初始化
             const subTree = render.call(instance.proxy,instance.proxy); // 作为this,后续this会改
             patch(null,subTree,container,anchor); // 创造了subTree的真实节点并且插入了
             instance.subTree = subTree;
             instance.isMounted = true
         }else{ // 组件内部更新
             const subTree = render.call(instance.proxy,instance.proxy);
             patch(instance.subTree,subTree,container,anchor);
             instance.subTree = subTree;
         }
     }
     // 组件的异步更新
     const effect = new ReactiveEffect(componentUpdateFn,()=> queueJob(instance.update))
     // 我们将组件强制更新的逻辑保存到了组件的实例上,后续可以使用
     let update = instance.update = effect.run.bind(effect); // 调用effect.run可以让组件强制重新渲染
     update();
 }

属性更新

const My = {
    props:{address:String},
    render(){return h('div',this.address)}
}
const VueComponent  = { 
    data(){
        return {name:'zf',age:13,flag:false}
    },
    render(){ 
        return h(Fragment,[
            h('button',{onClick:() => this.flag = !this.flag},'切换渲染'),
            h(My,{address:this.flag? '天龙苑':'回龙观'}),
        ]) 
    }
}
render(h(VueComponent),app);
const updateComponent = (n1,n2)=>{
    const instance = (n2.component = n1.component);
    const {props:prevProps} = n1;
    const {props:nextProps} = n2;
    updateProps(instance,prevProps,nextProps)
}
const processComponent = (n1,n2,container,anchor)=>{
    if(n1 == null){
        mountComponent(n2,container,anchor);
    }else{
        // 组件更新逻辑
        updateComponent(n1,n2)
    }
}

props.ts

const 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
}
export function updateProps(instance,prevProps,nextProps){
    if(hasPropsChanged(prevProps,nextProps)){ // 比较前后属性是否一致
        for(const key in nextProps){ // 循环props
            instance.props[key] = nextProps[key]; // 响应式属性更新后会重新渲染
        }
        for(const key in instance.props){ // 循环props
            if(!(key in nextProps)){
                delete instance.props[key]
            }
        }
    }
}

这里我们将更新逻辑放到componentFn中,因为除了属性更新之外,插槽也会导致页面更新

const shouldUpdateComponent = (n1,n2) =>{
    const { props: prevProps, children: prevChildren } = n1
    const { props: nextProps, children: nextChildren } = n2;
    	
    if(prevChildren || nextChildren) return true
    
    if(prevProps === nextProps) return false;
    return hasPropsChanged(prevProps,nextProps)
}
const updateComponent = (n1,n2)=>{
    const instance = (n2.component = n1.component);
    if(shouldUpdateComponent(n1,n2)){
        instance.next = n2 // 将新的虚拟节点放到next属性上
        instance.update(); // 属性变化手动调用更新方法
    }
}
export function updateProps(prevProps,nextProps){
    for(const key in nextProps){ // 循环props
        prevProps[key] = nextProps[key]; // 响应式属性更新后会重新渲染
    }
    for(const key in prevProps){ // 循环props
        if(!(key in nextProps)){
            delete prevProps[key]
        }
    }
}
function updateComponentPreRender(instance,next){
    instance.next = null;
    instance.vnode = next;
    updateProps(instance.props,next.props)
}
const componentUpdateFn = ()=>{
    if(!instance.isMounted){
        // ...
    }else{
        let {next} = instance;
        if(next){
            updateComponentPreRender(instance,next)
        }
        const subTree = render.call(instance.proxy,instance.proxy);
        patch(instance.subTree,subTree,container,anchor);
        instance.subTree = subTree;
    }
}

setup函数作用

组件的render函数每次更新时都会重新执行,但是setup函数只会在组件挂载时执行一次。

  • setup函数是compositionAPI的入口
  • 可以在函数内部编写逻辑,解决vue2中反复横跳问题
  • setup返回函数时为组件的render函数,返回对象时对象中的数据将暴露给模板使用
  • setup中函数的参数为props、context({slots,emit,attrs,expose})
const My = {
    props:{address:String},
    render(){return h('div',this.address)}
}
const VueComponent  = { 
    props:{
        address:String
    },
    setup(props){
        const name = ref('jw');
        return {
            name,
            address:props.address
        }
    },
    render (){
        return h(Text,`${this.address},${this.name}`)
    }
}
render(h(VueComponent,{address:'回龙观'}),app);

setup函数进行解析

export function setupComponent(instance){
    const {props,type} = instance.vnode;
    initProps(instance,props);

    let {setup} = type
    if(setup){ // 对setup做相应处理
        const setupContext = {};
        const setupResult = setup(instance.props,setupContext);
        console.log(setupResult)
        if(isFunction(setupResult)){
            instance.render = setupResult;
        }else if(isObject(setupResult)){
            instance.setupState = proxyRefs(setupResult); // 这里对返回值进行结构
        }
    }

    instance.proxy = new Proxy(instance,PublicInstanceProxyHandlers);
    const data = type.data;
    if(data){
        if(!isFunction(data)) return console.warn('The data option must be a function.')
        instance.data = reactive(data.call(instance.proxy))
    }
    if(!instance.render){
        instance.render = type.render
    }
}

新增取值范围

const PublicInstanceProxyHandlers = {
    get(target,key){
        const {data,props,setupState} = target;
        if(data && hasOwn(data,key)){
            return data[key];
        }else if(hasOwn(props,key)){
            return props[key];
        }else if(setupState && hasOwn(setupState,key)){ // setup返回值做代理
            return setupState[key];
        }
        const publicGetter = publicPropertiesMap[key];
        if(publicGetter){
            return publicGetter(target)
        }
    },
    set(target,key,value){
        const {data,props,setupState} = target;
        if(data && hasOwn(data,key)){
            data[key] = value;
            return true;
        }else if(hasOwn(props,key)){
            console.warn(`Attempting to mutate prop "${key}". Props are readonly.`)
            return false;
        } else if(setupState && hasOwn(setupState,key)){ // setup返回值做代理
            setupState[key] = value
        }
        return true;
    }
}

实现emit方法

const VueComponent = {
    setup(props,ctx){
        const handleClick = ()=>{
            ctx.emit('myEvent');
        }
        return ()=>h('button',{onClick:handleClick},'点我啊')
    }
}
render(h(VueComponent,{onMyEvent:()=>{alert(1000)}}),document.getElementById('app'))
const setupContext = {
    attrs:instance.attrs,
    emit:(event,...args)=>{
        const eventName = `on${event[0].toUpperCase() + event.slice(1)}`;
        const handler = instance.vnode.props[eventName]; // 找到绑定的方法
        // 触发方法执行
        handler && handler(...args);
    }
};

slot实现

const MyComponent = {
    render(){
        return h(Fragment,[
            h('div',[this.$slots.header()]), // 获取插槽渲染
            h('div',[this.$slots.body()]),
            h('div',[this.$slots.footer()]),
        ])
    }
}
const VueComponent = {
    setup(){
        return ()=>h(MyComponent,null,{ // 渲染组件时传递对应的插槽属性
            header:() => h('p','头'),
            body:() => h('p','体'),
            footer:() => h('p','尾')
        })
    }
}
render(h(VueComponent),app)
export const createVNode = (type,props,children = null)=>{
    // ....
    if(children){
        let type = 0;
        if(Array.isArray(children)){
            type = ShapeFlags.ARRAY_CHILDREN;
        }else if(isObject(children)){ // 类型是插槽
            type = ShapeFlags.SLOTS_CHILDREN
        }else{
            children = String(children);
            type = ShapeFlags.TEXT_CHILDREN
        }
        vnode.shapeFlag |= type
    }
    return vnode;
}
const publicPropertiesMap = {
    $attrs:i=> i.attrs,
    $slots:i=>i.slots
}
function initSlots(instance,children){
    if(instance.vnode.shapeFlag & ShapeFlags.SLOTS_CHILDREN){
        instance.slots = children;
    }else{
        instance.slots = {};
    }
}
export function createComponentInstance(vnode){
    const instance = { // 组件的实例
        slots:null // 初始化插槽属性
    }
    return instance
}
export function setupComponent(instance){
    const {props,type,children} = instance.vnode;
    initProps(instance,props);
    initSlots(instance,children) // 初始化插槽
}

生命周期实现原理

生命周期需要让当前实例关联对应的生命周期,这样在组件构建过程中就可以调用对应的钩子

component.ts

export const setCurrentInstance = (instance) =>currentInstance = instance
export const getCurrentInstance= () => currentInstance 
export const unsetCurrentInstance= () => currentInstance = null
setCurrentInstance(instance); // 在调用setup的时候保存当前实例
const setupResult = setup(instance.props,setupContext);
unsetCurrentInstance(null);

创建生命周期钩子

apiLifecycle.ts

export const enum LifecycleHooks {
    BEFORE_MOUNT = 'bm',
    MOUNTED = 'm',
    BEFORE_UPDATE = 'bu',
    UPDATED = 'u'
}
function createHook(type){
    return (hook,target = currentInstance) =>{ // 调用的时候保存当前实例
        if(target){
            const hooks = target[type] || (target[type] = []);
            const wrappedHook = () =>{
                setCurrentInstance(target); // 当生命周期调用时 保证currentInstance是正确的
                hook.call(target); 
                setCurrentInstance(null);
            }
            hooks.push(wrappedHook);
        }
    }
}
export const onBeforeMount = createHook(LifecycleHooks.BEFORE_MOUNT);
export const onMounted = createHook(LifecycleHooks.MOUNTED);
export const onBeforeUpdate = createHook(LifecycleHooks.BEFORE_UPDATE);
export const onUpdated = createHook(LifecycleHooks.UPDATED);

钩子调用

const componentUpdateFn = ()=>{
    if(!instance.isMounted){
        const {bm,m} = instance
        if(bm){ // beforeMount
            invokeArrayFns(bm)
        }
        const subTree = render.call(renderContext,renderContext);
        patch(null,subTree,container,anchor);
        if(m){ // mounted
            invokeArrayFns(m)
        }
        instance.subTree = subTree
        instance.isMounted = true;
    }else{
        let {next,bu,u} = instance;
        if(next){
            updateComponentPreRender(instance,next)
        }
        if(bu){ // beforeUpdate
            invokeArrayFns(bu)
        }
        const subTree = render.call(renderContext,renderContext);
        patch(instance.subTree,subTree,container,anchor)
        if(u){ // updated
            invokeArrayFns(u)
        }
        instance.subTree = subTree
    }
}

shared.ts

export const invokeArrayFns = (fns) => {
    for (let i = 0; i < fns.length; i++) {
        fns[i](); // 调用钩子方法
    }
}