Vue响应式原理整理笔记
目录
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响应式原理整理笔记相关推荐
- vue 数组删除 dome没更新_详解Vue响应式原理
摘要: 搞懂Vue响应式原理! 作者:浪里行舟 原文:深入浅出Vue响应式原理 Fundebug经授权转载,版权归原作者所有. 前言 Vue 最独特的特性之一,是其非侵入性的响应式系统.数据模型仅仅是 ...
- Vue 响应式原理(双向数据绑定) 怎样实现 响应式原理?
Vue 响应式原理(双向数据绑定) 怎样实现 响应式原理? 我们在Vue里面,定义在Data里的属性,叫做响应式属性. 每一个vue组件被创建的时候,同时还有一个对象被创建出来了,这个对象我们是看不到 ...
- 手把手教你剖析vue响应式原理,监听数据不再迷茫
Object.defineProperty实现vue响应式原理 一.组件化基础 1."很久以前"的组件化 (1)asp jsp php 时代 (2)nodejs 2.数据驱动视图( ...
- Vue响应式原理的简单模型
1.前言 最近在梳理vue响应式的原理,有一些心得,值得写一篇博客出来看看. 其实之前也尝试过了解vue响应式的原理,毕竟现在面试看你用的是vue的话,基本上都会问你几句vue响应式的原理.以往学习这 ...
- Vue响应式原理(看这一篇就够了)
你肯定听说过Object.denfineProperty或是Proxy\reflect,这的确是在VUE响应式原理中起重要作用的一部分代码,但这远远不能代表整个流程的精妙.上图: 不懂没关系,请往下看 ...
- Vue源码--解读vue响应式原理
原文链接:https://geniuspeng.github.io/2018/01/05/vue-reactivity/ Vue的官方说明里有深入响应式原理这一节.在此官方也提到过: 当你把一个普通的 ...
- Vue响应式原理详细讲解
面试官:请你简单的说说什么是Vue的响应式. 小明:mvvm就是视图模型模型视图,只有数据改变视图就会同时更新. 面试官:说的很好,回去等通知吧. 小明:.... Vue响应式原理 先看官方的说法 简 ...
- 一篇文章带你吃透VUE响应式原理
本篇响应式原理介绍比较长,全文大概1w+字.虽然内容繁杂,但阅读过后,绝对会让你对vue的响应式有更加深刻的理解. 分块阅读,效果更佳.(建议读者有一定vue使用经验和基础再食用) 首先上图,下面这张 ...
- 深入了解 Vue 响应式原理(数据拦截)
前言 在上一章节我们已经粗略的分析了整个的Vue 的源码(还在草稿箱,需要梳理清楚才放出来),但是还有很多东西没有深入的去进行分析,我会通过如下几个重要点,进行进一步深入分析. 深入了解 Vue 响应 ...
最新文章
- python--gevent高并发socket
- 如何解决SSL/TLS握手过程中失败的错误?
- django 1.8 官方文档翻译: 3-4-5 内建基于类的视图的API
- 纠结mac和pc怎么选,可以看看这个
- 【交易技术前沿】新一代证券交易系统应用架构的研究
- Linux配置NTP服务器
- Lambda表达式 对List集合去重
- C 语言学习笔记(一):C 语言的开发环境
- Linux开发环境搭建与使用——Linux简史
- 小米air2se耳机只有一边有声音怎么办_几款两百元以内的耳机使用体验
- 11210怎么等于24_算24点
- spring cloud bus的使用及使用bus发布自定义事件
- 华为云用docker部署halo
- 孩子学习arduino好还是单片机好
- FBG光纤光栅反射器的特点
- picpick快捷键
- 视频画中画的实现(窗口剪裁)
- 桌面移动错误,变成了D:/
- 何海涛算法面试题感悟之二:设计包…
- opencv判断一个点是否在轮廓内pointPolygonTest的用法