vue3响应式数据原理

Effect 原理解析 与 实现

引言:

vue、react 框架的核心都是数据驱动视图也就是model => view,实现的核心也就是 数据响应。

主要就三步:

  1. 创建响应式的数据 defineProperty、pxoxy。这样使用、修改数据的事件我们都能捕捉到

  2. 在使用响应式的数据时收集依赖,把跟该数据相关的副作用都储存起来

  3. 在修改响应式的数据时触发依赖,执行相关的副作用

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

一、effect:副作用函数

1.类似于vue2.0中watch 的升级版,如果函数中用到的响应式的数据发生了变化,则会执行该函数

// Effect 的简单应用
const component = defineComponent({
            name: 'zhenAPP',
            template: `
                <div>
                    <button @click="addHandler">add</button>
                </div>
            `,
            setup(props) {
                const data = reactive({
                    count: 0,
                });
                console.log('-----------创建reactive------------')
                console.log('创建reactive对象:', data)
                const addHandler = () => {
                    data.count++;
                };
                effect(() => {
                    console.log('1',data.count)
                });
                return {
                    addHandler,
                };
            },
        });


// 在 Vue.js 3.0 中,初始化一个应用的方式如下
import { createApp } from 'vue'
import App from './app'
const app = createApp(App)
app.mount('#app')
// 设置并运行带副作用的渲染函数
setupRenderEffect(instance, initialVNode, container, anchor, parentSuspense ...)

二、proxy 与reflect

Object.defineProperty API 的一些缺点:

1. 不能监听对象属性新增和删除;
2. 初始化阶段递归执行 Object.defineProperty 带来的性能负担。

const p = new Proxy(target, handlerObject)

handlerObject = {
  get(target,key,receiver){ // receiver 可以理解成改变get函数中的this指向,默认就是handlerObject
    
    },
  set(target,key,value,receiver){
    
  }
}

vue3源码的调试方法:

  • 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('创建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>

三、响应式api reactive的实现

// 0 reactactive模块的存储变量:reactiveMap
//  作用1:维护整个应用数据代理,防止对同一个对象重复代理,比如父子组件共享某个响应数据
//  作用2:weakmap 本身的优势,这样某个组件卸载之后,组件上的响应数据也会被删除,reactiveMap也会删除响应数据。防止内存泄露
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生成响应式对象 
  const proxy = new Proxy(
    target,
    mutableHandlers
  )
  // 存入map
  proxyMap.set(target, proxy)
  return proxy
}

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

    const res = Reflect.get(target, key, receiver)
        //重点: track 实现依赖收集。effect 中接下来会分析
    track(target, TrackOpTypes.GET, key)
    
    if (isObject(res)) {
      // 深度代理 
      return reactive(res)
    }

    return res
  }
}
// 5. set 函数的代理:调用了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本身是响应对象,把他变成普通对象
      // 对应get中 isObject(res),方便统一处理
      // 这也是vue3与vue2 不同的地方
      // { person: {name:'tom'} }
      // vue2在代理的时候,两层都会代理
      // vue3在代理的时候,只代理第一层,在使用到person的时候才会代理第二层
      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)) {
        // 重点:在改变响应对象的值的时候,调用trigger 触发响应
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
    }
    return result
  }
}

三、Effect的依赖收集与响应触发 (分-总-分-问题)

// 1. Effct 函数定义 与 局部变量缓存
export interface ReactiveEffect<T = any> {
  (): T
  _isEffect: true
  id: number
  active: boolean // active是effect激活的开关,打开会收集依赖,关闭会导致收集依赖无效
  raw: () => T // 原始监听函数
  deps: Array<Dep> // 存储依赖的deps
  options: ReactiveEffectOptions
  allowRecurse: boolean
}
type Dep = Set<ReactiveEffect>
type KeyToDepMap = Map<any, Dep>


// 应用中 响应对象对应的 KeyToDepMap
const targetMap = new WeakMap<any, KeyToDepMap>()
// 当前执行的effect
let activeEffect: ReactiveEffect | undefined
// 执行中的effect栈
const effectStack: ReactiveEffect[] = []


// 2. Effct 
export function effect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
  // fn已经是一个effect函数了,利用fn.raw重新创建effect
  if (isEffect(fn)) {
    fn = fn.raw
  }
  // 创建监听函数
  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++}),造成循环引用
    if (!effectStack.includes(effect)) {
      cleanup(effect) // effect.deps = []
      try {
        effectStack.push(effect)
        activeEffect = effect
        // 调用原始函数时,如果响应式数据取值了
        // 会触发这个响应式对象的getter,getter里面就会调用track方法收集依赖
        return fn() 
        
      } finally {
        effectStack.pop()
        // 指向最后一个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 函数:收集依赖
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函数:触发依赖
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
  }
  // 确定需要触发的依赖 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) => {
    // 如果传入自定义调度器则执行自定义的,可以扩展effect执行
    if (effect.options.scheduler) {
      effect.options.scheduler(effect)
    } else {
      effect()
    }
  }

  effects.forEach(run)
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,635评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,628评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,971评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,986评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,006评论 6 394
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,784评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,475评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,364评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,860评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,008评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,152评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,829评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,490评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,035评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,156评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,428评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,127评论 2 356

推荐阅读更多精彩内容