Vue 2 implements a reactivity system that tracks dependencies and updates views when data changes. This system works differently for objects versus arrays.
Objectt Reactivity
For objects, Vue utilizes the defineReactive function, which internally employs Object.defineProperty to establish getter and setter methods for each property. The system recursively traverses object properties to establish reactivity at all level.
Array Reactivity
Arrays require special handling since Vue can't directly detect changes when using array indices. Instead, Vue intercepts and wraps seven key array methods:
push- Adds elements to the end of an array, returns new lengthpop- Removes the last element, returns the removed elementshift- Removes the first element, returns the removed elementunshift- Adds elements to the beginning, returns new lengthsort- Sorts elements by Unicode code points, returns the arraysplice- Adds/removes elements at any position, returns removed elementsreverse- Reverses array order, returns the array
Project Structure
public/index.html
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue Reactivity Demo</title>
</head>
<body>
<div id="app">Hello Vue</div>
<script src="../dist/umd/framework.js"></script>
<script>
const app = new Framework({
el: '#app',
data: {
count: 100,
info: { level: 10 },
value: 30,
items: [8, 9, 10, { score: 99 }]
}
})
</script>
</body>
</html>
state.js
import { observe } from "./observer/index.js"
export function initializeState(vm) {
const options = vm.$options
if (options.props) {
initializeProps(vm)
}
if (options.methods) {
initializeMethods(vm)
}
if (options.data) {
initializeData(vm)
}
if (options.computed) {
initializeComputed(vm)
}
if (options.watch) {
initializeWatch(vm)
}
}
function initializeProps(vm) {
// Implementation would go here
}
function initializeMethods(vm) {
// Implementation would go here
}
function initializeData(vm) {
let data = vm.$options.data
vm.data = data = typeof data === 'function' ? data.call(vm) : data
// Observe the data
observe(data)
}
function initializeComputed(vm) {
// Implementation would go here
}
function initializeWatch(vm) {
// Implementation would go here
}
init.js
import initializeState from "./state"
export function setupInitMixin(Framework) {
Framework.prototype._init = function (options) {
const vm = this
vm.$options = options
// Initialize state
initializeState(vm)
}
}
index.js
import setupInitMixin from "./init"
function Framework(options) {
// Initialize
this._init(options)
}
// Add _init method to prototype
// Setup init mixin
setupInitMixin(Framework)
export default Framework
observer/index.js
import arrayMethods from "./array";
class Observer {
constructor(value) {
// Mark the value as observed
Object.defineProperty(value, '__observer__', {
enumerable: false,
configurable: false,
value: this
})
if (Array.isArray(value)) {
// Override array prototype
value.__proto__ = arrayMethods
this.observeArray(value)
} else {
this.walk(value)
}
}
observeArray(value) {
value.forEach(item => observe(item));
// Observe each array element
}
walk(value) {
const keys = Object.keys(value)
for (let i = 0; i < keys.length; i++) {
let key = keys[i]
let v = value[key]
defineReactive(value, key, v)
}
}
}
function defineReactive(data, key, value) {
// Value might be an object, so observe it
observe(value)
Object.defineProperty(data, key, {
get() {
console.log(`Getting ${key}`);
return value
},
set(newV) {
if (newV === value) return
observe(newV) // New value might be an object
console.log(`Setting ${key} to`, newV);
value = newV
}
})
}
export function observe(data) {
if (typeof data !== 'object' || data === null) {
return;
}
return new Observer(data)
}
array.js
// Create a new object with Array.prototype as its prototype
let arrayOverrides = Object.create(Array.prototype);
// Override array methods for reactivity
['push', 'pop', 'shift', 'unshift', 'sort', 'splice', 'reverse']
.forEach((method) => {
arrayOverrides[method] = function (...args) {
console.log('Array modified, triggering view update');
// Handle newly inserted elements
let inserted;
switch (method) {
case 'push':
case 'unshift':
inserted = args;
break;
case 'splice':
inserted = args.slice(2)
}
if (inserted) {
// Observe each inserted element
this.__observer__.observeArray(inserted)
}
// Call original method
return Array.prototype[method].call(this, ...args)
}
});
export default arrayOverrides
package.json
{
"name": "vue-reactivity-demo",
"version": "1.0.0",
"description": "A simplified implementation of Vue's reactivity system",
"main": "index.js",
"scripts": {
"dev": "rollup -c -w"
},
"keywords": ["vue", "reactivity", "frontend"],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.22.9",
"@babel/preset-env": "^7.22.9",
"rollup": "^2.79.1",
"rollup-plugin-babel": "^4.4.0",
"rollup-plugin-serve": "^2.0.2"
}
}