正文
大家都知道 Vue 的一个核心特点是数据驱动 如果按照以往 Jquery 的思想 咱们数据变化了想要同步到视图就必须要手动操作 dom 更新 但是 Vue 帮我们做到了数据变动自动更新视图的功能 那在 Vue 内部就一定有一个机制能监听到数据变化然后触发更新 本篇主要介绍响应式数据的原理
1.数据初始化
javascript
new Vue({
el: '#app',
router,
store,
render: (h) => h(App),
})
这段代码 大家一定非常熟悉 这就是 Vue 实例化的过程 从 new 操作符 咱们可以看出 Vue 其实就是一个构造函数 没啥特别的 传入的参数就是一个对象 我们叫做 options(选项)
javascript
// 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 初始化可能会处理很多事情 比如数据处理 事件处理 生命周期处理等等 所以划分不同文件引入利于代码分割
javascript
// 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 实例调用
javascript
// src/state.js
import { observe } from './observer/index.js'
// 初始化状态 注意这里的顺序 比如我经常面试会问到 是否能在data里面直接使用prop的值 为什么?
// 这里初始化的顺序依次是 prop>methods>data>computed>watch
export function initState(vm) {
// 获取传入的数据对象
const opts = vm.$options
if (opts.props) {
initProps(vm)
}
if (opts.methods) {
initMethod(vm)
}
if (opts.data) {
// 初始化data
initData(vm)
}
if (opts.computed) {
initComputed(vm)
}
if (opts.watch) {
initWatch(vm)
}
}
// 初始化data数据
function initData(vm) {
let data = vm.$options.data
// 实例的_data属性就是传入的data
// vue组件data推荐使用函数 防止数据在组件之间共享
data = vm._data = typeof data === 'function' ? data.call(vm) : data || {}
// 把data数据代理到vm 也就是Vue实例上面 我们可以使用this.a来访问this._data.a
for (let key in data) {
proxy(vm, `_data`, key)
}
// 对数据进行观测 --响应式数据核心
observe(data)
}
// 数据代理
function proxy(object, sourceKey, key) {
Object.defineProperty(object, key, {
get() {
return object[sourceKey][key]
},
set(newValue) {
object[sourceKey][key] = newValue
},
})
}
initState 咱们主要关注 initData 里面的 observe 是响应式数据核心 所以另建 observer 文件夹来专注响应式逻辑 其次我们还做了一层数据代理 把 data 代理到实例对象 this 上
2.对象的数据劫持
javascript
// 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还是一个对象会继续走一遍defineReactive 层层遍历一直到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 里面去通知视图更新
思考 1.这样的数据劫持方式对数组有什么影响?
这样递归的方式其实无论是对象还是数组都进行了观测 但是我们想一下此时如果 data 包含数组比如 a:[1,2,3,4,5] 那么我们根据下标可以直接修改数据也能触发 set 但是如果一个数组里面有上千上万个元素 每一个元素下标都添加 get 和 set 方法 这样对于性能来说是承担不起的 所以此方法只用来劫持对象
思考 2.Object.defineProperty 缺点?
对象新增或者删除的属性无法被 set 监听到 只有对象本身存在的属性修改才会被劫持
3.数组的观测
因为对数组下标的拦截太浪费性能 对 Observer 构造函数传入的数据参数增加了数组的判断
javascript
// 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])
}
}
}
对数组原型重写之前咱们先要理解这段代码 这段代码的意思就是给每个响应式数据增加了一个不可枚举的__ob__属性 并且指向了 Observer 实例 那么我们首先可以根据这个属性来防止已经被响应式观察的数据反复被观测 其次 响应式数据可以使用__ob__来获取 Observer 实例的相关方法 这对数组很关键
javascript
// src/obserber/index.js
class Observer {
// 观测值
constructor(value) {
Object.defineProperty(value, '__ob__', {
// 值指代的就是 Observer 的实例
value: this,
// 不可枚举
enumerable: false,
writable: true,
configurable: true,
})
}
}
javascript
// 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
}
})
4.响应式数据的思维导图
小结
至此 Vue 的响应式数据原理已经完结 大家可以看着思维导图自己动手写一遍核心代码哈 需要注意的是 里面对于 this 的引用很多 不同的环境 this 的指向不同 大家不要搞混淆 然后目前能实现的功能仅仅是对数据进行了响应式观测 但是对数据修改后怎么导致视图重新渲染 这块还需要结合 Watcher 和 dep 采用观察者模式实现依赖收集和派发更新的过程