Vue.js3.0 响应式系统原理

Vue.js响应式原理回顾
  • Proxy对象实现属性监听
  • 多层属性嵌套,在访问属性过程中处理下一级属性
  • 默认监听动态添加的属性
  • 默认监听属性的删除操作
  • 默认监听数组索引和 length属性
  • 可以作为单独的模块使用
核心方法
  • reactive/ref/toRefs/computed
  • effect watch/watchEffect是vue3 runtime.core中实现的,内部使用effect底层函数
  • track 收集依赖
  • trigger 触发更新
响应式系统原理——Proxy

ProxyReflect是ES6 为了操作对象而提供的新 API

proxy中有两个需要注意的地方:

  • set 和 deleteProperty 中需要返回布尔类型的值

    <script>
          'use strict'
          // set 和 deleteProperty 中需要返回布尔类型的值
          // 在严格模式下,如果返回 false 的话会出现 Type Error 的异常
          const target = {
            foo: 'xxx',
            bar: 'yyy'
          }
          // Reflect.getPrototypeOf()相当于Object.getPrototypeOf()
          const proxy = new Proxy(target, {
            // receiver代表当前的的Proxy对象或者继承Proxy的对象
            get (target, key, receiver) {
              // return target[key]
              // Reflect反射,代码运行期间获取对象中的成员
              return Reflect.get(target, key, receiver)
            },
            set (target, key, value, receiver) {
              // target[key] = value
              // Reflect.set设置成功返回true 设置失败返回false
              return Reflect.set(target, key, value, receiver)
            },
            deleteProperty (target, key) {
              // delete target[key]
              return Reflect.deleteProperty(target, key)
            }
          })
    
          proxy.foo = 'zzz'
          // delete proxy.foo
    </script>
    

    如果set和deleteProperty返回false时,页面会报错

    image-20210414080553080.png
  • Proxy 和 Reflect 中使用的 receiver指向

    // Proxy 中 receiver:Proxy 或者继承 Proxy 的对象
    // Reflect 中 receiver:如果 target 对象中设置了 getter,getter 中的 this 指向 receiver
    
    const obj = {
        get foo() {
            console.log(this)
            return this.bar
        },
    }
    
    const proxy = new Proxy(obj, {
        get(target, key, receiver) {
            if (key === 'bar') {
                return 'value - bar'
            }
            return Reflect.get(target, key, receiver)
        },
    })
    console.log(proxy.foo)
    

    不传递receiver时,可以看到this返回的是obj对象,proxy.foo返回undefined

    image-20210414080743227.png

    当传递了receiver时,this指向Proxy对象

    image-20210414080825068.png
响应式系统原理——reactive
  • 接收一个参数,判断这参数是否是对象,不是直接返回,只能转换对象为响应式对象

  • 创建拦截器对象handler,设置get/set/deleteProperty

  • 返回Proxy 对象

    // reactivily/index.js
    const isObject = (val) => val !== null && typeof val === 'object'
    export function reactive(target) {
      if (!isObject(target)) return
    
      const handler = {
        get(target, key, receiver) {
          console.log('get', key, target)
        },
        set(target, key, value, receiver) {
          console.log('set', key, value)
          return value
        },
        deleteProperty(target, key) {
          console.log('delete', key)
          return target
        },
      }
    
      return new Proxy(target, handler)
    }
    

    测试set和delete,结果如下

    image-20210414082410979.png

reactive实现思路:

  1. 定义handler对象,用于Proxy的第二个参数(拦截器对象)
  2. get方法实现
    • 收集依赖
    • 返回target中对于key的value
    • 如果value为对象,需要再次转为响应式对象
  3. set方法中实现
    • 获取key属性的值,判断新旧值是否相同,相同时返回true
    • 不同时,先将target中的key对应的value修改为新值
    • 最后触发更新
  4. deleteProperty方法实现
    • 首先判断target本身是否存在key
    • 删除target中的key,并返回成功或失败
    • 删除成功,触发更新

代码示例:

const isObject = (val) => val !== null && typeof val === 'object'
const convert = (val) => (isObject(val) ? reactive(val) : val)
const hasOwnProperty = Object.prototype.hasOwnProperty
const hasOwn = (target, key) => hasOwnProperty.call(target, key)

export function reactive(target) {
  if (!isObject(target)) return

  const handler = {
    get(target, key, receiver) {
      // 收集依赖
      const value = Reflect.get(target, key, receiver)
      return convert(value)
    },
    set(target, key, value, receiver) {
      const oldValue = Reflect.get(target, key, receiver)
      let result = true
      if (oldValue !== value) {
        let result = Reflect.set(target, key, value, receiver)
        // 触发更新
      }
      return result
    },
    deleteProperty(target, key) {
      const hasKey = hasOwn(target, key)
      const result = Reflect.deleteProperty(target, key)
      if (hasKey && result) {
        // 触发更新
      }
      return result
    },
  }

  return new Proxy(target, handler)
}

