vue2.0响应式原理 - defineProperty

这个原理老生常谈了,就是拦截对象,给对象的属性增加set 和 get方法,因为核心是defineProperty所以还需要对数组的方法进行拦截

一、变化追踪

  • 把一个普通 JavaScript 对象传给 Vue 实例的 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter。
  • Object.defineProperty 是仅 ES5 支持,且无法 shim 的特性,这也就是为什么 Vue 不支持 IE8 以及更低版本浏览器的原因。
  • 用户看不到 getter/setter,但是在内部它们让 Vue 追踪依赖,在属性被访问和修改时通知变化。
  • 每个组件实例都有相应的 watcher 实例对象,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会通知 watcher 重新计算,从而致使它关联的组件得以更新。

原理:在初次渲染的过程中就会调用对象属性的getter函数,然后getter函数通知wather对象将之声明为依赖,依赖之后,如果对象属性发生了变化,那么就会调用settter函数来通知watcher,watcher就会在重新渲染组件,以此来完成更新。

二、变化检测问题

Vue 不能检测到对象属性的添加或删除。由于 Vue 会在初始化实例时对属性执行 getter/setter 转化过程,所以属性必须在 data 对象上存在才能让 Vue 转换它,这样才能让它是响应的。

var vm = new Vue({
el: '#app',
data:{
a:1,
k: {}
}
}) // `vm.a` 是响应的

vm.b = 2
// `vm.b` 是非响应的

对于已经创建的实例,Vue 不允许动态添加根级别的响应式 property。但是,可以使用 Vue.set(object, propertyName, value) 方法向嵌套对象添加响应式 property。

Vue.set(vm.someObject, 'b', 2)

//您还可以使用 vm.$set 实例方法,这也是全局 Vue.set 方法的别名:
this.$set(this.someObject,'b',2)

// 代替 `Object.assign(this.someObject, { a: 1, b: 2 })`
this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 })

Vue 不能检测以下数组的变动:

  1. 当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue
  2. 当你修改数组的长度时,例如:vm.items.length = newLength
// Vue.set
Vue.set(vm.items, indexOfItem, newValue)
// Array.prototype.splice
vm.items.splice(indexOfItem, 1, newValue) vm.$set(vm.items, indexOfItem, newValue)
vm.items.splice(newLength)

三、声明响应式属性

由于 Vue 不允许动态添加根级响应式属性,所以你必须在初始化实例前声明根级响应式属性,可以为一个空值

如果你在 data 选项中未声明 message,Vue 将警告你渲染函数在试图访问的属性不存在。

var vm = new Vue({
data: {
// 声明 message 为一个空值字符串
message: ''
},
template: '<div>{{ message }}</div>'
})
// 之后设置 `message`
vm.message = 'Hello!'

四、异步更新队列

Vue 在更新 DOM 时是异步执行的。

只要观察到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。如果同一个 watcher 被多次触发,只会一次推入到队列中。

Vue 在内部尝试对异步队列使用原生的 Promise.then 和 MutationObserver,如果执行环境不支持,会采用 setTimeout(fn, 0) 代替。

五、拦截

Object.defineProperty缺点

  • 无法监听数组的变化
  • 需要深度遍历,浪费内存

对对象进行拦截

function observer(target){
// 如果不是对象数据类型直接返回即可
if(typeof target !== 'object'){
return target
}
// 重新定义key
for(let key in target){
defineReactive(target,key,target[key])
}
}
//更新
function update(){
console.log('update view')
}
function defineReactive(obj,key,value){
  
  // 校验----对象嵌套对象,递归劫持
observer(value); Object.defineProperty(obj,key,{
get(){
// 在get 方法中收集依赖
return value
},
set(newVal){
if(newVal !== value){
observer(value);
update(); // 在set方法中触发更新
}
}
})
}
let obj = {name:'youxuan'}
observer(obj);
obj.name = 'webyouxuan';

数组方法劫持

let oldProtoMehtods = Array.prototype;
let proto = Object.create(oldProtoMehtods);
['push','pop','shift','unshift'].forEach(method=>{
Object.defineProperty(proto,method,{
get(){
update();
oldProtoMehtods[method].call(this,...arguments)
}
})
})

function observer(target){
if(typeof target !== 'object'){
return target
}
// 如果不是对象数据类型直接返回即可
if(Array.isArray(target)){
Object.setPrototypeOf(target,proto);
// 给数组中的每一项进行observr
for(let i = 0 ; i < target.length;i++){
observer(target[i])
}
return
};
// 重新定义key
for(let key in target){
defineReactive(target,key,target[key])
}
}

Vue3.0数据响应机制 - Proxy

首先熟练一下ES6中的 ProxyReflect 及 ES6中为我们提供的 MapSet两种数据结构。

Proxy

  用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。

语法:const p = new Proxy(target, handler)

参数

  target:要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
  handler:一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。
const handler = {
get: function(obj, prop) {
return prop in obj ? obj[prop] : 37;
}
}; const p = new Proxy({}, handler);
p.a = 1;
p.b = undefined; console.log(p.a, p.b); // 1, undefined
console.log('c' in p, p.c); // false, 37

Reflect

是一个内置的对象,它提供拦截 JavaScript 操作的方法

1、Reflect.deleteProperty(targetpropertyKey)

  作为函数的delete操作符,相当于执行 delete target[name]

2、Reflect.set(targetpropertyKeyvalue[, receiver])

  将值分配给属性的函数。返回一个Boolean,如果更新成功,则返回true

3、Reflect.get(targetpropertyKey[, receiver])

  获取对象身上某个属性的值,类似于 target[name]。

先应用再说原理:

let p = Vue.reactive({name:'youxuan'});
Vue.effect(()=>{ // effect方法会立即被触发
console.log(p.name);
})
p.name = 'webyouxuan';; // 修改属性后会再次触发effect方法

一、reactive方法实现

通过proxy 自定义获取、增加、删除等行为

1) 对象操作

// 1、声明响应式对象
function reactive(target) {
return createReactiveObject(target);
}
// 是否是对象类型
function isObject(target) {
return typeof target === 'object' && target !== null;
}
// 2、创建
function createReactiveObject(target) {
// 判断target是不是对象,不是对象不必继续
if (!isObject(target)) {
return target;
}
const handlers = {
get(target, key, receiver) { // 取值
console.log('获取')
let res = Reflect.get(target, key, receiver);
return res;
},
set(target, key, value, receiver) { // 更改 、 新增属性
console.log('设置')
let result = Reflect.set(target, key, value, receiver);
return result;
},
deleteProperty(target, key) { // 删除属性
console.log('删除')
const result = Reflect.deleteProperty(target, key);
return result;
}
}
// 开始代理
observed = new Proxy(target, handlers);
return observed;
} let p = reactive({ name: 'youxuan' });
console.log(p.name); // 获取
p.name = 'webyouxuan'; // 设置
delete p.name; // 删除

深层代理

由于我们只代理了第一层对象,所以对age对象进行更改是不会触发set方法的,但是却触发了get方法,这是由于 p.age会造成 get操作

let p = reactive({ name: "123", age: { num: 10 } });
p.age.num = 11

get改进方案

  这里我们将p.age取到的对象再次进行代理,这样在去更改值即可触发set方法

get(target, key, receiver) {
// 取值
console.log("获取");
let res = Reflect.get(target, key, receiver);

  // 懒代理,只有当取值时再次做代理
  return isObject(res)? reactive(res) : res;
}

2)数组操作

Proxy默认可以支持数组,包括数组的长度变化以及索引值的变化

产生问题:

let p = reactive([1,2,3,4]);
p.push(5);

会触发两次set方法,第一次更新的是数组中的第4项,第二次更新的是数组的length

 屏蔽掉多次触发:
