帶你瞭解 vue-next(Vue 3.0)之 爐火純青

看完上兩章 初入茅廬 小試牛刀 以後,你們應該對vue-next(Vue 3.0) 的 API 使用已經瞭如指掌了。好奇的同窗必定對 vue-next 響應式的原理充滿好奇,本章就帶你解密!javascript

前言

最新vue-next 的源碼發佈了,雖然是 pre-alpha 版本,但這時候實際上是閱讀源碼的比較好的時機。在 vue 中,比較重要的東西固然要數它的響應式系統,在以前的版本中,已經有若干篇文章對它的響應式原理和實現進行了介紹,這裏就不贅述了。在 vue-next 中,其實現原理和以前仍是相同的,即經過觀察者模式和數據劫持,只不過對其實現方式進行了改變。html

所以,這篇文章我也打算按這種風格來寫一下利用最近空閒時間閱讀 vue-next 響應式模塊的源碼的一些心得與體會,算是拋磚引玉,同時實現一個極簡的響應式系統。vue

若有錯誤,還望指正。java

vue-next 數據響應機制 - Proxy

在學習vue-next以前,你必需要先熟練掌握ES6中的 ProxyReflectES6中爲咱們提供的 MapSet兩種數據結構react

先應用再說原理:git

const { reactive, effect} = Vue

let p = reactive({name:'zhuanzhuan'});

// effect方法會當即被觸發
effect(()=>{ 
    console.log(p.name);
})
p.name = '轉轉'; // 修改屬性後會再次觸發effect方法
複製代碼

源碼是採用ts編寫,爲了便於你們理解原理,這裏咱們採用js來從0編寫,以後再看源碼就很是的輕鬆啦!github

reactive方法實現

看源碼面試

function reactive(target) {
      // if trying to observe a readonly proxy, return the readonly version.
      if (readonlyToRaw.has(target)) {
          return target;
      }
      // target is explicitly marked as readonly by user
      if (readonlyValues.has(target)) {
          return readonly(target);
      }
      return createReactiveObject(
        target, 
        rawToReactive, 
        reactiveToRaw, 
        mutableHandlers, 
        mutableCollectionHandlers
        );
  }

function createReactiveObject( target, toProxy, toRaw, baseHandlers, collectionHandlers ) {
      if (!isObject(target)) {
          {
              console.warn(`value cannot be made reactive: ${String(target)}`);
          }
          return target;
      }
      // target already has corresponding Proxy
      let observed = toProxy.get(target);
      if (observed !== void 0) {
          return observed;
      }
      // target is already a Proxy
      if (toRaw.has(target)) {
          return target;
      }
      // only a whitelist of value types can be observed.
      if (!canObserve(target)) {
          return target;
      }
      const handlers = collectionTypes.has(target.constructor)
          ? collectionHandlers
          : baseHandlers;
      observed = new Proxy(target, handlers);
      toProxy.set(target, observed);
      toRaw.set(observed, target);
      return observed;
  }
複製代碼

稍微精簡下chrome

function reactive(target) {
    const handlers = collectionTypes.has(target.constructor)
        ? collectionHandlers
        : baseHandlers
    observed = new Proxy(target, handlers)
    return observed
}
複製代碼
const collectionTypes = new Set([Set, Map, WeakMap, WeakSet]);
複製代碼

基本上除了Set, Map, WeakMap, WeakSet,都是baseHandlerssegmentfault

baseHandlers實現:

function createGetter(isReadonly, shallow = false) {
      return function get(target, key, receiver) {
          const res = Reflect.get(target, key, receiver);
          if (isSymbol(key) && builtInSymbols.has(key)) {
              return res;
          }
          if (shallow) {
              track(target, "get" /* GET */, key);
              // TODO strict mode that returns a shallow-readonly version of the value
              return res;
          }
          if (isRef(res)) {
              return res.value;
          }
          track(target, "get" /* GET */, key);
          return isObject(res)
              ? isReadonly
                  ? // need to lazy access readonly and reactive here to avoid
                      // circular dependency
                      readonly(res)
                  : reactive(res)
              : res;
      };
  }
複製代碼

返回值若是是object,就再走一次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:'zhuanzhuan'});

console.log(p.name); // 獲取

p.name = '轉轉'; // 設置
delete p.name; // 刪除
複製代碼

咱們繼續考慮多層對象如何實現代理

let p = reactive({ name: "zhuanzhuan", age: { num: 3 } });
p.age.num = 4
複製代碼

因爲咱們只代理了第一層對象,因此對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

看下源碼是如何處理的:

很簡單,用的hasOwProperty, set確定會出發屢次,可是通知只出去一次, 好比數組修改length的時候,hasOwPropertytrue, 那就不觸發

function set(target, key, value, receiver) {
      value = toRaw(value);
      const oldValue = target[key];
      if (isRef(oldValue) && !isRef(value)) {
          oldValue.value = value;
          return true;
      }
      const hadKey = hasOwn(target, key);
      const result = Reflect.set(target, key, value, receiver);
      // don't trigger if target is something up in the prototype chain of original
      if (target === toRaw(receiver)) {
          /* istanbul ignore else */
          {
              const extraInfo = { oldValue, newValue: value };
              if (!hadKey) {
                  trigger(target, "add" /* ADD */, key, extraInfo);
              }
              else if (hasChanged(value, oldValue)) {
                  trigger(target, "set" /* SET */, key, extraInfo);
              }
          }
      }
      return result;
  }
