Vue3 beta已經發布一段時間了,八月份Vue3也要正式上線了,準備好了解Vue3的基本原理了麼?vue
如下代碼只介紹VUE的實現邏輯,不會覆蓋全部的實際應用用例。react
首先,仍是從掛載一個dom元素開始git
vue3中咱們能夠利用暴露出來的 h
函數來渲染一個模版,他接收的參數就是一個用js表示的dom結構 tag標籤,屬性,孩子節點
假設咱們有一個用js構建的vdom的模版,長這樣github
const vdom = h('div', {
class: 'red'
}, [
h('span', null, 'hello')
])
複製代碼
那麼接下來咱們就該解析這個結構,用以把他掛載到真實的dom結構上算法
假設咱們已經實現了一個mount方法,那麼咱們就只須要調用mount方法完成掛載數組
// 傳入要掛載的虛擬dom,和父節點
mount(vdom, document.getElementById('app'))
複製代碼
如此mount方法須要解決的就是app
// 掛載元素
function mount(vNode, container) {
const elm = document.createElement(vNode.tag)
// props
if (vNode.props) {
for (const key in vNode.props) {
const attr = vNode.props[key]
if (key.startsWith('on')) {
// 事件監聽
const type = key.substr(2).toLocaleLowerCase()
elm.addEventListener(type, attr)
} else {
elm.setAttribute(key, attr)
}
}
}
// children
if (vNode.children) {
if (typeof vNode.children === 'string') {
elm.textContent = vNode.children
} else {
// 遞歸解析子元素
vNode.children.forEach(child => {
mount(child, elm)
})
}
}
container.appendChild(elm)
}
複製代碼
OK,這樣咱們就完成了一個很是簡單的dom掛載過程。框架
dom掛載了以後,咱們還可能觸發一些操做來更新dom,好比點擊按鈕改變顏色,數字++這種操做,這個時候咱們不須要去操做真實的dom,只須要根據新的dom,對舊的dom打補丁dom
假設咱們已經實現了這樣一個patch函數,他能夠對舊元素打補丁。函數
const vdom2 = h('div', {
class: 'red'
}, [
h('span', null, 'hello')
])
patch(vdom, vdom2)
複製代碼
這裏須要對比新舊的vdom,因此咱們對mount改造一下,存儲一下真實dom結構,方便後面操做。
const elm = vNode.elm = document.createElement(vNode.tag)
複製代碼
這要就能夠經過vdom.elm訪問真實的dom結構
接下來考慮一下,patch須要作什麼?
比較兩個節點是否是同一種節點,若是是的話繼續比較屬性和子節點。
若是不是的話,就須要進行節點的替換,節點的替換也是很複雜的處理,這裏不討論。
這裏須要注意的是,會出現不少分支狀況,新舊節點的props可能都不存在,都存在,或者新的存在,或者舊的存在。而後每個props可能出現變化,或者未變化。
一樣的,這裏只討論attribute的狀況,而且只討論都有props的狀況。在vue裏面處理這個狀況是很複雜的,這裏只探討 補丁
的思路
const oldProps = n1.props || {}
const newProps = n2.props || {}
// 若是這個屬性存在 或者新增
for (const key in newProps) {
const oldValue = oldProps[key]
const newValue = newProps[key]
// 新增了屬性 或者 兩個屬性的值 不相等,須要變動節點內容了
if (oldValue !== newValue) {
elm.setAttribute(key, newValue)
}
}
// 刪除了屬性
for (const key in oldProps) {
if (!(key in newProps)) {
// 刪除了一個屬性
elm.removeAttribute(key)
}
}
複製代碼
一樣的,children的比較也會遇到相同的問題,會有不少分支狀況須要去考慮。
並且最複雜的實際上是兩個children都是數組的時候,vue裏面會要求顯示的設定key值,以減輕 diff
算法的壓力,這個模式叫作 key模式
這裏就假設沒有key值,而且簡單粗暴的比較兩個children的每一個節點。
// 比較children
const oldChildren = n1.children
const newChildren = n2.children
if (typeof newChildren === 'string') {
if (typeof oldChildren === 'string') {
// 兩個節點都是字符節點,內不一樣時,修改內容
} else {
// 新節是字符 舊節點是 數組 直接替換
}
} else if (Array.isArray(newChildren)) {
if (typeof oldChildren === 'string') {
// 舊節點只是字符 新節點是數組 掛載新節點
} else if (Array.isArray(oldChildren)) {
// 若是兩個節點都是數組 這裏vue中用到key的模式去判斷是否是同一個元素
// 假設沒有key 咱們只比較兩個數組的 index 相同的部分
const commonLength = Math.min(newChildren.length, oldChildren.length)
for (let i = 0; i < commonLength; i++) {
// 比較一下 公共部分的 每個child
}
// 接下來比較一下差別部分
if (newChildren.length > oldChildren.length) {
// 新的子節點多一些,掛載新的子節點
}
if (newChildren.length < oldChildren.length) {
// 新的子節點少一些 刪除了子節點
}
}
}
複製代碼
假設咱們有一個這樣的程序
let a = 10
let b = a * 10
複製代碼
咱們但願a被修改的時候,b也跟着被修改。
這裏咱們就能夠叫作b的修改 是 a的修改的 反作用 effect
想像一個EXCEL表格中,咱們定義了一個 公式(function)
,B列 = A列 * 10,當A的值改變時,B也會隨之改變。
事實上,就至關於有個onAchange函數,在a改變時輸出b = a * 10
onAchange(() => {
b = a * 10
})
/** * () => { b = a * 10 } 這個函數就是a改變 所執行 的 反作用 / 複製代碼
那麼 如何實現這個 onAchange呢? 聯想一下react的 setState
let _state, _update // 定義一個_state 保存state 定義一個_update保存 執行更改的反作用
const onStateChange = update => {
_update = update // 保存反作用
}
const setState = newState => {
_state = newState
_update() // 觸發反作用
}
複製代碼
setState能夠暴露給框架的使用者,顯示的調用setState 告訴 框架 應該觸發我這個操做的反作用了。
可是在vue中,咱們是 state.a = newValue
這樣去更新一個值得,那麼Vue是如何作的呢?
先來看一個簡單的vue3提供的新的API使用示例
import {
reactive watchEffect
} from 'vue'
// 調用reactive包裝state的值 就會返回以一個狀態響應式的值
// 包含了依賴收集
const state = reactive({
count: 0
})
// 追蹤這個函數使用過的全部的東西,他執行過程當中使用的每個響應式屬性
// 當咱們修改state.count的時候這個函數會被再次執行
watchEffect(() => {
console.log(state.count)
}) // 0
state.count++ // 1
複製代碼
這兩個API 是 Composition API
中的一部分,徹底獨立的,能夠與options API共存的新的API
來看看這兩個API是怎麼實現的
想一想這裏要作什麼
1. 調用watchEffect 傳入一個effect 以後,這個 effect 應該被做爲一個反作用,被依賴被收集起來,等待調用
2. 這個effect 所依賴的參數發生改變時,effct 應該被再次執行
複製代碼
let activeEffect
// 依賴關係
class Dep {
constructor(value) {
this.subscribers = new Set()
this._value = value
}
get value() { // 利用getter自動執行依賴收集
this.depend()
return this._value
}
set value(newVlue) { // 利用setter自動執行反作用
this._value = newVlue
this.notify()
}
// 收集依賴
depend() {
if (activeEffect) this.subscribers.add(activeEffect)
}
// 觸發依賴
notify() {
this.subscribers.forEach(effect => effect())
}
}
function watchEffect(effect) {
activeEffect = effect
effect()
activeEffect = null
}
const dep = new Dep('hello')
watchEffect(() => {
console.log(dep.value)
})
dep.value = 'world!'
複製代碼
前面實現的dep類,讓dep去保存value,以便觸發value變動的時候去觸發notify,調用反作用函數。在真正的vue中,則是代理整個對象,讓對象的每個屬性,對應一個dep,value是對象的,而不是dep的
因此這裏首先,咱們要實現這個reactive,那麼reavtive究竟作了什麼呢?
1. 代理整個對象,當咱們訪問對象的屬性時,對整個屬性加上依賴追蹤
2. 當屬性的值改變時,觸發依賴追蹤,觸發反作用
複製代碼
那麼,在vue2中,這個事情是由 Object.defineProperty
去完成的,他確實完成了代理對象的工做,表現也還不錯。可是不可避免的他存在一些缺點:
1. 須要遍歷對象的每個屬性去爲每個屬性綁定,遇到對象嵌套的狀況還須要遞歸
2. 沒法處理這個對象身上自己沒有的屬性的變動
3. 代理數組時,須要hack到數組的原型上去改變原有的方法,這也是爲何在vue2中直接用 `array[index]` 這樣的方式修改數組,不會觸發響應式的緣由
複製代碼
在vue3中,這個功能的核心就是 proxy
,proxy的特性這就就不詳細說了,感興趣的能夠自行查閱API,proxy也很好的解決了 Object.defineProperty
的痛點。
首先,咱們確定仍是須要 Dep
這個依賴類,那咱們在訪問對象的屬性時,經過proxy攔截一下這個動做,爲這個屬性綁定一個依賴追蹤,把全部屬性都綁定上依賴追蹤,就須要有一個東西存儲起來,這裏選擇 Map
,那還有就是每個對象都須要爲每個屬性綁定依賴追蹤,因此要定位到 這個屬性是這個對象的
,就還須要在外層再來一個 Map
,告訴咱們哪一個對象對應哪個 屬性依賴Map
結構就是這樣的 對象 => 對象屬性的map( 對象屬性 => 屬性對應的依賴 )
const targetMap = new WeakMap() // 收集全部 對象和 整個對象 的依賴映射
// 對象 => 對象屬性的map( 對象屬性 => 屬性對應的依賴 )
function getDep(target, key) {
let depsMap = targetMap.get(target)
if (!depsMap) {
depsMap = new Map() // 對象的每個屬性 與 對應的依賴 的映射
targetMap.set(target, depsMap)
}
let dep = depsMap.get(key)
if (!dep) {
dep = new Dep() //
depsMap.set(key, dep)
}
return dep
}
const reactiveHandler = {
get(target, key, receiver) {
const dep = getDep(target, key)
dep.depend() // 依賴收集
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
const dep = getDep(target, key)
const result = Reflect.set(target, key, value, receiver)
dep.notify() // 觸發反作用
return result
}
}
function reactive(obj) {
// 代理對象
return new Proxy(obj, reactiveHandler)
}
複製代碼
爲何 proxy
解決了 Object.defineProperty
的痛點呢? 你能夠看到,這裏沒有循環,沒有遞歸了。也不用去特殊處理數組了
好比 array.push
,它其實會先訪問array.length,觸發length + 1的操做,這裏隱式的調用了get方法觸發了依賴收集
如今咱們有了h函數,有了掛載函數mount,dep依賴類, ractive響應式,watchEffect反作用監聽
那麼咱們如今就實現了一個簡單的vue程序,把他們放到一塊兒,寫一個 $mount
函數,也就是掛載APP的函數
function mountApp(component, container) {
let isMounted = false
let oldDom
// 當依賴改變時 會再次進入這個反作用函數
watchEffect(() => {
// 若是是mounted以前,那就先掛載app
if (!isMounted) {
oldDom = component.render()
mount(oldDom, container)
isMounted = true
} else {
// 若是app已經掛載,就比較兩個Vdom 打補丁
const newDom = component.render()
patch(oldDom, newDom)
oldDom = newDom
}
})
}
const App = {
data: reactive({
count: 0
}),
render() {
return h('span', {
onClick: () => {
this.data.count++
}
}, this.data.count + '')
}
}
// ok你已經實現了一個mini-vue3程序
mountApp(App, document.getElementById('app'))
複製代碼
當你點擊屏幕的div時,你會神奇的發現,數字在累加!這就說明你已經實現了一個mini-vue3程序
事實上 composition API = reactivity API + Lifecycle API
,並且在vue3中實現依賴收集能夠直接使用ref