Vue组件实现原理详细分析

 更新时间:2023年01月18日 09:09:32   作者:volit_  
这篇文章主要介绍了Vue组件基础操作,组件是vue.js最强大的功能之一,而组件实例的作用域是相互独立的,这就意味着不同组件之间的数据无法相互进行直接的引用

1.渲染组件

从用户的角度来看,一个有状态的组件实际上就是一个选项对象。

const Componetn = {
    name: "Button",
    data() {
        return {
            val: 1
        }
    }
}

而对于渲染器来说,一个有状态的组件实际上就是一个特殊的vnode。

const vnode = {
    type: Component,
    props: {
        val: 1
    },
}

通常来说,组件渲染函数的返回值必须是其组件本身的虚拟DOM。

const Component = {
    name: "Button",
    render() {
        return {
            type: 'button',
            children: '按钮'
        }
    }
}

这样在渲染器中,就可以调用组件的render方法来渲染组件了。

function mountComponent(vnode, container, anchor) {
    const componentOptions = vnode.type;
    const { render } = componentOptions;
    const subTree = render();
    patch(null, subTree, container, anchor);
}

2.组件的状态与自更新

在组件中,我们约定组件使用data函数来定义组件自身的状态,同时可以在渲染函数中,调用this访问到data中的状态。

const Component = {
    name: "Button",
    data() {
        return {
            val: 1
        }
    }
    render() {
        return {
            type: 'button',
            children: `${this.val}`
        }
    }
}
function mountComponent(vnode, container, anchor) {
    const componentOptions = vnode.type;
    const { render, data } = componentOptions;
    const state = reactive(data); // 将data封装成响应式对象
    effect(() => {
        const subTree = render.call(state,state); // 将data本身指定为render函数调用过程中的this
    	patch(null, subTree, container, anchor);
    });
}

但是,响应式数据修改的同时,相对应的组件也会重新渲染,当多次修改组件状态时,组件将会连续渲染多次,这样的性能开销明显是很大的。因此,我们需要实现一个任务缓冲队列,来让组件渲染只会运行在最后一次修改操作之后。

const queue = new Set();
let isFlushing = false;
const p = Promise.resolve();
function queueJob(job) {
    queue.add(job);
    if(!isFlushing) {
        isFlushing = true;
        p.then(() => {
            try {
                queue.forEach(job=>job());
            } finally {
                isFlushing = false;
                queue.length = 0;
            }
        })
    }
}
function mountComponent(vnode, container, anchor) {
    const componentOptions = vnode.type;
    const { render, data } = componentOptions;
    const state = reactive(data); // 将data封装成响应式对象
    effect(() => {
        const subTree = render.call(state,state); // 将data本身指定为render函数调用过程中的this
    	patch(null, subTree, container, anchor);
    }, {
        scheduler: queueJob
    });
}

3.组件实例和生命周期

组件实例实际上就是一个状态合集,它维护着组件运行过程中的所有状态信息。

function mountComponent(vnode, container, anchor) {
    const componentOptions = vnode.type;
    const { render, data } = componentOptions;
    const state = reactive(data); // 将data封装成响应式对象
    const instance = {
        state,
        isMounted: false, // 组件是否挂载
        subTree: null // 组件实例
    }
    vnode.component = instance;
    effect(() => {
        const subTree = render.call(state,state); // 将data本身指定为render函数调用过程中的this
    	if(!instance.isMounted) {
            patch(null, subTree, container, anchor);
            instance.isMounted = true;
		} else{
            ptach(instance.subTree, subTree, container, anchor);
        }
        instance.subTree = subTree; // 更新组件实例
    }, {
        scheduler: queueJob
    });
}

因为isMounted这个状态可以区分组件的挂载和更新,因此我们可以在这个过程中,很方便的插入生命周期钩子。

function mountComponent(vnode, container, anchor) {
    const componentOptions = vnode.type;
    const { render, data, beforeCreate, created, beforeMount, mounted, beforeUpdate, updated } = componentOptions;
    beforeCreate && beforeCreate(); // 在状态创建之前,调用beforeCreate钩子
    const state = reactive(data); // 将data封装成响应式对象
    const instance = {
        state,
        isMounted: false, // 组件是否挂载
        subTree: null // 组件实例
    }
    vnode.component = instance;
    created && created.call(state); // 状态创建完成后,调用created钩子
    effect(() => {
        const subTree = render.call(state,state); // 将data本身指定为render函数调用过程中的this
    	if(!instance.isMounted) { 
            beforeMount && beforeMount.call(state); // 挂载到真实DOM前,调用beforeMount钩子
            patch(null, subTree, container, anchor);
            instance.isMounted = true;
            mounted && mounted.call(state); // 挂载到真实DOM之后,调用mounted钩子
		} else{
            beforeUpdate && beforeUpdate.call(state); // 组件更新状态挂载到真实DOM之前,调用beforeUpdate钩子
            ptach(instance.subTree, subTree, container, anchor);
        	updated && updated.call(state); // 组件更新状态挂载到真实DOM之后,调用updated钩子
        }
        instance.subTree = subTree; // 更新组件实例
    }, {
        scheduler: queueJob
    });
}

4.props与组件状态的被动更新

通常,我们会指定组件接收到的props。因此,对于一个组件的props将会有两部分的定义:传递给组件的props和组件定义的props。

const Component = {
    name: "Button",
    props: {
        name: String
    }
}
function mountComponent(vnode, container, anchor) {
    const componentOptions = vnode.type;
    const { render, data, props: propsOptions, beforeCreate, created, beforeMount, mounted, beforeUpdate, updated } = componentOptions;
    beforeCreate && beforeCreate(); // 在状态创建之前,调用beforeCreate钩子
    const state = reactive(data); // 将data封装成响应式对象
    // 调用 resolveProps 函数解析出最终的 props 数据与 attrs 数据
    const [props, attrs] = resolveProps(propsOptions, vnode.props);
    const instance = {
        state,
        // 将解析出的 props 数据包装为 shallowReactive 并定义到组件实例上
        props: shallowReactive(props),
        isMounted: false, // 组件是否挂载
        subTree: null // 组件实例
    }
    vnode.component = instance;
    // ...
}
function resolveProps(options, propsData) {
    const props = {}; // 存储定义在组件中的props属性
    const attrs = {}; // 存储没有定义在组件中的props属性
    for(const key in propsData ) {
    	if(key in options) {
            props[key] = propsData[key];
        } else {
            attrs[key] = propsData[key];
        }
    }
    return [props, attrs];
}

我们把由父组件自更新所引起的子组件更新叫作子组件的被动更新。当子组件发生被动更新时,我们需要做的是:

  • 检测子组件是否真的需要更新,因为子组件的 props 可能是不变的;
  • 如果需要更新,则更新子组件的 props、slots 等内容。
function patchComponet(n1, n2, container) {
 	const instance = (n2.component = n1.component);
    const { props } = instance;
    if(hasPropsChanged(n1.props, n2.props)) {
        // 检查是否需要更新props
        const [nextProps] = resolveProps(n2.type.props, n2.props);
        for(const k in nextProps) {
            // 更新props
            props[k] = nextProps[k];
        }
        for(const k in props) {
            // 删除没有的props
            if(!(k in nextProps)) delete props[k];
        }
    }
}
function hasPropsChanged( prevProps, nextProps) {
    const nextKeys = Object.keys(nextProps);
    if(nextKeys.length !== Object.keys(preProps).length) {
        // 如果新旧props的数量不对等,说明新旧props有改变
        return true;
    }
    for(let i = 0; i < nextKeys.length; i++) {
        // 如果新旧props的属性不对等,说明新旧props有改变
        const key = nextKeys[i];
        if(nextProps[key] !== prevProps[key]) return true;
    }
    return false;
}

