# 关于Vue3
Vue3与10月6号左右宣布即将发布,作者也公开了底层的基于proxy语法重新构建的vue3新版本,大量使用了typescript去构建,rollup打包,风格也借鉴了react 15.8版本的hooks,相当于vue3中没有了this对象(use,mixin等基于this的API可能有改变,router和vuex也将基于inject,provid API重写--猜测),底层响应式原理重写(delete,set等由于defineprototypeAPI的缺点产生的API也将没有),Vue3相关的API可以看这里Vue Composition API目前还没出中文版本,熟悉react hooks的可直接上手。个人还是更期待react17.x后的concurrent模式带来的提升,hooks也早已是react提出已久的编程思想了。
# 核心API
# reactive
响应式核心,相关核心API依赖,使用proxy代理对象并返回proxy,相当于Vue2的definePropertyAPI,避免了数组的缺陷,可劫持数据的所有操作,具体查看相关API文档
# effect
与react的useEffect用法相识,当前先触发一次,之后对数据的更改都会执行该函数,仅对所传递的函数内有响应式的数据生效,且只有函数内的变量状态改变才触发,依赖收集很巧妙的利用JS单线程的特点,极大程度上自动优化了框架性能,使用者不必太关心性能问题。
# 运行
打包一份vue3的代码,试试效果
<!DOCTYPE html>
<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="./vue.global.js"></script>
<!-- vue仓库打包的代码 -->
</head>
<body>
<script>
let obj1={
name:'CJ',
age:23
}//原对象
let p1=Vue.reactive(obj1);//返回代理后的proxy对象
console.log(p1);//Proxy{name:"CJ}
Vue.effect(()=>{//影响:当p1的name属性发生改变时触发函数执行,仅仅只有name属性更改触发
console.log("effect1:name改变"+p1.name);
})//立即执行一次 打印effect1:name改变CJ
Vue.effect(()=>{//同上
console.log("effect2:name改变"+p1.name);
})//立即执行一次 打印effect2:name改变CJ
Vue.effect(()=>{//影响:当p1的age属性发生改变时触发函数执行,仅仅只有age属性更改触发
console.log("effect1:age改变"+p1.age);
})//立即执行一次 打印effect1:age改变23
Vue.effect(()=>{//同上
console.log("effect2:age改变"+p1.age);
})//立即执行一次 打印effect2:age改变23
p1.name="CJI@";//name属性有两个effect函数,所以执行两次
// effect1:name改变CJI@
// effect2:name改变CJI@
p1.name="CJI1";//同理
// effect1:name改变CJI1
// effect2:name改变CJI1
p1.name="CJI2";//同理
p1.name="CJI3";//同理
p1.age=24;//age更改也有两个effect,也是两次。
// 控制台结果
// effect1:age改变24
// effect2:age改变24
</script>
</body>
</html>
开始感觉真的很奇怪,vue3是如何做到effect内的函数仅因为自己函数内有的变量改动才会触发执行,感觉好神奇,每次依赖收集都能收集如此颗粒化。
# 核心原理及实现
使用typescript构建,刚学,比较菜,就当原生JS看就好了
# reactive.ts
源码路径:vue-next\packages\reactivity\src\reactive.ts
将数据转换为响应式,使用proxy代理对应的target,劫持对应的get,set,weakMap做代理映射表,基于其弱引用的特性,key只能为Object类型,对象不存在时GC自动回收其value,性能优化。
let {track,trigger}=require('./track');//触发器和收集依赖
import {proxyConstructor,Handle} from './interface';//ts接口,类型约束handler和proxy
let toProxy=new WeakMap();//存储代理对象弱引用,key只能为非简单类型,key为空时方便GC机制回收对用的value
let toRaw=new WeakMap();//防止重复代理已经代理过的proxy对象做缓存检查
export function reactive(target:any){//就是想和源码一样的调用过程而已
return createReactiveObj(target)
}
let handle:Handle={//proxy处理函数,new proxy传入处理器,建议先跳到createReactiveObj方法
get(target,key,receiver){
track(target,key)//effect内包裹的函数每次先执行都会获取内部被劫持变量
//触发对应的get,执行依赖收集
const res=Reflect.get(target,key,receiver);//Reflect能返回操作是否成功
return res;
},
set(target,key,val,receiver){
let res:boolean;
let oldVal=target[key];
if(!target.hasOwnProperty(key)){//没有属性为新增
//防止劫持对象为数组时触发两次set避免不必要的更新
//第一次改变为设置该数组的下标,如果是删除或者新增是则会触发该数组第项的length对象的改变
trigger(target,'add',key);//通知触发器执行对应的依赖
res=Reflect.set(target,key,val,receiver);//使用reflect能够返回操作是否成功
return res;
}else if(oldVal!==val){//对象已有改属性代表修改
res=Reflect.set(target,key,val,receiver);
trigger(target,'update',key);//通知触发器执行对应的依赖
return res;
}
return false;
},
deleteProperty(target,key){
return Reflect.deleteProperty(target,key)
}
};
//代理监测
function createReactiveObj<T extends Object >(target:T|any):proxyConstructor{
if(typeof target==='object'||target!=null){//type of null ===object===true
const targetProxy=toProxy.get(target)//代理映射表弱引用,查看是否以及被代理过
if(targetProxy){//防止代理重复的代理对象
return targetProxy;
}
if(toRaw.has(target)){//反向映射表防止传入多次代理已代理过的对象
return target
}
let observe:proxyConstructor=new Proxy(target,handle);
toProxy.set(target,observe);//添加代理映射表
toRaw.set(observe,target);//添加反向代理映射表
return observe;
}else{
return target;
}
}
# track.ts
effect函数收集监听对象effect里target对应的函数,利用执行函数时会访问到函数内监听对象的值,触发handler的get,执行收集依赖,相同的key放入同一个set集合,改变时在set函数内触发对应key的set集合的函数依次执行
let {effectStacks}=require('./effect');//存储effect函数的栈接口数组
let targetMap=new WeakMap();
export function track(target,key){//收集依赖
let effect=effectStacks[effectStacks.length-1];//得到effect内的函数
if(effect){
let depsMap=targetMap.get(target);//查看是否有该对象map
if(!depsMap){//没有则新建对应target的map对象
targetMap.set(target,depsMap=new Map());
}
let deps=depsMap.get(key);//得到该map下的所收集的set集合的依赖
if(!deps){//没有则新建空set
depsMap.set(key,deps=new Set());
}
if(!deps.has(effect)){//没有effect则添加
deps.add(effect);
}
}
}//函数执行一遍后targetMap内的结构就应该是这样的解构
// {
// target:{
// key:[effect1,effect2,effect3]//,
// key:[effect1]
// },
// target2:{
// key:[effect1,effect2,effect3]//,
// key:[effect1]
// }
// }
export function trigger(target,type,key){//触发依赖执行
let depsMap=targetMap.get(target);//获取对象下的map
if(!depsMap){
}else{//有map的话就代表该map下有set集合的effect函数
let deps=depsMap.get(key);//是否有该key的effect set集合
if(deps){//有就遍历整个set对象执行对应的effect内的函数
deps.forEach(fn => {
fn();
});
}
}
}
# effect.ts
存储effect栈数组,每次effect函数调用时都会往effect栈数组推入依赖的函数,然后执行一次effect内包裹的函数去触发proxy的get,再去触发收集依赖,以特定的格式收集存储该依赖,执行完毕后移除push的effect函数。
let effectStacks:Array<Function>=[];
function createReactiveEffect(fn:Function):void{
let effect =function(){
return run(effect,fn);
}
effect();
}
function run (effect:Function,fn:Function):void{
try{
effectStacks.push(effect);//effect栈推入effect函数
fn();//首次调用,执行一次,触发get函数内的track收集依赖,此时的栈内已有effect函数。
//track会将该函数已特定的形式存储起来
}finally{
effectStacks.pop();//删除push的effect,等待下次effect调用该函数,再次重新执行
}
}
function effect (fn:Function){
return createReactiveEffect(fn);
}
export {
effectStacks,
effect
};
# 效果
因为环境是ts构建,所以使用node去执行对应的代码
import {reactive} from './reactive';
import {effect } from './effect';
let obj1={name:'CJ'}
let p1=reactive(obj1);
effect(()=>{
console.log("effect1:"+p1.name)
})
effect(()=>{
console.log("effect2:"+p1.name)
})
p1.name="CJI@";
p1.name="CJI1";
p1.name="CJI2";
p1.name="CJI3";
//输出顺序
// effect1:CJ name effect1 首次执行
// effect2:CJ name effect2首次执行
// effect1:CJI@ name effect1 修改触发
// effect2:CJI@ name effect2 修改触发
// effect1:CJI1 name effect1 修改触发
// effect2:CJI1
// effect1:CJI2
// effect2:CJI2
// effect1:CJI3
// effect2:CJI3
# 效果
使用node去执行对应的代码
import {reactive} from './reactive';
import {effect } from './effect';
let obj1={name:'CJ'}
let p1=reactive(obj1);
effect(()=>{
console.log("effect1:"+p1.name)
})
effect(()=>{
console.log("effect2:"+p1.name)
})
p1.name="CJI@";
p1.name="CJI1";
p1.name="CJI2";
p1.name="CJI3";
//输出顺序
// effect1:CJ name effect1 首次执行
// effect2:CJ name effect2首次执行
// effect1:CJI@ name effect1 修改触发
// effect2:CJI@ name effect2 修改触发
// effect1:CJI1 name effect1 修改触发
// effect2:CJI1
// effect1:CJI2
// effect2:CJI2
// effect1:CJI3
// effect2:CJI3
# @vue/composition-api
从国庆节到现在还没听说vue3要彻底发布的消息,每天都迫不及待,终于在好奇心的驱使下去研究了一波composition-api的玩法,目前还没正式出vue3,在vue2项目中想要体验vue3的特性时需引入@vue/composition-api
# vue/cli3中使用vue/composition-api
初始化cli3项目就不多说了,直接甩命令
npm install -g @vue/cli
# OR
yarn global add @vue/cli
vue create vue3
#创建项目
cd vue3
#进入目录
yarn install
#安装依赖
npm run serve
#开启服务
一个cli3的基础服务就跑好了,为了能使我们使用vue3的特性,接下来开始安装composition-api
yarn add @vue/composition-api
安装后我们需要在vue项目中使用,需要引入@vue/composition-api,并使用vue的use 来安装
import Vue from 'vue';
import App from './App.vue';
import VueComponistionApi from '@vue/composition-api';//引入composition-api
Vue.config.productionTip = false;
Vue.use(VueComponistionApi);//安装插件
new Vue({
render: h => h(App),
}).$mount('#app');
接下来就能快乐的玩耍vue3的新特性了,首先来看看vue单文件结构的变化。
# 构建一个todosAPP
接下来我们来使用composition-api构建一个简单的todosAPP吧!!!
# V1
App.vue
<template>
<div id="app">
<input type="text" v-model="state.value">
<button @click="addItem">点击增加</button>
<ul>
<li v-for="(item, idx) in state.todos" :key="idx">
{{ item.name }}
<span style="float:right" @click="finishItem(idx)"
>是否完成:{{ item.isFinished ? "是" : "否" }}</span
>
</li>
</ul>
</div>
</template>
<script>
import { reactive, onMounted } from "@vue/composition-api";//引入composition-api
export default {
setup() {//所有生命周期或者函数API等都在setUp中
onMounted(() => {//渲染完成,对应vue2mounted
console.log("mounted!");
});
const state = reactive({//将数据转换为响应式
todos: [],
value:''
});
const addItem=()=>{//添加代办项
state.todos.push({
isFinished:false,
name:state.value
});
state.value="";
}
const finishItem=(idx)=>{//完成代办
state.todos[idx].isFinished=true;
}
//return的元素可以直接在template中取到,可返回需要的函数和state对象
return {
state,
addItem,
finishItem
};
}
};
</script>
<style>
#app {
font-family: "Avenir", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
这样一个简单的todosAPP就完成了,是不是总感觉和vue2有点相识,不是传说中vue3可以抽离逻辑代码吗,接下来我们修改一些代码,将我们的业务抽离出去
# V2
<template>
<div id="app">
<input type="text" v-model="state.value" />
<button @click="addItem">点击增加</button>
<ul>
<li v-for="(item, idx) in state.todos" :key="idx">
{{ item.name }}
<span style="float:right" @click="finishItem(idx)"
>是否完成:{{ item.isFinished ? "是" : "否" }}</span
>
</li>
</ul>
</div>
</template>
<script>
import { reactive, onMounted } from "@vue/composition-api";
const useTodos = () => {//将业务抽成函数并返回业务需要的状态和函数
onMounted(() => {
console.log("mounted!");
});
const state = reactive({
todos: [],
value: ""
});
const addItem = () => {
state.todos.push({
isFinished: false,
name: state.value
});
state.value = "";
};
const finishItem = idx => {
state.todos[idx].isFinished = true;
};
return {//返回状态
state,
addItem,
finishItem
};
};
export default {
setup(props,content) {
console.log(props,content)
return {//必须返回视图层需要调用到的函数或者对象
...useTodos(),
// ...useOthers()
};
}
};
</script>
<style>
#app {
font-family: "Avenir", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
# props以及content
- props的接收方式变了,vue2使用props对象来定义值的类型和默认值,vue3的props传递在setup函数的第一个参数
- this没了,vue2中所有关于组件的状态都挂载在this对象下,vue3中setup函数的第二个参数传递的this对象
export default {
setup(props,content) {//props和this
console.log(props,content)//
return {//必须返回视图层需要调用到的函数或者对象
...useTodos(),
// ...useOthers()
};
}
};
# 变化
个人感觉总体没什么变化,以前很多旧的概念和API都能直接使用,如果真要说变化的地方,就像是作者说的那样,底层性能和虚拟DOM的优化等,有兴趣可以网上看看,对于开发者而言,能抽离业务逻辑好处真的太多太多了,而且也更大的扩展的vue的编码方式,自由度也更大了,周末快乐,做饭去了