简单吃透vue数据劫持

1,069 阅读4分钟

前言:

众所周知vue2是通过Object.defineProperty,vue3通过proxy实现对于数据也就是data的劫持,然后监听数据的变化也就是响应式,从而让页面与数据保持同步刷新,今天和大家聊一下vue2中数据劫持,通过原生实现让大家彻底搞懂,彻底了解,并且轻松过面试嘻嘻

思考

Vue2在创建一个实例后做了什么事呢 ? 这里我们简单写点代码

const vm = new Vue({
    el: '#app',
    data() {
        return {
            title: '标题',
            main: '研究数据劫持',
            author: '黑猫',
            list: [1, 2, 3, 4],
            info: {
                a: {
                    b: 1
                }
            }
        }
    }
})
console.log(vm)

由于是研究数据劫持,我们就不对其他方面过多讨论,也帮助大家把重点放在data上

image.png

image.png

1.初始化工作

从打印可以看出:
1.我们在data定义的属性被放在了Vue的实例中,
2.传入到这个vue的构造函数的对象(options)也被挂载到实例上
3.我们可以通过_data以及$data都可以访问到我们定义的属性,
4.每个属性都被做了数据劫持,
那么刚从这里看,这个数据劫持有什么意义呢? 先简单实现这一部分功能

// 初始化Vue
function Vue(options) {
    this._init(options)
}
// 初始化操作
Vue.prototype._init = function (options) {
    let vm = this
    vm.$options = options
    initState(vm)
}

function initState(vm) {
    // 对后面的数据进行初始化操作
}

这里我们简单的将我们传入的对象挂载到了Vue的$options属性上,然后调用一个initState函数准备对后面的数据进行劫持操作
为了让大家看的方便,这里我不采用模块化的写法,单文件搞定(莫说我菜)

image.png 刚才我们最后调用了initState函数并且把实例vm传递进去,传递vm是为了我们方便为它赋值,以及做代理

console.log(vm.title) // 标题
console.log(vm._data.title) //标题
vm._data.title = '修改标题1'
console.log(vm.title) // 修改标题1
vm.title = '修改标题2'
console.log(vm.title) // 修改标题2

2.初步代理

前面也说了vue会把数据渲染到vue实例,以及_data属性中,因此我们也可以通过上述代码去访问以及修改都是木得问题的,它依然是响应式刷新(因为做了数据劫持),因此让我们来完善initState函数以及后面的操作吧

function initState(vm) {
    let options = vm.$options
    if (options.data) {
        initData(vm)
    }
}

function initData(vm) {
    let data = vm.$options.data
    vm._data = data = typeof data === 'function' ? data.call(vm) : {}
    for (let key in data) {
        proxy(vm, '_data', key)
    }
}

function proxy(vm, target, key) {
    Object.defineProperty(vm, key, {
        get() {
            return vm[target][key]
        },
        set(newValue) {
            vm[target][key] = newValue
            // 这里就可以做响应式刷新页面呀,做侦听器呀,等等操作,本次就不讨论了
        }
    })
}

这里我们将传入的对象挂载到了vue的实例中,并且判断有没有传入data这个属性呢,有的话我们就对其进行数据劫持
然后我们将data的数据渲染到实例中,并且给每一个数据进行数据劫持, 初步的数据劫持就完成啦

image.png

 console.log(vm.title)
 vm.title = '设置标题'

image.png
初步工作已经完成,但是从上述图片看我们并没有对_data里面的属性做数据劫持呀,这是肯定不行的,我们下面继续完善

image.png

3.完善_data数据中的数据劫持

那么这里非常简单,我们直接在initData这个函数顺便把_data里面的数据顺便做个劫持不就完事了嘛

function initData(vm) {
    let data = vm.$options.data
    vm._data = data = typeof data === 'function' ? data.call(vm) : {}
    for (let key in data) {
        proxy(vm, '_data', key)
    }
    observe(vm._data) //直接在这里添一行做处理啦
}

此时我们的代码完整如下

// 初始化Vue
function Vue(options) {
    this._init(options)
}
// 初始化操作
Vue.prototype._init = function (options) {
    let vm = this
    vm.$options = options
    initState(vm)
}
// 初始化状态
function initState(vm) {
    let options = vm.$options
    if (options.data) {
        initData(vm)
    }
}
// 初始化data
function initData(vm) {
    let data = vm.$options.data
    vm._data = data = typeof data === 'function' ? data.call(vm) : {}
    for (let key in data) {
        proxy(vm, '_data', key)
    }
    observe(vm._data)
}
// 对全局访问data做数据劫持
function proxy(vm, target, key) {
    Object.defineProperty(vm, key, {
        get() {
            // console.log(`访问属性`)

            return vm[target][key]
        },
        set(newValue) {
            //  console.log(`设置属性`)
            vm[target][key] = newValue
        }
    })
}

那我们就来完善这个observe函数以及后面的功能吧,而且请大家现在记住observe函数在做数据劫持,在做数据劫持,在做数据劫持

image.png

function observe(data) {
    if (typeof data !== 'object' || data === null) return // 此判断后面解释
    return new Observer(data)
}

function Observer(data) {
    if (Array.isArray(data)) {
        // 先不了解,后面解释
    } else {
        this.walk(data)
    }
}
// 对_data的数据进行处理
Observer.prototype.walk = function (data) {
    var keys = Object.keys(data)
    for (let i = 0; i < keys.length; i++) {
        let key = keys[i],
            value = data[key]
        defineReactiveData(data, key, value)
    }
}
// 数据劫持
function defineReactiveData(data, key, value) {
    observe(value)
    Object.defineProperty(data, key, {
        get() {
            console.log('获取数据', value)
            return value
        },
        set(newValue) {
            console.log('设置新值', newValue)
            if (newValue === value) return // 做点小优化嘻嘻
            observe(newValue)
            value = newValue
        }
    })
}
  1. 首先请大家看看刚才打印的图片,这个observe传入的data是data函数执行后返回的data对象,可不要搞混淆了
  2. 在这里可能有同学很奇怪为啥在这里对data做一个判断呢,为啥不直接在上面做了不就好了,现在我只能告诉大家这个函数它可能重复使用,因此我们必须要在这里判断data是对象的情况,
  3. Observer这个函数针对数组或对象做不同的操作来完成数据劫持,我们先把对象的搞好,后面来再叙述数组的情况
  4. Observer的原型walk方法来处理data是对象的情况,来做数据劫持,然后通过defineReactiveData来完成具体操作
  5. 在defineReactiveData函数中我们对_data的每一个数据都做了数据劫持

为什么我们在defineReactiveData函数中要递归调用observe函数,以及在设置值的时候也要递归调用observe函数呢? 请大家再回上面看看observe函数是用来做啥的 看看下面的代码

//这里info是个2层对象忘记可以看看开头模板
 console.log(vm.info.a.b)
 vm.info.a = 33

请大家思考这个最里面的 b 应不应该走数据劫持,我们设置这个a对象应不应该走数据劫持?
答案是肯定应该呀,data中的所有数据都递归做过数据劫持,大家可以去页面渲染,然后改变最深层的值验证是不是走了代理进而触发响应式重新渲染修改后数据的页面,这里不做演示,因此我们需要递归对它们进行数据劫持,那我们刚在observe函数中对data是不是对象的判断是不是就非常关键了呀,如果是个普通值得话我们就不需要递归了呀,嘻嘻,这样不就搞懂前面得意思啦
此时完整代码如下

// 初始化Vue
function Vue(options) {
    this._init(options)
}
// 初始化操作
Vue.prototype._init = function (options) {
    let vm = this
    vm.$options = options
    initState(vm)
}
// 挂载options以及处理data函数
function initState(vm) {
    let options = vm.$options
    if (options.data) {
        initData(vm)
    }
}
// 处理data
function initData(vm) {
    let data = vm.$options.data
    vm._data = data = typeof data === 'function' ? data.call(vm) : {}
    for (let key in data) {
        proxy(vm, '_data', key)
    }
    observe(vm._data)
}
// 给从Vue实例直接访问属性做代理
function proxy(vm, target, key) {
    Object.defineProperty(vm, key, {
        get() {
            // console.log(`访问属性`)
            return vm[target][key]
        },
        set(newValue) {
            //  console.log(`设置属性`)
            vm[target][key] = newValue
        }
    })
}

// 为vm的_data中的数据做数据劫持
function observe(data) {
    if (typeof data !== 'object' || data === null) return
    return new Observer(data)
}

// 数组对象处理方法不同
function Observer(data) {
    if (Array.isArray(data)) {
        // 先不了解,后面解释
    } else {
        this.walk(data)
    }
}
//针对对象代理的准备工作
Observer.prototype.walk = function (data) {
    var keys = Object.keys(data)
    for (let i = 0; i < keys.length; i++) {
        let key = keys[i],
            value = data[key]
        defineReactiveData(data, key, value)
    }
}

// 对data为对象的情况做数据劫持
function defineReactiveData(data, key, value) {
    observe(value)
    Object.defineProperty(data, key, {
        get() {
            console.log('获取数据', value)
            return value
        },
        set(newValue) {
            console.log('设置新值', newValue)
            if (newValue === value) return // 做点小优化嘻嘻
            observe(newValue)
            value = newValue
        }
    })
}

4.数组的数据劫持实现

当面试被问到vue2的数据劫持时,如果只说用到Object.defineproperty的话并不能说明你确实懂这个,比如对于数组,当它被做数据劫持的时候Object.defineproperty能检测到嘛,那我们使用数组的方法呢,又会怎么样呢?比如这段代码

const vm = {
    data: {
        list: [1, 2, 3, 4]
    }
}
for (let key in vm.data) {
    Object.defineProperty(vm, key, {
        get() {
            console.log('数组被读')

            return vm.data[key]
        },
        set(newValue) {
            console.log('数组被写')
            vm.data[key] = newValue
        }
    })
}
console.log(vm.list)
vm.list = 3
vm.list = [1, 2, 3]
vm.list.push(1)
vm.list.pop()
console.log(vm.list)

