vue3響應(yīng)式數(shù)據(jù)原理

Effect 原理解析 與 實(shí)現(xiàn)

引言:

vue部凑、react 框架的核心都是數(shù)據(jù)驅(qū)動(dòng)視圖也就是model => view,實(shí)現(xiàn)的核心也就是 數(shù)據(jù)響應(yīng)市栗。

主要就三步:

  1. 創(chuàng)建響應(yīng)式的數(shù)據(jù) defineProperty谓苟、pxoxy诉瓦。這樣使用茬腿、修改數(shù)據(jù)的事件我們都能捕捉到

  2. 在使用響應(yīng)式的數(shù)據(jù)時(shí)收集依賴聚至,把跟該數(shù)據(jù)相關(guān)的副作用都儲(chǔ)存起來(lái)

  3. 在修改響應(yīng)式的數(shù)據(jù)時(shí)觸發(fā)依賴,執(zhí)行相關(guān)的副作用

    <template>
    <div>{{msg}}</div>
    </template>
    <script>
    export default {
    data(){
    return {
    msg: 'hello world'
    }
    },
    methods: {
    change(){
    this.msg = 'zhenganlin'
    }
    }
    }
    </script>

一、effect:副作用函數(shù)

1.類似于vue2.0中watch 的升級(jí)版固该,如果函數(shù)中用到的響應(yīng)式的數(shù)據(jù)發(fā)生了變化锅减,則會(huì)執(zhí)行該函數(shù)

// Effect 的簡(jiǎn)單應(yīng)用
const component = defineComponent({
            name: 'zhenAPP',
            template: `
                <div>
                    <button @click="addHandler">add</button>
                </div>
            `,
            setup(props) {
                const data = reactive({
                    count: 0,
                });
                console.log('-----------創(chuàng)建reactive------------')
                console.log('創(chuàng)建reactive對(duì)象:', data)
                const addHandler = () => {
                    data.count++;
                };
                effect(() => {
                    console.log('1',data.count)
                });
                return {
                    addHandler,
                };
            },
        });


// 在 Vue.js 3.0 中,初始化一個(gè)應(yīng)用的方式如下
import { createApp } from 'vue'
import App from './app'
const app = createApp(App)
app.mount('#app')
// 設(shè)置并運(yùn)行帶副作用的渲染函數(shù)
setupRenderEffect(instance, initialVNode, container, anchor, parentSuspense ...)

二伐坏、proxy 與reflect

Object.defineProperty API 的一些缺點(diǎn):

1. 不能監(jiān)聽對(duì)象屬性新增和刪除怔匣;
2. 初始化階段遞歸執(zhí)行 Object.defineProperty 帶來(lái)的性能負(fù)擔(dān)。

const p = new Proxy(target, handlerObject)

handlerObject = {
  get(target,key,receiver){ // receiver 可以理解成改變get函數(shù)中的this指向桦沉,默認(rèn)就是handlerObject
    
    },
  set(target,key,value,receiver){
    
  }
}

vue3源碼的調(diào)試方法:

  • clone vue-next
  • npm run dev
  • 新建html文件每瞒,引用打包的js
    <!DOCTYPE html>
    <html>
    <head>
        <title>vue-demo</title>
    </head>
    <body>
        <div id="app"></div>
        <script src="./packages/vue/dist/vue.global.js"></script>
        <script>
            const { defineComponent, createApp, reactive, toRefs, watchEffect } = Vue;
            console.log(Vue)
            const component = defineComponent({
                template: `
                    <div>
                        {{ data.count }}
                        <button @click="addHandler">add</button>
                    </div>
                `,
                setup(props) {
                    const data = reactive({
                        count: 0,
                        person: {
                            name: 'zhenganlin'
                        }
                    });
                    console.log('創(chuàng)建reactive: ', data)
                    const addHandler = () => {
                        data.count++;
                        data.person = {age:25}
                    };
                    // watchEffect(() => {
                    //     console.log(data.count)
                    // });
                    return {
                        data,
                        addHandler,
                    };
                },
            });
            createApp(component).mount(document.querySelector('#app'));
        </script>
    </body>
    </html>

三、響應(yīng)式api reactive的實(shí)現(xiàn)

