點進來!和尤大一塊兒寫vue3源碼!

vue3原理 - mini-vue3

Vue3 beta已經發布一段時間了,八月份Vue3也要正式上線了,準備好了解Vue3的基本原理了麼?vue

如下代碼只介紹VUE的實現邏輯,不會覆蓋全部的實際應用用例。react

掛載dom

首先,仍是從掛載一個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

  • 解析vdom
  • 解析props(假設這裏只有attr,沒有props,一些其餘自定義屬性)
  • 解析children子節點
// 掛載元素
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打補丁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須要作什麼?

比較兩個節點是否是同一種節點,若是是的話繼續比較屬性和子節點。

若是不是的話,就須要進行節點的替換,節點的替換也是很複雜的處理,這裏不討論。

  1. 比較新舊兩個props

這裏須要注意的是,會出現不少分支狀況,新舊節點的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)
    }
}
複製代碼
  1. 比較新舊兩個children

一樣的,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 和依賴跟蹤生效

想一想這裏要作什麼

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!'
複製代碼
  1. 第二步 實現 reactive 響應式的部分

前面實現的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方法觸發了依賴收集

mini-vue3

如今咱們有了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

完整代碼地址

相關文章
相關標籤/搜索