Vue3初探+響應式原理剖析
文檔:
調試環境搭建
-
遷出 Vue3 源碼:
git clone https://github.com/vuejs/vue-next.git
安裝依賴:yarn
-
生成 sourcemap 文件,package.json
"dev":"node scripts/dev.js --sourcemap"
-
編譯:yarn dev
生成結果:packages\vue\dist\vue.global.js
源碼結構
源碼位置是在 package 文件內,實際上源碼主要分為兩部分,編譯器和運行時環境。
-
編譯器
compiler-core 核心編譯邏輯
compiler-dom 針對瀏覽器平臺編譯邏輯
compiler-sfc 針對單文件組件編譯邏輯
compiler-ssr 針對服務端渲染編譯邏輯
-
運行時環境
runtime-core 運行時核心
runtime-dom 運行時針對瀏覽器的邏輯
runtime-test 瀏覽器外完成測試環境仿真
reactivity 響應式邏輯
template-explorer 模板瀏覽器
vue 代碼入口,整合編譯器和運行時
server-renderer 服務器端渲染
share 公用方法
Vue3 初探
測試代碼:~/packages/samples/01-hello-vue3.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>hello vue3</title>
<script src="../dist/vue.global.js"></script>
</head>
<body>
<div id='app'>
<h1>{{message}}</h1>
</div>
<script>
Vue.createApp({
data: { message: 'Hello Vue 3!' }
}).mount('#app')
</script>
</body>
</html>
Composition API
Composition API 字面意思是組合 API,它是為了實現基于函數的邏輯復用機制而產生的。
基本使用
數據響應式:創建 02-composition-api.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<script src="../dist/vue.global.js"></script>
</head>
<body>
<div id="app">
<h1>Composition API</h1>
<div>count: {{ state.count }}</div>
</div>
<script>
const {
createApp,
reactive
} = Vue;
// 聲明組件
const App = {
// setup是一個新的組件選項,它是組件內使用Composition API的入口
// 調用時刻是初始化屬性確定后,beforeCreate之前
setup() {
// 響應化:接收一個對象,返回一個響應式的代理對象
const state = reactive({ count: 0 })
// 返回對象將和渲染函數上下文合并
return { state }
}
}
createApp(App).mount('#app')
</script>
</body>
</html>
計算屬性
<div>doubleCount: {{doubleCount}}</div>
const { computed } = Vue;
const App = {
setup() {
const state = reactive({
count: 0,
// computed()返回一個不可變的響應式引用對象
// 它封裝了getter的返回值
doubleCount: computed(() => state.count * 2)
})
}
}
事件處理
<div @click="add">count: {{ state.count }}</div>
const App = {
setup() {
// setup中聲明一個add函數
function add() {
state.count++
}
// 傳入渲染函數上下文
return { state, add }
}
}
偵聽器
const { watch } = Vue;
const App = {
setup() {
// state.count變化cb會執行
// 常用方式還有watch(()=>state.count, cb)
watch(() => {
console.log('count變了:' + state.count);
})
}
}
引用對象:單個原始值響應化
<div>counter: {{ counter }}</div>
const { ref } = Vue;
const App = {
setup() {
// 返回響應式的Ref對象
const counter = ref(1)
setTimeout(() => {
// 要修改對象的value
counter.value++
}, 1000);
// 添加counter
return { state, add, counter }
}
}
體驗邏輯組合
03-logic-composition.html
const { createApp, reactive, onMounted, onUnmounted, toRefs } = Vue
// 鼠標位置偵聽
function useMouse () {
// 數據響應化
const state = reactive({ x: 0, y: 0 })
const update = e => {
state.x = e.pageX
state.y = e.pageY
}
onMounted(() => {
window.addEventListener('mousemove', update)
})
onUnmounted(() => {
window.removeEventListener('mousemove', update)
})
// 轉換所有key為響應式數據
return toRefs(state)
}
// 事件檢測
function useTime () {
const state = reactive({ time: new Date() })
onMounted(() => {
setInterval(() => {
state.time = new Date()
}, 1000)
})
return toRefs(state)
}
// 邏輯組合
const MyComp = {
template: `
<div>x:{{x}}y:{{y}}</div>
<p>time:{{time}}</p>
`,
setup () {
// 使用鼠標邏輯
const { x, y } = useMouse()
// 使用時間邏輯
const { time } = useTime()
// 返回使用
return { x, y, time }
}
}
createApp().mount(MyComp, '#app')
對比 mixins,好處顯而易見:
x,y,time 來源清晰;
不會與 data、props 等命名沖突。
Vue3 響應式原理
Vue2 響應式原理
// 1.對象響應化:遍歷每個key,定義getter、setter
// 2.數組響應化:覆蓋數組原型方法,額外增加通知邏輯
const originalProto = Array.prototype
const arrayProto = Object.create(originalProto)
['push', 'pop', 'shift', 'unshift', 'splice', 'reverse', 'sort'].forEach(method => {
arrayProto[method] = function () {
originalProto[method].apply(this, arguments)
notifyUpdate()
}
})
function observe (obj) {
if (typeof obj !== 'object' || obj === null) {
return
}
if (Array.isArray(obj)) {
Object.setPrototypeOf(obj, arrayProto)
} else {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
defineReactive(obj, key, obj[key])
}
}
}
function defineReactive (obj, key, val) {
observe(val) // 解決嵌套問題
Object.defineProperty(obj, key, {
get () {
return val
},
set (newVal) {
if (newVal !== val) {
observe(newVal) // 新值是對象的情況
val = newVal
notifyUpdate()
}
}
})
}
function notifyUpdate () {
console.log('頁面更新');
}
Vue2 響應式弊端:
響應化過程需要遞歸遍歷,消耗較大
新加或刪除屬性無法監聽
數組響應化需要額外實現
Map、Set、Class 等無法響應式
修改語法有限制
Vue3 響應式原理剖析
Vue3 使用 ES6 的 Proxy 特性來解決這些問題。
創建 04-reactivity.js
function reactive (obj) {
if (typeof obj !== 'object' && obj !== null) {
return obj
}
// Proxy 相當于在對象外層加攔截
// http://es6.ruanyifeng.com/#docs/proxy
const observed = new Proxy(obj, {
get (target, key, receiver) {
// Reflect 用于執行對象默認操作,更規范、更友好
// proxy 和 object 的方法 Reflect 都有對應
// http://es6.ruanyifeng.com/#docs/reflect
const res = Reflect.get(target, key, receiver)
console.log(`獲取${key}:${res}`);
return res
},
set (target, key, value, receiver) {
const res = Reflect.set(target, key, value, receiver)
console.log(`設置${key}:${res}`);
return res
}
})
return observed
}
測試代碼
const state = reactive({
foo: 'foo',
bar: { a: 1 }
})
// 1.獲取
state.foo // ok
// 2.設置已存在屬性
state.foo = 'fooooooo' // ok
// 3.設置不存在屬性
state.dong = 'dong' // ok
// 4.刪除屬性
delete state.dong // ok
嵌套對象響應式
測試:嵌套對象不能響應
// 設置嵌套對象屬性
react.bar.a = 10 // no ok
添加對象類型遞歸
// 提取幫助方法
const isObject = val => val !== null && typeof val === 'object'
function reactive (obj) {
// 判斷是否對象
if (!isObject(obj)) {
return obj
}
const observed = new Proxy(obj, {
get (target, key, receiver) {
// ...
// 如果是對象需要遞歸
return isObject(res) ? reactive(res) : res
},
// ...
})
}
避免重復代理
重復代理,比如:
reactive(data) // 以代理過的純對象
reactive(react) // 代理對象
解決方式:將之前代理結果緩存,get 時直接使用。
const toProxy = new WeakMap() // 形如obj:observed
const toRaw = new WeakMap() // 形如observed:obj
function reactive (obj) {
// ...
// 查找緩存,避免重復代理
if (toProxy.has(obj)) {
return toProxy.get(obj)
}
if (toRaw.has(obj)) {
return obj
}
const observed = new Proxy(...)
// 緩存代理結果
toProxy.set(obj, observed)
toRaw.set(observed, obj)
return observed
}
// 測試效果
console.log(reactive(data) === state)
console.log(reactive(data) === state)
依賴收集
建立響應數據 key 和更新函數之間的對應關系。
用法
// 設置響應函數
effect(() => console.log(state.foo))
// 用戶修改關聯數據會觸發響應函數
state.foo = 'xxx'
設計
設計三個函數:
effect:將回調函數保存起來備用,立即執行一次回調函數觸發它里面一些響應數據的 getter;
track:getter 中調用 track,把前面存儲的回調函數和當前 target、key 之間建立映射關系;
trigger:setter 中調用 trigger,把 target,key 對應的響應函數都執行一遍。
target、key 和響應函數映射關系
// 大概結構如下所示
// target | depsMap
// obj | key | Dep
// k1 | effect1,effect2...
// k2 | effect3,effect4...
// {target: {key: [effect1,...]}}
實現
設置響應函數,創建 effect 函數。
// 保存當前活動響應函數作為getter和effect之間橋梁
const effectStack = []
// effect任務:執行fn并將其入棧
function effect (fn) {
const rxEffect = function () {
// 1.捕獲可能的異常
try {
// 2.入棧,用于后續依賴收集
effectStack.push(rxEffect)
// 3.運行fn,觸發依賴收集
return fn()
} finally {
// 4.執行結束,出棧
effectStack.pop()
}
}
// 默認執行一次響應函數
rxEffect()
// 返回響應函數
return rxEffect
}
依賴收集和觸發
function reactive (obj) {
// ...
const observed = new Proxy(obj, {
get (target, key, receiver) {
// ...
// 依賴收集
track(target, key)
return isObject(res) ? reactive(res) : res
},
set (target, key, value, receiver) {
// ...
// 觸發響應函數
trigger(target, key)
return res
}
})
}
// 映射關系表,結構大致如下:
// {target: {key: [fn1,fn2]}}
let targetMap = new WeakMap()
function track (target, key) {
// 從棧中取出響應函數
const effect = effectStack[effectStack.length - 1]
if (effect) {
// 獲取target對應依賴表
let depsMap = targetMap.get(target)
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
// 獲取key對應的響應函數集
let deps = depsMap.get(key)
if (!deps) {
deps = new Set()
depsMap.set(key, deps)
}
// 將響應函數加入到對應集合
if (!deps.has(effect)) {
deps.add(effect)
}
}
}
// 觸發target.key對應響應函數
function trigger (target, key) {
// 獲取依賴表
const depsMap = targetMap.get(target)
if (depsMap) {
// 獲取響應函數集合
const deps = depsMap.get(key)
if (deps) {
// 執行所有響應函數
deps.forEach(effect => {
effect()
})
}
}
}
總結
Vue3 會兼容之前寫法,僅 Portal、Suspense 等少量新 api 需要看看,Composition API 則是可選的。
正式版路線圖還有一段時間,相關工具、生態、庫都跟上需要時間。
vue3比vue2好嗎?
殺手級特性:Composition API
用戶體驗:響應式革新、time-slicing
更好的類型推斷支持
兼容性