Vue响应式原理

Reactive-in-Depth.png

Vue数据劫持的实现,做一个自己的理解&简单总结。虽然Vue3.0即将到来,我想Vue2.x也不至于马上过时。

今天就从Vue2.x 与 Vue.3.0 数据劫持如何实现数据双向绑定。

数据劫持: 指的是在访问或者修改对象的某个属性时,通过一段代码拦截这个行为,进行额外的操作或者修改返回结果。

Vue2.x 选择的 Object.defineProperty

Object.defineProperty 对大家都来说应该不陌生了。算是面试的一道必考题?(细品:那掌握好了是不是就是一道送分题呢?)可以点击这里回顾一下 Object.defineProperty的文档

我们来认清Object.defineProperty的几个局限性

  • 兼容性是IE8+,这也就是为什么Vue不支持IE8及以下版本的原因
  • 不能监听数组的变化,Vue通过重写数组原型的方法来实现数据劫持。
  • 对于深层次嵌套对象需要做递归遍历。
  • 必须遍历对象的每个属性。如果要扩展该对象,就必须手动去为新的属性设置setter、getter方法。 这也就是为什么Vue开发中的不在 data 中声明的属性无法自动拥有双向绑定效果的原因。需要我们手动去调用Vue.set()

我们做个类似Vue简易的数据劫持

  1. 视图更新触发的函数
// 当我们监听的数据发生变化后调用改函数
function update() {
    console.log('数据变化啦,更新视图')
}

  1. 通过 Object.defineProperty 处理 data 中的每个属性
// 通过 Object.defineProperty 处理 target 中的每个属性 key
function defineReactive(target, key, value) {
    Object.defineProperty(target, key, {
        get() {
            return value;
        },
        set(val) {
            // 如果改变的数据和原来一样将不做任何处理
            if (val !== value) {
                 // 数据更新了,调用update
                 update();
                 value = val;
            } 
        }
    })
}
  1. 监听data的函数
function observer(target) {
    // 如果不是对象,直接返回;如果是null也直接返回
    if (typeof target !== 'object' || !target) return target;
    
    // 遍历对象obj的所有key,完成属性配置
    Object.keys(target).forEach(key => defineReactive(target, key, target[key]))
}
  1. 测试步骤1、2、3
// 需要监听的data对象
const data = {
    level: 1,
    info: {
        name: 'cc'
    }
}

// 调用监听函数监听 data
observer(data)

// 修改data的值 视图更新
data.level = 2

// 看到视图确实更新了

// 我们不妨尝试了一下data深层次对象的修改
data.info.name = 'yy'

// 控制台什么都是没有

  1. 想必你也发现了,监听data只到了对象的第一层。data深层次的数据,并没有被监听。所以我们需要对data做一个逐层遍历(递归),直到把每个对象的每个属性都调用 Object.defineProperty() 为止。
// 改改步骤二的代码
function defineReactive(target, key, value) {
    // 在这里新增代码
    // 当value为object我们再做一次数据监听,直到value不是object为止
    if (typeof value === 'object') {
        observer(value)
    }
    
    // 以下代码和步骤2没有区别
    Object.defineProperty(target, key, {
        get() {
            return value;
        },
        set(val) {
            // 如果改变的数据和原来一样将不做任何处理
            if (val !== value) {
                 // 数据更新了,调用update
                 update();
                 value = val;
            } 
        }
    })
}
  1. 再对步骤5的修改做一次测试
const data = {
    level: 1,
    info: {
        name: 'cc'
    },
    a: {
        a: {
            a: {
                a: 1
            }
        }
    }
}

// 我们尝试改变data.info.name的值
data.info.name = 'xy'  // 视图更新了!

// 我们尝试跟深层次的修改
data.a.a.a.a = 2  // ok 视图也更新了

// 那么我再试试其他方式
// 先修改data.info的值
data.info = { name: 'cc' } // 没毛病,视图更新了,但此时data.info的指向已经发生了变化
// 然后再修改data.info.name
data.info.name = 'xy' // emmmmmm... 又是什么都没有
  1. 我们针对步骤5再做一次修改
// 修改步骤5的代码
function defineReactive(target, key, value) {
    if (typeof value === 'object') {
        observer(value)
    }
    Object.defineProperty(target, key, {
        get() {
            return value;
        },
        set(newVal) {
            // 如果改变的数据和原来一样将不做任何处理
            if (newVal !== value) {
                // 在这里新增代码
                // 如果设置newVal是object,对newVal做监听
                if (typeof newVal === 'object') {
                    observer(newVal)
                }
                 // 数据更新了,调用update
                 update();
                 value = newVal;
            } 
        }
    })
}
  1. 再对步骤7的修改做一次测试
const data = {
    level: 1,
    info: {
        name: 'cc'
    }
}

// 先修改data.info的值
data.info = { name: 'cc' } // 没毛病,视图更新了
// 然后再修改data.info.name
data.info.name = 'xy' // 也没毛病,视图更新了
  1. 我们都知道typeof 数据返回的也是object
const data = {
    arr: []
}

// 尝试对数组做更改
arr.push(1); // 然鹅,并没有任何输出
  1. 前面有说明Object.defineProperty 对数组是起不到任何作用的。那Vue如何实现的呢? Vue是通过修改数组的原型方法来实现数据劫持(做一些视图更新、渲染的操作)。
const methods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']

// 遍历methods数组
methods.forEach(method => {
    // 原生Array的原型方法
    const originalArray = Array.prototype[method]
    
    // 重写Array原型上对应的方式
    Array.prototype[method] = function() {
        // 做视图更新或者渲染操作
        update();
        
        // 视图更新了,调用对应的原生方法
        // arguments 将该有的参数也传进来
        originalArray.call(this, ...arguments);
    }
})
  1. 又到了验证一下步骤10的时候啦!
const data = {
    arr: []
}

data.arr.push(1) // 视图更新了
  1. 看了上面的代码,可能就有疑问了。我们明显直接修改的是 Array.prototype的方法。这样会导致一个问题。没有被监听的数组,也会触发update()。如下:
var normalArray = [];

normalArray.push(1); // wtf 竟然也触发了视图更新

结果明显不是我们想要的。我们希望的是:Array原有的方法保持不变,但是又要引用到原来的方法的实现。

我们可以简单地处理下啦。

①先修改步骤10的代码

const methods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
const arrayList = [] 

// 遍历methods数组
methods.forEach(method => {
    // 原生Array的原型方法
    const originalArray = Array.prototype[method]
    
    // 重写Array原型上对应的方式
    arrayList[method] = function() {
        // 做视图更新或者渲染操作
        update();
        
        // 视图更新了,调用对应的原生方法
        // arguments 将该有的参数也传进来
        originalArray.call(this, ...arguments);
    }
})

②再修改步骤7的代码

function defineReactive(target, key, value) {
    if (typeof value === 'object') {
        // 通过链去找我们定义好的方法
        if (Array.isArray(value)) {
            value.__proto__ = arrayList
        }
        observer(value)
    }
    Object.defineProperty(target, key, {
        get() {
            return value;
        },
        set(val) {
            // 如果改变的数据和原来一样将不做任何处理
            if (val !== value) {
                // 在这里新增代码,如果设置val是object,对val做监听
                if (typeof val === 'object') {
                    // 通过链去找我们定义好的方法
                    if (Array.isArray(val)) {
            val.__proto__ = arrayList
          }
                    observer(val)
                }
                 // 数据更新了,调用update
                 update();
                 value = val;
            } 
        }
    })
}
  1. 完整代码
const methods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
const arrayList = []

// 遍历methods数组
methods.forEach(method => {
    // 原生Array的原型方法
    const originalArray = Array.prototype[method]

    // 重写Array原型上对应的方式
    arrayList[method] = function() {
        // 做视图更新或者渲染操作
        update();

        // 视图更新了,调用对应的原生方法
        // arguments 将该有的参数也传进来
        originalArray.call(this, ...arguments);
    }
})


// 当我们监听的数据发生变化后调用改函数
function update() {
    console.log('数据变化啦,更新视图')
}

function observer(target) {
    // 如果不是对象,直接返回
    if (typeof target !== 'object' || !target) return target;

    // 遍历对象obj的所有key,完成属性配置
    Object.keys(target).forEach(key => defineReactive(target, key, target[key]))
}


function defineReactive(target, key, value) {
    if (typeof value === 'object') {
        if (Array.isArray(value)) {
            value.__proto__ = arrayList
        }
        observer(value)
    }
    Object.defineProperty(target, key, {
        get() {
            return value;
        },
        set(newVal) {
            // 如果改变的数据和原来一样将不做任何处理
            if (newVal !== value) {
                // 在这里新增代码,如果设置newVal是object,对newVal做监听
                if (typeof newVal === 'object') {
                    if (Array.isArray(newVal)) {
            newVal.__proto__ = arrayList
          }
                    observer(newVal)
                }
                 // 数据更新了,调用update
                 update();
                 value = newVal;
            }
        }
    })
}


const data = {
  level: 1,
  info: {
    name: 'cc'
  },
  arr: []
}

observer(data)

// 自行打开注释行测试即可

// ①
// data.level = 2

// ②
// data.info.name = 'xy'

// ③
/*
data.info = {name: 'cc'}
data.info.name = 'xy'
*/

// ④
// data.arr.push(1)

// ⑤
/*
data.arr = []
data.arr.push(1)
*/


值得注意的是:数组不支持长度的修改,也不支持通过数组的索引进行更改。例如以下方式是不会触发视图更新,只有上面列举的7个方式或者直接替换一个新的数组才会触发视图更新。数组更新检测

data.arr.length = 3
data.arr[1] = 1

Vue3.0 选择的 Proxy

Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。

function update() {
  console.log('数据变化啦,更新视图')
}

const data = {
  level: 1,
  info: {
    name: 'cc'
  },
  arr: []
}

const handler = {
  get(target, property) {
    // 如果值为对象,在对该值进行数据劫持
    if (typeof target[property] === 'object' && target[property] !== null) {
      return new Proxy(target[property], handler)
    }
    return Reflect.get(target, property)
  },

  set(target, property, value) {
    if (property === 'length') {
      return true
    }
    update()
    return Reflect.set(target, property, value)
  }
}

const proxy = new Proxy(data, handler)

proxy.level = 2
proxy.info.name = 'yy'
proxy.arr.push(1)
proxy.arr[1] = 1

Proxy最大的问题应该就是兼容性了,但是3.0都准备发布了,我们值得简单一试~

你可能感兴趣的:(Vue响应式原理)