共计 5315 个字符,预计需要花费 14 分钟才能阅读完成。
原文链接:https://ssshooter.com/2021-07…
几年来看了不少 Vue 原理的文章,在这些文章的帮忙下,我也屡次尝试本人了解 Vue 的源码,终于,我感觉是时候本人输入一下内容了,心愿能够从不同于其余文章的角度带大家相熟 Vue。
这个专题天然是分多个局部解说 Vue 源码,第一篇就先讲最最经典的 Vue 响应式原理吧!
在正式讲原理之前,我感觉应该首先讲明确上面几个概念吧。
Dep
var Dep = function Dep() {
this.id = uid++
this.subs = []}
Dep 的含意,天然就是 dependency(也就是 依赖,一个计算机领域的名词)。
就像编写 node.js 程序,常会应用 npm 仓库的依赖。在 Vue 中,依赖具体指的是 响应式解决后的数据。前面会提到,响应式解决的要害函数之一是在很多 Vue 原理文章都会提到的 defineReactive
。
Dep 与每个响应式数据绑定后,该响应式数据就会成为一个依赖(名词),上面介绍 Watcher 时会提到,响应式数据可能被 watch、computed、在模板中应用 3 种状况依赖(动词)。
subs
Dep 对象下有一个 subs 属性,是一个数组,很容易猜出,就是 subscriber(订阅者)列表的意思咯。订阅者可能是 watch 函数、computed 函数、视图更新函数。
Watcher
Watcher 是 Dep 里提到的 订阅者(不要和前面的 Observer 观察者搞混)。
因为 Watcher 的性能在于及时响应 Dep 的更新,就像一些 App 的订阅推送,你(Watcher)订阅了某些资讯(Dep),资讯更新时会揭示你浏览。
deps
与 Dep 领有 subs 属性相似,Watcher 对象也有 deps 属性。这样形成了 Watcher 和 Dep 就是一个多对多的关系,相互记录的起因是当一方被革除的时候能够及时更新相干对象。
Watcher 如何产生
下面屡次提到的 watch、computed、渲染模板产生 Watcher,在 Vue 源码里都有扼要易懂的体现:
mountComponent
的vm._watcher = new Watcher(vm, updateComponent, noop);
initComputed
的watchers[key] = new Watcher(vm, getter || noop, noop, computedWatcherOptions)
$watcher
的var watcher = new Watcher(vm, expOrFn, cb, options);
Observer
Observer 是观察者,他 负责递归地察看(或者说是解决)响应式对象(或数组)。在打印出的实例里,能够留神到响应式的对象都会带着一个 __ob__
,这是曾经被察看的证实。观察者没有下面的 Dep 和 Watcher 重要,略微理解下就能够了。
walk
Observer.prototype.walk
是 Observer 初始化时递归解决的外围办法,不过此办法用于解决对象,另外还有 Observer.prototype.observeArray
解决数组。
外围流程
依照下面几个概念的关系,如何搭配,该如何实现数据响应式更新?
首先定下咱们的指标:天然是在数据更新时,主动刷新视图,显示最新的数据。
这就是下面提到的 Dep 和 Watcher 的关系,数据是 Dep,而 Watcher 触发的是页面渲染函数(这是最重要的 watcher)。
然而新问题随之而来,Dep 怎么晓得有什么 Watcher 依赖于他?
Vue 采纳了一个很有意思的办法:
- 在运行 Watcher 的回调函数前,先记下以后 Watcher 是什么(通过 Dep.target)
- 运行回调函数中用到响应式数据,那么 必然会调用响应式数据的 getter 函数
- 在响应式数据的 getter 函数中就能记下以后的 Watcher,建设 Dep 和 Watcher 的关系
- 之后,在响应式数据更新时,必然会 调用响应式数据的 setter 函数
- 基于之前建设的关系,在 setter 函数中就能触发对应 Watcher 的回调函数了
代码
上述逻辑就在 defineReactive
函数中。这个函数入口不少,这里先讲比拟重要的 observe
函数。
在 observe
函数中会 new Observer 对象,其中应用 Observer.prototype.walk
对对象中的值进行一一响应式解决,应用的就是 defineReactive
函数。
因为 defineReactive
函数太重要了,而且也不长,所以间接贴到这边讲比拟不便。
function defineReactive(obj, key, val, customSetter, shallow) {var dep = new Dep()
depsArray.push({dep, obj, key})
var property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {return}
// cater for pre-defined getter/setters
var getter = property && property.get
var setter = property && property.set
var childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {var value = getter ? getter.call(obj) : val
if (Dep.target) {dep.depend()
if (childOb) {childOb.dep.depend()
if (Array.isArray(value)) {dependArray(value)
}
}
}
return value
},
set: function reactiveSetter(newVal) {var value = getter ? getter.call(obj) : val
// 后半局部诡异的条件是用于判断新旧值都是 NaN 的状况
if (newVal === value || (newVal !== newVal && value !== value)) {return}
// customSetter 用于揭示你设置的值可能存在问题
if ('development' !== 'production' && customSetter) {customSetter()
}
if (setter) {setter.call(obj, newVal)
} else {val = newVal}
childOb = !shallow && observe(newVal)
dep.notify()},
})
}
首先每个响应式的值都是一个“依赖 ”,所以第一步咱们先借闭包的能力给每个值造一个 Dep。(到 Vue 3 就不须要闭包啦)
接着看外围的三个参数:
- obj 以后须要响应式解决的值所在的对象
- key 值的 key
- val 以后的值
这个值还可能之前就定义了本人的 getter、setter,所以在做 Vue 的响应式解决时先解决本来的 getter、setter。
下面在外围流程中提到在 getter 函数会建设 Dep 和 Watcher 的关系,具体来说依附的是 dep.depend()
。
上面贴一下 Dep
和 Watcher
相互调用的几个办法:
Dep.prototype.depend = function depend() {if (Dep.target) {Dep.target.addDep(this)
}
}
Watcher.prototype.addDep = function addDep(dep) {
var id = dep.id
if (!this.newDepIds.has(id)) {this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {dep.addSub(this)
}
}
}
Dep.prototype.addSub = function addSub(sub) {this.subs.push(sub)
}
通过这几个函数,能够领略到了 Dep
和 Watcher
盘根错节的关系……不过看起来曲折,简略来说,其实做的就是下面说的相互增加到多对多列表。
你能够在 Dep 的 subs 找到所有订阅同一个 Dep 的 Watcher,也能够在 Watcher 的 deps 找到所有该 Watcher 订阅的所有 Dep。
然而外面还有一个暗藏问题,就是 Dep.target
怎么来呢?先放一放,后会作出解答。先接着看看 setter 函数,其中的要害是 dep.notify()
。
Dep.prototype.notify = function notify() {
// stabilize the subscriber list first
var subs = this.subs.slice()
for (var i = 0, l = subs.length; i < l; i++) {subs[i].update()}
}
不难理解,就是 Dep 揭示他的订阅者列表(subs)里的所有人更新,所谓订阅者都是 Watcher,subs[i].update()
调用的也就是 Watcher.prototype.update
。
那么来看一下 Watcher 的 update
做了什么——
Watcher.prototype.update = function update() {if (this.lazy) {this.dirty = true} else if (this.sync) {this.run()
} else {queueWatcher(this)
}
}
在这里我感觉有两个点比拟值得开展,所以挖点坑 😂
- 坑 1:这里如果不是同步更新的话会跑到 queueWatcher,之后再来讲异步更新,同时也升高了这里的了解难度,总之晓得 queueWatcher 在一顿操作之后还是会运行 run 就好了
- 坑 2:Watcher 的 cb 函数可能会解决 watch、computed 和 组件更新函数。尤其重要的是组件更新函数,也正在这里进行 Vue 页面更新,所以这里也值得开展,为升高了解难度,只有晓得更新在这里触发即可,更新办法前面再说
- 坑 3:能够看到 lazy 时其实没有运行上面的步骤只会标记数据更新过,在下次取值再计算新的值
Watcher.prototype.run = function run() {if (this.active) {var value = this.get()
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
var oldValue = this.value
this.value = value
if (this.user) {
try {this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(
e,
this.vm,
'callback for watcher"' + this.expression + '"'
)
}
} else {this.cb.call(this.vm, value, oldValue)
}
}
}
}
这段代码的重点在于须要 当初 get 办法中对 Dep.target 进行了设置。
因为只有 Dep.target
存在,之后在回调函数 cb(例如页面渲染函数就是一个典型的 Watcher cb)调用时,Dep.prototype.depend
能力真正失效。再之后的逻辑,就回到应用响应式数据的取值,所有都连起来了!造成闭环(滑稽)!这就是下面 depend()
遗留问题的答案。
总结
- Dep 与数据关联,代表数据能够成为依赖
- Watcher 与 watch、computed、渲染函数关联,代表这些函数能够成为依赖的订阅者
- Observer 算是一个解决 Dep 的入口,递归解决响应式数据
- Watcher 的回调函数在应用响应式数据时,会先设置
Dep.target
- 响应式数据在 getter 函数中通过
Dep.target
得悉调用者,并与调用者建设订阅者和依赖的关系 - 响应式数据在 setter 函数中遍历 subs 告诉所有订阅者该数据更新
- 当订阅者为视图更新函数(
updateComponent
->_update
)时,用户就能在响应式数据更新时看到页面更新,从而实现响应式更新成果
虽说粗略来说这个算法并不难理解,但实际上还有许多其余机制与这个算法一起合作,组成残缺的 Vue。例如下面挖的坑:更新队列和组件更新的函数自身的实现,都值得学习。
另外还有代码里还有更多小细节,这就留下感兴趣的大家本人钻研啦。
PS. 因为我的表达能力切实不算好,再加上常识的咒骂,不确定这篇文字是否能真的讲清楚 Vue 响应式原理,如果有什么看不懂的中央请在评论区提出,谢谢大家 💡
惟一参考链接:Vue.js