前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Vue3 从ref 函数入手透彻理解响应式原理

Vue3 从ref 函数入手透彻理解响应式原理

作者头像
用户7413032
发布2022-03-09 20:49:10
1.5K0
发布2022-03-09 20:49:10
举报
文章被收录于专栏:佛曰不可说丶佛曰不可说丶

前言

vue3从发布开始已经有一年有余,近来开始撸源码,真是惭愧至极,啥也别说了,洗心革面 开干!直接上源码枯燥乏味

这里仅仅是我自己的理解响应式原理之后的简版代码

目标

我们今天的目标

  • 1、通过从ref 入手,彻底的了解响应式的原理
  • 2、理解effect 的副作用函数是怎么响应式执行的

ref 函数的原理

首先我们来看看ref官方文档是怎么解释ref 函数的

代码语言:javascript
复制
接受一个内部值并返回一个响应式且可变的 ref 对象。ref 对象具有指向内部值的单个 property.value。

通俗的将其实就是当前的ref 函数返回的值就是一个对象,这个对象包含get 和set ,转换成es5 就是Object.defineProperty 监听的一个值

废话少说,看代码

代码语言:javascript
复制
// 判断是不是对象
const isObject = (val) => val !== null && typeof val === 'object';

// ref的函数
function ref(val) {
  // 此处源码中为了保持一致,在对象情况下也做了用value 访问的情况value->proxy对象
  // 我们在对象情况下就不在使用value 访问
  return isObject(val) ? reactive(val) : new refObj(val);
}

//创建响应式对象
class refObj {
  constructor(val) {
    this._value = val;
  }
  get value() {
    // 在第一次执行之后触发get来收集依赖
    track(this, 'value');
    return this._value;
  }
  set value(newVal) {
    console.log(newVal);
    this._value = newVal;
    trigger(this, 'value');
  }
};

看了上述代码我们发现,其实当前的这个神奇的响应式的值,就是一个对象 ,当你改变这个值的时候,就会触发当前这个对象的get 和set 从而达到响应式的能力

接下来发现是个对象就好办了,我们就能在get 和set 的方法中去做一些事情,比如建立副作用和当前这个值的关系,也就是依赖收集

但是此时又会有个问题,如果在ref 中传入一个对象,new 当前这个对象的时候,就不好使了,因为里面的值就监听不到了

于是vue3中大名鼎鼎的Proxy登场了

Proxy

具体的使用方法,咱就不介绍,vue3出来这么长时间了, 相信大家都明白他的特性,直接上代码

代码语言:javascript
复制
// 对象的响应式处理 在这里我们为了理解原理原理暂时不考虑对象里嵌套对象的情况
// 其实对象的响应式处理也就是重复执行reactive 
function reactive(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      // Reflect用于执行对象默认操作,更规范、函数式
      // Proxy和Object的方法Reflect都有对应
      const res = Reflect.get(target, key, receiver);
      track(target, key);
      return res;
    },
    set(target, key, value, receiver) {
      const res = Reflect.set(target, key, value, receiver);
      trigger(target, key);
      return res;
    },
    deleteProperty(target, key) {
      const res = Reflect.deleteProperty(target, key);
      trigger(target, key);
      return res;
    }
  });
}

上述代码中,将对象类型的也变成了响应式对象,接下来就是重点的地方了,要在这两个响应式的对象的get 和set 中去做依赖收集,和派发对应的副作用更新

既然需要副作用,那么怎么也要先收集一下吧,于是effect相当于桥梁函数

effect实现

总的来说这个effect 做了什么事情呢?他其实就是对当前的副作用函数进行包装,然后执行,触发副作用函数中的get,在get中在收集当前副作用,代码如下

代码语言:javascript
复制
// 保存临时依赖函数用于包装
const effectStack = [];

// 在源码中为为了方法的通用性,他还传入了很多参数用于兼容不同情况
// 我们意在理解原理,只需要包装fn 即可
function effect(fn) {
  // 包装当前依赖函数
  const effect = function reactiveEffect() {
    // 模拟源码中也加入错误处理,为了避免你瞎写出现错误的情况,这就是框架的高明之处
    if (!effectStack.includes(effect)) {
      try {
        // 给当前函数放入临时栈中,为在下面执行中,触发get,在依赖收集中能找到当前变量的依赖项来建立关系
        effectStack.push(fn);
        // 执行当前函数,开始依赖收集了
        return fn();
      } finally {
        // 执行成功了出栈
        effectStack.pop();
      }
    };
  };

  effect();
}

他的原理比较巧妙,利用一个栈,将当前正在执行的副作用函数临时存储,在get中取出,存入依赖对象中。

那么如此一来,顺其自然的就需要有一个函数去收集依赖(这个依赖有可能是一个render 函数,也有可能是一个副作用,我们的例子中,由于没有涉及视图渲染相关,都是副作用),于是定义一个track 函数去收集依赖

track实现

废话少说上代码

代码语言:javascript
复制
// 依赖关系的map对象只能接受对象
let targetMap = new WeakMap();

//  在收集的依赖中建立关系
function track(target, key) {
  // 取出最后一个数据内容
  const effect = effectStack[effectStack.length - 1];
  // 如果当前变量有依赖
  if (effect) {
    //判断当前的map中是否有target
    let depsMap = targetMap.get(target);
    // 如果没有
    if (!depsMap) {
      // new map存储当前weakmap
      depsMap = new Map();
      targetMap.set(target, depsMap);
    }
    // 获取key对应的响应函数集
    let deps = depsMap.get(key);
    if (!deps) {
      // 建立当前key 和依赖的关系,因为一个key 会有多个依赖
      // 为了防止重复依赖,使用set
      deps = new Set();
      depsMap.set(key, deps);
    }
    // 存入当前依赖
    if (!deps.has(effect)) {
      deps.add(effect);
    }
  }
}

