加深对vue响应式的理解(上)

274 阅读6分钟
面试官的一番话让我印象很深,”如果你会忘记说不你并不真正理解,你要记住,你需要知道为什么需要“,我觉得还是非常有意义的话,至少对于我,促使我去思考为什么。

什么是响应式?

今天的主题是Vue响应式,那么什么是响应式呢?

有一个简单的例子,在我们编写原生html代码时,我们都会下载一个Live Server插件,在编程中,响应式系统能够自动"感知"数据的变化,并及时更新与之相关的内容,无需手动干预,响应式系统会自动更新用户界面,确保显示的永远是最新的数据状态。

为什么需要使用响应式? 简而言之,言而简之,当数据发生变化时,我们需要及时的去更新页面,也就是数据驱动视图。 响应式的设计让数据的变化能够流畅地驱动页面的更新,为用户提供即时且无缝衔接的交互体验。

3. 实现一个简单的响应式系统

让我们从头开始,逐步构建一个简单的响应式系统。我们想想,如果我们需要实现一个响应式系统,我们需要什么?

首先,我需要一个工具,能够感知或捕获到数据变化,如果我们去寻求答案,更多的推荐可能会是Proxy 和 Object.defineProperty,那么我们就来看看怎么个事儿。

文档在此,有空可以看看Object.definePropertyProxy

通常,我们习惯地称为数据劫持数据代理,它俩也正分别是Vue2.X和Vue3.X的核心。 我们就先来看看使用数据劫持如何来实现响应式的。

3.1 Vue 2 的实现

简单讲讲,Object.defineProperty接受三个参数

obj 要定义属性的对象。

prop 一个字符串或 Symbol,指定了要定义或修改的属性键。

descriptor 要定义或修改的属性的描述符。

喔~第一个参数是对象,也难怪vue2中的data会是一个对象呢, 那么现在我们可以尝试去“劫持”一个对象了:

const obj = {
    name: 'kailin'
}
//let value = obj.name 
Object.defineProperty(obj, 'name', {
    get() {
        console.log(`you are getting name`);
        return obj.name
        // return value
    },
    set(newVal) {
        if(newVal !== value){
            console.log(`you are setting name to ${newVal}`);
            obj.name = newVal
            //value = newVal 
        }
        
    }
}) 
obj.name = 'kailin'//you are setting name to kailin
console.log(obj.name);//RangeError: Maximum call stack size exceeded

不会你也是这样想的吧,那么恭喜你,踩到第一个坑了。仔细想想,在get中,我们return和赋值的时候,是不是还需要读取一遍obj.name呢,这样就实现了一个自调用(死循环),自然要爆栈了。所以,我们可以在外面保存一下obj.name的值。

接下来,又有一个问题,data既然是一个对象,data所有的key都需要被劫持,所以,可以创建一个方法将所有的key进行数据劫持。

const observer = (obj) => {
    walk (obj)
}
const walk = (obj) =>{
    Object.keys(obj).forEach(key => defineReactive(obj, key, obj[key]))
}
const defineReactive = (obj, key, value) => {
    Object.defineProperty(obj, key, {
       //...
    })
}

那么现在,我已经有了一个方法observer能够对一个对象中的所有属性进行劫持了,那么,真的是这样吗? 我们可以试验一下:

const data = {
    person: {
        weight:140 ,
        tall: 180
    },
    hobby: 'coding',
    county: 'china',
    schools: ['pku','tsinghua']
}

我发现了几个问题:

  • 这套代码只能劫持一层对象或数组
  • 无法感知数组的方法(如push)或通过索引修改数组
  • 无法劫持新增的属性
  • delete删除属性无法被监听

我们先来解决简单的问题,劫持多层对象或多维数组: 在walk()方法中,可以进行判断是否是对象

const walk = (val) =>{
    if (Array.isArray(val)) {
        // 如果是数组,则调用 observeArray 递归处理
        observeArray(val);
    } else if (typeof val === 'object' && val !== null) {
        // 如果是对象,则递归处理对象的属性
        Object.keys(val).forEach(key => {
            defineReactive(val, key, val[key]); 
            observer(val[key]); 
        });
    }
   }

通过这样的方法,我们就可以对多层的对象或数组进行劫持了。那么对数组的操作呢? 在vue2中,尤大想了这样一个方法,重写了数组上的七种方法['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'],当然,并不是我们所想的那样。我们都知道,数组的方法都是挂载在Array的原型prototype上的,所以他想了一个办法:

  • 保存原始数组的原型,并且创建一个新的原型对象,让它继承自数组原型,这样并不是多此一举,而是为了不污染全局的Array.toprotype
  • 遍历需要劫持的七种方法,重写这些方法,同时保留原始功能,将原始的方法执行但是this指向新的原型对象(是你的话callapplybind你会使用哪一个呢?),所以这是“借鸡生蛋”?
  • 当使用 pushunshiftsplice时,将传入的参数继续劫持一遍,这样就能够检测到数组的变化了。

传送门:好奇就点点我吧!

那么对于上面的过程,我们思考几个问题:

  • 数组身上的方法那么多,为什么仅仅要劫持这七个方法呢?

    • 这七个方法会直接对数组的结构(长度或内容)产生变化。
    • Vue 的响应式依赖追踪需要在这些操作发生时通知视图更新
  • 为什么只对push、unshift、splice三个方法的参数进行劫持呢?

    • 因为只有这三种方法有手段新增数据,对于删除的数据,就没有必要去监管了

那么,我们还剩下几个问题

  1. 无法感知通过索引修改数组和 delete删除对象属性
  2. 直接修改数组的length属性
  3. 无法监听对象的新增属性

其实,这也是Vue2的缺陷,当然,我们也可以使用别的方法来达到目的

操作方式原因解决方案示例代码
通过索引修改数组Object.defineProperty 无法拦截数组索引操作使用 Vue.setsplice 替代Vue.set(arr, 3, 99)
向对象添加新的属性属性新增时未定义 gettersetter使用 Vue.setVue.set(obj, 'b', 2)
删除对象的属性delete 不会触发依赖更新使用 Vue.deleteVue.delete(obj, 'a')

当然,到现在,我们也只完成了第一步,当然也是最核心的一步,除了对数据的监听,我们还需要及时地依赖(副作用)收集和触发、更新视图,如何去实现这一功能呢?。 在此处就不深入探讨了,大家可以看看站内相关的优秀文章和官方源码

下文:Vue3的响应式