Vue2源码学习与实现(一、响应式原理)

97 阅读2分钟

一、Object.defineProperty()

Object.defineProperty()是实现Vue数据响应式的核心,IE9以下不支持,直接导致Vue2无法支持IE9以下的浏览器,下面是Object.defineProperty()的基本用法

let defineProperty = {}

let name = 'jack'
//将defineProperty 的 name 属性和 name 绑定在一起
Object.defineProperty(defineProperty, 'name', {
    get() {
        console.log('get触发啦')
        return name
    },
    set(newValue) {
        console.log('set触发啦')
        name = newValue
    }
})

console.log(defineProperty.name)
// console :  
//        jack
//        get触发啦   
defineProperty.name = 'tom'
console.log('name:'name)
//        set触发啦
//        name:tom

二、实现原理前基本准备

1、项目基本配置

image.png

2、新建Vue函数,获取data中的属性

思路:

1、获取Vue中传进的options参数

2、将用户的options挂载到vm实例上

3、 根据不同的data类型,获取data中属性值。data可能存在两种形式(参照函数作用域的原因):

①根实例中的data可以是函数,也可以是对象

②组件中data只能是函数

// src/index.js
import { initMixin } from "./init"
//耦合所有方法
function Vue(options) { //options:用户选项
    this._init(options)
}
initMixin(Vue)
export default Vue
//init.js
import { initState } from "./state"
export function initMixin(Vue) {
    Vue.prototype._init = function (options) {
        const vm = this
        vm.$options = options //将用户的选项挂载到实例上
        //初始化状态
        initState(vm)
    }
}
//state.js
export function initState(vm) {
    const opts = vm.$options
    if (opts.data) {
        initData(vm)
    }
}
function initData(vm) {
    let data = vm.$options.data
    //根实例data可以是对象,也可以是函数,组件data只能是函数
    data = typeof data === 'function' ? data.call(this) : data
    vm._data = data
}

三、响应式原理不同情况处理

1、普通对象

  • 遍历获取的data的key和value,通过 Object.defineProperty() 添加响应式
  • 此时响应式的data不存在于Vue实例中,为挂载到vm中,vm._data = data,Vue实例可通过_data形式访问数据
  • 为进一步优化vm对于data中数据的访问,进行二次添加响应式,直接将vm._data与vm添加响应式,此时可以通过vm[属性名]直接访问数据
//state.js
import { observe } from "./observe/index"

export function initState(vm) {
    const opts = vm.$options
    if (opts.data) {
        initData(vm)
    }
}
function proxy(vm, target, key) {
    Object.defineProperty(vm, key, {  //vm.name
        get() {
            return vm[target][key]    //vm._data.name
        },
        set(newValue) {
            vm[target][key] = newValue
        }
    })
}

function initData(vm) {
    let data = vm.$options.data
    //根实例data可以是对象,也可以是函数,组件data只能是函数
    data = typeof data === 'function' ? data.call(this) : data
    vm._data = data
    //vm._data 用vm来代理
    for (let key in data) {
        proxy(vm, '_data', key)
    }

}
//observe/index.js
class Observer {
    constructor(data) {
        //object.defineProperty只能劫持已经存在的属性($set,$delete)
            this.walk(data)
    }
    walk(data) { //循环遍历对象,劫持属性
        Object.keys(data).forEach(key => {
            defineReactive(data, key, data[key])
        })
    }
}

export function defineReactive(target, key, value) {
    Object.defineProperty(target, key, {
        get() {
            return value
        },
        set(newValue) {
            if (value === newValue) return
            value = newValue
        }
    })
}
export function observe(data) {
    //对这个对象进行劫持
    if (typeof data !== 'object' || data == null) {
        return
    }
    if (data.__ob__ instanceof Observer) {
        return data.__ob__
    }
    return new Observer(data)

}


