参考文献
一、下载vue
Git 仓库地址:github.com/vuejs/vue.g…
- git clone github.com/vuejs/vue.g…
- pnpm install(vue是用pnpm管理工具,用npm会报错,用yarn会找不到依赖包)
- pnpm run dev
学习思路:
先自己搜索->提出问题->再深入源码学习
二、变化侦测
0、现象
-
在 data 里的数据可以通过 v-bind 插入页面中,并且一旦发生改变会被更新到页面上
-
v-model 绑定的数据,在 data 里改变或者在页面上改变后,会被通知给对方,进行数据更新
-
props 里面绑定的数据,当父元素中更新后,子元素的数据也会被更新
-
watch 和 computed 里的数据,在被更新后,页面上的对应数据也会被更新
提出问题
这是怎么实现的呢?vue是怎么发现数据变化的呢?又是怎么对数据变化做出反应?
总之,先进行通过信息搜集,对问题进行浅层了解👇
1、准备工作
UI = render(state)
-
状态
state是输入,页面UI输出,状态输入一旦变化了,页面输出也随之而变化。我们把这种特性称之为数据驱动视图。
调研vue的变化侦测
-
Data 中所有的属性,最后都出现在 vm 上
- Vm 上所有的属性及 vue 原型上所有的属性,在 vue 模板中可以直接用
-
Object.defineProperty
-
通过 object.defineProperty 为对象设置属性和属性值,在 getter 中取值时调用方法,setter 中赋值时调用方法就可以实现在读的时候知道被读了,在写的时候知道被写了
-
提出问题
-
为什么 vue 要在 vm 实例上对 data 里所有的数据备份呢?不可以直接用自己定义的吗?
-
现在知道了数据侦测的更新触发方式,在定义数据的时候,用 Object.defineProperty 把数据变成响应式
-
let val Object.defineProperty(obj, property, { enumerable: true, configurable: true, get: function() { // do sth. return val }, set: function(value) { // do sth. val = value } }) -
在 getter 读取数据的时候,记录下来这个地方与这个数据有关系,更新的时候需要通知它
-
在 setter 读取数据的时候,根据上面记录的关系,把里面的所有用到这个数据的地方都通知一遍
-
问题又来了!
-
数据读取时,依赖关系里存储的是什么呢?怎么才能存储用到数据的地方呢?
-
数据更新后,怎么做才能把与该数据相关的地方都更新呢?
-
Object.definedProperty 的具体实现流程
去源码里找答案
2、数据侦测
数据侦测
D:\Projects\good-projects\vue\src\core\observer\index.ts
关键在于:Observer 类
首先,在定义数据的时候,vue为所有的数据逐层绑定observer,让数据可观测,知道数据什么时候发生变化
export class Observer {
constructor(public value: any, public shallow = false, public mock = false) {
/*
* 给 value 新增一个 __ob__ 属性,值是该 value 的 observer 实例
* 相当于给 vaue 打上标记,表示它已经被转化成响应式的了,避免重复
*/
def(value, '__ob__', this)
// 接下来处理特殊情况
// 如果 value 是数组
if (isArray(value)) {
this.observeArray(value)
}
// 当value是对象时,遍历所有属性,逐个将其变成 getter/setter
else {
const keys = Object.keys(value)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
defineReactive(value, key, NO_INITIAL_VALUE, undefined, shallow, mock)
}
}
}
// 当 value 是数组时,遍历所有属性,逐个将其变成 getter/setter
observeArray(value: any[]) {
for (let i = 0, l = value.length; i < l; i++) {
observe(value[i], false, this.mock)
}
}
}
// 当 value 是数组时,在观察者对象上创建观察者实例
export function observe(/*...*/): Observer | void {
// 已有观察者实例
if (value && hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
return value.__ob__
}
// 在特定情况下,创建观察者实例
if (//...) {
return new Observer(value, shallow, ssrMockReactivity)
}
}
/*
* 关键代码:使一个对象转化成可观测对象
*/
export function defineReactive(/*...*/) {
/*
* 核心中的核心
*/
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
// 处理 getter...
},
set: function reactiveSetter(newVal) {
// 处理 setter...
}
})
}
在上面的代码中:
- 我们定义了
observer类,它用来将一个正常的object转换成可观测的object。 - 并且给
value新增一个ob属性,值为该value的Observer实例。这个操作相当于为value打上标记,表示它已经被转化成响应式了,避免重复操作。 - 然后判断数据的类型,只有
object类型的数据才会调用walk将每一个属性转换成getter/setter的形式来侦测变化。 - 最后,在
defineReactive中当传入的属性值还是一个object时使用new observer(val)来递归子属性,这样我们就可以把obj中的所有属性(包括子属性)都转换成getter/seter的形式来侦测变化。 也就是说,只要我们将一个object传到observer中,那么这个object就会变成可观测的、响应式的object。
通过 Object.defineProperty 实现数据代理,_data 对 data 进行数据劫持
3、收集依赖
收集依赖
D:\Projects\good-projects\vue\src\core\observer\dep.ts
关键在于:dep 类
能够记录与当前数据存在依赖关系的所有地方,并在当前数据发生改变时,通知视图更新这些地方
提出问题
-
什么是依赖关系?怎么收集存储依赖关系?
-
怎么用代码表示“用到该数据的地方”?并且如何通知更新?
什么是依赖关系
我们把"谁用到了这个数据"称为"谁依赖了这个数据"
怎么进行依赖收集
我们给每个数据都建一个依赖数组(因为一个数据可能被多处使用),谁依赖了这个数据(即谁用到了这个数据)我们就把谁放入这个依赖数组中,那么当这个数据发生变化的时候,我们就去它对应的依赖数组中,把每个依赖都通知一遍,告诉他们:"你们依赖的数据变啦,你们该更新啦!"。
依赖管理器**Dep**类
// dep 是一个依赖管理器,负责管理某个数据的依赖数据
export default class Dep {
constructor() {
this.id = uid++
// subs 数据用于存放依赖
this.subs = []
}
// 新增依赖
addSub(sub: DepTarget) {
this.subs.push(sub)
}
// 删除依赖,并在下一次程序冲洗时清空
removeSub(sub: DepTarget) {
this.subs[this.subs.indexOf(sub)] = null
// ...
}
// 添加一个依赖
depend(info?: DebuggerEventExtraInfo) {
if (Dep.target) {
Dep.target.addDep(this)
// ...
}
}
// 通知所有依赖更新
notify(info?: DebuggerEventExtraInfo) {
// stabilize the subscriber list first
const subs = this.subs.filter(s => s) as DepTarget[]
if (__DEV__ && !config.async) {
// 排序
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
const sub = subs[i]
// ...更新
sub.update()
}
}
}
怎么应用 dep,什么时候收集,什么时候响应
- 在 getter 中收集依赖
- 在 setter 中通知依赖更新
在 defineReactive ****里应用 dep
export function defineReactive() {
const dep = new Dep()
// 获取对象的自有属性描述符(非原型继承)
const property = Object.getOwnPropertyDescriptor(obj, key)
// 跳过不可配置属性
if (property && property.configurable === false) {
return
}
// 考虑预定义的 getter/setter
const getter = property && property.get
const setter = property && property.set
if (
(!getter || setter) &&
(val === NO_INITIAL_VALUE || arguments.length === 2)
) {
val = obj[key]
}
// shallow:当前是否为浅层属性
// true:则返回当前属性值
// false:则遍历当前属性,为子属性进行观察
let childOb = shallow ? val && val.__ob__ : observe(val, false, mock)
// ...
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val
// 处理 getter
// 核心代码,收集依赖
dep.depend()
},
set: function reactiveSetter(newVal) {
const value = getter ? getter.call(obj) : val
// 处理 setter
// 核心代码,通知依赖更新
dep.notify()
}
})
return dep
}
参数解释
window.target
在 Vue.js 的源码中,window.target 是一个全局变量,用于在依赖收集过程中标记当前正在被观察的观察者(Watcher)实例。这个机制是 Vue 响应式系统的一部分,用于确保当访问响应式数据时,能够追踪到哪些观察者依赖于这些数据。
这种使用全局变量的方式在多线程环境中可能会遇到问题,因为它假设在任何给定时刻只有一个观察者在执行。然而,在单线程的 JavaScript 环境nn中,这通常是安全的,因为 Vue 保证在任何时刻只有一个观察者实例在运行
在 Vue 3.x 中,这个功能已经被 TargetStack 替代,这是一个更安全和更健壮的实现,它使用一个栈来管理当前的观察者,而不是依赖全局变量。这种方式可以更好地处理嵌套的观察者和异步操作,确保依赖收集的准确性。
dep.target
Dep.target 的作用与 window.target 类似,但它是作为 Dep 类的一个静态属性存在的,而不是全局变量。
在 Vue.js 的响应式系统中,每个响应式数据属性都会有一个 Dep 实例与之关联。当一个观察者(Watcher)访问某个响应式数据属性时,会触发该属性的 get 访问器,进而调用 Dep 实例的 depend 方法。在这个方法中,Dep.target 被用来检查是否有当前正在执行的观察者,如果有,就将这个观察者添加到依赖列表中。
使用 Dep.target 而不是 window.target 的好处是,它可以避免全局变量可能带来的问题,并且可以更好地支持多个观察者。在 Vue 3.x 中,由于使用了 Proxy 来实现响应式系统,Dep.target 被用来在 Proxy 的 get 和 set 陷阱(trap)函数中追踪依赖和触发更新。
4、通知更新
数据侦测
D:\Projects\good-projects\vue\src\core\observer\watcher.ts
关键在于:watcher 类
怎么用代码表示“用到该数据的地方”?并且如何通知更新?
其实在Vue中还实现了一个叫做Watcher的类,而Watcher类的实例就是我们上面所说的那个"谁"。换句话说就是:谁用到了数据,谁就是依赖,我们就为谁创建一个Watcher实例。在之后数据变化时,我们不直接去通知依赖更新,而是通知依赖对应的Watch实例,由Watcher实例去通知真正的视图。
想想 watcher 的实际使用场景
vm.$watch('a.b.c', function (newVal, oldVal) {
// do something
})
当 data.a.b.c 发生变化时,调用函数
- 具体实现:将当前的 watch 实例加在 data.a.b.c 数据上,一旦数据进行更新时,会通知watcher,并触发参数中的回调函数
// watcher 类是实现 depTarget 接口
export default class Watcher implements DepTarget {
constructor(vm,expOrFn,cb/*...*/) {
this.vm = vm;this.cb = cb;
this.getter = parsePath(expOrFn);
this.value = this.get();
}
// 把数据添加到对应的依赖管理中
get() {
pushTarget(this)
const vm = this.vm
value = this.getter.call(vm, vm)
return value
}
// dep.depend 中调用该方法,把 dep 加入列表中
addDep(dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
// 当依赖改变时,会触发该方法
update() {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
}
// 将路径传入,例如:data{a{b{c:1}}}
// 参数 path = "a.b.c"; obj = data
export function parsePath(path: string): any {
if (bailRE.test(path)) {
return
}
// segments = [a,b,c]
const segments = path.split('.')
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]]
}
// 逐层取值,最后返回 1
return obj
}
}
总结:
1.observer类
会把对应的数值转换成 getter/setter 的形式
对于普通的数值,用 def(value, 'ob', this)
- 在def 中用 Object.defineproperty 进行转换
- !!!Vue.js 的响应式系统主要针对对象属性进行数据侦测,而对于基本数据类型(如字符串、数字、布尔值等),Vue 并不使用
Object.defineProperty来追踪其变化。这是因为基本数据类型的属性是不可枚举的,而且它们没有对象属性那样的 getter 和 setter 可以被劫持。对于数组,用 this.observeArray(value),
- 在observeArray中遍历数组,逐个调用observe(value[i], false, this.mock)
- 在 observe 中 return new Observer(value, shallow, ssrMockReactivity),实现递归
对于对象,用 defineReactive(value, key, NO_INITIAL_VALUE, undefined, shallow, mock)
在defineReactive 里
会新建一个依赖数组 dep[]
对于对象,用 observe(val, false, mock)
- 在 observe 中 return new Observer(value, shallow, ssrMockReactivity),实现递归
对于本层次数值,用Object.defineproperty
getter 部分:调用 dep.depend
Setter 部分:调用 dep.notify
2.dep 类
DepTarget 是一个继承自 DebuggerOptions 的接口,
全局创建一个 Dep.target
在 constructor 中新建一个 this.subs 数组存放依赖
addSub:新增依赖,this.subs.push(sub)
removeSub:删除依赖,置空:this.subs[this.subs.indexOf(sub)] = null
depend:添加依赖,Dep.target.addDep(this)
- 调用全局变量 Dep.target 实现类 watcher 中的 addDep 方法
notify:通知依赖更新
- 遍历 this.subs 里的所有依赖,执行sub.update()
3.watcher 类
watcher 类是 depTarget 接口的实现
Constructor:将对象的路径解析出来 this.getter = parsePath(expOrFn)
- 在 parsePath 中根据对象和路径把对应的值取出来放入 getter 中
get:评估 getter,收集依赖
执行全局的 pushTarget 方法
- 把当前的 DepTarget 添加到全局的依赖管理栈中
- 并赋值给当前全局唯一的 Dep.target 进行观察
返回评估后的当前值
addDep:新增依赖
- 把该 dep.id 加入 newDepIds/newDeps 中 watch
- 调用 dep 类中 dep.addSub(this),把 dep 加入依赖列表
update:当依赖改变时,触发该方法
- 如果是异步,执行 this.run()
- 如果是同步,执行 scheduler.ts 中的 queueWatcher,将这个 watcher 实例放入 watcher 队列中
run:核心代码是 this.cb.call(this.vm, value, oldValue),执行 watcher 参数对应的回调函数
初始化过程和数据被修改后的过程
-
初始化过程:
-
实例化Vue
-
调用defineReactive方法监听对象中的数据
-
Watcher构造函数被调用
-
触发被监听数据的get方法
-
Dep收集到依赖
-
-
数据被修改后的过程:
-
数据被修改
-
触发被监听数据的set方法
-
调用dep.notify方法
-
触发已经收集到subs数组中的每一个依赖的update方法(定义在watcher中)
-
视图更新
-
5、新的问题
怎么知道观测值变了呢?
getter/setter 是配置对象的属性,本质上问的是什么时候调用 object 的 getter/setter,不是 vue 中的语法
但是现在 vue 重新实现了对观测对象 getter/setter 的调用,要求用 set 方法来新增的属性才会引发调用
-
读?
- .或者[]
-
写?
- 属性值发生变化,直接赋值, vm.myObject.a = 2;vm.myArray[0] = 10
- 数组索引或长度属性变化,改变 .length,vm.myArray.length = 4
- 新属性的添加,vm.newProperty = 'newValue'
// Vue.set 方法或者组件的 $set 方法来添加新属性,这样可以确保新属性是响应式的。
Vue.set(vm, 'newProperty', 'newValue'); // 正确的方式添加新属性
为什么需要这样实现数据侦测
先说目标:
为了实现 vue 的响应式编码,希望达到:
-
数据与页面的实时响应
- 页面上渲染的时候可以用到最新数据
- 数据在发生变化的时候,页面上会相应地更新
Js 中有一个 object.defineproperty 机制,可以辅助实现一种响应式的更新
Object.defineProperty(obj, property, {
enumerable: true,
configurable: true,
get: function() {
// 当 obj[property] 被读取时,do sth
return val
},
set: function(value) {
// 当 obj[property] 被修改时,do sth
notify("用到 value 的地方")
}
})
Vue 把某数据 data 用 object.defineproperty 包装成响应式的数据,以此场景来考虑:
为了实现 “页面上渲染的时候可以用到最新数据”
- 每次读取data,就返回 data。在 getter 里面进行操作,
为了实现“数据在发生变化的时候,页面上会相应地更新”
-
每次更改 data,就更改页面上用到该数据的所有地方。在 setter 里操作
有了一个新问题:页面哪里会用到 data 呢?
为了回答这个问题
-
每次读取 data 时,约等于这个地方用到了 data,于是把这个地方记录下来
-
每次更改 data 时,遍历记录列表,挨个进行更新
又有了一个新问题:记录的到底是什么?“地方”该怎么记录?
实际上
vue 在这种情况都会放置一个 watcher 实例,
watcher 实例里面包含一个回调函数,如果 watcher 实例被触发,会调用回调函数,触发用到的地方进行更新
6、数组的变化侦测
为什么Object数据和Array型数据会有两种不同的变化侦测方式?
这是因为对于Object数据我们使用的是JS提供的对象原型上的方法Object.defineProperty,而这个方法是对象原型上的,所以Array无法使用这个方法,所以我们需要对Array型数据设计一套另外的变化侦测机制。
vue对数组的一套变化侦测
特点在于
getter可以正常触发,正常收集依赖
但是setter里,因为array不是对象,所以无法监测内部数据的变化
- 因此重写数组方法,调用这7个数组方法,约等于数组发生改变,则进行 dep.notify 操作
注意
思考一下,我们不能直接修改 Array.prototype因为这样会污染全局的Array,我们希望 arrayMethods只对 data中的Array 生效。
所以我们只需要把 arrayMethods 赋值给 data 的 proto 上就好了。