實現(xiàn)可數(shù)據(jù)可響應的方式
通過可響應對象,實現(xiàn)對數(shù)據(jù)的偵測光绕,從而告知外界數(shù)據(jù)變化女嘲。
- 單一的訪問器getter和setter
- Object.defineProperty()--實現(xiàn)了vue2.0響應式原理
- Proxy--實現(xiàn)3.0版本的原理
vue2.0響應式原理的弊端
需要對 Object 和 Array 兩種類型采用不同的處理方式。
為了感知 Array 的變化诞帐,對 Array 原型上幾個改變數(shù)組自身的內(nèi)容的方法做了攔截欣尼,雖然實現(xiàn)了對數(shù)組的可響應,但同樣存在一些問題停蕉。vue2.0的數(shù)組方法攔截
defineProperty 通過遞歸實現(xiàn) getter/setter 也存在一定的性能問題愕鼓。
vue3.0主要使用的語法
1.reflect
Reflect對象與Proxy對象一樣,也是擁有get慧起、set方法菇晃。
- Reflect對象的設計目的:
- 將Object對象的一些明顯屬于語言內(nèi)部的方法(比如Object.defineProperty),放到Reflect對象上
- 修改某些Object方法的返回結果蚓挤,讓其變得更合理
- 讓Object操作都變成函數(shù)行為
- Reflect對象的方法與Proxy對象的方法一一對應磺送,只要是Proxy對象的方法,就能在Reflect對象上找到對應的方法灿意。
2.關于proxy
Proxy 可以代理數(shù)組估灿,并且 API 提供了get 、 set缤剧。
let data = { foo: 'foo' }
let p = new Proxy(data, {
get(target, key, receiver) {
return target[key]
},
set(target, key, value, receiver) {
console.log(receiver)// receiver是proxy對象本身
console.log('set value')
target[key] = value // target就是data對象
}
})
p.foo = 123
//Proxy{}
//{foo: "foo"} "set value"
// 此時data被修改馅袁,p是代理data的代理對象
let data = [1,2,3]
let p = new Proxy(data, {
get(target, key, receiver) {
return target[key]
},
set(target, key, value, receiver) {
console.log('set value')
target[key] = value
}
})
p.push(4) // Uncaught TypeError: 'set' on proxy: trap returned falsish for property '3'
- 產(chǎn)生問題:set也需要返回值
需要修改為
let data = [1,2,3]
let p = new Proxy(data, {
get(target, key, receiver) {
return target[key]
},
set(target, key, value, receiver) {
console.log('set value')
target[key] = value
//++
return true
//++
}
})
p.push(4)
// set value // 打印2次
打印兩次是因為處理數(shù)組時候push不但修改數(shù)組的項還修改了length
let data = [1,2,3]
let p = new Proxy(data, {
get(target, key, receiver) {
console.log('get value:', key)
return target[key]
},
set(target, key, value, receiver) {
console.log('set value:', key, value)
target[key] = value
return true
}
})
p.push(1)
// get value: push
// get value: length
// set value: 3 1
// set value: length 4
- 產(chǎn)生問題:多次觸發(fā)set和get就會多次觸發(fā)view的更新,此時只需要觸發(fā)一次set更新
可以用類似于 debounce 的操作處理多次執(zhí)行的問題(vue3.0有更好的方式)
function reactive(data, cb) {
let timer = null
return new Proxy(data, {
get(target, key, receiver) {
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
clearTimeout(timer)
timer = setTimeout(() => {
cb && cb()
}, 0);
return Reflect.set(target, key, value, receiver)
}
})
}
let ary = [1, 2]
let p = reactive(ary, () => {
console.log('trigger')
})
p.push(3)
// trigger
但是vue3.0提供了更好的方式處理執(zhí)行重復的問題荒辕,利用私有屬性
(以下沒有完全解決重復問題汗销,對于數(shù)組只是避免了length對數(shù)組更新的重復渲染犹褒,像splice,unshift這種多次修改索引的方法還會觸發(fā)多次)
function trigger(){
console.log('觸發(fā)視圖更新')
}
function isObject(target){
return typeof target==='object'&&target!==null;
}
function reactive(target){
if(!isObject(target)){
return target;
}
const handler={
set(target,key,value,receiver){
console.log('setValue',value)
//如果觸發(fā)的是私有屬性的話弛针,觸發(fā)視圖的更新
if(!target.hasOwnProperty(key)){//添加新屬性
trigger();
}else if(value!==target[key]){//set的值變化叠骑,更新屬性
trigger();
}
return Reflect.set(target,key,value,receiver)
},
get(target,key,receiver){
const res=Reflect.get(target,key,receiver);
console.log('getValue',res)
return res;
},
deleteProperty(target,key){
return Reflect.deleteProperty(target,key);
}
}
let observer=new Proxy(target,handler);
return observer;
}
let obj=[1,2,3];
let p=reactive(obj)
p.unshift(1);
監(jiān)聽深層的對象
let obj={
a:[1,2,3],
b:{c:4}
};
let p=reactive(obj)
p.b.c = 2
// getValue: {c:4} 不觸發(fā)set
- 產(chǎn)生問題:proxy 只能代理一層;
因為沒有觸發(fā) set 的輸出钦奋,反而是觸發(fā)了 get 座云,因為 set 的過程中訪問了 bar 這個屬性。
深度數(shù)據(jù)監(jiān)聽付材,可以用 遞歸 處理問題朦拖,把對象的每一層都進行代理(同vue2.0),但是優(yōu)于2.0,3.0的處理方式是只有使用數(shù)據(jù)的當前屬性的時候才會去代理厌衔,2.0則是全部代理璧帝,3.0的處理方式節(jié)約的大量不必要的工作。
function trigger(){
console.log('觸發(fā)視圖更新')
}
function isObject(target){
return typeof target==='object'&&target!==null;
}
function hasOwn(){
}
function reactive(target){
if(!isObject(target)){
return target;
}
const handler={
set(target,key,value,receiver){
//如果觸發(fā)的是私有屬性的話富寿,觸發(fā)視圖的更新
if(!target.hasOwnProperty(key)){//添加新屬性
trigger();
}else if(value!==target[key]){//set的值變化睬隶,更新屬性
trigger();
}
return Reflect.set(target,key,value,receiver)
},
get(target,key,receiver){
const res=Reflect.get(target,key,receiver);
if(isObject(res)){
return reactive(res)
}
return res;
},
deleteProperty(target,key){
return Reflect.deleteProperty(target,key);
}
}
let observer=new Proxy(target,handler);
return observer;
}
let obj={
a:[1,2,3],
b:{c:1}
}
let p=reactive(obj)
p.b.c=4
// 觸發(fā)視圖更新
有些數(shù)據(jù)并非需要偵測,我們需要對數(shù)據(jù)偵測做更細的控制页徐,全部監(jiān)聽浪費一部分的性能苏潜,vue3.0有更好的解決辦法,weakMap变勇。
3.weakMap對象恤左,實現(xiàn)完整的reactive數(shù)據(jù)監(jiān)聽
WeakMap對象也是鍵值對的集合。它的鍵必須是對象類型搀绣,值可以是任意類型飞袋。它的鍵被弱保持,也就是說链患,當其鍵所指對象沒有其他地方引用的時候巧鸭,它會被GC回收掉。WeakMap
提供的接口與Map
相同麻捻。
- 我認為使用WeakMap主要是因為
- WeakMap的鍵是對象類型纲仍,保持弱引用。
- 列表是否存在取決于垃圾回收器的狀態(tài)贸毕,性能比object更高
//++ ----------
const toProxy= new WeakMap()
const toRaw = new WeakMap()
// ++ ----------
function trigger(){
console.log('觸發(fā)視圖更新')
}
function isObject(target){
return typeof target==='object'&&target!==null;
}
function reactive(target){
if(!isObject(target)){
return target;
}
// ++ ----------
//如果代理表中存在target了就說明巷折,target已經(jīng)被代理過了,就返回代理過的對象
if(toProxy.get(target)){
console.log('target已經(jīng)被代理過了崖咨,是一個obj對象')
return toProxy.get(target)
}
if(toRaw.has(target)){//如果這個對象被代理過了,就把對象原封不動的返回
console.log('target已經(jīng)被代理過了,是一個proxy對象')
return target
}
// ++ ----------
const handler={
set(target,key,value,receiver){
const oldValue = target[key];
//如果觸發(fā)的是私有屬性的話油吭,觸發(fā)視圖的更新
if(!target.hasOwnProperty(key)){//添加新屬性击蹲,用hasOwnProperty是因為可以屏蔽一些無用的屬性更新署拟,像數(shù)組的length等
trigger();
}else if(value!==oldValue){//set的值變化,更新屬性
trigger();
}
return Reflect.set(target,key,value,receiver)
},
get(target,key,receiver){
const res=Reflect.get(target,key,receiver);
if(isObject(res)){
return reactive(res)
}
return res;
},
deleteProperty(target,key){
return Reflect.deleteProperty(target,key);
}
}
let observer=new Proxy(target,handler);
// ++ ----------
toProxy.set(target,observer);//記錄原對象代理過的結果
toRaw.set(observer,target);
// ++ ----------
return observer;
}
let obj={
a:[1,2,3],
b:{c:1}
}
let obj2={
a:[1,2,3],
b:{c:1}
}
let p=reactive(obj)
p=reactive(p);// p是被代理過的對象
let n=reactive(obj2);
let n1=reactive(obj2);//多次代理一個對象obj1=
以上實現(xiàn)了一個簡易的數(shù)據(jù)監(jiān)聽
4. 依賴收集(發(fā)布訂閱)歌豺,實現(xiàn)響應式
更新視圖主要依賴于effect函數(shù)推穷。
let obj=reactive({name:'zf'});
effect(()=>{ // effect會執(zhí)行兩次,默認先執(zhí)行一次 之后依賴的數(shù)據(jù)變化了 會再次執(zhí)行类咧。
console.log(obj.name)//會調(diào)用get方法
});
obj.name='jw';//代理的屬性值被修改
effect的作用是首先會執(zhí)行一次傳入的函數(shù)馒铃,之后如果代理的屬性有變化,會繼續(xù)觸發(fā)傳入effect的函數(shù)痕惋。
所以此時需要把傳入effect的函數(shù)包裝成一個響應式的函數(shù)区宇。
//棧 先進后出
let activeEffectStacks=[]; //
//響應式 副作用
function effect(fn){
// 需要把fn這個函數(shù)包裝成響應式的函數(shù),在把這個函數(shù)默認先執(zhí)行一次
let effect = createReactiveEffect(fn);
effect()//默認執(zhí)行一次
}
function createReactiveEffect(fn){
let effect=function(){//這個就是創(chuàng)建的響應式的effect
return run(effect,fn) // 運行 1.讓fn執(zhí)行 2.把這個effect存入到棧中
}
return effect;// 返回這個包裝好的函數(shù)
}
function run (){//運行fn并把effect存起來
try{//try包裹防止fn內(nèi)報錯
activeEffectStacks.push(effect);
fn();//利用了js是單線程的。
//fn觸發(fā)值戳,里邊讀取了proxy的屬性议谷,所以會進入到reactive的get中。
}finally{
activeEffectStacks.pop();//用完釋放
}
}
reactive的get中收集依賴
const toProxy= new WeakMap()
const toRaw = new WeakMap()
function trigger(){
console.log('觸發(fā)視圖更新')
}
function isObject(target){
return typeof target==='object'&&target!==null;
}
function reactive(target){
if(!isObject(target)){
return target;
}
//如果代理表中存在target了就說明堕虹,target已經(jīng)被代理過了卧晓,就返回代理過的對象
if(toProxy.get(target)){
console.log('target已經(jīng)被代理過了,是一個obj對象')
return toProxy.get(target)
}
if(toRaw.has(target)){//如果這個對象被代理過了赴捞,就把對象原封不動的返回
console.log('target已經(jīng)被代理過了,是一個proxy對象')
return target
}
const handler={
set(target,key,value,receiver){
// const oldValue = target[key];
const res = Reflect.set(target, key, value, receiver)
//如果觸發(fā)的是私有屬性的話逼裆,觸發(fā)視圖的更新
//if(!target.hasOwnProperty(key)){//添加新屬性,用hasOwnProperty是因為可以屏蔽一些無用的屬性更新赦政,像數(shù)組的length等(經(jīng)測試會引起bug)
//++++++++++++++++++++++++++++
// trigger(target,'add',key);
//++++++++++++++++++++++++++++
//}else if(value!==oldValue){//set的值變化胜宇,更新屬性
//++++++++++++++++++++++++++++
// trigger(target,'set',key);
//++++++++++++++++++++++++++++
//}
if (target.hasOwnProperty(key)) {
trigger(target, '', key);
}
return res
},
get(target,key,receiver){
const res=Reflect.get(target,key,receiver);
//++++++++++++++++++++++++++++++++++++++++++++++++
// 收集依賴 訂閱 ,把當前的key和effect對應起來
track(target,key) // 如果target上的這個key變化了重新讓數(shù)組中的effect執(zhí)行即可昼钻,所以需要建立target中的key于effect的關聯(lián)(關聯(lián)的結構如下圖所示)
//++++++++++++++++++++++++++++++++++++++++++++++++
if(isObject(res)){
return reactive(res)
}
return res;
},
deleteProperty(target,key){
return Reflect.deleteProperty(target,key);
}
}
let observer=new Proxy(target,handler);
toProxy.set(target,observer);//記錄原對象代理過的結果
toRaw.set(observer,target);
return observer;
}
track用于對應target中的key與effect的關聯(lián)
let targetMap=new WeakMap();
function track(target,key){//如果這個target中的key變化了 就執(zhí)行數(shù)組里activeEffectStacks的方法掸屡。
let effect=activeEffectStacks[activeEffectStacks.length-1];
if(effect){//有對應的關系 才創(chuàng)建關聯(lián)
let depsMap=targetMap.get(target);
if(!depsMap){
targetsMap.set(target,depsMap=new Map())
}
let deps=depsMap.get(key);
if(!deps){
depsMap.set(key,deps=new Set());
}
if(!deps.has(effect)){
deps.add(effect);
}
}
// 以上是動態(tài)創(chuàng)建依賴關系。
}
trigger 觸發(fā)key對應的effect然评,更新視圖
function trigger(target,type,key){
let depsMap=targetMap.get(target);
if(depsMap){
let deps=depsMap.get(key);
if(deps){//將當前key對應的effect依此執(zhí)行
deps.forEach(effect=>effect())
}
}
}
完整vue3.0響應式代碼
const toProxy = new WeakMap()
const toRaw = new WeakMap()
function isObject(target) {
return typeof target === 'object' && target !== null;
}
function reactive(target) {
if (!isObject(target)) {
return target;
}
// 如果代理表中存在target了就說明仅财,target已經(jīng)被代理過了,就返回代理過的對象
if (toProxy.get(target)) {
console.log('target已經(jīng)被代理過了碗淌,是一個obj對象')
return toProxy.get(target)
}
if (toRaw.has(target)) { // 如果這個對象被代理過了盏求,就把對象原封不動的返回
console.log('target已經(jīng)被代理過了,是一個proxy對象')
return target
}
const handler = {
set(target, key, value, receiver) {
// const oldValue = target[key];
const res = Reflect.set(target, key, value, receiver)
// 如果觸發(fā)的是私有屬性的話,觸發(fā)視圖的更新
// if (!target.hasOwnProperty(key)) { // 添加新屬性亿眠,用hasOwnProperty是因為可以屏蔽一些無用的屬性更新碎罚,像數(shù)組的length等(經(jīng)測試會引起bug)
// trigger(target, 'add', key);
// } else if (value !== oldValue) { // set的值變化,更新屬性
// console.log(target, key, oldValue, value, 24)
// trigger(target, 'set', key);
// }
if (target.hasOwnProperty(key)) {
trigger(target, '', key);
}
return res
},
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver);
// 收集依賴 訂閱 纳像,把當前的key和effect對應起來
track(target, key) // 如果target上的這個key變化了重新讓數(shù)組中的effect執(zhí)行即可荆烈,所以需要建立target中的key于effect的關聯(lián)
if (isObject(res)) {
return reactive(res)
}
return res;
},
}
const observer = new Proxy(target, handler);
toProxy.set(target, observer);// 記錄原對象代理過的結果
toRaw.set(observer, target);
return observer;
}
// 棧 先進后出
const activeEffectStacks = []; //
// 響應式 副作用
function effect(fn) {
// 需要把fn這個函數(shù)包裝成響應式的函數(shù),在把這個函數(shù)默認先執(zhí)行一次
const effect = createReactiveEffect(fn);
effect()// 默認執(zhí)行一次
}
function createReactiveEffect(fn) {
const effect = function () { // 這個就是創(chuàng)建的響應式的effect
return run(effect, fn) // 運行 1.讓fn執(zhí)行 2.把這個effect存入到棧中
}
return effect;// 返回這個包裝好的函數(shù)
}
function run(effect, fn) { // 運行fn并把effect存起來
try { // try包裹防止fn內(nèi)報錯
activeEffectStacks.push(effect);
fn();// 利用了js是單線程的。
// fn觸發(fā),里邊讀取了proxy的屬性憔购,所以會進入到reactive的get中宫峦。
} finally {
activeEffectStacks.pop();// 用完釋放
}
}
const targetMap = new WeakMap();
function track(target, key) { // 如果這個target中的key變化了 就執(zhí)行數(shù)組里activeEffectStacks的方法。
console.log(activeEffectStacks, target, '-----------track')
const effect = activeEffectStacks[activeEffectStacks.length - 1];
if (effect) { // 有對應的關系 才創(chuàng)建關聯(lián)
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, depsMap = new Map())
}
let deps = depsMap.get(key);
if (!deps) {
depsMap.set(key, deps = new Set());
}
if (!deps.has(effect)) {
deps.add(effect);
}
}
}
// 以上是動態(tài)創(chuàng)建依賴關系玫鸟。
function trigger(target, type, key) {
const depsMap = targetMap.get(target);
console.log(target, depsMap, '--------trigger')
if (depsMap) {
const deps = depsMap.get(key);
if (deps) { // 將當前key對應的effect依此執(zhí)行
console.log(deps.size)
deps.forEach(effect => effect())
}
}
}
let obj={name:'haha'};
let observer= reactive(obj);
effect(()=>{
console.log(observer.name);
})
observer.name='xixi';
observer.name='xixi';
//執(zhí)行兩次haha和xixi
(以下不會再次觸發(fā)更新的情況)
//1.對象只監(jiān)聽到了外層
const obj = { name: { n: 'haha' } };
const observer = reactive(obj);
effect(() => {
console.log(observer.name, '-------------108');
})
observer.name.n = 'xixi';
//必須這樣監(jiān)聽
const obj = { name: { n: 'haha' } };
const observer = reactive(obj);
effect(() => {
console.log(observer.name.n);
})
observer.name.n = 'xixi';
//2.對象內(nèi)層的數(shù)組操作問題
const obj1 = { a: [1, 2, 3] };
const oarr = reactive(obj1);
effect(() => {
oarr.a.forEach((item)=>{
console.log(item,'更新視圖');
})
})
oarr.a[0]=2;
oarr.a.push(4);
原因是:
set(target, key, value, receiver) {
// const oldValue = target[key];
const res = Reflect.set(target, key, value, receiver)
// 添加的新屬性或數(shù)組的項會導致更新失效
// if (!target.hasOwnProperty(key)) {
// // trigger(target, 'add', key);
// } else if (value !== oldValue) {
// console.log(target, 'key:', key, oldValue, value)
// trigger(target, 'set', key);
// console.log(value, oldValue)
// }
//以上處理方式需要改為
//如果數(shù)組方法過濾掉length等屬性會出問題
if (target.hasOwnProperty(key)) {
trigger(target, '', key);
}
return res
},
備上html測試方式
<div id="box" style="margin-left: 20px;">
<span id="app"></span>
<span id="add" style="margin-left: 10px;display: inline-block;width: 10px;height: 10px;cursor: pointer;">+</span>
</div>
<script src="./vue3.js"></script>
<script>
let ele = document.getElementById('app');
let btn = document.getElementById('add');
let o = {
number:{name:1},
}
let i=0;
let reactiveData = reactive(o);
effect(()=>{
console.log(reactiveData.number,'------')
})
btn.addEventListener('click', () => {
reactiveData.number.name=i++;
},false)
// let o = {
// number: [1],
// }
// let i=0;
// let reactiveData = reactive(o);
// effect(()=>{
// console.log(reactiveData.number,'------')
// ele.innerHTML = reactiveData.number
// })
// btn.addEventListener('click', () => {
// reactiveData.number.push(i++);
// },false)
</script>