Skip to content

Instantly share code, notes, and snippets.

@Jinjiang
Created May 25, 2020 16:19
Show Gist options
  • Save Jinjiang/f9b6f968af980cfd21cfc713e59db91b to your computer and use it in GitHub Desktop.
Save Jinjiang/f9b6f968af980cfd21cfc713e59db91b to your computer and use it in GitHub Desktop.
All code samples in "Understanding Reactivity in Vue 3.0"
$ npx create-vite-app hello-world
$ cd hello-world
$ npm install
$ npm run dev
new Vue({
el: '#app',
template: '<div @click="x++">{{x}} + {{y}} = {{z}}</div>',
data() {
return { x: 1, y: 2 }
},
computed: {
z() { return this.x + this.y }
},
watch: {
x(newValue, oldValue) {
console.log(`x is changed from ${oldValue} to ${newValue}`)
}
}
})
// data
const data = { x: 1, y: 2 }
// real data and deps behind
let realX = data.x
let realY = data.y
const realDepsX = []
const realDepsY = []
// make it reactive
Object.defineProperty(data, 'x', {
get() {
trackX()
return realX
},
set(v) {
realX = v
triggerX()
}
})
Object.defineProperty(data, 'y', {
get() {
trackY()
return realY
},
set(v) {
realY = v
triggerY()
}
})
// track and trigger a property
const trackX = () => {
if (isDryRun && currentDep) {
realDepsX.push(currentDep)
}
}
const trackY = () => {
if (isDryRun && currentDep) {
realDepsY.push(currentDep)
}
}
const triggerX = () => {
realDepsX.forEach(dep => dep())
}
const triggerY = () => {
realDepsY.forEach(dep => dep())
}
// observe a function
let isDryRun = false
let currentDep = null
const observe = fn => {
isDryRun = true
currentDep = fn
fn()
currentDep = null
isDryRun = false
}
// define 3 functions
const depA = () => console.log(`x = ${data.x}`)
const depB = () => console.log(`y = ${data.y}`)
const depC = () => console.log(`x + y = ${data.x + data.y}`)
// dry-run all dependents
observe(depA)
observe(depB)
observe(depC)
// output: x = 1, y = 2, x + y = 3
// mutate data
data.x = 3
// output: x = 3, x + y = 5
data.y = 4
// output: y = 4, x + y = 7
<template>
<p>
<span>Count is: {{ count }}</span>
<button @click="count++">increment</button>
is positive: {{ isPositive }}
</p>
</template>
<script>
export default {
data: () => ({ count: 0 }),
computed: {
isPositive() { return this.count > 0 }
}
}
</script>
<template>
<p>
<span>My name is {{ name.given }} {{ name.family }}</span>
<button @click="update">update name</button>
</p>
</template>
<script>
export default {
data: () => ({
name: {
given: 'Jinjiang'
}
}),
methods: {
update() {
this.name.family = 'Zhao'
}
}
}
</script>
<template>
<ul>
<li v-for="item, index in list" :key="index">
{{ item }}
<button @click="edit(index)">edit</button>
</li>
</ul>
</template>
<script>
export default {
data() {
return {
list: [
'Client meeting',
'Plan webinar',
'Email newsletter'
]
}
},
methods: {
edit(index) {
const newItem = prompt('Input a new item')
if (newItem) {
this.list[index] = newItem
}
}
}
}
</script>
<template>
<ul>...</ul>
<!-- btw Vue 3.0 supports multi-root template like this -->
<button @click="clean">clean</button>
</template>
<script>
export default {
data: ...,
methods: {
...,
clean() { this.list.length = 0 }
}
}
</script>
<template>
<div>
<ul>
<li v-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>
export default {
data: () => ({
list: new Set([
'Client meeting',
'Plan webinar',
'Email newsletter'
])
}),
created() {
console.log(this.list)
},
methods: {
remove(item) {
this.list.delete(item)
},
add() {
const newItem = prompt('Input a new item')
if (newItem) {
this.list.add(newItem)
}
},
clean() {
this.list.clear()
}
}
}
</script>
import { readonly } from 'vue'
export default {
data: () => ({
test: readonly({ name: 'Vue' })
}),
methods: {
update(){
this.test.name = 'Jinjiang'
}
}
}
<template>
<div>
Hello {{ test.name }}
<button @click="update">should not update</button>
</div>
</template>
<script>
import { markRaw } from 'vue'
export default {
data: () => ({
test: markRaw({ name: 'Vue' })
}),
methods: {
update(){
this.test.name = 'Jinjiang'
console.log(this.test)
}
}
}
</script>
import { ref, reactive, readonly, markRaw, computed, toRefs } from 'vue'
export default {
setup(props) {
const counter = ref(0)
const increment = () => counter.value++
const proxy = reactive({ x: 1, y: 2 })
const frozen = readonly({ x: 1, y: 2 })
const oneTimeLargeData = markRaw({ ... })
const isZero = computed(() => counter.value === 0)
const propRefs = 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
}
}
}
// store.js
import { ref, computed } from 'vue'
export const firstName = ref('Jinjiang')
export const lastName = ref('Zhao')
// getter only version
export const fullName = computed(() => `${firstName.value} ${lastName.value}`)
// getter + setter version
export const fullName2 = computed({
get: () => `${firstName.value} ${lastName.value}`,
set: (v) => {
const names = v.split(' ')
if (names.length > 0) {
firstName.value = names[0]
}
if (names.length > 1) {
lastName.value = names[names.length - 1]
}
}
})
// another-file.js
import { firstName, lastName, fullName, fullName2 } from './store.js'
console.log(fullName.value) // Jinjiang Zhao
firstName.value = 'Evan'
lastName.value = 'You'
console.log(fullName.value) // Evan You
fullName2.value = 'Jinjiang Zhao'
console.log(firstName.value) // Jinjiang
console.log(lastName.value) // Zhao
<template>
<input v-model="email" />
</template>
<script>
import { customRef } from 'vue'
import { validate } from 'isemail'
export default {
data() {
return {
email: customRef((track, trigger) => {
const value = ''
return {
get() {
track()
return value
},
set(v) {
if (validate(v)) {
value = v
trigger()
}
}
}
})
}
}
}
</script>
// track, trigger, reactive handlers
const track = (...arguments) => console.log('track', ...arguments)
const trigger = (...arguments) => console.log('trigger', ...arguments)
const reactiveHandlers = { ... }
// set an invisible skip flag to raw data
const markRaw = data => Object.defineProperty(data, '__v_skip', { value: true })
// create a proxy only when there is no skip flag on the data
const reactive = data => {
if (data.__v_skip) {
return data
}
return new Proxy(data, reactiveHandlers)
}
// create a proxy object for the data
const data = { x: 1, y: 2 }
const rawData = markRaw(data)
const reactiveData = readonly(data)
console.log(rawData === data) // true
console.log(reactiveData === data) // true
import { reactive, readonly, markRaw } from 'vue'
const ComponentFoo = {
data() {
return {
reactiveX: { x: 1 },
reactiveXInAnotherWay: reactive({ x: 1 }),
immutableY: readonly({ y: 2 }),
needntChangeReactivelyZ: markRaw({ z: 3 })
}
},
// ...
}
const { markRaw } from 'vue'
const obj = { x: 1 }
const result = markRaw(obj)
console.log(obj === result) // true
const ComponentFoo = {
data() {
return {
obj,
result
}
},
// ...
}
import { shallowReactive, shallowReadonly } from 'vue'
const ComponentFoo = {
data() {
return {
x: shallowReactive({ a: { b: 1 } }),
y: shallowReadonly({ a: { b: 1 } })
}
}
}
const map = new Map()
map.has('x')
map.get('x')
map.set('x', 1)
map.delete('x')
// a Map to record dependets
const dependentMap = new Map()
// track and trigger a property
const track = (type, data, propName) => {
if (isDryRun && currentFn) {
if (!dependentMap.has(data)) {
dependentMap.set(data, new Map())
}
if (!dependentMap.get(data).has(propName)) {
dependentMap.get(data).set(propName, new Set())
}
dependentMap.get(data).get(propName).add(currentFn)
}
}
const trigger = (type, data, propName) => {
dependentMap.get(data).get(propName).forEach(fn => fn())
}
// observe
let isDryRun = false
let currentFn = null
const observe = fn => {
isDryRun = true
currentFn = fn
fn()
currentFn = null
isDryRun = false
}
const track = (...arguments) => console.log('track', ...arguments)
const trigger = (...arguments) => console.log('trigger', ...arguments)
// all behaviors of a proxy by operation types
const handlers = {
get(...arguments) { track('get', ...arguments); return Reflect.get(...arguments) },
has(...arguments) { track('has', ...arguments); return Reflect.set(...arguments) },
set(...arguments) { Reflect.set(...arguments); trigger('set', ...arguments) },
deleteProperty(...arguments) {
Reflect.set(...arguments);
trigger('delete', ...arguments)
},
// ...
}
// create a proxy object for the data
const data = { x: 1, y: 2 }
const proxy = new Proxy(data, handlers)
// will call `trigger()` in `set()`
proxy.z = 3
// create a proxy object for an array
const arr = [1,2,3]
const arrProxy = new Proxy(arr, handlers)
// will call `track()` & `trigger()` when get/set by index
arrProxy[0]
arrProxy[1] = 4
// will call `trigger()` when set `length`
arrProxy.length = 0
const data = { x: 1, y: 2 }
// all behaviors of a proxy by operation types
const handlers = {
get(data, propName, proxy) {
console.log(`Get ${propName}: ${data[propName]}!`)
return data[propName]
},
has(data, propName) { ... },
set(data, propName, value, proxy) { ... },
deleteProperty(data, propName) { ... },
...
}
// create a proxy object for the data
const proxy = new Proxy(data, handlers)
// print: 'Get x: 1' and return `1`
proxy.x
const data = { x: 1, y: 2 }
// all behaviors of a proxy by operation types
const handlers = {
get(data, propName, proxy) {
console.log(`Get ${propName}: ${data[propName]}!`)
// same behavior as before
return Reflect.get(data, propName, proxy)
},
has(...arguments) { return Reflect.set(...arguments) },
set(...arguments) { return Reflect.set(...arguments) },
deleteProperty(...arguments) { return Reflect.set(...arguments) },
// ...
}
// create a proxy object for the data
const proxy = new Proxy(data, handlers)
// print: 'Get x: 1' and return `1`
proxy.x
import { reactive, computed, effect } from '@vue/reactivity'
const data = { x: 1, y: 2 }
const proxy = reactive(data)
const z = computed(() => proxy.x + proxy.y)
// print 'sum: 3'
effect(() => console.log(`sum: ${z.value}`))
console.log(proxy.x, proxy.y, z.value) // 1, 2, 3
proxy.x = 11 // print 'sum: 13'
console.log(proxy.x, proxy.y, z.value) // 11, 2, 13
// … handlers
// … observe
// make data and arr reactive
const data = { x: 1, y: 2 }
const proxy = new Proxy(data, handlers)
const arr = [1, 2, 3]
const arrProxy = new Proxy(arr, handlers)
// observe functions
const depA = () => console.log(`x = ${proxy.x}`)
const depB = () => console.log(`y = ${proxy.y}`)
const depC = () => console.log(`x + y = ${proxy.x + proxy.y}`)
const depD = () => {
let sum = 0
for (let i = 0; i < arrProxy.length; i++) {
sum += arrProxy[i]
}
console.log(`sum = ${sum}`)
}
// dry-run all dependents
observe(depA)
observe(depB)
observe(depC)
observe(depD)
// output: x = 1, y = 2, x + y = 3, sum = 6
// mutate data
proxy.x = 3
// output: x = 3, x + y = 5
arrProxy[1] = 4
// output: sum = 8
const track = (...arguments) => console.log('track', ...arguments)
const trigger = (...arguments) => console.log('trigger', ...arguments)
// all behaviors of a proxy by operation types
const handlers = {
get(...arguments) { track('get', ...arguments); return Reflect.get(...arguments) },
has(...arguments) { track('has', ...arguments); return Reflect.set(...arguments) },
set(...arguments) {
console.warn('This is a readonly proxy, you couldn\'t modify it.')
},
deleteProperty(...arguments) {
console.warn('This is a readonly proxy, you couldn\'t modify it.')
},
// ...
}
// create a proxy object for the data
const data = { x: 1, y: 2 }
const readonly = new Proxy(data, handlers)
// will warn that you couldn't modify it
readonly.z = 3
// will warn that you couldn't modify it
delete readonly.x
// store.js
import { ref } from 'vue'
export const counter = ref(0)
// bar.vue
<template>
<button @click="increment">increment</button>
</template>
<script>
import { counter } from './store.js'
export {
methods: {
increment() { counter.value++ }
}
}
</script>
import { reactive, toRef, toRefs } from 'vue'
const proxy = reactive({ x: 1, y: 2 })
const refX = toRef(proxy, 'x')
proxy.x = 3
console.log(refX.value) // 3
const refs = toRefs(proxy)
proxy.y = 4
console.log(refs.x.value) // 3
console.log(refs.y.value) // 4
import { shallowRef } from 'vue'
const data = { x: 1, y: 2 }
const ref = shallowRef(data)
// won't trigger update
ref.value.x = 3
// will trigger update
ref.value = { x: 3, y: 2 }
// store.js
// This won't work.
export const counter = 0;
// This won't works neither.
// import { reactive } from 'vue'
// export const counter = reactive(0)
// foo.vue
<template>
<div>
{{ counter }}
</div>
</template>
<script>
import { counter } from './store.js'
export {
data() {
return { counter }
}
}
</script>
// bar.vue
<template>
<button @click="counter++">increment</button>
</template>
<script>
import { counter } from './store.js'
export {
data() {
return { counter }
}
}
</script>
// store.js
import { ref } from 'vue'
// use Symbol to avoid naming conflict
export const key = Symbol()
// create the store
export const createStore = () => {
const counter = ref(0)
const increment = () => counter.value++
return { counter, increment }
}
// App.vue
import { provide } from 'vue'
import { key, createStore } from './store'
export default {
setup() {
// provide data first
provide(key, createStore())
}
}
// Foo.vue
import { inject } from 'vue'
import { key } from './store'
export default {
setup() {
// you could inject state with the key
// and rename it before you pass it into the template
const { counter } = inject(key)
return { x: counter }
}
}
// Bar.vue
import { inject } from 'vue'
import { key } from './store'
export default {
setup() {
// you could inject state with the key
// and rename it before you pass it into the template
const { increment } = inject(key)
return { y: increment }
}
}
// store.js
import { ref, watch, watchEffect } from 'vue'
export const counter = 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 options
watch(counter, (newValue, oldValue) =>
console.log(`The counter: from ${oldValue} to ${newValue}`)
)
import * as React from "react";
import { effect, reactive } from "@vue/reactivity";
const Vue = ({ setup, render }) => {
const Comp = props => {
const [renderResult, setRenderResult] = React.useState(null);
const [reactiveProps] = React.useState(reactive({}));
Object.assign(reactiveProps, props);
React.useEffect(() => {
const data = { ...setup(reactiveProps) };
effect(() => setRenderResult(render(data)));
}, []);
return renderResult;
};
return Comp;
};
const Foo = Vue({
setup: () => {
const counter = ref(0);
const increment = () => {
counter.value++;
};
return { x: counter, y: increment };
},
render: ({ x, y }) => <h1 onClick={y}>Hello World {x.value}</h1>
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment