目录

0、vue介绍

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

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

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


0、vue介绍

1.Vue.js是用于构建交互式的 Web 界面的库。

2.它提供了 MVVM数据绑定和一个可组合的组件系统,具有简单、灵活的API。从技术上讲,Vue.js集中在MVVM模式上的视图模型层,并通过双向数据绑定连接视图和模型。

3.实际的DOM操作和输出格式被抽象出来成指令和过滤器。相比其它的MVVM 框架,Vue.js 更容易上手。

4.Vue.js是一个用于创建Web交互界面的库。它让你通过简单而灵活的API创建由数据驱动的UI组件。

5.内核生成:Es6和--->类和继承

6.Model:js对象。

View:html视图。

通过事件驱动去监听视图的变化,方法和指令,监听视图对象的方法。

双向绑定(v-model);

DOM事件:JS;

7.Css\js\html\-->js类,模式:MVC模式(虚拟DOM),单向数据流,js的角度封装

8.vue是单独拿出来,MVVM模式(分开的,用事件去监听DOM),双向数据流,采用指令(标记中的一个属性)

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)实现调度实行

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

// 这段代码本来会输出的结果是:
/**12结束**/
// 现在我们想让它变成
/**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 ++;
// 这段代码本来会输出的结果是:
/**123**/
// 现在我们想让它变成
/**13**/

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

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响应式原理整理笔记相关推荐

  1. vue 数组删除 dome没更新_详解Vue响应式原理

    摘要: 搞懂Vue响应式原理! 作者:浪里行舟 原文:深入浅出Vue响应式原理 Fundebug经授权转载,版权归原作者所有. 前言 Vue 最独特的特性之一,是其非侵入性的响应式系统.数据模型仅仅是 ...

  2. Vue 响应式原理(双向数据绑定) 怎样实现 响应式原理?

    Vue 响应式原理(双向数据绑定) 怎样实现 响应式原理? 我们在Vue里面,定义在Data里的属性,叫做响应式属性. 每一个vue组件被创建的时候,同时还有一个对象被创建出来了,这个对象我们是看不到 ...

  3. 手把手教你剖析vue响应式原理,监听数据不再迷茫

    Object.defineProperty实现vue响应式原理 一.组件化基础 1."很久以前"的组件化 (1)asp jsp php 时代 (2)nodejs 2.数据驱动视图( ...

  4. Vue响应式原理的简单模型

    1.前言 最近在梳理vue响应式的原理,有一些心得,值得写一篇博客出来看看. 其实之前也尝试过了解vue响应式的原理,毕竟现在面试看你用的是vue的话,基本上都会问你几句vue响应式的原理.以往学习这 ...

  5. Vue响应式原理(看这一篇就够了)

    你肯定听说过Object.denfineProperty或是Proxy\reflect,这的确是在VUE响应式原理中起重要作用的一部分代码,但这远远不能代表整个流程的精妙.上图: 不懂没关系,请往下看 ...

  6. Vue源码--解读vue响应式原理

    原文链接:https://geniuspeng.github.io/2018/01/05/vue-reactivity/ Vue的官方说明里有深入响应式原理这一节.在此官方也提到过: 当你把一个普通的 ...

  7. Vue响应式原理详细讲解

    面试官:请你简单的说说什么是Vue的响应式. 小明:mvvm就是视图模型模型视图,只有数据改变视图就会同时更新. 面试官:说的很好,回去等通知吧. 小明:.... Vue响应式原理 先看官方的说法 简 ...

  8. 一篇文章带你吃透VUE响应式原理

    本篇响应式原理介绍比较长,全文大概1w+字.虽然内容繁杂,但阅读过后,绝对会让你对vue的响应式有更加深刻的理解. 分块阅读,效果更佳.(建议读者有一定vue使用经验和基础再食用) 首先上图,下面这张 ...

  9. 深入了解 Vue 响应式原理(数据拦截)

    前言 在上一章节我们已经粗略的分析了整个的Vue 的源码(还在草稿箱,需要梳理清楚才放出来),但是还有很多东西没有深入的去进行分析,我会通过如下几个重要点,进行进一步深入分析. 深入了解 Vue 响应 ...

最新文章

  1. python--gevent高并发socket
  2. 如何解决SSL/TLS握手过程中失败的错误?
  3. django 1.8 官方文档翻译: 3-4-5 内建基于类的视图的API
  4. 纠结mac和pc怎么选,可以看看这个
  5. 【交易技术前沿】新一代证券交易系统应用架构的研究
  6. Linux配置NTP服务器
  7. Lambda表达式 对List集合去重
  8. C 语言学习笔记(一):C 语言的开发环境
  9. Linux开发环境搭建与使用——Linux简史
  10. 小米air2se耳机只有一边有声音怎么办_几款两百元以内的耳机使用体验
  11. 11210怎么等于24_算24点
  12. spring cloud bus的使用及使用bus发布自定义事件
  13. 华为云用docker部署halo
  14. 孩子学习arduino好还是单片机好
  15. FBG光纤光栅反射器的特点
  16. picpick快捷键
  17. 视频画中画的实现(窗口剪裁)
  18. 桌面移动错误,变成了D:/
  19. 何海涛算法面试题感悟之二:设计包…
  20. opencv判断一个点是否在轮廓内pointPolygonTest的用法

热门文章

  1. Java数字位数不足前面补0的几种办法
  2. 获取顺序栈的栈顶元素
  3. python的super函数详解
  4. Vue2到Vue3实战必备技能(一)
  5. SkeyeVSS智慧渣土可视化管理系统赋能渣土车辆智能化管理
  6. 数字信号处理——FFT
  7. hevc 继续色度半像素差值
  8. 硬核! 逛了4年Github ,一口气把我收藏的 Java 开源项目分享给你!
  9. LogicFlow从新手到入门
  10. 使用Hibernate进行CRUD操作