• Vue源码之数据响应式


    写在前面

    阅读本文需要先了解Object.defineProperty知识,不了解的移步咱之前的文章:Vue源码探索之知识小储备 ——01.Object.defineProperty VS proxy

    🌰 先看一段小例子

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
    </head>
    
    <body>
        <div id="app"></div>
    </body>
    <script>
        function defineReactive(obj, key, val) {
            Object.defineProperty(obj, key, {
                get() {
                    console.log('get', key)
                    return val
                },
                set(newVal) {
                    if (val !== newVal) {
                        val = newVal
                        update()
                    }
                }
            })
        }
        const obj = {}
        defineReactive(obj, 'foo', new Date().toLocaleString())
    
        setInterval(() => {
            obj.foo = new Date().toLocaleString()
        }, 1000)
    
        function update() {
            document.getElementById('app').innerHTML = obj.foo
        }
    </script>
    
    </html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    这样我们就实现了一个页面数据跟随数据变化而刷新的案例。这也是vue实现响应式的核心原理。但是往往我们开发中的对象没有这么纯粹,大多多级嵌套对象的情况,也有数组的情况。所以下面我们来做更深层级的解析。

    🍀 基本实现

    核心思想:递归遍历传入obj,定义每个属性的拦截

    // val是形参 但是却可在外部访问 --闭包
    function defineReactive (obj, key, val) {
      // 递归遍历
      observe(obj[key])
      Object.defineProperty(obj, key, {
        get () {
          return val
        },
        set (newVal) {
          if (val !== newVal) {
          	// 如果newVal是对象,也要对其进行社会主义教育
            observe(newVal)
            update()
            val = newVal
          }
        }
      })
    }
    
    function observe (obj) {
      if (typeof obj !== 'object' || obj === null) {
        return
      }
      Object.keys(obj).forEach(key => {
      	// 对obj每个key执行拦截
        defineReactive(obj, key, obj[key])
      })
    }
    
    function update() {
    	console.log('这里数据发生了改变,进行视图更新')
    }
    
    const obj = {
      a: '120',
      b: '555',
      c: {
        d: '1231'
      }
    }
    
    observe(obj)
    // 普通更新
    obj.a = 5
    console.log(obj.a)
    // 嵌套属性更新
    obj.c.d = 'kjk1'
    console.log(obj.c.d)
    // 赋值是对象时
    obj.c.d = {k: '卡卡'}
    obj.c.d.k = 'test'
    console.log(obj.c.d.k)
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53

    🐯 数组的情况

    如果是数组,是需要写hack方法的,重新定义数组的七个方法:

    const oldArrayProperty = Array.prototype
    const arrayProto = Object.create(oldArrayProperty)
    
    //从数组原型中获取这7个方法,并覆盖为可以发送更新通知的函数实现
    const methodsToPatch = ['push', 'pop', 'unshift', 'shift', 'sort', 'splice', 'reverse']
    methodsToPatch.forEach(methodName => {
      console.log(oldArrayProperty[methodName])
      Object.defineProperty(arrayProto, methodName, {
        value () {
          console.log('you change the array')
          update()
          oldArrayProperty[methodName].apply(this, arguments)
        }
      })
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    对observe中的数组的情况则替换数组的原型

    function observe (obj) {
      if (typeof obj !== 'object' || obj === null) {
        return
      }
      if (Array.isArray(obj)) {
        Object.setPrototypeOf(obj, arrayProto)
        return
      }
      Object.keys(obj).forEach(key => {
      	// 对obj每个key执行拦截
        defineReactive(obj, key, obj[key])
      })
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    🌷特殊情况

    你一定遇到过,如果在vue的data中没有定义某一个属性直接给其通过this.data.XX=XXX进行赋值时,当这个属性发生改变时,页面并不会更新。这是由于 Vue 会在初始化实例时进行双向数据绑定,使用Object.defineProperty()对属性遍历添加 getter/setter 方法,所以属性必须在 data 对象上存在时才能进行上述过程 ,这样才能让它是响应的。如果要给对象添加新的属性,此时新属性没有进行过上述过程,不是响应式的,所以会出想数据变化,页面不变的情况。此时Vue官方给提供了$set这个API。

    我们这里也给实现下,其实非常简单:

    function $set (obj, key, val) {
      defineReactive(obj, key, val)
    }
    
    set(obj, 'ff', '的活动')
    obj.ff = 'ddpm'
    console.log(obj.ff)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    🍂 简版vue之数据响应式

    结合以上的实践,对比源码的函数拆分及命名整理出一份简版源码如下:

    
    // 给一个obj定义一个响应式的属性
    function defineReactive (obj, key, val) {
      // 递归
      // val如果是个对象,就需要递归处理
      observe(val)
    
      Object.defineProperty(obj, key, {
        get () {
          return val
        },
        set (newVal) {
          if (newVal !== val) {
            console.log('set', key, val)
            val = newVal
            // 新值如果是对象,仍然需要递归遍历处理
            observe(newVal)
          }
        }
      })
    }
    
    class MVue {
      constructor (options) {
        // 保存options
        this.$options = options
        this.$data = options.data
    
        // 将data进行响应式处理
        observe(this.$data)
    
        // 代理
        proxy(this)
      }
    }
    
    // 遍历响应式处理
    function observe (obj) {
      if (typeof obj !== 'object' || obj == null) {
        return obj
      }
    
      new Observer(obj)
    }
    
    const oldArrayProperty = Array.prototype
    const arrayProto = Object.create(oldArrayProperty)
    
    const methodsToPatch = ['push', 'pop', 'unshift', 'shift', 'sort', 'splice', 'reverse']
    methodsToPatch.forEach(methodName => {
      Object.defineProperty(arrayProto, methodName, {
        value () {
          console.log('you change the array')
          oldArrayProperty[methodName].apply(this, arguments)
        }
      })
    })
    
    class Observer {
      constructor (obj) {
        // 判断传入obj类型,做相应处理
        if (Array.isArray(obj)) {
          Object.setPrototypeOf(obj, arrayProto)
        } else {
          this.walk(obj)
        }
      }
    
      walk (obj) {
        Object.keys(obj).forEach((key) => defineReactive(obj, key, obj[key]))
      }
    }
    
    // 能够将传入对象中的所有key代理到指定对象上
    function proxy (vm) {
      Object.keys(vm.$data).forEach((key) => {
        Object.defineProperty(vm, key, {
          get () {
            return vm.$data[key]
          },
          set (v) {
            vm.$data[key] = v
          }
        })
      })
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87

    测试代码:

    DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Documenttitle>
    head>
    
    <body>
        <div id="app">{{ counter }}div>
    body>
    <script src="./index.js">script>
    <script>
        const app = new MVue({
            el: '#app',
            data: {
                counter: 1
            }
        })
        setInterval(() => {
            app.counter++
        }, 1000)
    script>
    
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    打开控制台,就可以看到counter的变化都被监听到了~

  • 相关阅读:
    Leetcode Practice -- 数组
    再谈 FireBird 的自增字段用 FireDAC 来处理
    Redis缓存面临的缓存击穿问题
    做强礼品经济韧性与活力,金秋10月第30届深圳礼品展来袭!
    C++:类与对象(2)
    基于RFID技术的烟草供应链数字化管理方案
    Typescript 综合笔记:解读一个github中的React 网页
    rerank来提升RAG的准确度的策略
    Logger.error还不知道怎么传参打印?看完这个你就明白了
    使用Postman调试API遇到“400 Bad Request”问题
  • 原文地址:https://blog.csdn.net/weixin_43867717/article/details/126661814