Debugging Environment Setup
-
Git clone github.com/vuejs/vue-n…
-
Install dependency: yarn –ignore-scripts
-
Generate the sourcemap file, package.json
"dev": "node scripts/dev.js --sourcemap" Copy the code
-
Compile: yarn dev
Package execution process
graph TD
package.json --> dev.js --> rollup.config.js
dev.js
const execa = require('execa')
const { fuzzyMatchTarget } = require('./utils')
const args = require('minimist')(process.argv.slice(2))
// Use vue folder by default, target folder
const target = args._.length ? fuzzyMatchTarget(args._)[0] : 'vue'
// Set the format to support the way commonJS umd
const formats = args.formats || args.f
// Source mapping
const sourceMap = args.sourcemap || args.s
const commit = execa.sync('git'['rev-parse'.'HEAD']).stdout.slice(0.7)
execa(
'rollup'['-wc'.'--environment'[`COMMIT:${commit}`.`TARGET:${target}`.`FORMATS:${formats || 'global'}`,
sourceMap ? `SOURCE_MAP:true` : ` `
]
.filter(Boolean)
.join(', ')] and {stdio: 'inherit'})Copy the code
rollup.config.js
import path from 'path'
import ts from 'rollup-plugin-typescript2'
import replace from '@rollup/plugin-replace'
import json from '@rollup/plugin-json'
if(! process.env.TARGET) {throw new Error('TARGET package must be specified via --environment flag.')}const masterVersion = require('./package.json').version
/ / package
const packagesDir = path.resolve(__dirname, 'packages')
// Submodule directory, default vue
const packageDir = path.resolve(packagesDir, process.env.TARGET)
const name = path.basename(packageDir)
const resolve = p= > path.resolve(packageDir, p)
const pkg = require(resolve(`package.json`))
const packageOptions = pkg.buildOptions || {}
// ensure TS checks only once for each build
let hasTSChecked = false
const outputConfigs = {
'esm-bundler': {
file: resolve(`dist/${name}.esm-bundler.js`),
format: `es`
},
'esm-browser': {
file: resolve(`dist/${name}.esm-browser.js`),
format: `es`
},
cjs: { / / the node. Js
file: resolve(`dist/${name}.cjs.js`),
format: `cjs`
},
global: {// Immediate execution Mode Specifies the default execution mode
file: resolve(`dist/${name}.global.js`),
format: `iife`
},
// runtime-only builds, for main "vue" package only
'esm-bundler-runtime': {
file: resolve(`dist/${name}.runtime.esm-bundler.js`),
format: `es`
},
'esm-browser-runtime': { //
file: resolve(`dist/${name}.runtime.esm-browser.js`),
format: 'es'
},
'global-runtime': {
file: resolve(`dist/${name}.runtime.global.js`),
format: 'iife'}}const defaultFormats = ['esm-bundler'.'cjs']
const inlineFormats = process.env.FORMATS && process.env.FORMATS.split(', ')
const packageFormats = inlineFormats || packageOptions.formats || defaultFormats
const packageConfigs = process.env.PROD_ONLY
? []
: packageFormats.map(format= > createConfig(format, outputConfigs[format]))
if (process.env.NODE_ENV === 'production') {
packageFormats.forEach(format= > {
if (packageOptions.prod === false) {
return
}
if (format === 'cjs') {
packageConfigs.push(createProductionConfig(format))
}
if (/^(global|esm-browser)(-runtime)? /.test(format)) {
packageConfigs.push(createMinifiedConfig(format))
}
})
}
export default packageConfigs
function createConfig(format, output, plugins = []) {
if(! output) {console.log(require('chalk').yellow(`invalid format: "${format}"`))
process.exit(1)}// Generate the corresponding mapping configuration for the source codeoutput.sourcemap = !! process.env.SOURCE_MAP output.externalLiveBindings =false
const isProductionBuild =
process.env.__DEV__ === 'false' || /\.prod\.js$/.test(output.file)
const isBundlerESMBuild = /esm-bundler/.test(format)
const isBrowserESMBuild = /esm-browser/.test(format)
const isNodeBuild = format === 'cjs'
const isGlobalBuild = /global/.test(format)
if (isGlobalBuild) {
output.name = packageOptions.name
}
constshouldEmitDeclarations = process.env.TYPES ! =null && !hasTSChecked
// TS mapping support
const tsPlugin = ts({
check: process.env.NODE_ENV === 'production' && !hasTSChecked,
tsconfig: path.resolve(__dirname, 'tsconfig.json'),
cacheRoot: path.resolve(__dirname, 'node_modules/.rts2_cache'),
tsconfigOverride: {
compilerOptions: {
sourceMap: output.sourcemap,
declaration: shouldEmitDeclarations,
declarationMap: shouldEmitDeclarations
},
exclude: ['**/__tests__'.'test-dts']}})// we only need to check TS and generate declarations once for each build.
// it also seems to run into weird issues when checking multiple times
// during a single build.
hasTSChecked = true
// Import file
const entryFile = /runtime$/.test(format) ? `src/runtime.ts` : `src/index.ts`
const external =
isGlobalBuild || isBrowserESMBuild
? packageOptions.enableNonBrowserBranches
? // externalize postcss for @vue/compiler-sfc
// because @rollup/plugin-commonjs cannot bundle it properly
['postcss']
: // normal browser builds - non-browser only imports are tree-shaken,
// they are only listed here to suppress warnings.
['source-map'.'@babel/parser'.'estree-walker']
: // Node / esm-bundler builds. Externalize everything.
[
...Object.keys(pkg.dependencies || {}),
...Object.keys(pkg.peerDependencies || {}),
...['path'.'url'] // for @vue/compiler-sfc
]
// the browser builds of @vue/compiler-sfc requires postcss to be available
// as a global (e.g. http://wzrd.in/standalone/postcss)
output.globals = {
postcss: 'postcss'
}
constnodePlugins = packageOptions.enableNonBrowserBranches && format ! = ='cjs'
? [
require('@rollup/plugin-node-resolve').nodeResolve({
preferBuiltins: true
}),
require('@rollup/plugin-commonjs') ({sourceMap: false
}),
require('rollup-plugin-node-builtins') (the),require('rollup-plugin-node-globals') (the)] : []// Here is the main configuration information
return {
input: resolve(entryFile),
// Global and Browser ESM builds inlines everything so that they can be
// used alone.
external,
plugins: [
json({
namedExports: false
}),
tsPlugin,
createReplacePlugin(
isProductionBuild,
isBundlerESMBuild,
isBrowserESMBuild,
// isBrowserBuild?(isGlobalBuild || isBrowserESMBuild || isBundlerESMBuild) && ! packageOptions.enableNonBrowserBranches, isGlobalBuild, isNodeBuild ), ... nodePlugins, ... plugins ], output,onwarn: (msg, warn) = > {
if (!/Circular/.test(msg)) {
warn(msg)
}
},
treeshake: {
moduleSideEffects: false}}}function createProductionConfig(format) {
return createConfig(format, {
file: resolve(`dist/${name}.${format}.prod.js`),
format: outputConfigs[format].format
})
}
function createMinifiedConfig(format) {
const { terser } = require('rollup-plugin-terser')
return createConfig(
format,
{
file: outputConfigs[format].file.replace(/\.js$/.'.prod.js'),
format: outputConfigs[format].format
},
[
terser({
module: /^esm/.test(format),
compress: {
ecma: 2015.pure_getters: true}})])}Copy the code
Generate results:
- packages\vue\dist\vue.global.js
- packages\vue\dist\vue.global.js.map
Run the yarn serve
Source code analysis
Compiler and runtime environment.
The compiler
- Compiler-core Core compiler logic
- Compiler-dom compiles logic for the browser platform
- Compiler-sfc compiles logic for single-file components
- Compiler-ssr renders the compilation logic for the server
Runtime environment
- Run-time core Specifies the runtime core
- Run-time dom runtime logic for browsers
- Run-time test Completes the test environment simulation outside the browser
- Reactivity reactive logic
- Template-explorer Specifies the template browser
- Vue code entry, integrate compiler and runtime
- Server-renderer Server-side rendering
- Share public method
Test cases
<! DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, Initial - scale = 1.0 "> < title > Document < / title > < / head > < body > < div id =" app "> < h1 @ click =" onclick "> {{MSG}} < / h1 > <comp></comp> </div> <script src=".. /dist/vue.global.js"></script> <script> const {createApp} = vue // instance creation const app = createApp({data() {// even if the root object Return {MSG: 'hello, vue3'}}, methods: {onclick() {console.log('click me'); } }, }) app.component('comp', { template:'<div>comp</div>', }) app.mount('#app') </script> </body> </html>Copy the code
Composition API
The composition API was created to implement a function-based logic reuse mechanism.
Instances are instantiated via the factory function return.
advantage
- You can use tree-shking to remove useless code during packaging.
- Eliminate this and just build a simple context
- The same business code is written together, avoiding the problem of code scrolling up and down frequently on complex pages
<! DOCTYPEhtml>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="Width = device - width, initial - scale = 1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<script src=".. /dist/vue.global.js"></script>
</head>
<body>
<div id="app">
<h1>Composition API</h1>
<div>count: {{ state.count }}</div>
</div>
<script>
const {
createApp,
reactive,
onMounted
} = Vue;
// Declare the component
const App = {
// Setup is a new component option that is the entry point for using the Composition API within a component
// The call time is after the initialization attribute is determined and before the beforeCreate
setup() {
// Responsivity: receives an object and returns a responsive proxy object
const state = reactive({ count: 0 })
onMounted(() = > {
console.log('mounted');
})
// Repeat logic can be registered multiple times to achieve business split calls
onMounted(() = > {
console.log('mounted2');
})
// The return object will be merged with the render function context
return { state }
}
}
createApp(App).mount('#app')
</script>
</body>
</html>// Mounted //mounted2 Is displayedCopy the code
Commonly used API
// Calculate attributes
<div>doubleCount: {{doubleCount}}</div>
const { computed } = Vue;
const App = {
setup() {
const state = reactive({
count: 0.// Computed () returns an immutable responsive reference object
// It encapsulates the return value of the getter
doubleCount: computed(() = > state.count * 2)}}}// Event processing
<div @click="add">count: {{ state.count }}</div>
const App = {
setup() {
Declare an add function in setup
function add() {
state.count++
}
// Pass in the render function context
return { state, add }
}
}
// Listener: watch()
const { watch } = Vue;
const App = {
setup() {
// state.count Changes to cb will be performed
watch(() = > state.count, (val, oldval) = > {
console.log('the count has changed:+ val); }}})// Reference object: single-value responsivity, mainly to achieve a business code cohesion, avoid code scrolling up and down
<div>counter: {{ counter }}</div>
const { ref } = Vue;
const App = {
setup() {
// return a responsive Ref object
const counter = ref(1)
setTimeout(() = > {
// Change the value of the object
counter.value++
}, 1000);
/ / add a counter
return { state, add, counter }
}
}
Copy the code
The logical combination
//03-logic-composition.html
<! DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, "> <title>composition API </title> <script SRC =".. / dist/vue. Global. Js "> < / script > < / head > < body > < div > < h1 > logic combination < / h1 > < div id =" app "> < / div > < / div > < script > const { createApp, reactive, onMounted, onUnmounted, toRefs } = Vue; Function useMouse() {reactive({x: 0, y: reactive); 0 }) const update = e => { state.x = e.pageX state.y = e.pageY } onMounted(() => { window.addEventListener('mousemove', update) }) onUnmounted(() => { window.removeEventListener('mousemove', Function useTime() {const state = reactive({time: new Date() }) onMounted(() => { setInterval(() => { state.time = new Date() }, 1000)}) return toRefs(state)} const MyComp = {template: '<div>x: {{x}} y: {{y}}</div> <p>time: {{time}}</p> ', setup() {const {x, Y} = useMouse() const {time} = useTime() {return {x, y, time } } } createApp(MyComp).mount('#app') </script> </body> </html>Copy the code
Mixins,
- X,y, and time come from clear sources
- Does not conflict with names such as data and props
- Increased maintainability:
Vue3 response principle
Vue2 principle
// 1. Object responsivity: iterate over each key and define getters and setters
// 2. Array responsivity: Override the array prototype method with additional notification logic
const originalProto = Array.prototype
const arrayProto = Object.create(originalProto) ; ['push'.'pop'.'shift'.'unshift'.'splice'.'reverse'.'sort'].forEach(
method= > {
arrayProto[method] = function () {
originalProto[method].apply(this.arguments)
notifyUpdate()
}
}
)
function observe(obj) {
if (typeofobj ! = ='object' || obj == null) {
return
}
// Add array type judgment, if array overrides its prototype
if (Array.isArray(obj)) {
Object.setPrototypeOf(obj, arrayProto)
} else {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
defineReactive(obj, key, obj[key])
}
}
}
function defineReactive(obj, key, val) {
observe(val) // Resolve nested objects
Object.defineProperty(obj, key, {
get() {
return val
},
set(newVal) {
if(newVal ! == val) { observe(newVal)// The new value is the case of the object
val = newVal
notifyUpdate()
}
}
})
}
function notifyUpdate() {
console.log('Page updated! ')}Copy the code
Disadvantages of vuE2 response type:
- The reactivity process requires recursive traversal and consumes a lot of energy
- New or deleted attributes cannot be listened on
- Array responsivity requires additional implementation
- Map, Set, and Class cannot respond
- There are limitations to changing the syntax
Vue3 response principle analysis
Vue3 uses ES6 Proxy features to address these issues.
advantage
The proxy is added at runtime without having to walk through the object in advance to build binding related methods and maintain relationships
Proxy is used to modify the default behavior of certain operations, which is equivalent to making changes at the language level. Therefore, it is a kind of "meta programming", that is, programming a programming language.Copy the code
//reactivity.js
function reactive(obj) {
if (typeofobj ! = ='object'&& obj ! =null) {
return obj
}
// Proxy adds interception to an object
const observed = new Proxy(obj, {
get(target, key, receiver) {
// Reflect is used to perform the default action on objects, which is more formal and friendlier
// Both Proxy and Object methods Reflect
// http://es6.ruanyifeng.com/#docs/reflect
const res = Reflect.get(target, key, receiver)
console.log(` access${key}:${res}`)
return res
},
set(target, key, value, receiver) {
const res = Reflect.set(target, key, value, receiver)
console.log(` set${key}:${value}`)
return res
},
deleteProperty(target, key) {
const res = Reflect.deleteProperty(target, key)
console.log(` delete${key}:${res}`)
return res
}
})
return observed
}
// Test the code
const state = reactive({
foo: 'foo'
})
/ / 1. Access
state.foo // ok
// 2. Set existing properties
state.foo = 'fooooooo' // ok
// 3. Set nonexistent properties
state.dong = 'dong' // ok
// 4. Delete attributes
delete state.dong // ok
Copy the code
Nested object responsive
// Test: nested objects cannot respond
const state = reactive({
bar: { a: 1}})// Set nested object properties
state.bar.a = 10 // no ok
Copy the code
Add object type recursion
// Extract the help method
const isObject = val= >val ! = =null && typeof val === 'object'
function reactive(obj) {
// Determine whether the object is an object
if(! isObject(obj)) {return obj
}
const observed = new Proxy(obj, {
get(target, key, receiver) {
// ...
// If it is an object, you need to recurse
return isObject(res) ? reactive(res) : res
},
/ /...
}
// Avoid duplicate proxies
reactive(data) // The proxied pure object
reactive(react) // Proxy object
// Cache the previous proxy result and use it directly when getting
const toProxy = new WeakMap(a)/ / form such as obj: observed
const toRaw = new WeakMap(a)/ / form such as observed: obj
function reactive(obj) {
/ /...
// Find the cache to avoid duplicate proxies
if (toProxy.has(obj)) {
return toProxy.get(obj)
}
if (toRaw.has(obj)) {
return obj
}
const observed = new Proxy(...).// Cache proxy results
toProxy.set(obj, observed)
toRaw.set(observed, obj)
return observed
}
// Test the effect
console.log(reactive(data) === state)
console.log(reactive(state) === state)
Copy the code
Depend on the collection
Establish the correspondence between the response data key and the update function.
// Use to set the response function
effect(() = > console.log(state.foo))
// When the user modifies the associated data, the response function is triggered
state.foo = 'xxx'
Copy the code
Implement three functions:
- Effect: Saves the callback function for later use, and immediately executes the callback function to trigger the getter for some of the response data in it
- Track: Call track in the getter to map the previously stored callback function to the current target and key
- Trigger: Trigger is called in the setter, and the target and key response functions are executed
Target, key, and response function mappings
- The general structure is shown below
- WeakMap Map Set
- {target: {key: [effect1,…] }}
implementation
Set the response function and create the effect function, where only the variables used in the effect function are treated in a reactive manner.
The complete code
// Save the current active response function as a bridge between the getter and effect
const effectStack = []
// effect task: execute fn and push it onto the stack
function effect(fn) {
const rxEffect = function () {
// 1. Catch possible exceptions
try {
// 2. Push for subsequent dependency collection
effectStack.push(rxEffect)
// 3. Run fn to trigger dependency collection
return fn()
} finally {
// 4. The execution is complete and the stack is removed
effectStack.pop()
}
}
// The response function is executed once by default
rxEffect()
// Return the response function
return rxEffect
}
// Rely on collection and triggering
function reactive(obj) {
if(! isObject(obj)) {return obj
}
const observed = new Proxy(obj, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
// Rely on collection
track(target, key)
return isObject(res) ? reactive(res) : res
},
set(target, key, value, receiver) {
const res = Reflect.set(target, key, value, receiver)
trigger(target, key)
return res
},
deleteProperty(target, key) {
const res = Reflect.deleteProperty(target, key)
trigger(target, key)
return res
}
})
return observed
}
// Create a mapping table.
// {target: {key: [fn1,fn2]}}
let targetMap = new WeakMap(a)function track(target, key) {
// Remove the response function from the stack
const effect = effectStack[effectStack.length - 1]
if (effect) {
// Get the dependency table for target
let depsMap = targetMap.get(target)
if(! depsMap) { depsMap =new Map()
targetMap.set(target, depsMap)
}
// Get the set of response functions corresponding to key
let deps = depsMap.get(key)
if(! deps) { deps =new Set()
depsMap.set(key, deps)
}
// Add the response function to the corresponding collection
if(! deps.has(effect)) { deps.add(effect) } } }// Trigger the target.key response function
function trigger(target, key) {
// Get the dependency table
const depsMap = targetMap.get(target)
if (depsMap) {
// Get the set of response functions
const deps = depsMap.get(key)
if (deps) {
// Execute all response functions
deps.forEach(effect= > {
effect()
})
}
}
}
// Test the code
const state = reactive({
foo: 'foo'.bar: { a: 1 }
})
effect(() = > {// This method can be understood as vue performs dispatch comparison virtual DOM refresh page
console.log('effect:', state.foo)
})
// state.foo
// state.foo = 'fooooooo'
// state.dong = 'dong'
// delete state.dong
// Run time proxy
// state.bar.a = 10
Copy the code
The test code
<! DOCTYPEhtml>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="Width = device - width, initial - scale = 1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script src="./reactive.js"></script>
<script>
const data = reactive({
msg: 'xxx'
})
effect(() = > {// This method can be understood as vue performs dispatch comparison virtual DOM refresh page
app.innerText = data.msg
})
setTimeout(() = > {
data.msg = 'yyyy'
}, 1000);
</script>
</body>
</html>
Copy the code