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

  1. The installationtypescriptrollupDependent dependencies
  2. @rollup/plugin-node-resolve: Parsing third-party modules enables us to support third-party modules
  3. @rollup/plugin-json: Supports parsing JSON
  4. execa: Uses multiple processes
  5. --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:

  1. Remove the code that packages all modules, packaging only one module
  2. Change the rollup configuration from-cModified to-cwForm 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.3reactivityFile 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.

  1. ineffectAdd atrackMethod 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
  1. First of all, we havetryLt.effect, and get the currenteffectAnd then executefn
  2. Then, infinallyI’m going to push it off the stackactiveEffectPoint back to the previous oneeffect
  3. Then we add a prevent before we execute all thiseffectThe 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.

  1. Adding an identifier
// packages/reactivity/src/operators.ts
// ...

export const enum TriggerOrTypes {
  ADD = 'add',
  SET = 'set'
}
Copy the code
  1. addtriggermethods
// packages/reactivity/src/effect.ts

/ /...
export function trigger(target, type, key, newValue, oldValue) {}Copy the code
  1. calltrigger
// 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
  1. addtriggercontent
// 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.