共计 13286 个字符,预计需要花费 34 分钟才能阅读完成。
前言
很多时候咱们都对 源码
展现出了肯定的渴求,但当被问到到底为什么想看源码时,答案无非也就那么几种:
- 为了面试
- 为了在简历上写本人会源码
- 理解底层原理 学习高手思路
- 通过源码来学习一些小技巧 ( 骚操作)
- 对框架如何实现的各种性能感到好奇
- 内卷重大 不看不行 逆水行舟 逆水行舟
- 本人也想造轮子 先看看他人都是怎么做的
- 各种公众号和卖课的都在贩卖焦虑 被洗脑洗的
但其实很少人会真正的看明确源码,一方面是因为代码量切实是太多了,另一方面则是当咱们浏览他人代码的时候就是容易搞得一头雾水。因为每个人的编码方式以及编码习惯都天壤之别,在看一个编码习惯与本人不同的人的代码时是很累的。
况且不仅是因为每个人的编码格调相差甚远,人与人之间各自善于的技术方向以及技术水平也都是 横看成岭侧成峰
, 远近高下各不同
。刨除掉以上的种种原因之后,更重要的一个起因是很多人框架用的都不够精通呢、用过的API
也就那么几个常见的,其余不罕用但很高阶的 API
都没怎么用过,连用都没用明确呢,这样的人看源码的时候当然会被绕晕啦!
那必定有人会说:尤雨溪他框架就肯定用的很 6 吗?我每天都在用他的框架写代码,他还不肯定有我纯熟呢!
这么说的确有肯定的情理,但如果论底层,他比谁都理解。之所以咱们啃不动源码的很重要的一个起因就是:细枝末节的货色切实是太多了,很容易令大家找不到重点。这些细枝末节的货色天然有它们存在的情理,但它们确成为了咱们行走在钻研源码这条路上的绊脚石。
题外话
怎么学习源码才是最迷信的形式呢?咱们来看一个例子:有一些听起来十分高大上的高科技产品,如 电磁轨道炮
。各个军事强国都在争相摸索这一畛域,假如有一天,咱们一沉睡来成为了 国家电磁轨道炮首席研究员
,是专门负责钻研电磁轨道炮底层技术的。那么当咱们拆解一个电磁轨道炮的时候,大概率你是看不懂它的外部结构的。因为外面会蕴含许多非常复杂的 高强度资料
、 管制磁力的电极
、 笔直波折的电线
、 进步精准度的安装
以及一些 利于使用者操控的封装
等等…
那么此时的你可能就不太容易搞明确 电磁轨道炮的真正原理
,直到有一次在网上偶然间看到一个视频,视频中的人用了一些磁铁、若干钢珠、以及几个咱们日常生活中可能搞到的资料来制作了一个 简易版的电磁轨道炮
。这样咱们一下子就可能搞懂 电磁轨道炮的真正原理
,尽管这样的轨道炮并不能真正的用于实战,但只有咱们明确了最根底的那局部,咱们就能够在此基础上一步步进行扩大,缓缓弄懂整个可能用于实战的简单轨道炮。
源码也是同理,咱们依照
电磁轨道炮
的思路一步步来,先搞清楚最外围的根底局部,缓缓的再一步步去进阶。这样的学习办法比咱们必定一上来就去拆解一个完整版的电磁轨道炮
要强得多
既然咱们有这样的需要,那么作为一个风行框架的作者就必然会有所回应:在一次培训的过程中,尤雨溪
率领大家写了一个十分微型的 Vue3
。不过惋惜这是他在国外办过的为期一天的培训,咱们国内的观众并没有福分可能享受到 被框架作者培训
的这么一次教学。但好在 尤雨溪曾经把代码全副上传到了 codepen 上
,大家能够点击这个链接来浏览尤雨溪亲手写的代码,或者也能够抉择留在本篇文章内,看我来用中文为大家解说 尤雨溪的亲笔代码
!
响应式篇
尤雨溪
在某次直播时曾示意过:Vue3 的源码要比 Vue2 的源码要好学很多
。Vue3
在架构以及模块的耦合关系设计方面比 Vue2
更好,能够一个模块一个模块看,这样比拟容易了解。如果是刚上手,能够从 Reactivity
看起。因为 Reactivity
是整个 Vue3
中跟内部没有任何耦合的一个模块。
Reactivity
就是咱们常说的 响应式
,赫赫有名的React
也是这个意思,不信认真比照一下前五个字母。那么什么是响应式呢?想想看 React
是什么框架?MVVM
对吧?MVVM
的主打口号是:
数据驱动视图!
也就是说当数据产生扭转时咱们会从新渲染一下组件,这样就可能达到一批改数据,页面上用到这个数据的中央就会实时发生变化的成果。不过在数据发生变化时也不仅仅只是可能更新视图,还能够做些别的呢!尤雨溪在创立 @vue/reactivity
这个模块的时候,借鉴的是 @nx-js/observer-util 这个库。咱们来看一眼它在 GitHub
上README.md
里展现的一段示例代码:
import {observable, observe} from '@nx-js/observer-util';
const counter = observable({num: 0});
const countLogger = observe(() => console.log(counter.num));
// 这行代码将会调用 countLogger 这个函数并打印出:1
counter.num++;
是不是很像 Vue3
的reactive
和 watchEffect
啊?其实就是咱们提前定义好一个函数,当函数外面依赖的数据项发生变化时就会主动执行这段函数,这就是响应式!
数据驱动视图那就更容易了解了,既然当数据发生变化时能够执行一段函数,那么这段函数为什么不能够执行一段更新视图的操作呢:
import {store, view} from 'react-easy-state';
const counter = store({
num: 0,
up() {this.num++;}
});
// 这是一个响应式的组件, 当 counter.num 发生变化时会主动从新渲染组件
const UserComp = view(() => <div onClick={counter.up}>{counter.num}</div>);
react-easy-state
是他们 ( 尤雨溪借鉴的那个库 ) 专门针对 React
来进行封装的,不难看出 view
这个函数就是 observe
函数的一个变形,observe
是要你传一个函数进去,你函数外面想执行啥就执行啥。而 view
是要你传一个组件进去,当数据变动时会去执行他们提前写好的一段更新逻辑,那不就跟你本人在 observe
里写一段更新操作是一样的嘛!用了这个库写进去的 React
就像是在写 Vue
一样。
源码
了解了什么是响应式之后就能够不便咱们来查看源码了,来看看 尤雨溪
是怎么仅用十几行代码就实现的 响应式
:
let activeEffect
class Dep {subscribers = new Set()
depend() {if (activeEffect) {this.subscribers.add(activeEffect)
}
}
notify() {this.subscribers.forEach(effect => effect())
}
}
function watchEffect(effect) {
activeEffect = effect
effect()}
实现完了,再来看看该怎么用:
const dep = new Dep()
let actualCount = 0
const state = {get count() {dep.depend()
return actualCount
},
set count(newCount) {
actualCount = newCount
dep.notify()}
}
watchEffect(() => {console.log(state.count)
}) // 0
state.count++ // 1
如果在观看这十几二十来行代码时都会感觉绕的话,那就阐明你的根底属实不怎么样。因为明眼人一眼就可以看进去,这是一个十分经典的设计模式:公布 - 订阅模式
公布 - 订阅模式
如果不太理解 公布 - 订阅模式
的话,咱们能够简略的来讲一下。但如果你对这些设计模式早已一目了然,并且可能轻松读懂方才那段代码的话,倡议暂且先跳过这一段。
在 《JavaScript 设计模式与开发实际》
一书中,作者 曾探
为公布 - 订阅模式
举了一个非常活泼形象的例子:
小明最近看上了一套房子,到了售楼处之后才被告知,该楼盘的房子早已售罄。好在售楼 MM 通知小明,不久之后还有一些尾盘推出,开发商正在办理相干手续,手续办好后便能够购买。但到底是什么时候,目前还没有人可能晓得。
于是小明记下了售楼处的电话,当前每天都会打电话过来询问是不是曾经到了购买工夫。除了小明,还有小红、小强、小龙也会每天向售楼处征询这个问题。一个星期过后,售楼 MM 决定辞职,因为厌倦了每天答复 1000 个雷同内容的电话。
当然事实中没有这么笨的销售公司,实际上故事是这样的:小明来到之前,把电话号留在了售楼处。售楼 MM 许可他,新楼盘一推出就马上发信息告诉小明。小红、小强和小龙也是一样,他们的电话号码都被记录售楼处的花名册上,新楼盘推出的时候,售楼 MM 会打开花名册,遍历下面的电话号码,顺次发送一条短信来告诉他们。
在刚刚的例子中,发送短信告诉就是一个典型的公布 - 订阅模式,小明、小红等购买者都是订阅者,他们订阅了房子开售的音讯。售楼处作为发布者,会在适合的时候遍历花名册上的电话号码,顺次给购房者公布音讯。
如果你已经用过 xxx.addEventListener
这个函数为 DOM
增加过事件的话,那么实际上就曾经算是用过 公布 - 订阅模式
啦!想一想是不是和售楼处的这个例子很类似:
- 咱们须要在肯定条件下干一些事件
- 但咱们不晓得的是这个条件会在什么工夫点成立
- 所以咱们留下咱们的函数
- 当条件成立时主动执行
那么咱们就来简略的模仿一下 addEventListener
产生的事件以便于大家了解 公布 - 订阅模式
:
class DOM {
#eventObj = {click: [],
mouseover: [],
mouseout: [],
mousemove: [],
keydown: [],
keyup: []
// 还有很多事件类型就不一一写啦
}
addEventListener (event, fn) {this.#eventObj[event].push(fn)
}
removeEventListener (event, fn) {const arr = this.#eventObj[event]
const index = arr.indexOf(fn)
arr.splice(index, 1)
}
click () {this.#eventObj.click.forEach(fn => fn.apply(this))
}
mouseover () {this.#eventObj.mouseover.forEach(fn => fn.apply(this))
}
// 还有很多事件办法就不一一写啦
}
咱们来用一下试试:
const dom = new DOM()
dom.addEventListener('click', () => console.log('点击啦!'))
dom.addEventListener('click', function () {console.log(this) })
dom.addEventListener('mouseover', () => console.log('鼠标进入啦!'))
dom.addEventListener('mouseover', function () {console.log(this) })
// 模仿点击事件
dom.click() // 顺次打印出:'点击啦!' 和相应的 this 对象
// 模仿鼠标事件
dom.mouseover() // 顺次打印出:'鼠标进入啦!' 和相应的 this 对象
const fn = () => {}
dom.addEventListener('click', fn)
// 还能够移除监听
dom.removeEventListener('click', fn)
通过这个简略的案例应该就可能明确 公布 - 订阅模式
了吧?
咱们来援用一下 《JavaScript 设计模式与开发实际》
为公布 - 订阅模式
总结进去的三个要点:
- 首先要指定好谁充当发布者(比方售楼处)
在本例中是 dom 这个对象
- 而后给发布者增加一个缓存列表,用于寄存回调函数以便告诉订阅者(售楼处的花名册)
在本例中是 dom.#eventObj
- 最初公布音讯的时候,发布者会遍历这个缓存列表,顺次触发外面寄存的订阅者回调函数(遍历花名册,挨个发短信)
记住这三个要点后,再来看一眼尤大的代码,看是不是合乎这仨要点:
发布者
:dep 对象缓存列表
:dep.subscribers公布音讯
:dep.notify()
所以这是一个典型的 公布 - 订阅模式
增强版
尤雨溪
的第一版代码实现的还是有些过于简陋了,首先用起来就很不不便,因为咱们每次定义数据时都须要这么手写一遍 getter
和setter
、手动的去执行一下依赖收集函数以及触发的函数。这个局部显然是能够持续进行封装的,那么再来看一眼 尤雨溪
实现的第二版:
let activeEffect
class Dep {subscribers = new Set()
depend() {if (activeEffect) {this.subscribers.add(activeEffect)
}
}
notify() {this.subscribers.forEach(effect => effect())
}
}
function watchEffect(effect) {
activeEffect = effect
effect()
activeEffect = null
}
function reactive(raw) {
// 应用 Object.defineProperty
// 1. 遍历对象上存在的 key
Object.keys(raw).forEach(key => {
// 2. 为每个 key 都创立一个依赖对象
const dep = new Dep()
// 3. 用 getter 和 setter 重写原对象的属性
let realValue = raw[key]
Object.defineProperty(raw, key, {get() {
// 4. 在 getter 和 setter 里调用依赖对象的对应办法
dep.depend()
return realValue
},
set(newValue) {
realValue = newValue
dep.notify()}
})
})
return raw
}
能够看到这一版实现的就比上一版好多了,而且感觉 尤雨溪
在写这一版代码时比上一版更加认真。因为这版代码里有着具体的正文,所以必定是认真解说的一段代码。只不过原来的正文都是用英文写的,我给它翻译成了中文。
不过各位看官请释怀,除了正文被我翻译成了中文以外,其余的中央我一个字母都没有动过,就连空格都是放弃的原汁原味的缩进,为的就是可能让大家看到的是
尤雨溪的一手代码
😋
不难看出,这版代码在实现上用到了两种设计模式,它们别离是 代理模式
以及咱们刚刚讲过的 公布 - 订阅模式
。所以说学好设计模式是如许重要的一件事件。如果对设计模式感兴趣的话能够去 B 站搜寻前端学不动,目前正在连载设计模式中,个人感觉比慕课网那门卖288
的 JavaScript 设计模式课讲的更清晰。
代理模式
代理模式绝对比较简单,都不必上代码,借用 《JavaScript 设计模式核⼼原理与应⽤实际》
的作者 修言
举的一个十分乏味的例子就能让大家明确:
我有个共事,技术很强,发型也很强。多年来因为沉迷 coding,耽搁了人生大事。迫于寻找另一半的欲望比拟急迫,该共事同时是多个优质高端婚恋网站的注册 VIP。工作之余,他经常给咱们分享近期的
相亲情感生活停顿。“你们看,这个妹子头像是不是超可恶!”共事哥这天挖掘了一个新的婚介所,他举起手机,朝身边几位疯狂挥动。
“哥,那是新垣结衣。。。”共事哥的同桌无奈地摇摇头,没有停下 coding 的手。
共事哥复原了沉着,叹了口气:“这种婚恋平台的机制就是这么严格,一进来只能看到其它会员的姓名、年龄和自我介绍。要想看到自己的照片或者获得对方的联系方式,得先向平台付费成为 VIP 才行。哎,我又要买个 VIP 了。”
我一听,哇,这婚恋平台把代理模式玩挺 6 啊!大家想想,主体是共事 A,指标对象是新垣结衣头像的未知妹子。共事 A 不能间接与未知妹子进行沟通,只能通过第三方(婚介所)间接获取对方的一些信息,他可能获取到的信息和权限,取决于第三方违心给他什么——这不就是典型的代理模式吗?
用法
这一版的响应式在应用起来就要难受的多:
const state = reactive({count: 0})
watchEffect(() => {console.log(state.count)
}) // 0
state.count++ // 1
应用形式基本上就和 Vue3
的用法截然不同了!能够看到响应式最外围的原理其实就是 公布 - 订阅
+ 代理模式
。不过这还不是最终版,因为他用的是ES5
的Object.defineProperty
来做的 代理模式
,如果在不思考兼容IE
的状况下还是 ES6
的Proxy
更适宜做 代理
,因为Proxy
翻译过去就是 代理权
、 代理人
的意思。所以 Vue3
采纳了 Proxy
来重构整个响应式代码,咱们来看一下 尤雨溪
写进去的最终版 (Proxy
版)
Proxy 版
let activeEffect
class Dep {subscribers = new Set()
constructor(value) {this._value = value}
get value() {this.depend()
return this._value
}
set value(value) {
this._value = value
this.notify()}
depend() {if (activeEffect) {this.subscribers.add(activeEffect)
}
}
notify() {this.subscribers.forEach((effect) => {effect()
})
}
}
function watchEffect(effect) {
activeEffect = effect
effect()
activeEffect = null
}
// proxy version
const reactiveHandlers = {get(target, key) {const value = getDep(target, key).value
if (value && typeof value === 'object') {return reactive(value)
} else {return value}
},
set(target, key, value) {getDep(target, key).value = value
}
}
const targetToHashMap = new WeakMap()
function getDep(target, key) {let depMap = targetToHashMap.get(target)
if (!depMap) {depMap = new Map()
targetToHashMap.set(target, depMap)
}
let dep = depMap.get(key)
if (!dep) {dep = new Dep(target[key])
depMap.set(key, dep)
}
return dep
}
function reactive(obj) {return new Proxy(obj, reactiveHandlers)
}
能够看到这一版的代码又比上一版更加简单了点,但在用法上还是和上一版截然不同:
const state = reactive({count: 0})
watchEffect(() => {console.log(state.count)
}) // 0
state.count++ // 1
咱们来重点解说一下最终版的代码,这一版代码才是最 优良
的。麻雀虽小,五脏俱全,不仅做了最根本的 公布 - 订阅模式
+ 代理模式
,而且还用到了许多小技巧来做了性能方面的优化。
详解
首先尤大定义了一个名为 activeEffect
的空变量,用于寄存 watchEffect
传进来的函数:
// 定义一个临时寄存 watchEffect 传进来的参数的变量
let activeEffect
接下来定义了一个名为 Dep
的类,这个 Dep
应该是 Dependence
的缩写,意为 依赖
。实际上就相当于 公布 - 订阅模式
中的发布者类:
// 定义一个 Dep 类,该类将会为每一个响应式对象的每一个键生成一个发布者实例
class Dep {
// 用 Set 做缓存列表以避免列表中增加多个完全相同的函数
subscribers = new Set()
// 构造函数承受一个初始化的值放在公有变量内
constructor(value) {this._value = value}
// 当应用 xxx.value 获取对象上的 value 值时
get value() {
// 代理模式 当获取对象上的 value 属性的值时将会触发 depend 办法
this.depend()
// 而后返回公有变量内的值
return this._value
}
// 当应用 xxx.value = xxx 批改对象上的 value 值时
set value(value) {
// 代理模式 当批改对象上的 value 属性的值时将会触发 notify 办法
this._value = value
// 先改值再触发 这样保障触发的时候用到的都是曾经批改后的新值
this.notify()}
// 这就是咱们常说的依赖收集办法
depend() {
// 如果 activeEffect 这个变量为空 就证实不是在 watchEffect 这个函数外面触发的 get 操作
if (activeEffect) {
// 但如果 activeEffect 不为空就证实是在 watchEffect 里触发的 get 操作
// 那就把 activeEffect 这个存着 watchEffect 参数的变量增加进缓存列表中
this.subscribers.add(activeEffect)
}
}
// 更新操作 通常会在值被批改后调用
notify() {
// 遍历缓存列表里寄存的函数 并顺次触发执行
this.subscribers.forEach((effect) => {effect()
})
}
}
之前两版尤大都是在里头定义了一个变量用于保留 响应式对象
每一个键所对应的值,而这次是间接把值放进了 Dep
类的定义里,定义成了 getter
和setter
,在获取值时会进行依赖收集操作,而在批改值时会进行更新操作。
接下来又定义了一个跟 Vue3
的watchEffect
名称一样的函数:
// 模拟 Vue3 的 watchEffect 函数
function watchEffect(effect) {
// 先把传进来的函数放入到 activeEffect 这个变量中
activeEffect = effect
// 而后执行 watchEffect 外面的函数
effect()
// 最初把 activeEffect 置为空值
activeEffect = null
}
咱们在应用时不是会在这个函数外面再传进一个函数么:
watchEffect(() => state.xxx)
这个函数就被赋值给了 activeEffect
这个变量下面去,而后立即执行这个函数,一般来说这个函数外面都会有一些 响应式对象
的对吧?既然有,那就会触发 getter
去进行依赖收集操作,而依赖收集则是判断了 activeEffect
这个变量有没有值,如果有,那就把它增加进缓存列表里。等到执行完这个函数后,就立刻将 activeEffect
这个变量置为空值,避免不在 watchEffect
这个函数中触发 getter
的时候也执行依赖收集操作。
接下来就是定义了一个 Proxy
代理的解决对象:
const reactiveHandlers = {
// 当触发 get 操作时
get(target, key) {
// 先调用 getDep 函数取到外面寄存的 value 值
const value = getDep(target, key).value
// 如果 value 是对象的话
if (value && typeof value === 'object') {
// 那就把 value 也变成一个响应式对象
return reactive(value)
} else {
// 如果 value 只是根本数据类型的话就间接将值返回
return value
}
},
// 当触发 set 操作时
set(target, key, value) {
// 调用 getDep 函数并将外面寄存的 value 值从新赋值成 set 操作的值
getDep(target, key).value = value
}
}
如果对 Proxy
不是很理解的话,倡议看看阮一峰的《ES6 入门教程》,写的还是不错的。
刚刚那个对象在 get
和set
操作中都用到了 getDep
这个函数,这个函数时在前面定义的,他会用到一个叫 targetToHashMap
的WeakMap
数据结构来存储数据:
// 定义一个 WeakMap 数据类型 用于寄存 reactive 定义的对象以及他们的发布者对象集
const targetToHashMap = new WeakMap()
接下来就是定义 getDep
函数啦:
// 定义 getDep 函数 用于获取 reactive 定义的对象所对应的发布者对象集里的某一个键对应的发布者对象
function getDep(target, key) {
// 获取 reactive 定义的对象所对应的发布者对象集
let depMap = targetToHashMap.get(target)
// 如果没获取到的话
if (!depMap) {
// 就新建一个空的发布者对象集
depMap = new Map()
// 而后再把这个发布者对象集存进 WeakMap 里
targetToHashMap.set(target, depMap)
}
// 再获取到这个发布者对象集里的某一个键所对应的发布者对象
let dep = depMap.get(key)
// 如果没获取到的话
if (!dep) {
// 就新建一个发布者对象并初始化赋值
dep = new Dep(target[key])
// 而后将这个发布者对象放入到发布者对象集里
depMap.set(key, dep)
}
// 最初返回这个发布者对象
return dep
}
这个中央就略微有点绕了,咱们来上图:
每一个传进 reactive
里去的对象,都会被存在 WeakMap
里的键上。而每一个键所对应的值,就是一个Map
:
// targetToHashMap: {const obj1 = reactive({ num: 1}) // {num: 1}: new Map(),
const obj2 = reactive({num: 2}) // {num: 2}: new Map(),
const obj3 = reactive({num: 3}) // {num: 3}: new Map()
// }
那值 (Map) 里存的又是什么呢?存的是:
假如咱们 reactive
了一个对象 {a: 0, b: 1, c: 2}
,那么Map
外面存的就是:
{'a': new Dep(0),
'b': new Dep(1),
'c': new Dep(2)
}
就是把对象的键放到 Map
的键上,而后在用 new Dep
创立一个发布者对象,再把值传给 Dep
。Vue3 之所以性能比 Vue2 强很多的其中一个十分重要的优化点就是这个Proxy
。并不是说Proxy
的性能就比 Object.defineProperty
高多少,而是说在 Proxy
里的解决形式比 Vue2
期间的好很多:Vue2
的响应式是一上来就一顿 遍历
+ 递归
把你定义的所有数据全都变成响应式的,这就会导致如果页面上有很多很简单的数据结构时,用 Vue2
写的页面就会白屏一小段时间。毕竟 遍历
+ 递归
还是绝对很慢的一个操作嘛!
而 React
就没有这个故障,当然 Vue3
也不会有这个故障。从代码中能够看出,当咱们获取对象上的某个键对应的值时,会先判断这个值到底有没有对应的发布者对象,没有的话再创立发布者对象。而且当获取到的值是援用类型时再把这个值变成 响应式对象
,等你用到了响应式对象里的值时再去新建发布者对象。
总结成一句话就是:
Vue3
是用到哪局部的数据的时候,再把数据变成响应式的。而Vue2
则是不管三七二十一,刚开局就全都给你变成响应式数据。
最初一步就是定义 reactive
函数啦:
// 模拟 Vue3 的 reactive 函数
function reactive(obj) {
// 返回一个传进来的参数对象的代理对象 以便应用代理模式拦挡对象上的操作并利用公布 - 订阅模式
return new Proxy(obj, reactiveHandlers)
}
流程图
为了便于大家了解,咱们应用一遍 reactive
和watchEffect
函数,而后顺便看看到底产生了什么:
首先咱们用 reactive
函数定义了一个对象 {num: 0}
,这个对象会传给Proxy
的第一个参数,此时还并没有产生什么事件,那么接下来咱们就在 watchEffect
里打印一下这个对象的 num
属性:
此时传给 watchEffect
的这个函数会赋值给 actibveEffect
这个变量下来,而后立刻执行这个函数:
在执行的过程中发现有 get
操作,于是被 Proxy
所拦挡,走到了 get
这一步:
因为在 get
操作中须要用 getDep
函数,于是又把 {num: 0}
传给了 getDep
,key 是 num,所以相当于getDep({num: 0}, 'num')
。进入到getDep
函数体内,须要用 targetToHashMap
来获取 {num: 0}
这个键所对应的值,但目前 targetToHashMap
是空的,所以基本获取不到任何内容。于是进入判断,新建一个 Map
赋值给 targetToHashMap
,相当于:targetToHashMap.set({num: 0}, new Map())
,紧接着就是获取这个Map
的key
所对应的值:
因为 Map
也是空的,所以还是获取不到值,于是进入判断,新建一个 Dep
对象:
因为是用 getDep(...xxx).value
来获取到这个对象的 value
属性,所以就会触发getter
:
顺着 getter
咱们又来到了 depend
办法中,因为 activeEffect
有值,所以进入判断,把 activeEffect
退出到 subscribes
这个 Set
构造中。此时依赖收集局部就暂且告一段落了,接下来咱们来扭转 obj.num
的值,看看都会产生些什么:
首先会被 Proxy
拦挡住 set
操作,而后调用 getDep
函数:
获取到 dep
对象后,就会批改它的 value
属性,从而触发 setter
操作:
最初咱们来到了告诉 (notify) 阶段,在告诉阶段会找到咱们的缓存列表(subscribers),而后顺次触发外面的函数:
那么此时就会运行 () => console.log(obj.num)
这个函数,你认为这就完了吗?当然没有!因为运行了 obj.num
这个操作,所以又会触发 get
操作被 Proxy
拦挡:
获取到咱们之前创立过的发布者对象后,又会触发发布者对象的 getter
操作:
一顿绕,绕到 depend
办法时,咱们须要检测一下 activeEffect
这个变量:
因为不会进入到判断外面去,所以执行了个寂寞 ( 啥也没执行),那么接下来的代码便是:
最终打印出了10
。
结语
没想到短短这么七十来行代码这么绕吧?所以说抽丝剥茧的学习办法有多重要。如果间接看源码的话,这外面必定还会有各种各样的判断。比方 watchEffect
当初没做任何的判断对吧?那么当咱们给 watchEffect
传了一个不是函数的参数时会怎么?当咱们给 reactive
对象传数组时又会怎么?当传 Map
、Set
时呢?传根本数据类型时呢?而且即便当初咱们不思考这些状况,就传一个对象,外面不要有数组等什么其余的货色,watchEffect
也只传函数。那么其实在应用体验上还是有一点与 Vue3
的watchEffect
不同的中央,那就是不能在 watchEffect
外面扭转响应式对象的值:
而写成这样就没有问题:
可是在 Vue3
的watchEffect
里就不会呈现这样的情况。这是因为如果在 watchEffect
里对响应式对象进行赋值操作的话就又会触发 set
操作,从而被 Proxy
拦挡,而后又绕到 notify
的办法下面去了,notify
又会把 watchEffect
里的函数运行一遍,后果又发现外面有 set
操作 ( 因为是同一段代码嘛 ),而后又会去运行notify
办法,持续触发 set
操作造成死循环。
所以咱们还须要思考到这种死循环的状况,但如果真的思考的这么全面的话,那置信代码量也相当大了,咱们会被进一步绕晕。所以先吃透这段代码,而后缓缓的咱们再来看真正的源码都是怎么解决这些状况的。或者也能够先不看源码,本人思考一下这些问题该如何去解决,而后写出本人的逻辑来,测试没有问题后再去跟 Vue3
的源码进行比照,看看本人实现的和尤雨溪实现的形式有何异同。
本篇文章到这里就要告一段落了,但还没完,这只是
响应式局部
。之后还有虚构 DOM
、diff 算法
、组件化
、根组件挂载
等局部。
如果等不及看下一篇解析文章的话,也能够间接点击这个链接进入到 codepen
里自行钻研 尤雨溪
写的代码。代码量很少,是咱们学习 Vue3
原理的 绝佳材料
!学会了原理之后哪怕不去看真正的源码,在面试的时候都能够跟面试官吹两句。因为毕竟不会有哪个面试官考查源码时会问:你来说一下Vue3
的某某文件的第 996
行代码写的是什么?考查必定也重点考查的是原理,很少会去考查各种判断参数的边界状况解决。所以 点赞
+ 关注
,跟着 尤雨溪
学源码不迷路!
本文首发于公众号:前端学不动