image.png
从打印的图上我们可以看出当我们使用push,pop,unshift,shift,...等方法的时候只触发了读取操作,并没有触及写入操作, 这个问题是非常严重的,想象当我们操作数组的时候,如果使用数组的方法,但是检测不到,那何来的响应式?那么Vue是如何做到当使用方法操作数组,也能捕获到,或者响应页面变化呢?

image.png 这个是vue的处理,代码是顶部的模板,我们通过控制台可以看出vue在数组上面多加了一层原型,改写了数组的pop,push,reverse,shift,sort,splice,unshit方法,这一层原型的原型才是数组原来的方法,很显然Vue内部对这几个方法做了改写,所以我们在操作数组的时候使用这些方法的时候才能被检测到,比如最直观的页面响应变化嘻嘻。 那我们就来实现一下这个过程,直接从刚才实现Observer这个函数中来对数组这种情况进行处理

function Observer(data) {
    if (Array.isArray(data)) {
        data.__proto__ = arrMethods
        observeArr(data)
    } else {
        this.walk(data)
    }
}
const ARR_METHODS = ['push', 'pop', 'unshift', 'shift', 'splice', 'sort', 'reverse']
const originArray = Array.prototype,
    arrMethods = Object.create(originArray)

ARR_METHODS.forEach((item) => {
    arrMethods[item] = function (...arg) {
        // 执行原来数组的方法
        let rt = originArray[item].apply(this, arg)
        console.log('数组新方法', arg) // 在这里就可以检测到
        let newArr
        switch (item) {
            case 'push':
            case 'unshit':
                newArr = arg
                break
            case 'splice':
                newArr = arg.slice(2)
            default:
                break
        }
        newArr && observeArr(newArr)
    }
})
function observeArr(arr) {
    for (let i = 0; i < arr.length; i++) {
        observe(arr[i])
    }
}

我们直接在这里在这给数组改写原型来检测这些方法,同时又因为数组里面可能也出现嵌套的可能因此我们多调用 observeArr这个函数来处理嵌套就可以监听深层次的数据写入操作啦
此时完整代码如下

// 初始化Vue
function Vue(options) {
    this._init(options)
}
// 初始化操作
Vue.prototype._init = function (options) {
    let vm = this
    vm.$options = options
    initState(vm)
}

// 初始化状态
function initState(vm) {
    let options = vm.$options
    if (options.data) {
        initData(vm)
    }
}
// data的初始化函数
function initData(vm) {
    let data = vm.$options.data
    vm._data = data = typeof data === 'function' ? data.call(vm) : {}
    for (let key in data) {
        proxy(vm, '_data', key)
    }
    observe(vm._data)
}

// 对于vm实例第一层的数据劫持
function proxy(vm, target, key) {
    Object.defineProperty(vm, key, {
        get() {
            // console.log(`访问属性`)
            return vm[target][key]
        },
        set(newValue) {
            //  console.log(`设置属性`)
            vm[target][key] = newValue
        }
    })
}

// 对于vm._data
function observe(data) {
    if (typeof data !== 'object' || data === null) return
    return new Observer(data)
}
// 要改写的数组方法
const ARR_METHODS = ['push', 'pop', 'unshift', 'shift', 'splice', 'sort', 'reverse']
const originArray = Array.prototype,
    arrMethods = Object.create(originArray)

// 改写数组方法
ARR_METHODS.forEach((item) => {
    arrMethods[item] = function (...arg) {
        // 执行原来数组的方法
        let rt = originArray[item].apply(this, arg)
        console.log('数组新方法', arg)
        let newArr
        switch (item) {
            case 'push':
            case 'unshit':
                newArr = arg
                break
            case 'splice':
                newArr = arg.slice(2)
            default:
                break
        }
        newArr && observeArr(newArr)
    }
})

function observeArr(arr) {
    for (let i = 0; i < arr.length; i++) {
        observe(arr[i])
    }
}
// 对于数组和对象分别处理劫持
function Observer(data) {
    console.log(data)
    if (Array.isArray(data)) {
        data.__proto__ = arrMethods
        observeArr(data)
    } else {
        this.walk(data)
    }
}
Observer.prototype.walk = function (data) {
    var keys = Object.keys(data)
    for (let i = 0; i < keys.length; i++) {
        let key = keys[i],
            value = data[key]
        defineReactiveData(data, key, value)
    }
}
function defineReactiveData(data, key, value) {
    observe(value)
    Object.defineProperty(data, key, {
        get() {
            console.log('获取数据', value)
            return value
        },
        set(newValue) {
            console.log('设置新值', newValue)
            if (newValue === value) return // 做点小优化嘻嘻
            observe(newValue)
            value = newValue
        }
    })
}

结语

到这里,vue2的数据劫持差不多就结束啦,如果大家看这个代码不方便理解可以到 源码链接查看,别忘记点个star