mini-vue

By implementing a Mini-VUe3 to deepen my understanding of VUE, learn slowly and record what I have learned

This Mini-vue3 is learned through The Git warehouse of Cui Xiaorui. Teacher Ruan Yifeng recommended to learn Vue3

The warehouse address: Mini-Vue3

Source: the mini – vue

Pure own learning, if there are mistakes, but also hope you correct, including

In the study-every-day warehouse, there are big boss brain map, need to take oh ~

Vue3 dependency graph module

1. Responsive systemsreactivity

Speaking of VUE, we all know about vUE’s two-way data binding, which allows us to manipulate data and pages declaratively based on the MVVM model

Vue as the middle layer, responsive system, is an indispensable part, first to achieve a simple version of the reactivity responsive system module, based on this, to facilitate the implementation of the following functions

In Vue3, the ReActivity module, as a separate module, can be provided as a tool for others to use, so it is relatively independent and easy to read and understand what it is doing


1.1 Proxy Proxy object

Responsivity in VUe2 is implemented via get/set of the Object.defineProperty Api

Vue3 is implemented through the Proxy object. Why do YOU need to change it? There are many articles in this article.

  1. Based on Proxy Reflect reflection, can be agents in almost all of object’s behavior, there are a dozen, including the get/set/deleteProperty/definePropertygetOwnPropertyDescriptor etc, we should try to be no dead Angle response type

  2. Secondly, Proxy is the current research direction of browser, and its performance will only be better and faster in the future

For details, go to MDN to learn about Proxy

1.2 Reactivity

We have the Proxy to know the object to Proxy, can know the object’s property value changes, so when to collect related dependencies, create Proxy object?

These are functions that we’re familiar with

Reactive/Readonly these methods wrap our object as a proxy object and create getter/setter methods

The minimalist version

export function reactive(raw) {
  return new Proxy(raw, {
    get(target, key) {
      const res = Reflect.get(target, key)
      // Do dependency tracing
      track(target, key)
      return res
    }
  }),
  set(target, key, value) {
    const res = Reflect.set(target, key, value)

    // Trigger the update
    trigger(target, key)
    return res
  }
}
Copy the code

Since we need to distinguish whether isReadonly is a read-only object, we provide an identifier through a closure

function createGetter(isReadonly = false) {
  return function get(target, key) {
    const res = Reflect.get(target, key)

    // Dependency tracing, since read-only objects do not change, we do not need to trace
    if(! isReadonly) { track(target, key) }return res
  }
}
Copy the code

1.3 Effect Side effect function

By using Reactive/Readonly we get the proxy object, and it should be noted that there are two functions, track/trigger, that do dependency tracking and data change notification

Our responsive system needs to create dependencies through the effect side effect function, in vue’s case the render function, which accesses the properties of the proxy object and triggers get to get our dependencies, Then, when the data changes, the render function in the set can be told to create a new VNode to compare the diff algorithm and update the DOM

This is a simple version of the model and the implementation of the data structure, in the source code, VUE will be fn abstract into a class, encapsulation, convenient to achieve more functions

Look at the code

/ * * *@description Side effects function, collect dependencies *@param { Function } fn* /
export function effect(fn, options?) {
  // 1. Initialize
  const _effect = newReactiveEffect(fn, options? .shceduler) extend(_effect, options)// 2. Call the 'run' method, that is, call fn to trigger the internal 'get/set'
  _effect.run()

  // 3. Return 'runner' function
  const runner: any = _effect.run.bind(activeEffect)
  runner.effect = _effect
  return runner
}
Copy the code

ReactiveEffectabstractedfnclass

/ * * *@description The collected dependent function class */
export class ReactiveEffect {
  private _fn: () = > void

  publicshceduler? :() = > void | undefined

  deps: any[] = []

  constructor(fn: () => void, shceduler? : () = >void) {
    this._fn = fn
    this.shceduler = shceduler
  }

  run() {
    // 1. Set the target of dependency collection to the current instance
    activeEffect = this
    // 2. Execute 'fn' and call internal 'get' to collect 'fn'
    const result = this._fn()

    return result
  }
}
Copy the code

