We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
将需要变成响应式的数据通过reactive, shallowReactive, readonly, shallowReadonly中任一方法包装后返回proxy对象,当包装后的proxy对象在effect中取值时会依赖收集,当赋值时,会重新执行effect。
reactive
shallowReactive
readonly
shallowReadonly
proxy
effect
<!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 src="node_modules/@vue/reactivity/dist/reactivity.global.js"></script> <script> let { reactive, shallowReactive, readonly, shallowReadonly, effect } = VueReactivity; let zijue = { name: 'zijue', age: 18, address: { city: 'wh' } }; let proxy = reactive(zijue); // 包装返回proxy对象,当在effect中取值时会进行依赖收集,当赋值时,会重新执行effect // effect 会默认执行,执行时会手机属性的依赖;watch computed都是基于这个effect来实现的 effect(()=>{ app.innerHTML = proxy.name + ': ' + proxy.age + ': ' + proxy.address.city; }); setTimeout(()=>{ proxy.name = 'xiaochi'; // 一秒后修改name属性 }, 1000); setTimeout(()=>{ proxy.address.city = 'xg'; // 两秒后修改address.city属性 }, 2000); </script> </body> </html>
上面展示的是reactive的使用效果,四个响应式方法区别如下:
目录结构如下:
packages # 模块根路径 ├── reactivity # 响应式模块 │ ├── package.json │ └── src │ ├── handlers.ts # 存放proxy具体处理逻辑 │ ├── index.ts # 包入口 │ └── reactive.ts # 存放响应式方法 └── shared # 共享方法模块 ├── package.json └── src └── index.ts # 包入口
packages/reactivity/src/index.ts
export * from './reactive';
packages/reactivity/src/reactive.ts
import { isObject } from '@vue/shared'; import { mutableHandler, readonlyHandlers, shallowReactiveHandlers, shallowReadonlyHanlders } from "./handlers"; export function reactive(target){ return createReactiveObject(target, false, mutableHandler); } export function shallowReactive(target){ return createReactiveObject(target, false, shallowReactiveHandlers); } export function readonly(target){ return createReactiveObject(target, true, readonlyHandlers); } export function shallowReadonly(target){ return createReactiveObject(target, true, shallowReadonlyHanlders); } function createReactiveObject(target, isReadonly, baseHandler){ /** * target 创建代理的目标 * isReadonly 是否为只读 * baseHandler 针对不同的方式创建不同的代理对象 */ if(!isObject(target)){ return target; } // 如果是对象,就做代理 new Proxy let proxy = new Proxy(target, baseHandler); return proxy; }
其中import { isObject } from '@vue/shared';跨模块引入会出现如下错误:
import { isObject } from '@vue/shared';
按照提示,需要在tsconfig.json中配置:
tsconfig.json
"moduleResolution": "node", "baseUrl": "./", "paths": { "@vue/*": [ "packages/*/src" ] },
packages/reactivity/src/handlers.ts
// 此文件负责proxy中get、set的具体实现 export const mutableHandler = { } export const shallowReactiveHandlers = { } export const readonlyHandlers = { } export const shallowReadonlyHanlders = { }
let { reactive, shallowReactive, readonly, shallowReadonly, effect } = VueReactivity; let zijue = { name: 'zijue', age: 18, address: { city: 'wh' } }; let proxy = reactive(zijue); let proxy = reactive(zijue); // 多次调用
如上面这段代码,当对同一对象多次调用响应式函数时,我们希望只做一次代理,后续调用,直接走缓存查找并返回。 那么如何处理呢?答案是:weakMap。
weakMap
为什么不用map而用weakMap?
map
通过查看浏览器内存快照查看两者区别:
function User() { } let user = new User(); let map = new Map(); map.set(user, 1); user = null;
function User() { } let user = new User(); let map = new WeakMap(); map.set(user, 1); user = null;
所以最终处理如下:
// 添加缓存 const reactiveMap = new WeakMap(); const readonlyMap = new WeakMap(); // reactive与readonly代理对象的结果是不一样的,所以需要将两者分别缓存 function createReactiveObject(target, isReadonly, baseHandler) { if (!isObject(target)) { return target; } let proxyMap = isReadonly ? readonlyMap : reactiveMap; let existProxy = proxyMap.get(target); if (existProxy) { return existProxy; } // 如果是对象,就做代理 new Proxy let proxy = new Proxy(target, baseHandler); proxyMap.set(target, proxy); return proxy; }
proxy处理器handler最终就是为了实现不同get与set方法(是否只读、是否浅代理),故将get与set的创建提取出来封装:
function createGetter(isReadonly = false, isShallow = false) { return function get(target, key, receiver) { /** * target 代理的源对象 * key 取值的属性 * receiver 代理对象 */ console.log('proxy getter'); } } function createSetter(isShallow = false) { return function set(target, key, value, receiver) { console.log('proxy setter'); } } const get = createGetter(); // 非只读,非浅代理 const shallowGet = createGetter(false, true); // 非只读,浅代理 const readonlyGet = createGetter(true); // 只读,非浅代理 const shallowReadonlyGet = createGetter(true, true); // 只读,浅代理 // readonly只读没有set const set = createSetter(); // 非浅代理 const shallowSet = createSetter(true); // 浅代理 export const mutableHandler = { get, set } export const shallowReactiveHandlers = { get: shallowGet, set: shallowSet } const readonlySet = { set(target, key, value, receiver) { throw Error(`Proxy '${JSON.stringify(target)}' that property '${key}' is a read-only, cannot set '${key}' to '${value}'.`); } } /** Object.assign 方法用于将所有可枚举属性的值从一个或多个源对象分配到目标对象。它将返回目标对象 * const target = { a: 1, b: 2 }; * const source = { b: 4, c: 5 }; * const returnedTarget = Object.assign(target, source); * console.log(target); // expected output: Object { a: 1, b: 4, c: 5 } * console.log(returnedTarget); // expected output: Object { a: 1, b: 4, c: 5 } */ const extend = Object.assign; // 可以作为公用方法放入shared模块中 export const readonlyHandlers = extend({ get: readonlyGet }, readonlySet); export const shallowReadonlyHanlders = extend({ get: shallowReadonlyGet }, readonlySet);
在完善get方法之前,我们先介绍一个API:Reflect。是用于在proxy对象中操作的最佳拍档,通过下面一个例子进行说明:
Reflect
例子原blog地址
const target = { get foo() { return this.bar; }, bar: 3 }; const handler = { get(target, propertyKey, receiver) { if (propertyKey === 'bar') return 2; console.log('Reflect.get ', Reflect.get(target, propertyKey, receiver)); // this in foo getter references Proxy instance; logs 2 console.log('target[propertyKey] ', target[propertyKey]); // this in foo getter references "target" - logs 3 } }; const obj = new Proxy(target, handler); console.log(obj.bar); // 2 obj.foo; // Reflect.get 2 // target[propertyKey] 3
所以为了正确的代理原对象,我们需要在proxy中使用Reflect。
取值部分的get逻辑如下:
get
function createGetter(isReadonly = false, isShallow = false) { return function get(target, key, receiver) { /** * target 代理的源对象 * key 取值的属性 * receiver 代理对象 */ // 一般使用Proxy会配合Reflect使用 const res = Reflect.get(target, key, receiver); if (!isReadonly) { // 不是只读属性,收集此属性用于之后值变化时更新视图 console.log('收集当前属性,之后属性值改变,更新视图', key); } if (isShallow) { // 浅代理,只代理第一层属性,更深层次不做处理 return res; } if (isObject(res)) { // 懒代理;当我们取值时才去做递归代理,如果不取值默认只代理一层 return isReadonly ? readonly(res) : reactive(res); } return res; } }
set方法主要是需要知道是增加值还是修改值,便于后续逻辑的执行;同时处理好数组新增带来的length属性修改的二次触发问题。代码如下:
set
length
function createSetter(isShallow = false) { /** 针对数组而言,如果调用push方法,就会产生两次触发 * 1.第一次给数组新增了一项,同时也修改了长度 * 2.因为修改了长度,所以第二次触发set(此次触发是无意义的) */ return function set(target, key, value, receiver) { // 设置属性:新增、修改 const oldValue = Reflect.get(target, key, receiver); // 获取老值 /** 如何判断数组是新增还是修改 * key是数字 && key < target.length => 新增 * 否则就是修改 */ let hasKey = isArray(target) && isIntegerKey(key) ? Number(key) < target.length : hasOwnProp(target, key); const res = Reflect.set(target, key, value, receiver); // 必须先判断是否有key,再更改值 if (!hasKey) { console.log('新增'); } else if (hasChanged(oldValue, value)) { console.log('修改'); } else { console.log('无变化'); // 数组第二次触发会在此处,无意义,所以此处不添加逻辑 } return res; } }
先来看看effect函数的使用效果:
let zijue = { name: 'zijue', age: 18, address: { city: 'wh' }, arr: [1, 2, 3] }; let proxy = reactive(zijue); effect(() => { // 1.默认effect中函数会执行一次,执行的时候应该把用到的属性和这个effect关联起来 console.log(proxy.name); }); // 2.下次更新属性的时候,会再次执行这个effect
那么初步搭建一下effect函数的结构,effect肯定是一个高级函数,返回一个响应式的effect,同时可以通过参数控制默认是否执行的行为。如下:
export function effect(fn, options: any = {}) { const effect = createReactiveEffect(fn, options); if (!options.lazy) { effect(); } return effect; } function createReactiveEffect(fn, options) { const effect = function reactiveEffect() { console.log('effect'); } return effect; }
初步结构构建后,首选需要解决effect嵌套的问题:
effect(() => { console.log(proxy.name); effect(() => { console.log(proxy.age); }); console.log(proxy.address); })
对于上面这段代码,我们肯定是希望proxy.name和proxy.address收集的是外层的effect,而proxy.age收集的是里层的effect。不难想到这种类似于函数调用的形式,最好就是用栈来维护属性与effect的关系。
proxy.name
proxy.address
proxy.age
export function effect(fn, options: any = {}) { const effect = createReactiveEffect(fn, options); if (!options.lazy) { effect(); } return effect; } let activeEffect; // 当前调用的effect const effectStack = []; let id = 0; function createReactiveEffect(fn, options) { const effect = function reactiveEffect() { try { effectStack.push(effect); activeEffect = effect; return fn(); } finally { // 返回值后最终也会执行的逻辑 effectStack.pop(); activeEffect = effectStack[effectStack.length - 1]; } } effect.id = id++; return effect; }
目前,effect中传入的函数执行时,会去取值,但是当修改值,没有办法再次执行effect重新渲染视图,所以需要在取值时做依赖收集。
需要实现的逻辑是:
我们需要两个函数:一个依赖收集函数track,一个触发effect函数trigger
track
trigger
reactivity/src/effects.ts
// 一个属性对应多个effect,一个effect对应多个属性 ==> 多对多的关系 const targetMap = new WeakMap(); export function track(target, action, key) { /** targetMap = WeakMap{ target: Map{ key: Set(Effect1, Effect2) } } */ if (activeEffect == undefined) { return; // 用户只是取值,而且这个值不是在effect中使用的,什么都不用收集 } let depsMap = targetMap.get(target); if (!depsMap) { targetMap.set(target, (depsMap = new Map())); } let dep = depsMap.get(key); if (!dep) { depsMap.set(key, (dep = new Set())); } if (!dep.has(activeEffect)) { dep.add(activeEffect); } } export function trigger(target, action, key, newValue, oldValue?) { // 去映射表中找到属性对应的effect,让其重新执行 const depsMap = targetMap.get(target); if (!depsMap) return; // 只是改了属性,这个属性没有在effect中使用 const effectSet = new Set(); const add = (effects) => { // 如果同时有多个属性依赖的effect是同一个,new Set()会去重 if (effects) { effects.forEach(effect => effectSet.add(effect)); } } add(depsMap.get(key)); // 将属性收集effects添加到统一的集合中 effectSet.forEach((effect: any) => effect()); // 遍历所有收集的effect并执行 }
reactivity/src/handlers.ts
function createGetter(isReadonly = false, isShallow = false) { return function get(target, key, receiver) { const res = Reflect.get(target, key, receiver); if (!isReadonly) { track(target, 'get', key); // 依赖收集 } if (isShallow) { return res; } if (isObject(res)) { return isReadonly ? readonly(res) : reactive(res); } return res; } } function createSetter(isShallow = false) { return function set(target, key, value, receiver) { let hasKey = isArray(target) && isIntegerKey(key) ? Number(key) < target.length : hasOwnProp(target, key); const res = Reflect.set(target, key, value, receiver); if (!hasKey) { trigger(target, 'add', key, value, oldValue); // 新增触发 } else if (hasChanged(oldValue, value)) { trigger(target, 'set', key, value, oldValue); // 修改触发 } return res; } }
需要说明的是,对数组的使用需要JSON.stringify(proxy.arr)包裹,当调用JSON.stringify( )的时候,会访问数组中每一个属性,包括length。否则proxy.arr这样只是对arr属性进行了收集,arr是一个数组(引用类型),修改arr的值就不会触发此effect重新执行。
JSON.stringify(proxy.arr)
JSON.stringify( )
proxy.arr
arr
目前我们写的代码对于数组的操作还有些bug:
effect(()=>{ console.log(proxy.arr[2]); }); proxy.arr.length = 1; // 未收集length属性,修改length一样需要触发
如上述代码:只对数组下标2属性做了依赖收集,未对length属性依赖收集,所以当修改length属性已经影响到数组的取值时依旧不会重新执行effect。对于这种情况,我们需要去手动触发:
2
export function trigger(target, action, key, newValue, oldValue?) { // 去映射表中找到属性对应的effect,让其重新执行 const depsMap = targetMap.get(target); if (!depsMap) return; // 只是改了属性,这个属性没有在effect中使用 const effectSet = new Set(); const add = (effects) => { // 如果同时有多个属性依赖的effect是同一个,new Set()会去重 if (effects) { effects.forEach(effect => effectSet.add(effect)); } } if (key === 'length' && isArray(target)) { // 当修改的是数组的length属性时,需要视情况触发更新 depsMap.forEach((deps, key) => { if (key > newValue || key === 'length') { // 此处的key是收集依赖的属性 add(deps); // 当收集的依赖的属性 < 更改的数组长度时,需要手动触发 } }); } else { add(depsMap.get(key)); // 将属性收集effects添加到统一的集合中 } effectSet.forEach((effect: any) => effect()); // 遍历所有收集的effect并执行 }
push
未触发的原因:当数组push值时,走trigger(target, 'add', key, value)新增触发逻辑,key为Int且数组未收集;所以此处也需要手动更新。
if (key === 'length' && isArray(target)) { // 当修改的是数组的length属性时,需要视情况触发更新 depsMap.forEach((deps, key) => { if (key > newValue || key === 'length') { // 此处的key是收集依赖的属性 add(deps); // 当收集的依赖的属性 < 更改的数组长度时,需要手动触发 } }); } else { add(depsMap.get(key)); // 将属性收集effects添加到统一的集合中 // 当数组push值时,走trigger(target, 'add', key, value)新增触发逻辑,key为Int且数组未收集;所以此处也需要手动更新 switch (action) { case 'add': if (isArray(target) && isIntegerKey(key)) { add(depsMap.get('length')); // 增加属性,需要触发length的依赖收集 } } }
The text was updated successfully, but these errors were encountered:
No branches or pull requests
Vue3中reactivity模块的用法
将需要变成响应式的数据通过
reactive
,shallowReactive
,readonly
,shallowReadonly
中任一方法包装后返回proxy
对象,当包装后的proxy
对象在effect
中取值时会依赖收集,当赋值时,会重新执行effect
。上面展示的是
reactive
的使用效果,四个响应式方法区别如下:reactive
:会将对象里的所有对象属性都进行进行代理shallowReactive
:只代理第一层对象readonly
:会代理对象,但是属性不能修改,同时不进行依赖收集节约性能shallowReadonly
:因为外层没有收集依赖,虽然内层能改但是不会更新视图Vue3响应式原理的实现
响应式模块结构的初步搭建
目录结构如下:
其中
import { isObject } from '@vue/shared';
跨模块引入会出现如下错误:按照提示,需要在
tsconfig.json
中配置:对同一对象多次调用
reactive
的处理如上面这段代码,当对同一对象多次调用响应式函数时,我们希望只做一次代理,后续调用,直接走缓存查找并返回。
那么如何处理呢?答案是:
weakMap
。为什么不用
map
而用weakMap
?weakMap
:key 只能是对象;weakMap是弱引用,如果对象key被销毁,weakMap可以自动释放掉map
:key 可以是其它类型通过查看浏览器内存快照查看两者区别:
所以最终处理如下:
完善proxy中的get、set的生成
proxy处理器handler最终就是为了实现不同get与set方法(是否只读、是否浅代理),故将get与set的创建提取出来封装:
补充get方法的处理逻辑
在完善get方法之前,我们先介绍一个API:
Reflect
。是用于在proxy
对象中操作的最佳拍档,通过下面一个例子进行说明:所以为了正确的代理原对象,我们需要在
proxy
中使用Reflect
。取值部分的
get
逻辑如下:补充set方法的处理逻辑
set
方法主要是需要知道是增加值还是修改值,便于后续逻辑的执行;同时处理好数组新增带来的length
属性修改的二次触发问题。代码如下:effect函数的实现
先来看看
effect
函数的使用效果:那么初步搭建一下
effect
函数的结构,effect
肯定是一个高级函数,返回一个响应式的effect
,同时可以通过参数控制默认是否执行的行为。如下:初步结构构建后,首选需要解决
effect
嵌套的问题:对于上面这段代码,我们肯定是希望
proxy.name
和proxy.address
收集的是外层的effect
,而proxy.age
收集的是里层的effect
。不难想到这种类似于函数调用的形式,最好就是用栈来维护属性与effect
的关系。依赖收集的原理
目前,
effect
中传入的函数执行时,会去取值,但是当修改值,没有办法再次执行effect
重新渲染视图,所以需要在取值时做依赖收集。需要实现的逻辑是:
我们需要两个函数:一个依赖收集函数
track
,一个触发effect函数trigger
修复数组更新方法的bug
目前我们写的代码对于数组的操作还有些bug:
length
未触发effect
重新执行如上述代码:只对数组下标
2
属性做了依赖收集,未对length
属性依赖收集,所以当修改length
属性已经影响到数组的取值时依旧不会重新执行effect
。对于这种情况,我们需要去手动触发:push
方法时未触发effect
未触发的原因:当数组push值时,走trigger(target, 'add', key, value)新增触发逻辑,key为Int且数组未收集;所以此处也需要手动更新。
源码地址
The text was updated successfully, but these errors were encountered: