vue 3.0 原理源碼進階


在2019.10.5日發布了Vue3.0預覽版源碼,但是預計最早需要等到 2020 年第一季度才有可能發布 3.0 正式版。

可以直接看 github源碼。

新版Vue 3.0計划並已實現的主要架構改進和新功能:

  • 編譯器(Compiler)

    • 使用模塊化架構
    • 優化 "Block tree"
    • 更激進的 static tree hoisting 功能 (檢測靜態語法,進行提升)
    • 支持 Source map
    • 內置標識符前綴(又名"stripWith")
    • 內置整齊打印(pretty-printing)功能
    • 移除 Source map 和標識符前綴功能后,使用 Brotli 壓縮的瀏覽器版本精簡了大約10KB
  • 運行時(Runtime)

    • 速度顯著提升
    • 同時支持 Composition API 和 Options API,以及 typings
    • 基於 Proxy 實現的數據變更檢測
    • 支持 Fragments (允許組件有從多個根結點)
    • 支持 Portals (允許在DOM的其它位置進行渲染)
    • 支持 Suspense w/ async setup()
目前不支持 IE11

1.剖析Vue Composition API

可以去看官方地址

  • Vue 3 使用ts實現了類型推斷,新版api全部采用普通函數,在編寫代碼時可以享受完整的類型推斷(避免使用裝飾器)
  • 解決了多組件間邏輯重用問題 (解決:高階組件、mixin、作用域插槽)
  • Composition API 使用簡單

先嘗鮮Vue3.0看看效果

<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>
到這里你會發現響應式才是 Vue的靈魂

2.源碼目錄剖析

packages目錄中包含着Vue3.0所有功能

├── packages
│   ├── compiler-core # 所有平台的編譯器 │   ├── compiler-dom # 針對瀏覽器而寫的編譯器 │   ├── reactivity # 數據響應式系統 │   ├── runtime-core # 虛擬 DOM 渲染器 ,Vue 組件和 Vue 的各種API │   ├── runtime-dom # 針對瀏覽器的 runtime。其功能包括處理原生 DOM API、DOM 事件和 DOM 屬性等。 │   ├── runtime-test # 專門為測試寫的runtime │   ├── server-renderer # 用於SSR │   ├── shared # 幫助方法 │   ├── template-explorer │   └── vue # 構建vue runtime + compiler

compiler
compiler-core主要功能是暴露編譯相關的API以及baseCompile方法
compiler-dom基於compiler-core封裝針對瀏覽器的compiler (對瀏覽器標簽進行處理)

runtime
runtime-core 虛擬 DOM 渲染器、Vue 組件和 Vue 的各種API
runtime-testDOM結構格式化成對象,方便測試
runtime-dom 基於runtime-core編寫的瀏覽器的runtime (增加了節點的增刪改查,樣式處理等),返回rendercreateApp方法

reactivity
單獨的數據響應式系統,核心方法reactiveeffect、 refcomputed

vue
整合 compiler + runtime

到此我們解析了 Vue3.0結構目錄,整體來看整個項目還是非常清晰的

再來嘗嘗鮮:
我們可以根據官方的測試用例來看下如何使用Vue3.0

const app = { template:`<div>{{count}}</div>`, data(){ return {count:100} }, } let proxy = Vue.createApp().mount(app,container); setTimeout(()=>{ proxy.count = 200; },2000)
接下來我們來對比 Vue 2 和 Vue 3 中的響應式原理區別

3.Vue2.0響應式原理機制 - defineProperty

這個原理老生常談了,就是攔截對象,給對象的屬性增加set 和 get方法,因為核心是defineProperty所以還需要對數組的方法進行攔截

3.1 對對象進行攔截

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

3.2 數組方法劫持

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)
這里依賴收集的過程就不詳細描述了,我們把焦點放在 Vue3.0
  • Object.defineProperty缺點

    • 無法監聽數組的變化
    • 需要深度遍歷,浪費內存

4.Vue3.0數據響應機制 - Proxy

在學習Vue3.0之前,你必須要先熟練掌握ES6中的 ProxyReflect 及 ES6中為我們提供的 MapSet兩種數據結構

先應用再說原理:

let p = Vue.reactive({name:'youxuan'}); Vue.effect(()=>{ // effect方法會立即被觸發 console.log(p.name); }) p.name = 'webyouxuan';; // 修改屬性后會再次觸發effect方法
源碼是采用 ts編寫,為了便於大家理解原理,這里我們采用js來從0編寫,之后再看源碼就非常的輕松啦!

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

tupian

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

4.2 effect實現

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){ // 查看是否有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(); }) } }

我們發現如下問題

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

4.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 }

4.4 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); // 重新調用計算方法
到此我們將 Vue3.0核心的  Composition Api 就講解完畢了! 不管是面試還是后期的應用也再也不需要擔心啦!~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM