前言
经过前面的环境搭建以及项目构建,完成了基础的项目框架,下面学习实现vue3的reactivity。
观察官方如何使用
首先修改.npmrc
文件
# 解决一个问题 例如vue中有个依赖abc ,那么我们安装了vue就可以直接用abd,有一天vue不依赖abc了,那么你用abc就出错了,未来让这种幽灵依赖以后不出错,就在这里配置羞耻提升
shamefully-hoist = true
我们在vue3-plan上安装vue3
pnpm install vue -w
这个时候发现node_moules
中vue的依赖被展开了放在根目录上,在packages/reactivity/index.html
上引入vue官方的reactivity
。
<!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>
<!-- <script src="./dist/reactivity.global.js"></script> -->
<div id="app"></div>
<!-- 官方的 -->
<script src="../../node_modules/@vue/reactivity/dist/reactivity.global.js"></script>
<!-- 自己实现的 -->
<!-- <script src="./dist/reactivity.global.js"></script> -->
<script>
// effect 代表的是副作用函数,如果函数依赖发生改变,他就重新执行
// reactive 将数据变成响应式 相当于proxy
// shallowRactive,readonly,shallowReadonly
const {effect,reactive} = VueReactivity;
const state = reactive({name:"david",age:12,address:{num:567}})
//set 和 map 也可以劫持
effect(() => {
document.getElementById("app").innerHTML = `${state.name}今年${state.age}`
})
setTimeout(() =>{
state.age = 13
},1000)
</script>
</body>
</html>
通过上面的实验观察发现通过reactive
包裹之后的对象,能被监听到变化,然后effect
通过监听到变化而触发回调函数,从而打印出上面到语句。并且reactive
是能深层检测到对象的改变,当你修改了address
里面的num值时也能被监听到变化,这得益于vue3采用到proxy
。shallowRactive
和shallowReadonly
如名字,只能监听到表层,以为深处到属性并未做包装。
vue3对比vue2的变化
- 在Vue2的时候采用
defineProperty
来进行数据的劫持,需要对属性进行重写getter
和setter
性能差。 - 当新增属性和删除属性式就无法监听变化,需要通过
$set
、$delete
实现。 - 数组不采用defineProperty来进行劫持(浪费性能,对所有索引进行劫持会造成性能的浪费)需要对数组单独进行处理。
编写自己的响应式
首先引入的JS文件的html,从官方的引入链接改成引入自己的链接
<!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>
<!-- <script src="./dist/reactivity.global.js"></script> -->
<div id="app"></div>
<!-- 官方的 -->
<!-- <script src="../../node_modules/@vue/reactivity/dist/reactivity.global.js"></script> -->
<!-- 自己实现的 -->
<script src="./dist/reactivity.global.js"></script>
<script>
// effect 代表的是副作用函数,如果函数依赖发生改变,他就重新执行
// reactive 将数据变成响应式 相当于proxy
// shallowRactive,readonly,shallowReadonly
const {effect,reactive} = VueReactivity;
const state = reactive({name:"david",age:12,address:{num:567}})
//set 和 map 也可以劫持
effect(() => {
document.getElementById("app").innerHTML = `${state.name}今年${state.age}`
})
setTimeout(() =>{
state.age = 13
},1000)
</script>
</body>
</html>
为功能划分文件
在reactivity/src/
下新建effect.ts和reactive.ts文件,对应上面html的2个功能。
# reactive.ts
export function reactive() {}
# effect.ts
export function effect() {}
同时在index.ts中抛出这2个函数
import { effect } from './effect'
import { reactive } from './reactive'
export { effect, reactive }
这样html中引入编译好的JS文件就能获取这2个函数了。
-
编写reactive功能
import { isObject } from '@vue/shared' // 将数据转化成响应式数据,只能做对象的代理 export function reactive(target: object) { if (!isObject(target)) { return } // 并没有重新定义属性,只是代理,在取值的时候会调用get,同理赋值调用set const proxy = new Proxy(target, { get(target, key, recevier) { return target[key] }, set(target, value, key, recevier) { target[key] = value return true }, }) return proxy }
上面定义个了proxy代理对象,但是为啥不能如上图编写。看下面的解析
# 代码省略,和上面的一致 let target = { name : "java", get alias(){ //属性访问器写法 es5 console.log(this); // this { name: 'java', alias: [Getter] } return this.name; } } const proxy = new Proxy(target, { get(target, key, recevier) { // return target[key] ; // 这里控制台会打印出 alias name console.log(key) return Reflect.get(target,key,recevier) }, set(target, value, key, recevier) { target[key] = value return true }, }) proxy.alias // 通过proxy.alias,触发了get,然后return target[key],这个时候访问的是原对象target,traget又访问alias,alias访问了name,但是这个this是源对象,监控不到name,所以引入Reflect(反射)对象,这样访问alias,就回去代理对象上取值,这个时候this就变成了代理对象,那么this.name就又走一次get,这样name就被监控到。 recevier的作用是改变this指向
经过上面的修改,初步得到了一个代理对象的方法。此时如果用户在使用上面的代码的时候,他是这么写的
const {effect,reactive} = VueReactivity; let target = {name:"david",age:13,address:{num:134}}; let p1 = reactive(target) let p2 = reactive(target) console.log(p1 === p2) //打印出false,因为每次都new了一个新的Porxy();
那么实际上这2个应该使用一个对象的,为此我们修改一下上面的代码,增加缓存设置,这里用上了
WeakMap
。弱链接Map,好处在于key为null自动清空对应映射关系,其二是key只能为对象。修改上面的代码为import { isObject } from '@vue/shared' // 将数据转化成响应式数据,只能做对象的代理 export function reactive(target: object) { import { isObject } from '@vue/shared' const reactiveMap = new WeakMap() // key只能是对象 // 将数据转化成响应式数据,只能做对象的代理 export function reactive(target: object) { if (!isObject(target)) { return } let existingProxy = reactiveMap.get(target) if (existingProxy) { return existingProxy } // 并没有重新定义属性,只是代理,在取值的时候会调用get,同理赋值调用set const proxy = new Proxy(target, { get(target, key, recevier) { // return target[key] console.log(key) return Reflect.get(target, key, recevier) }, set(target, value, key, recevier) { // target[key] = value // return true return Reflect.set(target, key, value, recevier) }, }) reactiveMap.set(target, proxy) return proxy }
这个时候reactive就有了同一个对象代理多次,返回同一个代理。现在又有个新需求,如果代理再一次被代理,那应该返回代理,而不是代理的代理对象。
let target = {name:"david",age:12,address:{num:567}} const state = reactive(target) const state2 = reactive(state) console.log(state === state2) //false
那么怎么让判断为true呢,早期的处理方式是,
WeakMap
,正方向存一次,反方向存一次就像
target
->proxy
proxy
->target
最新的处理方法是定一个枚举变量。当你你传入的是proxy的时候,可以看一下时候代理过,如果有,那么他一定走到了get方法,并且我们访问了ReactiveFlags.IS_RECEIVE
,那么就表示这个是被代理过的,就直接返回 target。import { isObject } from '@vue/shared' const reactiveMap = new WeakMap() // key只能是对象 const enum ReactiveFlags { IS_RECEIVE = `__v_isReactive`, } // 将数据转化成响应式数据,只能做对象的代理 // 同一个对象被代理多次返回同一个代理 // 代理再次被代理,返回原代理 export function reactive(target: object) { if (!isObject(target)) { return } // if (target[ReactiveFlags.IS_RECEIVE]) { return target } let existingProxy = reactiveMap.get(target) if (existingProxy) { return existingProxy } // 并没有重新定义属性,只是代理,在取值的时候会调用get,同理赋值调用set const proxy = new Proxy(target, { // 第一次是普通对象,只是代理,在取值的时候会调用get // 下一次你传入的是proxy的时候,可以看一下时候代理过,如果有,那么他一定走到了get方法,并且我们访问了ReactiveFlags.IS_RECEIVE, // 那么就表示这个是被代理过的,就直接返回 target get(target, key, recevier) { // return target[key] if (key == ReactiveFlags.IS_RECEIVE) { return true } console.log(key) return Reflect.get(target, key, recevier) }, set(target, value, key, recevier) { // target[key] = value // return true return Reflect.set(target, key, value, recevier) }, }) reactiveMap.set(target, proxy) return proxy }
评论区