这是一个Vue源码学习系列。打算开个手写Vue的坑,但愿能在写代码的同时能把其中的细节讲清楚,最终目的是实现一个简版的vue。不知道本身能写到哪一步,总之尽力而为。若是能完成的话,应该是自我超越了和无限的自信了。放个仓库的 传送门。javascript
场景:最近由于快到暑假了,产品韩梅梅提早一个月在大象拉了一个需求群,把研发李雷、小明拉进了群。她跟两位研发说,“咱们过几天要开发一个新需求,须要两位研发的支持,是关于暑假欢乐谷门票活动的需求,等产品逻辑梳理完,我们就进入开发。”。一个礼拜以后,韩梅梅在群里发出了需求文档,两位研发开始加班加点干活,需求完美上线。html
颇费特~ 好的,观察者模式讲完了。vue
纳尼?等等...等等...,您这是讲了个啥。java
咳咳...很差意思,从新来。react
咱们看一下上面这个场景,它分了几步git
总结下来,咱们发现有两个角色,一个发布者(产品韩梅梅),一个观察者(李雷等研发),当发布者的状态更新后,会进行通知观察者,观察者开始执行对应的动做。github
ok,让咱们试着写一下面试
class Dep {
constructor(state) {
this.watchers = []
this.state = state
}
// 添加观察者(研发)
add(watcher) {
!this.watchers.includes(watcher) && this.watchers.push(watcher)
}
// 移除观察者(研发)
remove(watcher) {
let index = this.watcher.indexOf(watcher)
if (~index) this.watcher.splice(index, 1)
}
// 状态更新, 通知所有观察者
notify() {
for (let watcher of this.watchers) watcher.update(this)
}
}
class Watcher {
constructor(value) {
this.value = value
}
// 更新
update() {
console.log('开始开发!')
}
}
const HanMeiMei = new Dep()
const XiaoMing = new Watcher()
const LiLei = new Watcher()
// 拉群!
HanMeiMei.add(XiaoMing)
HanMeiMei.add(LiLei)
await new Promise(resolve => setTimeout(resolve, 7 * 24 * 60 * 60 * 1000, '一周过去了')))
// 过了一周开始通知研发开发
HanMeiMei.notify()
复制代码
问:那若是换成Vue中的视图与数据之间的关系呢?哪一个是个发布者,哪一个是观察者。segmentfault
答:显而易见,数据是发布者,视图是观察者。当数据改变时,会通知视图,视图从新进行渲染。数组
这里有几个问题
ok,带着这些问题我们继续往下看
首先,明确一点,Vue实例中的响应数据,几乎所有都来源于data,就是那个Option API中的data。不论是props,computed这些都是基于data的。
其次,Vue中的Watcher分为了三种,
因此,what的答案就有了 数据收集了这三种观察者
说个面试的段子,面试官:vue怎么收集依赖的?
这个其实老生常谈,getter/setter的存储器嘛
诶,那你知道Array是怎么收集的吗?
知道知道,不就是hack的一些原生方法嘛
哦,那为何要hack呢,咋hack的呢,hack了哪些呢,不一样的方法之间又都是怎么处理的数据呢?
......
好了,回去等通知吧(一面挂)
这里信息量太大!关于为何要hack方法,尤大是给出了回答的,主要缘由是由于性能和使用方便之间的取舍,这篇文章有写道:segmentfault.com/a/119000001…
可是你要问我为啥数组附个值还能跟性能扯上关系,咱也不懂,咱也不敢问。
在vue实例化的时候,在beforeCreate和create之间,会有一个初始化数据的过程,这里会将data、computed所有初始化好,经过getter,哪里用到就在哪里收集观察者。
先从转换数据开始,咱们来简单实现一个,很简单就是迭代加递归,两个函数搞定。
// 咱们先来实现第一个函数observe
function observe(data) {
if (typeof data !== 'object') return
for (let key of Object.keys(data)) {
defineReactive(data, key)
}
return data
}
// 而后是defineReactive
function defineReactive(data, key) {
let val = data[key]
const dep = new Dep()
observe(val)
Object.defineProperty(data, key, {
configurable: false,
enumerable: true,
get() {
dep.depend()
return val
},
set(newVal) {
if (val === newVal) return
val = newVal
observe(val)
dep.notify()
}
})
}
复制代码
这里的逻辑很简单就是经过迭代+递归,将全部值都改成存取器。
这里注意defineReactive方法,我并无直接把data[key] 的这个value经过参数传进去,而是在函数内部取值,之因此为何作,这里先留一个悬念
。
这里出现了一个class Dep,这里其实Dep就是来收集Watcher的。
好的,咱们继续来实现Dep
class Dep {
constructor() {
this.watchers = new Set()
}
depend() {
if (Dep.Target) this.watchers.add(Dep.Target)
}
notify() {
let watchers = this.watchers
for (let watcher of watchers) {
watcher.update()
}
}
}
复制代码
这里Dep的实现也很简单,就是收集watchers
,使用Set
确保watcher
的惟一。
可是!这里又双叒出现了一个新的东西,Dep.Target
。这东西是个啥,其实看代码也能差很少发现,Dep.Target
确定是个Watcher
实例。
诶~,这么多Watcher实例它究竟是哪一个呢?
好问题!咱们先想一想一个场景,咱们有个数据好比是data
,咱们还有个渲染函数
,而后呢~这个渲染函数用到了这个data。
用到data了确定就会触发data的getter,从而收集依赖,那咱们要收集的依赖确定就是这个渲染函数
了。
相应的Dep.Target的值也就是这个渲染函数
。
哒嘎!
你觉得这样就结束了吗,No,No,No,嘛哒哒!
要是渲染函数里面还有个渲染函数咋整。
纳尼!还有这种操做吗!
有的,并且不少,当咱们组件里面嵌入了组件的时候就会出现。
我去,那不是很常见吗!那可怎么办。
别慌,咱们只要实现一个栈,有新的函数要执行,咱们就push进来,当函数执行结束,给他pop出去就行了。
ok,那咱们开始实现一下。
Dep.Target = null
const watcherStack = []
// 入栈
function pushTarget(watcher) {
Dep.Target = watcher
watcherStack.push(watcher)
}
// 出栈
function popTarget() {
watcherStack.pop()
Dep.Target = watcherStack[watcherStack.length - 1]
}
复制代码
完美解决~ 那么最后剩下的的就是watcher的实现了。
上面说过,watcher一共有三种,咱们先实现最简单、最基础的renderWatcher。
class Watcher {
constructor(getter) {
this.getter = getter
this.value = undefined
this.value = this.get()
}
get() {
pushTarget(this)
this.getter()
popTarget()
}
update() {
this.value = this.get()
}
}
复制代码
这里的逻辑很简单,参数getter
就是要执行的函数。对于RenderWatcher
来讲getter
就是渲染函数。 好的!万事具有,咱们来试着跑个例子。
<body>
<div id="app"></div>
<script src="./reactive/reactive.js"></script>
<script> const data = observe({ age: 12, name: 'Sunyanzhe' }) // 渲染函数 function renderFunction() { document.querySelector('#app').innerHTML = `我叫${data.name}, 我${data.age}岁` } // renderWatcher const renderWatcher = new Watcher(renderFunction) setTimeout(() => { data.age = 25 }, 2000) </script>
</body>
复制代码
来,咱们捋一下流程:
getter/setter
new Watcher
的时候,这时候Dep.Target
是renderWatcher
renderFunction
,读取到name
和age
的属性时,触发getter
收集Dep.Target
,也就是renderWatcher
。此时name
和age
中的Dep
实例都存了renderWatcher
setter
,触发age
的Dep
中保存的watcher
的update
方法。此时更新视图。renderFunction
执行,读取到age
时,值为25
。别问为啥两秒,一我的就从12变成25了,经历痛苦会让人瞬间成长😂
总听文章里说,computed有什么懒加载,缓存。那是个什么玩意啊
好说,由于computed实际上是一个getter,是函数就要执行嘛。懒加载的意思就是它何时被用到了,它何时执行这个函数。
那缓存又是什么呢?
也很简单,就是computed中用到的值若是没发生改变的话,它的getter函数不进行计算,而是直接用上一次得出的结果。
ok,先到这里,咱们先捋一捋思路
首先,刚刚说到,computed能够被缓存,当它用到的值没有发生改变时,getter不须要执行。
也就是说computed自己也是要有Dep,用来收集数据。
其次,他是lazy的,因此即便数据发生了改变也不用当即执行函数,获取结果。而是能够等到,何时再次用到这个computed的值再去计算。好比在render函数中用到
最后,computed中的数据改变后不能只通知computed的值须要从新更新,还须要通知用到computed的地方也要进行一次更新
总结下来就是,若是一个render函数中有用到computed,那么computed中的数据更新,不只要通知computed的值要改变,还要告诉render函数进行从新执行。而当render函数从新执行的时候,就会再次获取computed。这时computed才会执行他的getter函数
好了,思路捋清了,咱们实现一下。为此咱们要修改一下以前的Dep和Watcher,而且咱们还要实现一个computed方法。
class Dep {
constructor() {
this.watchers = new Set()
}
// 这里发生了变化
addSub(watcher) {
this.watchers.add(watcher)
}
// 这里发生了变化
depend() {
if (Dep.Target) {
Dep.Target.addDep(this)
}
}
notify() {
let watchers = this.watchers
for (let watcher of watchers) {
watcher.update()
}
}
}
class Watcher {
constructor( getter, options ) {
this.getter = getter
this.deps = new Set()
this.value = undefined
this.lazy = undefined
this.dirty = undefined
if (options) {
this.lazy = !!options.lazy
}
this.dirty = this.lazy
this.value = this.lazy
? undefined
: this.get()
}
get() {
pushTarget(this)
let value = this.getter()
popTarget()
return value
}
addDep(dep) {
dep.addSub(this)
this.deps.add(dep)
}
depend() {
let deps = this.deps
for (let dep of deps) {
dep.depend()
}
}
evalute() {
this.value = this.get()
this.dirty = false
}
update() {
if (this.lazy) {
this.dirty = true
} else {
Promise.resolve().then(() => {
this.run()
})
}
}
run() {
this.value = this.get()
}
}
function computed(computedGetter) {
const options = { lazy: true }
const computedWathcer = new Watcher(computedGetter, options)
const result = {}
Object.defineProperty(result, 'value', {
get() {
if (computedWathcer.dirty) {
computedWathcer.evalute()
}
if (Dep.Target) {
computedWathcer.depend()
}
return computedWathcer.value
}
})
return result
}
复制代码
看到这里确定很晕,不要紧,我们再举一个🌰,结合🌰来看懂这块逻辑。你们目前只须要关注一点,就是update中咱们用了微任务。
ok,先看例子
<body>
<div id="app"></div>
<script src="./reactive/reactive.js"></script>
<script> const data = observe({ age: 12, name: 'Sunyanzhe' }) function renderFunction() { document.querySelector('#app').innerHTML = `我叫${data.name}, 我${data.age}岁,明年${nextYear.value}` } const nextYear = computed(() => data.age + 1) const renderWatcher = new Watcher(renderFunction) setTimeout(() => { data.age = 25 }, 2000) </script>
</body>
复制代码
咱们仍是捋一下执行顺序
dirty
为true
。这个很重要!栈
。此时的栈中有RenderWatcher。栈
中推入了ComputedWatcher。重点来了,RenderWatcher还没执行完!因此目前栈中有两个watcher。computedWatcher的get执行完,出栈,将获得的值赋给value属性。修改dirty属性为false这里为何使用了微任务,是由于执行顺序的问题,咱们的computed的计算必需要在renderWatcher的更新以后,这样才能收集到对应的依赖。在Vue源码中,有一个执行更新的队列,它会将全部的watcher进行排序,避免报错。
其实,watch也很简单,就是加了个callback。
watch比较迷惑的地方其实它的getter是什么,在renderWatcher中,getter是render函数;在computedWatcher中,getter是getter函数;那么watch是什么呢。
其实很简单就是个travers函数,想一想咱们是怎么写watch的
watch: {
prop1(val) {
console.log(val)
}
}
// 转换为
$watch(() => {vm._data.prop1}, console.log)
复制代码
这里面第一个函数是getter,用来收集依赖,第二个就是callback了
那deep呢? deep其实就是深度遍历
废话少说,直接开始实现!
其实很简单,咱们只须要加个callback,找个地方调用一下就行了。
因此咱们就改一下constructor和run这两个
class Watcher {
constructor( getter, options, cb ) {
//...
this.cb = cb
this.user = undefined
if (options) {
this.user = !!options.user
}
// ...
}
run() {
let newValue = this.get()
if (this.user && newValue !== this.value) {
// 调用回调
this.cb(newValue, this.value)
this.value = newValue
}
}
}
function watch(watcheGetter, callback) {
const options = { user: true }
new Watcher(watcheGetter, options, callback)
}
复制代码
就是如此的简单,比computed简单多了~
最后看一下效果
<body>
<div id="app"></div>
<script src="./reactive/reactive.js"></script>
<script> const data = observe({ age: 12, name: 'Sunyanzhe' }) function renderFunction() { document.querySelector('#app').innerHTML = `我叫${data.name}, 我${data.age}岁,明年${nextYear.value}` } const nextYear = computed(() => data.age + 1) const renderWatcher = new Watcher(renderFunction) watch( () => data.name, (val, oldVal) => { console.log('new---', val) console.log('old---', oldVal) }) setTimeout(() => { data.name = 'yanzhe' }, 1000) setTimeout(() => { data.age = 25 }, 2000) </script>
</body>
复制代码
在上文中谈到的为何DefineReactive不传value的缘由,在这个issue中:github.com/vuejs/vue/p…,主要缘由是,数据自己就能够是getter/setter
Vue中的源码思路与本文一致,主要多了边界问题的处理,以及数组的hack,有关数组的处理须要你们去看源码去理解