一、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、项目基本配置
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
}
})