vue3.0响应式原理(四)

最后简单介绍下ref与computed,首先先看ref的主要代码:

export function ref(raw?: unknown) {
  if (isRef(raw)) {
    return raw
  }
  raw = convert(raw)
  const r = {
    _isRef: true,
    get value() {
      track(r, OperationTypes.GET, 'value')
      return raw
    },
    set value(newVal) {
      raw = convert(newVal)
      trigger(
        r,
        OperationTypes.SET,
        'value',
        __DEV__ ? { newValue: newVal } : void 0
      )
    }
  }
  return r as Ref
}

Vue.reactive用于对象,Vue.ref则用于基础类型,例如:

const observed = Vue.ref('test')
Vue.effect(() => {
    console.log(observed.value) // test,test2
})
observed.value = 'test2'

通过源码其实能知道,ref其实就是通过将基础类型外面包了一层value属性,通过get set来进行发布与订阅,ref也可以与reactive搭配使用,当在reactive中使用时,会自动识别ref的value属性,例如:

const observed = Vue.reactive({name:'test',age:Vue.ref(20)})
Vue.effect(() => {
    console.log(observed.age)  // 20
})

age属性被包装成了对象,这样的好处是对象指针指向的是内存地址,将age属性赋值给一个变量,修改变量值值,由于指针指向相同内存空间,原值也会有变化,例如:

const age = Vue.ref(37)
const obj = Vue.reactive({
    age: age,
    person: {
        name:'test',
        age:age
    }
})
Vue.effect(() => {
    let age = obj.age
    let personAge = obj.person.age
})
obj.age = 40
console.log(obj.age, obj.person.age) //40,40

如果不使用ref的话,运行结果为40,37

又例如:

const ref = Vue.ref(18)
const observed = Vue.reactive({name:'test',age:ref})
Vue.effect(() => {
    let age = ref
    let personAge = observed.age
})
observed.age = 30
console.log(ref.value) // 30

最后再讲一下computed,源码如下:

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

  if (isFunction(getterOrOptions)) {
    getter = getterOrOptions
    setter = __DEV__
      ? () => {
          console.warn('Write operation failed: computed value is readonly')
        }
      : NOOP
  } else {
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }

  let dirty = true
  let value: T

  const runner = effect(getter, {
    lazy: true,
    // mark effect as computed so that it gets priority during trigger
    computed: true,
    scheduler: () => {
      dirty = true
    }
  })
  return {
    _isRef: true,
    // expose effect so computed can be stopped
    effect: runner,
    get value() {
      if (dirty) {
        value = runner()
        dirty = false
      }
      // When computed effects are accessed in a parent effect, the parent
      // should track all the dependencies the computed property has tracked.
      // This should also apply for chained computed properties.
      trackChildRun(runner)
      return value
    },
    set value(newValue: T) {
      setter(newValue)
    }
  }
}

首先判断如果传入的为函数,将其作为getter,setting为空(只读),如果传入的是对象,将其get属性赋值给getter,set属性赋值给setter,这些和Vue2一样。

接下来是核心代码:

let dirty = true
let value: T
const runner = effect(getter, {
  lazy: true,
  // mark effect as computed so that it gets priority during trigger
  computed: true,
  scheduler: () => {
    dirty = true
  }
})
return {
  _isRef: true,
  // expose effect so computed can be stopped
  effect: runner,
  get value() {
    if (dirty) {
      value = runner()
      dirty = false
    }
    // When computed effects are accessed in a parent effect, the parent
    // should track all the dependencies the computed property has tracked.
    // This should also apply for chained computed properties.
    trackChildRun(runner)
    return value
  },
  set value(newValue: T) {
    setter(newValue)
  }
}

能看出返回的是一个对象,有value属性,同样value属性具有get和set函数,其实和ref一样,包装了成了一个具有value属性的对象。其中get作用于依赖收集,当dirty为true时,触发runner函数依赖收集,然后将dirty置为false,其中trackChildRun(runner)作用也很简单,在依赖收集时将父副作用函数添加到dep依赖,这样当发布时触发不仅会触发计算属性的副作用函数,也会触发父副作用函数,例如:

const observed = Vue.reactive({name:'test',age:18})
const message = Vue.computed(() => {
    return `年龄是:${observed.age}`
})
Vue.effect(() => {
    console.log('我被触发了')  // 打印了2次
    console.log(message.value) // 18,30
})
observed.age = 30

显然,如果不绑定父副作用,observed.age更改后Vue.effect并不会触发。

dirty主要的作用其实也很简单,用过redux的应该知道reselect,dirty的作用类似,用来判断是否要重新计算计算属性。具体用处下面会说到,接下来创建一个副作用函数,函数体就是getter,三个参数,lazy之前说过,副作用函数默认为初始执行一次,当lazy为true时初始不执行,scheduler之前也说过,设置了scheduler后,发布触发的不是effect函数而是scheduler函数,至于最后computed属性主要是作为标注符,主要用于发布trigger函数中:

export function trigger(
  target: object,
  type: OperationTypes,
  key?: unknown,
  extraInfo?: DebuggerEventExtraInfo
) {
  // .....
  computedRunners.forEach(run)
  effects.forEach(run)
}

通过判断effect是否具有computed参数,筛选出计算属性的副作用函数,必须要先执行计算属性副作用函数再执行普通副作用函数,原因其实也在于dirty,只有当trigger发布时执行计算属性scheduler后,将dirty置为true,然后执行父副作用,父副作用执行过程中又会触发计算属性的get(通过trackChildRun绑定父副作用的依赖),如果普通副作用在前面的话就有可能先执行父副作用,触发计算属性的get,这个时候dirty并没有通过scheduler置为ture,显然并不会重新执行计算属性并依赖收集,直接返回之前的值,不信的话,可以将副作用执行顺序换下,然后运行代码如下:

const observed = Vue.reactive({name:'test',age:18})
const message = Vue.computed(() => {
    return `年龄是:${observed.age}`
})
Vue.effect(() => {
    console.log('我被触发了')  // 打印了2次
    console.log(message.value) // 18,18
})
observed.age = 30

至于为什么使用dirty,一开始有说过,可以提高运行效率,防止不必要的运算,例如:

const observed = Vue.reactive({name:'test',age:18})
const message = Vue.computed(() => {
    console.log('依赖收集,返回结果')  // 打印了一次
    return `年龄是:${observed.age}`
})
testEffect = Vue.effect(() => {
    const value = observed.name
    console.log(message.value) // 年龄是:18,年龄是:18
})
observed.name = 'test2'

计算属性的依赖并没有name属性,故name变化不会触发计算属性的scheduler方法将dirty置为true,name变化触发testEffect副作用,当需要获取计算属性message.value时触发其get发现dirty为false,这时直接返回之前的值18。

至此,vue3响应式原理基本讲完了。

你可能感兴趣的:(vue3,前端,vue)