Vue組件實現(xiàn)原理詳細(xì)分析
1.渲染組件
從用戶的角度來看,一個有狀態(tài)的組件實際上就是一個選項對象。
const Componetn = { name: "Button", data() { return { val: 1 } } }
而對于渲染器來說,一個有狀態(tài)的組件實際上就是一個特殊的vnode。
const vnode = { type: Component, props: { val: 1 }, }
通常來說,組件渲染函數(shù)的返回值必須是其組件本身的虛擬DOM。
const Component = { name: "Button", render() { return { type: 'button', children: '按鈕' } } }
這樣在渲染器中,就可以調(diào)用組件的render方法來渲染組件了。
function mountComponent(vnode, container, anchor) { const componentOptions = vnode.type; const { render } = componentOptions; const subTree = render(); patch(null, subTree, container, anchor); }
2.組件的狀態(tài)與自更新
在組件中,我們約定組件使用data函數(shù)來定義組件自身的狀態(tài),同時可以在渲染函數(shù)中,調(diào)用this訪問到data中的狀態(tài)。
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封裝成響應(yīng)式對象 effect(() => { const subTree = render.call(state,state); // 將data本身指定為render函數(shù)調(diào)用過程中的this patch(null, subTree, container, anchor); }); }
但是,響應(yīng)式數(shù)據(jù)修改的同時,相對應(yīng)的組件也會重新渲染,當(dāng)多次修改組件狀態(tài)時,組件將會連續(xù)渲染多次,這樣的性能開銷明顯是很大的。因此,我們需要實現(xiàn)一個任務(wù)緩沖隊列,來讓組件渲染只會運行在最后一次修改操作之后。
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封裝成響應(yīng)式對象 effect(() => { const subTree = render.call(state,state); // 將data本身指定為render函數(shù)調(diào)用過程中的this patch(null, subTree, container, anchor); }, { scheduler: queueJob }); }
3.組件實例和生命周期
組件實例實際上就是一個狀態(tài)合集,它維護著組件運行過程中的所有狀態(tài)信息。
function mountComponent(vnode, container, anchor) { const componentOptions = vnode.type; const { render, data } = componentOptions; const state = reactive(data); // 將data封裝成響應(yīng)式對象 const instance = { state, isMounted: false, // 組件是否掛載 subTree: null // 組件實例 } vnode.component = instance; effect(() => { const subTree = render.call(state,state); // 將data本身指定為render函數(shù)調(diào)用過程中的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這個狀態(tài)可以區(qū)分組件的掛載和更新,因此我們可以在這個過程中,很方便的插入生命周期鉤子。
function mountComponent(vnode, container, anchor) { const componentOptions = vnode.type; const { render, data, beforeCreate, created, beforeMount, mounted, beforeUpdate, updated } = componentOptions; beforeCreate && beforeCreate(); // 在狀態(tài)創(chuàng)建之前,調(diào)用beforeCreate鉤子 const state = reactive(data); // 將data封裝成響應(yīng)式對象 const instance = { state, isMounted: false, // 組件是否掛載 subTree: null // 組件實例 } vnode.component = instance; created && created.call(state); // 狀態(tài)創(chuàng)建完成后,調(diào)用created鉤子 effect(() => { const subTree = render.call(state,state); // 將data本身指定為render函數(shù)調(diào)用過程中的this if(!instance.isMounted) { beforeMount && beforeMount.call(state); // 掛載到真實DOM前,調(diào)用beforeMount鉤子 patch(null, subTree, container, anchor); instance.isMounted = true; mounted && mounted.call(state); // 掛載到真實DOM之后,調(diào)用mounted鉤子 } else{ beforeUpdate && beforeUpdate.call(state); // 組件更新狀態(tài)掛載到真實DOM之前,調(diào)用beforeUpdate鉤子 ptach(instance.subTree, subTree, container, anchor); updated && updated.call(state); // 組件更新狀態(tài)掛載到真實DOM之后,調(diào)用updated鉤子 } instance.subTree = subTree; // 更新組件實例 }, { scheduler: queueJob }); }
4.props與組件狀態(tài)的被動更新
通常,我們會指定組件接收到的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(); // 在狀態(tài)創(chuàng)建之前,調(diào)用beforeCreate鉤子 const state = reactive(data); // 將data封裝成響應(yīng)式對象 // 調(diào)用 resolveProps 函數(shù)解析出最終的 props 數(shù)據(jù)與 attrs 數(shù)據(jù) const [props, attrs] = resolveProps(propsOptions, vnode.props); const instance = { state, // 將解析出的 props 數(shù)據(jù)包裝為 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]; }
我們把由父組件自更新所引起的子組件更新叫作子組件的被動更新。當(dāng)子組件發(fā)生被動更新時,我們需要做的是:
- 檢測子組件是否真的需要更新,因為子組件的 props 可能是不變的;
- 如果需要更新,則更新子組件的 props、slots 等內(nèi)容。
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的數(shù)量不對等,說明新舊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數(shù)據(jù)與組件本身的數(shù)據(jù)都需要暴露到渲染函數(shù)中,并使渲染函數(shù)能夠通過this訪問它們,因此我們需要封裝一個渲染上下文對象。
function mountComponent(vnode, container, anchor) { // ... const instance = { state, // 將解析出的 props 數(shù)據(jù)包裝為 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("屬性不存在"); } } }); // 生命周期函數(shù)調(diào)用時要綁定渲染上下文對象 created && created.call(renderContext); // ... }
5.setup函數(shù)的作用與實現(xiàn)
setup函數(shù)時Vue3新增的組件選項,有別于Vue2中的其他組件選項,setup函數(shù)主要用于配合組合式API,為用戶提供一個地方,用于創(chuàng)建組合邏輯、創(chuàng)建響應(yīng)式數(shù)據(jù)、創(chuàng)建通用函數(shù)、注冊生命周期鉤子等。在組件的整個生命周期中,setup函數(shù)只會在被掛載的時候執(zhí)行一次,它的返回值可能有兩種情況:
- 返回一個函數(shù),該函數(shù)作為該組件的render函數(shù)
- 返回一個對象,該對象中包含的數(shù)據(jù)將暴露給模板
此外,setup函數(shù)接收兩個參數(shù)。第一個參數(shù)是props數(shù)據(jù)對象,另一個是setupContext是和組件接口相關(guān)的一些重要數(shù)據(jù)。
cosnt { slots, emit, attrs, expose } = setupContext; /** slots: 組件接收到的插槽 emit: 一個函數(shù),用來發(fā)射自定義事件 attrs:沒有顯示在組件的props中聲明的屬性 expose:一個函數(shù),用來顯式地對外暴露組件數(shù)據(jù) */
下面我們來實現(xiàn)一下setup組件選項。
function mountComponent(vnode, container, anchor) { const componentOptions = vnode.type; const { render, data, setup, /* ... */ } = componentOptions; beforeCreate && beforeCreate(); // 在狀態(tài)創(chuàng)建之前,調(diào)用beforeCreate鉤子 const state = reactive(data); // 將data封裝成響應(yīng)式對象 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函數(shù)返回渲染函數(shù),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("屬性不存在"); } } }); // 生命周期函數(shù)調(diào)用時要綁定渲染上下文對象 created && created.call(renderContext); }
6.組件事件和emit的實現(xiàn)
在組件中,我們可以使用emit函數(shù)發(fā)射自定義事件。
function mountComponent(vnode, container, anchor) { const componentOptions = vnode.type; const { render, data, setup, /* ... */ } = componentOptions; beforeCreate && beforeCreate(); // 在狀態(tài)創(chuàng)建之前,調(diào)用beforeCreate鉤子 const state = reactive(data); // 將data封裝成響應(yīng)式對象 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函數(shù)進行一些特別處理。
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.插槽的工作原理及實現(xiàn)
顧名思義,插槽就是指組件會預(yù)留一個槽位,該槽位中的內(nèi)容需要由用戶來進行插入。
<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> 標(biāo)題 </h1> </templete> <templete #body> <section>內(nèi)容</section> </templete> <tempelte #footer> <p> 腳注 </p> </tempelte> </Component> </templete>
而上述父組件將會被編譯為如下函數(shù):
function render() { retuen { type: Component, children: { header() { return { type: 'h1', children: '標(biāo)題' } }, body() { return { type: 'section', children: '內(nèi)容' } }, 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函數(shù)中,我們就只需要直接取vnode的children對象就可以了。當(dāng)然我們同樣需要對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是用來注冊生命周期函數(shù)鉤子的。對于生命周期函數(shù)的獲取,我們可以定義一個currentInstance變量存儲當(dāng)前正在初始化的實例。
let currentInstance = null; function setCurrentInstance(instance) { currentInstance = instance; }
然后我們在組件實例中添加mounted數(shù)組,用來存儲當(dāng)前組件的mounted鉤子函數(shù)。
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執(zhí)行之前,設(shè)置當(dāng)前實例 setCurrentInstance(instance); const setupResult = setup(shallowReadonly(instance.props),setupContext); //執(zhí)行完后重置 setCurrentInstance(null); // ... }
然后就是onMounted本身的實現(xiàn)和執(zhí)行時機了。
function onMounted(fn) { if(currentInstance) { currentInstace.mounteds.push(fn); } else { console.error("onMounted鉤子只能在setup函數(shù)中執(zhí)行"); } } function mountComponent(vnode, container, anchor) { // ... effect(() => { const subTree = render.call(state,state); // 將data本身指定為render函數(shù)調(diào)用過程中的this if(!instance.isMounted) { beforeMount && beforeMount.call(state); // 掛載到真實DOM前,調(diào)用beforeMount鉤子 patch(null, subTree, container, anchor); instance.isMounted = true; instance.mounted && instance.mounted.forEach( hook => { hook.call(renderContext); }) // 掛載到真實DOM之后,調(diào)用mounted鉤子 } else{ // ... } instance.subTree = subTree; // 更新組件實例 }, { scheduler: queueJob }); }
到此這篇關(guān)于Vue組件實現(xiàn)原理詳細(xì)分析的文章就介紹到這了,更多相關(guān)Vue組件內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
vue?async?await?promise等待異步接口執(zhí)行完畢再進行下步操作教程
在Vue中可以使用異步函數(shù)和await關(guān)鍵字來控制上一步執(zhí)行完再執(zhí)行下一步,這篇文章主要給大家介紹了關(guān)于vue?async?await?promise等待異步接口執(zhí)行完畢再進行下步操作的相關(guān)資料,需要的朋友可以參考下2023-12-12Vue中通過Vue.extend動態(tài)創(chuàng)建實例的方法
這篇文章主要介紹了Vue中通過Vue.extend動態(tài)創(chuàng)建實例的方法,本文通過實例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價值,需要的朋友可以參考下2019-08-08Vue組件庫ElementUI實現(xiàn)表格列表分頁效果
這篇文章主要為大家詳細(xì)介紹了Vue組件庫ElementUI實現(xiàn)表格列表分頁效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-06-06