Fork me on GitHub

浅尝试一下实现简单的Vue3

vue3

渲染器

作用:虚拟DOM转成真实DOM。

工作原理:递归地遍历虚拟DOM,调用原生js创建真实的DOM。

虚拟DOM

虚拟DOM就是一个用来描述真实DOM的JS对象。

1
2
3
4
5
6
7
const vnode = {
tag: 'div',
props: {
onClick: () => console.log('This is a div')
},
children: 'click me'
}

组件

虚拟DOM元素的封装,可以是返回虚拟DOM的函数,也可以是一个对象(这个对象必须有一个函数用来产出组件要渲染的虚拟DOM)

1
2
3
4
5
6
7
8
9
const myComponent = function() {
return {
tag: 'div',
props: {
onClick: () => console.log('This is a div')
},
children: 'click me'
}
}

实现一个渲染器

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
// 假设一个虚拟dom
const vnode = {
tag: myComponent
}
// 渲染函数
function renderer(vnode, container) {
// 判断vnode描述的是dom标签还是组件
if(typeof vnode.tag === 'string') {
mountElement(vnode, container)
} else if(typeof vnode.tag === 'function') {
mountComponent(vnode, container)
}
}

// 渲染标签元素
function mountElement(vnode, container) {
// 创建元素
const el = document.createElement(vnode.tag)
// 绑定事件或设置属性
for(const key in vnode.props) {
// 事件
if(key.startsWith('on')) {
const eName = key.substr(2).toLowerCase()
el.addEventListener(eName, vnode.props[key])
}
}
// children
if(typeof vnode.children === 'string') {
// 文本节点
el.appendChild(document.createTextNode(vnode.children))
} else if(vnode.children instanceof Array) {
// 递归添加子元素
vnode.children.forEach(child => renderer(child, el))
}

if(container) container.appendChild(el)
}

// 渲染组件
function mountComponent(vnode, container) {
// 调用组件函数获取虚拟dom
const subtree = vnode.tag()
// 递归的调用渲染函数
renderer(subtree, container)
}

响应式数据

实现原理:对数据的“读取”和“设置”操作的拦截,在副作用函数和响应式数据间建立联系。当“读取”时,把当前执行的副作用函数放入“桶”;当“设置”时,把副作用函数取出并执行。

Proxy

可以创建一个代理对象,能够实现对其他对象的代理。

1
2
3
4
5
6
const n = new Proxy(obj, {
// 拦截读取属性
get() {},
// 拦截设置属性
set() {},
})

实现简单的响应式数据

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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
// 数据
const data = {
text: 'hello world'
}
// 全局变量存被注册的副作用函数
let activeEffect

// 注册副作用函数
function effect(fn) {
//设置当前激活的副作用函数
const effectFn = () => {
activeEffect = fn
fn()
}
// 存所有与该副作用函数相关联的依赖集合
effectFn.deps = []
// 执行副作用函数
effectFn()
}
// 存副作用函数的桶
const bucket = new WeakMap() // new Set() => 副作用函数没有和目标对象字段绑定
/*
WeakMap由 target --> Map 构成
Map由 key --> Set 构成
*/

// 代理
const obj = new Proxy(data, {
// 拦截读取属性
get(target, key) {
// 副作用函数添加到桶
track(target, key)
return target[key]
},
// 拦截设置属性
set(target, key, newVal) {
// 设置新值
target[key] = newVal
// 取出副作用函数并执行
trigger(target, key)
return true
},
})

// get调用
function track(target, key) {
// 没有activeEffect
if(!activeEffect) return target[key]
// 从桶中获取depsMap
let depsMap = bucket.get(target) // Map类型
// 没有depsMap,新建一个与target绑定
if(!depsMap) {
depsMap = new Map()
bucket.set(target, depsMap)
}
// 获取与key相关的所有副作用函数
let deps = depsMap.get(key) // Set类型
if(!deps) {
deps = new Set()
depsMap.set(key, deps)
}
// 存副作用函数
// bucket.add(activeEffect)
// 添加当前激活的副作用函数
deps.add(activeEffect)
}

// set调用
function trigger(target, key) {
// 取出副作用函数并执行
// bucket.forEach(fn => fn())
const depsMap = bucket.get(target)
if(!depsMap) return
// 获取与key相关的所有副作用函数
const effects = depsMap.get(key)
effects && effects.forEach(fn => fn())
}
// 需要执行的副作用函数
effect(function effectFn() {
document.body.innerText = obj.text
})

computed

计算属性实际上是一个懒执行的副作用函数

watch

实现原理:利用副作用函数重新执行时的可调度性

编译器

作用:将模板字符串编译成渲染函数。

render 函数是模板编译后的产物,它负责构建 VNode 树,构建好的 VNode 会传递给 patch,patch 根据 VNode 的关系生成真实dom节点树。

编译器过程:

  • 用来将模板字符串解析为模板AST的解析器(parser)
  • 用来将模板AST转换为JavaScript AST的转换器(transformer)
  • 根据JavaScript AST生成render函数代码的生成器(generator)

实现一个编译器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const template = `<p>{{ message }}</p>`

function compiler(template) {// render
// 解析模版,生成 ast
const ast = parse(template)
// 将 ast 生成渲染函数
const render = generate(ast)
return render
}

function parse() {

}

function generate() {

}
-------------完结撒花 -------------