前言

很多时候咱们都对源码展现出了肯定的渴求,但当被问到到底为什么想看源码时,答案无非也就那么几种:

  • 为了面试
  • 为了在简历上写本人会源码
  • 理解底层原理 学习高手思路
  • 通过源码来学习一些小技巧(骚操作)
  • 对框架如何实现的各种性能感到好奇
  • 内卷重大 不看不行 逆水行舟 逆水行舟
  • 本人也想造轮子 先看看他人都是怎么做的
  • 各种公众号和卖课的都在贩卖焦虑 被洗脑洗的

但其实很少人会真正的看明确源码,一方面是因为代码量切实是太多了,另一方面则是当咱们浏览他人代码的时候就是容易搞得一头雾水。因为每个人的编码方式以及编码习惯都天壤之别,在看一个编码习惯与本人不同的人的代码时是很累的。

况且不仅是因为每个人的编码格调相差甚远,人与人之间各自善于的技术方向以及技术水平也都是横看成岭侧成峰远近高下各不同。刨除掉以上的种种原因之后,更重要的一个起因是很多人框架用的都不够精通呢、用过的API也就那么几个常见的,其余不罕用但很高阶的API都没怎么用过,连用都没用明确呢,这样的人看源码的时候当然会被绕晕啦!

那必定有人会说:尤雨溪他框架就肯定用的很6吗?我每天都在用他的框架写代码,他还不肯定有我纯熟呢!

这么说的确有肯定的情理,但如果论底层,他比谁都理解。之所以咱们啃不动源码的很重要的一个起因就是:细枝末节的货色切实是太多了,很容易令大家找不到重点。这些细枝末节的货色天然有它们存在的情理,但它们确成为了咱们行走在钻研源码这条路上的绊脚石。

题外话

怎么学习源码才是最迷信的形式呢?咱们来看一个例子:有一些听起来十分高大上的高科技产品,如电磁轨道炮。各个军事强国都在争相摸索这一畛域,假如有一天,咱们一沉睡来成为了国家电磁轨道炮首席研究员,是专门负责钻研电磁轨道炮底层技术的。那么当咱们拆解一个电磁轨道炮的时候,大概率你是看不懂它的外部结构的。因为外面会蕴含许多非常复杂的高强度资料管制磁力的电极笔直波折的电线进步精准度的安装以及一些利于使用者操控的封装等等…

那么此时的你可能就不太容易搞明确电磁轨道炮的真正原理,直到有一次在网上偶然间看到一个视频,视频中的人用了一些磁铁、若干钢珠、以及几个咱们日常生活中可能搞到的资料来制作了一个简易版的电磁轨道炮。这样咱们一下子就可能搞懂电磁轨道炮的真正原理,尽管这样的轨道炮并不能真正的用于实战,但只有咱们明确了最根底的那局部,咱们就能够在此基础上一步步进行扩大,缓缓弄懂整个可能用于实战的简单轨道炮。

源码也是同理,咱们依照电磁轨道炮的思路一步步来,先搞清楚最外围的根底局部,缓缓的再一步步去进阶。这样的学习办法比咱们必定一上来就去拆解一个完整版的电磁轨道炮要强得多

既然咱们有这样的需要,那么作为一个风行框架的作者就必然会有所回应:在一次培训的过程中,尤雨溪率领大家写了一个十分微型的Vue3。不过惋惜这是他在国外办过的为期一天的培训,咱们国内的观众并没有福分可能享受到被框架作者培训的这么一次教学。但好在尤雨溪曾经把代码全副上传到了codepen上,大家能够点击这个链接来浏览尤雨溪亲手写的代码,或者也能够抉择留在本篇文章内,看我来用中文为大家解说尤雨溪的亲笔代码

响应式篇

尤雨溪在某次直播时曾示意过:Vue3 的源码要比 Vue2 的源码要好学很多Vue3在架构以及模块的耦合关系设计方面比Vue2更好,能够一个模块一个模块看,这样比拟容易了解。如果是刚上手,能够从Reactivity看起。因为Reactivity是整个Vue3中跟内部没有任何耦合的一个模块。

Reactivity就是咱们常说的响应式,赫赫有名的React也是这个意思,不信认真比照一下前五个字母。那么什么是响应式呢?想想看React是什么框架?MVVM对吧?MVVM的主打口号是:

数据驱动视图!

也就是说当数据产生扭转时咱们会从新渲染一下组件,这样就可能达到一批改数据,页面上用到这个数据的中央就会实时发生变化的成果。不过在数据发生变化时也不仅仅只是可能更新视图,还能够做些别的呢!尤雨溪在创立@vue/reactivity这个模块的时候,借鉴的是@nx-js/observer-util这个库。咱们来看一眼它在GitHubREADME.md里展现的一段示例代码:

import { observable, observe } from '@nx-js/observer-util';const counter = observable({ num: 0 });const countLogger = observe(() => console.log(counter.num));// 这行代码将会调用 countLogger 这个函数并打印出:1counter.num++;

