目录
初始化
initProps():父组件传的 props 列表,proxy() 把属性代理到当前实例上
vm._props.xx 变成 vm.xx
initData():判断data和props、methods是否重名,proxy() 把属性代理到当前实例上
this.xx
observe():给数据加上监听器(除了vnode/非引用类型(object/array))
Observe:标记响应式、分类(defineReactive()递归监听、observe()监听)
defineReactive():定义响应式对象
依赖收集
1.挂载前 生成一个组件渲染watcher
2. Dep.target 赋值为当前渲染 watcher 并压入栈targetStack(为了恢复用)
3.vm._render() 生成并渲染 vnode
4.访问数据,触发getter,dep收集watcher
5.更新数据,触发setter,遍历通知所有watcher更新
Dep类:管理Watcher
subs: Array
static target: ?Watcher:全局的 Watcher,同一时间只能存在一个全局的 Watcher
Watcher类:依赖/观察者/订阅者
watcher.run() :执行回调,传旧值新值
新旧Dep实例数组
派发更新
queueWatcher()优化:watcher队列,nextTick后执行
依赖的渲染结果:父watcher在子前,user watcher在渲染watcher前
defineProperty 缺陷处理
属性的增删无法触发 setter :Vue.set() 增属性
不能检测到数组元素的变化:重写数组方法,把原本的 push 保存起来,再做响应式处理
初始化
在 new Vue 初始化的时候,会对组件的数据 props 和 data 进行初始化
export function initMixin (Vue: Class<Component>) {
// 在原型上添加 _init 方法
Vue.prototype._init = function (options?: Object) {
...
vm._self = vm
initLifecycle(vm) // 初始化实例的属性、数据:$parent, $children, $refs, $root, _watcher...等
initEvents(vm) // 初始化事件:$on, $off, $emit, $once
initRender(vm) // 初始化渲染: render, mixin(混入,data,methods...)
callHook(vm, 'beforeCreate') // 调用生命周期钩子函数
initInjections(vm) // 初始化 inject(子孙传值)
initState(vm) // 初始化组件数据:props, data, methods, watch, computed
initProvide(vm) // 初始化 provide
callHook(vm, 'created') // 调用生命周期钩子函数
...
}
}
命名前缀
$
:公共属性
_
:私有属性
响应式数据相关: initProps()
、initData()
、observe()
initProps():父组件传的 props
列表,proxy()
把属性代理到当前实例上
vm._props.xx
变成 vm.xx
- 遍历父组件传进来的
props
列表 - 校验每个属性的命名、类型、default 属性等,都没有问题就调用
defineReactive
设置成响应式 - 然后用
proxy()
把属性代理到当前实例上,如把vm._props.xx
变成vm.xx
,就可以访问
// 把不在默认 vm 上的属性,代理到实例上
// 可以让 vm._props.xx 通过 vm.xx 访问
if (!(key in vm)) {
proxy(vm, _props, key)
}
//vue自定义的proxy 函数,将 key 代理到组件实例上。这意味着你可以直接通过 vm.key 访问这个属性,而不必使用 vm._props.key
区别于js中的new Proxy(target, handler)
initData():判断data和props、methods是否重名,proxy()
把属性代理到当前实例上
this.xx
- 初始化一个 data,并拿到 keys 集合
- 遍历 keys 集合,来判断有没有和 props 里的属性名或者 methods 里的方法名重名的
- 没有问题就通过
proxy()
把 data 里的每一个属性都代理到当前实例上,就可以通过this.xx
访问了 - 最后再调用
observe
监听整个 data
if (!isReserved(key)) {
// 都不重名的情况下,代理到 vm 上
// 可以让 vm._data.xx 通过 vm.xx 访问
proxy(vm, `_data`, key)
observe():给数据加上监听器(除了vnode/非引用类型(object/array))
- 如果是 vnode 的对象类型或者不是引用类型,就直接跳出
- 否则就给没有添加 Observer 的数据添加一个 Observer,也就是监听者
Virtual DOM节点(vnode):
vnode
对象通常用于表示虚拟DOM树的节点,而不是真实的数据对象。这些节点描述了组件的结构,而不是数据的值。Vue的响应式系统是建立在对象的引用类型(如Object、Array)
基本数据类型(如Number、String、Boolean)或null等,它们是不可变的,无法被Vue追踪到变化
Observe:标记响应式、分类(defineReactive()递归监听、observe()监听)
- 给当前 value 打上已经是响应式属性的标记,避免重复操作
- 然后判断数据类型
- 如果是对象,就遍历对象,调用 defineReactive()创建响应式对象
- 如果是数组,就遍历数组,调用 observe()对每一个元素进行监听
用 this.msg = 'xxx'
能触发 setter
派发更新,但是我们修改数组并不是用 this.arr = xxx
,而是用 this.arr.push(xxx)
等修改数组的方法
defineReactive():定义响应式对象
var obj = {}; //定义一个空对象
Object.defineProperty(obj, 'val', {//定义要修改对象的属性
get: function () {
console.log('获取对象的值')
},
set: function (newVal) {
console.log('设置对象的值:最新的值是'+newVal);
}
});
obj.hello = 'hello world'
- 先初始化一个 dep 实例
- 如果是对象就调用 observe,递归监听,以保证不管结构嵌套多深,都能变成响应式对象
- 然后调用 Object.defineProperty() 劫持对象属性的 getter 和 getter
- 如果获取时,触发 getter 会调用 dep.depend() 把观察者 push 到依赖的数组 subs 里去,也就是依赖收集
- 如果更新时,触发 setter 会做以下操作
- 新值没有变化或者没有 setter 属性的直接跳出
- 如果新值是对象就调用 observe() 递归监听
- 通过对应的所有依赖(
Watcher
),然后调用 dep.notify() 派发更新
依赖收集
1.挂载前 生成一个组件渲染watcher
渲染watcher掌管当前组件的视图更新
2. Dep.target
赋值为当前渲染 watcher
并压入栈targetStack(为了恢复用)
3.vm._render()
生成并渲染 vnode
vm 实例,也就是平常用的 this
4.访问数据,触发getter,dep收集watcher
5.更新数据,触发setter,遍历通知所有watcher更新
每个响应式数据都有一个Dep
来管理它的一个/多个依赖
Dep类:管理Watcher
subs: Array<Watcher>
static target: ?Watcher:全局的 Watcher,同一时间只能存在一个全局的 Watcher
dep.target
的作用是建立依赖关系和追踪数据的Watcher
因为更新异步的特性,如果同时有多个全局 Watcher
在同一时间被触发,可能导致不可预测的结果,甚至可能引发性能问题。
通过在全局只维护一个 dep.target
,Vue 确保在任何时刻只有一个 Watcher
在执行更新操作,避免了潜在的竞争条件和性能问题。
-
在
Watcher
对象被创建:当你创建一个Watcher
对象,它会将自身设置为当前的dep.target
。这是因为该Watcher
正在计算或依赖于响应式数据,因此需要建立依赖关系。 -
在计算属性的求值过程中:如果你有一个计算属性(
computed
),当该计算属性的值被求值时,Vue 会将当前的dep.target
设置为该计算属性的Watcher
,以建立依赖关系。 -
在渲染过程中:当组件渲染时,Vue 会创建一个渲染组件的
Watcher
,该Watcher
负责渲染组件的模板。在渲染过程中,当前的dep.target
会被设置为渲染Watcher
,以确保建立正确的依赖关系。
let uid = 0
export default class Dep {
static target: ?Watcher;//可选属性可以不存在或者是 null 或 undefined
subs: Array<Watcher>;
id: number;
constructor () {
this.id = uid++//确保每个 Dep 实例具有唯一的标识符
this.subs = []
}
...
depend () {
if (Dep.target) {
// 调用 Watcher 的 addDep 函数
Dep.target.addDep(this)
}
}
// 派发更新
notify () {
...
}
}
// 同一时间只有一个观察者使用,赋值观察者
Dep.target = null
const targetStack = []//管理当前活动的观察者的栈
export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
Watcher类:依赖/观察者/订阅者
watcher.run() :执行回调,传旧值新值
新旧Dep实例数组
let uid = 0
export default class Watcher {
...
constructor (
vm: Component,
...
isRenderWatcher?: boolean
) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
// Watcher 实例持有的 Dep 实例的数组
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
...
}
get ()
// 该函数用于缓存 Watcher
// 因为在组件含有嵌套组件的情况下,需要恢复父组件的 Watcher
pushTarget(this)
let value
const vm = this.vm
try {
// 调用回调函数,也就是upcateComponent,对需要双向绑定的对象求值,从而触发依赖收集
value = this.getter.call(vm, vm)
} catch (e) {
...
} finally {
// 深度监听
if (this.deep) {
traverse(value)
}
// 恢复Watcher
popTarget()
// 清理不需要了的依赖
this.cleanupDeps()
}
return value
}
// 依赖收集时调用
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)) {
// 把当前 Watcher push 进数组
dep.addSub(this)
}
}
}
// 清理不需要的依赖(下面有)
cleanupDeps () {
...
}
// 派发更新时调用(下面有)
update () {
...
}
// 执行 watcher 的回调
run () {
...
}
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
}
派发更新
queueWatcher()优化:watcher队列,nextTick后执行
优化:在每次数据改变的时候不会都触发 watcher 回调,而是把这些 watcher 都添加到一个队列里,然后在 nextTick 后才执行(下次 DOM 更新循环结束之后,执行延迟回调,就可以拿到更新后的 DOM 相关信息)
依赖的渲染结果:父watcher在子前,user watcher在渲染watcher前
defineProperty 缺陷处理
属性的增删无法触发 setter :Vue.set() 增属性
不能检测到数组元素的变化:重写数组方法,把原本的 push 保存起来,再做响应式处理
深入浅出 Vue 响应式原理源码剖析 - 掘金
纯干货!图解Vue响应式原理 - 掘金