track: Collects dependent functions

/ * * *@description When the 'get' method is called, dependency collection * is performed@param Target The object currently tracked *@param Key Indicates the current access key */
export function track(target, key) / /console.log('trigger track -> target:${target} key:${key}`) // Get the current tracing objectlet depsMap = targetMap.get(target) // Check whether a dependency exists. If not, add itif (! depsMap) {
    depsMap = new Map()
    targetMap.set(target, depsMap)
  }

  // @desc: get the dependency of the current object's 'key'
  let dep = depsMap.get(key)
  // If no, add one
  if(! dep) { dep =new Set()
    depsMap.set(key, dep)
  }

  // @desc: Manually trigger 'track' to allow others to join a responsive system such as' ref '
  trackEffects(dep)
}

export function trackEffects(dep) {
  // @desc: If it has already been added, avoid adding it again
  if(! dep.has(activeEffect)) {// Add the dependency to the corresponding 'dep'
    dep.add(activeEffect)
    activeEffect.deps.push(dep)
  }
}
Copy the code

**trigger: ** Trigger dependent change method

/ * * *@description When the 'set' method is called, the change function * is triggered@param Target The object currently tracked *@param Key Indicates the current access key */
export function trigger(target, key) {
  // console.log(' trigger trigger -> target: ${target} key:${key} ')

  const depsMap = targetMap.get(target)
  const dep = depsMap.get(key)

  // @desc: Manually trigger 'trigger' so that others can join the responsive system, such as' ref '
  triggerEffects(dep)
}

export function triggerEffects(dep) {
  for (const effect of dep) {
    if (effect.shceduler) {
      effect.shceduler()
    } else {
      effect.run()
    }
  }
}
Copy the code

1.4 API implementation

The basic shelf is set up, through the model and basic introduction, there is a basic concept, the following to specific implementation of Vue3 in various apis

effect

Look at the basic implementation

/ * * *@description Side effects function, collect dependencies *@param { Function } fn* /
export function effect(fn, options?) {
  // 1. Initialize
  const _effect = newReactiveEffect(fn, options? .shceduler)// Object.assign makes it easy to add attributes to instances, such as onStop, to listen for callbacks to exit dependent systems
  extend(_effect, options)

  // 2. Call the 'run' method, that is, call fn to trigger the internal 'get/set'
  _effect.run()

  // 3. Return 'runner' function
  const runner: any = _effect.run.bind(activeEffect)
  runner.effect = _effect
  return runner
}
Copy the code

Effect side effects need to be used with dependencies, also known as reactiveeffects. Let’s look at collecting dependencies in detail

/ * * *@description The collected dependent function class */
export class ReactiveEffect {
  private _fn: () = > void
  After the first reactive trigger, let the user decide what to do with subsequent set operations
  publicshceduler? :() = > void | undefinedonStop? :() = > void
  deps: any[] = []
  active: boolean = true

  constructor(fn: () => void, shceduler? : () = >void) {
    this._fn = fn
    this.shceduler = shceduler
  }

  run() {
    // After executing 'stop', dependency collection should be avoided and the dependency collection switch should not be enabled
    // Fn function execution rights are retained because it exits the reactive system
    if (!this.active) {
      return this._fn()
    }

    // 1. Enable dependency collection
    shouldTrack = true
    // 2. Set the target for dependency collection
    activeEffect = this
    // 3. Execute 'fn' and call internal 'get' to collect 'fn'
    const result = this._fn()
    // 4. Disable the dependency collection function
    shouldTrack = false

    return result
  }

  Exit the responsive system
  stop() {
    // Whether in a responsive system
    if (this.active) {
      clearupEffect(this)
      // If a callback is given, the callback is performed
      if (this.onStop) this.onStop()
      this.active = false}}}Copy the code

ActiveEffect is set when the _fn method is called in the run method, which is passed in at the time of the call, and then collected into the reactive mapping table, which is the track method of get

