Vue 3.X与Vue 2.X访问数据差异

694 阅读2分钟

CgqCHl8iOeqAJJlaAAHAhGDRoDQ714副本.png 按这个响应式原理写一遍源码原理。ps:能水好几篇,针不戳

Vue 3.X与Vue 2.X访问数据差异

对于props、data等处理

Vue 2.x

inject
1,从自身开始遍历父级找到provid,赋值给inject
while (source) {
   if (source._provided && hasOwn(source._provided, provideKey)) {
        result[key] = source._provided[provideKey]
         break
   }
   source = source.$parent
}
2,把所有inject,添加到vm(实例)上

props
1,遍历props,当不存在vm上时添加到vm
for (const key in propsOptions) {
    if (!(key in vm)) {
      proxy(vm, `_props`, key)
    }
}

methods
1,methods为函数时,改变this指向至vm。添加到vm(实例)上
vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm)
非生产环境:methods[key]不为函数、props上有的、key的首字母是_ 或 $报错

data
1,获取data值。如果是函数data.call(vm,vm)执行,如果是对象直接返回
let data = vm.$options.data
data = vm._data = typeof data === 'function'
  ? getData(data, vm)
  : data || {}
 
2,获取data对象的key值。添加到vm(实例)上
   props中已经存在的就不能添加到vm(实例)上
   非生产环境:methods[key]会报错提示,但会覆盖methods的在vm上的元素
const keys = Object.keys(data)
let i = keys.length
while (i--) {
  const key = keys[i]
  if (props && hasOwn(props, key)) {
    ...
  } else if (!isReserved(key)) {
    proxy(vm, `_data`, key)
  }
}

computed
如果vm上已有key元素,判断对应的data、props、methods报错。否则添加到vm(实例)上
if (!(key in vm)) {
  defineComputed(vm, key, userDef)
} else if (process.env.NODE_ENV !== 'production') {
 if (key in vm.$data) {
   warn(`The computed property "${key}" is already defined in data.`, vm)
 } else if (vm.$options.props && key in vm.$options.props) {
   warn(`The computed property "${key}" is already defined as a prop.`, vm)
 } else if (vm.$options.methods && key in vm.$options.methods) {
   warn(`The computed property "${key}" is already defined as a method.`, vm)
 }
}

可以看到Vue 2.x的options顺序是

  • inject
  • props: vm(实例)上不存在时添加
  • methods: 直接覆盖inject、props在vm(实例)上的元素
  • data: props上已存在同名元素时,不能添加到vm(实例)上
  • computed: vm(实例)上不存在时添加

beforeCreate到created之间执行

Vue 3.x

添加了setup,且在beforeCreate之前执行还能获取props和slot。

 const mountComponent = (initialVNode, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
  // 创建组件实例
  const instance = (initialVNode.component = createComponentInstance(initialVNode, parentComponent, parentSuspense))
  // 设置组件实例
  setupComponent(instance)
  // 设置并运行带副作用的渲染函数
  setupRenderEffect(instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized)
}

createComponentInstance初始化组件实例

  初始化组件实例
  const instance: ComponentInternalInstance = {
    uid: uid++,
    vnode,
    type,
    parent,

    propsOptions: normalizePropsOptions(type, appContext),
    emitsOptions: normalizeEmitsOptions(type, appContext),

    // emit
    emit: null as any, // to be set immediately
    emitted: null,

    // state
    ctx: EMPTY_OBJ,
    data: EMPTY_OBJ,
    props: EMPTY_OBJ,
    attrs: EMPTY_OBJ,
    slots: EMPTY_OBJ,
    refs: EMPTY_OBJ,
    setupState: EMPTY_OBJ,
    setupContext: null,

    // lifecycle 
    isMounted: false,
    isUnmounted: false,
    isDeactivated: false,
    bc: null,
    c: null,
    bm: null,
    m: null,
    bu: null,
    u: null,
    um: null,
    bum: null,
    da: null,
    a: null,
    rtg: null,
    rtc: null,
    ec: null
    
    ...
  }
 
  instance.root = parent ? parent.root : instance
  instance.emit = emit.bind(null, instance)

  return instance

setupComponent

组件实例的设置流程,对 setup 函数的处理就在这里完成

setup执行有两个形参,setup(props,{attrs,slots,emit}) 为了可以访问也就会在setup执行前处理

  const { props, children } = instance.vnode
  const isStateful = isStatefulComponent(instance)
  
  初始化Props
  initProps(instance, props, isStateful, isSSR)
  初始化插槽
  initSlots(instance, children)
  
  处理 setup 函数和完成组件实例设置
  const setupResult = isStateful
    ? setupStatefulComponent(instance, isSSR)
    : undefined
  isInSSRComponentSetup = false
  return setupResult
  
  setup中的形参emit在创建组件实例时就完成了
  initProps、initSlots、setupStatefulComponent这三个细说说

initProps

  const [options, needCastKeys] = instance.propsOptions
  if (props) {
    for (const key in props) {
      const value = props[key]

      // 统一转成驼峰
      // 在propsOptions的options中的就是props属性,其他的就都是attrs属性了
      let camelKey
      if (options && hasOwn(options, (camelKey = camelize(key)))) {
        props[camelKey] = value
      } else if (!isEmitListener(instance.emitsOptions, key)) {
        attrs[key] = value
      }
    }
  }
  
  setup中的形参attrs和props就完成了

initSlots

instance.slots = children

setupStatefulComponent

创建代理属性访问缓存
instance.accessCache = Object.create(null)
创建上下文ctx的代理,页面渲染的属性访问与之后this访问的都是这个proxy
instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers)
PublicInstanceProxyHandlers
    const {
      ctx,
      setupState,
      data,
      props,
      accessCache,
      type,
      appContext
    } = instance
    
    if (key[0] !== '$') {
      const n = accessCache![key]
      if (n !== undefined) {
        switch (n) {
          case AccessTypes.SETUP:
            return setupState[key]
          case AccessTypes.DATA:
            return data[key]
          case AccessTypes.CONTEXT:
            return ctx[key]
          case AccessTypes.PROPS:
            return props![key]
        }
      } else if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) {
        accessCache![key] = AccessTypes.SETUP
        return setupState[key]
      } else if (data !== EMPTY_OBJ && hasOwn(data, key)) {
        accessCache![key] = AccessTypes.DATA
        return data[key]
      } else if (
        (normalizedProps = instance.propsOptions[0]) &&
        hasOwn(normalizedProps, key)
      ) {
        accessCache![key] = AccessTypes.PROPS
        return props![key]
      } else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) {
        accessCache![key] = AccessTypes.CONTEXT
        return ctx[key]
      } else if (!__FEATURE_OPTIONS_API__ || !isInBeforeCreate) {
        accessCache![key] = AccessTypes.OTHER
      }
    }

顺序是setupStatedatapropsctx。每次查询都需要hasOwn,所以添加了accessCache记录变量的setupStatedatapropsctx中位置再次查询时就根据accessCache中的数据去访问对应元素的数据。(一个优化点)

set也是这个顺序setupStatedatapropsctx,只是修改props时

从这里就能看出,顺序是setupStatedatapropsctx。由于是访问每个元素下的变量而不是添加到ctx实例上,也就不会出现覆盖问题

  import { ref } from 'vue'
  export default {
    data() {
      return {
        msg: 'msg from data'
      }
    },
    setup() {
      const msg = ref('msg from setup')
      return {
        msg
      }
    },
    methods: {
      random() {
        this.msg = Math.random()
      }
    }
  }
   显示的会是setup.msg的变量,且this.msg修改的也是msg中的变量

最后看一下setup的执行

    const { setup } = Component
    
    setup是否使用参数
    const setupContext = (instance.setupContext =
      setup.length > 1 ? createSetupContext(instance) : null)

    currentInstance = instance
    const setupResult = callWithErrorHandling(
      setup,
      instance,
      ErrorCodes.SETUP_FUNCTION,
      [__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]
    )
    
    handleSetupResult(instance, setupResult, isSSR)
createSetupContext
    {
      attrs: instance.attrs,
      slots: instance.slots,
      emit: instance.emit,
    }
callWithErrorHandling
  let res
  try {
    res = args ? fn(...args) : fn()
  } catch (err) {
    handleError(err, instance, type)
  }
  return res

handleSetupResult

  if (isFunction(setupResult)) {
    // setup 返回渲染函数
    instance.render = setupResult
  }
  else if (isObject(setupResult)) {
    // 把 setup 返回结果变成响应式
    instance.setupState = reactive(setupResult)
  }
  //标准化模板或渲染函数
  finishComponentSetup(instance)
Vue 2.xVue 3.x
inject、props、methods、data、computed写在vm实例中,其中变量名称不能重复setupState、data、props、ctx在各自对应的元素中,变量名称可以重复
inject、props、methods、data、computed在beforeCreate到created中统一初始化setupState、data、props、slot、ctx在setup执行前初始化
变量名称不能重复变量名称可以重复,但要注意存取时的优先级