// 0 reactactive模塊的存儲(chǔ)變量:reactiveMap
//  作用1:維護(hù)整個(gè)應(yīng)用數(shù)據(jù)代理纯露,防止對(duì)同一個(gè)對(duì)象重復(fù)代理剿骨,比如父子組件共享某個(gè)響應(yīng)數(shù)據(jù)
//  作用2:weakmap 本身的優(yōu)勢(shì),這樣某個(gè)組件卸載之后埠褪,組件上的響應(yīng)數(shù)據(jù)也會(huì)被刪除浓利,reactiveMap也會(huì)刪除響應(yīng)數(shù)據(jù)。防止內(nèi)存泄露
export const reactiveMap = new WeakMap<Target, any>()

// 1.reactive 方法
export function reactive(target: object) {
  // ...排除不能代理的一些情況
  return createReactiveObject(
    target,
    mutableHandlers,
  )
}

// 2.createReactiveObject proxy代理
function createReactiveObject(
  target: Target,
  baseHandlers: ProxyHandler<any>,
) {
 
  // 代理去重 target already has corresponding Proxy
  const proxyMap =  reactiveMap
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  // 利用proxy生成響應(yīng)式對(duì)象 
  const proxy = new Proxy(
    target,
    mutableHandlers
  )
  // 存入map
  proxyMap.set(target, proxy)
  return proxy
}

// 3.baseHandlers
const mutableHandlers = {
  get: createGetter(),
  set: createSetter(),
}
// 4. get 函數(shù)的代理:調(diào)用了track 方法
function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {

    const res = Reflect.get(target, key, receiver)
        //重點(diǎn): track 實(shí)現(xiàn)依賴收集钞速。effect 中接下來(lái)會(huì)分析
    track(target, TrackOpTypes.GET, key)
    
    if (isObject(res)) {
      // 深度代理 
      return reactive(res)
    }

    return res
  }
}
// 5. set 函數(shù)的代理:調(diào)用了trigger方法
function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    const oldValue = (target as any)[key]
    if (!shallow) {
      // 如果value本身是響應(yīng)對(duì)象贷掖,把他變成普通對(duì)象
      // 對(duì)應(yīng)get中 isObject(res),方便統(tǒng)一處理
      // 這也是vue3與vue2 不同的地方
      // { person: {name:'tom'} }
      // vue2在代理的時(shí)候,兩層都會(huì)代理
      // vue3在代理的時(shí)候渴语,只代理第一層苹威,在使用到person的時(shí)候才會(huì)代理第二層
      value = toRaw(value)
    } else {
      // in shallow mode, objects are set as-is regardless of reactive or not
    }
      
    const result = Reflect.set(target, key, value, receiver)
    // 防止原型鏈的影響--ppt
    if (target === toRaw(receiver)) {
        // 重點(diǎn):在改變響應(yīng)對(duì)象的值的時(shí)候屠升,調(diào)用trigger 觸發(fā)響應(yīng)
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
    }
    return result
  }
}

三狭郑、Effect的依賴收集與響應(yīng)觸發(fā) (分-總-分-問題)

// 1. Effct 函數(shù)定義 與 局部變量緩存
export interface ReactiveEffect<T = any> {
  (): T
  _isEffect: true
  id: number
  active: boolean // active是effect激活的開關(guān),打開會(huì)收集依賴汇在,關(guān)閉會(huì)導(dǎo)致收集依賴無(wú)效
  raw: () => T // 原始監(jiān)聽函數(shù)
  deps: Array<Dep> // 存儲(chǔ)依賴的deps
  options: ReactiveEffectOptions
  allowRecurse: boolean
}
type Dep = Set<ReactiveEffect>
type KeyToDepMap = Map<any, Dep>


// 應(yīng)用中 響應(yīng)對(duì)象對(duì)應(yīng)的 KeyToDepMap
const targetMap = new WeakMap<any, KeyToDepMap>()
// 當(dāng)前執(zhí)行的effect
let activeEffect: ReactiveEffect | undefined
// 執(zhí)行中的effect棧
const effectStack: ReactiveEffect[] = []


// 2. Effct 
export function effect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
  // fn已經(jīng)是一個(gè)effect函數(shù)了翰萨,利用fn.raw重新創(chuàng)建effect
  if (isEffect(fn)) {
    fn = fn.raw
  }
  // 創(chuàng)建監(jiān)聽函數(shù)
  const effect = createReactiveEffect(fn, options)
  if (!options.lazy) {
    effect()
  }
  return effect
}

