你通过本文能了解到什么?
- 数据劫持的原理
- this 是如何访问到data中的属性的
- v-model 原理
众所周知,vue2的响应式原理是通过 Object.defineProperty 来实现的,那么他具体的实现方式是什么样的呢?
首先从流程的方面讲解
1. 通过Object.keys 获取到data返回对象的属性,遍历data[key] 执行observe
2. 执行observe 主要功能是对data[key]的值 实例化 Observer
【observer函数源码】(点击展开) ```js /* * * observe 源码 */ export function observe( value: any, // 是对象 shallow?: boolean, // false ssrMockReactivity?: boolean // false ): Observer | void { if (!isObject(value) || isRef(value) || value instanceof VNode) { // 不是对象 不是nul 不是ref(v3) 并且不再vnode实例 return } let ob: Observer | void if (hasOwn(value, 'ob') && value.ob instanceof Observer) { ob = value.ob } else if ( shouldObserve && (ssrMockReactivity || !isServerRendering()) && (isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && // isExtensible 是否可以扩展 !value.__v_skip /* ReactiveFlags.SKIP */ // 不跳过 ) { // 没有被Observe 实例的属性 ob = new Observer(value, shallow, ssrMockReactivity) } return ob } ```
3. new Observe 的主要功能及源码
-
添加 dep 属性,并实例化 Dep
-
为对象属性添加 __ob__属性,并指向实例后的对象
-
判断 value 是否是数组
- 是数组的情况下
- 首先会给这个数组 重写他的 数组方法:'push','pop','shift','unshift','splice','sort','reverse'.
- 调用 observeArray 遍历数组的值, 递归执行 observe方法
-
是对象的情况下对value 的 key 遍历执行defineReactive, 此时就要执行数据劫持了
4. defineReactive
只讲 最主要的功能 且目前只考虑到渲染watcher 和 computedWatcher 访问的情况
-
首先会在此方法里面 创建一个单独的 dep 实例, 用来做依赖收集和通知更新的,注意:在get时会用到此 dep 实例 。形成了 闭包
-
获取到 data对象属性的每一个值val,并递归 执行 observe(val) 这里只有对象或者数组类型的会递归下去
-
对 data 的属性进行 Object.defineProperty 的数据劫持
-
劫持之前 是初始化data的行为, 在访问data属性时,会触发get
-
在访问data属性时, 可能是渲染watcher(组件是一个渲染wathcer) 也可能是computedWatcher(每一个 computed 属性都会创建一个 computedWatcher ),这里先讲渲染wathcer,
-
在初始化 new Vue 时会执行$mount(...),mount 就会创建一个渲染watcher,并且会执行watcher 的run 方法
-
wathcer.run 又会执行watcher.get,此时就会访问到data属性,
-
在watcher.get 时, 会把 全局的一个 target 标记为当前的渲染watcher
-
所以在访问data属性时,Dep.target 是渲染watcher
-
此时通过闭包的原理,之前创建的 dep 来进行收集依赖,执行 dep.depend(),并且判断当前val 是否是对象,如果是访问的对象,也要让对象的 dep 也收集依赖,就是在 new Observe 时创建的属于对象的 dep 实例
-
dep.depend 执行后,分为两种, 一种是渲染 watcher 收集依赖, 一种是 computedWatcher 收集依赖
- 先只讲 watcher 收集依赖的情况
- dep.depend 执行时,因为当前的 Dep.target 是渲染watcher 所以,会执行 watcher.addDep ,然后 addDep 又会执行 dep的addSub 方法
- 所以 data属性的值,通过闭包,Dep 实例收集到wathcer在dep.subs 数组中
- watcher 通过 Dep.target 来收集当前需要收集到的依赖(也就是 Dep 实例),收集到 watcher.newDep数组中, 他俩相互依赖到对方
js// dep 的 depend 方法 function depend() { if (Dep.target) { // 渲染wathcer Dep.target.addDep(this) } } // watcher 的 addDep 方法 function addDep(dep: Dep) { const id = dep.id if (!this.newDepIds.has(id)) { this.newDepIds.add(id) this.newDeps.push(dep) // 收集dep if (!this.depIds.has(id)) { dep.addSub(this) // 让dep 收集watcher } } }
-
这 getter 就完成了收集依赖
-
setter 通知更新
- 首先会判断新值和旧值是否相同,相同则不通知 watcher 进行更新
- 不同的话,会对新值进行数据劫持
- 之后也是通过闭包中的 dep 来通知更新 dep.notify()
- notify 会遍历当前 dep 所收集到的 watcher 来进行update
- update 方法也是把更新的动作放到了一个队列中去,按照浏览器的 EventLoop 事件处理流程来执行
js// 首先subs 中存放的都是这个 watcher 依赖的data 数据 // 所以便利 watcher 进行让所有的watcher 都进行更新 // dep.notify 函数 function notify() { const subs = this.subs.slice() // 每一个data的key都有一份订阅者列表 for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() // 更新视图 } } } // watcher update 函数 function update() { queueWatcher(this) // 如果Watcher既不是lazy也不是sync,则会执行queueWatcher(this)。这是最常见的更新情况。queueWatcher()函数将当前Watcher放入一个队列(异步更新队列)。Vue会按照一定的策略(如nextTick、微任务等)批量处理队列中的Watcher,依次调用它们的run()方法,实现异步批量更新。这样可以避免短时间内大量数据变化导致的频繁DOM操作,提高整体性能。 }
【defineReactive函数源码 和 Dep class】(点击展开)
js
function defineReactive(
obj: object,
key: string,
val?: any,
customSetter?: Function | null,
shallow?: boolean,// 浅的
mock?: boolean
) {
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)// 获取对象自身属性上的描述符
if (property && property.configurable === false) { // configurable 不可改变和删除的
return
}
// cater for pre-defined getter/setters
// 满足预定义的 getter/setters
const getter = property && property.get
const setter = property && property.set
if (
(!getter || setter) &&
(val === NO_INIITIAL_VALUE || arguments.length === 2)
) {
val = obj[key]
/**
* obj[key] 的真实内容
* 1. vm[$attrs]
* 2. vm[$listeners]
*/
}
// shallow 意思是 浅的 所以反是深 子级的观察
let childOb = !shallow && observe(val, false, mock) // 递归value
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {// 活性 应该是灵活的意思
const value = getter ? getter.call(obj) : val // 如果 值的描述属性有get 就用userdef 的
if (Dep.target) {
dep.depend() // 用到了闭包
if (childOb) {
// 只有对象才有
childOb.dep.depend()
// 当访问的对象的时候 如果对象的值是数组 就走这里
if (isArray(value)) {
dependArray(value)
}
}
}
return isRef(value) && !shallow ? value.value : value
},
set: function reactiveSetter(newVal) {
const value = getter ? getter.call(obj) : val
if (!hasChanged(value, newVal)) {
return
}
if (__DEV__ && customSetter) { // 定制了setter
customSetter()
}
if (setter) {
setter.call(obj, newVal)
} else if (getter) {
// #7981: for accessor properties without setter
// 对于没有setter的访问器属性
return
} else if (isRef(value) && !isRef(newVal)) { // v3
value.value = newVal
return
} else {
val = newVal
}
// 子级的观察者
childOb = !shallow && observe(newVal, false, mock)
dep.notify() // 通知
}
})
return dep // Dep 实例
}
js
class Dep {
static target?: DepTarget | null
id: number
subs: Array<DepTarget>
constructor() {
this.id = uid++
this.subs = []
}
addSub(sub: DepTarget) {
this.subs.push(sub)
}
removeSub(sub: DepTarget) {
remove(this.subs, sub)
}
depend() {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify(info?: DebuggerEventExtraInfo) {
const subs = this.subs.slice() // 每一个data的key都有一份订阅者列表
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update() // 更新视图
}
}
}
数据依赖收集总结
每个属性通过 defineReactive 创建一个 dep 实例, getter 时 通过闭包的原理,让每一个 属性中的dep 收集 wathcer,在 dep 收集 watcher 的同时,watcher 也收集 dep,两个实例相互收集对方。
data 中的属性,this 是如何访问和设置的?
此原理比较简单易懂,原理就是代理每个属性到this上,请阅读步骤
- 在 new Vue 时,会执行init方法,此方法会对数据进行初始化,
- 在初始化 data 属性时,会先获取到组件 data 返回的对象, 并赋值给 this._data
- 之后遍历 data 对象的值, 通过 Object.defineProperty 将 this 作为target(目标对象),key作为访问的key,
- 重写get 方法,每次get时,都get this._data[key]
- 重写set 方法。每次set是,都set this._data[key]
js
function Vue(options) {
console.log('开始实例化vue')
this._init(options)
}
function proxy(target: Object, sourceKey: string, key: string) {
sharedPropertyDefinition.get = function proxyGetter() {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter(val) {
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
proxy(vm, '_data', key)
v-model 指令的原理
-
数据绑定 : 当在表单元素上使用
v-model
时,Vue 会在内部将其转化为对元素value
属性(或其他特定属性,如<select>
的value
或<checkbox>
/<radio>
的checked
)的绑定。例如,<input v-model="message">
相当于 Vue 自动做了:value="message"
(即v-bind:value="message"
)的绑定,确保表单元素的显示值与 Vue 实例中message
数据属性的当前值相匹配。 -
事件监听与数据更新 :
v-model
不仅绑定了数据到表单元素的属性,还监听了相应表单元素的特定输入事件(通常是input
事件,对于某些类型可能为change
事件),并在事件触发时更新数据属性。当用户在表单元素上进行输入或选择时,会触发相应的事件,Vue 会捕获该事件并提取新的值,然后将这个值赋给绑定的数据属性。例如,对于<input v-model="message">
,每当用户在输入框内输入字符,就会触发input
事件,Vue 会更新message
的值。 -
数据变化驱动视图更新 : 由于 Vue 实现了数据响应式,当
v-model
绑定的数据属性值发生变化时,Vue 会检测到这一变化并通过其内部的虚拟DOM(VNode)和Diff算法来确定需要对实际DOM进行哪些最小化的更新操作,确保视图与最新数据状态保持一致。这意味着一旦message
的值在事件处理中被更新,Vue 会自动刷新相关表单元素的显示值。 -
修饰符 :
v-model
支持一些修饰符以改变其默认行为。例如,lazy
修饰符会让 Vue 监听change
而非input
事件,只在用户完成输入并离开表单元素时才更新数据;number
修饰符会自动将用户的输入值转化为数字;trim
修饰符则会自动去除输入值的首尾空格。 -
在自定义组件中的应用 : 当
v-model
用于自定义组件时,Vue 会期望该组件遵循特定的约定(接收名为value
的属性并触发名为input
的自定义事件来传递新值)。开发者可以通过组件的model
选项自定义这些属性和事件的名称,以适应不同的组件设计。总结来说,
v-model
的原理在于它巧妙地结合了 Vue 的数据绑定(v-bind
)和事件监听(v-on
)机制,实现了表单元素与数据属性之间的高效、自动化的双向数据流。通过这种简化的语法,开发者可以轻松地在 Vue 应用中创建交互式的表单,无需手动处理繁琐的事件监听和数据同步逻辑。
总结
- 双向绑定原理就是通过 Observe、Dep、 Watcher 三大类来进行数据劫持和依赖收集
- this 访问和设置数据也是通过 Object.definePropety 来进行属性操作的
- v-model 是通过 vue 的数据绑定和事件监听方式实现的
如有错误可以帮忙指出并帮忙解答一下~~~