测试,创建html文件进行测试:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script type="module">
    import { reactive } from './reactivity/index.js'
    const obj = reactive({
      name: 'zs',
      age: 18
    })
    obj.name = 'lisi'
    delete obj.age
    console.log(obj)
  </script>
</body>
</html>
响应式系统原理——收集依赖
image-20210415080133182.png

image-20210414082624298.png
  • 依赖收集过程中会创建3个集合,分别是targetMap、depsMap和dep
  • targetMap作用是记录目标对象和一个字典(depsMap),使用WeakMap弱引用,当目标对象失去引用之后,可以销毁
  • targetMap的值是depsMap,depsMap的key是目标对象的属性名称,值是一个set集合dep
  • dep中存储的是effect函数,因为可以多次调用一个effect,在effect中访问同一个属性,这时该属性会收集多次依赖,对应多个effect函数
  • 通过这种结构,可以存储目标对象,目标对象属性,以及属性对应的effect函数
  • 一个属性可能对应多个函数,当触发更新时,在这个结构中根据目标对象属性找到effect函数然后执行
  • 收集依赖的track函数内部,首先根据当前targetMap对象找到depsMap,如果没找到要给当前对象创建一个depsMap,并添加到targetMap中,如果找到了再根据当前使用的属性在depsMap找到对应的dep,dep中存储的是effect函数,如果没有找到时,为当前属性创建对应的dep集合,并且存储到depsMap中,如果找到当前属性对应的dep集合,就把当前的effect函数存储到集合中

effect方法实现

实现思路:

  1. effect接收函数作为参数
  2. 执行函数并返回响应式对象去收集依赖,收集依赖过程中将callback存储起来,需要在后面的track函数中能够访问到这里的callback
  3. 依赖收集完毕设置activeEffect为null

代码实现:

let activeEffect = null
export function effect (callback) {
  activeEffect = callback
  callback() // 访问响应式对象属性,去收集依赖
  activeEffect = null
}

track方法实现

实现思路:

  1. track接收两个参数,目标对象target和需要跟踪的属性key
  2. 内部需要将target存储到targetMap中,targetMap定义在外面,除了track使用外,trigger函数也要使用
  3. activeEffect不存在直接返回,否则需要在targetMap中根据当前target找depsMap
  4. 判断是否找到depsMap,因为target可能还没有收集依赖
  5. 未找到,为当前target创建depsMap去存储对应的键和dep对象,并添加到targetMap中
  6. 根据属性查找对应的dep对象,dep是个集合,存储effect函数
  7. 判断是否存在,未找到时创建新的dep集合并添加到depsMap中
  8. 将effect函数添加到dep集合中
  9. 在收集依赖的get中调用这个函数

代码实现:

let targetMap = new WeakMap()
export function track(target, key) {
  if (!activeEffect) 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()))
  }
  dep.add(activeEffect)
}

此时,整个依赖收集过程已经完成

trigger方法实现

依赖收集完成后需要触发更新

实现思路:

  1. 参数target和key
  2. 根据target在targetMap中找到depsMap
  3. 未找到时,直接返回
  4. 再根据key找对应的dep集合,effect函数
  5. 如果dep有值,遍历dep集合执行每一个effect函数
  6. 在set和deleteProperty中触发更新

代码实现:

export function trigger(target, key) {
  const depsMap = targetMap.get(target)
  if (!depsMap) return
  const dep = depsMap.get(key)
  if (dep) {
    dep.forEach((effect) => {
      effect()
    })
  }
}

依赖收集和触发更新代码完成,创建html文件进行测试

<body>
  <script type="module">
    import { reactive, effect } from './reactivity/index.js'

    const product = reactive({
      name: 'iPhone',
      price: 5000,
      count: 3
    })
    let total = 0 
    effect(() => {
      total = product.price * product.count
    })
    console.log(total)

    product.price = 4000
    console.log(total)

    product.count = 1
    console.log(total)

  </script>
</body>

打开浏览器控制台,可以看到输出结果如下

image-20210416084313137.png
响应式系统原理——ref

ref vs reactive

  • ref可以把基本数据类型数据,转成响应式对象
  • ref返回的对象,重新赋值成对象也是响应式的
  • reactive返回的对象,重新赋值丢失响应式
  • reactive返回的对象不可以解构

