响应式数据原理
一、数据初始化
new Vue({
el: '#app',
data() {
return {
a: [1, 2, 3],
name: '123',
}
},
})
这段代码 大家一定非常熟悉 这就是 Vue 实例化的过程 从 new 操作符 咱们可以看出 Vue 其实就是一个构造函数 没啥特别的 传入的参数就是一个对象 我们叫做 options(选项)
// src/index.js
import { initMixin } from './init.js'
// Vue就是一个构造函数 通过new关键字进行实例化
function Vue(options) {
// 这里开始进行Vue初始化工作
this._init(options)
}
// _init方法是挂载在Vue原型的方法 通过引入文件的方式进行原型挂载需要传入Vue
// 此做法有利于代码分割
initMixin(Vue)
export default Vue
因为在 Vue 初始化可能会处理很多事情 比如数据处理 事件处理 生命周期处理等等 所以划分不同文件引入利于代码分割
// src/init.js
import { initState } from './state'
export function initMixin(Vue) {
Vue.prototype._init = function (options) {
const vm = this
// 这里的this代表调用_init方法的对象(实例对象)
// this.$options就是用户new Vue的时候传入的属性
vm.$options = options
// 初始化状态
initState(vm)
}
}
initMixin 把_init 方法挂载在 Vue 原型 供 Vue 实例调用
// src/state.js
import { observe } from './observer/index'
import { isFunction, proxy } from './utils'
export function initState(vm) {
const opts = vm.$options
if (opts.props) {
initProps(vm)
}
if (opts.methods) {
initMethods(vm)
}
if (opts.data) {
initData(vm)
}
if (opts.computed) {
initComputed(vm)
}
if (opts.watch) {
initWatch(vm)
}
}
// 初始化data数据
function initData(vm) {
// 实例的_data属性就是传入的data
// vue组件data推荐使用函数 防止数据在组件之间共享
let data = vm.$options.data
data = vm._data = isFunction(data) ? data.call(vm) : data
// 把data数据代理到vm 也就是Vue实例上面 我们可以使用this.a来访问this._data.a
// 用户去vm.xxx => vm._data.xxx
for (let key in data) {
proxy(vm, '_data', key)
}
// 数据劫持 对数据进行观测 --响应式数据核心
observe(vm._data)
}
function initProps(vm) {}
function initMethods(vm) {}
function initComputed(vm) {}
function initWatch(vm) {}
// src/utils.js
export function isFunction(val) {
return typeof val === 'function'
}
// 代理
export function proxy(vm, source, key) {
Object.defineProperty(vm, key, {
get() {
return vm[source][key]
},
set(newValue) {
vm[source][key] = newValue
},
})
}
initState 咱们主要关注 initData 里面的 observe 是响应式数据核心 所以另建 observer 文件夹来专注响应式逻辑 其次我们还做了一层数据代理 把 data 代理到实例对象 this 上
二、对象的数据劫持
// src/obserber/index.js
class Observer {
// 观测值
constructor(value) {
this.walk(value)
}
walk(data) {
// 对象上的所有属性依次进行观测
let keys = Object.keys(data)
for (let i = 0; i < keys.length; i++) {
let key = keys[i]
let value = data[key]
defineReactive(data, key, value)
}
}
}
// Object.defineProperty数据劫持核心 兼容性在ie9以及以上
function defineReactive(data, key, value) {
observe(value) // 递归关键
// --如果value还是一个对象会继续走一遍odefineReactive 层层遍历一直到value不是对象才停止
// 思考?如果Vue数据嵌套层级过深 >>性能会受影响
Object.defineProperty(data, key, {
get() {
console.log('获取值')
return value
},
set(newValue) {
if (newValue === value) return
console.log('设置值')
value = newValue
},
})
}
export function observe(value) {
// 如果传过来的是对象或者数组 进行属性劫持
if (
Object.prototype.toString.call(value) === '[object Object]' ||
Array.isArray(value)
) {
return new Observer(value)
}
}
数据劫持核心是 defineReactive 函数 主要使用 Object.defineProperty 来对数据 get 和 set 进行劫持 这里就解决了之前的问题 为啥数据变动了会自动更新视图 我们可以在 set 里面去通知视图更新
这样的数据劫持方式对数组有什么影响?
这样递归的方式其实无论是对象还是数组都进行了观测 但是我们想一下此时如果 data 包含数组比如 a:[1,2,3,4,5] 那么我们根据下标可以直接修改数据也能触发 set 但是如果一个数组里面有上千上万个元素 每一个元素下标都添加 get 和 set 方法 这样对于性能来说是承担不起的 所以此方法只用来劫持对象
Object.defineProperty 缺点?
对象新增或者删除的属性无法被 set 监听到 只有对象本身存在的属性修改才会被劫持
三、数组的观测
// src/obserber/index.js
import { arrayMethods } from './array'
class Observer {
constructor(value) {
if (Array.isArray(value)) {
// 这里对数组做了额外判断
// 通过重写数组原型方法来对数组的七种方法进行拦截
value.__proto__ = arrayMethods
// 如果数组里面还包含数组 需要递归判断
this.observeArray(value)
} else {
this.walk(value)
}
}
observeArray(items) {
for (let i = 0; i < items.length; i++) {
observe(items[i])
}
}
}
因为对数组下标的拦截太浪费性能 对 Observer 构造函数传入的数据参数增加了数组的判断
// src/obserber/index.js
import { defineProperty, isObject } from '../utils'
class Observer {
// 观测值
constructor(value) {
defineProperty(value, '__ob__', this)
}
}
// src/utils.js
export function defineProperty(target, key, value) {
Object.defineProperty(target, key, {
enumerable: false, // 不能被枚举
configurable: false,
value,
})
}
对数组原型重写之前咱们先要理解这段代码 这段代码的意思就是给每个响应式数据增加了一个不可枚举的ob属性 并且指向了 Observer 实例 那么我们首先可以根据这个属性来防止已经被响应式观察的数据反复被观测 其次 响应式数据可以使用ob来获取 Observer 实例的相关方法 这对数组很关键
// src/obserber/array.js
// 先保留数组原型
const arrayProto = Array.prototype
// 然后将arrayMethods继承自数组原型
// 这里是面向切片编程思想(AOP)--不破坏封装的前提下,动态的扩展功能
export const arrayMethods = Object.create(arrayProto)
let methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'reverse',
'sort',
]
methodsToPatch.forEach((method) => {
arrayMethods[method] = function (...args) {
// 这里保留原型方法的执行结果
const result = arrayProto[method].apply(this, args)
// 这句话是关键
// this代表的就是数据本身 比如数据是{a:[1,2,3]} 那么我们使用a.push(4) this就是a ob就是a.__ob__ 这个属性就是上段代码增加的 代表的是该数据已经被响应式观察过了指向Observer实例
const ob = this.__ob__
// 这里的标志就是代表数组有新增操作
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
default:
break
}
// 如果有新增的元素 inserted是一个数组 调用Observer实例的observeArray对数组每一项进行观测
if (inserted) ob.observeArray(inserted)
// 之后咱们还可以在这里检测到数组改变了之后从而触发视图更新的操作--后续源码会揭晓
return result
}
})