Write in front: this is a study of vue3 source notes, records vue3 reactivity implementation process and principle. The outline was not carefully laid out.
There are two reasons for writing this: 1. To learn about VUe3; 2. In order to develop a good habit of taking notes in the process of learning.
1. Basic build
1.1 Initializing the Project
1.1.1
Initialize the default package.json
yarn init -y
Copy the code
Create packages folder and modify package.json file:
{
"name": "my-vue3",
+ "private": true,"Version" : "1.0.0", "main" : "index. Js", "license" : "MIT",+ "workspaces": [
+ "packages/*"
+]
}
Copy the code
1.1.2
Create the Reactivity folder and shared folder in the Package folder, init a package.json file and a SRC folder, respectively, and create an index.ts file in both SRC folders. The project catalog is as follows
vue
|- /packages
+ |- /reactivity
+ |- /src
+ |- index.ts
+ |- package.json
+ |- /shared
+ |- /src
+ |- index.ts
+ |- package.json
package.json
Copy the code
Modify package.json files in reactivity and shared directories
// Take reactivity as an example.- "name": "reactivity",
+ "name": "@vue/reactivity","Version" : "1.0.0", "main" : "index. Js", / / commonJs (Nodejs)+ "dist/reactivity.esm-bundler.js", // webpack, ES6
"license": "MIT",
+ "buildOptions": {// custom configuration for rollup
+ "name": "VueReactivity", // custom global package name (shared global name does not matter
+ "formats": [// Supported types
+ "cjs", // node
+ "esm-bundler", //es6
+ "global" // Shared is not needed as a shared module of vUE
+]
+}
}
Copy the code
Module export
// packages/reactivity/src/index.ts
const Reactivity = {
}
export {
Reactivity
}
Copy the code
// packages/shared/src/index.ts
const shared = {
}
export {
shared
}
Copy the code
1.1.3 Installing Dependencies
Install dependencies in the root directory
- The installation
typescript
与rollup
Dependent dependencies @rollup/plugin-node-resolve
: Parsing third-party modules enables us to support third-party modules@rollup/plugin-json
: Supports parsing JSONexeca
: Uses multiple processes--ignore-workspace-root-check
: Ignore the workspace
yarn add typescript rollup rollup-plugin-typescript2 @rollup/plugin-node-resolve @rollup/plugin-json execa --ignore-workspace-root-check
Copy the code
1.1.4 Adding scripts Command
Create the scripts folder in the root directory with the following project directory
vue
|- /packages
+ |- /scripts
+ dev.js // development environment, which can be packaged for a module package
+ build.js // production environment, package all
package.json
Copy the code
{" name ":" my - vue3 ", "private" : true, "version" : "1.0.0", "main" : "index. Js", "license" : "MIT", "workspaces" : [ "packages/*" ],+ "scripts": {
+ "dev": "node scripts/dev.js",
+ "build": "node scripts/build.js"
+},"Dependencies" : {" @ rollup/plugin - json ":" ^ 4.1.0 ", "@ rollup/plugin - node - resolve", "^ 11.2.1", "execa" : "^ 5.0.0 rollup", ""," ^ 2.43.0 ", "a rollup - plugin - typescript2" : "^ 0.30.0", "typescript" : "^ holdings"}}Copy the code
1.1.5 Build package command
// scripts/build.js
const fs = require('fs')
// Synchronously read files in packages directory
const targets = fs.readdirSync('packages').filter(f= > {
// Filter out the files that need to be packed
// Keep the folder and delete the files
if(! fs.statSync(`packages/${f}`).isDirectory()) {
return false
}
return true
})
// Package method
const build = async (target) => {
// Target for packaging
console.log(target)
}
// A method to package multiple targets
const runParallel = (targets, iteratorFn) = > {
const res = []
for (const item of targets) {
// Each package is a promise
const p = iteratorFn(item)
res.push(p)
}
return Promise.all(res)
}
// Package the target directories one by one, in parallel
runParallel(targets, build)
// runParallel(targets, build).then(() => {
// console.log(' All packages are packed ')
// })
Copy the code
At this point, we have basically completed the prototype of packaging multiple objects, and we have gradually improved our packaging method, Build
// scripts/build.js
const fs = require('fs')
+ const execa = require('execa') // Start the child process for packaging, and finally use rollup for packaging. Const build = async (target) => {// The first argument is the command to execute rollup, The second is to execute the argument // -c to adopt a configuration file // --environment to adopt an environment variable // TARGET TARGET // The information packed by the stdio child is shared with the parent process+ await execa('rollup', ['-c', '--environment', `TARGET:${target}`, {stdio: inherit}])
}
Copy the code
1.1.6 configuration rollup
Create the rollup.config.js file in the root directory, and let’s see if we can get the package destination in rollup
// rollup.config.js
console.log(process.env.TARGET, '----rollup-----')
Copy the code
The correct output will be found in the console after yarn build (ignore the throw error because we did not export the file) reactivity —-rollup—– and shared —-rollup—–
We then start formally configuring rollup.config.js
// rollup configuration
import path from 'path'
// 8. Introduce plug-ins
import json from '@rollup/plugin-json'
import resolvePlugin from '@rollup/plugin-node-resolve'
import ts from 'rollup-plugin-typescript2'
// Get package.json from the corresponding module based on the target attribute in the environment variable
// 1. Find packages folder in current directory
const packagesDir = path.resolve(__dirname, 'packages')
// 2. Find the corresponding destination directory to package
const packageDir = path.resolve(packagesDir, process.env.TARGET)
// encapsulates a method to find files under the corresponding package
const resolve = p= > path.resolve(packageDir, p)
// 3. Find package.json for the corresponding package directory
const pkg = require(resolve('package.json'))
// Get the file name
const name = path.basename(packageDir)
// 4. Create a mapping table for the packaging type, and format the content to be packaged according to the provided formats
// outputConfig is the output configuration based on the package.json buildOptions configuration of the corresponding package
const outputConfig = {
'esm-bundler': {
file: resolve(`dist/${name}.esm-bundler.js`),
format: 'es'
},
'cjs': {
file: resolve(`dist/${name}.cjs.js`),
format: 'cjs'
},
'global': {
file: resolve(`dist/${name}.global.js`),
format: 'iife' // Execute the function immediately}}// 5. Corresponding package.json buildOptions in the package
const options = pkg.buildOptions
// 7. Create rollup configuration
const createConfig = (format, output) = > {
output.name = options.name
// See if the requirement needs to generate sourcemAP
output.sourcemap = true
// Generate the rollup configuration
return {
input: resolve(`src/index.ts`), / / the entry
output, / / export
plugins: [ // Plugins, note the order
json(),
ts(),
resolvePlugin() // Parse third-party module plug-ins]}}// 6. Rollup the final configuration to be exported
export default options.formats.map(format= > createConfig(format, outputConfig[format]))
Copy the code
At this point, the TS plug-in needs to be configured for our entire rollup, so let’s start configuring ts
npx tsc --init
Copy the code
NPX TSC –init generates a tsconfig.js file and changes the values of the target and module fields in the file to ES2015 or higher. Here we change the values to ESNEXT. Change to true
Next, go to rollup.config.js to apply the file
// rollup.config.js
/ /...
const createConfig = (format, output) = > {
output.name = options.name
// See if the requirement needs to generate sourcemAP
output.sourcemap = true
// Generate the rollup configuration
return {
input: resolve(`src/index.ts`), / / the entry
output, / / export
plugins: [ // Plugins, note the order
json(),
ts({
tsconfig: path.resolve(__dirname, 'tsconfig.json')
}),
resolvePlugin() // Parse third-party module plug-ins]}}/ /...
Copy the code
1.1.7 Complete the configuration and pack the package
yarn build
Copy the code
Once the package is complete we can look in the packages folder to see the corresponding module and there will be a dist folder with the corresponding files that we configured initially
Such as:
CommonJS: exports.Reactivity = Reactivity for Node
Esm-bundler. js for ES6: export {Reactivity}
Global variable defined if there is global.js: var VueReactivity
1.2 Development Environment
1.2.1 Development Environment Configuration
Following the steps above, we have completed the process from zero to successful packaging using rollup. Next, we will briefly configure the Dev environment
// /scripts/dev.js
// const fs = require('fs')
const execa = require('execa')
// Synchronously read files in packages directory
const targets = 'reactivity'
// Package method
const build = async (target) => {
// The first argument is the executed command rollup, and the second argument is the executed argument
-c indicates that a configuration file is used
/ / - the cw listening in
// --environment indicates that environment variables are used
/ / TARGET goal
// The third argument stdio shares the information packed by the child process with the parent process
await execa('rollup'['-cw'.'--environment'.`TARGET:${target}`] and {stdio: 'inherit' })
}
build(targets)
Copy the code
The dev environment is much simpler to configure than the production environment:
- Remove the code that packages all modules, packaging only one module
- Change the rollup configuration from
-c
Modified to-cw
Form a listening package for the file
After the configuration is complete, you only need to modify the content in the corresponding package file to package the package again
1.2.2 References between modules
Each module must reference other modules. In order to reduce the cost of reference, we configure this slightly
When you run the yarn command (yarn only), an @vue file is generated in node_modules. There are two shortcut folders in the file, reactivity and shared, whose paths correspond to two files in the Packages folder. Click to jump to the corresponding files. The reason is the workspaces field in package.json and the name field configuration in package.json in the file. We can then try referencing shared directly in reactivity/ SRC /index.ts
+ import { shared } from '@vue/shared'
const Reactivity = {}
export {
Reactivity
}
Copy the code
We’ll see that the ts throws the wrong reference, indicating that we need to set the moduleResolution option to “node” and add aliases like the “Paths” option.
// tsconfig.json
{
"compilerOptions": {
// ......
+ "moduleResolution": "Node",
+ "baseUrl": ".", // If not configured, paths cannot be used
+ "paths": {
+ "@vue/*": [
+ "packages/*/src"
+]
+}}}Copy the code
At this point, the references between modules are completed, and we can begin to write functions in the Reactivity
1.3reactivity
File to add proxy methods
Start by creating a reactivity. ts file in the reactivity/ SRC directory that contains the vue data response method as follows.
// reactivity/src/reactive.ts
// Data response
export function reactive() {}// The first layer is the data response
export function shallowReactive() {}// Only read data
export function readonly() {}// The first layer is read only
export function shallowReadonly() {}export {
reactive,
shallowReactive,
readonly,
shallowReadonly
}
Copy the code
Then change the reactivity/ SRC /index.ts file to
// reactivity/src/index.ts
export { reactive, shallowReactive, readonly, shallowReadonly } from './reactive'
Copy the code
1.3.1 Implementation of data proxy
We will first implement Reactive, and then step by step implement other functions and optimize the code
// reactivity/src/reactive.ts
import { isObject } from '@vue/shared'
export function reactive (target){
// isObject is a method implemented in @vue/shared
// 1. Do not use Proxy if the type is not object
if(! isObject(target))return target
// use Proxy
const proxy = new Proxy(target, {
// target: indicates the target object. Key: the name of the property to be obtained. Receiver: Proxy or an object that inherits Proxy
get: function (target, key, receiver) {
// Use Reflect to Reflect the value
const res = Reflect.get(target, key, receiver)
// If an object is obtained, the object is proxied
// Vue2 is recursive from the start, while vue3 is a lazy proxy
if (isObject(res)) return reactive(res)
return res
},
set: function (target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver)
// Not yet processed
return result
}
})
/ / return the proxy
return proxy
}
Copy the code
And then we implement shallowReactive
// reactivity/src/reactive.ts
import { isObject } from '@vue/shared'
export function reactive (target){
/ /...
}
export function shallowReactive (target){
// isObject is a method implemented in @vue/shared
// 1. Do not use Proxy if the type is not object
if(! isObject(target))return target
// use Proxy
const proxy = new Proxy(target, {
// target: indicates the target object. Key: the name of the property to be obtained. Receiver: Proxy or an object that inherits Proxy
get: function (target, key, receiver) {
// Use Reflect to Reflect the value
const res = Reflect.get(target, key, receiver)
return res
},
set: function (target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver)
return result
}
})
/ / return the proxy
return proxy
}
Copy the code
We found that the two functions were basically the same, the difference being whether the next layer needed to be proxied or not, and we optimized the two pieces of code to make them Currified
// reactivity/src/reactive.ts
/ /...
// Create a proxy event
function createProxyHandler(target, baseHandlers) {
// 1. Do not use Proxy if the type is not object
if(! isObject(target))return target
// 2. Data interception is extracted
const proxy = new Proxy(target, baseHandlers)
return proxy
}
Copy the code
So once we extract the data interceptor part, and that’s not all, there’s a lot of repetitive code in that part, we’re going to do baseHandlers
// reactivity/src/reactive.ts
// 1. Extract the corresponding data interception part
export function reactive(target) {
return createProxyHandler(target, mutableHandlers)
}
export function shallowReactive(target) {
return createProxyHandler(target, shallowReactiveHandlers)
}
export function createProxyHandler(target, baseHandlers) {
/ /...
}
Copy the code
Because the baseHandlers part is the data interceptor part, we created a new file to code the baseHandlers part, and our project went in a much better direction.
Create the basehandlers.ts file
// packages/reactivity/src/baseHandlers.ts
// 2. Get, set
const get = createGetter()
const shallowGet = createGetter(true)
const set = createSetter()
const shallowSet = createSetter(true)
// 1. Create data interception
// reactive corresponds to data interception
export const mutableHandlers = {
get,
set
}
// shallowReactive
export const shallowReactiveHandlers = {
get: shallowGet,
shallowSet
}
// 3. Create an interception method for get
function createGetter(isShallow = false) {
// First we need to return a get method
return function get(target, key, receiver) {
// target: indicates the target object. Key: the name of the property to be obtained. Receiver: Proxy or an object that inherits Proxy
// Use Reflect to Reflect the value
const res = Reflect.get(target, key, receiver)
// If it is a shallow proxy, the next layer will not be proxy
if (isShallow) return res
// If an object is obtained, the object is proxied
// Vue2 is recursive from the start, while vue3 is a lazy proxy
if (isObject(res)) return reactive(res)
return res
}
}
function createSetter(isShallow = false) {
return function set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver)
// shallowSet will not be processed for now
return result
}
}
Copy the code
So far, most of the functions of VUe3’s Reactive and shallowReactive are implemented.
We can test these two functions first. Create an example folder in the root directory and create a 1.reaction-api.html to test.
<! DOCTYPEhtml>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="Width = device - width, initial - scale = 1.0">
<title>Document</title>
</head>
<body>
<script src=".. /node_modules/@vue/reactivity/dist/reactivity.global.js"></script>
<script>
let { reactive, shallowReactive } = VueReactivity
const state = reactive({ name: 'jerry'.age: { n: 18}})const state1 = shallowReactive({ name: 'jerry'.age: { n: 18}})console.log('state.age', state.age)
console.log('state1.age', state1.age)
</script>
</body>
</html>
Copy the code
When you open the file in a browser, it’s clear from the console that state.age is still propped and state1.age is just a plain object
You can then test the two newly created methods in example/ 1.reaction-api.html.
However, when using data, the following situations can occur
const state = reactive({ name: 'jerry' })
const state1 = reactive(state)
Copy the code
Here state is a proxied object and is proxied again, which is a waste of resources, and given that we have the readonly type later, we can deal with these situations.
// packages/reactivity/src/reactive.ts
/ /...
// WeakMap uses objects as storage keys and garbage collection automatically without causing memory leaks
const reactiveMap = new WeakMap(a)const readonlyMap = new WeakMap(a)// 1. Add an isReadonly parameter
function createReactiveObject (target, isReadonly, baseHandlers) {
if(! isObject(res))return target
// 2. If an object has already been proxied
const proxyMap = isReadonly ? readonlyMap : reactiveMap
const exisitProxy = proxyMap.get(target)
// 3. Return this data if the agent already exists
if (exisitProxy) return exisitProxy
const proxy = new Proxy(target, baseHandlers)
return proxy
}
Copy the code
Next, we continue to implement the readonly and shallowReadonly methods in VUe3
// packages/reactivity/src/reactive.ts
import { isObject } from "@vue/shared"
import { mutableHandlers, shallowReactiveHandlers, readonlyHandlers, shallowReadonlyHandlers } from './baseHandlers'
// export function reactive ...
// export function shallowReactive ...
// 1. Add the readonly method and shallowReadonly contents
export function readonly (target) {
return createProxyHandler(target, readonlyHandlers)
}
export function shallowReadonly (target) {
return createProxyHandler(target, true, shallowReadonlyHandlers)
}
// function createProxyHandler ...
Copy the code
Improve baseHandlers
// packages/reactivity/src/baseHandlers.ts
import { isObject } from '@vue/shared'
const readonlySet = (target, key) = > console.warn(`set on key ${key} faild`)
// 2. Modify the incoming parameters and add the corresponding read-only create
const get = createGetter()
const shallowGet = createGetter(false.true)
const readonlyGet = createGetter(true)
const shallowReadonlyGet = createGetter(true.true)
const set = createSetter()
const shallowSet = createSetter()
// export const mutableHandlers ...
// 3. Export only read parts
export const readonlyHandlers = {
get: readonlyGet,
set: readonlySet
}
export const shallowReadonlyHandlers ={
get: shallowReadonlyGet,
set: readonlySet
}
// 1. Add whether to read only
function createGetter (isReadonly = false, isShallow = false) {
// target: indicates the target object. Key: the name of the property to be obtained. Receiver: Proxy or an object that inherits Proxy
return function get (target, key, receiver) {
const res = Reflect.get(target, key, receiver)
if(! isReadonly) {// Instead of just reading, you need to collect dependencies and update the corresponding view as the data changes
}
if (isShallow) return res
// Determine whether the next layer is read-only or responsive based on whether it is read-only
if (isObject(res)) return isReadonly ? readonly(res) : reactive(res)
return res
}
}
Copy the code
1.4 implementation effect
Create the effect.ts file under Packages /reactivity/ SRC
// packages/reactivity/src/effect.ts
// 1. Export effect
/ * * * *@param Fn effect the first parameter *@param Options effect configuration */
export function effect (fn, options = {}) {
// effect needs to be responsive and can be re-executed with data changes
const effect = createReactiveEffect(fn, options)
// Default effect needs to be executed first
if(! options.lazy) {// Lazy does not need to be executed
effect()
}
return effect
}
// effect flag, used to distinguish different effects
let uid = 0
// Create a reactive effect
function createReactiveEffect (fn, options) {
const effect = function reactiveEffect() {
// Execute the fn passed in on the page
fn()
}
// Add an identifier to effect
effect.id = uid++
// This is used to identify the reactive effect
effect._isEffect = true
// leave the corresponding function in effect
effect.raw = fn
// Keep the user attributes
effect.options = options
return effect
}
Copy the code
Re-create an HTML file in the Example directory to test effect
<script>
let { effect, reactive } = VueReactivity
let state = reactive({ name: 'jerry'.age: 12 })
effect(() = > {
app.innerHTML = `${state.name}`
state.age++
})
state.name = 'xxxx'
</script>
Copy the code
Open the browser and the data effect method using Reactive is triggered successfully.
However, we found that the effect method did not fire when we changed the state data because we did not collect the dependencies, and we continued to refine it.
- in
effect
Add atrack
Method is used to collect dependencies
// packages/reactivity/src/effect.ts
/ /...
// Used to collect dependencies
export function track (target, type, key) {
console.log(target, type, key)
}
Copy the code
Add an operates.ts file in the sibling directory to mark the trace type
// packages/reactivity/src/operators.ts
export const enum TrackOpTypes {
GET = 'get'
}
Copy the code
Add the track method when retrieving data
// packages/reactivity/src/baseHandlers.ts
// ...
import { track } from './effect'
// ...
function createGetter(isReadonly = false, isShallow = false) {
return function get (target, key, receiver) {
const res = Reflect.get(target, key, receiver)
if(! isReadonly) {// 1. Add a collection dependency
track(target, TrackOpType.GET, key)
}
/ /...}}Copy the code
At this point, open the browser to check, and it will be found that the track method will be triggered as many times as the value is taken. However, we do not know which effect is triggered, so we need to add a variable of activeEffect to record which effect is triggered
// packages/reactivity/src/effect.ts
let uid = 0
// Store the current effect
let activeEffect
function createReactiveEffect(fn, options) {
const effect = function reactiveEffect() {
// Store the current effect
activeEffect = effect
// 5. When the function is executed, the get method is required
return fn()
}
// Create an effect flag to distinguish the effect
effect.id = uid++
// This is a reactive effect
effect._isEffect = true
// leave the original function corresponding to effect
effect.raw = fn
// Save the user attributes on effect
effect.options = options
return effect
}
export function track (target, type, key) {
console.log(target, type, key)
}
Copy the code
If we write the following code on the page:
effect(() = > {
console.log(state.name)
effect(() = > {
console.log(state.age)
})
})
Copy the code
After executing the second effect, the activeEffect still points to the second effect when it needs to point to the first, and we need a stack to store it
// packages/reactivity/src/effect.ts
let uid = 0
// Store the current effect
let activeEffect
let effectStack = []
function createReactiveEffect(fn, options) {
const effect = function reactiveEffect() {
// 4. Execute fn repeatedly to prevent effect from being pushed repeatedly
if(! effectStack.includes(effect)) {// 2. Use try to prevent fn from throwing errors. Use finally to ensure that this effect can point to the next effect
try {
// 1. Push current effect to the stack
effectStack.push(effect)
// Store the current effect
activeEffect = effect
// When the function is executed, the get method is required
return fn()
} finally {
// 3. Remove current effect from stack
effecStack.pop()
activeEffect = effectStack[effectStack.length -1]}}}// Create an effect flag to distinguish the effect
effect.id = uid++
// This is a reactive effect
effect._isEffect = true
// leave the original function corresponding to effect
effect.raw = fn
// Save the user attributes on effect
effect.options = options
return effect
}
export function track (target, type, key) {
// 5. The attribute key corresponds to effect
console.log(target, type, key, activeEffect)
}
Copy the code
- First of all, we have
try
Lt.effect
, and get the currenteffect
And then executefn
- Then, in
finally
I’m going to push it off the stackactiveEffect
Point back to the previous oneeffect
- Then we add a prevent before we execute all this
effect
The judgment of being repeatedly pushed
Once this is done, we can map attributes to effects in track to collect dependencies, as shown below
// packages/reactivity/src/effect.ts
/ /...
// Collect the current effect function for an attribute in an object
const targetMap = new WeakMap(a)export function track(target, type, key) {
if (activeEffect === undefined) return
// 1. Locate the target in the targetMap table
let depsMap = targetMap.get(target)
// 2. Check whether the object exists in the targetMap mapping table
if(! depsMap) {If the object does not exist in the 2.1 mapping table, it will be stored and its value will be set to an empty Map
targetMap.set(target, (depsMap = new Map()))}Select * from depsMap; // select * from depsMap
let dep = depsMap.get(key)
// 4. Check whether the depsMap table of the target attribute of targetMap can obtain the key
if(! dep) {// 4.1 If the attribute key does not exist in the mapping table, it is stored and its value is stored using Set
depsMap.set(key, dep = new Set()) // Use Set storage to prevent duplication
}
// 5. Check whether the depsMap key of the target attribute of targetMap stores the corresponding effect
if(! dep.has(activeEffect)) {5.1 Add this effect if the key does not exist
dep.add(activeEffect)
}
console.log(targetMap)
}
Copy the code
When we open the test page in the browser, we can see that the dependencies are collected correctly even if there are multiple effects on the page and they all use state.
Now that we have collected the dependency of effect on ‘state’, we can trigger the corresponding effect when’ state’ changes. It is important to note that there are two types of state changes — additions and modifications, and we need to distinguish between them.
- Adding an identifier
// packages/reactivity/src/operators.ts
// ...
export const enum TriggerOrTypes {
ADD = 'add',
SET = 'set'
}
Copy the code
- add
trigger
methods
// packages/reactivity/src/effect.ts
/ /...
export function trigger(target, type, key, newValue, oldValue) {}Copy the code
- call
trigger
// packages/reactivity/src/baseHandlers.ts
/ /...
function createSetter(shallow = false) {
return function set(target, key, value, receiver) {
const oldValue = target[key]
// 1. Check whether there is a value
// Array and set the subscript
const hadKey = isArray(target) && isIntegerKey(key)
? Number(key) < target.length
: hasOwn(target, key)
const result = Reflect.set(target, key, value, receiver)
// 2. Determine the condition to trigger
if(! hadKey) {/ / new
trigger(target, TiggerOrTypes.ADD, key, value)
} else if (hasChanged(oldValue, value)) {
// The new value is not equal to the old value
trigger(target, TriggerOrTypes.SET, key, value, oldValue)
}
return result
}
}
Copy the code
- add
trigger
content
// packages/reactivity/src/effect.ts
/ /...
export function trigger(target, type, key, newValue, oldValue) {
// 1. Check whether this attribute has been collected
const depsMap = targetMap.get(target)
if(! depsMap)return
// 2. All effects need to be stored in one set and executed together
const effects = new Set(a)// It still needs to be de-weighted
// 5. Add the final effect to be executed
const add = (effectsToAdd) = > {
if (effectsToAdd) {
effectsToAdd.forEach(effect= > {
effects.add(effect)
})
}
}
// Arrays are special and need to be handled separately
// 3. Check whether the length of the array is changed
if (isArray(target) && key === 'length') {
depsMap.forEach((dep, key)) {
if (key === 'length' || key > newValue) {
// whether the key in the mapping table is 'length'
// state.arr.length = 5 template > {{state.arr.length}}
Whether the subscript of the dependent data in 3.2 dependencies is greater than the new length of the array
// effect > state.arr[2] other > state.length = 1
// 4. Add all the qualified effects to the effects to be executed
add(dep)
}
}
} else {
// 6 Check whether the key is undefined
if(key ! = =void) {
// if it is not undefined, it must be modified
add(depsMap.get(key))
}
switch (type) {
case TriggerOrTypes.ADD:
if (isArray(target) && isIntegerKey(key)) {
add(depsMap.get('length'))}}}// 7. Finally trigger all effects at once
effects.forEach(effect= > effect())
}
Copy the code
In this way, we implement data responses of object and array types
1.5 ref and toRef
The first thing we need to know is that Reactive uses proxy internally, while ref’s final underlying implementation is defineProperty (www.babeljs.cn/)
Create the ref.ts file in the Packages /reactivity/ SRC directory
Implement ref and shallowRef
//packages/reactivity/src/ref.ts
// 1. ref
export function ref(value) {
// Value can be a normal type, so we need to convert a normal type to an object
// Value can also be an object. If it is an object, then we will end up using proxy
return createRef(value)
}
export function shallowRef(value) {
return createRef(value, true)}/ / 3.2
const convert = val= > isObject(val) ? reactive(val) : val
// 3. Proxy data instance
class Refimpl<T> {
private _value: T // The final value returned
public readonly __v_isRef = true
constructor(private _rawValue: T, public readonly _shallow = false){
// 3.1 If shallowRef is a common type or object type, only the layer is proxy. Otherwise, check whether _rawValue is a common type or object type. If _rawValue is object type, reactive uses proxy for proxy
this._value = _shallow ? _rawValue : convert(_rawValue)
}
// Finally get the data through.value
get value() {
// In order to execute effect in a responsive manner, we need to track the data
track(this, TrackOpTyoes.GET, 'value')
/ / the return value
return this._value
}
set value(newValue) {
// Check whether the new value is equal to the last value
if (hasChanged(newValue, this.rawValue)) {
// Update the last value to the new value and set the current value to the new value
this.rawValue = newValue
/ / the same 3.1
this._value = this._shallow ? newValue : convert(newValue)
/ / triggering effect
trigger(this, TriggerOrTypes.SET, 'value', newValue)
}
}
}
// 2. CreateRef returns an instance
function createRef(rawValue, shallow = false) {
return new Refimpl(rawValue, shallow)
}
Copy the code
ToRef and toRefs
// packages/reactivity/src/ref.ts
/ /...
/ / 2. The transformation
class ObjectRefImpl {
public readonly __v_isRef = true
constructor(private readonly _target, private readonly _key){}
get value() {
// Dependencies are collected if the original object is reactive
return this._target[this.key]
}
set value() {
// If the original object is reactive, the update is triggered
this._target[this.key] = newVal
}
}
// toRef 1. Convert the value of the object toRef
export function toRef(target, key) {
Target [key] = ref (target[key]);
return new ObjectRefImpl(target, key)
}
// 3. toRefs
export function toRefs(target) {
// Check whether it is an array or an object
const ret = isArray(target) ? new Array(target.length) : {}
for (let key in target) {
ret[key] = toRef(target, key)
}
// Return all attributes
return ret
}
Copy the code
Basically, the main API in Vue3 ReActivity is implemented, although there are a lot of details and functionality left unaddressed, but basically enough to understand, after all, to learn the framework rather than write one (although I don’t deserve it either).
Recently, I plan to read teacher Li Bing’s “Working Principle and Practice of Browser” again, set up a flag, and write it into notes, with a lot of content, I hope I can persist in taking notes.