set(target, key, value, receiver) {
// 更改、新增属性
let oldValue = target[key]; // 获取上次的值
let hadKey = hasOwn(target,key); // 看这个属性是否存在
let result = Reflect.set(target, key, value, receiver);
if(!hadKey){ // 新增属性
console.log('更新 添加')
}else if(oldValue !== value){ // 修改存在的属性
console.log('更新 修改')
}
// 当调用push 方法第一次修改时数组长度已经发生变化
// 如果这次的值和上次的值一样则不触发更新
return result;
}

解决重复使用reactive情况

// 情况1.多次代理同一个对象
let arr = [1,2,3,4];
let p = reactive(arr);
reactive(arr); // 情况2.将代理后的结果继续代理
let p = reactive([1,2,3,4]);
reactive(p);

通过hash表的方式来解决重复代理的情况

const toProxy = new WeakMap(); // 存放被代理过的对象
const toRaw = new WeakMap(); // 存放已经代理过的对象
function reactive(target) {
// 创建响应式对象
return createReactiveObject(target);
}
function isObject(target) {
return typeof target === "object" && target !== null;
}
function hasOwn(target,key){
return target.hasOwnProperty(key);
}
function createReactiveObject(target) {
if (!isObject(target)) {
return target;
}
let observed = toProxy.get(target);
if(observed){ // 判断是否被代理过
return observed;
}
if(toRaw.has(target)){ // 判断是否要重复代理
return target;
}
const handlers = {
get(target, key, receiver) {
// 取值
console.log("获取");
let res = Reflect.get(target, key, receiver);
return isObject(res) ? reactive(res) : res;
},
set(target, key, value, receiver) {
let oldValue = target[key];
let hadKey = hasOwn(target,key);
let result = Reflect.set(target, key, value, receiver);
if(!hadKey){
console.log('更新 添加')
}else if(oldValue !== value){
console.log('更新 修改')
}
return result;
},
deleteProperty(target, key) {
console.log("删除");
const result = Reflect.deleteProperty(target, key);
return result;
}
};
// 开始代理
observed = new Proxy(target, handlers);
toProxy.set(target,observed);
toRaw.set(observed,target); // 做映射表
return observed;
}

二、effect实现

effect意思是副作用,此方法默认会先执行一次。如果数据变化后会再次触发此回调函数。

let user= {name:'大鹏'}
let p = reactive(user);
effect(()=>{
console.log(p.name); // 大鹏
})

实现方法

function effect(fn) {
const effect = createReactiveEffect(fn); // 创建响应式的effect
effect(); // 先执行一次
return effect;
}
const activeReactiveEffectStack = []; // 存放响应式effect
function createReactiveEffect(fn) {
const effect = function() {
// 响应式的effect
return run(effect, fn);
};
return effect;
}
function run(effect, fn) {
try {
activeReactiveEffectStack.push(effect);
return fn(); // 先让fn执行,执行时会触发get方法,可以将effect存入对应的key属性
} finally {
activeReactiveEffectStack.pop(effect);
}
}

当调用fn()时可能会触发get方法,此时会触发track

const targetMap = new WeakMap();
function track(target,type,key){
// 查看是否有effect
const effect = activeReactiveEffectStack[activeReactiveEffectStack.length-1];
if(effect){
let depsMap = targetMap.get(target);
if(!depsMap){ // 不存在map
targetMap.set(target,depsMap = new Map());
}
let dep = depsMap.get(target);
if(!dep){ // 不存在set
depsMap.set(key,(dep = new Set()));
}
if(!dep.has(effect)){
dep.add(effect); // 将effect添加到依赖中
}
}
}

当更新属性时会触发trigger执行,找到对应的存储集合拿出effect依次执行、

function trigger(target,type,key){
const depsMap = targetMap.get(target);
if(!depsMap){
return
}
let effects = depsMap.get(key);
if(effects){
effects.forEach(effect=>{
effect();
})
}
}

我们发现如下问题

  新增了值,effect方法并未重新执行,因为push中修改length已经被我们屏蔽掉了触发trigger方法,所以当新增项时应该手动触发length属性所对应的依赖。

let school = [1,2,3];
let p = reactive(school);
effect(()=>{
console.log(p.length);
})
p.push(100);

解决

