Анализ исходного кода Vue3 — обнаружение данных

Vue.js

Эта статья синхронизирована в личном блогеshymean.comвверх, добро пожаловать, чтобы следовать

Vue3.0 выпустил бета-версию, давайте посмотрим на исходный код. В этой серии будет две статьи, включая общий процесс подачи заявки, новую адаптивную систему и API композиции.

Одной из особенностей Vue является система реагирования на данные.Поскольку это относительно независимая система, давайте начнем здесь и посмотрим, как новый прокси-сервер реализует сбор зависимостей данных и уведомление.

Содержание этой статьи немного длинное и может быть прочитано главами по оглавлению.

Ссылаться на:

Предварительные знания

Прежде чем учиться, вам нужно знать некоторые предпосылки JavaScript:Reflectа такжеProxy.

Reflect

ReflectИнкапсулировать операции некоторых объектов, в основном для интеграции некоторых нестандартных мест, существовавших в JS раньше

Я чувствую, что роль Reflect в основном состоит в том, чтобы интегрировать некоторые разрозненные инструментальные функции вReflectна этом объекте, напримерin,deleteравный оператор иFunction.prototype.applyи т.д. API

Более интересной особенностью являетсяReflect.getтретий параметрreceiver

var a = {
  name: 'a',
  get greet(){
    return 'hello ' + this.name
  }
}

console.log(a.greet); // "hi, I am a"

var receiver = {
  name: 'receiver'
}
// receiver 修改类似于 通过函数的apply修改内部this指向一样,不过这里修改的是访问对象
// 通过这个功能,可以实现新对象借助原对象部分属性和方法的功能
var res = Reflect.get(a, 'greet', receiver); // "hi, I am receiver"
console.log(res)

JavaScript Proxy

Для того, чтобы собирать зависимости и обновления данных, когда изменения данных, Vue2 пропускаетdefinePropertyПерехватите набор и получите дескрипторы доступа к свойствам.

definePropertyЕсть некоторые проблемы, самая распространенная проблема в том, что нельзя контролировать объект и динамически добавляемые свойства массива, даже если Vue2 переписывает методы, связанные с прототипом массива, его все равно нельзя отслеживать.arr[1]=xxxэта форма.

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

Прокси-объекты используются для определения пользовательского поведения для основных операций.

const p = new Proxy(target, handler)

вhandlerОбъект — это объект-заполнитель, который содержит набор определенных свойств и содержит различные ловушки прокси-сервера.trap,如setполучить и т. д.

Некоторые детали, на которые следует обратить внимание

  • setМетод должен возвращать логическое значение в строгом режиме, еслиsetметод возвращаетfalsish(в том числе undefined, false и т. д.), будет выброшено исключение, эти детали более хлопотны, вы можете пройтиReflectиметь дело с

  • Если прокси-объект является массивом, при вызовеpush,popКогда вы ждете метод, не только элементы массива будут изменены, но иlengthи другие атрибуты, в это время, если проксиset, он будет запущен несколько раз.

let arr = [100,200,300]
let p = new Proxy(arr, {
  get(target, key) {
    return target[key]
  },
  set(target, key, value, receiver) {
    console.log('set value', key)
    target[key] = value
    return true
  }
})

p.push(400)
// set value 3 第一次data[3] = 400
// set value length 第二次 data.length = 4

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

  • Прокси могут только агенты
let o = {x:{y:100},z:10}
let p = new Proxy(o, {
    get(target, key) {
        console.log('get value', key)
        return target[key]
    },
    set(target, key, value, receiver) {
        console.log('set value', key)
        target[key] = value
        return true
    }
})

p.x.y =100 // 只输出了get value x, 无法监听到set value

Поэтому, когда прокси-объект представляет собой многоуровневую вложенную структуру, разработчику необходимо реализовать преобразование вложенного атрибутивного объекта вProxyобъект

let handler = {
    get(target, key) {
        console.log('get value', key)
        return target[key]
    },
    set(target, key, value, receiver) {
        console.log('set value', key)
        target[key] = value
        return true
    }
}

let x = {y:100}
let o = {x:new Proxy(x, handler),z:10}
let p = new Proxy(o, handler)

p.x.y =100
// get value x
// set value y

Таким образом, есть некоторые проблемы с прокси

  • При проксировании массива ловушка может срабатывать несколько раз, и необходимо реализовать дедупликацию.
  • Вложенные объекты необходимо вручную преобразовать в прокси, что можно реализовать рекурсивно.

TypeScript

Использование Vue2flowОбнаружение типов, полностью поддерживаемое Vue3TypeScript, поэтому чтение исходного кода требует некоторых знаний, связанных с TS, обратитесь к предыдущему

lerna

Vue3 принимаетlernaОрганизуйте исходный код проекта (сейчас многие крупные проекты используют lerna), я писал статью раньшеИспользование verdaccio и lerna и управление пакетами npmЗдесь не вводить слишком много.

среда разработки

Затем создайте среду разработки исходного кода.

# 如果下载比较慢的话可以从gitee上面克隆备份仓库
git clone git+https://github.com/vuejs/vue-next.git

# 安装依赖,需要一会
yarn install

# 开始rollup watch
npm run dev

cd packages/vue/examples/

существуетexamplesВы можете просмотреть различные демо-коды. Здесь мы напрямую выбираем код в каталоге композиции для просмотра нового синтаксиса. Настоятельно рекомендуется прочитать исходный код перед чтением исходного кода.документация по составу API, что очень важно для понимания дизайна API в Vue3! !

Затем измените исходный кодvue/src/index.ts, обновите страницу, вы увидите эффект изменения, а затем начните счастливое время исходного кода~

rective

Документация показываетrectiveБазовый вариант использования для

const { reactive, watchEffect, computed } = Vue
const state = reactive({
    count: 0
})

function render() {
    document.body.innerHTML = `count is ${state.count}`
}

watchEffect(render) // 初始化会调用一次render

setTimeout(() => {
    state.count = 100 // state.count发生变化,会通过watchEffect重新触发render
}, 1000)

выглядитreactiveМетод будет прокси, чтобы вернуть прокси-объект, иwatchEffectЗарегистрированная функция обратного вызова будет выполняться повторно при изменении свойства объекта.render.

Далее, давайте начнем с этих двух методов

function reactive(target: object) {
  return createReactiveObject(target,false,mutableHandlers,mutableCollectionHandlers)
}

function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>
) {
  // 检测target是否是对象,是否已经设置__v_isReactive、__v_skip、__v_isReadonly等
  // ...
  const observed = new Proxy(
    target,
    // 根据target是否是Set, Map, WeakMap, WeakSet对象来判断使用哪一种handler
    collectionTypes.has(target.constructor) ? collectionHandlers : baseHandlers 
  )
  
  return observed
}

Давайте сначала посмотрим на обычные объектыbaseHandlers, то есть входящийmutableHandlersэлемент конфигурации

export const mutableHandlers: ProxyHandler<object> = {
  get: createGetter(),
  set: createSetter(),
  // 下面三个方法均是通过`Reflect`来实现相关操作
  deleteProperty,
  has,
  ownKeys
}

createGetter собирает зависимости

const targetMap = new WeakMap<any, KeyToDepMap>()

function createGetter(isReadonly = false, shallow = false) {
  return function get(target: object, key: string | symbol, receiver: object) {
    // ... 根据 key与ReactiveFlags 返回特殊值
    // ... 处理target为数组时的get操作

    const res = Reflect.get(target, key, receiver)
    // ... 处理res是Ref类型的操作
    if (isRef(res)) {
      if (targetIsArray) {
        !isReadonly && track(target, TrackOpTypes.GET, key)
        return res
      } else {
        // ref unwrapping, only for Objects, not for Arrays.
        return res.value
      }
    }
    // 收集依赖
    !isReadonly && track(target, TrackOpTypes.GET, key)
    // 判断属性值类型,递归调用reactive处理,返回新的Proxy
    return isObject(res) ? reactive(res) : res
  }
}

видно только при включенииgetКогда тип значения атрибута обнаружен, а затем значение атрибута обрабатываетсяreactiveОперация, общая производительность должна иметь много улучшения с точки зрения рекурсивно угона, все получилось, когда Vue2 инициализируется.

Мы знаем, что нам нужно собирать зависимости при запуске get, вы можете видетьtrackВот что справляется с работой

export function track(target: object, type: TrackOpTypes, key: unknown) {
  // 初始化target的依赖列表,通过Map保存,每个依赖可能依赖target某个或某些属性,因此该Map的键值是target的每个属性
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  let dep = depsMap.get(key)
  // 对于每个属性key而言,通过Set保存依赖该key的activeEffect
  if (!dep) {
    depsMap.set(key, (dep = new Set()))
  }
  // activeEffect是一个全局变量
  if (!dep.has(activeEffect)) {
    dep.add(activeEffect)
    activeEffect.deps.push(dep)
  }
}

При рендеринге представления в методе рендеринга передатьgetвызыватьtrack,ПотомactiveEffectДобавьте в список зависимостей свойства данных, и все готово.Коллекция зависимостей.

Абстракция изменений эффектов

Мы столкнулись с новой концепцией здесьeffect, включая предыдущийeffect,watchEffectи другие методы. похожий наWatcherОбъект, используемый для инкапсуляции различных изменений.

На примереwatchEffectЗарегистрированную функцию обратного вызова можно понимать как эффект.

// 从类型声明可以看出,effect是一个包含如下属性的函数
export interface ReactiveEffect<T = any> {
  (...args: any[]): T
  _isEffect: true
  id: number
  active: boolean
  raw: () => T
  deps: Array<Dep>
  options: ReactiveEffectOptions
}

Тогда взглянитеwatchEffectреализация

export function watchEffect(
  effect: WatchEffect,
  options?: WatchOptionsBase
): WatchStopHandle {
  return doWatch(effect, null, options)
}

function doWatch(
  source: WatchSource | WatchSource[] | WatchEffect,
  cb: WatchCallback | null,
  { immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ
): WatchStopHandle {
  const instance = currentInstance
  // 根据source的类型封装getter,内部执行source的调用
  let getter: () => any = () => {return callWithErrorHandling(source, instance) }

  // 根据 flush字符串初始化调度器,决定何时调用getter
  let scheduler: (job: () => any) => void = job => queuePostRenderEffect(job, instance && instance.suspense)

  // 调用effect注册
  const runner = effect(getter, {
    lazy: true, // 由于下面会直接调用runner,因此lazy传入了true
    computed: true,
    onTrack,
    onTrigger,
    scheduler: applyCb ? () => scheduler(applyCb) : scheduler
  })

  recordInstanceBoundEffect(runner)
  runner()

  // 返回一个取消effect的方法
  return () => {
    stop(runner)
    if (instance) {
      remove(instance.effects!, runner)
    }
  }
}

Здесь мы видимmountComponentсерединаinstance.updateпохожийrunnerметоды, они на самом деле являются проходомeffectФункция упаковки.

const effectStack: ReactiveEffect[] = []
let activeEffect: ReactiveEffect | undefined

export function effect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
  if (isEffect(fn)) {
    fn = fn.raw
  }
  // 创建effect对象(函数)
  const effect = createReactiveEffect(fn, options)
  if (!options.lazy) {
    // 在前面初始化instance.update时会先调用一次 componentEffect,从而完成页面的初始化渲染 
    effect() 
  }
  return effect
}
// 下面这段源码基本是原样copy过来的,没有做删减
function createReactiveEffect<T = any>(
  fn: (...args: any[]) => T,
  options: ReactiveEffectOptions
): ReactiveEffect<T> {
  const effect = function reactiveEffect(...args: unknown[]): unknown {
    if (!effect.active) {
      return options.scheduler ? undefined : fn(...args)
    }
    if (!effectStack.includes(effect)) {
      cleanup(effect) // 清除effect.deps
      try {
        enableTracking()
        effectStack.push(effect)
        activeEffect = effect // 将全局变量activeEffect 设置为当前运行的effect,然后调用effect
        return fn(...args)
      } finally {
        // finally中的代码始终都能执行
        effectStack.pop()
        resetTracking()
        activeEffect = effectStack[effectStack.length - 1] // 将activeEffect重置为上一个effect
      }
    }
  } as ReactiveEffect
  effect.id = uid++
  effect._isEffect = true
  effect.active = true
  effect.raw = fn
  effect.deps = []
  effect.options = options
  return effect
}