由于props数据与组件本身的数据都需要暴露到渲染函数中,并使渲染函数能够通过this访问它们,因此我们需要封装一个渲染上下文对象。

function mountComponent(vnode, container, anchor) {
    // ...
    const instance = {
        state,
        // 将解析出的 props 数据包装为 shallowReactive 并定义到组件实例上
        props: shallowReactive(props),
        isMounted: false, // 组件是否挂载
        subTree: null // 组件实例
    }
    vnode.component = instance;
    const renderContext = next 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, v, r) {
            const { state, props } = t;
            if(state && k in state) {
                state[k] = v;
            } else if(k in props) {
                props[k] = v;
            } else {
                console.error("属性不存在");
            }
        }
    });
    // 生命周期函数调用时要绑定渲染上下文对象
    created && created.call(renderContext);
    // ...
}

5.setup函数的作用与实现

setup函数时Vue3新增的组件选项,有别于Vue2中的其他组件选项,setup函数主要用于配合组合式API,为用户提供一个地方,用于创建组合逻辑、创建响应式数据、创建通用函数、注册生命周期钩子等。在组件的整个生命周期中,setup函数只会在被挂载的时候执行一次,它的返回值可能有两种情况:

  • 返回一个函数,该函数作为该组件的render函数
  • 返回一个对象,该对象中包含的数据将暴露给模板

此外,setup函数接收两个参数。第一个参数是props数据对象,另一个是setupContext是和组件接口相关的一些重要数据。

cosnt { slots, emit, attrs, expose } = setupContext;
/**
	slots: 组件接收到的插槽
	emit: 一个函数,用来发射自定义事件
	attrs:没有显示在组件的props中声明的属性
	expose:一个函数,用来显式地对外暴露组件数据
*/

下面我们来实现一下setup组件选项。

function mountComponent(vnode, container, anchor) {
    const componentOptions = vnode.type;
    const { render, data, setup, /* ... */ } = componentOptions;
    beforeCreate && beforeCreate(); // 在状态创建之前,调用beforeCreate钩子
    const state = reactive(data); // 将data封装成响应式对象
    const [props, attrs] = resolveProps(propsOptions, vnode.props);
    const instance = {
        state,
        props: shallowReactive(props),
        isMounted: false, // 组件是否挂载
        subTree: null // 组件实例
    }
    const setupContext = { attrs };
    const setupResult = setup(shallowReadOnly(instance.props), setupContext);
    let setupState = null;
    if(typeof setResult === 'function') {
        if(render) console.error('setup函数返回渲染函数,render选项将被忽略');
        render = setupResult;
    } else {
        setupState = setupResult;
    }
    vnode.component = instance;
    const renderContext = next Proxy(instance, {
        get(t, k, r) {
            const {state, props} = t;
            if(state && k in state) {
                return setupState[k]; // 增加对setupState的支持
            } else if (k in props) [
                return props[k];
            ] else {
                console.error("属性不存在");
            }
        },
        set(t, k, v, r) {
            const { state, props } = t;
            if(state && k in state) {
                setupState[k] = v; // 增加对setupState的支持
            } else if(k in props) {
                props[k] = v;
            } else {
                console.error("属性不存在");
            }
        }
    });
    // 生命周期函数调用时要绑定渲染上下文对象
    created && created.call(renderContext);
}

6.组件事件和emit的实现

在组件中,我们可以使用emit函数发射自定义事件。

function mountComponent(vnode, container, anchor) {
    const componentOptions = vnode.type;
    const { render, data, setup, /* ... */ } = componentOptions;
    beforeCreate && beforeCreate(); // 在状态创建之前,调用beforeCreate钩子
    const state = reactive(data); // 将data封装成响应式对象
    const [props, attrs] = resolveProps(propsOptions, vnode.props);
    const instance = {
        state,
        props: shallowReactive(props),
        isMounted: false, // 组件是否挂载
        subTree: null // 组件实例
    }
    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('事件不存在');
        }
    }
    const setupContext = { attrs, emit };
    // ...
}

由于没有在组件props中声明的属性不会被添加到props中,因此所有的事件都将不会被添加到props中。对此,我们需要对resolveProps函数进行一些特别处理。

function resolveProps(options, propsData) {
    const props = {}; // 存储定义在组件中的props属性
    const attrs = {}; // 存储没有定义在组件中的props属性
    for(const key in propsData ) {
    	if(key in options || key.startWidth('on')) {
            props[key] = propsData[key];
        } else {
            attrs[key] = propsData[key];
        }
    }
    return [props, attrs];
}

7.插槽的工作原理及实现

顾名思义,插槽就是指组件会预留一个槽位,该槽位中的内容需要由用户来进行插入。

<templete>
	<header><slot name="header"></slot></header>
    <div>
        <slot name="body"></slot>
    </div>
    <footer><slot name="footer"></slot></footer>
</templete>

在父组件中使用的时候,可以这样来使用插槽:

<templete>
	<Component>
    	<templete #header>
            <h1>
                标题
            </h1>
        </templete>
        <templete #body>
        	<section>内容</section>
        </templete>
        <tempelte #footer>
            <p>
                脚注
            </p>
        </tempelte>
    </Component>
</templete>

而上述父组件将会被编译为如下函数:

function render() {
    retuen {
        type: Component,
        children: {
            header() {
                return { type: 'h1', children: '标题' }
            },
            body() {
                return { type: 'section', children: '内容' }
            },
            footer() {
                return { type: 'p', children: '脚注' }
            }
        }
    }
}

而Component组件将会被编译为:

function render() {
    return [
        {
            type: 'header',
            children: [this.$slots.header()]
        },
        {
            type: 'bdoy',
            children: [this.$slots.body()]
        },
        {
            type: 'footer',
            children: [this.$slots.footer()]
        }
    ]
}

在mountComponent函数中,我们就只需要直接取vnode的children对象就可以了。当然我们同样需要对slots进行一些特殊处理。

function mountComponent(vnode, container, anchor) {
    // ...
    const slots = vnode.children || {};
    const instance = {
        state,
        props: shallowReactive(props),
        isMounted: false, // 组件是否挂载
        subTree: null, // 组件实例
    	slots
    }
    const setupContext = { attrs, emit, slots };
    const renderContext = next Proxy(instance, {
        get(t, k, r) {
            const {state, props} = t;
            if(k === '$slots') { // 对slots进行一些特殊处理
                return slots;
            }
            // ...
        },
        set(t, k, v, r) {
            // ...
        }
    });
    // ...
}

8.注册生命周期

在setup中,有一部分组合式API是用来注册生命周期函数钩子的。对于生命周期函数的获取,我们可以定义一个currentInstance变量存储当前正在初始化的实例。

let currentInstance = null;
function setCurrentInstance(instance) {
    currentInstance = instance;
}

然后我们在组件实例中添加mounted数组,用来存储当前组件的mounted钩子函数。

function mountComponent(vnode, container, anchor) {
    // ...
    const slots = vnode.children || {};
    const instance = {
        state,
        props: shallowReactive(props),
        isMounted: false, // 组件是否挂载
        subTree: null, // 组件实例
    	slots,
        mounteds
    }
    const setupContext = { attrs, emit, slots };
    // 在setup执行之前,设置当前实例
    setCurrentInstance(instance);
    const setupResult = setup(shallowReadonly(instance.props),setupContext);
	//执行完后重置
    setCurrentInstance(null);
    // ...
}

然后就是onMounted本身的实现和执行时机了。

