在2019.10.5日發(fā)布了Vue3.0
預(yù)覽版源碼诲宇,但是預(yù)計最早需要等到 2020 年第一季度才有可能發(fā)布 3.0 正式版居灯。
可以直接看 github源碼宿礁。
新版Vue 3.0
計劃并已實現(xiàn)的主要架構(gòu)改進和新功能:
- 編譯器(Compiler)
- 使用模塊化架構(gòu)
- 優(yōu)化 "Block tree"
- 更激進的 static tree hoisting 功能 (檢測靜態(tài)語法墩衙,進行提升)
- 支持 Source map
- 內(nèi)置標(biāo)識符前綴(又名"stripWith")
- 內(nèi)置整齊打诱⒌(pretty-printing)功能
- 移除 Source map 和標(biāo)識符前綴功能后泌霍,使用 Brotli 壓縮的瀏覽器版本精簡了大約10KB
- 運行時(Runtime)
- 速度顯著提升
- 同時支持 Composition API 和 Options API货抄,以及 typings
- 基于 Proxy 實現(xiàn)的數(shù)據(jù)變更檢測
- 支持 Fragments (允許組件有從多個根結(jié)點)
- 支持 Portals (允許在DOM的其它位置進行渲染)
- 支持 Suspense w/ async setup()
目前不支持
IE11
1.剖析Vue Composition API
可以去看官方地址
- Vue 3 使用
ts
實現(xiàn)了類型推斷,新版api全部采用普通函數(shù)烹吵,在編寫代碼時可以享受完整的類型推斷(避免使用裝飾器) - 解決了多組件間邏輯重用問題 (解決:高階組件碉熄、mixin、作用域插槽)
- Composition API 使用簡單
先嘗鮮Vue3.0看看效果
<script src="vue.global.js"></script>
<div id="container"></div>
<script>
function usePosition(){ // 實時獲取鼠標(biāo)位置
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'}); // 定義響應(yīng)數(shù)據(jù)
const {x,y} = usePosition(); // 使用公共邏輯
Vue.onMounted(()=>{
console.log('當(dāng)組掛載完成')
});
Vue.onUpdated(()=>{
console.log('數(shù)據(jù)發(fā)生更新')
});
Vue.onUnmounted(()=>{
console.log('組件將要卸載')
})
function changeName(){
state.name = 'webyouxuan';
}
return { // 返回上下文,可以在模板中使用
state,
changeName,
x,
y
}
},
template:`<button @click="changeName">{{state.name}} 鼠標(biāo)x: {{x}} 鼠標(biāo): {{y}}</button>`
}
Vue.createApp().mount(App,container);
</script>
到這里你會發(fā)現(xiàn)響應(yīng)式才是
Vue
的靈魂
2.源碼目錄剖析
packages目錄中包含著Vue3.0
所有功能
├── packages
│ ├── compiler-core # 所有平臺的編譯器
│ ├── compiler-dom # 針對瀏覽器而寫的編譯器
│ ├── reactivity # 數(shù)據(jù)響應(yīng)式系統(tǒng)
│ ├── runtime-core # 虛擬 DOM 渲染器 肋拔,Vue 組件和 Vue 的各種API
│ ├── runtime-dom # 針對瀏覽器的 runtime锈津。其功能包括處理原生 DOM API、DOM 事件和 DOM 屬性等凉蜂。
│ ├── runtime-test # 專門為測試寫的runtime
│ ├── server-renderer # 用于SSR
│ ├── shared # 幫助方法
│ ├── template-explorer
│ └── vue # 構(gòu)建vue runtime + compiler
compiler
compiler-core
主要功能是暴露編譯相關(guān)的API
以及baseCompile
方法
compiler-dom
基于compiler-core
封裝針對瀏覽器的compiler
(對瀏覽器標(biāo)簽進行處理)
runtime
runtime-core
虛擬 DOM 渲染器琼梆、Vue 組件和 Vue 的各種API
runtime-test
將DOM
結(jié)構(gòu)格式化成對象性誉,方便測試
runtime-dom
基于runtime-core
編寫的瀏覽器的runtime
(增加了節(jié)點的增刪改查,樣式處理等)茎杂,返回render
错览、createApp
方法
reactivity
單獨的數(shù)據(jù)響應(yīng)式系統(tǒng),核心方法reactive
煌往、effect
倾哺、 ref
、computed
vue
整合 compiler
+ runtime
到此我們解析了
Vue3.0
結(jié)構(gòu)目錄刽脖,整體來看整個項目還是非常清晰的
再來嘗嘗鮮:
我們可以根據(jù)官方的測試用例來看下如何使用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 中的響應(yīng)式原理區(qū)別
3.Vue2.0響應(yīng)式原理機制 - defineProperty
這個原理老生常談了羞海,就是攔截對象
,給對象的屬性增加set
和 get
方法曲管,因為核心是defineProperty
所以還需要對數(shù)組的方法進行攔截
3.1 對對象進行攔截
function observer(target){
// 如果不是對象數(shù)據(jù)類型直接返回即可
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方法中觸發(fā)更新
}
}
})
}
let obj = {name:'youxuan'}
observer(obj);
obj.name = 'webyouxuan';
3.2 數(shù)組方法劫持
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
}
// 如果不是對象數(shù)據(jù)類型直接返回即可
if(Array.isArray(target)){
Object.setPrototypeOf(target,proto);
// 給數(shù)組中的每一項進行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'; // 更改數(shù)組中的對象也會觸發(fā)試圖更新
console.log(obj)
這里依賴收集的過程就不詳細描述了,我們把焦點放在
Vue3.0
上
-
Object.defineProperty缺點
- 無法監(jiān)聽數(shù)組的變化
- 需要深度遍歷院水,浪費內(nèi)存
4.Vue3.0數(shù)據(jù)響應(yīng)機制 - Proxy
在學(xué)習(xí)Vue3.0之前腊徙,你必須要先熟練掌握ES6中的 Proxy、Reflect 及 ES6中為我們提供的 Map檬某、Set兩種數(shù)據(jù)結(jié)構(gòu)
先應(yīng)用再說原理:
let p = Vue.reactive({name:'youxuan'});
Vue.effect(()=>{ // effect方法會立即被觸發(fā)
console.log(p.name);
})
p.name = 'webyouxuan';; // 修改屬性后會再次觸發(fā)effect方法
源碼是采用
ts
編寫撬腾,為了便于大家理解原理,這里我們采用js來從0編寫橙喘,之后再看源碼就非常的輕松啦时鸵!
4.1 reactive方法實現(xiàn)
通過proxy 自定義獲取、增加厅瞎、刪除等行為
function reactive(target){
// 創(chuàng)建響應(yīng)式對象
return createReactiveObject(target);
}
function isObject(target){
return typeof target === 'object' && target!== null;
}
function createReactiveObject(target){
// 判斷target是不是對象,不是對象不必繼續(xù)
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('設(shè)置')
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'; // 設(shè)置
delete p.name; // 刪除
我們繼續(xù)考慮多層對象如何實現(xiàn)代理
let p = reactive({ name: "youxuan", age: { num: 10 } });
p.age.num = 11
由于我們只代理了第一層對象初坠,所以對
age
對象進行更改是不會觸發(fā)set方法的和簸,但是卻觸發(fā)了get
方法,這是由于p.age
會造成get
操作
get(target, key, receiver) {
// 取值
console.log("獲取");
let res = Reflect.get(target, key, receiver);
return isObject(res) // 懶代理碟刺,只有當(dāng)取值時再次做代理锁保,vue2.0中一上來就會全部遞歸增加getter,setter
? reactive(res) : res;
}
這里我們將
p.age
取到的對象再次進行代理,這樣在去更改值即可觸發(fā)set
方法
我們繼續(xù)考慮數(shù)組問題
我們可以發(fā)現(xiàn)Proxy
默認可以支持數(shù)組半沽,包括數(shù)組的長度變化以及索引值的變化
let p = reactive([1,2,3,4]);
p.push(5);
但是這樣會觸發(fā)兩次
set
方法爽柒,第一次更新的是數(shù)組中的第4
項,第二次更新的是數(shù)組的length
我們來屏蔽掉多次觸發(fā)者填,更新操作
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('更新 修改')
}
// 當(dāng)調(diào)用push 方法第一次修改時數(shù)組長度已經(jīng)發(fā)生變化
// 如果這次的值和上次的值一樣則不觸發(fā)更新
return result;
}
解決重復(fù)使用reactive情況
// 情況1.多次代理同一個對象
let arr = [1,2,3,4];
let p = reactive(arr);
reactive(arr);
// 情況2.將代理后的結(jié)果繼續(xù)代理
let p = reactive([1,2,3,4]);
reactive(p);
通過
hash表
的方式來解決重復(fù)代理的情況
const toProxy = new WeakMap(); // 存放被代理過的對象
const toRaw = new WeakMap(); // 存放已經(jīng)代理過的對象
function reactive(target) {
// 創(chuàng)建響應(yīng)式對象
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)){ // 判斷是否要重復(fù)代理
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
方法基本實現(xiàn)完畢,接下來就是與Vue2中的邏輯一樣實現(xiàn)依賴收集和觸發(fā)更新
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); // 觸發(fā)添加
}else if(oldValue !== value){
+ trigger(target,'set',key); // 觸發(fā)修改
}
return result;
}
track的作用是依賴收集占哟,收集的主要是
effect
心墅,我們先來實現(xiàn)effect
原理酿矢,之后再完善track
和trigger
方法
4.2 effect實現(xiàn)
effect意思是副作用,此方法默認會先執(zhí)行一次怎燥。如果數(shù)據(jù)變化后會再次觸發(fā)此回調(diào)函數(shù)瘫筐。
let school = {name:'youxuan'}
let p = reactive(school);
effect(()=>{
console.log(p.name); // youxuan
})
我們來實現(xiàn)effect
方法,我們需要將effect
方法包裝成響應(yīng)式effect
铐姚。
function effect(fn) {
const effect = createReactiveEffect(fn); // 創(chuàng)建響應(yīng)式的effect
effect(); // 先執(zhí)行一次
return effect;
}
const activeReactiveEffectStack = []; // 存放響應(yīng)式effect
function createReactiveEffect(fn) {
const effect = function() {
// 響應(yīng)式的effect
return run(effect, fn);
};
return effect;
}
function run(effect, fn) {
try {
activeReactiveEffectStack.push(effect);
return fn(); // 先讓fn執(zhí)行,執(zhí)行時會觸發(fā)get方法策肝,可以將effect存入對應(yīng)的key屬性
} finally {
activeReactiveEffectStack.pop(effect);
}
}
當(dāng)調(diào)用fn()
時可能會觸發(fā)get
方法,此時會觸發(fā)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添加到依賴中
}
}
}
當(dāng)更新屬性時會觸發(fā)trigger
執(zhí)行隐绵,找到對應(yīng)的存儲集合拿出effect
依次執(zhí)行
function trigger(target,type,key){
const depsMap = targetMap.get(target);
if(!depsMap){
return
}
let effects = depsMap.get(key);
if(effects){
effects.forEach(effect=>{
effect();
})
}
}
我們發(fā)現(xiàn)如下問題
let school = [1,2,3];
let p = reactive(school);
effect(()=>{
console.log(p.length);
})
p.push(100);
新增了值驳糯,
effect
方法并未重新執(zhí)行,因為push
中修改length
已經(jīng)被我們屏蔽掉了觸發(fā)trigger
方法氢橙,所以當(dāng)新增項時應(yīng)該手動觸發(fā)length
屬性所對應(yīng)的依賴酝枢。
function trigger(target, type, key) {
const depsMap = targetMap.get(target);
if (!depsMap) {
return;
}
let effects = depsMap.get(key);
if (effects) {
effects.forEach(effect => {
effect();
});
}
// 處理如果當(dāng)前類型是增加屬性,如果用到數(shù)組的length的effect應(yīng)該也會被執(zhí)行
if (type === "add") {
let effects = depsMap.get("length");
if (effects) {
effects.forEach(effect => {
effect();
});
}
}
}
4.3 ref實現(xiàn)
ref可以將原始數(shù)據(jù)類型也轉(zhuǎn)換成響應(yīng)式數(shù)據(jù)悍手,需要通過.value
屬性進行獲取值
function convert(val) {
return isObject(val) ? reactive(val) : val;
}
function ref(raw) {
raw = convert(raw);
const v = {
_isRef:true, // 標(biāo)識是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實現(xiàn)
computed
實現(xiàn)也是基于 effect
來實現(xiàn)的坦康,特點是computed
中的函數(shù)不會立即執(zhí)行竣付,多次取值是有緩存機制的
先來看用法:
let a = reactive({name:'youxuan'});
let c = computed(()=>{
console.log('執(zhí)行次數(shù)')
return a.name +'webyouxuan';
})
// 不取不執(zhí)行,取n次只執(zhí)行一次
console.log(c.value);
console.log(c.value);
function computed(getter){
let dirty = true;
const runner = effect(getter,{ // 標(biāo)識這個effect是懶執(zhí)行
lazy:true, // 懶執(zhí)行
scheduler:()=>{ // 當(dāng)依賴的屬性變化了滞欠,調(diào)用此方法古胆,而不是重新執(zhí)行effect
dirty = true;
}
});
let value;
return {
_isRef:true,
get value(){
if(dirty){
value = runner(); // 執(zhí)行runner會繼續(xù)收集依賴
dirty = false;
}
return value;
}
}
}
修改effect
方法
function effect(fn,options) {
let effect = createReactiveEffect(fn,options);
if(!options.lazy){ // 如果是lazy 則不立即執(zhí)行
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 說明不需要執(zhí)行effect
effect.scheduler(); // 將dirty設(shè)置為true,下次獲取值時重新執(zhí)行runner方法
}else{
effect(); // 否則就是effect 正常執(zhí)行即可
}
});
let a = reactive({name:'youxuan'});
let c = computed(()=>{
console.log('執(zhí)行次數(shù)')
return a.name +'webyouxuan';
})
// 不取不執(zhí)行,取n次只執(zhí)行一次
console.log(c.value);
a.name = 'zf10'; // 更改值 不會觸發(fā)重新計算,但是會將dirty變成true
console.log(c.value); // 重新調(diào)用計算方法
到此我們將
Vue3.0
核心的Composition Api
就講解完畢了筛璧! 不管是面試還是后期的應(yīng)用也再也不需要擔(dān)心啦逸绎!~
歡迎關(guān)注前端優(yōu)選 webyouxuan 精彩文章,等你來看夭谤!