2、给对象中包含对象

  • 添加响应式的过程中,判断value是否为object,如果是object,递归调用observe(),递归添加响应式
//observe/index.js
export function defineReactive(target, key, value) {
    observe(value)  //递归,对所有的对象都进行属性劫持
    Object.defineProperty(target, key, {
        get() {
            return value
        },
        set(newValue) {
            if (value === newValue) return
            value = newValue
        }
    })
}

3、劫持数组方法

  • 如果通过数组下标的方式来访问数组,来实现数组的响应式,则需要遍历数组的每一个值为其添加set()和get(),对于数组长度为千、万为单位时,意味着需要增加成千上万的set()和get(),这会大大影响整体代码的执行速度。
  • 判断数据为object时,通过observe添加响应式
  • 直接操作Array原型上的方法并进行重写,这样干掉了原本的push等,再也无法调用原生的slice,contact等,应该保留数组原有方法,重写部分方法
  • 对数组产生影响的七个方法'push','pop','shift','unshift','reverse','sort','splice'进行重写
//observe/index.js
import { newArrayProto } from './array'
class Observer {
    constructor(data) {
        //object.defineProperty只能劫持已经存在的属性($set,$delete)
        // data.__ob__ = this  //给数据加了一个标识 如果数据上有__ob__,则说明这个属性被观测过
        Object.defineProperty(data, '__ob__', {
            value: this,
            enumerable: false
        })
        if (Array.isArray(data)) {
            //重写数组方法,7个变异方法,是可以修改数组本身
            //对数组中对象进行监控
            // data.__proto__ = {  //这样干掉了原本的push,再也无法调用原生的slice,contact等;保留数组原有方法,重写部分方法
            //     push() {
            //         console.log('this is repush')
            //     }
            // }
            data.__proto__ = newArrayProto
            this.observeArray(data)
        } else {
            this.walk(data)
        }
    }
    walk(data) { //循环遍历对象,劫持属性
        Object.keys(data).forEach(key => {
            defineReactive(data, key, data[key])
        })
    }
    observeArray(data) { //观测数组
        data.forEach(item => {
            observe(item)
        })
    }
}
export function defineReactive(target, key, value) {
    observe(value)  //递归,对所有的对象都进行属性劫持
    Object.defineProperty(target, key, {
        get() {
            return value
        },
        set(newValue) {
            if (value === newValue) return
            value = newValue
        }
    })
}
export function observe(data) {
    //对这个对象进行劫持
    if (typeof data !== 'object' || data == null) {
        return
    }
    if (data.__ob__ instanceof Observer) {
        return data.__ob__
    }
    return new Observer(data)
}

array.js
//重写数组中部分方法
let oldArrayProto = Array.prototype
//newArrayProto.__proto__ = oldArrayProto
export let newArrayProto = Object.create(oldArrayProto)

let methods = [
    'push',
    'pop',
    'shift',
    'unshift',
    'reverse',
    'sort',
    'splice'
]//concat slice不会改变原数组

methods.forEach(method => {
    //arr.push(1,2,3)
    newArrayProto[method] = function (...args) {
        const result = oldArrayProto[method].call(this, ...args)
        return result
    }

})

4、给数组中新增数据添加响应式

只有push,unshift,push能够对数组新增数据,对其进行处理

//array.js
methods.forEach(method => {
    //arr.push(1,2,3)
    newArrayProto[method] = function (...args) {
        const result = oldArrayProto[method].call(this, ...args)
        //对新增的数据进行劫持
        let inserted  //新增的内容
        let ob = this.__ob__
        switch (method) {
            case 'push':
            case 'unshift': //arr.unshift(1,2,3)
                inserted = args
                break;
            case 'splice':  //arr,splice(0,1,{a:1},{b:2})
                inserted = args.slice(2)
            default:
                break;
        }
        if (!inserted) {
            //对新增内容进行观测
            ob.observeArray(inserted)
        }
        return result
    }
})