尤雨溪国外教程:亲手带你写个简易版的Vue!

⚠️本文为掘金社区首发签约文章,未获受权禁止转载前端

前言

不少时候咱们都对源码展示出了必定的渴求,但当被问到究竟为何想看源码时,答案无非也就那么几种:vue

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

但其实不多人会真正的看明白源码,一方面是因为代码量实在是太多了,另外一方面则是当咱们阅读别人代码的时候就是容易搞得一头雾水。由于每一个人的编码方式以及编码习惯都截然不同,在看一个编码习惯与本身不一样的人的代码时是很累的。react

何况不只是因为每一个人的编码风格相差甚远,人与人之间各自擅长的技术方向以及技术水平也都是横当作岭侧成峰远近高低各不一样。刨除掉以上的种种缘由以后,更重要的一个缘由是不少人框架用的都不够精通呢、用过的API也就那么几个常见的,其余不经常使用但很高阶的API都没怎么用过,连用都没用明白呢,这样的人看源码的时候固然会被绕晕啦!git

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

这么说确实有必定的道理,但若是论底层,他比谁都了解。之因此咱们啃不动源码的很重要的一个缘由就是:细枝末节的东西实在是太多了,很容易令你们找不到重点。这些细枝末节的东西天然有它们存在的道理,但它们确成为了咱们行走在钻研源码这条路上的绊脚石。github

题外话

怎样学习源码才是最科学的方式呢?咱们来看一个例子:有一些听起来很是高大上的高科技产品,如电磁轨道炮。各个军事强国都在争相探索这一领域,假设有一天,咱们一觉醒来成为了国家电磁轨道炮首席研究员,是专门负责研究电磁轨道炮底层技术的。那么当咱们拆解一个电磁轨道炮的时候,大几率你是看不懂它的内部构造的。由于里面会包含许多很是复杂的高强度材料控制磁力的电极蜿蜒曲折的电线提升精准度的装置以及一些利于使用者操控的封装等等…面试

那么此时的你可能就不太容易搞明白电磁轨道炮的真正原理,直到有一次在网上偶然间看到一个视频,视频中的人用了一些磁铁、若干钢珠、以及几个咱们平常生活中可以搞到的材料来制做了一个简易版的电磁轨道炮。这样咱们一会儿就可以搞懂电磁轨道炮的真正原理,虽然这样的轨道炮并不能真正的用于实战,但只要咱们明白了最基础的那部分,咱们就能够在此基础上一步步进行扩展,慢慢弄懂整个可以用于实战的复杂轨道炮。算法

源码也是同理,咱们按照电磁轨道炮的思路一步步来,先搞清楚最核心的基础部分,慢慢的再一步步去进阶。这样的学习方法比咱们确定一上来就去拆解一个完整版的电磁轨道炮要强得多vue-router

既然咱们有这样的需求,那么做为一个流行框架的做者就必然会有所回应:在一次培训的过程当中,尤雨溪带领你们写了一个很是微型的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)里存的又是什么呢?存的是:

WX20210729-192101.png

假设咱们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函数,而后顺便看看到底发生了什么:

WX20210730-132843.png

首先咱们用reactive函数定义了一个对象{ num: 0 },这个对象会传给Proxy的第一个参数,此时还并无发生什么事情,那么接下来咱们就在watchEffect里打印一下这个对象的num属性:

WX20210730-133331.png

此时传给watchEffect的这个函数会赋值给actibveEffect这个变量上去,而后当即执行这个函数:

WX20210730-133925.png

在执行的过程当中发现有get操做,因而被Proxy所拦截,走到了get这一步:

WX20210730-140428.png

因为在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所对应的值:

WX20210730-141344.png

因为Map也是空的,因此仍是获取不到值,因而进入判断,新建一个Dep对象:

WX20210730-141809.png

因为是用getDep(...xxx).value来获取到这个对象的value属性,因此就会触发getter

WX20210730-142346.png

顺着getter咱们又来到了depend方法中,因为activeEffect有值,因此进入判断,把activeEffect加入到subscribes这个Set结构中。此时依赖收集部分就暂且告一段落了,接下来咱们来改变obj.num的值,看看都会发生些什么:

WX20210730-144029.png

首先会被Proxy拦截住set操做,而后调用getDep函数:

WX20210730-143810.png

获取到dep对象后,就会修改它的value属性,从而触发setter操做:

WX20210730-144603.png

最后咱们来到了通知(notify)阶段,在通知阶段会找到咱们的缓存列表(subscribers),而后依次触发里面的函数:

WX20210730-144912.png

那么此时就会运行() => console.log(obj.num)这个函数,你觉得这就完了吗?固然没有!因为运行了obj.num这个操做,因此又会触发get操做被Proxy拦截:

WX20210730-145721.png

获取到咱们以前建立过的发布者对象后,又会触发发布者对象的getter操做:

WX20210730-150316.png

一顿绕,绕到depend方法时,咱们须要检测一下activeEffect这个变量:

WX20210730-150635.png

因为不会进入到判断里面去,因此执行了个寂寞(啥也没执行),那么接下来的代码即是:

WX20210730-151831.png

最终打印出了10

结语

没想到短短这么七十来行代码这么绕吧?因此说抽丝剥茧的学习方法有多重要。若是直接看源码的话,这里面确定还会有各类各样的判断。好比watchEffect如今没作任何的判断对吧?那么当咱们给watchEffect传了一个不是函数的参数时会怎样?当咱们给reactive对象传数组时又会怎样?当传MapSet时呢?传基本数据类型时呢?并且即便如今咱们不考虑这些状况,就传一个对象,里面不要有数组等什么其余的东西,watchEffect也只传函数。那么其实在使用体验上仍是有一点与Vue3watchEffect不一样的地方,那就是不能在watchEffect里面改变响应式对象的值:

WX20210730-152653.png

而写成这样就没有问题:

WX20210730-152902.png

但是在Vue3watchEffect里就不会出现这样的情况。这是由于若是在watchEffect里对响应式对象进行赋值操做的话就又会触发set操做,从而被Proxy拦截,而后又绕到notify的方法上面去了,notify又会把watchEffect里的函数运行一遍,结果又发现里面有set操做(由于是同一段代码嘛),而后又会去运行notify方法,继续触发set操做形成死循环。

因此咱们还须要考虑到这种死循环的状况,但若是真的考虑的这么全面的话,那相信代码量也至关大了,咱们会被进一步绕晕。因此先吃透这段代码,而后慢慢的咱们再来看真正的源码都是怎么处理这些状况的。或者也能够先不看源码,本身思考一下这些问题该如何去处理,而后写出本身的逻辑来,测试没有问题后再去跟Vue3的源码进行对比,看看本身实现的和尤雨溪实现的方式有何异同。

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

若是等不及看下一篇解析文章的话,也能够直接点击这个连接进入到codepen里自行钻研尤雨溪写的代码。代码量不多,是咱们学习Vue3原理的绝佳资料!学会了原理以后哪怕不去看真正的源码,在面试的时候均可以跟面试官吹两句。由于毕竟不会有哪一个面试官考察源码时会问:你来讲一下Vue3的某某文件的第996行代码写的是什么?考察确定也重点考察的是原理,不多会去考察各类判断参数的边界状况处理。因此点赞+关注,跟着尤雨溪学源码不迷路!

往期精彩文章

相关文章
相关标签/搜索