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>

这样我们就实现了一个页面数据跟随数据变化而刷新的案例。这也是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)

🐯 数组的情况

如果是数组,是需要写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)
    }
  })
})

对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])
  })
}

🌷特殊情况

你一定遇到过,如果在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)

🍂 简版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
      }
    })
  })
}

测试代码:

<!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">{{ counter }}</div>
</body>
<script src="./index.js"></script>
<script>
    const app = new MVue({
        el: '#app',
        data: {
            counter: 1
        }
    })
    setInterval(() => {
        app.counter++
    }, 1000)
</script>

</html>

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值