一、Vue3结构分析

1、Vue2与Vue3的对比

  • 对TypeScript支持不友好(所有属性都放在了this对象上,难以推倒组件的数据类型)
  • 大量的API挂载在Vue对象的原型上,难以实现TreeShaking。
  • 架构层面对跨平台dom渲染开发支持不友好,vue3允许自定义渲染器,扩展能力强。
  • CompositionAPI。受ReactHook启发
  • 对虚拟DOM进行了重写、对模板的编译进行了优化操作...

2、Vue3设计思想

  • Vue3.0更注重模块上的拆分,在2.0中无法单独使用部分模块。需要引入完整的Vuejs(例如只想使用使用响应式部分,但是需要引入完整的Vuejs), Vue3中的模块之间耦合度低,模块可以独立使用。拆分模块
  • Vue2中很多方法挂载到了实例中导致没有使用也会被打包(还有很多组件也是一样)。通过构建工具Tree-shaking机制实现按需引入,减少用户打包后体积。重写API
  • Vue3允许自定义渲染器,扩展能力强。不会发生以前的事情,改写Vue源码改造渲染方式。扩展更方便

依然保留了Vue2的特点:

依旧是声明式框架,底层渲染逻辑不关心(命令式比较关注过程,可以控制怎么写最优?编写过程不同),如for和reduce

采用虚拟DOM

区分编译时和运行时

内部区分了编译时(模板?编程成js代码,一般在构建工具中使用)和运行时

简单来说,Vue3 框架更小,扩展更加方便

3、monorepo管理项目

Monorepo 是管理项目代码的一个方式,指在一个项目仓库(repo)中管理多个模块/包(package)。也就是说是一种将多个package放在一个repo中的代码管理模式。Vue3内部实现了一个模块的拆分, Vue3源码采用 Monorepo 方式进行管理,将模块拆分到package目录中。

  • 一个仓库可维护多个模块,不用到处找仓库
  • 方便版本管理和依赖管理,模块之间的引用,调用都非常方便
  • 每个包可以独立发布

早期使用yarn workspace + lerna来管理项目,后面是pnpm

pnpm介绍

快速,节省磁盘空间的包管理器,主要采用符号链接的方式管理模块

  1. 快速

  2. 高效利用磁盘空间

pnpm 内部使用基于内容寻址的文件系统来存储磁盘上所有的文件,这个文件系统出色的地方在于:

  • 不会重复安装同一个包。用 npm/yarn 的时候,如果 100 个项目都依赖 lodash,那么 lodash 很可能就被安装了 100 次,磁盘中就有 100 个地方写入了这部分代码。但在使用 pnpm 只会安装一次,磁盘中只有一个地方写入,后面再次使用都会直接使用hardlink(硬链接)
  • 即使一个包的不同版本,pnpm 也会极大程度地复用之前版本的代码。比如 lodash 有 100 个文件,更新版本之后多了一个文件,那么磁盘当中并不会重新写入 101 个文件,而是保留原来的 100 个文件的hardlink,仅仅写入那一个新增的文件
  1. 支持Monorepo

pnpm 与 npm/yarn 一个很大的不同就是支持了 monorepo

  1. 安全性高

之前在使用 npm/yarn 的时候,由于 node_module 的扁平结构,如果 A 依赖 B, B 依赖 C,那么 A 当中是可以直接使用 C 的,但问题是 A 当中并没有声明 C 这个依赖。因此会出现这种非法访问的情况。但 pnpm自创了一套依赖管理方式,很好地解决了这个问题,保证了安全性

默认情况下,pnpm 则是通过使用符号链接的方式仅将项目的直接依赖项添加到node_modules的根目录下。

安装和初始化

  • 全局安装(node版本>16)
npm install pnpm -g

  • 初始化
pnpm init

配置workspace

根目录创建pnpm-workspace.yaml

packages:
- 'packages/*'

将packages下所有的目录都作为包进行管理。这样我们的Monorepo就搭建好了。确实比 lerna + yarn workspace 更快捷

4、项目结构

packages

  • reactivity:响应式系统
  • runtime-core:与平台无关的运行时核心 (可以创建针对特定平台的运行时 - 自定义渲染器)
  • runtime-dom: 针对浏览器的运行时。包括DOM API,属性,事件处理等
  • runtime-test:用于测试
  • server-renderer:用于服务器端渲染
  • compiler-core:与平台无关的编译器核心
  • compiler-dom: 针对浏览器的编译模块
  • compiler-ssr: 针对服务端渲染的编译模块
  • template-explorer:用于调试编译器输出的开发工具
  • shared:多个包之间共享的内容
  • vue:完整版本,包括运行时和编译器
                                    +---------------------+
| |
| @vue/compiler-sfc |
| |
+-----+--------+------+
| |
v v
+---------------------+ +----------------------+
| | | |
+------------>| @vue/compiler-dom +--->| @vue/compiler-core |
| | | | |
+----+----+ +---------------------+ +----------------------+
| |
| vue |
| |
+----+----+ +---------------------+ +----------------------+ +-------------------+
| | | | | | |
+------------>| @vue/runtime-dom +--->| @vue/runtime-core +--->| @vue/reactivity |
| | | | | |
+---------------------+ +----------------------+ +-------------------+

scripts

Vue3在开发环境使用esbuild打包,生产环境采用rollup打包

包的相互依赖

安装

把packages/shared安装到packages/reactivity

pnpm install @vue/shared@workspace --filter @vue/reactivity

使用

在reactivity/src/computed.ts中引入shared中相关方法

import { isFunction, NOOP } from '@vue/shared' // ts引入会报错

const onlyGetter = isFunction(getterOrOptions)
if (onlyGetter) {
...
} else {
...
}
...

tips:@vue/shared引入会报错,需要在tsconfig.json中配置

{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@vue/compat": ["packages/vue-compat/src"],
"@vue/*": ["packages/*/src"],
"vue": ["packages/vue/src"]
}
},
}

5、打包

所有包的入口均为src/index.ts这样可以实现统一打包.

• reactivity/package.json

{
"name": "@vue/reactivity",
"version": "3.2.45",
"main": "index.js",
"module":"dist/reactivity.esm-bundler.js",
"unpkg": "dist/reactivity.global.js",
"buildOptions": {
"name": "VueReactivity",
"formats": [
"esm-bundler",
"cjs",
"global"
]
}
}

• shared/package.json

{
"name": "@vue/shared",
"version": "3.2.45",
"main": "index.js",
"module": "dist/shared.esm-bundler.js",
"buildOptions": {
"formats": [
"esm-bundler",
"cjs"
]
}
}

formats 为自定义的打包格式,有 esm-bundler 在构建工具中使用的格式、 esm-browser 在浏览器中使用的格式、 cjs 在node中使用的格式、 global 立即执行函数的格式

开发环境esbuild打包

开发时 执行脚本, 参数为要打包的模块

