最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

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

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

1.響應(yīng)式數(shù)據(jù)和副作用函數(shù)

(1)副作用函數(shù)

副作用函數(shù)就是會產(chǎn)生副作用的函數(shù)。

function effect() {
    document.body.innerText = 'hello world.'
}

? 當effect函數(shù)執(zhí)行時,它會設(shè)置body的內(nèi)容,而body是一個全局變量,除了effect函數(shù)外任何地方都可以訪問到,也就是說effect函數(shù)的執(zhí)行會對其他操作產(chǎn)生影響,即effect函數(shù)是一個副作用函數(shù)。

(2)響應(yīng)式數(shù)據(jù)

const obj = { text: 'hello world.'};
function effect() {
    document.body.innerText = obj.text;
}
obj.text = 'text';

? 當 obj.text = 'text' 這條語句執(zhí)行之后,我們期望 document.body.innerText 的值也能夠隨之修改,這就是通常意義上的響應(yīng)式數(shù)據(jù)。

2.響應(yīng)式數(shù)據(jù)的基本實現(xiàn)

? 上文中,對響應(yīng)式數(shù)據(jù)進行描述的代碼段,并未實現(xiàn)真正的響應(yīng)式數(shù)據(jù)。而通過觀察我們可以發(fā)現(xiàn),要實現(xiàn)真正的響應(yīng)式數(shù)據(jù),我們需要對數(shù)據(jù)的讀取和設(shè)置進行攔截。當有操作對響應(yīng)式數(shù)據(jù)進行讀取中,我們將其添加至一個依賴隊列,當修改響應(yīng)式數(shù)據(jù)的值時,將依賴隊列中的操作依次取出,并執(zhí)行。以下使用Proxy對該思路進行實現(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

? 但是,該實現(xiàn)仍然存在缺陷,比如說只能通過effect函數(shù)的名字實現(xiàn)依賴收集。

3.設(shè)計一個完善的響應(yīng)式系統(tǒng)

(1)消除依賴收集的硬綁定

? 這里我們使用一個active變量來保存當前需要進行依賴收集的函數(shù)。

const bucket = new Set();
const data = { text: "hello world." };
let activeEffect; // 新增一個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; // 將當前函數(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è)計仍然存在很多問題,比如說,當訪問一個obj對象上并不存在的屬性假設(shè)為val時,邏輯上并沒有存在對obj.val的訪問,因此該操作不會產(chǎn)生任何響應(yīng),但實際上,當val的值被修改后,傳入effect的匿名函數(shù)會再次執(zhí)行。

(2)基于屬性的依賴收集

? 上一個版本的響應(yīng)式系統(tǒng)只能對攔截對象所有的get和set操作進行響應(yīng),并不能做到細粒度的控制。考慮針對屬性進行依賴攔截,主要有三個角色,對象、屬性和依賴方法。因此考慮修改bucket的結(jié)構(gòu),由原來的Set修改為WeakMap(target,Map(key,activeEffect));這樣就可以針對屬性進行細粒度的依賴收集了。

ps.使用WeakMap是因為WeakMap是對key的弱引用,不會影響垃圾回收機制的工作,當target對象不存在任何引用時,說明target對象已不被需要,這時target對象將會被垃圾回收。如果換成Map,即時target不存在任何引用,Map已然會保持對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

? 對于一段三元運算符 obj.flag? obj.text : 'text',我們所期望的結(jié)果是,當obj.flag的值為false時,不會對obj.text屬性進行響應(yīng)操作。 如果是上面那段程序,當obj.flag的值為false時,操作obj.text仍然會進行相應(yīng)操作,因為obj.text對應(yīng)的依賴仍然存在。對此如果我們能夠在每次的函數(shù)執(zhí)行之前,將其從之前相關(guān)聯(lián)的依賴集合中移除,就可以達到目的了。這里通過修改副作用函數(shù)來實現(xiàn):

function effect(fn) {
  const effectFn = () => {
    // 在依賴函數(shù)執(zhí)行之前,清除依賴函數(shù)之前的依賴項
    cleanup(effectFn);
    activeEffect = effectFn;
    fn();
  };
  // 給副作用函數(shù)添加一個deps數(shù)組用來收集和該副作用函數(shù)相關(guān)聯(lián)的依賴
  effectFn.deps = [];
  effectFn();
}
// cleanup函數(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); // 這里需要創(chuàng)建一個新集合來遍歷,因為foreach循環(huán)會對新加入序列的元素也執(zhí)行遍歷,若遍歷直接原集合,會出現(xiàn)死循環(huán)。
  effectToRun.forEach((fn) => fn());
}

(4)嵌套effect

