尤大大在B站進行了直播講解Vue3.0的一些新特性,因爲時間關係 我並無遇上直播 後期觀看了一些視頻和文章對3.0的特性有了必定的瞭解,特此寫下此Blog進行記錄.vue
ts
實現了類型推斷,新版api所有采用普通函數,在編寫代碼時能夠享受完整的類型推斷(避免使用裝飾器)<script src="vue.global.js"></script> <div id="container"></div> <script> function usePosition(){ // 實時獲取鼠標位置 let state = Vue.reactive({x:0,y:0}); function update(e) { state.x= e.pageX state.y = e.pageY } Vue.onMounted(() => { window.addEventListener('mousemove', update) }) Vue.onUnmounted(() => { window.removeEventListener('mousemove', update) }) return Vue.toRefs(state); } const App = { setup(){ // Composition API 使用的入口 const state = Vue.reactive({name:'youxuan'}); // 定義響應數據 const {x,y} = usePosition(); // 使用公共邏輯 Vue.onMounted(()=>{ console.log('當組掛載完成') }); Vue.onUpdated(()=>{ console.log('數據發生更新') }); Vue.onUnmounted(()=>{ console.log('組件將要卸載') }) function changeName(){ state.name = 'webyouxuan'; } return { // 返回上下文,能夠在模板中使用 state, changeName, x, y } }, template:`<button @click="changeName">{{state.name}} 鼠標x: {{x}} 鼠標: {{y}}</button>` } Vue.createApp().mount(App,container); </script>
簡單能夠理解爲 將各個功能模塊聚合react
無需相似Vue2.0 按照特定的格式劃分 將模塊的各個功能分散於不一樣的生命週期鉤子函數中,項目各個模塊之間耦合更低。web
首先總結回憶一下2.0中響應式實現的原理機制api
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';
首先寫一個vue方法,在裏面定義所須要的數據,用vue.prototype.obersever註冊get和set,遍歷全部的obj而後取到每個obj裏面每個obj[i],而後判斷obj[i]的typeof是否是object,若是是那麼從新遍歷若是不是那麼利用obj.defineproperty進行存取數據,get是用了收集依賴,set裏面有一個newvalue是=你的value的,而後渲染render(),這樣就註冊完setget了,而後獲取新的值而後從新渲染。js部分完成。在頁面引入寫好的js而後new一個vue數組
由於defineProperty
是沒法監聽數組變化的,因此在vue中其實是經過hack了Array原型上的push
和pop
等方法來進行對數組的監聽的緩存
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]) } }
let obj = {hobby:[{name:'youxuan'},'喝']} observer(obj) obj.hobby[0].name = 'webyouxuan'; // 更改數組中的對象也會觸發試圖更新 console.log(obj)
數組監聽實現app
先把array.prototype
取出來,而後在拷貝一份用obj.create
(拷貝是爲了防止在修改的時候影響到原來的原型鏈),而後定義一個儲存着數組方法的數組arr,對arr進行forEach
循環,每次循環給拷貝的對象設置一個重寫也就是作一個裝飾着模式,原型鏈自己有數組方法,因此拷貝出來的對象也有那些方法。重寫先去把剛開始的原型鏈上的本來的方法好比push
方法用apply(this,arguments)而後再去觸發視圖更新,而後把這個prototype
關聯到get
上的prototype
,將prototype
替換,這樣push
方法就即會執行原來的push
方法又會執行觸發視圖更新函數
首先必須瞭解ES6中的Proxy,Reflect及Map,Setthis
Reflectprototype
Reflect 是一個內置的對象,它提供攔截 JavaScript 操做的方法。這些方法與proxy handlers的方法相同。Reflect不是一個函數對象,所以它是不可構造的。
Proxy
Proxy 對象用於定義基本操做的自定義行爲(如屬性查找、賦值、枚舉、函數調用等)。
const p = new Proxy(target, handler)
***target
***要使用 Proxy
包裝的目標對象(能夠是任何類型的對象,包括原生數組,函數,甚至另外一個代理)。
***handler
***一個一般以函數做爲屬性的對象,各屬性中的函數分別定義了在執行各類操做時代理 p
的行爲。
let p = Vue.reactive({name:'youxuan'}); Vue.effect(()=>{ // effect方法會當即被觸發 console.log(p.name); }) p.name = 'webyouxuan';; // 修改屬性後會再次觸發effect方法
reactive
方法實現經過proxy 自定義獲取、增長、刪除等行爲
function reactive(target){ // 建立響應式對象 return createReactiveObject(target); } function isObject(target){ return typeof target === 'object' && target!== null; } 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; // 刪除
那麼如何實現多層代理呢?
let p = reactive({ name: "youxuan", age: { num: 10 } }); p.age.num = 11
因爲咱們只代理了第一層對象,因此對age對象進行更改是不會觸發set方法的,可是卻觸發了get方法,這是因爲 p.age會形成 get操做
get(target, key, receiver) { // 取值 console.log("獲取"); let res = Reflect.get(target, key, receiver); return isObject(res) // 懶代理,只有當取值時再次作代理,vue2.0中一上來就會所有遞歸增長getter,setter ? reactive(res) : res; }
這裏咱們將p.age取到的對象再次進行代理,這樣在去更改值便可觸發set方法
接下來考慮一下數組的問題吧
咱們能夠發現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; }
到這裏reactive方法基本實現完畢,接下來就是與Vue2中的邏輯同樣實現依賴收集和觸發更新
get(target, key, receiver) { let res = Reflect.get(target, key, receiver); + track(target,'get',key); // 依賴收集 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){ + trigger(target,'add',key); // 觸發添加 }else if(oldValue !== value){ + trigger(target,'set',key); // 觸發修改 } return result; }
track的做用是依賴收集,收集的主要是effect,咱們先來實現effect原理,以後再完善 track和trigger方法
effect意思是反作用,此方法默認會先執行一次。若是數據變化後會再次觸發此回調函數。
let school = {name:'youxuan'} let p = reactive(school); effect(()=>{ console.log(p.name); // youxuan })
咱們來實現effect
方法,咱們須要將effect
方法包裝成響應式effect
。
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){ // 查看是否有effectconst 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(); }) } }
咱們發現以下問題
let school = [1,2,3]; let p = reactive(school); effect(()=>{ console.log(p.length); }) p.push(100);
新增了值,effect方法並未從新執行,由於push中修改length已經被咱們屏蔽掉了觸發trigger方法,因此當新增項時應該手動觸發length屬性所對應的依賴。
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能夠將原始數據類型也轉換成響應式數據,須要經過.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; }
問題又來了咱們再編寫個案例
let r = ref(1); let c = reactive({ a:r }); console.log(c.a.value);
這樣作的話豈不是每次都要多來一個.value,這樣太難用了
在get
方法中判斷若是獲取的是ref
的值,就將此值的value
直接返回便可
let res = Reflect.get(target, key, receiver); if(res._isRef){ return res.value }
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變成trueconsole.log(c.value); // 從新調用計算方法