欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Vue響應(yīng)式原理深入分析

 更新時(shí)間:2022年12月30日 09:19:20   作者:volit_  
響應(yīng)式就是當(dāng)對(duì)象本身(對(duì)象的增刪值)或者對(duì)象屬性(重新賦值)發(fā)生變化時(shí),將會(huì)運(yùn)行一些函數(shù),最常見的就是render函數(shù),下面這篇文章主要給大家介紹了關(guān)于Vue3響應(yīng)式原理的相關(guān)資料,需要的朋友可以參考下

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ì)算

    今天小編就為大家分享一篇vue 實(shí)現(xiàn)購(gòu)物車總價(jià)計(jì)算,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧
    2019-11-11
  • Vue?privide?和inject?依賴注入的使用詳解

    Vue?privide?和inject?依賴注入的使用詳解

    這篇文章主要介紹了Vue?privide?和inject?依賴注入的用法,本文通過示例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2022-10-10
  • Vue項(xiàng)目中使用百度地圖api的詳細(xì)步驟

    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)簽云效果的方法詳解

    這篇文章主要介紹了vue實(shí)現(xiàn)標(biāo)簽云效果的方法,結(jié)合實(shí)例形式詳細(xì)分析了vue標(biāo)簽云的實(shí)現(xiàn)技巧與相關(guān)操作注意事項(xiàng),需要的朋友可以參考下
    2019-08-08
  • element-ui表格列金額顯示兩位小數(shù)的方法

    element-ui表格列金額顯示兩位小數(shù)的方法

    這篇文章主要介紹了element-ui表格列金額顯示兩位小數(shù)的方法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧
    2018-08-08
  • vue3?使用setup語(yǔ)法糖實(shí)現(xiàn)分類管理功能

    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é)生信息管理

    vue實(shí)現(xiàn)簡(jiǎn)單學(xué)生信息管理

    這篇文章主要為大家詳細(xì)介紹了vue實(shí)現(xiàn)簡(jiǎn)單學(xué)生信息管理,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2020-05-05
  • vue實(shí)現(xiàn)打地鼠小游戲

    vue實(shí)現(xiàn)打地鼠小游戲

    這篇文章主要為大家詳細(xì)介紹了vue實(shí)現(xiàn)打地鼠小游戲,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2020-08-08
  • vue中的.sync修飾符用法及原理分析

    vue中的.sync修飾符用法及原理分析

    這篇文章主要介紹了vue中的.sync修飾符用法及原理分析,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2022-04-04
  • VUE3?Vite打包后動(dòng)態(tài)圖片資源不顯示問題解決方法

    VUE3?Vite打包后動(dòng)態(tài)圖片資源不顯示問題解決方法

    這篇文章主要給大家介紹了關(guān)于VUE3?Vite打包后動(dòng)態(tài)圖片資源不顯示問題的解決方法,可能是因?yàn)樵诓渴鸷蟮姆?wù)器環(huán)境中對(duì)中文文件名的支持不完善,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下
    2024-09-09

最新評(píng)論