Серия исходных кодов: Vue3 на простом языке (1)

Vue.js
Серия исходных кодов: Vue3 на простом языке (1)

автор:Сюй จุ๊บ, несанкционированное воспроизведение запрещено.

предисловие

Vue.js 3.0(именуемый в дальнейшемVue3), официально выпущенный 18 сентября 2020 г.,Vue3 BetaВерсия была выпущена еще в апреле этого года. Я считаю, что некоторые студенты уже начали испытывать это. Или вы уже ознакомились с официальными документами, и студенты, которые плохо владеют английским языком, должны использоватькликните сюда, это китайский документ. 😂😂😂😂😂😂

Я думаю, все обязательноVue2Исходный код или реализация основных функций имеют определенное понимание, эта статья должна узнать у меня.Vue2способ общаться и учиться со всемиVue3Исходный код, текущая версия3.0.2. Если есть ошибки или неточности, прошу исправить и дополнить.


каталог проекта

Структура каталогов

Давайте рассмотримVue2Исходный каталог

├── src
  ├── compiler    #  编译相关的模块
  ├── core        #  vue核心代码
  ├── platforms   #  平台相关
  ├── server      #  服务端渲染相关
  ├── sfc         #  vue单文件组件
  ├── shared      #  内容公用方法

посмотри сноваVue3Структура каталогов в исходном коде

├──packages
  ├── compiler-core       
  ├── compiler-dom        
  ├── compiler-sfc       
  ├── compiler-ssr     
  ├── reactivity         
  ├── runtime-core       
  ├── runtime-dom         
  ├── vue
  ├── shared
  ├── ...            

Вы можете выбрать несколько, чтобы рассказать о функциях этих модулей.

  • ядро компилятора: независимые от платформы модули компиляции, такие как базовые файлы шаблонов компиляции baseCompile, baseParse для генерации AST
  • компилятор-дом: основанный на ядре компилятора, это модуль компиляции для браузеров.Вы можете видеть, что он основан на baseCompile, baseParse и переписывает complie и parse.
  • компилятор-sfc: используется для компиляции однофайловых компонентов vue.
  • компилятор-ssr: рендеринг на стороне сервера
  • реактивность: vue независимый отзывчивый модуль
  • runtime-core: это также независимый от платформы базовый модуль с различными API-интерфейсами vue и средством визуализации виртуального дома.
  • runtime-dom: среда выполнения на основе ядра среды выполнения, зависящая от браузера.
  • vue: представить и экспортировать runtime-core и методы компиляции.

можно увидетьVue3Модули четко разделены, модули относительно независимы, и мы можем ссылаться на них отдельно.reactivityНа этот модуль также можно ссылатьсяcompiler-sfcРазработано в нашей собственнойpluginиспользовать его, например.vue-loader , viteиспользуются.

monorepo

Это потому чтоVue3использоватьmonorepoЭто способ управления кодом проекта. отличный отVue2управление кодом, оно находится вrepoуправлять несколькимиpackage, каждыйpackageУ всех есть свои объявления типов, модульные тесты.packageЕго также можно выпускать независимо, что, как правило, легче поддерживать, публиковать и читать.

от входа

Создайте экземпляр Vue

оглядыватьсяVue2Создайте экземпляр b Vue

import Vue from 'vue';
import App from './App.vue';

const vm = new Vue({
  /* 选项 */
}).$mount('#app');

Vue3Создайте экземпляр приложения в

import { createApp } from 'vue';
import App from './App.vue';

const app = Vue.createApp({
  /* 选项 */
});

const vm = app.mount('#app');

Можно видеть, что разница между ними заключается в том, чтоVue2Каждое приложение Vue вnewОдинVueКонструктор создает новый экземпляр Vue. пока вVue3В нем каждое приложение Vue создается с использованиемcreateAppФункция начинается с создания нового экземпляра приложения.

Суть в том, что хотя разницы нет, но положительно влияет в других аспектах.Следуя вышеприведенному примеру, если нам нужно зарегистрировать глобальный компонент, или изменить глобальную конфигурацию.

/* Vue2 的做法 */
Vue.component('my-component', {
  /* 选项 */
});
Vue.directive('my-directive', {});
Vue.mixin({
  /* ... */
});

