Vue響應(yīng)式原理深入分析
1.響應(yīng)式數(shù)據(jù)和副作用函數(shù)
(1)副作用函數(shù)
副作用函數(shù)就是會(huì)產(chǎn)生副作用的函數(shù)。
function effect() {
document.body.innerText = 'hello world.'
}
? 當(dāng)effect函數(shù)執(zhí)行時(shí),它會(huì)設(shè)置body的內(nèi)容,而body是一個(gè)全局變量,除了effect函數(shù)外任何地方都可以訪問到,也就是說effect函數(shù)的執(zhí)行會(huì)對(duì)其他操作產(chǎn)生影響,即effect函數(shù)是一個(gè)副作用函數(shù)。
(2)響應(yīng)式數(shù)據(jù)
const obj = { text: 'hello world.'};
function effect() {
document.body.innerText = obj.text;
}
obj.text = 'text';
? 當(dāng) obj.text = 'text' 這條語(yǔ)句執(zhí)行之后,我們期望 document.body.innerText 的值也能夠隨之修改,這就是通常意義上的響應(yīng)式數(shù)據(jù)。
2.響應(yīng)式數(shù)據(jù)的基本實(shí)現(xiàn)
? 上文中,對(duì)響應(yīng)式數(shù)據(jù)進(jìn)行描述的代碼段,并未實(shí)現(xiàn)真正的響應(yīng)式數(shù)據(jù)。而通過觀察我們可以發(fā)現(xiàn),要實(shí)現(xiàn)真正的響應(yīng)式數(shù)據(jù),我們需要對(duì)數(shù)據(jù)的讀取和設(shè)置進(jìn)行攔截。當(dāng)有操作對(duì)響應(yīng)式數(shù)據(jù)進(jìn)行讀取中,我們將其添加至一個(gè)依賴隊(duì)列,當(dāng)修改響應(yīng)式數(shù)據(jù)的值時(shí),將依賴隊(duì)列中的操作依次取出,并執(zhí)行。以下使用Proxy對(duì)該思路進(jìn)行實(shí)現(xiàn)。
const bucket = new Set();
const data = { text: "hello world." };
const obj = new Proxy(data, {
get(target, key) {
bucket.add(effect);
return target[key];
},
set(target, key, newVal) {
target[key] = newVal;
bucket.forEach((fn) => fn());
return true;
},
});
const body = {
innerText: "",
};
function effect() {
body.innerText = obj.text;
}
effect();
console.log(body.innerText); // hello world
obj.text = "text";
console.log(body.innerText); // text
? 但是,該實(shí)現(xiàn)仍然存在缺陷,比如說只能通過effect函數(shù)的名字實(shí)現(xiàn)依賴收集。
3.設(shè)計(jì)一個(gè)完善的響應(yīng)式系統(tǒng)
(1)消除依賴收集的硬綁定
? 這里我們使用一個(gè)active變量來保存當(dāng)前需要進(jìn)行依賴收集的函數(shù)。
const bucket = new Set();
const data = { text: "hello world." };
let activeEffect; // 新增一個(gè)active變量
const obj = new Proxy(data, {
get(target, key) {
if (activeEffect) {
bucket.add(activeEffect); // 添加active變量保存的函數(shù)
}
return target[key];
},
set(target, key, newVal) {
target[key] = newVal;
bucket.forEach((fn) => fn());
return true;
},
});
function effect(fn) {
activeEffect = fn; // 將當(dāng)前函數(shù)賦值給active變量
fn();
}
const body = {
innerText: "",
};
effect(() => {
body.innerText = obj.text;
});
console.log(body.innerText); // hello world
obj.text = "text";
console.log(body.innerText); // text
? 但是該設(shè)計(jì)仍然存在很多問題,比如說,當(dāng)訪問一個(gè)obj對(duì)象上并不存在的屬性假設(shè)為val時(shí),邏輯上并沒有存在對(duì)obj.val的訪問,因此該操作不會(huì)產(chǎn)生任何響應(yīng),但實(shí)際上,當(dāng)val的值被修改后,傳入effect的匿名函數(shù)會(huì)再次執(zhí)行。
(2)基于屬性的依賴收集
? 上一個(gè)版本的響應(yīng)式系統(tǒng)只能對(duì)攔截對(duì)象所有的get和set操作進(jìn)行響應(yīng),并不能做到細(xì)粒度的控制??紤]針對(duì)屬性進(jìn)行依賴攔截,主要有三個(gè)角色,對(duì)象、屬性和依賴方法。因此考慮修改bucket的結(jié)構(gòu),由原來的Set修改為WeakMap(target,Map(key,activeEffect));這樣就可以針對(duì)屬性進(jìn)行細(xì)粒度的依賴收集了。
ps.使用WeakMap是因?yàn)閃eakMap是對(duì)key的弱引用,不會(huì)影響垃圾回收機(jī)制的工作,當(dāng)target對(duì)象不存在任何引用時(shí),說明target對(duì)象已不被需要,這時(shí)target對(duì)象將會(huì)被垃圾回收。如果換成Map,即時(shí)target不存在任何引用,Map已然會(huì)保持對(duì)target的引用,容易造成內(nèi)存泄露。
// bucket的數(shù)據(jù)結(jié)構(gòu)修改為WeakMap
const bucket = new WeakMap();
const data = { text: "hello world." };
let activeEffect;
const obj = new Proxy(data, {
get(target, key) {
track(target, key);
return target[key];
},
set(target, key, newVal) {
target[key] = newVal;
trigger(target, key);
},
});
function track(target, key) {
if (!activeEffect) {
return;
}
let depsMap = bucket.get(target);
if (!depsMap) {
bucket.set(target, (depsMap = new Map()));
}
let deps = depsMap.get(key);
if (!deps) {
depsMap.set(key, (deps = new Set()));
}
deps.add(activeEffect);
}
function trigger(target, key) {
const depsMap = bucket.get(target);
if (!depsMap) return;
const effects = depsMap.get(key);
effects && effects.forEach((fn) => fn());
}
function effect(fn) {
activeEffect = fn;
fn();
}
const body = {
innerText: "",
};
effect(() => {
body.innerText = obj.text;
});
console.log(body.innerText); // hello world
obj.text = "text";
console.log(body.innerText); // text
(3)分支切換和cleanup
? 對(duì)于一段三元運(yùn)算符 obj.flag? obj.text : 'text',我們所期望的結(jié)果是,當(dāng)obj.flag的值為false時(shí),不會(huì)對(duì)obj.text屬性進(jìn)行響應(yīng)操作。 如果是上面那段程序,當(dāng)obj.flag的值為false時(shí),操作obj.text仍然會(huì)進(jìn)行相應(yīng)操作,因?yàn)閛bj.text對(duì)應(yīng)的依賴仍然存在。對(duì)此如果我們能夠在每次的函數(shù)執(zhí)行之前,將其從之前相關(guān)聯(lián)的依賴集合中移除,就可以達(dá)到目的了。這里通過修改副作用函數(shù)來實(shí)現(xiàn):
function effect(fn) {
const effectFn = () => {
// 在依賴函數(shù)執(zhí)行之前,清除依賴函數(shù)之前的依賴項(xiàng)
cleanup(effectFn);
activeEffect = effectFn;
fn();
};
// 給副作用函數(shù)添加一個(gè)deps數(shù)組用來收集和該副作用函數(shù)相關(guān)聯(lián)的依賴
effectFn.deps = [];
effectFn();
}
// cleanup函數(shù)實(shí)現(xiàn)
function cleanup(effectFn) {
for (let i = 0; i < effectFn.deps.length; i++) {
const deps = effectFn.deps[i];
deps.delete(effectFn);
}
effectFn.deps.length = 0;
}
function track(target, key) {
if (!activeEffect) {
return;
}
let depsMap = bucket.get(target);
if (!depsMap) {
bucket.set(target, (depsMap = new Map()));
}
let deps = depsMap.get(key);
if (!deps) {
depsMap.set(key, (deps = new Set()));
}
deps.add(activeEffect);
activeEffect.deps.push(deps); // 在這里收集相關(guān)聯(lián)的依賴
}
function trigger(target, key) {
const depsMap = bucket.get(target);
if (!depsMap) return;
const effects = depsMap.get(key);
const effectToRun = new Set(effects); // 這里需要?jiǎng)?chuàng)建一個(gè)新集合來遍歷,因?yàn)閒oreach循環(huán)會(huì)對(duì)新加入序列的元素也執(zhí)行遍歷,若遍歷直接原集合,會(huì)出現(xiàn)死循環(huán)。
effectToRun.forEach((fn) => fn());
}
(4)嵌套effect
? 雖然我們已經(jīng)解決了很多問題,但是作為響應(yīng)式系統(tǒng)中比較常見的場(chǎng)景之一的嵌套,我們還不能實(shí)現(xiàn)。因?yàn)槲覀兌x的activeEffect是一個(gè)變量,當(dāng)嵌套操作時(shí),無論怎樣,最后activeEffect變量中存放的都是操作的最后一個(gè)副作用函數(shù)。這里,我們通過加入一個(gè)effect棧的方式,來給這套響應(yīng)式系統(tǒng)添加嵌套功能。
// 定義一個(gè)effect棧
const effectStack = [];
function effect(fn) {
const effectFn = () => {
cleanup(effectFn);
activeEffect = effectFn;
effectStack.push(effectFn); // 在effect執(zhí)行之前,放入棧中
fn();
effectStack.pop(); // 執(zhí)行完畢立即彈出
activeEffect = effectStack[effectStack.length - 1]; // activeEffect指向新的effect
};
effectFn.deps = [];
effectFn();
}
(5)避免產(chǎn)生死循環(huán)
? 試看obj.val++這條語(yǔ)句,它實(shí)際上相當(dāng)于obj.val = obj.val+1,也就是進(jìn)行了一次讀取操作和一次賦值操作,共兩次操作。而若將該操作運(yùn)行在我們前面的響應(yīng)式系統(tǒng)中,它將會(huì)產(chǎn)生死循環(huán),因?yàn)楫?dāng)我們進(jìn)行了讀取操作后,會(huì)立即進(jìn)行賦值操作,而在賦值操作中,讀取操作再次被觸發(fā),然后循環(huán)的執(zhí)行這一系列操作。這里我們?cè)趖rigger函數(shù)中判斷trigger觸發(fā)的副作用函數(shù),是否與當(dāng)前正在執(zhí)行的副作用函數(shù)相同,若相同,則不執(zhí)行當(dāng)前副作用函數(shù)。這樣就能避免無限遞歸調(diào)用,避免內(nèi)存溢出。
function trigger(target, key) {
const depsMap = bucket.get(target);
if (!depsMap) return;
const effects = depsMap.get(key);
const effectToRun = new Set();
effects &&
effects.forEach((fn) => {
// 若正在執(zhí)行的副作用函數(shù)與當(dāng)前觸發(fā)的副作用函數(shù)相同,則不執(zhí)行
if (fn !== activeEffect) {
effectToRun.add(fn);
}
});
effectToRun.forEach((fn) => fn());
}
(6)實(shí)現(xiàn)調(diào)度實(shí)行
現(xiàn)在要實(shí)現(xiàn)一個(gè)這樣的效果:
effect(()=> {
console.log(obj.val);
});
obj.val ++;
console.log("結(jié)束");
// 這段代碼本來會(huì)輸出的結(jié)果是:
/**
1
2
結(jié)束
**/
// 現(xiàn)在我們想讓它變成
/**
1
結(jié)束
2
**/
這里我們可以通過給effect函數(shù)添加一個(gè)配置項(xiàng)來實(shí)現(xiàn):
effect(
()=> {
console.log(obj.val);
},
{
scheduler(fn) {
setTimeout(fn);
}
}
function effect(fn,options = {}) {
const effectFn = ()=> {
...
}
effectFn.deps = [];
effectFn.options = options; // 為副作用函數(shù)添加配置項(xiàng)
effectFn();
}
function trigger(target, key) {
const depsMap = bucket.get(target);
if (!depsMap) return;
const effects = depsMap.get(key);
const effectToRun = new Set();
effects &&
effects.forEach((fn) => {
if (fn !== activeEffect) {
effectToRun.add(fn);
}
});
effectToRun.forEach((fn) => {
// 若當(dāng)前依賴函數(shù)含有調(diào)度執(zhí)行,將當(dāng)前函數(shù)傳遞給調(diào)度函數(shù)執(zhí)行
if (fn.options.scheduler) {
fn.options.scheduler(fn); //將當(dāng)前函數(shù)傳遞給調(diào)度函數(shù)
} else {
fn();
}
});
}
如果還要實(shí)現(xiàn)一下效果:
effect(()=> {
console.log(obj.val);
});
obj.val ++;
obj.val ++;
// 這段代碼本來會(huì)輸出的結(jié)果是:
/**
1
2
3
**/
// 現(xiàn)在我們想讓它變成
/**
1
3
**/
這里通過添加一個(gè)任務(wù)執(zhí)行隊(duì)列來實(shí)現(xiàn):
const jobQueue = new Set();
const p = Promise.resolve();
let isFlushing = false;
effect(
()=> {
console.log(obj.val);
},
{
scheduler(fn){
jobQueue.add(fn);
flushJob();
}
}
);
function flushJob() {
if(isFlushing) return;
isFlushing = true;
p.then(()=> {
jobQueue.forEach(job=>job());
}).finally(()=> {
isFlushing = false;
})
}
? 像這樣,由于Set保證了任務(wù)的唯一性,也就是jobQueue中只會(huì)保存唯一的一個(gè)任務(wù),即當(dāng)前執(zhí)行的任務(wù)。而isFlushing標(biāo)記則保證任務(wù)只會(huì)執(zhí)行一次。而因?yàn)橥ㄟ^Promise將任務(wù)添加到了微任務(wù)隊(duì)列中,當(dāng)任務(wù)最后執(zhí)行的時(shí)候,obj.val的值已經(jīng)是3了。
(7)computed和lazy
? 計(jì)算屬性是vue中一個(gè)比較有特色的屬性,它會(huì)緩存表達(dá)式的計(jì)算結(jié)果,只有當(dāng)表達(dá)式依賴的變量發(fā)生變化時(shí),它才會(huì)進(jìn)行重新計(jì)算。實(shí)現(xiàn)計(jì)算屬性的前提是實(shí)現(xiàn)懶加載標(biāo)記,這里我們可以通過之前effect函數(shù)的配置項(xiàng)來實(shí)現(xiàn)。
effect(
()=> {
return ()=>obj.val * 2;
},
{
lazy: true; // 設(shè)置 lazy 標(biāo)記
}
);
effect(fn, options = {}) {
const effectFn = () => {
cleanup(effectFn);
activeEffect = effectFn;
effectStack.push(effectFn);
const res = fn();
effectStack.pop();
activeEffect = effectStack[effectStack.length - 1];
return res;
};
effectFn.deps = [];
effectFn.options = options;
if (!effectFn.options.lazy) { // 若副作用函數(shù)持有l(wèi)azy標(biāo)記,則直接將副作用函數(shù)返回
effectFn();
}
return effectFn;
}
通過上面對(duì)lazy標(biāo)記的設(shè)置,現(xiàn)在可以實(shí)現(xiàn)下面的效果:
const effectFn = effect(
()=> {
return ()=>obj.val * 2;
},
{
lazy: true; // 設(shè)置 lazy 標(biāo)記
}
)();
console.log(effectFn); // 2
在此基礎(chǔ)上,我們來實(shí)現(xiàn)computed
function computed(getter) {
let value;
let dirty = false;
const effectFn = effect(getter, {
lazy: true,
scheduler(){
if(!dirty) {
dirty = true;
tirgger(obj, 'value');
}
}
});
const obj = {
get value() {
if(!dirty) {
value = effectFn();
dirty = true;
}
track(target, 'value');
return value;
}
};
return obj;
}
(8)watch
? 想要實(shí)現(xiàn)watch,其實(shí)只需要添加一個(gè)scheduler(),像是這樣:
effect(
()=> {
consoloe.log(obj.val);
},
{
scheduler() {
console.log("數(shù)值發(fā)生了變化");
}
}
)
就可以實(shí)現(xiàn)一個(gè)基本的watch效果,現(xiàn)在來編寫一個(gè)功能完整的watch函數(shù)
function watch(source, cb) {
let getter;
if(typeof source === "function") { //若傳入()=> obj.val,則直接使用該匿名函數(shù)
getter = source;
} else {
getter = traverse(source); // 否則遞歸遍歷該對(duì)象的所有屬性,從而達(dá)到監(jiān)聽所有屬性的目的
}
let oldValue, newValue; // 保存新舊值
const effectFn = effect(getter, {
lazy: true,
scheduler() {
newValue = effectFn(); // 獲取新值
cb(oldValue, newValue);
oldValue = newValue; // 函數(shù)執(zhí)行完后,更新舊值。
}
});
oldValue = effectFn(); // 獲取初始舊值
}
function traverse(value, seen = new Set()) {
if(typeof value !== 'object' || value !== null || seen.has(value)) return ;
seen.add(value);
for(const k in seen) {
traverse(seen[k],seen);
}
}
到此這篇關(guān)于Vue響應(yīng)式原理深入分析的文章就介紹到這了,更多相關(guān)Vue響應(yīng)式原理內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
vue 實(shí)現(xiàn)購(gòu)物車總價(jià)計(jì)算
今天小編就為大家分享一篇vue 實(shí)現(xiàn)購(gòu)物車總價(jià)計(jì)算,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2019-11-11
Vue項(xiàng)目中使用百度地圖api的詳細(xì)步驟
在之前的一個(gè)小項(xiàng)目中,用到的顯示當(dāng)?shù)氐牡貓D功能,下面這篇文章主要給大家介紹了關(guān)于Vue項(xiàng)目中使用百度地圖api的詳細(xì)步驟,文中通過圖文介紹的非常詳細(xì),需要的朋友可以參考下2022-10-10
vue實(shí)現(xiàn)標(biāo)簽云效果的方法詳解
這篇文章主要介紹了vue實(shí)現(xiàn)標(biāo)簽云效果的方法,結(jié)合實(shí)例形式詳細(xì)分析了vue標(biāo)簽云的實(shí)現(xiàn)技巧與相關(guān)操作注意事項(xiàng),需要的朋友可以參考下2019-08-08
vue3?使用setup語(yǔ)法糖實(shí)現(xiàn)分類管理功能
這篇文章主要介紹了vue3?使用setup語(yǔ)法糖實(shí)現(xiàn)分類管理,本次模塊使用 vue3+element-plus 實(shí)現(xiàn)一個(gè)新聞?wù)镜暮笈_(tái)分類管理模塊,其中新增、編輯采用對(duì)話框方式公用一個(gè)表單,需要的朋友可以參考下2022-08-08
vue實(shí)現(xiàn)簡(jiǎn)單學(xué)生信息管理
這篇文章主要為大家詳細(xì)介紹了vue實(shí)現(xiàn)簡(jiǎn)單學(xué)生信息管理,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-05-05
VUE3?Vite打包后動(dòng)態(tài)圖片資源不顯示問題解決方法
這篇文章主要給大家介紹了關(guān)于VUE3?Vite打包后動(dòng)態(tài)圖片資源不顯示問題的解決方法,可能是因?yàn)樵诓渴鸷蟮姆?wù)器環(huán)境中對(duì)中文文件名的支持不完善,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-09-09