прибратьсяwatchEffect(cb)обработать

  • существуетdoWatchинкапсулировать cb вgetterсередина,
  • передачаeffect(getter),пройти черезcreateReactiveEffectвозвращает истинуReactiveEffectфункция, возложенная наrunner
  • назадdoWatchСредний, напрямую звонитьrunnerвоплощать в жизнь
    • пройдет при запуске ReactiveEffecteffectStackУстановить текущую глобальную переменнуюactiveEffect

activeEffectочень важная глобальная переменная, в предыдущемwatchEffect(render), запустит рендеринг при инициализации

function render() {
  document.body.innerHTML = `count is ${state.count}`
}

Поскольку внутренний метод имеет внутренний доступstate.count, вызоветstateОперация get прокси, чтобы ее можно было использовать вtrackпри прохождении черезactiveEffectдоступ к пакетуrenderЭффект метода, так что когдаstate.countКогда происходит изменение, соответствующий эффект можно запустить снова.Давайте изменим метод рендеринга.

function render() {
  // document.body.innerHTML = `count is ${state.count}`
  console.log('render') // 只有第一次初始化的时候回打印render
  document.body.innerHTML = '123'
}

watchEffect(render)
state.count // 触发一个get,然而并没有activeEffect,因此不会收集到相关的依赖
setTimeout(() => {
  state.count = 100 // state.count更新时也不会触发render
}, 1000)

Как видите, если он не срабатывает в обратном вызовеstate.count, зависимости не могут быть правильно отслежены. комбинироватьgetа такжеactiveEffect, который может точно собирать соответствующий эффект при изменении каждого свойства, что очень эффективно.

Также вwatchEffectКак видно из исходного кодаrunnerвыполняется синхронно, после завершения выполненияactiveEffectсбросить, если мы находимся вrenderАсинхронный доступ к методуstate.count, и не может правильно отслеживать зависимости.

function render() {
  setTimeout(()=>{
    document.body.innerHTML = `count is ${state.count}` // 放在回调里面
  })
}
watchEffect(render)
setTimeout(() => {
  state.count = 100 // 也不会更新视图
}, 1000)

createSetter уведомляет об изменениях

Наконец, давайте посмотрим на установленный прокси

function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    const oldValue = (target as any)[key]
    // 处理 ... ref
    const hadKey = hasOwn(target, key)
    const result = Reflect.set(target, key, value, receiver)
    // 判断target === toRaw(receiver) ,不处理target原型链更新的情况
    if (target === toRaw(receiver)) {
      if (!hadKey) {
        // 动态添加属性的情况
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
        // 属性更新的情况
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}

а такжеgetсерединаtrackПо аналогии,triggerДолжна быть логика обновления данных и уведомления зависимостей для обработки

export function trigger(
  target: object,
  type: TriggerOpTypes, // 表示不同的变化,如ADD、SET等
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
  // 找到target某个key的依赖
  const depsMap = targetMap.get(target)

  const effects = new Set<ReactiveEffect>()
  const computedRunners = new Set<ReactiveEffect>()
  // 将对应key的变化添加到effects或者computedRunners中
  const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect => {
        if (effect !== activeEffect || !shouldTrack) {
          if (effect.options.computed) {
            computedRunners.add(effect)
          } else {
            effects.add(effect)
          }
        }
      })
    }
  }

  // 根据type和key找到需要处理的depsMap,这里处理了各种特殊情况
  add(depsMap.get(key))
  // ...

  // 遍历effects和computedRunners
  const run = (effect: ReactiveEffect) => {
    // 如果effect自己配置了scheduler,则使用调度器运行effect
    if (effect.options.scheduler) {
      effect.options.scheduler(effect)
    } else {
      effect() // 可以看见effect实际上是一个函数
    }
  }

  // 计算属性先运行,这样可以保证其他属性在运行时能够获取到计算属性的值
  computedRunners.forEach(run)
  effects.forEach(run)
}

