跟着 coderwhy 的vue3視頻 一步一步實現的javascript
mini-vue 源碼連接html
Vue源碼包含三大核心:前端
實現一個簡潔版的 Mini-Vue, 包含三個模塊:vue
在傳統的前端開發中,咱們編寫本身的HTML,最終被渲染到瀏覽器上。java
而目前框架都會引入虛擬DOM來對真實的DOM進行抽象,這樣作有不少的好處node
該模塊主要包含三個功能:react
h 函數用於返回一個VNode對象ios
mount函數: 用於將VNode掛載在DOM上git
patch函數: 用於倆個VNode進行對比,判斷如何處理新的VNodegithub
h函數的做用就是 生成VNode, 而vnode本質上是一個JavaScript對象
實現一個h 函數很簡單,直接返回一個VNode對象便可
const h = (tag, props, children) => {
return {
tag,
props,
children
}
}
複製代碼
mount 函數的做用就是 掛載VNode, 將vnode掛載DOM元素上並顯示在瀏覽器上
實現思路:
根據 tag , 建立HTML元素,而且存到 vnode的el中 (目前只考慮 標籤 ,不考慮組件)
處理 props 屬性 (目前只考慮倆種狀況)
處理子節點(只考慮倆種狀況:字符串和數組)
代碼以下:
const h = (tag, props, children) => {
return {
tag,
props,
children
}
}
const mount = (vnode, container) => {
// 1. 建立 html 元素
const el = vnode.el = document.createElement(vnode.tag)
// 2. 處理 props屬性
if (vnode.props) {
for (const key in vnode.props) {
if (!vnode.props.hasOwnProperty(key)) {return}
const value = vnode.props[key]
if (key.startsWith('on')) {
el.addEventListener(key.slice(2).toLowerCase(), value)
} else {
el.setAttribute(key, value)
}
}
}
// 3. 處理子節點
if (vnode.children) {
if (typeof vnode.children === 'string') {
el.textContent = vnode.children
} else {
vnode.children.forEach(item => {
mount(item, el)
})
}
}
container.appendChild(el)
}
複製代碼
這樣就能實現簡單的渲染啦~
<body>
<div id="app"></div>
<script src="renderer.js"></script>
<script> const vnode = h('div', { class: 'wangpf' }, [ h('h2', { class: 'title' }, 'hello,I am wangpf'), h('div', null, '當前計數:100'), h('button', { class: 'btn' }, '+1') ]) mount(vnode, document.querySelector('#app')) </script>
</body>
複製代碼
patch 函數做用就是 對比倆個新舊vnode,將不一樣的給替換掉,運用到了 diff 。
對 patch 函數的實現,分爲倆種狀況 (n1爲舊的vnode,n2爲新的vnode)
代碼實現:
const patch = (n1, n2) => {
if (n1.tag !== n2.tag) {
const n1ElParent = n1.el.parentElement
n1ElParent.removeChild(n1.el)
mount(n2, n1ElParent)
} else {
// 1.取出 element對象,而且在 n2中進行保存
const el = n2.el = n1.el
// 2. 處理 props
const oldProps = n1.props || {}
const newProps = n2.props || {}
// 2.1 獲取全部的 newProps 添加到 el
for (const key in newProps) {
if (!newProps.hasOwnProperty(key)) {return}
const oldValue = oldProps[key]
const newValue = newProps[key]
if (oldValue !== newValue) {
if (key.startsWith('on')) {
el.addEventListener(key.slice(2).toLowerCase(), newValue)
} else {
el.setAttribute(key, newValue)
}
}
}
// 2.2 刪除舊的props
for (const key in oldProps) {
if (!newProps.hasOwnProperty(key)) {return}
if (key.startsWith('on')) {
el.removeEventListener(key.slice(2).toLowerCase())
}
if (!(key in newProps)) {
el.removeAttribute(key)
}
}
// 3. 處理 children
const oldChildren = n1.children || []
const newChildren = n2.children || []
if (typeof newChildren === 'string') { // 狀況一
if (typeof oldChildren === 'string') {
if (newChildren !== oldChildren) {
el.textContent = newChildren
}
} else {
el.innerHTML = newChildren
}
} else { // 狀況二:newChildren是個數組
if (typeof oldChildren === 'string') {
el.innerHTML = ''
newChildren.forEach(item => {
mount(item, el)
})
} else {
// 若是都是數組
// oldChildren : [v1,v2,v3]
// newChildren : [v1,v5,v7,v8,v9]
const commonLength = Math.min(newChildren.length, oldChildren.length)
for (let i = 0; i < commonLength; i++) {
patch(oldChildren[i], newChildren[i])
}
if (newChildren.length > oldChildren.length) {
newChildren.slice(oldChildren.length).forEach(item => {
mount(item, el)
})
}
if (newChildren.length < oldChildren.length) {
oldChildren.slice(newChildren.length).forEach(item => {
el.removeChild(item.el)
})
}
}
}
}
}
複製代碼
<body>
<div id="app"></div>
<script src="renderer.js"></script>
<script> // 1.經過 h 函數建立一個 vnode const vnode = h('div', { class: 'wangpf', id: 'aaa' }, [ h('h2', { class: 'title' }, 'hello,I am wangpf'), h('div', null, '當前計數:100'), h('button', { class: 'btn' }, '+1') ]) // 2. 經過 mount 函數, 將 vnode 掛載在div#app上 mount(vnode, document.querySelector('#app')) // 3. 建立新的 vnode setTimeout(() => { const newVnode = h('div', { class: 'pf', id: 'aaa' }, [ h('h2', { class: 'title' }, 'hello,I am wangpf'), h('div', null, '當前計數:0'), h('button', { class: 'btn222' }, '-1') ]) patch(vnode, newVnode) }, 2000) </script>
</body>
複製代碼
當定時器達到2s後, 新的vnode 會替換掉舊的vnode,經過 ptach 函數來diff出不一樣的地方進行替換。
大體上這就這樣簡單的實現一下渲染系統模塊,分別有 h函數(返回vnode對象)、mount函數(用於掛載到頁面上)、patch函數(對比新舊vnode,更新爲最新的)
響應式模塊是vue的重中之重,vue2版本是經過 Object.defineProperty
來進行對數據進行依賴收集劫持的 , vue3版本是經過 proxy
來實現的
換爲 proxy
緣由在於 defineProperty
這個API雖然兼容性好,可是不能檢測到對象和數組的變化,好比對對象的新增屬性,咱們須要去手動的給該屬性收集依賴(經過**$set**),才能實現響應式。 對於 proxy
來講, Proxy 是劫持的整個對象,不須要作特殊處理 (我以爲這個爲何換爲 proxy 的根本緣由)
雛形的響應式系統: 發佈訂閱的思想
// 響應式系統模塊
class Dep {
constructor() {
this.subscribers = new Set()
}
addEffect(effect) {
this.subscribers.add(effect)
}
notify() {
this.subscribers.forEach(effect => {
effect()
})
}
}
const info = { counter: 100 }
const doubleCounter = () => {
console.log(info.counter * 2)
}
const multiplyCounter = () => {
console.log(info.counter * info.counter)
}
const dep = new Dep()
dep.addEffect(doubleCounter)
dep.addEffect(multiplyCounter)
setInterval(() => {
info.counter++
dep.notify()
}, 2000)
複製代碼
上述代碼有不少不足之處,只要數據發生變化就得手動去調用。
咱們但願數據只要一發生變化,那麼就自動的去收集依賴並執行
因此改進了以下:
const dep = new Dep()
const watchEffect = (effect) => {
dep.addEffect(effect)
}
const info = { counter: 100 }
watchEffect(() => {
console.log(info.counter * 2)
})
watchEffect(() => {
console.log(info.counter * info.counter)
})
setInterval(() => {
info.counter++
dep.notify()
}, 2000)
複製代碼
我就用 watchEffect 來統一管理它, 只不過須要在 watchEffect 函數中執行邏輯。
但這仍是有些不足,好比不知道是誰的邏輯,並且並非自動收集依賴
所以,再次進行改進,以下:
// 響應式系統模塊
class Dep {
constructor() {
this.subscribers = new Set()
}
depend() {
if (activeEffect) {
this.subscribers.add(activeEffect)
}
}
notify() {
this.subscribers.forEach(effect => {
effect()
})
}
}
const dep = new Dep()
let activeEffect = null
const watchEffect = (effect) => {
activeEffect = effect
dep.depend()
activeEffect = null
}
const info = { counter: 100 }
watchEffect(() => {
console.log(info.counter * 2)
})
watchEffect(() => {
console.log(info.counter * info.counter)
})
setInterval(() => {
info.counter++
dep.notify()
}, 2000)
複製代碼
用 depend 來取代替 addEffect , 這樣作的目的是 不須要去知道 subscribers 添加的具體是什麼
可是呢, 這樣作會使得對 info 整個有依賴, 若是我想監聽 info 的某一個屬性,全部咱們須要有一個數據劫持的方法來實現。
這時候就能夠用vue2響應式原理的思想來實現了, 經過 Object.defineProperty
(Vue2響應式原理的核心)
// 響應式系統模塊
class Dep {
constructor() {
this.subscribers = new Set()
}
depend() {
if (activeEffect) {
this.subscribers.add(activeEffect)
}
}
notify() {
this.subscribers.forEach(effect => {
effect()
})
}
}
let activeEffect = null
const watchEffect = (effect) => {
activeEffect = effect
effect() // vue3 中 watchEffect 就會默認執行一次
activeEffect = null
}
const targetMap = new WeakMap()
const 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
}
// vue2 數據劫持原理
const reactive = (raw) => {
Object.keys(raw).forEach(key => {
const dep = getDep(raw, key)
let value = raw[key];
Object.defineProperty(raw, key, {
get() {
dep.depend()
return value
},
set(newValue) {
if (value !== newValue) {
value = newValue
dep.notify()
}
}
})
})
return raw
}
複製代碼
實現效果:
const info = reactive({ counter: 100 })
watchEffect(() => {
console.log(info.counter * 2)
})
watchEffect(() => {
console.log(info.counter * info.counter)
})
info.counter++
// 70 10000
// 67 202
// 70 10201
複製代碼
defineProperty
已經說過了,因此咱們可使用proxy
對 reactive 函數進行重構
// vue3 proxy 數據劫持
const reactive = (raw) => {
return new Proxy(raw, {
get(target, p, receiver) {
const dep = getDep(target, p)
dep.depend()
return target[p]
},
set(target, p, newValue, receiver) {
const dep = getDep(target, p)
target[p] = newValue
dep.notify()
}
})
}
複製代碼
上述已經實現了 渲染系統模塊和響應式系統模塊,接下來咱們就差最後一步了,模仿一下vue3 使用 createApp函數 做爲入口 以及mount函數將其掛載到頁面上
從框架的層面來講,咱們須要有倆部份內容:
const createApp = (rootComponent) => {
return {
mount(selector) {
let isMounted = false
let preVnode = null
watchEffect(() => {
// 首次須要掛載, 後邊就 patch
if (!isMounted) {
preVnode = rootComponent.render()
mount(preVnode, document.querySelector(selector))
isMounted = true
} else {
const newVnode = rootComponent.render()
patch(preVnode, newVnode)
preVnode = newVnode
}
})
}
}
}
複製代碼
// 1. 建立根組件
const App = {
data: reactive({
counter: 0
}),
render() {
return h('div', null, [
h('h2', null, `計數:${this.data.counter}`),
h('button', {
onClick: () => {this.data.counter++}
}, '+1')
])
}
}
// 2. 掛載根組件
createApp(App).mount('#app')
複製代碼
點擊便可完成加一操做!