Vue组件实现原理详细分析
作者:volit_
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组件内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!