19年的时候,Vue3就以山呼海啸之势向前端圈涌来,在2020.9.19号已经发布了正式版。Vue3相较与Vue2还是有很大改变的,带来了 Composition API RFC 版本,类似 React Hook 一样的写 Vue,可以自定义自己的 hook ,让使用者更加的灵活。本文会从一下几个角度讲讲(文章last version:2020-11-23):
-
一款框架的出生要经历哪些过程
-
Vue3中有那些新特性
-
Vue3会给我们的开发带来些什么(与Vue2对比)
-
Vue3源码文件结构
-
Vue3项目搭建的方式与Vue2的对比
-
本文重点:Vue3响应式原理reactivity模块是如何使用以及如何实现
这几个方面来讲解Vue3(需要有Vue2源码的相关知识基础,不然有些地方还是比较难理解的)
文章相关的demo及实现响应式原理代码地址
实现响应式原理代码地址:github.com/AlexGoing/v…
Vue3实战 demo:github.com/AlexGoing/v…
一款框架诞生需要的阶段
-
开发
-
alpha版:内部测试版 α
-
beta版:公开测试版 β
-
rc版:Release Candidate(候选版本)
-
stable版:稳定版
vue现阶段生态版本
Project
Status
vue-next
正式版v3.0.3 [GitHub地址]
vue-class-component
v8.0.0-rc1 [GitHub地址]
Vuex
rc2 [GitHub地址]
Vue Router
rc5 [GitHub地址]
Vue3新特性
这里讲解vue3的一些新特性已经是“老生长谈”的一些东西了,但是在说“废话”的同时,我对Vue3的一写新特性也有一写自己的理解,如果有不对,欢迎指正
- Performance
-
重写了虚拟Dom的实现(跳过静态节点,只处理动态节点,Vue2中vdom是不管动态静态节点都会全部diff)
-
优化编译
-
更高效的组件初始化
-
1.3~2 倍的更新性能
-
2~3 倍的 SSR 速度
- Tree shaking
- 按需求引用的内置的指令和方法,可以将无用模块“剪辑”,仅打包需要的模块
- Fragment
-
与Vue2.x不同的是,Vue3.x不再限于模板中的单个根节点,之前组件的节点必须只有一个根元素,Vue3可以有多个根元素,与React的<React.Fragment/>同理
-
早先版本称为,译作传送门 ,用于在当前组件之外呈现某些内容
-
可在嵌套层级中等待嵌套的异步依赖项 ,在异步组件未加载完成的时候,加载里的备用内容
-
Vue 3 的 Template 会支持多个根标签
- TypeScript
- vue3.0 对 TS 的支持度更高了,同时也支持 TSX 的使用;API 在 JS 与 TS 中的使用相同;类组件仍然可用,但是需要我们引入 vue-class-component@next,该模块目前在beta阶段。
- Custom Renderer API
-
自定义渲染器API
-
用户可以尝试WebGL自定义渲染器 (没用过,但是我个人觉得未来将会是个亮眼的功能)
- Composition API
-
Composition API 主要是提高了代码逻辑的可复用性,并且将 Reactivity 模块独立出来,这也使得 vue 3 变得更加灵活地与其他框架组合使用
-
组合式API,替换原有的 Options API
-
根据逻辑相关性组织代码,提高可读性和可维护性
-
更好的重用逻辑代码(避免mixins混入时命名冲突的问题)
-
但是依然可以延用 Options API
尤雨溪2020.4.16号关于Vue3进展的PPT链接(需要翻墙):PPT地址
Vue3会给我们的开发带来些什么(与Vue2对比):
写法上的不同
- Vue 3 的 Template 支持多个根标签,Vue 2 不支持
- 在Vue 3 的setup中用context代替this
- vue3中 v-model可以使用到任意组件,默认通过modelValue属性和自定义事件实现,不依赖于表单元素事件,且通过传递参数同一组件可以使用多个v-model,不需要固定的属性名和事件名了,手动去处理
- vue3由于fragments特性,vue3可以将v-for绑定在template标签上
- v-bind合并策略改变,同一属性的绑定后者会覆盖前者
- vue3中通过refs获取子组件或dom元素
- vue3监听数组改变 必须启动deep:true,否则,只能监控到地址的改变
- emits选项指定的事件由attrs属性上,交开发者控制
- 现在listeners的功能)
- teleport(新增)将内容插入到目标元素中
一些移除的方法
-
移除functional(函数式组件),使用了新的异步组件的生成方法
-
移除filter
-
移除$mount,统一使用createApp的mount方法来挂载
-
移除$destory,使用createApp的unmount方法来卸载
-
移除$children
-
移除inline template attributes内联模版
-
移除
$on,$once,$off实例方法 -
移除$listener
-
移除$scopedSlots
-
移除.native事件修饰符,并由emits选项代替
-
指令的生命周期钩子函数与组件生命周期对应
Vue源码项目结构
-
reactivity:响应式系统
-
runtime-core:与平台无关的运行时核心 (可以创建针对特定平台的运行时 - 自定义渲染器)
-
runtime-dom: 针对浏览器的运行时。包括DOM API,属性,事件处理等
-
runtime-test:用于测试
-
server-renderer:用于服务器端渲染
-
compiler-core:与平台无关的编译器核心
-
compiler-dom: 针对浏览器的编译模块
-
compiler-ssr: 针对服务端渲染的编译模块
-
template-explorer:调试编译器输出的开发工具
-
shared:多个包之间共享的内容
-
vue:完整版本,包括运行时和编译器
monorepo介绍
Vue3中 运用了monorepo,使用 yarn workspace + lerna来管理项目
monorepo 是一种管理代码的方式,在这种方式下会摒弃原先一个 module 一个 repo 的方式,取而代之的是把所有的 modules 都放在一个 repo 内来管理。简单来说,就是一个大的Git仓库,管理所有的代码(案例:create-react-app, Babel, react-router)
常见解决方案:
- lerna
lerna, 在项目 repo 中以lerna.json声明 packages 后,lerna 为项目提供了统一的 repo 依赖安装 (lerna bootstrap),统一的执行 package scripts (lerna run),统一的 npm 发版 (lerna publish) 等特性 官网:Learn
-
全局安装
npm install lerna -g lerna init
-
常用命令
//安装依赖生成软链 lerna bootstrap //查看所有包 lerna ls //发布包 lerna publish
- yarn
yarn workspace特性,yarn 突出的是对依赖的管理,包括 packages 的相互依赖、packages 对第三方的依赖,yarn 会以 semver 约定来分析 dependencies 的版本,安装依赖时更快、占用体积更小。 yarn 官网对 workspace的详细说明:Workspaces | Yarn
// vue-next项目中package.json
"workspaces": [
"packages/*"
],
Vue3项目搭建的方式
回顾Vue2的搭建方式
npm install -g @vue/cli
vue create xxx
基于 vue/cli 配置 vue3.0
npm install -g @vue/cli
vue --version // cli版本必须大于等于4.3.1
vue create xxx
vue add vue-next 或者 npm install @vue/composition-api --save
基于vite配置vue3.0
vite简介
-
基于浏览器原生 ES imports 的开发服务器,利用浏览器去解析 imports
-
同时不仅有 Vue 文件支持,还搞定了热更新,省略了打包的过程,开发时项目的整体启动速度和文件多少无关
-
快速的冷启动
-
即时的模块热更新
-
真正的按需编译
-
vite github地址:[vite]
npm init vite-app xxx cd xxx npm install npm run dev // 运行 npm run build // 打包
reactivity响应式原理介绍与实现
reactivity模块将响应式的功能和vue代码解耦,单独作为一个js库来调用在其他任何js运行的框架当中,都可以引入和使用vue3的这个响应式功能
响应式原理介绍
这里不全面讲解在Vue3中如何使用Composition API,只大概讲解其中的一部分,主要讲解实现响应式原理的思想
setup
-
setup 函数是一个新的组件选项,作为在组件内使用 Composition API 的入口点
-
运行在组件被实例化时候,在初始化props和beforeCreate之间调用
-
可以接收 props 和 context
-
this在setup()中不可用
-
返回的是一个对象,单文件组件模板中会用到返回的对象来渲染响应式视图
reactive
-
将一个普通对象经过Proxy的加工变为一个响应式的对象,reactive 基于proxy对数据进行深度监听,以此构建响应式(ref不是基于Proxy的,ref是基于Object.defineProperty())Proxy介绍:Proxy|MDN
-
用于处理引用数据类型
-
等同于 Vue2.x 的 Vue.observable()
-
与Vue2.x不同的是,Vue2.x是会先把数据对象递归遍历转化成为响应式对象,然后再使用,Vue3是当使用到数据的时候,再通过Proxy转化为响应式对象,这一点上也相比较Vue2.x节省了性能
-
加工后的对象属于深度克隆的对象,并非原对象
-
如果模板中的数据不想使用获取对象的方式来渲染的话,toRefs(把reactive中的每一项变为ref响应式的数据)
ref
-
接受一个参数值并返回一个响应式且可改变的 ref 对象
-
通常处理基本数据类型
-
ref 对象拥有一个指向内部值的单一属性 .value,当ref在模板中使用的时候,它会自动解套,无需在模板内额外书写 .value (例如const a = ref(0),在逻辑代码中使用a的值要加.value,在模板中使用可以直接使用a,不用加.value)
computed
-
传入一个 getter 函数,返回一个默认不可手动修改的 ref 对象
-
参数可修改为一个包含get和set函数的对象,创建一个可手动修改的计算状态
-
Vue3 的computed与 Vue2.x 的computed相比,他的用法稍有不同,内部实现不同,但是概念理解完全相同
watch
-
完全等效于 Vue2.x this.$watch
-
watch 需要侦听特定的数据源,并在回调函数中执行副作用
-
默认情况是懒执行的,也就是说仅在侦听的源变更时才执行回调
watchEffect
-
传入一个函数,并响应式追踪其依赖,并在其依赖变更时重新运行该函数
-
与watch不同的是,他会在项目初始化的时候就立即执行一次
响应式原理简单实现
进入正题,实现reactive,ref,computed,effect
项目src目录下建立reactivity目录,用来存放响应式的api(reactive等),每个响应式api对应一个js文件,每个文件导出相应的方法,reactivity目录如下
reactivity
│ ├── baseHandlers.js
│ ├── computed.js
│ ├── effect.js
│ ├── index.js
│ ├── operations.js
│ ├── reactive.js
│ └── ref.js
在reactivity目录下建立一个index.js文件,整合这些方法,对每个api文件既导入又导出
//index.js
export {computed} from './computed';
export {effect} from './effect';
export {reactive} from './reactive';
export {ref} from './ref';
reactive实现思想
Vue3 里使用了 Proxy 和 Reflect 实现reactive,前者拦截赋值操作,后者完成赋值的默认行为,Proxy介绍:Proxy|MDN、Reflect介绍:Reflect|MDN
Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程。
reactive用法:
import {reactive} from '@vue/reactivity'
const state = reactive({name: 'GSX', number: 20000, arr: [1 , 2, 3]});
//打印
console.log(state)
console.log(state.name)
//输出
Proxy {name: "GSX", number: 20000, arr: Array(3), __v_reactive: Proxy}
GSX
reactive.js简单实现
// target是我们传入的,要变成响应式的目标对象
export function reactive(target) {
// 用proxy进行代理 创建一个响应式的对象 目标对象可能不一定是数组或者对象
// mutableHandlers是baseHandlers.js导入的,对proxy处理过的对象进行操作的方法
return createReactiveObject(target, mutableHandlers);
}
function createReactiveObject(target, baseHandler) {
// 如果不是对象,就不执行
if(!isObject(target))
return
}
const observed = new Proxy(target, baseHandler);
// 返回代理后的结果
return observed;
}
把一个普通对象变成响应式的对象,大概流程如下
baseHandlers.js
const get = createGetter();
const set = createSetter();
function createGetter() {
// 进行取值操作
// proxy + reflect
return function get(target, key, reciver) {
const result = Reflect.get(target, key);
// 依赖收集
track(target, 'get', key);
if (isObject(result)) {
return reactive(result);
}
return result
}
}
function createSetter() {
// 进行设置值
return function set(target, key, value, reciver) {
const hadKey = hasOwn(target, key);
const oldVal = target[key];
const result = Reflect.set(target, key, value, reciver);
if (!hadKey) {
// 依赖更新(新增)
trigger(target, 'add', key, value, oldVal)
}
else if(hasChanged(value, oldVal)) {
// 依赖更新(修改)
流程图:
ref实现思想
ref的原理就是将一个普通值,转化成对象,并且在获取和设置值时可以增加依赖收集和触发更新的功能,ref通常用于处理基本数据类型的值,也可以处理引用数据类型,例如const value = ref({a:1, b:2});但是传入一个引用数据类型,ref会将传入的值用reactive进行处理成响应式数据
ref用法(其用法类似于React的useState()):
import {reactive, effect, ref, computed} from '@vue/reactivity'
const status = ref(true);
//打印
console.log(status)
console.log(status.value)
//输出
RefImpl
ref的简单实现:
export function ref(value) {
return createRef(value);
}
function convert(rawValue) {
return isObject(rawValue) ? reactive(rawValue) : rawValue
}
function createRef(rawValue) {
let value = convert(rawValue);
let result = {
__v_isRef: true,
get value() {
// 取值依赖收集
track(result, TrackOpTypes.GET, 'value')
return value
},
set value(newVal) {
if (hasChanged(newVal, rawValue)) {
rawValue = newVal;
value = newVal
// 设置时触发更新
trigger(result, TriggerOpTypes.SET, 'value')
}
}
}
retun result
}
ref实现流程图:
effect实现思想
-
effect并不是Composition API,在写Vue3的时候,也不会直接使用到effect,但是很多Composition API都是基于effect来实现的
-
effect翻译过来就是影响、副作用的意思
-
默认effect会立即执行,在执行之前,先把effect变成全局的activeEffect,以供响应式数据收集依赖。当依赖的值发生变化时effect会重新执行(也就是依赖收集、依赖更新的过程)
-
effect接收两个参数,一个是要执行的function,一个是传入Composition API的option选项
-
computed、watchEffect、watch等都是基于effect实现的,不同的api传入不同的option
-
effecf抛出两个很重要的方法,track()用于处理依赖收集,trigger()用于处理依赖更新
-
effecf相当于Vue2.x中的Watcher
-
effect其实就是一个依赖收集、依赖更新函数,在它内部访问了响应式数据,响应式数据就会把这个effect函数作为依赖收集起来,下次响应式数据改了就触发它重新执行。
effect的简单实现:
export function effect(fn, options = {}) {
// 数据一变 自动更新
const effect = createReactiveEffect(fn, options);
if (!options.lazy) {
effect();
}
return effect;
}
// 创建响应式effect
let uid = 0;
let activeEffect;
const effectStack = [];
function createReactiveEffect(fn, options) {
const effect = function reactiveEffect() {
if (!effectStack.includes(effect)) {
// 是一个依赖收集的过程
try {
effectStack.push(effect);
// 将effect放到了当前effect上
activeEffect = effect;
// 计算属性会用到
return fn();
}
finally {
effectStack.pop()
activeEffect = effectStack[effectStack.length -1]
}
}
}
effect.options = options;
effect.id = uid++;
// 依赖了哪些属性
effect.deps = []
// todo
return effect;
}
// 用法和map一致 但是WeakMap是弱引用 不会导致内存泄漏
const targetMap = new WeakMap();
export function track(target, type, key) {
// 如果当前没有activeEffect
if (activeEffect == undefined) {
return;
}
// 根据key进行取值
let depsMap = targetMap.get(target);
// 如果没有key,就构建一个
if(!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
let dep = depsMap.get(key);
// 如果没有dep就接着构建
if(!dep) {
depsMap.set(key, (dep = new Set()))
}
// 防止effect中多次记录
// 这里dep就是set
if(!dep.has(activeEffect)) {
// 属性依赖了effect
dep.add(activeEffect);
// 让这个effect记录dep属性
activeEffect.deps.push(dep);
}
}
export function trigger(target, type, key) {
console.log(111,targetMap,target, key);
// 获取当前对应的map
const depsMap = targetMap.get(target);
if (!depsMap) {
return;
}
const run = (effects) => {
if (effects) {
effects.forEach(effect => effect())
}
}
// 出发的时候判断两种情况,判断是修改还是添加
if (key !== null) {
// 去执行map下对应的kay的effec
run(depsMap.get(key));
}
if (type === 'add') {
// 对数组新增属性 会触发length对应的依赖,在取值的时候会对length进行依赖收集
run(depsMap.get(Array.isArray(target) ? 'length' : ''))
}
}
流程图,如果下面流程图的图看不清,请打开流程图的地址effect流程图
effect实现流程图:
computed实现思想
-
computed是基于effect实现的
-
和Vue2.x一样,默认执行getter,也可以设置setter
computed用法:
import {reactive, effect, ref, computed} from '@vue/reactivity'
const state = reactive({name: 'GSX', number: 20000, arr: [1 , 2, 3]});
// computed是lazy属性为true的effect
let allNumber = computed(()=>{
console.log('ok')
return state.number * 2;
});
//打印
console.log(allNumber.value);
//输出
ok
40000
简易版计算属性实现
export function computed(getterOrOptions) {
let getter;
let setter;
if (isFunction(getterOrOptions)) {
getter = getterOrOptions;
setter = () => {}
}
else{
getter = getterOrOptions.get;
setter = getterOrOptions.set;
}
// 默认第一次取值是执行getter方法的
let dirty = true;
let computed;
// 计算属性也是一个effect
let runner = effect(getter,{
// 懒加载标识
lazy: true,
// 这里仅仅是标识而已 是一个计算属性
computed: true,
scheduler:()=>{
if(!dirty){
// 计算属性依赖的值发生变化后 就会执行这个scheduler
dirty = true;
trigger(computed,TriggerOpTypes.SET,'value')
}
}
})
let value;
computed = {
get value(){
// 多次取值 不会重新执行effect
if(dirty){
value = runner();
dirty = false;
track(computed,TrackOpTypes.GET,'value')
}
return value;
},
set value(newValue){
setter(newValue);
}
}
return computed;
}
computed实现思想流程图:
总结
以上就是对reactive、ref、effect、computed的一个简单实现,在使用Vue3的过程中,需要注意一些点:
-
使用reactive时,你的数据不止要是 typeof === ‘object’,你还必须的是以下的几种数据结构, Object, Array, Map, Set, WeakMap, WeakSet ,不然得不到响应式数据,相应的api有reactive、shallowReactive、readonly、shallowReadonly
-
对象不能被 makRaw 处理过(判断ReactiveFlags.SKIP === "__v_skip"),也不能经过 Object.frozen() 处理
-
一个响应式数据不能即是readonly又是reactive,如果你给reactive传入一个已经是响应式数据的数据,reactive不会再次把数据变为响应式数据,会给你返回你传入的原值
-
我们在访问和设置某个响应式数据state的时候实际是调用了他们属性的 getter/setter 方法,但是这个state作为一一个返回值或者参数的时候,它实际是作为一个值传递到了另外要给方法中,所以他的 getter/setter 将会丢失,数据无法响应。 以下是举例
function getValue() { const value = reactive({a: 0, b: 0}) return value } export default { setup() { // 只取得getValu方法返回值的引用值,而值里面的getter/setter丢失 const { a, b } = getValue() // 响应丢失 return { a, b } // 响应丢失 return { ...getValue() } // 响应不丢失,这里的pos实际是getValu里面的pos的值,所以pos.x与pos.y的属性依旧存在 return { pos: getValue() } } }
解决方法:给返回的数据放到toRefs中,把一个响应式对象转换成普通对象,该普通对象的每个 property 都是一个 ref ,和响应式对象 property 一一对应
function getValue() {
const value = reactive({a: 0, b: 0})
return toRefs(value)
}
export default {
setup() {
// 下面的使用方法都可以,响应式不会丢失
const { a, b } = getValue()
return { a, b }
return { ...getValue() }
}
}
- 值得一提的是,Vue3中并没有抛弃Object.defineProperty(),响应式ref还是基于Object.defineProperty(),除过reactivity,其他模块也有用到Object.defineProperty()的地方,所以在面试中或者看文章时如果有人说Vue3完全抛弃了Object.defineProperty(),这个说法一定是错误的
品效合一,科技助力!
科技赋能,有效增长!
将心注入,全力以赴!