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. redis数据库如何批量删除键和设置过期时间?

    我们可以借助Linux中的xargs,在终端中执行命令来实现这两个功能. 一.批量删除键 批量删除以"key"开头key的方法,需要借助Linux中的xargs,在终端中执行以下命 ...

  2. Spring笔记(四)

    Spring JdbcTemplate 一.JdbcTemplate概念及使用 1. 什么是JdbcTemplate: Spring框架对JDBC进行封装,使用JdbcTemplate方便实现对数据库 ...

  3. redis setNx原子锁

    https://github.com/suqi/rlock/blob/master/rlock.py 保持逻辑并发情况不产生多次结果 常用于下单,钱包,抢购,秒杀等场景 1 LOCK_TIMEOUT ...

  4. IndentationError:unexpected indent”、“IndentationError:unindent does not match any outer indetation level”以及“IndentationError:expected an indented block Python常见错误

    错误的使用缩进量 记住缩进增加只用在以:结束的语句之后,而之后必须恢复到之前的缩进格式. 经典错误,一定要注意缩进,尤其是在非界面化下环境的代码修改

  5. 从 lite-apiserver 看 SuperEdge 边缘节点自治

    引言 在 SuperEdge 0.2.0版本中,lite-apiserver 进行了重大的架构升级和功能增强.本文将从 lite-apiserver 实现及其与其它 SuperEdge 组件协同的角度 ...

  6. 5. Mybatis UPDATE更新,DELETE删除

    案例: 1. update <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper ...

  7. mvnw 找不到或无法加载主类,找不到符号,类

    如果你出现"找不到或无法加载主类"的问题,很有可能是maven的问题,你可以尝试一下这种办法: 问题:关于maven什么东西都没动,上午可能运行都好好的,下午可能就出现了这个问题, ...

  8. Jquery 代码参考

    jquery 代码参考 jQuery(document).ready(function($){}); jQuery(window).on('load', function(){}); $('.vide ...

  9. TP5学习记录(Model篇)

    ThinkPHP 数据库操作 数据库连接 #在config/database.php设置数据库连接参数或者利用Db::connect()方法设置数据库连接 /* * public static fun ...

  10. Android Hook框架adbi的分析(1)---注入工具hijack

    本文博客地址:http://blog.csdn.net/qq1084283172/article/details/74055505 一.Android Hook框架adbi的基本介绍 adbi是And ...