function onMounted(fn) {
    if(currentInstance) {
        currentInstace.mounteds.push(fn);
    } else {
        console.error("onMounted钩子只能在setup函数中执行");
    }
}
function mountComponent(vnode, container, anchor) {
    // ...
    effect(() => {
        const subTree = render.call(state,state); // 将data本身指定为render函数调用过程中的this
    	if(!instance.isMounted) { 
            beforeMount && beforeMount.call(state); // 挂载到真实DOM前,调用beforeMount钩子
            patch(null, subTree, container, anchor);
            instance.isMounted = true;
            instance.mounted && instance.mounted.forEach( hook => {
                hook.call(renderContext);
            }) // 挂载到真实DOM之后,调用mounted钩子
		} else{
            // ...
        }
        instance.subTree = subTree; // 更新组件实例
    }, {
        scheduler: queueJob
    });
}

到此这篇关于Vue组件实现原理详细分析的文章就介绍到这了,更多相关Vue组件内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Vue自定义指令的使用实例介绍

    Vue自定义指令的使用实例介绍

    作为使用Vue的开发者,我们对Vue指令一定不陌生,诸如v-model、v-on、等,同时Vue也为开发者提供了自定义指令的api,熟练的使用自定义指令可以极大的提高了我们编写代码的效率,让我们可以节省时间开心的摸鱼
    2023-04-04
  • vue async await promise等待异步接口执行完毕再进行下步操作教程

    vue async await promise等待异步接口执行完毕再进行下步操作教程

    在Vue中可以使用异步函数和await关键字来控制上一步执行完再执行下一步,这篇文章主要给大家介绍了关于vue async await promise等待异步接口执行完毕再进行下步操作的相关资料,需要的朋友可以参考下
    2023-12-12
  • Vue组件之间四种通信方式详解

    Vue组件之间四种通信方式详解

    vue框架提供了前端开发组件的思想,可以通过组件来组合成一个完整的页面,都是随着组件数量原来越多,组件之间难免需要相互通信。本文将为大家介绍四种组件间的通信方式,需要的可以参考一下
    2022-01-01
  • Vue中通过Vue.extend动态创建实例的方法

    Vue中通过Vue.extend动态创建实例的方法

    这篇文章主要介绍了Vue中通过Vue.extend动态创建实例的方法,本文通过实例代码给大家介绍的非常详细,具有一定的参考借鉴价值,需要的朋友可以参考下
    2019-08-08
  • 解决element-ui的下拉框有值却无法选中的情况

    解决element-ui的下拉框有值却无法选中的情况

    这篇文章主要介绍了解决element-ui的下拉框有值却无法选中的情况,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-11-11
  • Vue组件库ElementUI实现表格列表分页效果

    Vue组件库ElementUI实现表格列表分页效果

    这篇文章主要为大家详细介绍了Vue组件库ElementUI实现表格列表分页效果,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-06-06
  • Vue路由的懒加载深入详解

    Vue路由的懒加载深入详解

    这篇文章主要介绍了vue-router路由懒加载及实现方式,路由懒加载的主要作用是将 路由对应的组件打包成一个个的js代码块,只有在这个路由被访问到的时候,才会加载对应组件的代码块,需要的朋友可以参考下
    2022-12-12
  • Vue2.0 事件的广播与接收(观察者模式)

    Vue2.0 事件的广播与接收(观察者模式)

    这篇文章主要介绍了Vue2.0 事件的广播与接收(观察者模式),小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-03-03
  • Vue 中页面传值的多种方式小结

    Vue 中页面传值的多种方式小结

    本文主要介绍了Vue 中页面传值的多种方式小结,主要包括路由传参、Vuex 状态管理、Props 属性和事件传递数据这几种,具有一定的参考价值,感兴趣的可以了解一下
    2023-10-10
  • vue之proxyTable代理超全面配置流程

    vue之proxyTable代理超全面配置流程

    这篇文章主要介绍了vue之proxyTable代理超全面配置流程,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-04-04

最新评论