"scripts": {
"dev": "node scripts/dev.js reactivity -f global"
}
// Using esbuild for faster dev builds.
// We are still using Rollup for production builds because it generates
// smaller files w/ better tree-shaking. // @ts-check
const { build } = require('esbuild')
const nodePolyfills = require('@esbuild-plugins/node-modules-polyfill')
const { resolve, relative } = require('path')
const args = require('minimist')(process.argv.slice(2)) const target = args._[0] || 'vue'
const format = args.f || 'global'
const inlineDeps = args.i || args.inline
const pkg = require(resolve(__dirname, `../packages/${target}/package.json`)) // resolve output
const outputFormat = format.startsWith('global')
? 'iife'
: format === 'cjs'
? 'cjs'
: 'esm' const postfix = format.endsWith('-runtime')
? `runtime.${format.replace(/-runtime$/, '')}`
: format const outfile = resolve(
__dirname,
`../packages/${target}/dist/${
target === 'vue-compat' ? `vue` : target
}.${postfix}.js`
)
const relativeOutfile = relative(process.cwd(), outfile) // resolve externals
// TODO this logic is largely duplicated from rollup.config.js
let external = []
if (!inlineDeps) {
// cjs & esm-bundler: external all deps
if (format === 'cjs' || format.includes('esm-bundler')) {
external = [
...external,
...Object.keys(pkg.dependencies || {}),
...Object.keys(pkg.peerDependencies || {}),
// for @vue/compiler-sfc / server-renderer
'path',
'url',
'stream'
]
} if (target === 'compiler-sfc') {
const consolidateDeps = require.resolve('@vue/consolidate/package.json', {
paths: [resolve(__dirname, `../packages/${target}/`)]
})
external = [
...external,
...Object.keys(require(consolidateDeps).devDependencies),
'fs',
'vm',
'crypto',
'react-dom/server',
'teacup/lib/express',
'arc-templates/dist/es5',
'then-pug',
'then-jade'
]
}
} build({
entryPoints: [resolve(__dirname, `../packages/${target}/src/index.ts`)],
outfile,
bundle: true,
external,
sourcemap: true,
format: outputFormat,
globalName: pkg.buildOptions?.name,
platform: format === 'cjs' ? 'node' : 'browser',
plugins:
format === 'cjs' || pkg.buildOptions?.enableNonBrowserBranches
? [nodePolyfills.default()]
: undefined,
define: {
__COMMIT__: `"dev"`,
__VERSION__: `"${pkg.version}"`,
__DEV__: `true`,
__TEST__: `false`,
__BROWSER__: String(
format !== 'cjs' && !pkg.buildOptions?.enableNonBrowserBranches
),
__GLOBAL__: String(format === 'global'),
__ESM_BUNDLER__: String(format.includes('esm-bundler')),
__ESM_BROWSER__: String(format.includes('esm-browser')),
__NODE_JS__: String(format === 'cjs'),
__SSR__: String(format === 'cjs' || format.includes('esm-bundler')),
__COMPAT__: String(target === 'vue-compat'),
__FEATURE_SUSPENSE__: `true`,
__FEATURE_OPTIONS_API__: `true`,
__FEATURE_PROD_DEVTOOLS__: `false`
},
watch: {
onRebuild(error) {
if (!error) console.log(`rebuilt: ${relativeOutfile}`)
}
}
}).then(() => {
console.log(`watching: ${relativeOutfile}`)
})

生产环境rollup打包

具体代码参考rollup.config.mjs

build.js

二、Vue3中Reactivity模块

1、vue3对比vue2的响应式变化

  • 在Vue2的时候使用defineProperty来进行数据的劫持, 需要对属性进行重写添加gettersetter性能差
  • 当新增属性和删除属性时无法监控变化。需要通过$set$delete实现
  • 数组不采用defineProperty来进行劫持 (浪费性能,对所有索引进行劫持会造成性能浪费)需要对数组单独进行处理

Vue3中使用Proxy来实现响应式数据变化。从而解决了上述问题

2、CompositionAPI

  • 在Vue2中采用的是OptionsAPI, 用户提供的data,props,methods,computed,watch等属性 (用户编写复杂业务逻辑会出现反复横跳问题)
  • Vue2中所有的属性都是通过this访问,this存在指向明确问题
  • Vue2中很多未使用方法或属性依旧会被打包,并且所有全局API都在Vue对象上公开。Composition API对 tree-shaking 更加友好,代码也更容易压缩。
  • 组件逻辑共享问题, Vue2 采用mixins 实现组件之间的逻辑共享; 但是会有数据来源不明确,命名冲突等问题。 Vue3采用CompositionAPI 提取公共逻辑非常方便

简单的组件仍然可以采用OptionsAPI进行编写,compositionAPI在复杂的逻辑中有着明显的优势~。 reactivity 模块中就包含了很多我们经常使用到的 API 例如:computed、reactive、ref、effect等

3、基本使用

const { effect, reactive } = VueReactivity
// console.log(effect, reactive);
const state = reactive({name: 'qpp', age:18, address: {city: '南京'}})
console.log(state.address);
effect(()=>{
console.log(state.name)
})

4、reactive实现

import { mutableHandlers } from'./baseHandlers';
// 代理相关逻辑import{ isObject }from'./util';// 工具方法
export function reactive(target: object) {
// if trying to observe a readonly proxy, return the readonly version.
if (isReadonly(target)) {
return target
}
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers,
reactiveMap
)
}
function createReactiveObject(target, baseHandler){
if(!isObject(target)){
return target;
}
...
const observed =new Proxy(target, baseHandler);
return observed
}

baseHandlers

import { isObject, hasOwn, hasChanged } from"@vue/shared";
import { reactive } from"./reactive";
const get = createGetter();
const set = createSetter();
function createGetter(){
return function get(target, key, receiver){
// 对获取的值进行放射
const res = Reflect.get(target, key, receiver);
console.log('属性获取',key)
if(isObject(res)){// 如果获取的值是对象类型,则返回当前对象的代理对象
return reactive(res);
}
return res;
}
}
function createSetter(){
return function set(target, key, value, receiver){
const oldValue = target[key];
const hadKey =hasOwn(target, key);
const result = Reflect.set(target, key, value, receiver);
if(!hadKey){
console.log('属性新增',key,value)
}else if(hasChanged(value, oldValue)){
console.log('属性值被修改',key,value)
}
return result;
}
}
export const mutableHandlers ={
get,// 当获取属性时调用此方法
set// 当修改属性时调用此方法
}

这里我只选了对最常用到的get和set方法的代码,还应该有 hasdeletePropertyownKeys 。这里为了快速掌握核心流程就先暂且跳过这些代码

5、effect实现

我们再来看effect的代码,默认effect会立即执行,当依赖的值发生变化时effect会重新执行

export let activeEffect = undefined;
// 依赖收集的原理是 借助js是单线程的特点, 默认调用effect的时候会去调用proxy的get,此时让属性记住
// 依赖的effect,同理也让effect记住对应的属性
// 靠的是数据结构 weakMap : {map:{key:new Set()}}
// 稍后数据变化的时候 找到对应的map 通过属性出发set中effect
function cleanEffect(effect) {
// 需要清理effect中存入属性中的set中的effect
// 每次执行前都需要将effect只对应属性的set集合都清理掉
// 属性中的set 依然存放effect
let deps = effect.deps
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effect)
}
effect.deps.length = 0; } // 创建effect时可以传递参数,computed也是基于effect来实现的,只是增加了一些参数条件而已
export function effect<T = any>(
fn: () => T,
options?: ReactiveEffectOptions
){
// 将用户传递的函数编程响应式的effect
const _effect = new ReactiveEffect(fn,options.scheduler);
// 更改runner中的this
_effect.run()
const runner = _effect.run.bind(_effect);
runner.effect = _effect; // 暴露effect的实例
return runner// 用户可以手动调用runner重新执行
}
export class ReactiveEffect {
public active = true;
public parent = null;
public deps = []; // effect中用了哪些属性,后续清理的时候要使用
constructor(public fn,public scheduler?) { } // 你传递的fn我会帮你放到this上
// effectScope 可以来实现让所有的effect停止
run() {
// 依赖收集 让熟悉和effect 产生关联
if (!this.active) {
return this.fn();
} else {
try {
this.parent = activeEffect
activeEffect = this;
cleanEffect(this); // vue2 和 vue3中都是要清理的
return this.fn(); // 去proxy对象上取值, 取之的时候 我要让这个熟悉 和当前的effect函数关联起来,稍后数据变化了 ,可以重新执行effect函数
} finally {
// 取消当前正在运行的effect
activeEffect = this.parent;
this.parent = null;
}
}
}
stop() {
if (this.active) {
this.active = false;
cleanEffect(this);
}
}
}

在effect方法调用时会对属性进行取值,此时可以进行依赖收集。

effect(()=>{
console.log(state.name)
// 执行用户传入的fn函数,会取到state.name,state.age... 会触发reactive中的getter
app.innerHTML = 'name:' + state.name + 'age:' + state.age + 'address' + state.address.city })

6、依赖收集

核心代码

// 收集属性对应的effect
export function track(target, type, key){}// 触发属性对应effect执行
export function trigger(target, type, key){}
function createGetter(){
return function get(target, key, receiver){
const res = Reflect.get(target, key, receiver);
// 取值时依赖收集
track(target, TrackOpTypes.GET, key);
if(isObject(res)){
return reactive(res);
}
return res;
}
}
function createSetter(){
return function set(target, key, value, receiver){
const oldValue = target[key];
const hadKey =hasOwn(target, key);
const result = Reflect.set(target, key, value, receiver);
if(!hadKey){
// 设置值时触发更新 - ADD
trigger(target, TriggerOpTypes.ADD, key);
}else if(hasChanged(value, oldValue)){
// 设置值时触发更新 - SET
trigger(target, TriggerOpTypes.SET, key, value, oldValue);
}
return result;
}
}

track的实现

const targetMap = new WeakMap();
export function track(target: object, type: TrackOpTypes, key: unknown){
if (shouldTrack && activeEffect) { // 上下文 shouldTrack = true
let depsMap = targetMap.get(target);
if(!depsMap){// 如果没有map,增加map
targetMap.set(target,(depsMap =newMap()));
}
let dep = depsMap.get(key);// 取对应属性的依赖表
if(!dep){// 如果没有则构建set
depsMap.set(key,(dep =newSet()));
} trackEffects(dep, eventInfo)
}
} export function trackEffects(
dep: Dep,
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
//let shouldTrack = false
//if (effectTrackDepth <= maxMarkerBits) {
// if (!newTracked(dep)) {
// dep.n |= trackOpBit // set newly tracked
// shouldTrack = !wasTracked(dep)
//}
//} else {
// Full cleanup mode.
// shouldTrack = !dep.has(activeEffect!)
} if (!dep.has(activeEffect!) {
dep.add(activeEffect!)
activeEffect!.deps.push(dep)
//if (__DEV__ && activeEffect!.onTrack) {
// activeEffect!.onTrack({
// effect: activeEffect!,
// ...debuggerEventExtraInfo!
// })
// }
}
}

trigger实现

export function trigger(target, type, key){
const depsMap = targetMap.get(target);
if(!depsMap){
return;
}
const run=(effects)=>{
if(effects){ effects.forEach(effect=>effect()); }
}
// 有key 就找到对应的key的依赖执行
if(key !==void0){
run(depsMap.get(key));
}
// 数组新增属性
if(type == TriggerOpTypes.ADD){
run(depsMap.get(isArray(target)?'length':'');
}}

依赖关系

作者:京东物流 乔盼盼

来源:京东云开发者社区 自猿其说Tech 转载请注明来源

Vue3设计思想及响应式源码剖析的更多相关文章

  1. STL源码剖析之序列式容器

    最近由于找工作需要,准备深入学习一下STL源码,我看的是侯捷所著的<STL源码剖析>.之所以看这本书主要是由于我过去曾经接触过一些台湾人,我一直觉得台湾人非常不错(这里不涉及任何政治,仅限 ...

  2. ASP.NET MVC通用权限管理系统(响应布局)源码更新介绍

    一.asp.net mvc 通用权限管理系统(响应布局)源码主要以下特点: AngelRM(Asp.net MVC)是基于asp.net(C#)MVC+前端bootstrap+ztree+lodash ...

  3. Django对中间件的调用思想、csrf中间件详细介绍、Django settings源码剖析、Django的Auth模块

    目录 使用Django对中间件的调用思想完成自己的功能 功能要求 importlib模块介绍 功能的实现 csrf中间件详细介绍 跨站请求伪造 Django csrf中间件 form表单 ajax c ...

  4. 豌豆夹Redis解决方案Codis源码剖析:Dashboard

    豌豆夹Redis解决方案Codis源码剖析:Dashboard 1.不只是Dashboard 虽然名字叫Dashboard,但它在Codis中的作用却不可小觑.它不仅仅是Dashboard管理页面,更 ...

  5. SpringMVC源码剖析(四)- DispatcherServlet请求转发的实现

    SpringMVC完成初始化流程之后,就进入Servlet标准生命周期的第二个阶段,即“service”阶段.在“service”阶段中,每一次Http请求到来,容器都会启动一个请求线程,通过serv ...

  6. SpringMVC源码剖析(二)- DispatcherServlet的前世今生

    上一篇文章<SpringMVC源码剖析(一)- 从抽象和接口说起>中,我介绍了一次典型的SpringMVC请求处理过程中,相继粉墨登场的各种核心类和接口.我刻意忽略了源码中的处理细节,只列 ...

  7. Flask核心机制--上下文源码剖析

    一.前言 了解过flask的python开发者想必都知道flask中核心机制莫过于上下文管理,当然学习flask如果不了解其中的处理流程,可能在很多问题上不能得到解决,当然我在写本篇文章之前也看到了很 ...

  8. petite-vue源码剖析-逐行解读@vue/reactivity之reactive

    在petite-vue中我们通过reactive构建上下文对象,并将根据状态渲染UI的逻辑作为入参传递给effect,然后神奇的事情发生了,当状态发生变化时将自动触发UI重新渲染.那么到底这是怎么做到 ...

  9. jQuery之Deferred源码剖析

    一.前言 大约在夏季,我们谈过ES6的Promise(详见here),其实在ES6前jQuery早就有了Promise,也就是我们所知道的Deferred对象,宗旨当然也和ES6的Promise一样, ...

  10. Apache Spark源码剖析

    Apache Spark源码剖析(全面系统介绍Spark源码,提供分析源码的实用技巧和合理的阅读顺序,充分了解Spark的设计思想和运行机理) 许鹏 著   ISBN 978-7-121-25420- ...

随机推荐

  1. 解读 --- System.Windows.Forms.Timer是前台线程吗?

    引言 今天同事问了我一个问题,System.Windows.Forms.Timer是前台线程还是后台线程,我当时想的是它是跟着UI线程一起结束的,应该是前台线程吧? 我确实没有仔细研究过他们的异同,所 ...

  2. 监控keepalived_vip控制容器的状态

    需求:监控server服务器的vip状态,如果vip存在,则判断容器是否启动,如果没有启动,则启动容器.如果vip不存在则关闭容器. 方法一: 查看代码 #!/bin/bash ip add | gr ...

  3. FastJson不成想还有个版本2啊:序列化大字符串报错

    背景 发现陷入了一个怪圈,写文章的话,感觉只有大bug或比较值得写的内容才会写,每次一写就是几千字,争取写得透彻一些,但这样,我也挺费时间,读者也未必有这么多时间看. 我想着,日常遇到的小bug.平时 ...

  4. Postgresql 批量插入命令COPY使用

    在很多场景下,我们经常会遇到将某个Excel或Csv文件中的数据,插入到Postgresql.对于这个需求,我们常规的处理办法就是将文件中的数据,按照文件表头名称转换成集合对象然后插入到数据库,当然这 ...

  5. TIDB - 分布式数据库

    TIDB(一) 重点 TIDB核心 数据存储-RocksDB Raft 协议 选举 数据同步 MVCC 表数据与kv映射关系 索引数据与kv 映射关系 元数据和sql 层计算 PD调度 HTAP 特性 ...

  6. GaoNeng:我是如何为OpenTiny贡献新组件的?

    本文共10076字,预计阅读20分钟 大家好啊,又是我GaoNeng.最近在给OpenTiny做贡献,感觉renderless这个架构还是挺有意思的,就贡献了一个color-picker组件,简单写篇 ...

  7. Codeforces-1095E-Almost-Regular-Bracket-Sequence

    题意 给定一个长度为 \(n\) 的小括号序列,求有多少个位置满足将这个位置的括号方向反过来后使得新序列是一个合法的括号序列.即在任意一个位置前缀左括号的个数不少于前缀右括号的个数,同时整个序列左右括 ...

  8. mysql关键字匹配度模糊查询

    有时候项目中需要进行模糊查询,但是要对查询出来的结果进行匹配度排序,比如搜索关键字是"北",需要把数据库中第一个字是北的排在前面,此时就可以用mysql的INSTR函数. INST ...

  9. 如何平息WPS for linux启动后,笔记本风扇的怒吼

    create:2022-09-06 20:02:45 WPS启动后,点击菜单栏右上角[未同步]按钮,不登录,直接关掉.几秒后,笔记本风扇嗷嗷叫.桌面conky显示wpscloudsvr进程CPU占用8 ...

  10. 58同城二手车数据爬虫——数字加密解码(Python原创)

    一.基础首页爬取 def crawler(): # 设置cookie cookie = '''cisession=19dfd70a27ec0e t_f805f7762a9a237a0deac37015 ...