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 不能检测以下数组的变动:
- 当你利用索引直接设置一个数组项时,例如:
vm.items[indexOfItem] = newValue
- 当你修改数组的长度时,例如:
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中的 Proxy、Reflect 及 ES6中为我们提供的 Map、Set两种数据结构。
Proxy
用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。
语法:const p = new Proxy(target, handler)
参数
要使用
Proxy
包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理
的行为。
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(, )
作为函数的delete
操作符,相当于执行 delete target[name]
。
2、Reflect.set(, , [, ])
将值分配给属性的函数。返回一个Boolean
,如果更新成功,则返回true
。
3、Reflect.get(, [, ])
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); // 重新调用计算方法