/* Vue3 的做法 */
const app = Vue.createApp({
  /* 选项 */
});
app.component('my-component' /* 组件 */); // 每个方法都是可链式的
app.directive('my-directive' /* 指令 */);
app.mixin();

Vue2Очень удобно использовать API и конфигурацию Vue для глобального изменения Vue, но очень недружелюбно для одной и той же страницы создавать экземпляры нескольких приложений через один и тот же конструктор Vue. Например

const app1 = new Vue({}).$mount('#app1');
const app2 = new Vue({}).$mount('#app2');

а такжеVue3Будет изменен только текущий сконфигурированный экземпляр приложения, что позволит избежать этой проблемы.

Запись исходного кода

Далее изimport { createApp } from 'vue';С этого предложения начинается изучение исходного кода.

в исходном кодеvueмодуль, мы можем видеть, что,vueНа самом деле этот модуль в основном импортируется и экспортируется.runtime-dom,complier. продолжатьruntime-domнаходясь в поискеcreateApp.

export const createApp = ((...args) => {
  // 可以看到真正的createApp 方法是在渲染器属性上的
  const app = ensureRenderer().createApp(...args)
  // ...
  const { mount } = app
  app.mount = (containerOrSelector: Element | string): any => {
    // ...
  }

  return app
}) as CreateAppFunction<Element>

/**
 * ensureRenderer 这里是为了执行createApp时,才给renderer渲染器赋值,也是优化的一点。
 * 只导入reactive, 没有执行createApp,不会执行 createRenderer,
 * 那么打包时 tree-shaking 可以摇掉 runtime-core 这个模块。
 * */
function ensureRenderer() {
  return renderer || (renderer = createRenderer<Node, Element>(rendererOptions))
}

существуетruntime-dom/index.tsВ нем вы можете найти определение createApp.Реализация внутри примерно разделена на три шага: создание экземпляра приложения, переписываниеmountметод, который возвращает экземпляр приложения приложения.

Затем мы экспортируем этот методruntime-coreпосмотри в модулеcreateRenderer, и найти настоящийcreateApp

// 1
export { createRenderer } from './renderer'

// 2
export function createRenderer (options) {
  return baseCreateRenderer(options)
}

// 3
function baseCreateRenderer(
  options: RendererOptions,
  createHydrationFns?: typeof createHydrationFunctions
) {
  const {
    insert: hostInsert,
    remove: hostRemove,
    patchProp: hostPatchProp,
    forcePatchProp: hostForcePatchProp,
    createElement: hostCreateElement,
    createText: hostCreateText,
    createComment: hostCreateComment,
    setText: hostSetText,
    setElementText: hostSetElementText,
    parentNode: hostParentNode,
    nextSibling: hostNextSibling,
    setScopeId: hostSetScopeId = NOOP,
    cloneNode: hostCloneNode,
    insertStaticContent: hostInsertStaticContent
  } = options

  // ...

  return {
    render,
    hydrate,
    createApp: createAppAPI(render, hydrate)
  }
}

// 4
export function createAppAPI(render, hydrate) {
  return function createApp(rootComponent, rootProps = null) {
    // ...
    const app: App = (context.app = {
      _uid: uid++,
      _component: rootComponent as ConcreteComponent,
      _props: rootProps,
      _container: null,
      _context: context,
      version,

      use (plugin) {
        // ...
        return app
      },
      mixin(mixin: ComponentOptions) {
        // ...
        return app
      },
      mount(rootContainer: HostElement, isHydrate?: boolean): any {
        if (!isMounted) {
          const vnode = createVNode(
            rootComponent as ConcreteComponent,
            rootProps
          )

          vnode.appContext = context

          if (isHydrate && hydrate) {
            // ...
          } else {
            render(vnode, rootContainer)
          }
          isMounted = true
          app._container = rootContainer
          ;(rootContainer as any).__vue_app__ = app

          return vnode.component!.proxy
        }
      },
      // ...
    }
  }
  • Примечание: в приведенном выше коде я опустил много кода, не связанного с созданием экземпляров pp.Я думаю, что понятнее смотреть исходный код, чем читать его построчно.

После четырех раундов мы, наконец,runtime-core/apiCreateAppнайти вcreateAppметод и экземпляр приложения. Вы можете увидеть экземпляр приложения иVue2API в конструкторе Vue в основном такой же. Например, в экземпляре приложенияuse,componentВ итоге будет возвращен экземпляр приложения, поддерживающий цепочку записи.

От вызова метода createApp до создания экземпляра приложения мы можем примерно увидетьruntime-domКак этот модуль основан наruntime-coreСоздайте визуализатор виртуального дома для браузера.

/**
 * 在 runtime-dom 里,调用 runtime-core 的 createRenderer 方法
 * 并传入 rendererOptions,这个 rendererOptions 里面其实包含着浏览器的DOM API,props
 * 例如 createElement、insertBefore 等,大家可以去 runtime-dom/nodeOps.ts 里面看看。
 * **/
function ensureRenderer() {
  return renderer || (renderer = createRenderer<Node, Element>(rendererOptions))
}

/**
 * 传入不同环境的 endererOptions,就可以生成不同环境的render
 * **/
function baseCreateRenderer(
  options: RendererOptions,
  createHydrationFns?: typeof createHydrationFunctions
) {
  // 这些变量最终都是为 redner 里 patch 服务的
  const {
    insert: hostInsert,
    remove: hostRemove,
    patchProp: hostPatchProp,
    createElement: hostCreateElement,
    // ...
  } = options

  // 此处生成对应浏览器环境的 render
  const render: RootRenderFunction = (vnode, container) => {
    if (vnode == null) {
      if (container._vnode) {
        unmount(container._vnode, null, null, true)
      }
    } else {
      patch(container._vnode || null, vnode, container)
    }
    flushPostFlushCbs()
    container._vnode = vnode
  }

  return {
    render,
    hydrate,
    // 以参数形式 传入 createApp 中, 最终供 app实例里的 mount 使用。
    createApp: createAppAPI(render, hydrate)
  }

давайте продолжим искатьruntime-core/apiCreateAppвнутриcreateAppметод

export function createAppAPI(render, hydrate) {
  return function createApp(rootComponent, rootProps = null) {
    // ...
    const app: App = (context.app = {
      // ...
      /**
       *  我们在项目里创建 app实例,再 mount 到某一节点
       *  最终会执行到这里,App 组件作为 rootComponent, render是浏览器环境的渲染器。
       * **/
      mount(rootContainer: HostElement, isHydrate?: boolean): any {
        if (!isMounted) {
          const vnode = createVNode(
            rootComponent as ConcreteComponent,
            rootProps
          )
          vnode.appContext = context

          if (isHydrate && hydrate) {
            // ...
          } else {
            render(vnode, rootContainer)
          }
          isMounted = true
          app._container = rootContainer
          ;(rootContainer as any).__vue_app__ = app

          return vnode.component!.proxy
        }
      }
      // ...
    }
  }

Конечно, мы в проекте.mount('#app')Вместо прямого выполнения монтирования в экземпляре приложения метод монтирования здесьruntime-coreне зависит от платформы. Фактическиruntime-domMount был переписан, что также для среды браузера.

Но это, эта статья не будет продолжаться, потому что процесс монтирования примерно сводится к созданию Vnode, рендерингу, генерированию реального DOM, что полезноVue3Новая отзывчивость в Китае, поэтому мы можем сначала посмотреть на этот модуль, который можно использовать самостоятельно без других нагрузок.reactivity. (Выкопайте себе яму, я расскажу об этом позже -.-|)


Отзывчивость с Vue3

Proxy

мы знаем, что вVue2внутри черезObject.definePropertyAPI перехватывает изменения данных, глубоко обходит объекты в функции данных и устанавливает каждое свойство в объекте.getter,setter.

вызыватьgetterпройдешьDepКласс выполняет операцию сбора зависимостей и собирает текущиеDep.target, то есть,watcher.

вызыватьsetter, выполнит операцию обновления отправки, выполнитdep.notifyСобраны различные типы уведомленийwatcherобновление, напримерcomputed watcher,user watcher,渲染 watcher.


Vue3использоватьProxyРефакторинг отзывчивой части,effectвместо этого функция побочного эффектаwatcher,

ProxyизgetВыполнить в ручкеtrack()Используется для отслеживания зависимостей коллекций (коллекцияactiveEffect, то есть,effect),

setВыполнить в ручкеtrigger()Используется для запуска ответа (того, который выполняет сборeffect)

автономный отзывчивый

неоднократно упоминалось ранее,reactivityЕго можно использовать независимо, например, мы используем его в node.

// index.js
const { effect, reactive } = require('@vue/reactivity');
// reactive 定义响应式数据,也就是用proxy 设置 get、set handle
const obj = reactive({ num: 1 });

// effect 定义副作用函数
effect(() => {
  console.log(obj.num);
});

// 修改num, trigger 触发响应,执行 effect
setInterval(() => {
  ++obj.num;
}, 1000);

node index.jsЗапустив этот скрипт, вы увидите, что консоль всегда будет печатать приращения.

reactive

Тогда давайте посмотрим на этот код@vue/reactivityвнутреннийreactive, effectБар. Мы сосредоточимся только на том, что нас сейчас волнует, после того как основной процесс в этом направлении будет завершен, разберемся с остальными методами.

Напишите краткое введениеReactiveFlagsЭто перечисление, потому что оно будет использоваться позже

export const enum ReactiveFlags {
  SKIP = '__v_skip',  // 这个属性值为 true 的对象 都会被跳过代理
  IS_REACTIVE = '__v_isReactive', // 获取是否是响应式
  IS_READONLY = '__v_isReadonly', // 是否是只读的
  RAW = '__v_raw' // 这个属性会应用到原始对象
}

Перейдите к рассмотрению реактивных методов.

export function reactive(target: object) {
  // 如果是只读的响应式数据,直接会返回本身哈
  if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
    return target
  }
  return createReactiveObject(
    target, // 对象
    false,  // 是否只读
    mutableHandlers, // proxy handle
    mutableCollectionHandlers  // 集合数据的 proxy handle
  )
}

function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>
) {
  if (!isObject(target)) {
    // 不是对象 直接返回
    return target
  }

  // 如果已经是响应式对象则直接返回, 除非是 readonly 作用在这个响应式对象上
  if (
    target[ReactiveFlags.RAW] &&
    !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
  ) {
    return target
  }

  // 一个缓存map,key是 target对象, value是响应式对象
  // 如果这个对象已经创建过响应式对象,则从 缓存map中读出返回
  const proxyMap = isReadonly ? readonlyMap : reactiveMap
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }

  // 带有 skip 标记 、被冻结等至不可扩展的,类型不是object array map set weakmap weakset 都在白名单之外,不创建代理
  const targetType = getTargetType(target)
  if (targetType === TargetType.INVALID) {
    return target
  }

  // 使用 proxy 来创建响应式对象
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  // 存入 缓存map 中
  proxyMap.set(target, proxy)
  return proxy
}

Какие операции перехватываются в baseHandlers?

// reactivity/baseHandlers.ts => mutableHandlers
// 这里我们是选择了普通对象的handlers来看的
export const mutableHandlers: ProxyHandler<object> = {
  get, // 访问对象属性的handler
  set, // 设置对象属性的handler
  deleteProperty, //删除对象属性handler
  has, // 针对 in 操作符的handler
  ownKeys // 对象上getOwnPropertyNames、getOwnPropertySymbols、keys 等方法的handler
};

использоватьProxyПреимущество в том, что я считаю, что многие люди что-то знают, даже если они не читали исходный код. НапримерProxyвыдумалObject.definePropertyТребуется рекурсивный объект, устанавливающий каждое свойствоsetter,getter, нет возможности перехватить некоторые другие операции, массивы также нужно взломать, требуются дополнительные методы для обработки новых свойств, а такие структуры данных, как Map, Set и weakMap, не могут реагировать.

Суммировать

Некоторые из вышеперечисленных используютсяProxyПозже мы увидим, какие еще оптимизации были сделаны в реализации кода. Пока мы понимаем первое предложение в примере, сначала выполнитеreactive, проксировать исходный объект, который мы передали, и возвращать прокси, который называется отзывчивым объектом. Наконец, мы назначаем возвращенный отзывчивый объектobj.

effect

Следуя последовательности приведенного выше примера, давайте посмотрим на это предложение

  effect(() => {
    console.log(obj.num);
  });

мы даемeffectВ методе передается функция() => { console.log(obj.num); }, функция обращается к свойству num реактивного объекта obj. Тогда давайте посмотримeffectисходный код.

export function effect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
  if (isEffect(fn)) {
    // 1. 如果 fn 有 effect 函数标示,就指向原始函数,在下面 createReactiveEffect 里就可以看到raw、_isEffect的定义
    fn = fn.raw
  }
  // 2. 创建一个响应式副作用函数
  const effect = createReactiveEffect(fn, options)
  if (!options.lazy) {
    // 3. 执行effect, 这个有没有像 computed watcher 的 lazy 属性 ,若为true就不立即执行,
    effect()
  }
  // 4. 返回包裹着 fn 的 effect 函数
  return effect
}

let uid = 0

const effectStack = [] // effect栈,记得 Vue2 里面 全局存 Watcher 的栈吗?
function createReactiveEffect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions
): ReactiveEffect<T> {
  const effect = function reactiveEffect(): unknown {
    if (!effect.active) {
      return options.scheduler ? undefined : fn()
    }

    if (!effectStack.includes(effect)) {
      // 这是一个优化,清除的 deps 里所有 dep 里的 effect,配合后面 track 里给 effect deps重新加 dep,相当于清除掉不需要的依赖,后面会详细说到 
      cleanup(effect)
      try {
        // 开启允许收集 也就是设这个变量shouldTrack为 true
        enableTracking()
        // 以下是压栈, 设置activeEffect,执行原始函数
        effectStack.push(effect)
        activeEffect = effect
        return fn() // 这里执行原始函数,就是引用到我们在函数里写的 响应式的对象的值,触发的响应式对象的 get handler
      } finally {
        // 最终出栈,停止收集,activeEffect 回指上一个effect,这里是对有嵌套关系的effect有作用
        effectStack.pop()
        resetTracking()
        activeEffect = effectStack[effectStack.length - 1]
      }
    }
  } as ReactiveEffect
  // 下面是effect的相关属性
  effect.id = uid++
  effect.allowRecurse = !!options.allowRecurse
  effect._isEffect = true
  effect.active = true
  effect.raw = fn
  effect.deps = [] // effect 对 dep 的双向依赖
  effect.options = options
  return effect
}