/ * * *@description When the 'get' method is called, dependency collection * is performed@param Target The object currently tracked *@param Key Indicates the current access key */
export function track(target, key) {
  // @desc: No collection status, return directly
  if(! isTracting())return

  // console.log(' trigger track -> target: ${target} key:${key} ')

  // Get the current trace object 'targetMap' is a global variable used to manage the entire project
  let depsMap = targetMap.get(target)
  // Check whether a dependency exists. If not, add it
  if(! depsMap) { depsMap =new Map()
    targetMap.set(target, depsMap)
  }

  // @desc: get the dependency of the current object's 'key'
  let dep = depsMap.get(key)
  // If no, add one
  if(! dep) { dep =new Set()
    depsMap.set(key, dep)
  }

  // @desc: Manually trigger 'track' to allow others to join a responsive system such as' ref '
  trackEffects(dep)
}

export function trackEffects(dep) {
  // @desc: If it has already been added, avoid adding it again
  if(! dep.has(activeEffect)) {// Add the dependency to the corresponding 'dep'
    dep.add(activeEffect)
    activeEffect.deps.push(dep)
  }
}

export function isTracting() {
  return shouldTrack && activeEffect
}
Copy the code

The trigger in the set is invoked when the next responsive object changes

/ * * *@description When the 'set' method is called, the change function * is triggered@param Target The object currently tracked *@param Key Indicates the current access key */
export function trigger(target, key) {
  // console.log(' trigger trigger -> target: ${target} key:${key} ')

  const depsMap = targetMap.get(target)
  const dep = depsMap.get(key)

  // @desc: Manually trigger 'trigger' so that others can join the responsive system, such as' ref '
  triggerEffects(dep)
}

export function triggerEffects(dep) {
  for (const effect of dep) {
    if (effect.shceduler) {
      // If the user needs to own the operation, use this scheme
      effect.shceduler()
    } else {
      effect.run()
    }
  }
}
Copy the code

reactive/readonly

These two methods are used to create reactive objects, and this is a Proxy object

// Reuse logic through encapsulation
export function reactive(raw) {
  return createActiveObject(raw, mutableHandlers)
}

export function readonly(raw) {
  return createActiveObject(raw, readonlyHandlers)
}

function createActiveObject(raw: any, baseHandler) {
  return new Proxy(raw, baseHandler)
}

function createGetter(isReadonly = false, shallow = false) {
  return function get(target, key) {
    // isReactive()
    if (key === ReactiveFlag.IS_REACTIVE) {
      return! isReadonly }// isReadonly()
    if (key === ReactiveFlag.IS_READONLY) {
      return isReadonly
    }

    const res = Reflect.get(target, key)
    
    // Whether shallow proxy, if yes directly return
    if (shallow) {
      return res
    }
    
    // Deep proxy
    if (isObject(res)) {
      return isReadonly ? readonly(res) : reactive(res)
    }

    // Do dependency tracing
    if(! isReadonly) { track(target, key) }return res
  }
}

function createSetter(isReadonly = false) {
  return function set(target, key, value) {
    if (isReadonly) {
      // Cannot be set, an error message is reported
      console.warn(`Cannot be edited key: The ${String(key)}, it is readonly`)
      return true
    }

    const res = Reflect.set(target, key, value)

    // Trigger the update
    trigger(target, key)
    return res
  }
}

// Avoiding repeated calls to 'createGet/set' can be done by caching
const get = createGetter()
const set = createSetter()

const readonlyGet = createGetter(true)
const readonlySet = createSetter(true)
export const mutableHandlers = {
  get,
  set
}

export const readonlyHandlers = {
  get: readonlyGet,
  set: readonlySet
}
Copy the code

And the response of the shallow shallowReactive/shallowReadonly type object

shallowReactive/shallowReadonly

/ / Readonly similarly
export function shallowReadonly(raw) {
  return createActiveObject(raw, shallowReadonlyHandlers)
}

const shallowReadonlyGet = createGetter(true.true)
// set, use readonly, because no new values can be set
export const shallowReadonlyHandlers = extend({}, readonlyHandlers, {
  get: shallowReadonlyGet
})
Copy the code

