Vue3——基础内容部分(小满版本)(二)https://developer.aliyun.com/article/1470373
进阶 响应式原理源码实现
响应式原理
Vue2 使用的是 Object.defineProperty Vue3
使用的是 Proxy
Vue2.0的不足
核心:核心代码是使用 Object.defineProperty () 来劫持对象中每一个属性的 set 和 get 方法
总结:
缺点一:如果对象中属性过多,那么需要给每个属性都绑定 defineProperty,十分损耗效率。
缺点二:只能监听对象属性的修改、读取,如果涉及删除、增加就无法响应式刷新页面了(只能通过 Vue.set)
缺点三:通过下标、length 修改数组也不会响应式刷新页面,可以直接重新赋值,或者使用 filter、map、concat、slice 等生成新数组对其赋值
- 对象只能劫持 设置好的数据
- 是否可以删除(Configurable)
- 是否可以枚举(Enumerable)
- 是否可以修改(Writable)
- 返回值(value)
- set(重要,修改触发的回调函数)
- get(重要,获取触发的回调函数)
- 新增的数据需要 Vue.Set (xxx) 数组只能操作七种方法,修改某一项值无法劫持(就算能劫持也会造成性能上的问题)
- 1.splice()2.push(),3.pop(),4.shift()5. unshift(),6.sort(),7.reverse()
Vue3 响应式
和 Vue2 不同的是,它的核心是 es6 的 Proxy 结合 Reflect 实现的,使用代理,可以不直接操作对象,这样就可以监听到所有对象的增删改查,同时也提升了效率。
手写实现reactive
实现最基础的get与set
const isObject = (target)=> target !=null && typeof target == 'object'//此行是在13分的时候加上的,准备递归,递归代码写在最下面,进行对比 export const reactive = <T extends object>(target:T)=>{//使用一个泛型约束 return new Proxy(target,{ get (target,key,receiver){//返回三个参数,分别为当前传入的对象、对象的属性、也是当前对象 let res = Reflect.get(target,key,receiver)//需要使用Reflect接收三个参数,也是取值的,防止上下文错乱 return res; //return target[key] 这种返回方式会造成上下文的错乱 }, set(target,key,value,receiver){//set需要接收的是一个布尔值 let res = Reflect.set(target,key,value,receiver);//Reflect刚好返回的就是布尔值 return res; } }) } reactive({})
effect track trigger 实现
实现 effect 副作用函数
使用一个全局变量
active 收集当前副作用函数,并且初始化的时候调用一下
let activeEffect:any//ts提示下需要这样定义一下,不然爆红线了 export const effect = (fn:Function)=>{//fn是匿名函数方便用户去自定义,实现依赖收集与更新 //_effect是一个闭包,里面全局变量(全局变量在effect外面已经定义)将闭包收集起来 const _effect = function (){ activeEffect = _effect//收集起来 fn()//需要调用一下去执行effect的副作用函数 } _effect()//首次默认调用一下 }
实现 track
//target参数被当作targetMap中的key收集起来了 //Value里的new Map的key对应的就算targetMap的key。然后new Map对key去收集依赖(也就是set结构),set结构下去存effect副作用函数 -----------------------------------------------------------> //注意:entries?: [object, any][] | null): WeakMap<object, any>,也就是说weakMap只接收对象类型 const targetMap = new WeakMap()//key需要拼装格式,需要一个全局变量先存一下 export const track = (target?:any,key?:any) => {//target是一个对象 //定义好第一层 let depsMap = targetMap.get(target)//target是targetMap下的key,我们先获取key if (!depsMap){//第一次值是取不到的 depsMap = new Map()//此时我们进入流程图的第一阶段,将new Map传入最外层的depsMap target.set(target,depsMap)//分别对应了new Map往下传的key与Value } //定义好第二层 let deps = depsMap.get(key)//获取第二层的new Set值 if (!deps){//一样的,第一次的值还是取不到的 deps = new Set()//取不到值,但是我们获取到了第二层的new Set depsMap.set(key,deps)//通过new Map将key与value关联起来 } deps.add(activeEffect)//将副作用函数添加进来,也就是流程图最下面的操作 }
export const trigger = (target:any,key:any)=>{ const depsMap = target.get(target)//通过targetMap下的key取到Map下的key了 if (depsMap){ const deps = depsMap.get(key)//又是重复的操作,通过Map下的key取到了依赖 //取到依赖后进行一个更新(遍历数组),调用上面定义好的副作用函数 deps.forEach((effect:any)=>effect()) }else{ return "不好意思,你没取到值" } }
完整形式代码
reactive有进行改动,并非单纯拼凑
let activeEffect:any export const effect:any = (fn:Function)=>{//fn是匿名函数方便用户去自定义,实现依赖收集与更新 //_effect是一个闭包,里面定义全局变量将闭包收集起来 const _effect = function (){ activeEffect = _effect//收集起来 fn()//需要调用一下去执行effect的副作用函数 } _effect()//首次默认调用一下 } //注意:entries?: [object, any][] | null): WeakMap<object, any>,也就是说weakMap只接收对象类型 const targetMap = new WeakMap()//key需要拼装格式,需要一个全局变量先存一下 export const track = (target?:any,key?:any) => {//target是一个对象 //定义好第一层 let depsMap = targetMap.get(target)//target是targetMap下的key,我们先获取key if (!depsMap){//第一次值是取不到的 depsMap = new Map()//此时我们进入流程图的第一阶段,将new Map传入最外层的depsMap target.set(target,depsMap)//分别对应了new Map往下传的key与Value } //定义好第二层 let deps = depsMap.get(key)//获取第二层的new Set值 if (!deps){//一样的,第一次的值还是取不到的 deps = new Set()//取不到值,但是我们获取到了第二层的new Set depsMap.set(key,deps)//通过new Map将key与value关联起来 } deps.add(activeEffect)//将副作用函数添加进来,也就是流程图最下面的操作 } export const trigger = (target:any,key:any)=>{ const depsMap = target.get(target)//通过targetMap下的key取到Map下的key了 if (depsMap){ const deps = depsMap.get(key)//又是重复的操作,通过Map下的key取到了依赖 //取到依赖后进行一个更新(遍历数组),调用上面定义好的副作用函数 deps.forEach((effect:any)=>effect()) }else{ return "不好意思,你没取到值" } } export const reactive = <T extends object>(target:T)=>{//使用一个泛型约束 return new Proxy(target,{ get (target,key,receiver){//返回三个参数,分别为当前传入的对象、对象的属性、接收方 let res = Reflect.get(target,key,receiver)//需要使用Reflect接收三个参数,也是取值的,防止上下文错乱 track(target,key)//传入target return res; //return target[key] 这种返回方式会造成上下文的错乱 }, set(target,key,value,receiver){//set需要接收的是一个布尔值 let res = Reflect.set(target,key,value,receiver);//Reflect刚好返回的就是布尔值 trigger(target,key)//传入trigger return res; } }) } reactive({})
测试代码
html使用import注意加上type 并且要起一个服务 如liveServer
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <div id="app"> </div> <script type="module"> import { reactive } from './reactive.js' import { effect } from './effect.js' const user = reactive({ name: "小满", age: 18 }) effect(() => { document.querySelector('#app').innerText = `${user.name} - ${user.age}` }) setTimeout(()=>{ user.name = '小满打出了康康卡' setTimeout(()=>{ user.age = '23' },1000) },2000) </script> </body> </html>
递归实现
import { track, trigger } from './effect' const isObject = (target) => target != null && typeof target == 'object' export const reactive = <T extends object>(target: T) => { return new Proxy(target, { get(target, key, receiver) { const res = Reflect.get(target, key, receiver) as object track(target, key) if (isObject(res)) {//深层次的劫持 return reactive(res) } return res }, set(target, key, value, receiver) { const res = Reflect.set(target, key, value, receiver) trigger(target, key) return res } }) }
新的测试html的demo
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <div id="app"> </div> <script type="module"> import { reactive } from './reactive.js' import { effect } from './effect.js' const user = reactive({ name: "小满", age: 18, foo:{ bar:{ sss:123 } } }) effect(() => { document.querySelector('#app').innerText = `${user.name} - ${user.age}-${user.foo.bar.sss}` }) setTimeout(()=>{ user.name = '麒麟哥的笔记是txt格式的' setTimeout(()=>{ user.age = '23' setTimeout(()=>{ user.foo.bar.sss = 66666666 },1000) },1000) },2000) </script> </body> </html>
第九章 — computed计算属性
- 接受一个 getter 函数,返回一个只读的响应式 ref 对象。该 ref 通过
.value
暴露 getter 函数的返回值。它也可以接受一个带有get
和set
函数的对象来创建一个可写的 ref 对象。 - 当依赖的属性的值发生变化的时候,才会触发他的更改,如果依赖的值,不发生变化的时候,使用的是缓存中的属性值。
购物车案例
<template> <div> <table style="width:800px" border> <thead> <tr> <th>名称</th> <th>数量</th> <th>价格</th> <th>操作</th> </tr> </thead> <tbody> <tr :key="index" v-for="(item, index) in data"> <td align="center">{{ item.name }}</td> <td align="center"> <button @click="AddAnbSub(item, false)">-</button> {{ item.num }} <button @click="AddAnbSub(item, true)">+</button> </td> <td align="center">{{ item.num * item.price }}</td> <td align="center"> <button @click="del(index)">删除</button> </td> </tr> </tbody> <tfoot> <tr> <td></td> <td></td> <td></td> <td align="center">总价:{{ $total }}</td> </tr> </tfoot> </table> </div> </template> <script setup lang="ts"> import { computed, reactive, ref } from 'vue' type Shop = { name: string, num: number, price: number } let $total = ref<number>(0) const data = reactive<Shop[]>([ { name: "小满的袜子", num: 1, price: 100 }, { name: "小满的裤子", num: 1, price: 200 }, { name: "小满的衣服", num: 1, price: 300 }, { name: "小满的毛巾", num: 1, price: 400 } ]) const AddAnbSub = (item: Shop, type: boolean = false): void => { if (item.num > 1 && !type) { item.num-- } if (item.num <= 99 && type) { item.num++ } } const del = (index: number) => { data.splice(index, 1) } $total = computed<number>(() => { return data.reduce((prev, next) => { return prev + (next.num * next.price) }, 0) }) </script> <style> </style>
第十章 — watch侦听器 & 源码讲解
用于声明在数据更改时调用的侦听回调。
watch
选项期望接受一个对象,其中键是需要侦听的响应式组件实例属性 (例如,通过 data
或 computed
声明的属性)—— 值是相应的回调函数。该回调函数接受被侦听源的新值和旧值。
第一个参数新值,第二个参数旧值
- 第三个参数是决定是否深层次监听的(deep:true),但其实我们通过reactive也一样可以实现深层次监听
- 但是有一个问题,就是通过控制台我们可以看到新值旧值都已经被新值覆盖了
- immediate决定是否一开始就自调用
侦听单个数据源
<template> <div> case1:<input v-model="message" type="text"> <hr> case2:<input type="text"> </div> <h1>{{tom}}</h1> </template> <script setup lang="ts"> import {ref} from "vue"; let message = ref<string>('小满') //监听器第一个参数:侦听的数据源(sources) 第二个参数 回调函数 cb(newVal,oldVal) watch(message,(newVal,oldVal)=>{ console.log('新的值----', newVal); console.log('旧的值----', oldVal); }) </script> <style scoped> </style>
侦听多个数据源
<template> <div> case1:<input v-model="message" type="text"> <hr> case2:<input v-model="message2" type="text"> </div> <h1>{{tom}}</h1> </template> <script setup lang="ts"> import {ref} from "vue"; let message = ref<string>('小满') let message2 = ref<string>('喜多川学姐') //使用数组的形式侦听多个数据源,返回的结果也会变成数组,结果顺序 按照 监听顺序 watch([message,message2],(newVal,oldVal)=>{//此时新值旧值也会变成一个数组 console.log('新的值----', newVal); console.log('旧的值----', oldVal); }) </script> <style scoped> </style>
监听 Reactive
使用 reactive 监听深层对象开启和不开启 deep 效果一样
深层次监听啦啦啦,需要给reactive开启第三个属性option(是一个配置项) => 里面有一个deep就是用来代表深度监听的
//监听语句变成如下 case1:<input v-model="message.nav.bar.name" type="text"> import { ref, watch ,reactive} from 'vue' let message = ref({//reactive已经隐性开启deep了,不需要再手动开启了 nav:{ bar:{ name:"学姐好好吃饭" } } }) watch([message,message2],(newVal,oldVal)=>{//此时新值旧值也会变成一个数组 console.log('新的值----', newVal); console.log('旧的值----', oldVal); },{deep:true})//深度监听
通过控制台打印,我们发现了其中的Proxy下的深层次监听
此时会发现有一个问题,那就是旧值跟新值的内容是一样的(原因是因为引用类型返回的新值是跟旧值一样的)
我们在使用的时候会发现一个问题,那就是我只改变了一个值,为什么没有改变的那个值(例如上面的喜多川学姐
)也跟着带出来了,我只想要得到改变的值,那要怎么办到呢? => 简洁版提问:想要侦听单一属性
- Vue推荐是让我们把要监听的变成一个函数(而不是直接.xxx.xxx得到的字符串,这样会报错的,因为不是Proxy所代理的对象)
- 创建一个回调函数去返回这个要侦听的属性
//区别就是()=>message.value.nav.bar.name跟单纯message.value.nav.bar.name,后者会报错 watch(()=>message.value.nav.bar.name,(newVal,oldVal)=>{//此时新值旧值也会变成一个数组 console.log('新的值----', newVal); console.log('旧的值----', oldVal); },{deep:true})
immediate
watch侦听器 第三参数
开局立即执行一次
watch(()=>message.value.nav.bar.name,(newVal,oldVal)=>{//此时新值旧值也会变成一个数组 console.log('新的值----', newVal); console.log('旧的值----', oldVal); },{immediate:true})
flush
watch侦听器 第三参数
1
watch(()=>message.value.nav.bar.name,(newVal,oldVal)=>{//此时新值旧值也会变成一个数组 console.log('新的值----', newVal); console.log('旧的值----', oldVal); },{ flush:"pre"//pre组件更新之前调用 sync同步执行 post组件更新之后做的执行 })
源码讲解 黄金流程
位于视频7分钟
源码位置:packages > runtime-core > src > apiWatch.ts > watch
// overload: watching reactive object w/ cb export function watch< T extends object, Immediate extends Readonly<boolean> = false >( source: T, cb: WatchCallback<T, Immediate extends true ? T | undefined : T>, options?: WatchOptions<Immediate> ): WatchStopHandle // implementation export function watch<T = any, Immediate extends Readonly<boolean> = false>( source: T | WatchSource<T>, cb: any, options?: WatchOptions<Immediate> ): WatchStopHandle { if (__DEV__ && !isFunction(cb)) { warn( `\`watch(fn, options?)\` signature has been moved to a separate API. ` + `Use \`watchEffect(fn, options?)\` instead. \`watch\` now only ` + `supports \`watch(source, cb, options?) signature.` ) } return doWatch(source as any, cb, options)//核心函数调用了doWatch。source数据源,cd回调函数(新旧值),option配置项 } function doWatch( //source支持4种模式,ref对象、reactive对象、数组侦听多个源,传入函数侦听单一属性 source: WatchSource | WatchSource[] | WatchEffect | object,//第一件事情格式化source,将四种类型都赋给一个函数 cb: WatchCallback | null, { immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ ): WatchStopHandle { if (__DEV__ && !cb) { if (immediate !== undefined) { warn( `watch() "immediate" option is only respected when using the ` + `watch(source, callback, options?) signature.` ) } if (deep !== undefined) { warn( `watch() "deep" option is only respected when using the ` + `watch(source, callback, options?) signature.` ) } }
const warnInvalidSource = (s: unknown) => { warn( `Invalid watch source: `, s, `A watch source can only be a getter/effect function, a ref, ` + `a reactive object, or an array of these types.` ) } //ref Reactive [msg,msg2] ()=>msg.bar.name const instance = currentInstance let getter: () => any let forceTrigger = false let isMultiSource = false //如果是ref对象 if (isRef(source)) { //创建一个getter函数并且读取了 ref对象的value属性 getter = () => source.value forceTrigger = isShallow(source) } else if (isReactive(source)) { //如果是Reactive 对象直接返回一个getter 函数 并且设置deep 为true,所以就是刚刚上面说reactive不需要在手动打开deep的原因,因为源码已经默认开启了 getter = () => source deep = true } else if (isArray(source)) { //如果是数组 就遍历该数组 然后处理里面的ref 和 Reactive isMultiSource = true forceTrigger = source.some(s => isReactive(s) || isShallow(s)) getter = () => source.map(s => {//对数组做了一个遍历 if (isRef(s)) { return s.value } else if (isReactive(s)) { return traverse(s)//traverse是一个递归,会把里面的每个属性都做一个侦听 } else if (isFunction(s)) { return callWithErrorHandling(s, instance, ErrorCodes.WATCH_GETTER) } else { __DEV__ && warnInvalidSource(s) } }) } else if (isFunction(source)) { //如果source 是一个函数 则会判断 cb是否存在 getter就会对 source进行简单的封装 if (cb) {//判断有无cb,决定走cb还是with effect // getter with cb getter = () => callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER) } else { // no cb -> simple effect getter = () => { if (instance && instance.isUnmounted) { return } if (cleanup) { cleanup() } return callWithAsyncErrorHandling( source, instance, ErrorCodes.WATCH_CALLBACK, [onCleanup] ) } } } else { getter = NOOP __DEV__ && warnInvalidSource(source) }
//处理deep 深度监听 if (cb && deep) { const baseGetter = getter getter = () => traverse(baseGetter())//traverse 一个比较耗时的递归 }
job.allowRecurse = !!cb //调度都赋给scheduler let scheduler: EffectScheduler if (flush === 'sync') {//如果sync就同步执行 scheduler = job as any // the scheduler function gets called directly } else if (flush === 'post') {//post的话又把值传给了job //组件更新之后执行 queuePostRenderEffect,源码中有看到这个就一定是在组件更新之后去做一个执行的 scheduler = () => queuePostRenderEffect(job, instance && instance.suspense) } else { // default: 'pre' job.pre = true if (instance) job.id = instance.uid scheduler = () => queueJob(job) }
let oldValue = isMultiSource ? [] : INITIAL_WATCHER_VALUE//初始化旧值 const job: SchedulerJob = () => { if (!effect.active) { return } if (cb) { // watch(source, cb) const newValue = effect.run() if ( deep || forceTrigger || (isMultiSource ? (newValue as any[]).some((v, i) => hasChanged(v, (oldValue as any[])[i]) ) : hasChanged(newValue, oldValue)) || (__COMPAT__ && isArray(newValue) && isCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance)) ) { // cleanup before running cb again if (cleanup) { cleanup() } callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [ newValue, // pass undefined as the old value when it's changed for the first time //第一次执行旧值 是undefined 或者 空数组 oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue, onCleanup ]) //直接赋值,如果是对象旧直接引用了,所以新值和旧值是一样的。这个就是为什么引用类型新旧值没有变化 oldValue = newValue } } else { // watchEffect effect.run() } }
第十一章 — watchEffect高级侦听器
- 跟watch是不一样的,watchEffect是非惰性的。函数会帮你自己去调用一下
watchEffect(一开始会自己自调用一次)
立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。
如果用到 message 就只会监听 message 就是用到几个监听几个 而且是非惰性 会默认调用一次
let message = ref<string>('') let message2 = ref<string>('') watchEffect(() => { //console.log('message', message.value); console.log('message2', message2.value); })
清除副作用
就是在触发监听之前会调用一个函数可以处理你的逻辑例如防抖
import { watchEffect, ref } from 'vue' let message = ref<string>('') let message2 = ref<string>('') watchEffect((oninvalidate) => { //console.log('message', message.value); oninvalidate(()=>{ }) console.log('message2', message2.value); })
停止跟踪 watchEffect 返回一个函数 调用之后将停止更新
const stop = watchEffect((oninvalidate) => { //console.log('message', message.value); oninvalidate(()=>{ }) console.log('message2', message2.value); },{ flush:"post", onTrigger () { } }) stop()
在watchEffect里面会先处理回调函数,在处理其他内容,所以我们可以在里面去进行防抖之类的操作
断言 (assertion) 是一种在程序中的一阶逻辑 (如:一个结果为真或假的逻辑判断式),目的为了表示与验证软件开发者预期的结果 —— 当程序执行到断言的位置时,对应的断言应该为真。若断言不为真时,程序会中止执行,并给出错误信息。
副作用刷新时机 flush 一般使用 post
pre |
sync |
post |
|
更新时机 |
组件更新前执行 |
强制效果始终同步触发 |
组件更新后执行 |
Vue3——基础内容部分(小满版本)(四)https://developer.aliyun.com/article/1470376