目录
初始化
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) {
// 在原型上添加 _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()
props
列表,proxy()
把属性代理到当前实例上vm._props.xx
变成 vm.xx
props
列表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)
proxy()
把属性代理到当前实例上this.xx
proxy()
把 data 里的每一个属性都代理到当前实例上,就可以通过 this.xx
访问了observe
监听整个 dataif (!isReserved(key)) {
// 都不重名的情况下,代理到 vm 上
// 可以让 vm._data.xx 通过 vm.xx 访问
proxy(vm, `_data`, key)
Virtual DOM节点(vnode):
vnode
对象通常用于表示虚拟DOM树的节点,而不是真实的数据对象。这些节点描述了组件的结构,而不是数据的值。Vue的响应式系统是建立在对象的引用类型(如Object、Array)
基本数据类型(如Number、String、Boolean)或null等,它们是不可变的,无法被Vue追踪到变化
用 this.msg = 'xxx'
能触发 setter
派发更新,但是我们修改数组并不是用 this.arr = xxx
,而是用 this.arr.push(xxx)
等修改数组的方法
var obj = {}; //定义一个空对象
Object.defineProperty(obj, 'val', {//定义要修改对象的属性
get: function () {
console.log('获取对象的值')
},
set: function (newVal) {
console.log('设置对象的值:最新的值是'+newVal);
}
});
obj.hello = 'hello world'
Watcher
),然后调用 dep.notify() 派发更新渲染watcher掌管当前组件的视图更新
Dep.target
赋值为当前渲染 watcher
并压入栈targetStack(为了恢复用)3.vm._render()
生成并渲染 vnode
vm 实例,也就是平常用的 this
每个响应式数据都有一个Dep
来管理它的一个/多个依赖
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;
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]
}
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()
}
}
}
优化:在每次数据改变的时候不会都触发 watcher 回调,而是把这些 watcher 都添加到一个队列里,然后在 nextTick 后才执行(下次 DOM 更新循环结束之后,执行延迟回调,就可以拿到更新后的 DOM 相关信息)
1.如果同一个 watcher
被多次触发,只会被推入到更新队列中一次,可以避免重复修改相同的dom,这种去除重复数据,对于避免不必要的计算和 DOM 操作是非常重要的
2.同步任务执行完毕
3.开始执行异步 watcher 队列的任务,一次性更新 DOM
// 定义watcher类
class Watcher {
update() {
// 放到watcher队列中,异步更新
queueWatcher(this);
}
// 触发更新
run() {
this.get();
}
}
// 队列中添加watcher
function queueWatcher(watcher) {
const id = watcher.id;
// 先判断watcher是否存在 去掉重复的watcher
if (!has[id]) {
queue.push(watcher);
has[id] = true;
if (!pending) {
pending = true;
// 使用异步更新watcher
nextTick(flushSchedulerQueue);
}
}
}
let queue = []; // 定义watcher队列
let has = {}; // 使用对象来保存id,进行去重操作
let pending = false; // 如果异步队列正在执行,将不会再次执行
// 执行watcher队列的任务
function flushSchedulerQueue() {
queue.forEach((watcher) => {
watcher.run();
if (watcher.options.render) {
// 在更新之后执行对应的回调: 这里是updated钩子函数
watcher.cb();
}
});
// 执行完成后清空队列 重置pending状态
queue = [];
has = {};
pending = false;
}
深入浅出 Vue 响应式原理源码剖析 - 掘金
纯干货!图解Vue响应式原理 - 掘金
Vue3响应式原理-CSDN博客