Резюме: Реализуйте минималистскую версию реактивного

Предыдущий код игнорируетshallowнеглубокий реактивный,readonlyВ других случаях мы можем реализовать 50-строчный кодreactive,

let activeEffect
let targetMap = new Map()

function reactive(obj){
    return new Proxy(obj, {
        get(target, key){
            track(target, key)
            return target[key]
        },
        set(target, key, value){
            target[key] = value
            trigger(target, key)
            return true
        }
    })
}

function track(target, key){
    let depMap = targetMap.get(target)
    if(!depMap) {
        targetMap.set(target, (depMap = new Map()))
    }
    let dep = depMap.get(key)
    if(!dep) {
        depMap.set(key, ( dep = new Set()))
    }
    if(!dep.has(activeEffect)){
        dep.add(activeEffect)
    }
}

function watchEffect(cb){
    activeEffect = cb
    cb()
}

function trigger(target, key){

    let depMap = targetMap.get(target)
    if(!depMap) return 
    let effects =  depMap.get(key)
    if(!effects) return 
    
    effects.forEach((effect)=>{
        effect()
    })
}

затем проверить

let {reactive, watchEffect} = require('./reactive')


let state = reactive({
    x: 100
})

function render(){
  let msg = `render template with state.x = ${state.x}`
  console.log(msg)
}

watchEffect(render)

setTimeout(()=>{
    state.x = 200
}, 1000)

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

Избегайте повторного выполнения эффектов

Мы также упустили из виду более важную особенность: когда состояние зависимостей постоянно меняется, как избежать триггеров, которые не сравниваются в середине? Например, в демонстрационном коде выше

setTimeout(() => {
  state.x = 100
  console.log(state.x) // 100
  state.x = 200
  console.log(state.x) // 200
}, 1000)
// 会连续打印两次 render template with state.x = 100 | 200

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

В Vue2 Watcher будет добавлен в очередь и дедуплицирован при добавлении вnextTickв единой очереди выполнения. В приведенной выше демонстрации, поскольку activeEffectrenderметод, мы можем вызвать рендеринг только один раз с помощью debounce

Так как же реализован Vue3? Давайте посмотрим на это

Это видно при срабатывании

const run = (effect: ReactiveEffect) => {
    // 如果effect自己配置了scheduler,则使用调度器运行effect
    if (effect.options.scheduler) {
      effect.options.scheduler(effect)
    } else {
      effect() // 可以看见effect实际上是一个函数
    }
  }

существуетdoWatchВы можете увидеть код ниже

scheduler = job => queuePostRenderEffect(job, instance && instance.suspense)
const runner = effect(getter, {
  //... 其他配置
  scheduler
})

Вы можете видеть, что в эффекте, созданном doWatch,schedulerКонфигурация, которая вызывается при запуске эффектаscheduler, он будет выполнен здесьqueuePostRenderEffect

// 使用了两个全局队列来维护
const queue: (Job | null)[] = []
const postFlushCbs: Function[] = []

export const queuePostRenderEffect = __FEATURE_SUSPENSE__
  ? queueEffectWithSuspense
  : queuePostFlushCb

export function queuePostFlushCb(cb: Function | Function[]) {
  // 将effect放入postFlushCbs队列
  if (!isArray(cb)) {
    postFlushCbs.push(cb)
  } else {
    postFlushCbs.push(...cb)
  }
  queueFlush()
}

export function queueJob(job: Job) {
  if (!queue.includes(job)) {
    queue.push(job)
    queueFlush()
  }
}