isReactive/isReadonly/isProxy

// See if there is a closure flag by triggering get
export const enum ReactiveFlag {
  IS_REACTIVE = '__v_reactive',
  IS_READONLY = '__v_readonly'
}
// When creating a reactive object, it is operated through a closure, so whether or not it is a reactive object can be provided through the closure identifier
// if (key === ReactiveFlag.IS_REACTIVE) {
// return ! isReadonly
// }
// if (key === ReactiveFlag.IS_READONLY) {
// return isReadonly
// }
export function isReactive(value) {
  return!!!!! value[ReactiveFlag.IS_REACTIVE] }export function isReadonly(value) {
  return!!!!! value[ReactiveFlag.IS_READONLY] }export function isProxy(value) {
  return isReactive(value) || isReadonly(value)
}
Copy the code

ref

Reactive is recommended because it has its drawbacks. It involves differences in assignment, such as direct assignment and missing response, which is the difference between variable assignment and attribute assignment. Ref changes the value property of the object, so the reactive will exist no matter how it is changed, while reactive is directly assigned to the variable, which is the memory reference of the changed variable

Look at the simplified implementation

class RefImpl {
  private _value: any

  private _raw: any

  public dep: Set<ReactiveEffect>

  public __v_isRef = true

  constructor(value) {
    this._raw = value
    this._value = convert(value)
    this.dep = new Set()}get value() {
    // How to add responsive, manual 'track', then need to own 'trigger'
    trackRefValue(this)
    return this._value
  }

  set value(newValue) {
    if (hasChanged(this._raw, newValue)) {
      this._raw = newValue
      this._value = convert(newValue)
      triggerEffects(this.dep)
    }
  }
}

function convert(value) {
  return isObject(value) ? reactive(value) : value
}

function trackRefValue(ref) {
  if (isTracting()) {
    trackEffects(ref.dep)
  }
}

export function ref(value) {
  return new RefImpl(value)
}

export function isRef(ref) {
  return!!!!! ref.__v_isRef }export function unRef(ref) {
  return isRef(ref) ? ref.value : ref
}

export function proxyRef(objectWithRef) {
  return new Proxy(objectWithRef, {
    get(target, key) {
      // get operation to provide the result of unpacking
      return unRef(Reflect.get(target, key))
    },
    set(target, key, value) {
      // If the new value is ref directly, if not, value needs to be assigned
      if(isRef(target[key]) && ! isRef(value)) {return (target[key].value = value)
      }
      return Reflect.set(target, key, value)
    }
  })
}
Copy the code

computed

With computed, it’s really clever to use a switch to manipulate whether or not you want to use a cache. You don’t call get, you don’t trigger a function to get a value. It’s lazy, and it uses shceduler so that it can be lazy and get a new value even after it changes

class ComputedRefImpl {
  private _getter: any

  private _value: any

  private _dirty = true

  private _effect: ReactiveEffect

  constructor(getter) {
    this._getter = getter

    // @tips:
    // 1. With 'effect', responsive object changes trigger 'getters' on their own, so' _dirty 'is meaningless
    // 2. So use 'shceduler' to customize the operation after the dependency collection
    // 3. Set '_dirty' to 'true' to get the latest value the next time you call 'get'
    this._effect = new ReactiveEffect(getter, () = > {
      this._dirty = true})}get value() {
    // @desc: Use the switch to avoid repeatedly calling 'getter' and cache the return value '_value'
    if (this._dirty) {
      this._dirty = false
      return (this._value = this._effect.run())
    }

    return this._value
  }
}

// @TODO setter
export function computed(getter) {
  return new ComputedRefImpl(getter)
}
Copy the code

1.5 summarize

  1. By implementing the short version, we readvueThe source code, also can be more handy
  2. The implementation of the reactive system version is relatively simple compared to other packages, and we use the model, andget/setIn a way that triggerstrack/trigger, dependency collection and trigger updates, can clearly understand this mode, I have to say simple and easy to use,computedThe implementation is also very skilled, really need you to play their own imagination, they can also go to do more useful tools