感受时间好快啊,也有一个月多没更文了。这图片仍是2020年产的呢~html
开启2021年之旅,这篇文章是2021年首发...但愿读者掘友多多支持哈~vue
整理了一下这段日子里学习的Vue3,我想以后Vue3会成为一种趋势。那就捉急学起来吧~react
这个api的调用时机:建立组件实例,而后初始化 props ,紧接着就调用setup
函数。从生命周期钩子的视角来看,它会在 beforeCreate
钩子以前被调用。ios
咱们能够在这个函数写大部分的业务逻辑,在Vue2中咱们是经过每一个选项的形式将代码逻辑分离开,好比methods,data,watch
选项。Vue3如今 改变了这样的模式(ps:也不能说改变吧,由于它也是能兼容Vue2的写法)ajax
这难道不是三国的理论吗?分久必合,合久并分
嘛axios
setup 是有2个可选的参数。api
如何理解响应式对象?我在另一篇文章中使用例子讲述过一个场景,可查看:【Vue.js进阶】总结我从Vue源码学到了什么(上)数组
如今来看看Vue3是如何生成响应式数据markdown
该函数接收一个对象做为参数,并返回一个代理对象。reactive
函数生成的对象若是没有合理的使用会丢失响应式。数据结构
先来看看什么状况下会失去响应式:
setup(){
const obj=reactive({
name:'金毛',
age:10
});
return{
...obj, //失去响应式了 由于obj失去了引用了
}
},
复制代码
如何解决这个失去响应式的问题?
return {
obj, //这样写那么模板就要都基于obj来调取, 类型{{obj.age}}
...toRefs(obj) //用toRefs包装,必须是reactive生成的对象, 普通对象不能够, 它把每一项都拿出来包了一下, 这样就能够在模板中使用 {{age}}。
}
复制代码
至于toRefs是什么接下来会讲述,如今先知道它是能够解决失去响应式的问题。
由于reactive
函数能够代理一个对象,但没法代理基本数据类型,因此须要使用ref
函数来间接对基本数据类型进行处理。该函数对基本数据类型数据进行装箱操做使得成为一个响应式对象,能够跟踪数据变化。
<span @click="addN">快来点击我呗~~</span>
<span>{{n}}</span>
复制代码
setup(){
const n=ref(1); //生成的n是一个对象, 这样方便vue去监控它
function addN(){
console.log(n.value,n,'.....')
n.value++; //注意这里要使用.value的形式, 由于n是对象, value才是他的值
}
return {
n, //返回出去页面的template才可使用它, {{n}} 不须要.value
addN,
}
}
复制代码
在实现ref以前,咱们先来了解认识Vue3源码中提供的track
和trigger
这个两个api。
track 和 trigger是依赖收集的核心
track
是用来跟踪收集依赖 (收集 effect):接收三个参数。那trigger
用来触发响应 (执行 effect)。(ps:文章的后续会讲述如何去实现这两个api,如今先留个疑问...)
这里是利用js是单线程的,那就能够在获取值的时候进行拦截依赖收集,在设置更新值时触发依赖的更新。因此ref的实现大体实现思路就能够写成:
function myRef(val: any) {
let value = val
const ref = {
get value() {
// 收集依赖
track(r, TrackOpTypes.GET, 'value')
return value
},
set value(newVal: any) {
if (newVal !== value) {
value = newVal
// 触发响应
trigger(r, TriggerOpTypes.SET, 'value')
}
}
}
return ref
}
复制代码
如上在reactive
模块的内容中经过使用toRefs
来包装生成的对象,那么所生成的对象是指向了对象相应 property 的ref
。
利用Vue3官网的🌰来加深理解:
const state = reactive({
foo: 1,
bar: 2
})
const stateAsRefs = toRefs(state)
// ref 和 原始property “连接”
state.foo++
console.log(stateAsRefs.foo.value) // 2
stateAsRefs.foo.value++
console.log(state.foo) // 3
复制代码
toRef 和toRefs 实现原理是一致的。toRef用来把一个响应式对象的的某个 key 值转换成 ref。而toRefs函数是把一个响应式对象的全部的key都转成了ref。
由于target目标对象自己就是一个响应式数据,已经经历过依赖收集,响应触发的这个拦截了,因此在实现toRef时就不须要了。
这里实现了toRef,那么toRefs也天然而来就能够实现了...(ps:经过遍历获得便可)
function toRef(target, key) {
return {
get value() {
return target[key]
},
set value(newVal){
target[key] = newVal
}
}
}
复制代码
在使用关于ref相关的内容时,咱们看到了在模板中没有.value去获取数据,可是在js代码块中使用.value去访问属性。
能够总结为Vue3的自动拆装箱:
JS :须要经过.value访问包装对象
模板: 自动拆箱,就是在模板中不须要使用.value访问
在Vue3中有一个反作用的概念,那什么是反作用呢?
接下来说述的api就是和这个反作用相关!
effect
该函数用于定义反作用,它的参数就是反作用函数,这个函数可能会产生反作用。默认状况下,这个反作用会先执行。
能够经过下面的代码来知晓:
import { effect,reactive } from '@vue/reactivity';
// import {watchEffect} from '@vue/runtime-core';
// 使用 reactive() 函数定义响应式数据
const obj = reactive({ text: 'hello' })
// 使用 effect() 函数定义反作用函数
effect(() => {
document.body.innerText = obj.text
})
// watchEffect(() => {
// document.body.innerText = obj.text
// })
// 一秒后修改响应式数据,这会触发反作用函数从新执行
setTimeout(() => {
obj.text += ' world'
}, 1000)
复制代码
也就是说effect接收的回调函数cb就是一个反作用,当数据发生变化的时候,这个cb就会被触发...
import {effect,reactive } from '@vue/reactivity';
const obj = reactive({ a: 1 })
effect(() => {
console.log(obj.foo)
}
obj.a++
obj.a++
obj.a++
//结果:2,3,4
复制代码
当effect函数和响应式数据创建了联系,那么只要响应式数据一发生改变,那么effect函数回调就会被执行。也就是说变几回执行几回。这样的性能是否是很差??
effect能够传递第二个参数 { scheduler: XXX }
, 指定调度器:XXX。
所谓调度器就是用来指定如何运行反作用函数的。
而watchEffect
函数就是基于这个调度器的原理来优化实现反作用。
import {reactive } from '@vue/reactivity';
import {watchEffect} from '@vue/runtime-core';
const obj = reactive({ a: 1 })
watchEffect(() => {
console.log(obj.foo)
}
obj.a++
obj.a++
obj.a++
//结果:4
复制代码
那这个实现思路是什么呢? 至关于用一个队列queue来收集这些cb,在收集以前作一个判断看看队列是否已经存在这个cb。而后经过while循环执行队列里的cb便可。 伪代码:
const queue= [];
let dirty = false;
function queueHandle(job) {
if (!queue.includes(job)) queue.push(job)
if (!dirty) {
dirty = true
Promise.resolve().then(() => {
let fn;
while(fn = queue.shift()) {
fn();
}
})
}
}
复制代码
通常在开发环境下不使用effect而是使用watchEffect。
刚刚上面讲述的是在同步的状况下,那么异步的反作用好比数据发生变动时又会发生一次ajax请求。咱们没办法判断哪一次的请求更快,这无形当中就给咱们带来了不肯定性...
那如何解决这种不肯定呢?
想办法清理失效时的回调,我所想的是经过在执行这一次的反作用时,清理上一次的异步反作用,使得以前挂起的异步操做无效。
Vue3经过在watchEffect传入的回调函数中能够接收一个 onInvalidate
函数做入参。
能够基于effect实现大体的原理:
import { effect } from '@vue/reactivity'
function watchEffect(fn: (onInvalidate: (fn: () => void) => void) => void) {
let cleanup: Function
function onInvalidate(fn: Function) {
cleanup = fn
}
// 封装一下 effect
// 在执行反作用函数以前,先使上一次无做用无效
effect(() => {
cleanup && cleanup();
fn(onInvalidate)
})
}
复制代码
Vue3提供了一个stop函数,用来中止反作用。
在effect 函数会返回一个值,这个值其实就是 effect 自己。将这个返回值传入到stop函数中,那么后续在更改数据,都没法实现effect 的回调函数被调用。
watchEffect
会维护与组件实例以及组件状态 (是否被卸载等) 的关系,若是一个组件被卸载,那么 watchEffect
也将被 stop
,但 effect
不会。
effect
是须要咱们收的去清楚反作用的,要否则它自己是不会主动被清除的。
再来谈谈watch
,它等同于组件的侦听器。watch
须要侦听特定的数据源,并在回调函数中执行反作用。默认状况下,它也是惰性的,即只有当被侦听的源发生变化时才执行回调。
这个api的实现和Vue2没什么很大的理解区别,关键Vue3在Vue2的基础上又扩展了一些功能,相对于Vue2更加完美了...
// 特定响应式对象监听
// 也可开启immediate: true, 这个和2.0没什么区别
watch(
text,
() => {
console.log("watch text:");
}
);
// 特定响应式对象监听 能够获取新旧值
watch(
text,
(newVal, oldVal) => {
console.log("watch text:", newVal, oldVal);
},
);
// 多响应式对象监听
watch(
[firstName,lastName],
([newFirst,newLast], [oldFirst,oldlast]) => {
console.log(newFirst,'新的first值',newLast,'新的last值')
},
);
复制代码
与 watchEffect 比较,watch 容许咱们:
还记得Vue2的$forceUpdate
这个强制性刷新吗? 在Vue3中也有一个强制去触发反作用的api。先来看看下面的代码:
//shallowRef它只代理 ref 对象自己,也就是说只有 .value 是被代理的,而 .value 所引用的对象并无被代理
const shallow = shallowRef({
greet: 'Hello, world'
})
// 第一次运行时记录一次 "Hello, world"
watchEffect(() => {
console.log(shallow.value.greet)
})
// 这不会触发做用,由于 ref 很浅层
shallow.value.greet = 'Hello, universe'
// 手动触发,记录 "Hello, universe"
triggerRef(shallow)
复制代码
2.x与 3.0的对照
beforeCreate -> 使用 setup()
created -> 使用 setup()
beforeMount -> onBeforeMount ---只能在setup里面使用
mounted -> onMounted ---只能在setup里面使用
beforeUpdate -> onBeforeUpdate ---只能在setup里面使用
updated -> onUpdated ---只能在setup里面使用
beforeDestroy -> onBeforeUnmount ---只能在setup里面使用
destroyed -> onUnmounted ---只能在setup里面使用
errorCaptured -> onErrorCaptured ---只能在setup里面使用
复制代码
Vue2的ref获取真实的dom元素this.$refs.XXX
,而Vue3也是经过ref获取真实的dom元素,可是写法上发生更改。
<div v-for="item in list" :ref="setItemRef"></div>
<p ref="content"></p>
复制代码
import { ref, onBeforeUpdate, onUpdated } from 'vue'
export default {
setup() {
//定义一个变量接收dom
let itemRefs = [];
let content=ref(null);
const setItemRef = el => {
itemRefs.push(el)
}
onBeforeUpdate(() => {
itemRefs = []
})
onUpdated(() => {
console.log(itemRefs)
})
//返出去的名称要与dom的ref相同, 这样就能够接收到dom的回调
return {
itemRefs,
setItemRef,
content
}
}
}
复制代码
以前开发一个公共组件或者是要封装一个公共的插件时会将功能挂在原型上或者是使用mixins。
其实这样子很差,挂在原型上使得Vue显得臃肿并且还会命名冲突的可能,mixins混入会使得代码跳跃,让阅读者的逻辑跳跃。
如今有个新的方案CompositionAPI,来实现插件的开发
1.能够将插件的公共功能使用provide函数和inject函数包装。
import {provide, inject} from 'vue';
// 这里使用symbol就不会形成变量名的冲突了, 这个命名权交给用户才是真正合理的架构设计
const StoreSymbol = Symbol()
export function provideString(store){
provide(StoreSymbol, store) //
}
//目标插件
export function useString() {
const store = inject(StoreSymbol)
/*拿到具体的值了,能够作相关的功能了*/
return store
}
复制代码
2.在根组件进行数据的初始化,引入provideString函数,并进行数据传输
export default {
setup(){
// 一些初始化'配置/操做'能够在这里进行
// 须要放在对应的根节点, 由于依赖provide 和 inject
provideString({
a:'可能我是axios',
b:'可能我是一个message弹框'
})
}
}
复制代码
3.在想要的组件中引入插件,完成相关的功能的使用
import { useString } from '../插件';
export default {
setup(){
const store = useString(); //可使用这个插件了
}
}
复制代码
上述留下来的疑问,如今在这里实现。
咱们先来分析一下Vue3响应式原理是如何实现的:
Object.defineProperty
进行拦截了,相反替代的方案是ES6的Proxy
。Dep
类和Watch
类,而是经过track
函数将目标对象和反作用进行相关联,经过trigger
进行依赖的响应。这只是大致的思路,实现过程当中还有挺多细节的分析,接下来一步一步讲解...
先来实现一个reactivity,实现这个函数要思考的问题是:
1.{a:{b:2}}像这种多层嵌套的对象,如何进行响应更新?
2.一个对象屡次调用reactivity函数,那么应该怎么处理?
3.一个对象的代理对象调用了reactivity函数,又应该怎么处理?
4.如何去判断对象是新增属性仍是修改属性?
//工具类函数
function isObject(obj){
return typeof obj==='object' && obj!==null?true:false;
}
function isOwnKey(target,key){
return Object.hasOwnProperty(target,key)?true:false;
}
复制代码
function reactivity(target){
return createReactivity(target);
}
let toProxy=new WeakMap(); //用来链接目标对象(key)与代理对象(value)的关系
let toRaw=new WeakMap(); //用来链接代理对象(key)与目标对象(value)的关系
function createReactivity(target){
if(!isObject(target)){
return ;
}
let mapProxy=toProxy.get(target); //处理目标对象被响应式处理屡次的状况
if(mapProxy){
return mapProxy;
}
if(toRaw.has(target)){ //处理目标对象的代理对象被响应式处理 let proxy=reactivity(obj);reactivity(proxy);
return target;
}
let proxy=new Proxy(target,{
get(target,key,receiver){
let res=Reflect.get(target,key);
//在获取属性的时候,收集依赖
track(target,key); ++++
return isObject(res)?reactivity(res):res; //递归实现多层嵌套的对象
},
set(target,key,value,receiver){
let res=Reflect.set(target, name, value,receiver);
let oldValue=target[key]; //获取老值,用于比较新值和老值的变化,改变了才修改
/**经过判断key是否已经存在来判断是新增属性仍是修改属性,而且在新增的时候可能会改变原有老的属性,这一点大多数人都不会被考虑到 */
if(!isOwnKey(target,key)){
console.log('新增属性');
//在设置属性的时候发布
trigger(target,'add',key); +++
}else if(oldValue!==value){
console.log('修改属性');
trigger(target,'set',key); +++
}
return res;
},
})
toProxy.set(target,proxy);
toRaw.set(proxy,target);
return proxy;
}
复制代码
如今来实现一个反作用effect函数,这个函数咱们考虑最简单的方式,就是传入一个fn做为入参。
咱们用一个全局的队列来存储effect,这个队列的存储的方式正如我刚刚所说的是栈队列的方式。
那么在一上来effect就会默认执行一次,那么先收集effect,而后利用js单线程的原理进行目标对象和effect进行相关联。
let effectStack=[]; //栈队列,先进后出
function effect(fn){
let effect=createEffect(fn);
effect(); //默认先执行一遍
}
function createEffect(){
let effect=function(){
run(effect,fn);
}
return effect;
}
//run执行函数,功能1.收集effect,2.执行fn
function run(effect,fn){
//利用try-finally来防止当发生错误的时候也会执行finally的代码
//利用js是单线程执行的。先收集再关联
try{
effectStack.push(effect);
fn();
}finally{
effectStack.pop();
}
}
复制代码
这一步的关键是在收集依赖的时候如何让目标对象的key和effect产生联系。 在这里有个特殊的数据结构:
{
target:{
key1:[effect1,effect12],
key2:[effect3,effect4],
key3:[effect5],
}
}
复制代码
每一个目标对象(做为key)都有对应的value(是个对象),而后对象(value)又映射了key和effect。
那么在响应依赖的时候,由于咱们获得了effect和目标对象所对应的key的关系了,那么遍历触发便可。
let targetMap=new WeakMap();
//收集依赖
function track(target,key){
let effect=effectStack[effectStack.length-1]; //从栈获取effect看看是否有反作用
if(effect){ //有关系才建立依赖
let depMap=targetMap.get(target);
if(!mapRarget){
targetMap.set(target,(depMap=new Map()));
}
let deps=depMap.get(key);
if(!deps){
mapDep.set(key,(deps=new Set()));
}
if(!deps.has(effect)){
deps.add(effect)
}
}
}
//响应依赖
function trigger(target,type,key){
let depMap=targetMap.get(target);
if(depMap){
let deps= depMap.get(key); //当前的key所对应的effect
if(deps){
deps.forEach(effect=>effect());
}
}
}
复制代码
好了,到这里Vue3的响应式原理基本上是实现了,固然你能够看完本篇文章再去看看源码,我相信你会更容易理解源码了~
在学习的过程当中,翻过Vue3的源码看了3天,说实话还真挺头疼。后来我决定不先看源码了,先学会如何去使用Vue3开始,而后再想一想为什么会这么用,最后到如何实现。
求知的路上真的是路漫漫其修远兮,吾将上下而求索