track就有讲究了,他其实是建立了当前的响应式对象的每一个key 和 依赖对应关系,从而当key 发生变换的时候通知所有的依赖更新,怕大家不太理解,贴心的画了张图供大家理解

image.png
image.png

根据图中结构,我们就能看到,所有依赖的数据结构

接下来我们就需要派发更新,使用trigger函数来处理

trigger实现

代码如下

代码语言:javascript
复制
// 用于触发更新
function trigger(target, key) {
  // 获取所有依赖内容
  const depsMap = targetMap.get(target);
  // 如果有依赖的话全部拉出来执行
  if (depsMap) {
    // 获取响应函数集合
    const deps = depsMap.get(key);
    if (deps) {
      // 执行所有响应函数 
      const run = (effect) => {
        // 源码中有异步调度任务,我们在这里省略
        effect();
      };
      deps.forEach(run);
    }
  }
}

从以上代码看就非常简单取出当前修改的key 对应的依赖,全部执行一下也就是所谓的派发更新,到这里基本响应式原理基本都结束了。就是这么简单且有趣!之后附上自己画的响应式流程图,供大家理解,不对之处请指点

image.png
image.png

最后

我自己所理解的vue的响应式模块到此全部完毕,当然源码中有这很多兼容处理,高端写法。我们这里只为研究原理,暂不深究如有兴趣请移步 reactivity模块,详细研究。结尾附上可以跑的完整源码,亲自尝试一下吧!

代码语言:javascript
复制
// 保存临时依赖函数用于包装
const effectStack = [];

// 依赖关系的map对象只能接受对象
let targetMap = new WeakMap();
// 判断是不是对象
const isObject = (val) => val !== null && typeof val === 'object';
// ref的函数
function ref(val) {
  // 此处源码中为了保持一致,在对象情况下也做了用value 访问的情况value->proxy对象
  // 我们在对象情况下就不在使用value 访问
  return isObject(val) ? reactive(val) : new refObj(val);
}

//创建响应式对象
class refObj {
  constructor(val) {
    this._value = val;
  }
  get value() {
    // 在第一次执行之后触发get来收集依赖
    track(this, 'value');
    return this._value;
  }
  set value(newVal) {
    console.log(newVal);
    this._value = newVal;
    trigger(this, 'value');
  }
};

// 对象的响应式处理 在这里我们为了理解原理原理暂时不考虑对象里嵌套对象的情况
// 其实对象的响应式处理也就是重复执行reactive 
function reactive(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      // Reflect用于执行对象默认操作,更规范、函数式
      // Proxy和Object的方法Reflect都有对应
      const res = Reflect.get(target, key, receiver);
      track(target, key);
      return res;
    },
    set(target, key, value, receiver) {
      const res = Reflect.set(target, key, value, receiver);
      trigger(target, key);
      return res;
    },
    deleteProperty(target, key) {
      const res = Reflect.deleteProperty(target, key);
      trigger(target, key);
      return res;
    }
  });
}

// 到此处,当前的ref 对象就已经实现了对数据改变的监听
const newRef = ref(0);
// 但是还是没有响应式的能力,那么他是怎样实现响应式的呢----依赖收集,触发更新=
// 用来做依赖收集 
// 在源码中为为了方法的通用性,他还传入了很多参数用于兼容不同情况
// 我们意在理解原理,只需要包装fn 即可
function effect(fn) {
  // 包装当前依赖函数
  const effect = function reactiveEffect() {
    // 模拟源码中也加入错误处理,为了避免你瞎写出现错误的情况,这就是框架的高明之处
    if (!effectStack.includes(effect)) {
      try {
        // 给当前函数放入临时栈中,为在下面执行中,触发get,在依赖收集中能找到当前变量的依赖项来建立关系
        effectStack.push(fn);
        // 执行当前函数,开始依赖收集了
        return fn();
      } finally {
        // 执行成功了出栈
        effectStack.pop();
      }
    };
  };

  effect();
}
//  在收集的依赖中建立关系
function track(target, key) {
  // 取出最后一个数据内容
  const effect = effectStack[effectStack.length - 1];
  // 如果当前变量有依赖
  if (effect) {
    //判断当前的map中是否有target
    let depsMap = targetMap.get(target);
    // 如果没有
    if (!depsMap) {
      // new map存储当前weakmap
      depsMap = new Map();
      targetMap.set(target, depsMap);
    }
    // 获取key对应的响应函数集
    let deps = depsMap.get(key);
    if (!deps) {
      // 建立当前key 和依赖的关系,因为一个key 会有多个依赖
      // 为了防止重复依赖,使用set
      deps = new Set();
      depsMap.set(key, deps);
    }
    // 存入当前依赖
    if (!deps.has(effect)) {
      deps.add(effect);
    }
  }
}
// 用于触发更新
function trigger(target, key) {
  // 获取所有依赖内容
  const depsMap = targetMap.get(target);
  // 如果有依赖的话全部拉出来执行
  if (depsMap) {
    // 获取响应函数集合
    const deps = depsMap.get(key);
    if (deps) {
      // 执行所有响应函数 
      const run = (effect) => {
        // 源码中有异步调度任务,我们在这里省略
        effect();
      };
      deps.forEach(run);
    }
  }
}
effect(() => {
  console.log(11111);
  // 在自己实现的effect中,由于为了演示原理,没有做兼容,不能来触发set,否则会死循环
  // vue源码中触发对effect中的做了兼容处理只会执行一次
  newRef.value;
});

 newRef.value++;
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2021/08/27 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 目标
  • ref 函数的原理
  • Proxy
  • effect实现
  • track实现
  • trigger实现
  • 最后
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档