function cleanup(effect: ReactiveEffect) {
  // deps 是一个 数组包着 Set集合的数据结构   [Set1(...), Set2(...), ...], 每一个 Set 就是,targetMap里面的dep
  const { deps } = effect
  if (deps.length) {
    for (let i = 0; i < deps.length; i++) {
      deps[i].delete(effect)
    }
    deps.length = 0
  }
}

Суммировать

На данный момент мы знаем, что выполнение эффекта (fn) в примере создаст, выполнит и вернет функцию эффекта.effectStackстек, глобальныйactiveEffectУказатель указывает на себя и выполняет входящий fn, который срабатывает в это времяobjобработчик получения. После выполнения fn выполнить unstack, остановить сбор,activeEffectУказывает на предыдущий эффект в стеке.

получить, сбор зависимостей

Как было сказано выше, выполняется входящий fn, который срабатывает в это время.objОбработчик get, затем давайте посмотрим на исходный код get.

function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {
    if (key === ReactiveFlags.IS_REACTIVE) {
      return !isReadonly
    } else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly
    } else if (
      key === ReactiveFlags.RAW &&
      receiver === (isReadonly ? readonlyMap : reactiveMap).get(target)
    ) {
      return target
    }
    // 还记得 ReactiveFlags 枚举里面的那些值吗, 都是通过上面那些判断来获取到相应的值,也因为都是私有属性,得到值直接返回即可,没必要往下走了

    const targetIsArray = isArray(target)
    // ['includes', 'indexOf', 'lastIndexOf'] 数组改变,这方法的结果可能也会发生改变,所以 get 里面做了特殊处理
    // 例如 执行arr.includes('xx') 时,会跟踪 arr 数组每一个下标
    if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
      return Reflect.get(arrayInstrumentations, key, receiver)
    }

    // 利用Reflect映射返回值
    const res = Reflect.get(target, key, receiver)

    if (
      isSymbol(key)
        ? builtInSymbols.has(key as symbol)
        : key === `__proto__` || key === `__v_isRef`
    ) {
      // 判断原生方法等,直接返回,不track了
      return res
    }

    if (!isReadonly) {
      // track 依赖收集操作
      track(target, TrackOpTypes.GET, key)
    }

    if (shallow) {
      // 这是表示浅响应的,比如 shallowReactive 方法,后面也会简单介绍下
      return res
    }

    if (isRef(res)) {
      // 这里 ref 是 reactivily 里面另一个api,实现相对简单,可以对基本类型创建个响应式对象,例如 num = ref(0) 会创建一个 value为0的响应式对象, isRef 就是判断是不是 ref 值
      const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
      return shouldUnwrap ? res.value : res
    }

    if (isObject(res)) {
      // 可以看到哈,子对象也是需要递归的去劫持的,但这里相比 Vue2 有个优化的点,
      // Vue2里面如果属性仍是个对象 数组等,则立即遍历子对象去做劫持
      // 而 Vue3 则是在访问到这个属性,发现值是对象再去转为响应式对象

      return isReadonly ? readonly(res) : reactive(res)
    }

    return res
  }
}

