Vue响应式原理深入分析

目录
  • 1.响应式数据和副作用函数
  • 2.响应式数据的基本实现
  • 3.设计一个完善的响应式系统

1.响应式数据和副作用函数

(1)副作用函数

副作用函数就是会产生副作用的函数。

function effect() {
    document.body.innerText = 'hello world.'
}

​ 当effect函数执行时,它会设置body的内容,而body是一个全局变量,除了effect函数外任何地方都可以访问到,也就是说effect函数的执行会对其他操作产生影响,即effect函数是一个副作用函数。

(2)响应式数据

const obj = { text: 'hello world.'};
function effect() {
    document.body.innerText = obj.text;
}
obj.text = 'text';

​ 当 obj.text = 'text' 这条语句执行之后,我们期望 document.body.innerText 的值也能够随之修改,这就是通常意义上的响应式数据。

2.响应式数据的基本实现

​ 上文中,对响应式数据进行描述的代码段,并未实现真正的响应式数据。而通过观察我们可以发现,要实现真正的响应式数据,我们需要对数据的读取和设置进行拦截。当有操作对响应式数据进行读取中,我们将其添加至一个依赖队列,当修改响应式数据的值时,将依赖队列中的操作依次取出,并执行。以下使用Proxy对该思路进行实现。

const bucket = new Set();
const data = { text: "hello world." };
const obj = new Proxy(data, {
  get(target, key) {
    bucket.add(effect);
    return target[key];
  },
  set(target, key, newVal) {
    target[key] = newVal;
    bucket.forEach((fn) => fn());
    return true;
  },
});
const body = {
  innerText: "",
};
function effect() {
  body.innerText = obj.text;
}
effect();
console.log(body.innerText); // hello world
obj.text = "text";
console.log(body.innerText); // text

​ 但是,该实现仍然存在缺陷,比如说只能通过effect函数的名字实现依赖收集。

3.设计一个完善的响应式系统

(1)消除依赖收集的硬绑定

​ 这里我们使用一个active变量来保存当前需要进行依赖收集的函数。

const bucket = new Set();
const data = { text: "hello world." };
let activeEffect; // 新增一个active变量
const obj = new Proxy(data, {
  get(target, key) {
    if (activeEffect) {
      bucket.add(activeEffect); // 添加active变量保存的函数
    }
    return target[key];
  },
  set(target, key, newVal) {
    target[key] = newVal;
    bucket.forEach((fn) => fn());
    return true;
  },
});
function effect(fn) {
  activeEffect = fn; // 将当前函数赋值给active变量
  fn();
}
const body = {
  innerText: "",
};
effect(() => {
  body.innerText = obj.text;
});
console.log(body.innerText); // hello world
obj.text = "text";
console.log(body.innerText); // text

​ 但是该设计仍然存在很多问题,比如说,当访问一个obj对象上并不存在的属性假设为val时,逻辑上并没有存在对obj.val的访问,因此该操作不会产生任何响应,但实际上,当val的值被修改后,传入effect的匿名函数会再次执行。

(2)基于属性的依赖收集

​ 上一个版本的响应式系统只能对拦截对象所有的get和set操作进行响应,并不能做到细粒度的控制。考虑针对属性进行依赖拦截,主要有三个角色,对象、属性和依赖方法。因此考虑修改bucket的结构,由原来的Set修改为WeakMap(target,Map(key,activeEffect));这样就可以针对属性进行细粒度的依赖收集了。

ps.使用WeakMap是因为WeakMap是对key的弱引用,不会影响垃圾回收机制的工作,当target对象不存在任何引用时,说明target对象已不被需要,这时target对象将会被垃圾回收。如果换成Map,即时target不存在任何引用,Map已然会保持对target的引用,容易造成内存泄露。

// bucket的数据结构修改为WeakMap
const bucket = new WeakMap();
const data = { text: "hello world." };
let activeEffect;
const obj = new Proxy(data, {
  get(target, key) {
    track(target, key);
    return target[key];
  },
  set(target, key, newVal) {
    target[key] = newVal;
    trigger(target, key);
  },
});
function track(target, key) {
  if (!activeEffect) {
    return;
  }
  let depsMap = bucket.get(target);
  if (!depsMap) {
    bucket.set(target, (depsMap = new Map()));
  }
  let deps = depsMap.get(key);
  if (!deps) {
    depsMap.set(key, (deps = new Set()));
  }
  deps.add(activeEffect);
}
function trigger(target, key) {
  const depsMap = bucket.get(target);
  if (!depsMap) return;
  const effects = depsMap.get(key);
  effects && effects.forEach((fn) => fn());
}
function effect(fn) {
  activeEffect = fn;
  fn();
}
const body = {
  innerText: "",
};
effect(() => {
  body.innerText = obj.text;
});
console.log(body.innerText); // hello world
obj.text = "text";
console.log(body.innerText); // text