实现原理:

  1. 判断 raw 是否是ref 创建的对象,如果是的话直接返回
  2. 判断 raw是否是对象,如果是对象调用reactive创建响应式对象,否则返回原始值
  3. 创建ref对象并返回,标识是否是ref对象,这个对象只有value属性,并且这个value属性具有set和get
  4. get中调用track收集依赖,收集依赖的对象是刚创建的r对象,属性是value,也就是当访问对象中的值,返回的是内部的变量value
  5. set中判断新旧值是否相等,不相等时将新值存储到raw中,并调用convert处理raw,最终把结果存储到value中,如果给value重新赋值为一个对象依然是响应式的,当raw是对象时,convert里调用reactive转换为响应式对象
  6. 最后触发更新

代码实现:

export function ref(raw) {
  // 判断 raw 是否是ref 创建的对象,如果是的话直接返回
  if (isObject(raw) && raw.__v_isRef) {
    return
  }
  let value = convert(raw)
  const r = {
    __v_isRef: true,
    get value() {
      track(r, 'value')
      return value
    },
    set value(newValue) {
      if (newValue !== value) {
        raw = newValue
        value = convert(raw)
        trigger(r, 'value')
      }
    },
  }
  return r
}

创建html文件进行测试:

<body>
  <script type="module">
    import { reactive, effect, ref } from './reactivity/index.js'

    const price = ref(5000)
    const count = ref(3)
   
    let total = 0 
    effect(() => {
      total = price.value * count.value
    })
    console.log(total)

    price.value = 4000
    console.log(total)

    count.value = 1
    console.log(total)

  </script>
</body>

打开控制台可以看到输出结果和上面的相同

响应式系统原理——toRefs

实现思路:

  1. 接收参数proxy,判断参数是否为reactive创建的对象,如果不是发出警告
  2. 判断传入参数,如果是数组创建长度是length的数组,否则返回空对象,因为传入的proxy可能是响应式数组或响应式对象
  3. 接着遍历proxy对象的所有属性,如果是数组遍历索引,将每一个属性都转换为类似ref返回的对象
  4. 创建toProxyRef函数,接收proxy和key,创建对象并最终返回对象(类似ref返回的对象)
  5. 创建标识属性__v_isRef,这里的get中不需要收集依赖,因为这里访问的是响应式对象,当访问属性时,内部的getter回去收集依赖,set不需要触发更新,调用代理对象内部的set触发更新
  6. 调用toProxyRef,将所有属性转换并存储到ret中
  7. toRefs将reactive返回的对象的所有属性都转换成一个对象,所以当对响应式对象进行解构的时候,解构出的每一个属性都是对象,而对象是引用传递,所以解构的属性依然是响应式的

代码实现:

export function toRefs(proxy) {
  const ret = proxy instanceof Array ? new Array(proxy.length) : {}

  for (const key in proxy) {
    ret[key] = toProxyRef(proxy, key)
  }

  return ret
}

function toProxyRef(proxy, key) {
  const r = {
    __v_isRef: true,
    get value() {
      return proxy[key]
    },
    set value(newValue) {
      proxy[key] = newValue
    },
  }
  return r
}

创建html进行测试:

<body>
  <script type="module">
    import { reactive, effect, toRefs } from './reactivity/index.js'

    function useProduct () {
      const product = reactive({
        name: 'iPhone',
        price: 5000,
        count: 3
      })
      
      return toRefs(product)
    }

    const { price, count } = useProduct()


    let total = 0 
    effect(() => {
      total = price.value * count.value
    })
    console.log(total)

    price.value = 4000
    console.log(total)

    count.value = 1
    console.log(total)

  </script>
</body>

打开控制台可以看到输出结果和上面的相同

响应式系统原理——computed

实现原理:

  1. 接收一个有返回值的函数作为参数,函数的返回值就是计算属性的值
  2. 监听这个函数内部的响应式数据变化,最后将函数执行结果返回
  3. computed内部会通过effect监听getter内部的响应式数据变化,因为在effect中执行getter访问响应式数据的getter会去收集依赖,当数据变化后,回去重新执行effect函数将getter结果在存储到result中

代码实现:

export function computed(getter) {
  const result = ref()

  effect(() => (result.value = getter()))

  return result
}

创建html文件进行测试:

<body>
  <script type="module">
    import { reactive, effect, computed } from './reactivity/index.js'

    const product = reactive({
      name: 'iPhone',
      price: 5000,
      count: 3
    })
    let total = computed(() => {
      return product.price * product.count
    })
   
    console.log(total.value)

    product.price = 4000
    console.log(total.value)

    product.count = 1
    console.log(total.value)

  </script>
</body>

打开控制台可以看到输出结果和上面的相同

github地址

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

推荐阅读更多精彩内容