function trigger(target, type, key) {
const depsMap = targetMap.get(target);
if (!depsMap) {
return;
}
let effects = depsMap.get(key);
if (effects) {
effects.forEach(effect => {
effect();
});
}
// 处理如果当前类型是增加属性,如果用到数组的length的effect应该也会被执行
if (type === "add") {
let effects = depsMap.get("length");
if (effects) {
effects.forEach(effect => {
effect();
});
}
}
}

三、ref实现

ref可以将原始数据类型也转换成响应式数据,需要通过.value属性进行获取值

function convert(val) {
return isObject(val) ? reactive(val) : val;
}
function ref(raw) {
raw = convert(raw);
const v = {
_isRef:true, // 标识是ref类型
get value() {
track(v, "get", "");
return raw;
},
set value(newVal) {
raw = newVal;
trigger(v,'set','');
}
};
return v;
}

问题又来了我们再编写个案例

  这样做的话岂不是每次都要多来一个.value,这样太难用了

let r = ref(1);
let c = reactive({
a:r
});
console.log(c.a.value);

解决

get方法中判断如果获取的是ref的值,就将此值的value直接返回即可

let res = Reflect.get(target, key, receiver);
if(res._isRef){
return res.value
}

四、computed实现

computed 实现也是基于 effect 来实现的,特点是computed中的函数不会立即执行,多次取值是有缓存机制的

let a = reactive({name:'youxuan'});
let c = computed(()=>{
console.log('执行次数')
return a.name +'webyouxuan';
})
// 不取不执行,取n次只执行一次
console.log(c.value);
console.log(c.value);
function computed(getter){
let dirty = true;
const runner = effect(getter,{ // 标识这个effect是懒执行
lazy:true, // 懒执行
scheduler:()=>{ // 当依赖的属性变化了,调用此方法,而不是重新执行effect
dirty = true;
}
});
let value;
return {
_isRef:true,
get value(){
if(dirty){
value = runner(); // 执行runner会继续收集依赖
dirty = false;
}
return value;
}
}
}

修改effect方法

function effect(fn,options) {
let effect = createReactiveEffect(fn,options);
if(!options.lazy){ // 如果是lazy 则不立即执行
effect();
}
return effect;
}
function createReactiveEffect(fn,options) {
const effect = function() {
return run(effect, fn);
};
effect.scheduler = options.scheduler;
return effect;
}

trigger时判断

deps.forEach(effect => {
if(effect.scheduler){ // 如果有scheduler 说明不需要执行effect
effect.scheduler(); // 将dirty设置为true,下次获取值时重新执行runner方法
}else{
effect(); // 否则就是effect 正常执行即可
}
});
let a = reactive({name:'youxuan'});
let c = computed(()=>{
console.log('执行次数')
return a.name +'webyouxuan';
})
// 不取不执行,取n次只执行一次
console.log(c.value);
a.name = 'zf10'; // 更改值 不会触发重新计算,但是会将dirty变成true console.log(c.value); // 重新调用计算方法