Вышеупомянутое завершеноgetобработчик, вы можете обнаружить, что операция get верна первойReactiveFlagsЗначения в этом перечислении перехватываются, а затем специальные методы в массиве оцениваются и обрабатываются отдельно, а затем используютсяReflectэтого хорошего партнера, чтобы оценить, а затемtrackДелать вещи, связанные со сбором зависимостей, и, наконец, возвращать результат. Тогда давайте посмотрим на трек.

// ./effect.ts

// 先看两个使用到的变量
activeEffect // 类似于 Vue2 里的 Dep.target, 一个watcher。 这里表示当前激活的effect
shouldTrack // 判断当前是否应该收集 ,effect函数内部执行一开始设这个变量为true
targetMap // 以原始对象为健 ,值也是一个weakMap,map以属性名为key,effect集合为value
export function track(target: object, type: TrackOpTypes, key: unknown) {
  if (!shouldTrack || activeEffect === undefined) {
    return
  }

  let depsMap = targetMap.get(target)
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  let dep = depsMap.get(key)
  if (!dep) {
    depsMap.set(key, (dep = new Set()))
  }
  // 以上这一堆判断,缓存,最终会形成 targetMap 样的数据结构
  /** 
   *  targetMap = {
   *    [target]: {
   *      [key]: new Set([ effect,... ])
   *    }
   *  }
  **/

  if (!dep.has(activeEffect)) { // effect 执行时,将 activeEffect 指向自己
    dep.add(activeEffect)
    // 当前激活的 effect 也会存储 dep集合,这其实是配合 effect 里面 cleanup 方法 清除不需要的依赖
    activeEffect.deps.push(dep)
  }
}