複製代碼

咱們來屏蔽掉屢次觸發,更新操做

function hasOwn(target,key){
  return target.hasOwnProperty(key);
}

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

image

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原理,以後再完善 tracktrigger方法

effect實現

effect意思是反作用,此方法默認會先執行一次。若是數據變化後會再次觸發此回調函數。

const p = reactive({name:'zhuanzhuan'})

effect(()=>{
    console.log(p.name);  // zhuanzhuan
})
複製代碼

咱們來實現effect方法,咱們須要將effect方法包裝成響應式effect

const activeReactiveEffectStack = []; // 存放響應式effect

function effect(fn) {
  const effect = createReactiveEffect(fn); // 建立響應式的effect
  effect(); // 先執行一次
  return 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屬性所對應的依賴。

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;
}
複製代碼

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

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實現

computed 實現也是基於 effect 來實現的,特色是computed中的函數不會當即執行,屢次取值是有緩存機制的

先來看用法:

let a = reactive({name:'zhuanzhuan'});

let c = computed(()=>{
  console.log('執行次數')
  return a.name +'今年3歲了';
})
// 不取不執行,取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:'zhuanzhuan'});

let c = computed(()=>{
  console.log('執行次數')
  return a.name +'今年3歲了';
})
// 不取不執行,取n次只執行一次
console.log(c.value);
a.name = '轉轉'; // 更改值 不會觸發從新計算,可是會將dirty變成true

console.log(c.value); // 從新調用計算方法

複製代碼

實現 vue-next 極簡的響應式系統

直接拷貝下面代碼,去運行看效果吧。推薦使用高版本的chrome瀏覽器!

my-vue-next.js 文件

// 存放被代理過的對象
let toProxy = new WeakMap()
// 存放已經代理過的對象
let toRaw = new WeakMap()
let tagetMap = new WeakMap()
let effectStack = []

const baseHander = {
  get(target, key){
    const res = Reflect.get(target, key)
    // 收集依賴
    track(target, key)
    // 遞歸尋找
    return typeof res == 'object' ? reactive(res) : res
  },
  set(target, key, val){
    const info = {oldValue: target[key], newValue:val}
    const res = Reflect.set(target, key, val)
    // 觸發更新
    trigger(target, key, info)
    return res
  }
}
function reactive(target){
  // 查詢緩存
  let observed = toProxy.get(target)
  if(observed){
    return observed
  }
  // 若是已經代理過了這個對象,則直接返回代理後的結果便可
  if(toRaw.get(target)){
    return target
  }
  observed = new Proxy(target, baseHander)
  // 設置緩存
  toProxy.set(target, observed)
  toRaw.set(observed, target)
  return observed
}

function trigger(target, key, info){
  // 觸發更新
  const depsMap = tagetMap.get(target)

  if(depsMap===undefined){
    return
  }
  const effects = new Set()
  const computedRunners = new Set()
  if(key){
    let deps = depsMap.get(key)

    if(!deps) return

    deps.forEach(effect=>{
      if(effect.computed){
        computedRunners.add(effect)
      }else{
        effects.add(effect)
      }
    })
  }
  effects.forEach(effect=> effect())
  computedRunners.forEach(effect=> effect())
}

function track(target, key){
  let effect = effectStack[effectStack.length - 1]

  if(effect){
    let depsMap = tagetMap.get(target)
    if(depsMap===undefined){
      depsMap = new Map()
      tagetMap.set(target, depsMap)
    }
    let dep = depsMap.get(key)
    if(dep===undefined){
      dep = new Set()
      depsMap.set(key, dep)
    }
    if(!dep.has(effect)){
      dep.add(effect)
    }
  }
}

// 存儲effect
function effect(fn,options={}){
  let e = createReactiveEffect(fn, options)

  // 首次頁面加載就須要先運行一次 effect 方法,讓頁面渲染
  if(!options.lazy){
    e()
  }
  return e
}

function createReactiveEffect(fn,options){
  const effect = function(...args){
    return run(effect, fn , args)
  }
  // 爲了調試查看
  effect.fn = fn
  effect.computed = options.computed
  effect.lazy = options.lazy
  return effect
}

function run(effect, fn , args){
  if(effectStack.indexOf(effect)===-1){
    try{
      effectStack.push(effect)
      return fn(...args)
    }
    finally{
      effectStack.pop()
    }
  }
}

function computed(fn){
  const runner = effect(fn,{computed:true, lazy:true})
  return {
    effect:runner,
    get value(){
      return runner()
    }
  }
}

複製代碼

index.html 文件

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
  <script src="./my-vue-next.js"></script>
</head>
<body>
  <div id='app'></div>
  <button id="btn">點我</button>
  <script> const root = document.querySelector('#app') const btn = document.querySelector('#btn') const obj = reactive({ name:'轉轉', age: 3 }) const double = computed(()=> obj.age *2) effect(()=>{ root.innerHTML = `<h1>${obj.name}今年${obj.age}歲了,乘以2是${double.value}</h1>` }) btn.addEventListener('click', ()=>{ const age = obj.age obj.age = age + 1 }, false) </script>
</body>
</html>


複製代碼

參考


看完初入茅廬小試牛刀爐火純青三章以後, 已經將vue-next核心的 Composition Api 就講解完畢了! 不論是面試仍是後期的應用也不再須要擔憂啦!~

相關文章
相關標籤/搜索