This article is just written for my knowledge and understanding of the coolest part in Vue: the reactivity system.
Background
As we know, Vue.js team is working on 3.0 for a while. Recently it released the first Beta version. That means the core tech design is stable enough. Now I think it's time to walk through something inside Vue 3.0. That's one of my most favorite parts: the reactivity system.
What's reactivity?
For short, reactivity means, the result of calculations, which depends on some certain data, will be automatically updated, when the data changes.
In modern web development, we always need to render some data-related or state-related views. So obviously, making data reactive could give us lots of benefits. In Vue, the reactivity system always exists from its very early version till now. And I think that's one of the biggest reasons why Vue is so popular.
Let's have a look at the reactivity system in the early version of Vue first.
Reactivity in Vue from 0.x to 1.x
The first time I touched Vue is about 2014, I guess it was Vue 0.10. At that time, you could just pass a plain JavaScript object into a Vue component through data option. Then you could use them in a piece of document fragment as its template with reactivity. Once the data changes, the view would be automatically updated. Also you could use computed and watch options to benefit yourself from the reactivity system in more flexible ways. Same to the later Vue 1.x.
newVue({el:'#app',template:'<div @click="x++">{{x}} + {{y}} = {{z}}</div>',data(){return{x:1,y:2}},computed:{z(){returnthis.x+this.y}},watch:{x(newValue,oldValue){console.log(`x is changed from ${oldValue} to ${newValue}`)}}})
You may found these APIs didn't change too much so far. Because they work the same totally.
So how does it work? How to make a plain JavaScript object reactive automatically?
Fortunately, in JavaScript we have an API Object.defineProperty() which could overwrite the getter/setter of an object property. So to make them reactive, there could be 3 steps:
Use Object.defineProperty() to overwrite getters/setters of all the properties inside a data object recursively. Besides behaving normally, it additionally injects a trigger inside all setters, and a tracker inside all getters. Also it will create a small Dep instance inside each time to record all the calculations which depend on this property.
Every time we set a value into a property, it will call the setter, which will re-evaluate those related calculations inside the Dep instance. Then you may ask how could we record all the related calculations. The fact is when each time we define a calculation like a watch function or a DOM update function, it would run once first - sometimes it runs as the initialization, sometimes it's just a dry-run. And during that running, it will touch every tracker inside the getters it depends on. Each tracker will push the current calculation function into the corresponding Dep instance.
So next time when some data changes, it will find out all related calculations inside the corresponding Dep instance, and then run them again. So the effect of these calculations will be updated automatically.
A simple implementation to observe data using Object.defineProperty is like:
// dataconstdata={x:1,y:2}// real data and deps behindletrealX=data.xletrealY=data.yconstrealDepsX=[]constrealDepsY=[]// make it reactiveObject.defineProperty(data,'x',{get(){trackX()returnrealX},set(v){realX=vtriggerX()}})Object.defineProperty(data,'y',{get(){trackY()returnrealY},set(v){realY=vtriggerY()}})// track and trigger a propertyconsttrackX=()=>{if (isDryRun&¤tDep){realDepsX.push(currentDep)}}consttrackY=()=>{if (isDryRun&¤tDep){realDepsY.push(currentDep)}}consttriggerX=()=>{realDepsX.forEach(dep=>dep())}consttriggerY=()=>{realDepsY.forEach(dep=>dep())}// observe a functionletisDryRun=falseletcurrentDep=nullconstobserve=fn=>{isDryRun=truecurrentDep=fnfn()currentDep=nullisDryRun=false}// define 3 functionsconstdepA=()=>console.log(`x = ${data.x}`)constdepB=()=>console.log(`y = ${data.y}`)constdepC=()=>console.log(`x + y = ${data.x+data.y}`)// dry-run all dependentsobserve(depA)observe(depB)observe(depC)// output: x = 1, y = 2, x + y = 3// mutate datadata.x=3// output: x = 3, x + y = 5data.y=4// output: y = 4, x + y = 7
Inside Vue 2.x and earlier, the mechanism roughly like this above, but much better abstracted, designed, and implemented.
For supporting more complex cases like arrays, nested properties, or mutating more than 2 properties at the same time, there are more implementation and optimization details inside Vue, but basically, the same mechanism to we mentioned before.
Reactivity in Vue 2.x
From 1.x to 2.x, it was a total rewrite. And it introduced some really cool features like virtual DOM, server-side rendering, low-level render functions, etc. But the interesting thing is the reactivity system didn't change too much, however, the usage above was totally different:
From 0.x to 1.x, the rendering logic depends on maintaining a document fragment. Inside that document fragment, there are some DOM update functions for each dynamic element, attribute, and text content. So the reactivity system mostly works between the data object and these DOM update functions. Since the functions all real DOM functions so the performance is not quite good. In Vue 2.x, this rendering logic of a Vue component became a whole pure JavaScript render function. So it would firstly return virtual nodes instead of real DOM nodes. Then it would update the real DOM based on the result of a fast mutation diff algorithm for the virtual DOM nodes. It was faster than before.
In Vue 2.6, it introduced a standalone API Vue.observalue(obj) to generate reactive plain JavaScript objects. So you could use them inside a render function or a computed property. It was more flexible to use.
At the same time, there are some discussions in Vue community about abstracting the reactivity system into an independent package for wider usage. However it didn't happen at that time.
Limitation of the reactivity system before 3.0
So far, Vue didn't change the reactivity mechanism. But it doesn't mean the current solution is ideally perfect. As I personally understand, there are some caveats:
Because of the limitation of Object.defineProperty we couldn't observe some data changes like:
Setting array items by assigning value to a certain index. (e.g. arr[0] = value)
Setting the length of an array. (e.g. arr.length = 0)
Adding a new property to an object. (e.g. obj.newKey = value)
So it needs some complementary APIs like Vue.$set(obj, newKey, value).
Because of the limitation of plain JavaScript data structure, for each reactive object there would be an unenumerable property named __ob__, which might lead to conflict in some extreme cases.
It didn't support more data types like Map and Set. Neither other non-plain JavaScript objects.
The performance is an issue. When the data is large, making it reactive when the initialization would cost visible time. There are some tips to flatten the initial cost but a little bit tricky.
Reactivity system in Vue 3.0
For short, in Vue 3.0, the reactivity system was totally rewritten with a new mechanism and new abstraction, as an independent package. And it also supports more modern JavaScript data types.
You may be familiar with it, maybe not. No worry. Let's quickly take a look at it first by creating a Vue 3.0 project.
Create a Vue 3.0 project
Until now, there is no stable full-featured project generator, since it's still in Beta. We could try Vue 3.0 through an experimental project named "vite":
Vite (French word for "quick", pronounced /vit/, like "veet") is a new breed of frontend build tooling that significantly improves the frontend development experience. It consists of two major parts:
You could see there is already a Vue component App.vue:
<template><p><span>Count is: {{count}}</span><button@click="count++">increment</button>
is positive: {{isPositive}}</p></template><script>exportdefault{data:()=>({count:0}),computed:{isPositive(){returnthis.count>0}}}</script>
There is a reactive property count and it's displayed in the <template>. When users click the "increment" button, the property count would be incremented, the computed property isPositive would be re-calculated too, and the UI would be updated automatically.
It seems nothing different to the former version so far.
Now let's try something impossible in early versions of Vue.
1. Adding new property
As we mentioned, in Vue 2.x and earlier, we couldn't observe newly added property automatically. For example:
<template><p><span>My name is {{name.given}}{{name.family}}</span><button@click="update">update name</button></p></template><script>exportdefault{data:()=>({name:{given:'Jinjiang'}}),methods:{update(){this.name.family='Zhao'}}}</script>
The update method couldn't work properly because the new property family couldn't be observed. So when adding this new property, the render function won't be re-calculated. If you want this work, you should manually use another complementary API as Vue.$set(this.name, 'family', 'Zhao').
But in Vue 3.0, it already works as well. You don't need Vue.$set anymore.
2. Assigning items to an array by index
Now let's try to set a value into an index of an array:
<template><ul><liv-for="item, index in list":key="index">{{item}}<button@click="edit(index)">edit</button></li></ul></template><script>exportdefault{data(){return{list:['Client meeting','Plan webinar','Email newsletter']}},methods:{edit(index){constnewItem=prompt('Input a new item')if (newItem){this.list[index]=newItem}}}}</script>
In Vue 2.x and earlier, when you click one of the "edit" buttons in the list item and input a new piece of a text string, the view won't be changed, because setting item with an index like this.list[index] = newItem couldn't be tracked. You should write Vue.$set(this.list, index, newItem) instead. But in Vue 3.0, it works, too.
3. Setting the length property of an array
Also if we add another button to the example above to clean all items:
<template><ul>...</ul><!-- btw Vue 3.0 supports multi-root template like this --><button@click="clean">clean</button></template><script>exportdefault{data:...,methods:{...,clean(){this.list.length=0}}}</script>
it won't work in Vue 2.x and earlier, because setting the length of an array like this.list.length = 0 couldn't be tracked. So you have to use other methods like this.list = []. But in Vue 3.0, all the ways above works.
4. Using ES Set/Map
Let's see a similar example with ES Set:
<template><div><ul><liv-for="item, index in list":key="index">{{item}}<button@click="remove(item)">remove</button></li></ul><button@click="add">add</button><button@click="clean">clean</button></div></template><script>exportdefault{data:()=>({list:newSet(['Client meeting','Plan webinar','Email newsletter'])}),created(){console.log(this.list)},methods:{remove(item){this.list.delete(item)},add(){constnewItem=prompt('Input a new item')if (newItem){this.list.add(newItem)}},clean(){this.list.clear()}}}</script>
Now we use a Set instead of an array. In Vue 2.x and earlier, fortunately it could be rendered properly for the first time. But when you remove, add, or clear, the view won't be updated, because they are not tracked. So usually we don't use Set or Map in Vue 2.x and earlier. In Vue 3.0, the same code would work as you like, because it totally supports them.
5. Using non-reactive properties
If we have some one-time consuming heavy data in a Vue component, probably it doesn't need to be reactive, because once initialized, it won't change. But in Vue 2.x and earlier, whatever you use them again, all the properties inside will be tracked. So sometimes it costs visible time. Practically, we have some other ways to walk-around but it's a little bit tricky.
In Vue 3.0, it provides a dedicated API to do this - markRaw:
<template><div>
Hello {{test.name}}<button@click="update">should not update</button></div></template><script>import{markRaw}from'vue'exportdefault{data:()=>({test:markRaw({name:'Vue'})}),methods:{update(){this.test.name='Jinjiang'console.log(this.test)}}}</script>
In this case, we use markRaw to tell the reactivity system, the property test and its descendants properties don't need to be tracked. So the tracking process would be skipped. At the same time, any further update on them won't trigger a re-render.
Additionally, there is another "twin" API - readonly. This API could prevent data to be mutated. For example:
So far we see the power and magic of the reactivity system in Vue 3.0. Actually there are more powerful ways to use it. But we won't move on immediately, because before mastering them, it's also great to know how it works behind Vue 3.0.
How it works
For short, the reactivity system in Vue 3.0 suits up with ES2015!
First part: simple data observer
Since ES2015, there are a pair of APIs - Proxy and Reflect. They are born to reactivity systems! Vue 3.0 reactivity system just be built based on that.
With Proxy you could set a "trap" to observe any operation on a certain JavaScript object.
constdata={x:1,y:2}// all behaviors of a proxy by operation typesconsthandlers={get(data,propName,proxy){console.log(`Get ${propName}: ${data[propName]}!`)returndata[propName]},has(data,propName){...},set(data,propName,value,proxy){...},deleteProperty(data,propName){...},// ...}// create a proxy object for the dataconstproxy=newProxy(data,handlers)// print: 'Get x: 1' and return `1`proxy.x
With Reflect you could behave the same as the original object.
constdata={x:1,y:2}// all behaviors of a proxy by operation typesconsthandlers={get(data,propName,proxy){console.log(`Get ${propName}: ${data[propName]}!`)// same behavior as beforereturnReflect.get(data,propName,proxy)},has(...args){returnReflect.set(...args)},set(...args){returnReflect.set(...args)},deleteProperty(...args){returnReflect.set(...args)},// ...}// create a proxy object for the dataconstproxy=newProxy(data,handlers)// print: 'Get x: 1' and return `1`proxy.x
So with Proxy + Reflect together, we could easily make a JavaScript object observable, and then, reactive.
consttrack=(...args)=>console.log('track',...args)consttrigger=(...args)=>console.log('trigger',...args)// all behaviors of a proxy by operation typesconsthandlers={get(...args){track('get',...args);returnReflect.get(...args)},has(...args){track('has',...args);returnReflect.set(...args)},set(...args){Reflect.set(...args);trigger('set',...args)},deleteProperty(...args){Reflect.set(...args);trigger('delete',...args)},// ...}// create a proxy object for the dataconstdata={x:1,y:2}constproxy=newProxy(data,handlers)// will call `trigger()` in `set()`proxy.z=3// create a proxy object for an arrayconstarr=[1,2,3]constarrProxy=newProxy(arr,handlers)// will call `track()` & `trigger()` when get/set by indexarrProxy[0]arrProxy[1]=4// will call `trigger()` when set `length`arrProxy.length=0
So this observer is better than Object.defineProperty because it could observe every former dead angle. Also the observer just needs to set up a "trap" to an object. So less cost during the initialization.
And it's not all the implementation, because in Proxy it could handle ALL kinds of behaviors with different purposes. So the completed code of handlers in Vue 3.0 is more complex.
For example if we run arrProxy.push(10), the proxy would trigger a set handler with 3 as its propName and 10 as its value. But we don't literally know whether or not it's a new index. So if we would like to track arrProxy.length, we should do more precise determination about whether a set or a deleteProperty operation would change the length.
Also this Proxy + Reflect mechanism supports you to track and trigger mutations in a Set or a Map. That means operations like:
In Vue 3.0, we also provide some other APIs like readonly and markRaw. For readonly what you need is just change the handlers like set and deleteProperty to avoid mutations. Probably like:
consttrack=(...args)=>console.log('track',...args)consttrigger=(...args)=>console.log('trigger',...args)// all behaviors of a proxy by operation typesconsthandlers={get(...args){track('get',...args);returnReflect.get(...args)},has(...args){track('has',...args);returnReflect.set(...args)},set(...args){console.warn('This is a readonly proxy, you couldn\'t modify it.')},deleteProperty(...args){console.warn('This is a readonly proxy, you couldn\'t modify it.')},// ...}// create a proxy object for the dataconstdata={x:1,y:2}constreadonly=newProxy(data,handlers)// will warn that you couldn't modify itreadonly.z=3// will warn that you couldn't modify itdeletereadonly.x
For markRaw, in Vue 3.0 it would set a unenumerable flag property named __v_skip. So when we are creating a proxy for data, if there is a __v_skip flag property, then it would be skipped. Probably like:
// track, trigger, reactive handlersconsttrack=(...args)=>console.log('track',...args)consttrigger=(...args)=>console.log('trigger',...args)constreactiveHandlers={...}// set an invisible skip flag to raw dataconstmarkRaw=data=>Object.defineProperty(data,'__v_skip',{value:true})// create a proxy only when there is no skip flag on the dataconstreactive=data=>{if (data.__v_skip){returndata}returnnewProxy(data,reactiveHandlers)}// create a proxy object for the dataconstdata={x:1,y:2}constrawData=markRaw(data)constreactiveData=readonly(data)console.log(rawData===data)// trueconsole.log(reactiveData===data)// true
Additionally, a trial of using WeakMap to record deps and flags
Although it's not implemented in Vue 3.0 finally. But there was another try to record deps and flags using new data structures in ES2015.
With Set and Map, we could maintain the relationship out of the data itself. So we don't need flag properties like __v_skip inside data any more - actually there are some other flag properties like __v_isReactive and __v_isReadonly in Vue 3.0. For example:
// a Map to record dependetsconstdependentMap=newMap()// track and trigger a propertyconsttrack=(type,data,propName)=>{if (isDryRun&¤tFn){if (!dependentMap.has(data)){dependentMap.set(data,newMap())}if (!dependentMap.get(data).has(propName)){dependentMap.get(data).set(propName,newSet())}dependentMap.get(data).get(propName).add(currentFn)}}consttrigger=(type,data,propName)=>{dependentMap.get(data).get(propName).forEach(fn=>fn())}// observeletisDryRun=falseletcurrentFn=nullconstobserve=fn=>{isDryRun=truecurrentFn=fnfn()currentFn=nullisDryRun=false}
Then with Proxy/Reflect together, we could track data mutation and trigger dependent functions:
// … handlers// … observe// make data and arr reactiveconstdata={x:1,y:2}constproxy=newProxy(data,handlers)constarr=[1,2,3]constarrProxy=newProxy(arr,handlers)// observe functionsconstdepA=()=>console.log(`x = ${proxy.x}`)constdepB=()=>console.log(`y = ${proxy.y}`)constdepC=()=>console.log(`x + y = ${proxy.x+proxy.y}`)constdepD=()=>{letsum=0for (leti=0;i<arrProxy.length;i++){sum+=arrProxy[i]}console.log(`sum = ${sum}`)}// dry-run all dependentsobserve(depA)observe(depB)observe(depC)observe(depD)// output: x = 1, y = 2, x + y = 3, sum = 6// mutate dataproxy.x=3// output: x = 3, x + y = 5arrProxy[1]=4// output: sum = 8
Actually in early beta version of Vue 3.0, it uses WeakMap instead of Map so there won't be any memory leak to be worried about. But unfortunately, the performance is not good when data goes large. So later it changed back to flag properties.
Btw, there is also a trial of using Symbols as the flag property names. With Symbols the extreme cases could also be relieved a lot. But the same, the performance is still not good as normal string property names.
Although these experiments are not preserved finally, I think it's a good choice if you would like to make a pure (but maybe not quite performant) data observer on your own. So just mention this a little bit here.
Quick summary
Anyway we make data reactive first, and observe functions to track all the data they depend on. Then when we mutate the reactive data, relevant functions would be triggered to run again.
All the features and their further issues above have already been completed in Vue 3.0, with the power of ES2015 features.
If you would like to see all the live version of the code sample about explaining main mechanism of reactivity system in Vue from 0.x to 3.0. You could check out this CodePen and see its "Console" panel:
Now we have already known the basic usage of it - that's passing something into the data option into a Vue component, and then using it into other options like computed, watch, or the template. But this time, in Vue 3.0, it provides more util APIs, like markRaw we mentioned before. So let's take a look at these util APIs.
First let me introduce reactive(data). Just as the name, this API would create a reactive proxy for the data. But here maybe you don't need to use this directly, because the data object you return from the data option will be set up with this API automatically.
Then if you just would like:
Some pieces of data immutable, then you could use readonly(data).
Some pieces of data not reactive, then you could use markRaw(data).
With these 2 APIs, you could create a "shallow" data proxy, which means they won't setting traps deeply. Only the first-layer properties in these data proxies would be reactive or readonly. For example:
In this case, this.x.a is reactive, but this.x.a.b is not; this.y.a is readonly, but this.y.a.b is not.
If you only consume reactive data inside its own component, I think these APIs above are totally enough. But when things come to the real world, sometimes we would like to share states between components, or just abstract state out of a component for better maintenance. So we need more APIs below.
2. Ref for primitive values
A ref could help you to hold a reference for a reactive value. Mostly it's used for a primitive value. For example, somehow we have a number variable named counter in an ES module, but the code below doesn't work:
// store.js// This won't work.exportconstcounter=0;// This won't works neither.// import { reactive } from 'vue'// export const counter = reactive(0)
There is one thing to notice: if you would like to access the value of refs out of a template, you should access its value property instead. For example, if we'd like to modify bar.vue to avoid data option, we could add an increment method to do this, with counter.value:
As the example above, the typical usage of these APIs is spreading a reactive object into several sub variables and keep the reactivity at the same time.
2.4 Advanced: shallowRef(data)
Only trigger update when the ref.value is assigned by another value. For example:
import{shallowRef}from'vue'constdata={x:1,y:2}constref=shallowRef(data)// won't trigger updateref.value.x=3// will trigger updateref.value={x:3,y:2}
Case: computed(…)
Similar idea to computed option inside a Vue component. But if you would like to share a computed state out of a component, I suggest you try this API:
This API is my best favorite API in Vue 3.0. Because with this API, you could define how and when to track/trigger your data, during getting or setting the value, that's totally mind-blowing!
That makes real-world user input much easier to handle.
3. Watch for effects
watchEffect(function), watch(deps, callback)
In a Vue component, we could watch data mutations by watch option or vm.$watch() instance API. But the same question: what about watching data mutations out of a Vue component?
Similar to computed reactivity API vs. computed option, we have 2 reactivity APIs: watchEffect and watch.
// store.jsimport{ref,watch,watchEffect}from'vue'exportconstcounter=ref(0)// Will print the counter every time it's mutated.watchEffect(()=>console.log(`The counter is ${counter.value}`))// Do the similar thing with more optionswatch(counter,(newValue,oldValue)=>console.log(`The counter: from ${oldValue} to ${newValue}`))
4. Standalone package & usage
Also in Vue 3.0, we have a standalone package for these. That is @vue/reactivity. You could also import most of the APIs we mentioned above, from this package. So the code is almost the same to above:
The only difference is there is no watch and watchEffect. Instead there is another low-level API named effect. Its basic usage is just similar to watchEffect but more flexible and powerful.
For more details, I suggest you to read the source code directly:
So far we know a lot of things about the reactivity system in Vue, from the early version to 3.0. Now it's time to show some use cases based on that.
Composition API
The first thing is definitely the Vue Composition API, which is new in 3.0. With reactivity APIs, we could organize our code logic more flexibly.
import{ref,reactive,readonly,markRaw,computed,toRefs}from'vue'exportdefault{setup(props){constcounter=ref(0)constincrement=()=>counter.value++constproxy=reactive({x:1,y:2})constfrozen=readonly({x:1,y:2})constoneTimeLargeData=markRaw({...})constisZero=computed(()=>counter.value===0)constpropRefs=toRefs(props)// could use a,b,c,d,e,f in template and `this`return{a:counter,b:increment,c:proxy,d:frozen,e:oneTimeLargeData,f:isZero,...propRefs}}}
I don't wanna show more demos about that because they are already everywhere. But IMO, for a further benefit few people talking about is, previously in Vue 2.x and earlier, we are used to putting everything on this, when we:
Create reactive data for a component instance.
Access data/functions in the template.
Access data/functions outside the component instance, mostly it happens when we set a template ref on a sub Vue component.
All 3 things always happen together. That means maybe we just:
Would like to access something in the template, but don't need reactivity.
Would like to create reactive data, but don't use that in the template.
Vue Composition API elegantly decouples them out by 2 steps:
create reactive data;
decide what the template needs.
Btw, for public instance members, I think the potential problem is still there. However, it's not a big matter so far.
Also, there are some other benefits, including but not limited to:
Maintain reusable code without worrying about the naming conflict.
Gathering logically related code together, rather than gathering instance members together with the same option type.
Better and easier TypeScript support.
Also in Composition API, there are more APIs like provide()/inject(), lifecycle hooks, template refs, etc. For more about Composition API, please check out this URL: https://composition-api.vuejs.org/.
Cross-component state sharing
When sharing data between components. Reactivity APIs is also a good choice. We could even use them out of any Vue component, and finally use them into a Vue app, for example, with the composition APIs provide and inject:
// store.jsimport{ref}from'vue'// use Symbol to avoid naming conflictexportconstkey=Symbol()// create the storeexportconstcreateStore=()=>{constcounter=ref(0)constincrement=()=>counter.value++return{counter,increment}}
// App.vueimport{provide}from'vue'import{key,createStore}from'./store'exportdefault{setup(){// provide data firstprovide(key,createStore())}}
// Foo.vueimport{inject}from'vue'import{key}from'./store'exportdefault{setup(){// you could inject state with the key// and rename it before you pass it into the templateconst{counter}=inject(key)return{x:counter}}}
// Bar.vueimport{inject}from'vue'import{key}from'./store'exportdefault{setup(){// you could inject state with the key// and rename it before you pass it into the templateconst{increment}=inject(key)return{y:increment}}}
So once user call y() in Bar.vue, the x in Foo.vue would be updated as well. You don't even need any more state management library to do that. That's quite easy to use.
Remember vue-hooks?
It's not an active project anymore. But I remember after React Hooks first time announced, Evan, the creator of Vue, just gave a POC under Vue in 1 day with less than 100 lines of code.
Why it could be done so easily with Vue. I think mostly because of the reactivity system in Vue. It already helps you done most of the job. What we need to do is just encapsulate them into a new pattern or more friendly APIs.
Writing React with Vue reactivity system
So let's try one more step POC. How about using Reactivity APIs in React to create React components?
import*asReactfrom"react";import{effect,reactive}from"@vue/reactivity";constVue=({setup,render})=>{constComp=props=>{const[renderResult,setRenderResult]=React.useState(null);const[reactiveProps]=React.useState(reactive({}));Object.assign(reactiveProps,props);React.useEffect(()=>{constdata={...setup(reactiveProps)};effect(()=>setRenderResult(render(data)));},[]);returnrenderResult;};returnComp;};constFoo=Vue({setup:()=>{constcounter=ref(0);constincrement=()=>{counter.value++;};return{x:counter,y:increment};},render:({x,y})=><h1onClick={y}>Hello World {x.value}</h1>});
I did a little test like above, it's not a full implementation. But somehow we could maintain a basic React component with 2 parts:
Pure data logic with reactivity.
Any data update would be observed and trigger component re-render.
Those correspond to setup and render functions as a Vue component does.
And there is no way to worry about whether or not I write a React hook outside a React component or inside a conditional block. Just code it as you like and make it happen as you imagine.
Final final conclusions
So that's all about the reactivity system in Vue, from early version to the latest 3.0 Beta. I'm still learning a lot of new stuff like programming languages, paradigms, frameworks, and ideas. They are all great and shining. But the reactivity system is always a powerful and elegant tool to help me solve all kinds of problems. And it's still keeping evolved.
With ES2015+, the new Reactivity APIs and its independent package, Composition APIs, Vue 3.0, and more amazing stuff in the ecosystem and community. Hope you could use them or get inspired from them, to build more great things much easier.
Hope you could know Vue and its reactivity system better through this article.