? 雖然我們已經(jīng)解決了很多問題,但是作為響應(yīng)式系統(tǒng)中比較常見的場景之一的嵌套,我們還不能實現(xiàn)。因為我們定義的activeEffect是一個變量,當嵌套操作時,無論怎樣,最后activeEffect變量中存放的都是操作的最后一個副作用函數(shù)。這里,我們通過加入一個effect棧的方式,來給這套響應(yīng)式系統(tǒng)添加嵌套功能。

// 定義一個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++這條語句,它實際上相當于obj.val = obj.val+1,也就是進行了一次讀取操作和一次賦值操作,共兩次操作。而若將該操作運行在我們前面的響應(yīng)式系統(tǒng)中,它將會產(chǎn)生死循環(huán),因為當我們進行了讀取操作后,會立即進行賦值操作,而在賦值操作中,讀取操作再次被觸發(fā),然后循環(huán)的執(zhí)行這一系列操作。這里我們在trigger函數(shù)中判斷trigger觸發(fā)的副作用函數(shù),是否與當前正在執(zhí)行的副作用函數(shù)相同,若相同,則不執(zhí)行當前副作用函數(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ù)與當前觸發(fā)的副作用函數(shù)相同,則不執(zhí)行
      if (fn !== activeEffect) {
        effectToRun.add(fn);  
      }
    });
  effectToRun.forEach((fn) => fn());
}

(6)實現(xiàn)調(diào)度實行

現(xiàn)在要實現(xiàn)一個這樣的效果:

effect(()=> {
    console.log(obj.val);
});
obj.val ++;
console.log("結(jié)束");

// 這段代碼本來會輸出的結(jié)果是:
/**
    1
    2
    結(jié)束
 **/
// 現(xiàn)在我們想讓它變成
/**
     1
     結(jié)束
     2
 **/

這里我們可以通過給effect函數(shù)添加一個配置項來實現(xiàn):

effect(
  ()=> {
    console.log(obj.val);
  },
  {
      scheduler(fn) {
          setTimeout(fn);
      }
  }
function effect(fn,options = {}) {
    const effectFn = ()=> {
       ...
    }
    effectFn.deps = [];
    effectFn.options = options; // 為副作用函數(shù)添加配置項
    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) => {
    // 若當前依賴函數(shù)含有調(diào)度執(zhí)行,將當前函數(shù)傳遞給調(diào)度函數(shù)執(zhí)行
    if (fn.options.scheduler) {
      fn.options.scheduler(fn); //將當前函數(shù)傳遞給調(diào)度函數(shù)
    } else {
      fn();
    }
  });
}

如果還要實現(xiàn)一下效果:

effect(()=> {
    console.log(obj.val);
});
obj.val ++;
obj.val ++;
// 這段代碼本來會輸出的結(jié)果是:
/**
	1
	2
	3
 **/
// 現(xiàn)在我們想讓它變成
/**
 	1
 	3
 **/

這里通過添加一個任務(wù)執(zhí)行隊列來實現(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中只會保存唯一的一個任務(wù),即當前執(zhí)行的任務(wù)。而isFlushing標記則保證任務(wù)只會執(zhí)行一次。而因為通過Promise將任務(wù)添加到了微任務(wù)隊列中,當任務(wù)最后執(zhí)行的時候,obj.val的值已經(jīng)是3了。

(7)computed和lazy

? 計算屬性是vue中一個比較有特色的屬性,它會緩存表達式的計算結(jié)果,只有當表達式依賴的變量發(fā)生變化時,它才會進行重新計算。實現(xiàn)計算屬性的前提是實現(xiàn)懶加載標記,這里我們可以通過之前effect函數(shù)的配置項來實現(xiàn)。

effect(
	()=> {
        return ()=>obj.val * 2;
    },
    {
        lazy: true; // 設(shè)置 lazy 標記
    }
);
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標記,則直接將副作用函數(shù)返回
    effectFn();
  }
  return effectFn;
}

通過上面對lazy標記的設(shè)置,現(xiàn)在可以實現(xiàn)下面的效果:

const effectFn = effect(
	()=> {
        return ()=>obj.val * 2;
    },
    {
        lazy: true; // 設(shè)置 lazy 標記
    }
)();
console.log(effectFn); // 2