vue2.0与3.0响应式原理机制的更多相关文章

  1. Vue 2.0 与 Vue 3.0 响应式原理比较

    Vue 2.0 的响应式是基于Object.defineProperty实现的 当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 prop ...

  2. Vue2.x响应式原理

    一.回顾Vue响应式用法 ​ vue响应式,我们都很熟悉了.当我们修改vue中data对象中的属性时,页面中引用该属性的地方就会发生相应的改变.避免了我们再去操作dom,进行数据绑定. 二.Vue响应 ...

  3. Vue2 响应式原理

    我们经常用vue的双向绑定,改变data的某个属性值,vue就马上帮我们自动更新视图,下面我们看看原理. Object的响应式原理: 可以看到,其实核心就是把object的所有属性都加上getter. ...

  4. [切图仔救赎]炒冷饭--在线手撸vue2响应式原理

    --图片来源vue2.6正式版本(代号:超时空要塞)发布时,尤雨溪推送配图. 前言 其实这个冷饭我并不想炒,毕竟vue3马上都要出来.我还在这里炒冷饭,那明显就是搞事情. 起因: 作为切图仔搬砖汪,长 ...

  5. 由浅入深,带你用JavaScript实现响应式原理(Vue2、Vue3响应式原理)

    由浅入深,带你用JavaScript实现响应式原理 前言 为什么前端框架Vue能够做到响应式?当依赖数据发生变化时,会对页面进行自动更新,其原理还是在于对响应式数据的获取和设置进行了监听,一旦监听到数 ...

  6. Vue2响应式原理

    vue2响应式原理 vue的特性:数据驱动视图和双向数据绑定.vue官方文档也提供了响应式原理的解释: 深入响应式原理 Object.defineProperty() Object.definePro ...

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

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

  8. 深入解析vue响应式原理

    摘要:本文主要通过结合vue官方文档及源码,对vue响应式原理进行深入分析. 1.定义 作为vue最独特的特性,响应式可以说是vue的灵魂了,表面上看就是数据发生变化后,对应的界面会重新渲染,那么响应 ...

  9. 浅谈vue响应式原理及发布订阅模式和观察者模式

    一.Vue响应式原理 首先要了解几个概念: 数据响应式:数据模型仅仅是普通的Javascript对象,而我们修改数据时,视图会进行更新,避免了繁琐的DOM操作,提高开发效率. 双向绑定:数据改变,视图 ...

随机推荐

  1. ArrayList这篇就够了

    提起ArrayList,相信很多小伙伴都用过,而且还不少用.但在几年之前,我在一场面试中,面试官要求说出ArrayList的扩容机制.很显然,那个时候的我并没有关注这些,从而错过了一次机会.不过好在我 ...

  2. 自动获取IMC系统所有网络设备资产信息

    1 #coding=utf8 2 3 """ 4 CMDB接口调用 5 """ 6 import csv 7 import json 8 i ...

  3. PAT (Basic Level) Practice (中文)1070 结绳 (25 分) 凌宸1642

    PAT (Basic Level) Practice (中文)1070 结绳 (25 分) 凌宸1642 题目描述 给定一段一段的绳子,你需要把它们串成一条绳.每次串连的时候,是把两段绳子对折,再如下 ...

  4. 生产环境中mysql数据库由主从关系切换为主主关系

    目录 一.清除原从数据库数据及主从关系 1.1.关闭主从数据库原有的主从关系 1.2.清除从数据库原有数据 二.将主库上的数据备份到从库 2.1.备份主库数据到从库 2.2.在从库使用tsc.sql文 ...

  5. D. 【例题4】字符串环

    解析 字符串的操作,可以用函数解决这个问题 s 2. f i n d ( s 1. s u b s t r ( i , j ) ) s2.find~(s1.substr~(i,~j)) s2.find ...

  6. 滴水逆向初级-C语言(二)

    2.1.C语言的汇编表示 c语言代码 int plus(int x,int y) { return 0; } void main() { __asm { mov eax,eax } //调用函数 pl ...

  7. .Net Core 路由处理

    用户请求接口路由,应用返回处理结果.应用中如何匹配请求的数据呢?为何能如此精确的找到对应的处理方法?今天就谈谈这个路由.路由负责匹配传入的HTTP请求,将这些请求发送到可以执行的终结点.终结点在应用中 ...

  8. 可视化运行Python的神器Jupyter Notebook

    目录 简介 Jupyter Notebook 启动notebook server notebook document 的结构 code cells markdown cells raw cells 以 ...

  9. Dynamic Programming 动态规划入门笔记

    算法导论笔记 programming 指的是一种表格法,并非编写计算机程序 动态规划与分治方法相似,都是通过组合子问题的解来求解原问题.但是分治法将问题划分为互不相交的子问题.而动态规划是应用与子问题 ...

  10. Day09_41_集合_Set

    Set集合 Set集合 - Set集合的特点是无序不可重复.Set集合类似于一个罐子,程序可以依次把多个对象"丢进"Set集合,而Set集合通常不能记住元素的添加顺序. - Set ...