是不是很像Vue3reactivewatchEffect啊?其实就是咱们提前定义好一个函数,当函数外面依赖的数据项发生变化时就会主动执行这段函数,这就是响应式!

数据驱动视图那就更容易了解了,既然当数据发生变化时能够执行一段函数,那么这段函数为什么不能够执行一段更新视图的操作呢:

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 activeEffectclass 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 = 0const state = {  get count() {    dep.depend()    return actualCount  },  set count(newCount) {    actualCount = newCount    dep.notify()  }}watchEffect(() => {  console.log(state.count)}) // 0state.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设计模式与开发实际》公布-订阅模式总结进去的三个要点:

  1. 首先要指定好谁充当发布者(比方售楼处)在本例中是 dom 这个对象
  2. 而后给发布者增加一个缓存列表,用于寄存回调函数以便告诉订阅者(售楼处的花名册)在本例中是 dom.#eventObj
  3. 最初公布音讯的时候,发布者会遍历这个缓存列表,顺次触发外面寄存的订阅者回调函数(遍历花名册,挨个发短信)

记住这三个要点后,再来看一眼尤大的代码,看是不是合乎这仨要点:

  • 发布者:dep 对象
  • 缓存列表:dep.subscribers
  • 公布音讯:dep.notify()

所以这是一个典型的公布-订阅模式

增强版

尤雨溪的第一版代码实现的还是有些过于简陋了,首先用起来就很不不便,因为咱们每次定义数据时都须要这么手写一遍gettersetter、手动的去执行一下依赖收集函数以及触发的函数。这个局部显然是能够持续进行封装的,那么再来看一眼尤雨溪实现的第二版:

let activeEffectclass 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)}) // 0state.count++ // 1

应用形式基本上就和Vue3的用法截然不同了!能够看到响应式最外围的原理其实就是公布-订阅+代理模式。不过这还不是最终版,因为他用的是ES5Object.defineProperty来做的代理模式,如果在不思考兼容IE的状况下还是ES6Proxy更适宜做代理,因为Proxy翻译过去就是代理权代理人的意思。所以Vue3采纳了Proxy来重构整个响应式代码,咱们来看一下尤雨溪写进去的最终版(Proxy版)

Proxy 版

let activeEffectclass 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 versionconst 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)}) // 0state.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类的定义里,定义成了gettersetter,在获取值时会进行依赖收集操作,而在批改值时会进行更新操作。

接下来又定义了一个跟Vue3watchEffect名称一样的函数:

// 模拟 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入门教程》,写的还是不错的。

刚刚那个对象在getset操作中都用到了getDep这个函数,这个函数时在前面定义的,他会用到一个叫targetToHashMapWeakMap数据结构来存储数据:

// 定义一个 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)}

流程图

为了便于大家了解,咱们应用一遍reactivewatchEffect函数,而后顺便看看到底产生了什么:

首先咱们用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()),紧接着就是获取这个Mapkey所对应的值:

因为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对象传数组时又会怎么?当传MapSet时呢?传根本数据类型时呢?而且即便当初咱们不思考这些状况,就传一个对象,外面不要有数组等什么其余的货色,watchEffect也只传函数。那么其实在应用体验上还是有一点与Vue3watchEffect不同的中央,那就是不能在watchEffect外面扭转响应式对象的值:

而写成这样就没有问题:

可是在Vue3watchEffect里就不会呈现这样的情况。这是因为如果在watchEffect里对响应式对象进行赋值操作的话就又会触发set操作,从而被Proxy拦挡,而后又绕到notify的办法下面去了,notify又会把watchEffect里的函数运行一遍,后果又发现外面有set操作(因为是同一段代码嘛),而后又会去运行notify办法,持续触发set操作造成死循环。

所以咱们还须要思考到这种死循环的状况,但如果真的思考的这么全面的话,那置信代码量也相当大了,咱们会被进一步绕晕。所以先吃透这段代码,而后缓缓的咱们再来看真正的源码都是怎么解决这些状况的。或者也能够先不看源码,本人思考一下这些问题该如何去解决,而后写出本人的逻辑来,测试没有问题后再去跟Vue3的源码进行比照,看看本人实现的和尤雨溪实现的形式有何异同。

本篇文章到这里就要告一段落了,但还没完,这只是响应式局部。之后还有虚构DOMdiff算法组件化根组件挂载等局部。

如果等不及看下一篇解析文章的话,也能够间接点击这个链接进入到codepen里自行钻研尤雨溪写的代码。代码量很少,是咱们学习Vue3原理的绝佳材料!学会了原理之后哪怕不去看真正的源码,在面试的时候都能够跟面试官吹两句。因为毕竟不会有哪个面试官考查源码时会问:你来说一下Vue3的某某文件的第996行代码写的是什么?考查必定也重点考查的是原理,很少会去考查各种判断参数的边界状况解决。所以点赞+关注,跟着尤雨溪学源码不迷路!

本文首发于公众号:前端学不动