在此基礎(chǔ)上,我們來實現(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

? 想要實現(xiàn)watch,其實只需要添加一個scheduler(),像是這樣:

effect(
	()=> {
        consoloe.log(obj.val);
    },
    {
        scheduler() {
            console.log("數(shù)值發(fā)生了變化");
        }
    }
)

就可以實現(xiàn)一個基本的watch效果,現(xiàn)在來編寫一個功能完整的watch函數(shù)

function watch(source, cb) {
    let getter;
    if(typeof source === "function") { //若傳入()=> obj.val,則直接使用該匿名函數(shù)
        getter = source;
    } else {
        getter = traverse(source); // 否則遞歸遍歷該對象的所有屬性,從而達到監(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)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • vue.js表單驗證插件(vee-validate)的使用教程詳解

    vue.js表單驗證插件(vee-validate)的使用教程詳解

    這篇文章主要介紹了vue.js表單驗證插件(vee-validate)的使用,本文通過實例代碼給大家介紹的非常詳細,具有一定的參考借鑒價值,需要的朋友可以參考下
    2019-05-05
  • Vue3中Element-Plus分頁(Pagination)組件的使用

    Vue3中Element-Plus分頁(Pagination)組件的使用

    Element-Plus分頁(Pagination)組件在開發(fā)過程中數(shù)據(jù)展示會經(jīng)常使用到,同時分頁功能也會添加到頁面中,下面我們就來學習一下它的具體使用,需要的可以參考一下
    2023-11-11
  • Vue插槽簡介和使用示例詳解

    Vue插槽簡介和使用示例詳解

    插槽就是子組件中的提供給父組件使用的一個占位符,用<slot></slot>?表示,父組件可以在這個占位符中填充任何模板代碼,如?HTML、組件等,填充的內(nèi)容會替換子組件的<slot></slot>標簽,這篇文章主要介紹了Vue插槽的理解和使用,需要的朋友可以參考下
    2023-03-03
  • Vue3+Vite如何解決“找不到模塊“@/components/xxx.vue”或其相應(yīng)的類型聲明ts(2307)”

    Vue3+Vite如何解決“找不到模塊“@/components/xxx.vue”或其相應(yīng)的類型聲明ts(2307)”

    在Vue項目中使用Vite時,可能因版本差異而需修改tsconfig.app.json而非tsconfig.json以解決編譯錯誤,本文分享個人解決經(jīng)驗,供參考
    2024-10-10
  • 去除Element-Plus下拉菜單邊框的實現(xiàn)步驟

    去除Element-Plus下拉菜單邊框的實現(xiàn)步驟

    Element-Plus 是 Element UI 的 Vue 3 版本,它提供了一套完整的組件庫,在使用 Element-Plus 進行開發(fā)時,我們可能會遇到需要自定義組件樣式的情況,本文將介紹如何使用 CSS 來去除 Element-Plus 下拉框的邊框,需要的朋友可以參考下
    2024-03-03
  • Vue+LogicFlow+Flowable實現(xiàn)工作流

    Vue+LogicFlow+Flowable實現(xiàn)工作流

    本文主要介紹了Vue+LogicFlow+Flowable實現(xiàn)工作流,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2023-12-12
  • vuex頁面刷新數(shù)據(jù)丟失解決方法詳解

    vuex頁面刷新數(shù)據(jù)丟失解決方法詳解

    這篇文章主要為大家介紹了vuex頁面刷新數(shù)據(jù)丟失解決方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2023-07-07
  • Vue Element UI + OSS實現(xiàn)上傳文件功能

    Vue Element UI + OSS實現(xiàn)上傳文件功能

    這篇文章主要為大家詳細介紹了Vue Element UI + OSS實現(xiàn)上傳文件功能,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2019-07-07
  • 詳解如何使用Vue實現(xiàn)圖像識別和人臉對比

    詳解如何使用Vue實現(xiàn)圖像識別和人臉對比

    隨著人工智能的發(fā)展,圖像識別和人臉識別技術(shù)已經(jīng)被廣泛應(yīng)用于各種應(yīng)用程序中,Vue為我們提供了許多實用工具和庫,可以幫助我們在應(yīng)用程序中進行圖像識別和人臉識別,在本文中,我們將介紹如何使用Vue進行圖像識別和人臉對比,需要的朋友可以參考下
    2023-06-06
  • 簡單了解vue 插值表達式Mustache

    簡單了解vue 插值表達式Mustache

    這篇文章主要介紹了vue 插值表達式Mustache的相關(guān)資料,文中講解非常細致,代碼幫助大家更好的理解和學習,感興趣的朋友可以了解下
    2020-07-07

最新評論

积石山| 思南县| 阿拉善左旗| 长治市| 松潘县| 兰坪| 贵定县| 马山县| 周宁县| 永修县| 台江县| 兴山县| 九寨沟县| 镶黄旗| 漳平市| 沅陵县| 山丹县| 泸西县| 黄浦区| 公安县| 襄城县| 邵武市| 新巴尔虎左旗| 南汇区| 格尔木市| 西和县| 陕西省| 永城市| 平顺县| 新郑市| 阿合奇县| 若尔盖县| 瑞金市| 永昌县| 徐州市| 资兴市| 台北市| 巴彦县| 瑞丽市| 酉阳| 大关县|