Vue3追本溯源(三)双向数据绑定

813 阅读5分钟

接上篇解析mount方法到setupStatefulComponent方法中,当调用createApp存在setup属性时(以本例为模版解析)开始执行setup方法,本文将详细解析setup方法,及数据如何设置钩子函数

本例

const { ref, createApp } = Vue
const app = createApp({ 
    // Vue3新增的setup属性
    setup(props) { 
        const message = ref('测试数据')
        const modifyMessage = () => {
            message.value = '修改后的测试数据'
        }
        return { message, modifyMessage }
    } 
}).mount('#app')

setup方法执行

// callWithErrorHandling函数的定义
export function callWithErrorHandling(
  fn: Function,
  instance: ComponentInternalInstance | null,
  type: ErrorTypes,
  args?: unknown[]
) {
  let res
  try {
    res = args ? fn(...args) : fn()
  } catch (err) {
    handleError(err, instance, type)
  }
  return res
}

// setupStatefulComponent方法中解析setup位置
const setupResult = callWithErrorHandling(
  setup,
  instance,
  ErrorCodes.SETUP_FUNCTION,
  [__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]
)

可以看到解析setup是在callWithErrorHandling中执行setup方法,参数是[instance.props, setupContext]instance.props(空对象)是创建instance对象时生成的,而setupContext则是根据setup的长度判读的,本例中为null。下面就开始执行setup函数。

const message = ref('测试数据')
const modifyMessage = () => {
    message.value = '修改后的测试数据'
} 
return { message, modifyMessage }

ref函数内部实现(创建RefImpl类的实例对象)

setup方法内部实现,(依本例来解析)首先定义数据message,其中执行了ref方法,ref方法也是Vue3暴露出来的一个函数,接下来看下ref内部具体的实现

// ref
export function ref(value?: unknown) {
  return createRef(value)
}
// isRef
export function isRef(r: any): r is Ref {
  return Boolean(r && r.__v_isRef === true)
}
// createRef
function createRef(rawValue: unknown, shallow = false) {
  if (isRef(rawValue)) {
    return rawValue
  }
  return new RefImpl(rawValue, shallow)
}

首先是执行了createRef函数,其内部首先调用isRef方法判断数据中是否包含__v_isRef属性,若包含则直接返回数据,否则new RefImpl()创建实例,一起看下RefImpl类的定义。

// RefImpl类
class RefImpl<T> {
  private _value: T
  private _rawValue: T

  public dep?: Dep = undefined
  public readonly __v_isRef = true

  constructor(value: T, public readonly _shallow = false) {
    this._rawValue = _shallow ? value : toRaw(value)
    this._value = _shallow ? value : convert(value)
  }

  get value() {
    trackRefValue(this)
    return this._value
  }

  set value(newVal) {
    newVal = this._shallow ? newVal : toRaw(newVal)
    if (hasChanged(newVal, this._rawValue)) {
      this._rawValue = newVal
      this._value = this._shallow ? newVal : convert(newVal)
      triggerRefValue(this, newVal)
    }
  }
}

// ReactiveFlags.RAW = __v_raw
export function toRaw<T>(observed: T): T {
  const raw = observed && (observed as Target)[ReactiveFlags.RAW]
  return raw ? toRaw(raw) : observed
}

const convert = <T extends unknown>(val: T): T =>
  isObject(val) ? reactive(val) : val

RefImpl类中首先定义__v_isRef属性为truedep属性为undefined,之后执行constructor传入value(本例为"测试数据"),设置_rawValue属性为toRaw(value)

toRaw方法是判断数据中是否包含__v_raw属性,存在则递归调用toRaw(value的__v_raw属性值),否则返回value

设置_value的属性值为convert(value)

convert方法是判断数据是否是引用类型,若是引用类型数据(数组、对象会调用reactive方法再次设置内部的钩子函数,本例为基础数据类型暂不解析reactive方法)

RefImpl类内部钩子函数(依赖收集、运行依赖)

