zxpnet网站 zxpnet网站
首页
前端
后端服务器
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

zxpnet

一个爱学习的java开发攻城狮
首页
前端
后端服务器
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 大前端课程视频归档
  • html

  • js

  • 前端框架

  • 自动化构建

  • typescript

  • es6

  • bootstrap

  • layer

  • vue

  • vue3

    • 邂逅Vue3和Vue3开发体验
    • Vue3基础语法
    • Vue3组件化开发
    • Vue3过渡&动画实现
    • Babel和devServer
    • Composition API
    • vue3高级语法
    • vue3源码
      • vue架构
        • 虚拟DOM
        • 三大核心系统
      • mini-vue
        • 渲染系统模块
        • 功能一:h函数,用于返回一个VNode对象
        • 功能二:mount函数,
        • 功能三:patch函数,
        • 可响应式系统模块
        • 应用程序入口模块
      • 为什么Vue3选择Proxy呢
      • 框架外层API设计
      • 源码阅读之createApp
      • 源码阅读之挂载根组件
    • vue-router路由
    • vuex状态管理
    • vue开源项目
    • vue3-cms项目笔记
    • pinia状态管理
  • vuepress

  • hexo博客

  • 文档

  • biz业务

  • frontend
  • vue3
shollin
2022-02-23
目录

vue3源码

  • vue架构
    • 虚拟DOM
    • 三大核心系统
  • mini-vue
    • 渲染系统模块
    • 可响应式系统模块
    • 应用程序入口模块
  • 为什么Vue3选择Proxy呢
  • 框架外层API设计
  • 源码阅读之createApp
  • 源码阅读之挂载根组件

# vue架构

# 虚拟DOM

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

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

  • 因为对于直接操作DOM来说是有很多的限制的,比如diff、clone等等,但是使用JavaScript编程语言来操作这些,就变得非常的简单;

  • 我们可以使用JavaScript来表达非常多的逻辑,而对于DOM本身来说是非常不方便的;

2、其次是方便实现跨平台,包括你可以将VNode节点渲染成任意你想要的节点

  • 如渲染在canvas、WebGL、SSR、Native(iOS、Android)上;

  • 并且Vue允许你开发属于自己的渲染器(renderer),在其他的平台上渲染;

# 三大核心系统

事实上Vue的源码包含三大核心:

  • Compiler模块:编译模板系统;
  • Runtime模块:也可以称之为Renderer模块,真正渲染的模块;
  • Reactivity模块:响应式系统;
  • image-20220224140402401

image-20220224091937761

# mini-vue

这里我们实现一个简洁版的Mini-Vue框架,该Vue包括三个模块:

  • 渲染系统模块;

  • 可响应式系统模块;

  • 应用程序入口模块;

# 渲染系统模块

渲染系统,该模块主要包含三个功能:

# 功能一:h函数,用于返回一个VNode对象

vnode其实就是一个对象,包含三个元素: 标签、属性、子vnode

# 功能二:mount函数,

用于将VNode挂载到DOM上,mount函数的实现:

  • 第一步:根据tag,创建HTML元素,并且存储到vnode的el中;
  • 第二步:处理props属性,如果以on开头,那么监听事件; 普通属性直接通过 setAttribute 添加即可;
  • 第三步:处理子节点,如果是字符串节点,那么直接设置textContent;如果是数组节点,那么遍历调用 mount 函数;
  • 第四步:将el挂载到container上 container.appendChild(el);

# 功能三:patch函数,

用于对两个VNode进行对比,决定如何处理新的VNode,patch函数的实现,分为两种情况:

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

  • 找到n1的el父节点,删除原来的n1节点的el;

  • 挂载n2节点到n1的el父节点上;

2、n1和n2节点是相同的节点:

A、处理props的情况

  • 先将新节点的props全部挂载到el上;

  • 判断旧节点的props是否不需要在新节点上,如果不需要,那么删除对应的属性;

B、处理children的情况

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

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

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

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

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

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

​ 1、 取出数组的最小长度;

​ 2、遍历所有的节点,新节点和旧节点进行path操作;

​ 3、如果新节点的length更长,那么剩余的新节点进行挂载操作;

​ 4、如果旧节点的length更长,那么剩余的旧节点进行卸载操作;

  <script src="./renderer.js"></script>
  <script>

    // 1.通过h函数来创建一个vnode
    const vnode = h('div', {class: "why", id: "aaa"}, [
      h("h2", null, "当前计数: 100"),
      h("button", {onClick: function() {}}, "+1")
    ]); // vdom

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

    // 3.创建新的vnode
    setTimeout(() => {
      const vnode1 = h('div', {class: "coderwhy", id: "aaa"}, [
        h("h2", null, "呵呵呵"),
        h("button", {onClick: function() {}}, "-1")
      ]); 
      patch(vnode, vnode1);
    }, 2000)

  </script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const h = (tag, props, children) => {
  // vnode -> javascript对象 -> {}
  return {
    tag,
    props,
    children
  }
}

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(item => {
        mount(item, el);
      })
    }
  }

  // 4.将el挂载到container上
  container.appendChild(el);
}

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 (newValue !== oldValue) {
        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.startsWith("on")) { // 对事件监听的判断
        const value = oldProps[key];
        el.removeEventListener(key.slice(2).toLowerCase(), value)
      } 
      if (!(key in newProps)) {
        el.removeAttribute(key);
      }
    }

    // 3.处理children
    const oldChildren = n1.children || [];
    const newChidlren = n2.children || [];

    if (typeof newChidlren === "string") { // 情况一: newChildren本身是一个string
      // 边界情况 (edge case)
      if (typeof oldChildren === "string") {
        if (newChidlren !== oldChildren) {
          el.textContent = newChidlren
        }
      } else {
        el.innerHTML = newChidlren;
      }
    } else { // 情况二: newChildren本身是一个数组
      if (typeof oldChildren === "string") {
        el.innerHTML = "";
        newChidlren.forEach(item => {
          mount(item, el);
        })
      } else {
        // oldChildren: [v1, v2, v3, v8, v9]
        // newChildren: [v1, v5, v6]
        // 1.前面有相同节点的原生进行patch操作
        const commonLength = Math.min(oldChildren.length, newChidlren.length);
        for (let i = 0; i < commonLength; i++) {
          patch(oldChildren[i], newChidlren[i]);
        }

        // 2.newChildren.length > oldChildren.length
        if (newChidlren.length > oldChildren.length) {
          newChidlren.slice(oldChildren.length).forEach(item => {
            mount(item, el);
          })
        }

        // 3.newChildren.length < oldChildren.length
        if (newChidlren.length < oldChildren.length) {
          oldChildren.slice(newChidlren.length).forEach(item => {
            el.removeChild(item.el);
          })
        }
      }
    }
  }
}
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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123

# 可响应式系统模块

对数据劫持,实现对象属性的监听,有两种实现方式,vue2采用的是Object.defineProperty方式, vue3采用的Proxy方式

let activeEffect = null;
function watchEffect(effect) {
  activeEffect = effect;
  effect(); // 调用函数,触发副作用
  activeEffect = null;
}

class Dep {  // 依赖类
  constructor() {
    this.subscribers = new Set(); // 保存哪些订阅者,订阅者模式
  }

  depend() {
    if (activeEffect) {
      this.subscribers.add(activeEffect);
    }
  }

  notify() {
    this.subscribers.forEach(effect => {
      effect();
    })
  }
}

// Map({key: value}): key是一个字符串
// WeakMap({key(对象): value}): key是一个对象, 弱引用
const targetMap = new WeakMap();
function getDep(target, key) {
  // 1.根据对象(target)取出对应的Map对象, 根map为单例
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    depsMap = new Map();
    targetMap.set(target, depsMap);
  }

  // 2.取出具体的dep对象
  let dep = depsMap.get(key);
  if (!dep) {
    dep = new Dep();
    depsMap.set(key, dep);
  }
  return dep;
}

// vue2对raw进行数据劫持
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) {
        if (value !== newValue) {
          value = newValue;
          dep.notify(); // 通知依赖
        }
      }
    })
  })
  return raw;
}

// 测试代码
const info = reactive({counter: 100, name: "why"});
const foo = reactive({height: 1.88});

// watchEffect1
watchEffect(function () {
  console.log("effect1:", info.counter * 2, info.name);
})
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

深入浅出Object.defineProperty() - 简书 (jianshu.com) (opens new window)

// vue3对raw进行数据劫持
function reactive(raw) {
  return new Proxy(raw, {
    get(target, key) {
      const dep = getDep(target, key);
      dep.depend();
      return target[key];
    },
    set(target, key, newValue) {
      const dep = getDep(target, key);
      target[key] = newValue;
      dep.notify();
    }
  })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

Javascript Proxy对象 简介 - 前端小老虎 - 博客园 (cnblogs.com) (opens new window)

# 应用程序入口模块

# 为什么Vue3选择Proxy呢

1、Object.definedProperty 是劫持对象的属性时,如果新增元素,那么Vue2需要再次 调用definedProperty,而 Proxy 劫持的是整个对象,不需要做特殊处理;

2、修改对象的不同:

使用 defineProperty 时,我们修改原来的 obj 对象就可以触发拦截; 而使用 proxy,就必须修改代理对象,即 Proxy 的实例才可以触发拦截;

3、Proxy 能观察的类型比 defineProperty 更丰富

  • has:in操作符的捕获器;

  • deleteProperty:delete 操作符的捕捉器;

  • 等等其他操作;

4、Proxy 作为新标准将受到浏览器厂商重点持续的性能优化;

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

JS Proxy(代理) - 简书 (jianshu.com) (opens new window)

# 框架外层API设计

从框架的层面来说,我们需要有两部分内容:

  • createApp用于创建一个app对象;

  • 该app对象有一个mount方法,可以将根组件挂载到某一个dom元素上;

# 源码阅读之createApp

image-20220224112950386

# 源码阅读之挂载根组件

image-20220224112933154

vue3高级语法
vue-router路由

← vue3高级语法 vue-router路由→

最近更新
01
国际象棋
09-15
02
成语
09-15
03
自然拼读
09-15
更多文章>
Theme by Vdoing | Copyright © 2019-2023 zxpnet | 粤ICP备14079330号-1
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式