Skip to content
New issue

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

2.vue3响应式原理 #33

Open
Zijue opened this issue Jul 21, 2021 · 0 comments
Open

2.vue3响应式原理 #33

Zijue opened this issue Jul 21, 2021 · 0 comments
Labels

Comments

@Zijue
Copy link
Owner

Zijue commented Jul 21, 2021

Vue3中reactivity模块的用法

将需要变成响应式的数据通过reactive, shallowReactive, readonly, shallowReadonly中任一方法包装后返回proxy对象,当包装后的proxy对象在effect中取值时会依赖收集,当赋值时,会重新执行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的使用效果,四个响应式方法区别如下:

  • reactive:会将对象里的所有对象属性都进行进行代理
  • shallowReactive:只代理第一层对象
  • readonly:会代理对象,但是属性不能修改,同时不进行依赖收集节约性能
  • shallowReadonly:因为外层没有收集依赖,虽然内层能改但是不会更新视图

Vue3响应式原理的实现

响应式模块结构的初步搭建

目录结构如下:

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';跨模块引入会出现如下错误:

按照提示,需要在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 = {
    
}

对同一对象多次调用reactive的处理

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

为什么不用map而用weakMap

  • weakMap:key 只能是对象;weakMap是弱引用,如果对象key被销毁,weakMap可以自动释放掉
  • map:key 可以是其它类型

通过查看浏览器内存快照查看两者区别:

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中的get、set的生成

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方法的处理逻辑

在完善get方法之前,我们先介绍一个API:Reflect。是用于在proxy对象中操作的最佳拍档,通过下面一个例子进行说明:

例子原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逻辑如下:

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方法的处理逻辑

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函数的实现

先来看看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.nameproxy.address收集的是外层的effect,而proxy.age收集的是里层的effect。不难想到这种类似于函数调用的形式,最好就是用来维护属性与effect的关系。

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重新渲染视图,所以需要在取值时做依赖收集。

需要实现的逻辑是:

  1. 当用户取值的时候,需要将activeEffect和属性做关联;
  2. 当用户更改属性值的时候,要通过属性找到effect从新执行。

我们需要两个函数:一个依赖收集函数track,一个触发effect函数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;
    }
}

修复数组更新方法的bug

需要说明的是,对数组的使用需要JSON.stringify(proxy.arr)包裹,当调用JSON.stringify( )的时候,会访问数组中每一个属性,包括length。否则proxy.arr这样只是对arr属性进行了收集,arr是一个数组(引用类型),修改arr的值就不会触发此effect重新执行。

目前我们写的代码对于数组的操作还有些bug:

  • 修改数组的length未触发effect重新执行
        effect(()=>{
            console.log(proxy.arr[2]);
        });
        proxy.arr.length = 1; // 未收集length属性,修改length一样需要触发

如上述代码:只对数组下标2属性做了依赖收集,未对length属性依赖收集,所以当修改length属性已经影响到数组的取值时依旧不会重新执行effect。对于这种情况,我们需要去手动触发:

reactivity/src/effects.ts

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方法时未触发effect

未触发的原因:当数组push值时,走trigger(target, 'add', key, value)新增触发逻辑,key为Int且数组未收集;所以此处也需要手动更新。

reactivity/src/effects.ts

    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的依赖收集
                }
        }
    }

源码地址

@Zijue Zijue added the vue3 label Jul 24, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant