按这个响应式原理写一遍源码原理。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
}
}
顺序是setupState、data、props、ctx。每次查询都需要hasOwn,所以添加了accessCache记录变量的setupState、data、props、ctx中位置再次查询时就根据accessCache中的数据去访问对应元素的数据。(一个优化点)
set也是这个顺序setupState、data、props、ctx,只是修改props时
从这里就能看出,顺序是setupState、data、props、ctx。由于是访问每个元素下的变量而不是添加到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.x | Vue 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执行前初始化 |
变量名称不能重复 | 变量名称可以重复,但要注意存取时的优先级 |