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

