Mini Vue

vue 整体流程

vue 的三个 core

Reactivity Module

创建 JS 响应对象,并观察其变化,当使用这些对象的代码运行时,他们会被跟踪,当响应式对象变化时,代码会运行。

Compiler Module

将 HTML 模板编译成渲染函数

Render Module

三个阶段

  1. Render Phase

渲染阶段,调用 render 函数,返回一个虚拟DOM节点

  1. Mount Phase

挂载阶段,使用 虚拟DOM节点并调用 DOM API 来创建网页

  1. Patch Phase

补丁阶段,新旧虚拟节点对比,并更新网页变化部分

总的流程

我们拥有 html template 和 设定的响应式对象

  1. Compiler Module 会将 template 转换为一个 render 函数

  2. Reactivity Module 初始化响应式对象

  3. Renderer Module (渲染)会调用 render 函数(而 render 函数引用了响应式对象),观察响应式对象的变化,render 函数返回一个虚拟 DOM 节点

  4. Renderer Module(挂载)调用 mount 函数,使用 虚拟 DOM 创建页面

  5. 如果响应式对象发生任何变化,render 会再次调用 render 函数,创建一个新的虚拟 DOM,新旧虚拟节点传入 Patch 函数,新旧虚拟节点对比,根据需要更新网页

compiler 和 render

在 js 中使用 render

在 js 中使用 render 创建元素

h 函数会调用 vnode函数 从而创建 vnode 对象

1
2
3
4
5
6
7
8
9
<script>
import {h} from 'vue'
render() {
return h('div', {
id: 'foo'
onClick: this.onClick
}, 'hello')
}
</script>

从 template 到 页面实例

1
2
3
4
5
<div class="app">
<div class="red">
<span>hello</span>
</div>
</div>

vue 在内部做的是:

  1. compiler 将 template 转为 render 函数,renderer 调用 render 函数,返回一个虚拟 DOM
1
2
3
4
5
6
7
import { h } from 'vue'

render() {
return h('div', { class: 'red' }, [
h('span', null, ['hello'])
])
}
  1. 虚拟 DOM 转为真实 DOM 并挂载到容器(父组件)上
1
mount(vdom, document.getElementById('app'))

写一个简单的 mount

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function mount(vnode, container) {
// 根据 虚拟DOM 创建 真实DOM
const el = document.createElement(vnode.tag)
// props
if (vnode.props) {
for (const key in vnode.props) {
const value = vnode.props[key]
el.setAttribute(key, value)
}
}
// children (假设要么是 字符串 要么是 vnode 的数组;实际在 vue 中是 字符串和 vnode 的混合数组)
if (vnode.children) {
vnode.children.forEach(child => {
if (typeof vnode.children === 'string') {
el.textContent = vnode.children
} else {
vnode.children.forEach(child => {
mount(child, el)
})
}
})
}
container.appendChild(el)
}

写一个简单的 patch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
function patch(n1, n2) {
if (n1.tag === n2.tag) {
const el = n2.el = n1.el
// props
const oldProps = n1.props || {}
const newProps = n2.props || {}
for (const key in newProps) {
const oldValue = oldProps[key]
const newValue = newProps[key]
if (newValue !== oldValue) {
el.setAttribute(key, newValue)
}
}
for (const key in oldProps) {
if (!(key in newProps)) {
el.removeAttribute(key)
}
}

// 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.textContent = newChildren
// oldChildren 的孩子怎么办? 不用删掉吗?
}
} else {
if (typeof oldChildren === 'string') {
el.innerHTML = ''
newChildren.forEach(child => {
mount(child, el)
})
} else {
// 这里写的是没有 key 的简单的 diff 算法; 带 key 的 diff 算法看源码
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(child => {
mount(el, child)
})
} else if (newChildren.length < oldChildren.length) {
oldChildren.slice(newChildren.length).forEach(child => {
el.removeChild(child)
})
}
}
}
} else {
// replace
}
}

写一个简单的 Dep

有点像 ref 的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 记录当前将要被订阅的副作用函数
let activeEffect

class Dep {
constructor(value) {
this.subscribers = new Set()
this._value = value
}
get value() {
this.depend()
return this._value
}
set value(newValue) {
this._value = newValue
this.notify()
}
// 订阅
depend() {
if (activeEffect) {
this.subscribers.add(activeEffect)
}
}
// 发布(触发副作用函数)
notify() {
this.subscribers.forEach(effect => {
effect()
})
}
}

// 监视副作用函数
function watchEffect(effect) {
activeEffect = effect
effect()
activeEffect = null
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// 实例化 dep
const dep = new Dep('hello')

// 监听副作用函数并订阅
// 调用 watchEffect(effect),activeEffect = effect,调用 effect,effect中的 dep.value 触发了 dep 的 get 事件,触发 depend 订阅,并返回 value,打印结果 ‘hello’
watchEffect(() => {
console.log(dep.value)
})

// 修改值,触发 dep 的 set 函数修改 value,并通知发布,触发所有订阅者的 effect 事件
dep.value = 'changed'

// hello changed

很多问题中的一个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const ok = new Dep(true)
const msg = new Dep('hello')

watchEffect(() => {
if (ok.value) {
console.log(msg.value)
} else {
console.log('false branch')
}
})

ok.value = false
msg.value = 'changed'
// false branch

理论上是什么都不会打印,因为 ok 已经被修改为 false 了,副作用函数 console.log(msg.value) 不应该存在了,可以理解为下面这样:

1
2
3
watchEffect(() => {
console.log('false branch')
})

所以当修改 msg 时,不应该触发这个副作用函数,但实际还是触发了。

写一个简单的 reactive

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
let activeEffect

class Dep {
subscribers = new Set()
depend() {
if (activeEffect) {
this.subscribers.add(activeEffect)
}
}
notify() {
this.subscribers.forEach(effect => {
effect()
})
}
}

function watchEffect(effect) {
activeEffect = effect
effect()
activeEffect = null
}

/* 有多个对象(target),每个对象有多个 key,因此 depsMap 关联 key 和 dep,而 targetMap 关联 对象 和 depsMap */
// 简单的说: targetMap在很多对象里找对象,depsMap在一个对象的很多key里找key
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
}

// proxy 的配置项
const reactiveHandlers = {
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
},
has(){

},
ownKeys() {

}
}

// 响应式化
function reactive(raw) {
return new Proxy(raw, reactiveHandlers)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// 将这个对象转为响应式, 调用 proxy 对这个对象做代理, 配置了 get 和 set 等 trap, 分别会调用 depend 和 notify
const state = reactive({
count: 0
})

// 监听副作用函数并订阅
watchEffect(() => {
console.log(state.count)
})

// 修改值
state.count += 1
// 0 1

Mini Vue
http://example.com/2022/09/28/Mini-Vue/
Author
John Doe
Posted on
September 28, 2022
Licensed under