自從Vue3
發布之後,composition API
這個字走入寫Vue
同學的視野之中,相信大家也一直聽到composition API
比之前的options API
有多好多強,如今由於@vue/composition-api
外掛程式的發布, Vue2
的同學也可以上車咯,接下來我們主要以響應式的ref
和reactive
來深入分析一下,這個插件是怎麼實現此功能的。
// 入口文件引入并注册 import Vue from 'vue' import VueCompositionAPI from '@vue/composition-api' Vue.use(VueCompositionAPI)
// vue文件使用 import { defineComponent, ref, reactive } from '@vue/composition-api' export default defineComponent({ setup () { const foo = ref('foo'); const obj = reactive({ bar: 'bar' }); return { foo, obj } } })
怎麼樣,看完是不是感覺和vue3
一模一樣,你可能會想:
#這是vue2
啊,我之前的data
、methods
裡面也有變數和方法,怎麼做到跟setup
傳回值打通合併在一起的。 【相關推薦:vuejs影片教學、web前端開發】
vue2
不是只有定義在 data
裡面的資料才會被處理成響應式的嗎? ref
和 reactive
是怎麼做到的呢?
vue2
響應式資料定義的約束(新增賦值原始物件沒有的屬性,陣列下標修改等),改用ref
和reactive
就沒問題嗎?
當然還有很多的疑惑,因為插件提供的API
相當多,覆蓋了絕大部分Vue3
所擁有的,這裡主要從這幾個問題來分析一下是如何做到的。
得益於Vue
的外掛系統,@vue/composition-api
像是 vue-router
、vuex
一樣也是透過官方提供的外掛程式來注入。
// 这里只贴跟本章要讲的相关代码 funciton mixin (Vue) { Vue.mixin({ beforeCreate: functionApiInit } } function install (Vue) { mixin(Vue); } export const Plugin = { install: (Vue: VueConstructor) => install(Vue), }
Vue
外掛程式就是向外面暴露一個install
的方法,當呼叫use
的時候會呼叫該方法,並且把Vue
建構子作為參數傳入,然後呼叫Vue.mixin
混入對應鉤子時要處理的函數。
接下來主要看下functionApiInit
做了什麼
function functionApiInit(this: ComponentInstance) { const vm = this const $options = vm.$options const { setup, render } = $options // render 相关 const { data } = $options $options.data = function wrappedData() { initSetup(vm, vm.$props) return isFunction(data) ? ( data as (this: ComponentInstance, x: ComponentInstance) => object ).call(vm, vm) : data || {} }
因為Vue
在beforeCreated
和created
生命週期之間,會initState
對資料進行處理,其中對data
的處理時就會呼叫$options.data
拿到定義的數據,所以這裡重新對該函數其包裹一層,這也是為什麼要選擇beforeCreate
鉤子注入的一個原因,必須在該函數調用前進行包裹。
接下來看initSetup
都做了什麼
function initSetup(vm: ComponentInstance, props: Record<any, any> = {}) { const setup = vm.$options.setup! const ctx = createSetupContext(vm) const instance = toVue3ComponentInstance(vm) instance.setupContext = ctx def(props, '__ob__', createObserver()) resolveScopedSlots(vm, ctx.slots) let binding: ReturnType<SetupFunction<Data, Data>> | undefined | null activateCurrentInstance(instance, () => { binding = setup(props, ctx) }) // setup返回是函数的情况 需要重写render函数 const bindingObj = binding Object.keys(bindingObj).forEach((name) => { let bindingValue: any = bindingObj[name] // 数据处理 asVmProperty(vm, name, bindingValue) }) return } }
這個函數比較長,不在本次要講解的主線上程式碼邏輯都刪除了,這個函數主要是創建了ctx
和把vm
實例轉換成Vue3
資料型別定義的instance
,然後執行setup
函數得到回傳值,然後遍歷每個屬性,呼叫asVmProperty
掛載到vm
上面,當然這裡的掛載不是直接透過把屬性和值加到vm
上面,這麼做會有一個問題,就是後續對該屬性的修改不能同步到vm
中,這裡採用的還是Vue
最常見的資料代理。
export function asVmProperty( vm: ComponentInstance, propName: string, propValue: Ref<unknown> ) { const props = vm.$options.props if (!(propName in vm) && !(props && hasOwn(props, propName))) { if (isRef(propValue)) { proxy(vm, propName, { get: () => propValue.value, set: (val: unknown) => { propValue.value = val }, }) } else { proxy(vm, propName, { get: () => { if (isReactive(propValue)) { ;(propValue as any).__ob__.dep.depend() } return propValue }, set: (val: any) => { propValue = val }, }) } }
看到這裡,相信你已經明白了在setup
中定義返回的為什麼能夠在template
、 data
、 methods
等之中去使用了,因為傳回的東西都已經被代理到vm
之上了。
ref
reactive
的實作)接下來我們來說說響應式相關的,為什麼ref
和reactive
也可以讓資料成為響應式的。
ref
的實作其實是對 reactive
再次封裝,主要用來給基本型別使用。
function ref(raw?: unknown) { if (isRef(raw)) { return raw } const value = reactive({ [RefKey]: raw }) return createRef({ get: () => value[RefKey] as any, set: (v) => ((value[RefKey] as any) = v), }) }
因為 reactive
接受的必須是一個對象,所有這裡使用了一個常數作為 ref
# 的 key
, 也就是
const value = reactive({ "composition-api.refKey": row })
export function createRef<T>( options: RefOption<T>, isReadonly = false, isComputed = false ): RefImpl<T> { const r = new RefImpl<T>(options) const sealed = Object.seal(r) if (isReadonly) readonlySet.set(sealed, true) return sealed } export class RefImpl<T> implements Ref<T> { readonly [_refBrand]!: true public value!: T constructor({ get, set }: RefOption<T>) { proxy(this, 'value', { get, set, }) } }
通过 new RefImpl
实例,该实例上有一个 value
的属性,对 value
做代理,当取值的时候返回 value[RefKey]
,赋值的时候赋值给 value[RefKey]
, 这就是为什么 ref
可以用在基本类型,然后对返回值的 .value
进行操作。调用 object.seal
是把对象密封起来(会让这个对象变的不能添加新属性,且所有已有属性会变的不可配置。属性不可配置的效果就是属性变的不可删除,以及一个数据属性不能被重新定义成为访问器属性,或者反之。但属性的值仍然可以修改。)
我们主要看下 reactive
的实现
export function reactive<T extends object>(obj: T): UnwrapRef<T> { const observed = observe(obj) setupAccessControl(observed) return observed as UnwrapRef<T> } export function observe<T>(obj: T): T { const Vue = getRegisteredVueOrDefault() let observed: T if (Vue.observable) { observed = Vue.observable(obj) } else { const vm = defineComponentInstance(Vue, { data: { $$state: obj, }, }) observed = vm._data.$$state } return observed }
我们通过 ref
或者 reactive
定义的数据,最终还是通过了变成了一个 observed
实例对象,也就是 Vue2
在对 data
进行处理时,会调用 observe
返回的一样,这里在 Vue2.6+
把observe
函数向外暴露为 Vue.observable
,如果是低版本的话,可以通过重新 new
一个 vue
实例,借助 data
也可以返回一个 observed
实例,如上述代码。
因为在 reactive
中定义的数据,就如你在 data
中定义的数据一样,都是在操作返回的 observed
,当你取值的时候,会触发 getter
进行依赖收集,赋值时会调用 setter
去派发更新,
只是定义在 setup
中,结合之前讲到的 setup
部分,比如当我们在 template
中访问一个变量的值时,vm.foo
-> proxy
到 setup
里面的 foo
-> observed
的 foo
,完成取值的流程,这会比直接在 data
上多代理了一层,因此整个过程也会有额外的性能开销。
因此使用该 API
也不会让你可以直接规避掉 vue2
响应式数据定义的约束,因为最终还是用 Object.defineProperty
去做对象拦截,插件同样也提供了 set API
让你去操作对象新增属性等操作。
通过上面的了解,相信你一定对于 Vue2
如何使用 composition API
有了一定的了解,因为 API
相当多, 响应式相关的就还有 toRefs、toRef、unref、shallowRef、triggerRef
等等,这里就不一一分析,有兴趣的可以继续看源码的实现。
写 Vue2
的同学也可以不用羡慕写 Vue3
的同学了,直接引入到项目就可以使用起来,虽然没有 vue3
那么好的体验,但是绝大部分场景还是相同的,使用时注意 README
文档最后的限制章节,里面讲了一些使用限制。
以上是解析Vue2實作composition API的原理的詳細內容。更多資訊請關注PHP中文網其他相關文章!