function queueFlush() {
  if (!isFlushing && !isFlushPending) {
    isFlushPending = true
    nextTick(flushJobs) // nextTick直接使用的Promise.then,在nextTick中执行flushJobs
  }
}

function flushJobs(seen?: CountMap) {
  isFlushPending = false
  isFlushing = true
  let job

  // 组件由父组件向子组件更新,父组件的id始终比子组件小(先构造)
  queue.sort((a, b) => getId(a!) - getId(b!))

  while ((job = queue.shift()) !== undefined) {
    if (job === null) {
      continue
    }
    // 依次运行effect
    callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
  }

  flushPostFlushCbs(seen)
  isFlushing = false
  // 如果在运行过程中调用了queueJob或者queuePostRenderEffect,则继续执行flushJobs
  if (queue.length || postFlushCbs.length) {
    flushJobs(seen)
  }
}

// 依次运行postFlushCbs中的回调,在前面scheduler中添加的job就会通过queuePostRenderEffect放在postFlushCbs中
export function flushPostFlushCbs(seen?: CountMap) {
  if (postFlushCbs.length) {
    // 关键的异步,对postFlushCbs进行去重,这意味着即使postFlushCbs存在多个相同的effect,也只会被执行一次
    const cbs = [...new Set(postFlushCbs)]
    postFlushCbs.length = 0
    for (let i = 0; i < cbs.length; i++) {
      cbs[i]()
    }
  }
}

Код здесь относительно прост, чтобы показать, как выполняется наш эффект.

  • Во-первых, зарегистрировав планировщик элемента конфигурации, вызывайте планировщик (эффект) при запуске
  • Планировщик вызывает queuePostFlushCb, чтобы поместить эффект в глобальную очередь postFlushCbs, и одновременно регистрирует flushJobs в nextTick.
  • В клубнах очереди и PostFlushcbs будут опустошены.postFlushCbsРаньше postFlushCbs будет дедуплицироваться через Set
  • Таким образом, один и тот же эффект будет выполняться только один раз в одних и тех же flushJobs, независимо от того, сколько раз планировщик (эффект) вызывался в предыдущем триггере.

Ref

Предыдущий анализ исходного кода позволил нам понять, как прокси-объекты Vue3 через прокси-сервер, и отследить эффект при получении в Vue3.setТриггерный эффект.

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

Официальный сайтИзлагает простейшую схему реализации через замыкание иwatchEffectвыполнитьcomputed(Настоятельно рекомендуется сначала прочитать эту статью на официальном сайте, что очень полезно для понимания дизайна Vue3)

function computed(getter) {
  let value
  watchEffect(() => {
    value = getter()
  })
  return value
}

Затем возникает проблема, когда вычисляемое свойство возвращает значение базового типа, хотя его можно перезапустить.watchEffectобратный вызов и обновить значение, но не может уведомить предыдущую зависимость oldValue, Это связано с тем, что обычные типы в JavaScript передаются по значению, а не по ссылке.

Решение этой проблемы состоит в том, чтобы вернуть объект, а затем проксировать значение ответа для сбора зависимостей.Ref.

function computed(getter) {
  const ref = {
    value: null,
  }
  watchEffect(() => {
    ref.value = getter()
  })
  return ref
}

Далее начнем с вычисляемого и посмотрим на роль и реализацию Ref

Ниже приведен базовый демонстрационный код

const state = reactive({
  count: 1
})
const doubleCount = computed(() => {
  return state.count * 2
})

function render() {
  document.body.innerHTML = `count is ${doubleCount.value}`
}

watchEffect(render)

setTimeout(() => {
  state.count = 100
}, 1000)

computed

Нижеcomputedисходный код

