关于前端:尤大怎么还生啃源码呢我这就亲手给你写个丐版Vue

60次阅读

共计 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 这个库。咱们来看一眼它在 GitHubREADME.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++;

是不是很像 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 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 设计模式与开发实际》公布 - 订阅模式 总结进去的三个要点:

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

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

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

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

增强版

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

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 的用法截然不同了!能够看到响应式最外围的原理其实就是 公布 - 订阅 + 代理模式 。不过这还不是最终版,因为他用的是ES5Object.defineProperty来做的 代理模式 ,如果在不思考兼容IE 的状况下还是 ES6Proxy更适宜做 代理 ,因为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 类的定义里,定义成了 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 行代码写的是什么?考查必定也重点考查的是原理,很少会去考查各种判断参数的边界状况解决。所以 点赞 + 关注 ,跟着 尤雨溪 学源码不迷路!

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

正文完
 0