RefImpl类最后会设置valueget、set钩子函数,get函数是当获取实例对象的value属性时会触发依赖的收集,并返回实例的_value属性,也就是创建实例对象时传入的数据;set钩子替换新的value值,并运行依赖。

后续如何触发value的钩子、怎样收集依赖,我们在解析生成的render方法时再做详细的分析

回到setup方法本身,数据处理完了,我们来看下方法

const modifyMessage = () => { message.value = '修改后的测试数据' }

可以看到定义修改数据的方法modifyMessage,观察函数体我们可以发现,是给数据的value属性设置新值,那么触发set钩子函数的位置应该是这里。

至于修改数据的方法如何挂到按钮的点击事件上去的,我们等到模版解析完,处理render函数时再仔细分析

最后setup函数返回了一个对象{ message, modifyMessage }。至此setup方法执行完成,返回的对象就是callWithErrorHandling方法返回的对象。然后我们回归到setupStatefulComponent方法中

Proxy代理setup返回的数据对象

if (isPromise(setupResult)) {
    // ...
} else {
  handleSetupResult(instance, setupResult, isSSR)
}

本例中返回的setupResultPromise对象,所以执行handleSetupResult方法

export function handleSetupResult(
  instance: ComponentInternalInstance,
  setupResult: unknown,
  isSSR: boolean
) {
    if (isFunction(setupResult)) {}
    else if (isObject(setupResult)) {
        // ...
        instance.setupState = proxyRefs(setupResult)
        if (__DEV__) {
          exposeSetupStateOnRenderContext(instance)
        }
        // ...
    } else if (__DEV__ && setupResult !== undefined) {}
    finishComponentSetup(instance, isSSR)
}

// proxyRefs
export function proxyRefs<T extends object>(
  objectWithRefs: T
): ShallowUnwrapRef<T> {
  return isReactive(objectWithRefs)
    ? objectWithRefs
    : new Proxy(objectWithRefs, shallowUnwrapHandlers)
}

// 
const shallowUnwrapHandlers: ProxyHandler<any> = {
  get: (target, key, receiver) => unref(Reflect.get(target, key, receiver)),
  set: (target, key, value, receiver) => {
    // ...
  }
}

可以看到handleSetupResult方法的内部首先执行proxyRefs,目的是通过Proxy代理setup函数的返回结果,为其属性设置set、get钩子函数,并赋值给instance.setupState。之后再执行exposeSetupStateOnRenderContext方法。

instance.ctx新增Proxy对象属性

export function exposeSetupStateOnRenderContext(
  instance: ComponentInternalInstance
) {
  const { ctx, setupState } = instance
  Object.keys(toRaw(setupState)).forEach(key => {
    if (!setupState.__isScriptSetup && (key[0] === '$' || key[0] === '_')) {
      // ... console.warn()
      return
    }
    Object.defineProperty(ctx, key, {
      enumerable: true,
      configurable: true,
      get: () => setupState[key], // ctx.xxx = setupState['xxx']
      set: NOOP
    })
  })
}

此函数作用主要是将setupState对象上的属性通过Object.defineProperty赋值给ctx对象

那么整个初始化流程就串连起来了,当我们执行生成的render方法时,根据with(ctx)的特性,获取ctx.message,则会触发setupState.message的获取,触发setupStateget钩子函数,实际上get钩子函数执行的unref方法其实就是获取RefImpl实例对象的value属性值,那么就会触发RefImpl实例对象的get钩子函数,从而获取数据然后收集依赖

回到handleSetupResult方法最后,执行了finishComponentSetup函数,在完成setup的解析之后,开始进行template模版编译。

总结

[以本例为模版解析] 执行setup函数,内部定义会使用到ref(或者reactive方法,是Vue3暴露对外的),本质是创建RefImpl的实例化对象,并增加get(收集依赖)、set(运行依赖)钩子函数。setup函数返回的对象通过Proxy代理并赋值给instancesetupState属性(为render函数解析时遇到相应的属性名而触发钩子函数做准备)。最后将setupState对象的属性值通过Object.defineProperty赋值给ctx对象(在解析render函数时会使用到)。后续将解析template模版,看是如何生成render函数的。