(3)分支切换和cleanup

​ 对于一段三元运算符 obj.flag? obj.text : 'text',我们所期望的结果是,当obj.flag的值为false时,不会对obj.text属性进行响应操作。 如果是上面那段程序,当obj.flag的值为false时,操作obj.text仍然会进行相应操作,因为obj.text对应的依赖仍然存在。对此如果我们能够在每次的函数执行之前,将其从之前相关联的依赖集合中移除,就可以达到目的了。这里通过修改副作用函数来实现:

function effect(fn) {
  const effectFn = () => {
    // 在依赖函数执行之前,清除依赖函数之前的依赖项
    cleanup(effectFn);
    activeEffect = effectFn;
    fn();
  };
  // 给副作用函数添加一个deps数组用来收集和该副作用函数相关联的依赖
  effectFn.deps = [];
  effectFn();
}
// cleanup函数实现
function cleanup(effectFn) {
  for (let i = 0; i < effectFn.deps.length; i++) {
    const deps = effectFn.deps[i];
    deps.delete(effectFn);
  }
  effectFn.deps.length = 0;
}
function track(target, key) {
  if (!activeEffect) {
    return;
  }
  let depsMap = bucket.get(target);
  if (!depsMap) {
    bucket.set(target, (depsMap = new Map()));
  }
  let deps = depsMap.get(key);
  if (!deps) {
    depsMap.set(key, (deps = new Set()));
  }
  deps.add(activeEffect);
  activeEffect.deps.push(deps); // 在这里收集相关联的依赖
}
function trigger(target, key) {
  const depsMap = bucket.get(target);
  if (!depsMap) return;
  const effects = depsMap.get(key);
  const effectToRun = new Set(effects); // 这里需要创建一个新集合来遍历,因为foreach循环会对新加入序列的元素也执行遍历,若遍历直接原集合,会出现死循环。
  effectToRun.forEach((fn) => fn());
}

(4)嵌套effect

​ 虽然我们已经解决了很多问题,但是作为响应式系统中比较常见的场景之一的嵌套,我们还不能实现。因为我们定义的activeEffect是一个变量,当嵌套操作时,无论怎样,最后activeEffect变量中存放的都是操作的最后一个副作用函数。这里,我们通过加入一个effect栈的方式,来给这套响应式系统添加嵌套功能。

// 定义一个effect栈
const effectStack = [];
function effect(fn) {
  const effectFn = () => {
    cleanup(effectFn);
    activeEffect = effectFn;
    effectStack.push(effectFn); // 在effect执行之前,放入栈中
    fn();
    effectStack.pop(); // 执行完毕立即弹出
    activeEffect = effectStack[effectStack.length - 1]; // activeEffect指向新的effect
  };
  effectFn.deps = [];
  effectFn();
}

(5)避免产生死循环

​ 试看obj.val++这条语句,它实际上相当于obj.val = obj.val+1,也就是进行了一次读取操作和一次赋值操作,共两次操作。而若将该操作运行在我们前面的响应式系统中,它将会产生死循环,因为当我们进行了读取操作后,会立即进行赋值操作,而在赋值操作中,读取操作再次被触发,然后循环的执行这一系列操作。这里我们在trigger函数中判断trigger触发的副作用函数,是否与当前正在执行的副作用函数相同,若相同,则不执行当前副作用函数。这样就能避免无限递归调用,避免内存溢出。

function trigger(target, key) {
  const depsMap = bucket.get(target);
  if (!depsMap) return;
  const effects = depsMap.get(key);
  const effectToRun = new Set();
  effects &&
    effects.forEach((fn) => {
      // 若正在执行的副作用函数与当前触发的副作用函数相同,则不执行
      if (fn !== activeEffect) {
        effectToRun.add(fn);
      }
    });
  effectToRun.forEach((fn) => fn());
}

(6)实现调度实行

现在要实现一个这样的效果:

effect(()=> {
    console.log(obj.val);
});
obj.val ++;
console.log("结束");

// 这段代码本来会输出的结果是:
/**
    1
    2
    结束
 **/
// 现在我们想让它变成
/**
     1
     结束
     2
 **/

