本文最后更新于 320 天前,如有失效请评论区留言。
miniVue
实现一个基础的vue,就需要Virtual Dom和reactive相结合
所以先把之前的代码搬过来
// vdom
function h (tag, props, children) {
return {
tag,
props,
children
}
}
function mount (vnode, container) {
const el = vnode.el = document.createElement(vnode.tag)
if (vnode.props) {
for (const key in vnode.props) {
const value = vnode.props[key]
el.setAttribute(key, value)
}
}
if (vnode.children) {
if (typeof vnode.children === 'string') {
el.textContent = vnode.children
} else {
vnode.children.forEach(child => {
if (typeof child === 'string') {
el.appendChild(document.createTextNode(child))
} else {
mount(child, el)
}
})
}
}
container.appendChild(el)
}
function patch (n1, n2) {
// 首先要确保他们是同一个标签
if (n1.tag === n2.tag) {
const el = n2.el = n1.el
const oldProps = n1.props || {}
const newProps = n2.props || {}
// 如果有了template模板,下面的这些对比都有可能直接跳过(比如检测到静态标签直接跳过对比)
// 对比新老中是否有key更改了
for (const key in newProps) {
const oldValue = oldProps[key]
const newValue = newProps[key]
if (newValue != oldValue) {
el.setAttribute(key, newValue)
}
}
// 当旧的中没有key要去除
for (const key in oldProps) {
if (!(key in newProps)) {
el.removeAttribute(key)
}
}
// 接着处理children,可能是一个'string' 或者 []
const oldChildren = n1.children
const newChildren = n2.children
if (typeof newChildren === 'string') {
if (typeof oldChildren === 'string') {
// 新老的children都是字符串 不同就直接替换
if (newChildren !== oldChildren) {
el.textContent = newChildren
}
} else {
el.textContent = newChildren
}
} else {
// 新的children是数组,老的是字符串就遍历循环添加
if (typeof oldChildren === 'string') {
el.innerHTML = ''
newChildren.forEach(child => {
mount(child, el)
})
} else {
// 当新老的children都是数组,有两种情况,一种使用v-for提供了一个唯一的key,用key去对比
// 另一种就是双端指针 头头尾尾,vue2中是 头头,尾尾,旧头新尾,旧尾新头
// 以上四种情况都匹配不到的话,就以新头对旧的vnode进行查找,看是否找到,都找不到就新建元素
// 这里只是做简单的对比,不一样就直接替换
const commonLength = Math.min(oldChildren.length, newChildren.length)
for (let i = 0; i < commonLength; i++) {
patch(oldChildren[i], newChildren[i])
}
// 还需要考虑newChildren更长或者更短的情况下进行新增和删除
if (newChildren.length > oldChildren.length) {
newChildren.slice(oldChildren.length).forEach(child => {
mount(child, el)
})
} else if (newChildren.length < oldChildren.length) {
oldChildren.slice(newChildren.length).forEach(child => {
el.removeChild(child.el)
})
}
}
}
} else {
// 当不是同一个类型就直接情况再挂载
const el = n2.el = n1.el
el.innerHTML = ''
mount(n2, el)
}
}
// reactivity
let activeEffect
class Dep {
subscribers = new Set()
depend () {
if (activeEffect) {
this.subscribers.add(activeEffect)
}
}
notify (effect) {
this.subscribers.forEach(effect => {
effect()
})
}
}
function watchEffect (effect) {
activeEffect = effect
effect()
activeEffect = null
}
const targetMap = new WeakMap()
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 reactiveHandles = {
get (target, key, receiver) {
const dep = getDep(target, key)
dep.depend()
// return target[key]
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 (raw) {
return new Proxy(raw, reactiveHandles)
}
接着使用这4个核心函数
首先创建一个App根组件,定义了一个count属性,并把这个count值加到渲染函数中
const App = {
data: reactive({
count: 0
}),
render () {
// 这里也可以String(this.data.count)
return h('div', null, this.data.count)
}
}
要对mount做出一点修改,这里的this.data.count就是0,需要增加number属性的判断,并且也要加强vnode.children判断
原:
if (vnode.children) {
if (typeof vnode.children === 'string') {
新:
if (vnode.children != null && vnode.children !== '') {
if (typeof vnode.children === 'string' || typeof vnode.children === 'number') {
然后我们再定义一个挂载函数,把id为app的div传入当作根,再把App传入进行渲染,根据是否是首次渲染,判断是挂载还是更新
function mountApp (component, containerString) {
const container = document.querySelector(containerString)
if (!container) {
return console.warn(`[Vue warn]: Failed to mount app: mount target selector "${containerString}" returned null.`);
}
let isMounted = false
let oldVdom
watchEffect(() => {
if (!isMounted) {
oldVdom = component.render()
mount(oldVdom, container)
isMounted = true
} else {
const newVdom = component.render()
patch(oldVdom, newVdom)
oldVdom = newVdom
}
})
}
mountApp(App, '#app')
我个人觉得可以把首次mount提取出来,减少一次判断
function mountApp (component, containerString) {
const container = document.querySelector(containerString)
if (!container) {
return console.warn(`[Vue warn]: Failed to mount app: mount target selector "${containerString}" returned null.`);
}
let oldVdom = component.render()
mount(oldVdom, container)
watchEffect(() => {
const newVdom = component.render()
patch(oldVdom, newVdom)
oldVdom = newVdom
})
}
mountApp(App, '#app')
这是我们就可以添加一个点击事件改变 this.data.count 来达到动态更新,
const App = {
data: reactive({
count: 0
}),
render () {
return h('div', {
onClick: () => {
this.data.count++
}
}, this.data.count)
}
}
但是由于我们之前没有添加事件的监听,所以也需要修改mount方法中props的判断 (这也是很多三方库为什么代码量这么多的原因,因为需要考虑所有传入情况,特别是异常情况,还要告知用户什么异常,以及如何处理)
原:
for (const key in vnode.props) {
const value = vnode.props[key]
el.setAttribute(key, value)
}
新:
for (const key in vnode.props) {
const value = vnode.props[key]
if (key.startsWith('on')) {
const name = key.slice(2).toLowerCase()
el.addEventListener(name, value)
}else{
el.setAttribute(key, value)
}
}
当然这里也是只处理onClick事件,还有很多 onSubmit、onInput、onKeypress、onKeydown、onKeyup、onMousedown、onMouseup、onMouseover、onMouseout等事件都没有添加
然而你会发现还是点不动,但是这时this.data.count的值确实是增加了,只是patch
方法中我们没有添加上对应number类型children的处理
原:
if (typeof newChildren === 'string') {
if (typeof oldChildren === 'string') {
// 新老的children都是字符串 不同就直接替换
if (newChildren !== oldChildren) {
el.textContent = newChildren
}
} else {
el.textContent = newChildren
}
}
新:
if (typeof newChildren === 'string' || typeof newChildren === 'number') {
if (typeof oldChildren === 'string' || typeof newChildren === 'number') {
// 新老的children都是字符串或数字 不同就直接替换
if (newChildren !== oldChildren) {
el.textContent = newChildren
}
} else {
el.textContent = newChildren
}
}
就成功实现一个点击更新count并对比更新渲染的效果
源码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script>
// vdom
function h (tag, props, children) {
return {
tag,
props,
children
}
}
function mount (vnode, container) {
const el = vnode.el = document.createElement(vnode.tag)
if (vnode.props) {
for (const key in vnode.props) {
const value = vnode.props[key]
if (key.startsWith('on')) {
const name = key.slice(2).toLowerCase()
el.addEventListener(name, value)
} else {
el.setAttribute(key, value)
}
}
}
if (vnode.children != null && vnode.children !== '') {
if (typeof vnode.children === 'string' || typeof vnode.children === 'number') {
el.textContent = vnode.children
} else {
vnode.children.forEach(child => {
if (typeof child === 'string') {
el.appendChild(document.createTextNode(child))
} else {
mount(child, el)
}
})
}
}
container.appendChild(el)
}
function patch (n1, n2) {
// 首先要确保他们是同一个标签
if (n1.tag === n2.tag) {
const el = n2.el = n1.el
const oldProps = n1.props || {}
const newProps = n2.props || {}
// 如果有了template模板,下面的这些对比都有可能直接跳过(比如检测到静态标签直接跳过对比)
// 对比新老中是否有key更改了
for (const key in newProps) {
const oldValue = oldProps[key]
const newValue = newProps[key]
if (newValue != oldValue) {
el.setAttribute(key, newValue)
}
}
// 当旧的中没有key要去除
for (const key in oldProps) {
if (!(key in newProps)) {
el.removeAttribute(key)
}
}
// 接着处理children,可能是一个'string' 或者 []
const oldChildren = n1.children
const newChildren = n2.children
if (typeof newChildren === 'string' || typeof newChildren === 'number') {
if (typeof oldChildren === 'string' || typeof newChildren === 'number') {
// 新老的children都是字符串或数字 不同就直接替换
if (newChildren !== oldChildren) {
el.textContent = newChildren
}
} else {
el.textContent = newChildren
}
} else {
// 新的children是数组,老的是字符串就遍历循环添加
if (typeof oldChildren === 'string') {
el.innerHTML = ''
newChildren.forEach(child => {
mount(child, el)
})
} else {
// 当新老的children都是数组,有两种情况,一种使用v-for提供了一个唯一的key,用key去对比
// 另一种就是双端指针 头头尾尾,vue2中是 头头,尾尾,旧头新尾,旧尾新头
// 以上四种情况都匹配不到的话,就以新头对旧的vnode进行查找,看是否找到,都找不到就新建元素
// 这里只是做简单的对比,不一样就直接替换
const commonLength = Math.min(oldChildren.length, newChildren.length)
for (let i = 0; i < commonLength; i++) {
patch(oldChildren[i], newChildren[i])
}
// 还需要考虑newChildren更长或者更短的情况下进行新增和删除
if (newChildren.length > oldChildren.length) {
newChildren.slice(oldChildren.length).forEach(child => {
mount(child, el)
})
} else if (newChildren.length < oldChildren.length) {
oldChildren.slice(newChildren.length).forEach(child => {
el.removeChild(child.el)
})
}
}
}
} else {
// 当不是同一个类型就直接情况再挂载
const el = n2.el = n1.el
el.innerHTML = ''
mount(n2, el)
}
}
// reactivity
let activeEffect
class Dep {
subscribers = new Set()
depend () {
if (activeEffect) {
this.subscribers.add(activeEffect)
}
}
notify (effect) {
this.subscribers.forEach(effect => {
effect()
})
}
}
function watchEffect (effect) {
activeEffect = effect
effect()
activeEffect = null
}
const targetMap = new WeakMap()
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 reactiveHandles = {
get (target, key, receiver) {
const dep = getDep(target, key)
dep.depend()
// return target[key]
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 (raw) {
return new Proxy(raw, reactiveHandles)
}
const App = {
data: reactive({
count: 0
}),
render () {
return h('div', {
onClick: () => {
this.data.count++
}
// }, String(this.data.count))
}, this.data.count)
}
}
function mountApp (component, containerString) {
const container = document.querySelector(containerString)
if (!container) {
return console.warn(`[Vue warn]: Failed to mount app: mount target selector "${containerString}" returned null.`);
}
let oldVdom = component.render()
mount(oldVdom, container)
watchEffect(() => {
const newVdom = component.render()
patch(oldVdom, newVdom)
oldVdom = newVdom
})
}
mountApp(App, '#app')
</script>
</body>
</html>