export function computed<T>(
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
) {
  let getter: ComputedGetter<T>
  let setter: ComputedSetter<T>

  if (isFunction(getterOrOptions)) {
    getter = getterOrOptions
  } else {
    getter = getterOrOptions.get
    setter = getterOrOptions.set // 处理set computed
  }

  let dirty = true // 判断getter是否需要重新运算
  let value: T 
  let computed: ComputedRef<T>

  const runner = effect(getter, {
    lazy: true, // 只有当用到computed的时候才运行求职
    computed: true, // 将effect.computed标志为true,这样会在普通的effect之前运行
    // 自定义调度器,在trigger时调用
    scheduler: () => {
      // 通知使用了computed.value的effect,只有当计算属性的getter已经被运行过才进行通知
      if (!dirty) {
        dirty = true
        trigger(computed, TriggerOpTypes.SET, 'value')
      }
    }
  })
  computed = {
    __v_isRef: true,
    effect: runner,
    get value() {
      if (dirty) {
        value = runner() // 调用runner 完成activeEffect设置,这样当计算属性依赖的其他状态发生变化时,可以重新触发getter,
        dirty = false // 缓存已经计算过的值
      }
      // runner运行完毕后会重置activeEffect为上一个effect,然后将依赖该computed的activeEffect添加到依赖中
      track(computed, TrackOpTypes.GET, 'value')
      return value
    },
    set value(newValue: T) {
      setter(newValue)
    }
  } as any
  // 可以看见,计算属性返回的是一个特殊的对象
  return computed
}

Хорошо, это выглядит яснее, в сочетании с приведенным выше примером

const doubleCount = computed(() => {
  return state.count * 2
})
function render() {
  document.body.innerHTML = `count is ${doubleCount.value}`
}
watchEffect(render)

Упростите весь процесс, чтобы

上游数据 -> computed -> 下游数据

конкретный процесс

  • передачаcomputed(getter)Обертка будет инициализирована, когдаgetterэффект
  • Когда запускается вычисляемое свойствоget value, эффект, который будет работать
    • Триггер в это времяgetterПолучение восходящих данных, от которых зависит вычисляемое свойство, и в то же время путем вызоваtrackСоберите данные вниз по течению, которые зависит от текущей вычисленной собственности
    • При изменении исходных данных эффект будет запущен повторно, потому что он настраивается здесь.scheduler, поэтому будем использоватьscheduler(effect)способ запустить эффект,
    • существуетscheduler(effect)сбросить грязный в , то звонитеtriggerУведомлять нисходящие данные

Ref

Точно так же мы можем посмотреть на реализацию обычного Ref

export function ref(value?: unknown) {
  return createRef(value)
}
function createRef(rawValue: unknown, shallow = false) {
  if (isRef(rawValue)) {
    return rawValue
  }
  let value = shallow ? rawValue : convert(rawValue)
  const r = {
    __v_isRef: true,
    get value() {
      track(r, TrackOpTypes.GET, 'value')
      return value
    },
    set value(newVal) {
      if (hasChanged(toRaw(newVal), rawValue)) {
        rawValue = newVal
        value = shallow ? newVal : convert(newVal)
        trigger(
          r,
          TriggerOpTypes.SET,
          'value'
        )
      }
    }
  }
  return r
}

имеютcomputedопыта, кажется легче здесь и здесь, вget valueкогдаtrackсобрать activeEffect, вset value, если значение изменится, передатьtriggerЭффект уведомления.

резюме

В этой статье в основном организована новая адаптивная система в Vue3,

  • пройти черезeffectИзменения инкапсуляции в Vue2 черезWatcherОтветственный
  • Через набор и получение каждого атрибута прокси-объекта активный эффект собирается через дорожку во время получения, и все обновления зависимостей соответствующего атрибута уведомляются через триггер во время набора.
  • Сохраните зависимости каждого свойства в каждом реактивном объекте через Map
  • Равномерно запускайте эффект в nextTick через очередь эффектов и дедуплицируйте эффект через Set
  • Чтобы решить проблему передачи общих типов по значению, Vue3 реализует объект типа Ref,computedЕго также можно рассматривать как особый вид Ref.

Пока у меня есть определенное понимание адаптивной системы Vue3, пойдем посмотримСоставной APIДолжно быть удобнее.