这里我们可以通过给effect函数添加一个配置项来实现:

effect(
  ()=> {
    console.log(obj.val);
  },
  {
      scheduler(fn) {
          setTimeout(fn);
      }
  }
function effect(fn,options = {}) {
    const effectFn = ()=> {
       ...
    }
    effectFn.deps = [];
    effectFn.options = options; // 为副作用函数添加配置项
    effectFn();
}
function trigger(target, key) {
  const depsMap = bucket.get(target);
  if (!depsMap) return;
  const effects = depsMap.get(key);
  const effectToRun = new Set();
  effects &&
    effects.forEach((fn) => {
      if (fn !== activeEffect) {
        effectToRun.add(fn);
      }
    });
  effectToRun.forEach((fn) => {
    // 若当前依赖函数含有调度执行,将当前函数传递给调度函数执行
    if (fn.options.scheduler) {
      fn.options.scheduler(fn); //将当前函数传递给调度函数
    } else {
      fn();
    }
  });
}

如果还要实现一下效果:

effect(()=> {
    console.log(obj.val);
});
obj.val ++;
obj.val ++;
// 这段代码本来会输出的结果是:
/**
	1
	2
	3
 **/
// 现在我们想让它变成
/**
 	1
 	3
 **/

这里通过添加一个任务执行队列来实现:

const jobQueue = new Set();
const p = Promise.resolve();
let isFlushing = false;
effect(
    ()=> {
        console.log(obj.val);
    },
    {
        scheduler(fn){
            jobQueue.add(fn);
            flushJob();
        }
    }
);
function flushJob() {
    if(isFlushing) return;
    isFlushing = true;
    p.then(()=> {
        jobQueue.forEach(job=>job());
    }).finally(()=> {
        isFlushing = false;
    })
}

​ 像这样,由于Set保证了任务的唯一性,也就是jobQueue中只会保存唯一的一个任务,即当前执行的任务。而isFlushing标记则保证任务只会执行一次。而因为通过Promise将任务添加到了微任务队列中,当任务最后执行的时候,obj.val的值已经是3了。

(7)computed和lazy

​ 计算属性是vue中一个比较有特色的属性,它会缓存表达式的计算结果,只有当表达式依赖的变量发生变化时,它才会进行重新计算。实现计算属性的前提是实现懒加载标记,这里我们可以通过之前effect函数的配置项来实现。

effect(
	()=> {
        return ()=>obj.val * 2;
    },
    {
        lazy: true; // 设置 lazy 标记
    }
);
effect(fn, options = {}) {
  const effectFn = () => {
    cleanup(effectFn);
    activeEffect = effectFn;
    effectStack.push(effectFn);
    const res = fn();
    effectStack.pop();
    activeEffect = effectStack[effectStack.length - 1];
    return res;
  };
  effectFn.deps = [];
  effectFn.options = options;
  if (!effectFn.options.lazy) { // 若副作用函数持有lazy标记,则直接将副作用函数返回
    effectFn();
  }
  return effectFn;
}

通过上面对lazy标记的设置,现在可以实现下面的效果:

const effectFn = effect(
	()=> {
        return ()=>obj.val * 2;
    },
    {
        lazy: true; // 设置 lazy 标记
    }
)();
console.log(effectFn); // 2

在此基础上,我们来实现computed

function computed(getter) {
    let value;
    let dirty = false;
    const effectFn = effect(getter, {
        lazy: true,
        scheduler(){
        	if(!dirty) {
                dirty = true;
                tirgger(obj, 'value');
            }
    	}
    });
    const obj = {
        get value() {
            if(!dirty) {
                value = effectFn();
                dirty = true;
            }
            track(target, 'value');
            return value;
        }
    };
    return obj;
}

(8)watch

​ 想要实现watch,其实只需要添加一个scheduler(),像是这样:

effect(
	()=> {
        consoloe.log(obj.val);
    },
    {
        scheduler() {
            console.log("数值发生了变化");
        }
    }
)

就可以实现一个基本的watch效果,现在来编写一个功能完整的watch函数

function watch(source, cb) {
    let getter;
    if(typeof source === "function") { //若传入()=> obj.val,则直接使用该匿名函数
        getter = source;
    } else {
        getter = traverse(source); // 否则递归遍历该对象的所有属性,从而达到监听所有属性的目的
    }
    let oldValue, newValue; // 保存新旧值
    const effectFn = effect(getter, {
        lazy: true,
        scheduler() {
            newValue = effectFn(); // 获取新值
            cb(oldValue, newValue);
            oldValue = newValue; // 函数执行完后,更新旧值。
        }
    });
    oldValue = effectFn(); // 获取初始旧值
}
function traverse(value, seen = new Set()) {
    if(typeof value !== 'object' || value !== null || seen.has(value)) return ;
    seen.add(value);
    for(const k in seen) {
        traverse(seen[k],seen);
    }
}

到此这篇关于Vue响应式原理深入分析的文章就介绍到这了,更多相关Vue响应式原理内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 一文带你深入理解Vue3响应式原理

    目录 响应式原理 2.0的不足 reactive和effect的实现 effect track trigger 测试代码 递归实现reactive 总结 响应式原理 Vue2 使用的是 Object.defineProperty  Vue3 使用的是 Proxy 2.0的不足 对象只能劫持 设置好的数据,新增的数据需要Vue.Set(xxx)  数组只能操作七种方法,修改某一项值无法劫持. reactive和effect的实现 export const reactive = <T extends

  • vue.js数据响应式原理解析

    目录 Object.defineProperty() 定义 defineReactive 函数 递归侦测对象的全部属性 流程分析 observe 函数 Observer 类 完善 defineReactive 函数 One More Thing Object.defineProperty() 得力于 Object.defineProperty() 的特性,vue 的数据变化有别于 react 和小程序,是非侵入式的.详细介绍可以看 MDN 文档,这里特别说明几点: get / set 属性是函数

  • 关于Vue3中的响应式原理

    目录 一.简介 二.响应核心 1.核心源码 2.逐步分析上述示例代码 3.收集依赖和触发依赖更新 三.V3.2的响应式优化 四.后话 一.简介 本章内容主要通过具体的简单示例来分析Vue3是如何实现响应式的.理解本章需要了解Vue3的响应式对象.只注重原理设计层面,细节不做太多讲解. 二.响应核心 1.核心源码 export class ReactiveEffect<T = any> { //是否激活 active = true //依赖列表 deps: Dep[] = [] // can b

  • 浅析一下Vue3的响应式原理

    目录 Proxy Reflect 举个例子 reactive effect track trigger Proxy Vue3 的响应式原理依赖了 Proxy 这个核心 API,通过 Proxy 可以劫持对象的某些操作. const obj = { a: 1 }; const p = new Proxy(obj, {   get(target, property, receiver) {     console.log("get");     return Reflect.get(tar

  • 深入理解Vue3响应式原理

    目录 响应式原理 手写实现 1.实现Reactive 2.实现依赖的收集和触发 effect影响函数 收集/添加依赖 触发依赖 3.移除/停止依赖 衍生类型 1.实现readonly 2.实现shallowReadonly 3.实现ref 4.实现computed 工具类 响应式原理 利用ES6中Proxy作为拦截器,在get时收集依赖,在set时触发依赖,来实现响应式. 手写实现 1.实现Reactive 基于原理,我们可以先写一下测试用例 //reactive.spec.ts describ

  • Vue响应式原理模拟实现原理探究

    目录 前置知识 数据驱动 数据响应式的核心原理 Vue 2.x Vue 3.x 发布订阅和观察者模式 发布/订阅模式 观察者模式 Vue响应式原理模拟实现 Vue Observer对data中的属性进行监听 Compiler Watcher Dep 测试代码 前置知识 数据驱动 数据响应式——Vue 最标志性的功能就是其低侵入性的响应式系统.组件状态都是由响应式的 JavaScript 对象组成的.当更改它们时,视图会随即自动更新. 双向绑定——数据改变,视图改变:视图改变,数据也随之改变 数据

  • Vue响应式原理与虚拟DOM实现步骤详细讲解

    目录 一.什么是响应式系统 二.实现原理 三.虚拟DOM实现 四.总结 一.什么是响应式系统 在Vue中,我们可以使用data属性来定义组件的数据.这些数据可以在模板中使用,并且当这些数据发生变化时,相关的DOM元素也会自动更新.这个过程就是响应式系统的核心.例如,我们在Vue组件中定义了一个count属性: <template> <div>{{ count }}</div> </template> <script> export default

  • Vue响应式原理Observer、Dep、Watcher理解

    开篇 最近在学习Vue的源码,看了网上一些大神的博客,看起来感觉还是蛮吃力的.自己记录一下学习的理解,希望能够达到简单易懂,不看源码也能理解的效果

  • 详细分析vue响应式原理

    前言 响应式原理作为 Vue 的核心,使用数据劫持实现数据驱动视图.在面试中是经常考查的知识点,也是面试加分项. 本文将会循序渐进的解析响应式原理的工作流程,主要以下面结构进行: 分析主要成员,了解它们有助于理解流程 将流程拆分,理解其中的作用 结合以上的点,理解整体流程 文章稍长,但大部分是代码实现,还请耐心观看.为了方便理解原理,文中的代码会进行简化,如果可以请对照源码学习. 主要成员 响应式原理中,Observe.Watcher.Dep这三个类是构成完整原理的主要成员. Observe,响

  • vue响应式原理与双向数据的深入解析

    了解object.defineProperty 实现响应式 清楚 observe/watcher/dep 具体指的是什么 了解 发布订阅模式 以及其解决的具体问题 在Javascript里实现数据响应式一般有俩种方案,分别对应着vue2.x 和 vue3.x使用的方式,他们分别是: 对象属性拦截 (vue2.x) Object.defineProperty 对象整体代理 (vue3.x) Proxy 提示:以下是本篇文章正文内容,下面案例可供参考 vue-响应式是什么? Vue 最独特的特性之一

  • Vue响应式原理的示例详解

    Vue 最独特的特性之一,是非侵入式的响应系统.数据模型仅仅是普通的 JavaScript 对象.而当你修改它们时,视图会进行更新.聊到 Vue 响应式实现原理,众多开发者都知道实现的关键在于利用 Object.defineProperty , 但具体又是如何实现的呢,今天我们来一探究竟. 为了通俗易懂,我们还是从一个小的示例开始: <body> <div id="app"> {{ message }} </div> <script> v

  • 一篇文章带你彻底搞懂VUE响应式原理

    目录 响应式原理图 编译 创建compile类 操作fragment 获取元素节点上的信息 获取文本节点信息 操作fragment 响应式 数据劫持 收集依赖 响应式代码完善 Dep类 全局watcher用完清空 依赖的update方法 需要注意的一个地方 双剑合璧 总结 首先上图,下面这张图,即为MVVM响应式原理的整个过程图,我们本篇都是围绕着这张图进行分析,所以这张图是重中之重. 响应式原理图 一脸懵逼?没关系,接下来我们将通过创建一个简单的MVVM响应系统来一步步了解这个上图中的全过程.

  • Vue响应式原理及双向数据绑定示例分析

    目录 前言 响应式原理 双向数据绑定 前言 之前公司招人,面试了一些的前端同学,因为公司使用的前端技术是Vue,所以免不了问到其响应式原理和Vue的双向数据绑定.但是这边面试到的80%的同学会把两者搞混,通常我要是先问响应式原理再问双向数据绑定原理,来面试的同学大都会认为是一回事,那么这里我们就说一下二者的区别. 响应式原理 是Vue的核心特性之一,数据驱动视图,我们修改数据视图随之响应更新,就很优雅~ Vue2.x是借助Object.defineProperty()实现的,而Vue3.x是借助

  • Vue响应式原理深入解析及注意事项

    前言 Vue最明显的特性之一便是它的响应式系统,其数据模型即是普通的 JavaScript 对象.而当你读取或写入它们时,视图便会进行响应操作.文章简要阐述下其实现原理,如有错误,还请不吝指正.下面话不多说了,来随着小编来一起学习学习吧. 响应式data <div id = "exp">{{ message }}</div> const vm = new Vue({ el: '#exp', data: { message: 'This is A' } }) vm

  • 详解VUE响应式原理

    目录 1.响应式原理基础 2.核心对象:Dep与Watcher 3.收集依赖与更新依赖 3.1 收集依赖 3.2 更新依赖 4.源码调试 4.1 测试的页面代码 1.对象说明 2.Dep与Watcher的关系 3.最终的关系结果 4.2  源码调试 1.收集依赖的入口函数:initState(页面初始化时执行); 2.初始化computed和watch时,生成Watcher实例化对象 总结 1.响应式原理基础 响应式基本原理是基于Object.defineProperty(obj, prop,

  • Vue响应式原理详解

    Vue 嘴显著的特性之一便是响应式系统(reactivity system),模型层(model)只是普通JavaScript对象,修改它则更新视图(view). Vue 响应式系统的底层细节 如何追踪变化 把一个普通的JavaScript对象传给Vue实例的data选项,Vue将遍历此对象的所有属性,并使用Object.defineProperty 把这些属性全部转为 getter/setter.Object.defineProperty是仅ES5支持,并无法shim的特性,这也就是为什么Vu

随机推荐