Vue学习笔记09-源码学习

h函数,mount函数,patch函数,源码解码

1. DOM渲染

1.1. 真实的DOM渲染

传统的前端开发中,编写自己的HTML,最终被渲染到浏览器上的,那么它是什么样的过程呢?

html代码 -> DOM树 -> 页面内容

1.2. 虚拟DOM的优势

  • 目前框架都会引入虚拟DOM来对真实的DOM进行抽象,这样做有很多的好处

  • 首先是可以对真实的元素节点进行抽象,抽象成VNode(虚拟节点),这样方便后续对其进行各种操作:

    • 因为对于直接操作DOM来说是有很多的限制的,比如diff、clone等等,但是使用JavaScript编程语言来操作这些,就变得非常的简单;
    • 可以使用JavaScript来表达非常多的逻辑,而对于DOM本身来说是非常不方便的;
  • 其次是方便实现跨平台,包括可以将VNode节点渲染成任意想要的节点

    • 如渲染在canvas、WebGL、SSR、Native(iOS、Android)上;
    • 并且Vue允许开发属于自己的渲染器(renderer),在其他的平台上渲染;

1.3. 虚拟DOM的渲染过程

2. 三大核心系统

  • 事实上Vue的源码包含三大核心:
    • Compiler Module:编译模板系统;
    • Runtime Module:也可以称之为Renderer模块,真正渲染的模块;
    • Reactivity Module:响应式系统;
  • Vue源码中的代码分类:

3. 三大系统协同工作

4. 实现Mini-Vue

  • 实现一个简洁版的Mini-Vue框架,该Vue包括三个模块:
    • 渲染系统模块;
    • 可响应式系统模块;
    • 应用程序入口模块;

4.1. 渲染系统实现

  • 该模块主要包含三个功能:
    • 功能一:h函数,用于返回一个VNode对象;
    • 功能二:mount函数,用于将VNode挂载到DOM上;
    • 功能三:patch函数,用于对两个VNode进行对比,决定如何处理新的VNode;
  • 功能函数写于 render.js
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
<!DOCTYPE html>
<html>
<body>
<div id="app"></div>
<script src="./render.js"></script>
<script>
// 1.通过h函数来创建一个vnode
const vnode = h('div',null,[
h('h2',null,'Hello World'),
h('button',{
onClick: ()=> {
console.log("clicked");
}
},'Hello')
]);

// 2.通过mount函数,将vnode挂载到div#app上
mount(vnode,document.querySelector("#app"));

// 3.创建新的vnode
const vnode1 = h('div',{class:'title'},[
h("h1",null,"hahaha"),
h("button",{
onClick: ()=>{
console.log("new vnode");
}
},"button")
]);

// 对两个VNode进行对比,决定如何处理新的VNode
setTimeout(()=>{
patch(vnode,vnode1);
},2000);
</script>
</body>
</html>

4.1.1. h函数 – 生成VNode

直接返回一个VNode对象即可

1
2
3
4
5
6
7
8
const h = (tag,props,children)=>{
// vnode->javascript->{}
return{
tag,
props,
children
}
}

4.1.2. Mount函数 – 挂载VNode

  1. 第一步:根据tag,创建HTML元素,并且存储 到vnode的el中;
  2. 第二步:处理props属性
    • 如果以on开头,那么监听事件;
    • 普通属性直接通过 setAttribute 添加即可;
  3. 第三步:处理子节点
    • 如果是字符串节点,那么直接设置textContent;
    • 如果是数组节点,那么遍历调用 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
25
26
27
28
29
30
const mount = (vnode,container)=>{
// vnode->element
// 1.创建出真实的原生,并且在vnode上保留el
const el = vnode.el = document.createElement(vnode.tag);

// 2.处理props
if(vnode.props){
for(const key in vnode.props){
const value = vnode.props[key];
if(key.startsWith("on")){
el.addEventListener(key.slice(2).toLowerCase(),value);
}else{
el.setAttribute(key,value);
}
}
}
// 3.处理children
if(vnode.children){
if(typeof vnode.children==='string'){
el.textContent = vnode.children;
}
else{
vnode.children.forEach(child => {
mount(child,el);
});
}
}
// 挂载到container
container.appendChild(el);
}

4.1.3. Patch函数 – 对比两个VNode

  • patch函数的实现,分为两种情况

  • n1和n2是不同类型的节点:

    • 找到n1的el父节点,删除原来的n1节点的el;
    • 挂载n2节点到n1的el父节点上;
  • n1和n2节点是相同的节点:

    • 处理props的情况

      • 先将新节点的props全部挂载到el上;
      • 判断旧节点的props是否不需要在新节点上,如果不需要,那么删除对应的属性;
    • 处理children的情况

      • 如果新节点是一个字符串类型,那么直接调用 el.textContent = newChildren;

      • 如果新节点不同一个字符串类型:

        • 旧节点是一个字符串类型

          • 将el的textContent设置为空字符串;

          • 就节点是一个字符串类型,那么直接遍历新节点,挂载到el上;

        • 旧节点也是一个数组类型

          • 取出数组的最小长度;
          • 遍历所有的节点,新节点和旧节点进行path操作;
          • 如果新节点的length更长,那么剩余的新节点进行挂载操作;
          • 如果旧节点的length更长,那么剩余的旧节点进行卸载操作;
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
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){
const oldValue = oldProps[key];
const newValue = newProps[key];
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(!(key in newProps)){
if(key.startsWith("on")){
const value = oldProps[key];
el.removeEventListener(key.slice(2).toLowerCase(),value);
}
else{
el.removeAttribute(key);
}

}
}

// 3.处理children
const oldChildren = n1.children;
const newChildren = n2.children;
if(typeof newChildren == 'string'){
el.textContent = newChildren;
}
else{

if(typeof oldChildren == 'string'){
// 删除原有元素内容
el.innerHTML = "";
// 挂载新内容
newChildren.forEach(child=>{
mount(child,el);
})
}
else{
// oldChildren: [v1,v2,v3,v4,v5]
// newChildren: [v1,v5,v6]
commonLength = Math.min(oldChildren.length,newChildren.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(child,el);
});
}
if(newChildren.length < oldChildren.length){
oldChildren.slice(newChildren.length).forEach(child=>{
el.removeChild(child);
});
}

}
}
}
}

4.2. 依赖收集系统

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
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;
function watchEffect(effect){
activeEffect = effect;
dep.depend();
effect();
activeEffect = null;
}

watchEffect(()=>{
console.log("依赖回调1");
})

watchEffect(()=>{
console.log("依赖回调2");
})

// 调用
dep.notify();

4.3. 响应式系统

4.3.1. Vue2实现

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
// weakmap 的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;
}

function 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){
value = newValue;
dep.notify();
}
})
})
return raw;
}

4.3.2. Vue3实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function reactive(raw){
return new Proxy(raw,{
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;
}
})
}

4.3.3. 为什么Vue3选择Proxy呢

  • Object.definedProperty 是劫持对象的属性时,如果新增元素:

    • 那么Vue2需要再次 调用definedProperty,而 Proxy 劫持的是整个对象,不需要做特殊处理;
  • 修改对象的不同:

    • 使用 defineProperty 时,修改原来的 obj 对象就可以触发拦截;
    • 而使用 proxy,就必须修改代理对象,即 Proxy 的实例才可以触发拦截;
  • Proxy 能观察的类型比 defineProperty 更丰富

    • has:in操作符的捕获器;
    • deleteProperty:delete 操作符的捕捉器;
    • 等等其他操作;
  • Proxy 作为新标准将受到浏览器厂商重点持续的性能优化;

  • 缺点:Proxy 不兼容IE,也没有 polyfill, defineProperty 能支持到IE9

4.4. 框架外层API设计

  • 从框架的层面来说,需要有两部分内容
    • createApp用于创建一个app对象;
    • 该app对象有一个mount方法,可以将根组件挂载到某一个dom元素上;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const createApp = (rootComponent)=>{
return {
mount(selector){
let isMounted = false;
let preVNode = null;

watchEffect(()=>{
if(!isMounted){
preVNode = rootComponent.render();
mount(preVNode,document.querySelector(selector));
isMounted = true;
}
else{
const newVNode = rootComponent.render();
patch(preVNode,newVNode);
preVNode = newVNode;
}
})
}
}
}

4.5. html调用整体设计

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
<!DOCTYPE html>
<html lang="en">
<head>
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script src="./render.js"></script>
<script src="./reactive-vue3.js"></script>
<script src="./index.js"></script>
<script>
const App = {
data: reactive({
counter: 0
}),
render(){
return h('button',{
onClick: ()=>{
this.data.counter++;
}
},this.data.counter+"")
}
}
const app = createApp(App);
app.mount("#app");

</script>
</body>
</html>

5. 源码阅读

5.1. createApp

5.2. 挂载根组件

5.3. instance

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// runtime-core/src/component.ts
const instance: ComponentInternalInstance = {}

// 1.处理props和attrs
instance.props
instance.attrs

// 2.处理slots
instance.slots

// 3.执行setup
const result = setup()
instance.setupState = proxyRefs(result);

// 4.编译template -> compile
<template> -> render函数
instance.render = Component.render = render函数

// 5.对vue2的options api进行知识
data/methods/computed/生命周期

5.4. 组件化的初始化

5.5. Compile过程

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
// compiler-core/src/compile.ts

// 对template进行解析,生成ast
const ast = isString(template) ? baseParse(template, options) : template
const [nodeTransforms, directiveTransforms] = getBaseTransformPreset(
prefixIdentifiers
)

// AST转换
transform(
ast,
extend({}, options, {
prefixIdentifiers,
nodeTransforms: [
...nodeTransforms,
...(options.nodeTransforms || []) // user transforms
],
directiveTransforms: extend(
{},
directiveTransforms,
options.directiveTransforms || {} // user transforms
)
})
)

// 生成代码
return generate(
ast,
extend({}, options, {
prefixIdentifiers
})
)

5.6. 生命周期回调

5.7. template中数据的使用顺序

下列案列中最终使用的是setup中的counter值0

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<template id="my-app">
<div>
<h2>{{counter}}</h2>
</div>
</template>
<script src="./vue.global.js"></script>
<script>
const App = {
template: "#my-app",
data: function(){
return {
counter: 100
}
}
setup(){
const counter = Vue.ref(0);
return{
counter
}
}
}

const app = Vue.createApp(App);
app.mount('#app');
</script>
</body>
</html>

源码

5.7.1. render.ts

runtime-core/render.ts/baseCreateRenderer/setupRenderEffect 中的 renderComponentRoot

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const setupRenderEffect: SetupRenderEffectFn = (
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
) => {
const componentUpdateFn = () => {
if (!instance.isMounted) {

if (el && hydrateNode) {}
else {
//调用renderComponentRoo渲染组件,生成子树的vnode
//因为template会被编译成render函数,所以renderComponentRoot就是去执行render函数获取对应的vnode
//renderComponentRoot中海油收集dynamicBlock
const subTree = (instance.subTree = renderComponentRoot(instance))
}

}
}
}

5.7.2. componentRenderUtil.ts

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
export function renderComponentRoot(
instance: ComponentInternalInstance
): VNode {
const {
type: Component,
vnode,
proxy,
withProxy,
props,
propsOptions: [propsOptions],
slots,
attrs,
emit,
render,
renderCache,
data,
setupState,
ctx,
inheritAttrs
} = instance

let result

try {
if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
// withProxy is a proxy with a different `has` trap only for
// runtime-compiled render functions using `with` block.
const proxyToUse = withProxy || proxy
result = normalizeVNode(
render!.call(
proxyToUse,
proxyToUse!,
renderCache,
props,
setupState,
data,
ctx
)
)
}
}
catch (err) {
result = createVNode(Comment)
}
return result
}

5.7.3. componentPublicInstance.ts

数据使用顺序决定先取setup中的数据

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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
get({ _: instance }: ComponentRenderContext, key: string) {
const {
// 存储 computed/methods
ctx,
// 存储 setup返回的结果
setupState,
// 存储 data中的数据
data,
// 存储 props
props,
accessCache,
type,
appContext }
= instance

if (key[0] !== '$') {
// 从缓存中取值
const n = accessCache![key]
// 第一次 n 为 undefined
if (n !== undefined) {
switch (n) {
case AccessTypes.SETUP:
return setupState[key]
case AccessTypes.DATA:
return data[key]
case AccessTypes.CONTEXT:
return ctx[key]
case AccessTypes.PROPS:
return props![key]
// default: just fallthrough
}
}
// 先从setup中取值
else if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) {
accessCache![key] = AccessTypes.SETUP
return setupState[key]
}
// 再从data中去取值
else if (data !== EMPTY_OBJ && hasOwn(data, key)) {
accessCache![key] = AccessTypes.DATA
return data[key]
}
// props
else if (
(normalizedProps = instance.propsOptions[0]) &&
hasOwn(normalizedProps, key)
)
{
accessCache![key] = AccessTypes.PROPS
return props![key]
}
else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) {
accessCache![key] = AccessTypes.CONTEXT
return ctx[key]
}
else if (!__FEATURE_OPTIONS_API__ || shouldCacheAccess) {
accessCache![key] = AccessTypes.OTHER
}
}

},

set(
{ _: instance }: ComponentRenderContext,
key: string,
value: any
): boolean {
const {
data,
setupState,
ctx
} = instance

return true
},

has(
{
_: {
data,
setupState,
accessCache,
ctx,
appContext,
propsOptions
}
}: ComponentRenderContext,
key: string
) {

},

defineProperty(
target: ComponentRenderContext,
key: string,
descriptor: PropertyDescriptor
) {
return Reflect.defineProperty(target, key, descriptor)
}
}
本文结束  感谢您的阅读