// 3.createReactiveEffect
function createReactiveEffect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions
): ReactiveEffect<T> {
  const effect = function reactiveEffect(): unknown {
    // 防止在effect(() => {data.count++}),造成循環(huán)引用
    if (!effectStack.includes(effect)) {
      cleanup(effect) // effect.deps = []
      try {
        effectStack.push(effect)
        activeEffect = effect
        // 調(diào)用原始函數(shù)時(shí),如果響應(yīng)式數(shù)據(jù)取值了
        // 會(huì)觸發(fā)這個(gè)響應(yīng)式對(duì)象的getter糕殉,getter里面就會(huì)調(diào)用track方法收集依賴
        return fn() 
        
      } finally {
        effectStack.pop()
        // 指向最后一個(gè)effect: 
        activeEffect = effectStack[effectStack.length - 1]
      }
    }
  } as ReactiveEffect
  effect.id = uid++
  effect.allowRecurse = !!options.allowRecurse
  effect._isEffect = true
  effect.active = true
  effect.raw = fn
  effect.deps = []
  effect.options = options
  return effect
}


// 4.track 函數(shù):收集依賴
export function track(target: object, type: TrackOpTypes, key: unknown) {
  if (!shouldTrack || activeEffect === undefined) {
    return
  }
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  let dep = depsMap.get(key)
  if (!dep) {
    depsMap.set(key, (dep = new Set()))
  }
  if (!dep.has(activeEffect)) {
    dep.add(activeEffect)
    activeEffect.deps.push(dep)
  }
}

// 5. trigger函數(shù):觸發(fā)依賴
export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
  const depsMap = targetMap.get(target)
  if (!depsMap) {
    // never been tracked
    return
  }
  // 確定需要觸發(fā)的依賴 set
  const effects = new Set<ReactiveEffect>()
  const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect => {
        if (effect !== activeEffect || effect.allowRecurse) {
          effects.add(effect)
        }
      })
    }
  }
  
  if (key !== void 0) {
    add(depsMap.get(key))
  }
  
  console.log('count的Effects', effects)
  const run = (effect: ReactiveEffect) => {
    // 如果傳入自定義調(diào)度器則執(zhí)行自定義的亩鬼,可以擴(kuò)展effect執(zhí)行
    if (effect.options.scheduler) {
      effect.options.scheduler(effect)
    } else {
      effect()
    }
  }

  effects.forEach(run)
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市阿蝶,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌玷过,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,542評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異辛蚊,居然都是意外死亡粤蝎,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門虑凛,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)碑宴,“玉大人,你說我怎么就攤上這事桑谍∧苟” “怎么了?”我有些...
    開封第一講書人閱讀 163,912評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵霉囚,是天一觀的道長(zhǎng)捕仔。 經(jīng)常有香客問我,道長(zhǎng)盈罐,這世上最難降的妖魔是什么榜跌? 我笑而不...
    開封第一講書人閱讀 58,449評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮盅粪,結(jié)果婚禮上钓葫,老公的妹妹穿的比我還像新娘。我一直安慰自己票顾,他們只是感情好础浮,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,500評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著奠骄,像睡著了一般豆同。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上含鳞,一...
    開封第一講書人閱讀 51,370評(píng)論 1 302
  • 那天影锈,我揣著相機(jī)與錄音,去河邊找鬼蝉绷。 笑死鸭廷,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的熔吗。 我是一名探鬼主播辆床,決...
    沈念sama閱讀 40,193評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼桅狠!你這毒婦竟也來(lái)了讼载?” 一聲冷哼從身側(cè)響起宵晚,我...
    開封第一講書人閱讀 39,074評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎维雇,沒想到半個(gè)月后淤刃,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,505評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡吱型,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,722評(píng)論 3 335
  • 正文 我和宋清朗相戀三年逸贾,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片津滞。...
    茶點(diǎn)故事閱讀 39,841評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡铝侵,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出触徐,到底是詐尸還是另有隱情咪鲜,我是刑警寧澤,帶...
    沈念sama閱讀 35,569評(píng)論 5 345
  • 正文 年R本政府宣布撞鹉,位于F島的核電站疟丙,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏鸟雏。R本人自食惡果不足惜享郊,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,168評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望孝鹊。 院中可真熱鬧炊琉,春花似錦、人聲如沸又活。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)柳骄。三九已至团赏,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間夹界,已是汗流浹背馆里。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留可柿,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,962評(píng)論 2 370
  • 正文 我出身青樓丙者,卻偏偏與公主長(zhǎng)得像复斥,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子械媒,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,781評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容