Vue 3.0 初相識

尤大大在B站進行了直播講解Vue3.0的一些新特性,因爲時間關係 我並無遇上直播 後期觀看了一些視頻和文章對3.0的特性有了必定的瞭解,特此寫下此Blog進行記錄.vue

1.剖析Vue Composition API

  • Vue 3 使用ts實現了類型推斷,新版api所有采用普通函數,在編寫代碼時能夠享受完整的類型推斷(避免使用裝飾器)
  • 解決了多組件間邏輯重用問題 (解決:高階組件、mixin、做用域插槽)
  • Composition 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.響應式原理機制變化

首先總結回憶一下2.0中響應式實現的原理機制api

2.1 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';

首先寫一個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數組

2.2數組劫持

由於defineProperty是沒法監聽數組變化的,因此在vue中其實是經過hack了Array原型上的pushpop等方法來進行對數組的監聽的緩存

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])
    }
}

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/88b53bdb-4e47-499f-9a64-12c28944cf85/Untitled.png

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方法又會執行觸發視圖更新函數

3.Vue3.0的監聽實現——Proxy

首先必須瞭解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方法

3.1 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中的邏輯同樣實現依賴收集和觸發更新

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/f2f74079-c404-498d-9b5b-aa674c954215/Untitled.png

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方法

3.2effect實現

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();
      });
    }
  }
}

3.3 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;
}

問題又來了咱們再編寫個案例

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
}

3.4computed實現

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); // 從新調用計算方法
相關文章
相關標籤/搜索