Почему вы говорите, что activeEffect.deps.push(dep) должен сотрудничать с очисткой для удаления ненужных зависимостей, см. следующий пример

// 例2: 多了一个 count 属性
const obj = reactive({num: 1, count: 0});

effect(() => {
  if (obj.num === 1) {
    ++obj.num
    // 我们只有在 num 为 1 的是时候再去访问 count
    console.log('count', obj.count)
  }
  console.log('num', obj.num)
},);

setTimeout(() => {
  // 因为第一次访问到了 count, 所以 我们修改 count 会去执行 effect
  console.log('第一次修改 count')
  obj.count = 2
}, 1000);

setTimeout(() => {
  // 上一次执行effect的时候,cleanup清除了所有依赖,但因为 num 为 2
  // 函数内部不会访问到 count ,并未去 track count,也就不会有重新 activeEffect.deps.push(dep) 这个操作
  // 所以修改 count 并不会执行 effect
  console.log('第二次修改 count')
  obj.count = 3
}, 1000);

setTimeout(() => {
  // num 每次都有访问到,所以正常触发响应式
  console.log('第三次修改 num')
  obj.num = 3
}, 1000);

В этом примере при первом выполнении эффекта он выполняется автоматически, и значение effect.deps будет равно[dep, dep], которые являются набором зависимостей num и набором зависимостей count соответственно.

Затем измените счетчик, чтобы вызвать выполнение эффекта, сначала выполнитеcleanup, удалит эффекты в коллекциях num и count dep,

Когда выполняется fn, поскольку осуществляется доступ только к num, зависимость от num добавит эффект, а зависимость от count — нет. И effect.deps будет толкать только num deps.

Поэтому, когда счетчик будет изменен позже, эффект не сработает.

Суммировать

Наконец, вернемся к основному процессу.В первом примере мы посещаем входящий fnobj.num, вызовем обработчик get, он получит значение 1 через Reflect.get, затем отследим наше свойство num, соберем эффект, и, наконец, структура targetMap будет следующей:

targetMap = {
  [obj]: {
    'num' : new Set([effect])
  }
}

установить, отправить обновления

Предыдущий раздел основан на процессе сбора и завершения. Среди них мы сказали, что изменение num вызовет выполнение эффекта, который на самом деле является отправкой обновлений, то есть выполнением обработчиков набора. Давайте посмотрим на исходный код обработчика set.

// ./baseHandlers.ts

function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    // 1. 先取old值
    const oldValue = (target as any)[key]
    if (!shallow) {
      value = toRaw(value) // toRaw 就是取原始对象,这里如果value是响应式对象,则一直取到原始对象
      if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
        // 如果老的值是 Ref 响应对象,而更新的值不是,那更新老的响应式对象的 value 属性值即可,不需要执行这里的trigger
        oldValue.value = value
        return true
      }
    } else {
      // in shallow mode, objects are set as-is regardless of reactive or not
    }
    
    // 2. 判断当前 set 的 key 存不存在与 target上
    const hadKey =
      isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : hasOwn(target, key)

    // 3. Reflect.set 求值 
    const result = Reflect.set(target, key, value, receiver)

    // 这里原始数据上原型链上数据操作,Reflect.set修改后,会再进来,所以有了这判断
    if (target === toRaw(receiver)) {
       // 4. 通过有没有当前 key 是不是已存在来决定是 add 的 triger,还是 set 的 trigger,set会多一个oldvalue
      if (!hadKey) {
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}

После прочтения логики set комментарии примерно обозначены как 4 шага, давайте посмотрим на триггер на последнем шаге

export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
  // 从之前 track 里面存储的 targetMap 里取出对应 depsMap
  const depsMap = targetMap.get(target)

  if (!depsMap) {
    // 没有依赖,直接返回,不会触发后面effect的执行
    return
  }

  const effects = new Set<ReactiveEffect>()
  const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
    if (effectsToAdd) { // 这是包含 effects 的 Set集合
      effectsToAdd.forEach(effect => {
        if (effect !== activeEffect || effect.allowRecurse) {
          // add 方法是要把 effect 统一 收集到 effects 这个集合里
          effects.add(effect)
        }
      })
    }
  }

  // ...
  add(depsMap.get(key)) // 把dep集合放到effects里
  // ...

  const run = (effect: ReactiveEffect) => {
    if (effect.options.scheduler) {
      // 一个调度器,可以去做排序、去重、放入 nexttick 中异步执行
      // 这个调度器我们也是可以自定义的
      effect.options.scheduler(effect)
    } else {
      // 否则直接执行
      effect()
    }
  }

  // 开始执行
  effects.forEach(run)
}

Суммировать

Пока что мы понимаем процесс распространения и обновления этого фонда.Согласно исходному примеру, Мы модифицируем num, запускаем обработчик set и удаляем dep, соответствующий num в targetMap. Эффекты в dep добавляются в большую коллекцию эффектов с помощью метода add. Наконец, выполните метод run для обхода и выполнения эффектов в Effects.

Суммировать

А пока учимсяVue3Начало закончилось. Поскольку мы новички, мы не можем делать все, и мы не можем понять функцию каждого кода, мы можем понять только самые важные и самые основные процессы. Как и в случае с деревом, после того, как мы разберемся с самым основным стволом, мы можем продолжить изучение других ветвей.

Например, на этот раз в адаптивном анализе есть много других API.Ref,shallowReactive,readonly shallowReadonlyподождите, но я думаю, что мы прошлиreactiveПосле понимания полной функции реактивного побочного эффекта, сбора зависимостей и основы обновления дистрибутива будет намного легче оглянуться назад на этот API.

ps: На этот раз мы понимаем базовую отзывчивость, мы можем изучить ее сноваVue3Логика рендеринга, API композиции и т.д., процесс обучения других модулей будет написан позже.Если что-то не так, поправьте меня, спасибо!