IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> JavaScript知识库 -> vue整体实现思想及mini-vue的实现 -> 正文阅读

[JavaScript知识库]vue整体实现思想及mini-vue的实现

一、三大核心系统

  • Compiler模块:编译模板系统
    主要用来将template模板转换为render渲染函数
  • Runtime模块:也可以称之为Renderer模块,真正渲染的模块
    主要用来将虚拟DOM转变为真实DOM,并且渲染到浏览器页面上
    主要的实现原理是利用了snabbdom的思想,具体可以看我的另一篇转载文章,点下方
    虚拟DOM原理
  • Reactivity模块:响应式系统
    主要用来当dom发生变化的时候,通过diff算法进行更新或者替换,然后再通过渲染系统渲染到浏览器页面上

对应源码位置
在这里插入图片描述
那么三个系统之间如何协同工作呢?
这里借用一张coderwey老师的制图
在这里插入图片描述

二、实现Mini-Vue

1、描述

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

  • 渲染系统模块
  • 可响应式系统模块
  • 应用程序入口模块

2、渲染系统实现

  • 功能一:h函数,用于返回一个VNode对象
  • 功能二:mount函数,用于将VNode挂载到DOM上
  • 功能三:patch函数,用于对两个VNode进行对比,决定如何处理新的VNode

h函数 – 生成VNode
h函数的实现:

  • 直接返回一个VNode对象即可
    在这里插入图片描述
    Mount函数 – 挂载VNode
  • mount函数的实现:
  • 第一步:根据tag,创建HTML元素,并且存储到vnode的el中;
  • 第二步:处理props属性
  • 如果以on开头,那么监听事件;
  • 普通属性直接通过 setAttribute 添加即可;
  • 第三步:处理子节点
  • 如果是字符串节点,那么直接设置textContent;
  • 如果是数组节点,那么遍历调用 mount 函数;
    在这里插入图片描述
    Patch函数 – 对比两个VNode
  • patch函数的实现,分为两种情况
  • 1、n1和n2是不同类型的节点:
  • 找到n1的el父节点,删除原来的n1节点的el;
  • 挂载n2节点到n1的el父节点上;
  • 2、n1和n2节点是相同的节点:
  • 处理props的情况
  • 先将新节点的props全部挂载到el上;
  • 判断旧节点的props是否不需要在新节点上,如果不需要,那么删除对应的属性;
  • 处理children的情况
  • 如果新节点是一个字符串类型,那么直接调用 el.textContent = newChildren;
  • 如果新节点不同一个字符串类型:
  • 旧节点是一个字符串类型
  • 将el的textContent设置为空字符串;
  • 旧节点是一个字符串类型,那么直接遍历新节点,挂载到el上;
  • 旧节点也是一个数组类型
  • 取出数组的最小长度;
  • 遍历所有的节点,新节点和旧节点进行path操作;
  • 如果新节点的length更长,那么剩余的新节点进行挂载操作;
  • 如果旧节点的length更长,那么剩余的旧节点进行卸载操作;
    在这里插入图片描述
    在这里插入图片描述

3、响应式系统实现

1、依赖收集系统的实现

思想:通过集合收集依赖,首先通过某个函数将依赖添加到集合中,然后当数据发生改变的时候,调用某个函数,重新遍历集合中的每一个函数,实现更新
在这里插入图片描述
从上面的代码,我们可以看出,存在很多弊端,需要我们手动调用addEffect函数才能将依赖添加进去,然后数据发生改变时,需要手动调用notify函数实现更新。

那么如何让它自动添加呢?

2、响应式系统Vue2的实现

class Dep {
  constructor() { 
    this.subscribers = new Set();  //添加一个集合
  }

  depend() {                //这个函数用来做依赖收集
    if (activeEffect) {
      this.subscribers.add(activeEffect);
    }
  }

  notify() {                //这个函数用来做通知更新
    this.subscribers.forEach(effect => {
      effect();
    })
  }
}

let activeEffect = null;   //根据这个变量是否为null来决定是否收集到依赖系统中
function watchEffect(effect) {  
  activeEffect = effect;
  effect();              //初始的时候执行一次
  activeEffect = null;  //收集完依赖之后再次置为空,方便添加下一个依赖
}


// Map({key: value}): key是一个字符串
// WeakMap({key(对象): value}): key是一个对象, 弱引用
const targetMap = new WeakMap();
function getDep(target, key) {   //工具函数,根据target和key获取到对应的map
  // 1.根据对象(target)取出对应的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) {   //响应式函数,vue2中主要通过defineProperty实现,参数raw是一个对象
  Object.keys(raw).forEach(key => {  //遍历对象的key
    const dep = getDep(raw, key);  //获取对应的dep,目的是只更新用到的依赖的函数,而不是不管三七二十一更新所有的
    let value = raw[key];

    Object.defineProperty(raw, key, {
      get() {
        dep.depend();  //调用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);
})

// watchEffect2
watchEffect(function () {
  console.log("effect2:", info.counter * info.counter);
})

// watchEffect3
watchEffect(function () {
  console.log("effect3:", info.counter + 10, info.name);
})

watchEffect(function () {
  console.log("effect4:", foo.height);
})

// info.counter++;
// info.name = "why";

foo.height = 2;

这里通过两个使用示例进行说明如何响应式
正如代码里所看到的,当执行

watchEffect(function () {
  console.log("effect1:", info.counter * 2, info.name);
})

这个函数的时候,会将

function () {
  console.log("effect1:", info.counter * 2, info.name);
}

赋值给activeEffect,并且执行,看下面的图片在这里插入图片描述
当执行的时候,会用到info.counter和info.name,而info这个对象通过reactive函数包裹,变成了响应式的对象,如下图
在这里插入图片描述
而通过reactive包裹,会进行数据劫持的操作,目的是为了只响应用到的dep依赖,而不是不管三七二十一都执行。

原理就是通过遍历对象的每一个key,然后通过getDep函数(这个函数在下面讲解)获得对应的Map,然后通过Object.defineProperty进行相应的响应,如果是用,那就调用get方法,然后通过depend函数将依赖添加到收集系统,如果数据有更新,比如info.name = ‘123456’,那么会调用set方法,进而调用notify进行数据更新,如下图所示
在这里插入图片描述
接下来说明一下,在reactive中是如何根据getDep函数来获取相应的map的。
先说明一下这里的数据结构,其实就是在一个map里映射了一组一组的数据,而这一组组的数据又是映射成了一个map,这样就可以根据getDep函数来精确的找到对应的key的map。

所以当用到info.counter和info.name的时候,其实用的就是defineProperty的get方法,如下图所示
在这里插入图片描述

3、响应式系统vue3的实现

class Dep {
  constructor() {
    this.subscribers = new Set();
  }

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

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

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


// Map({key: value}): key是一个字符串
// WeakMap({key(对象): value}): key是一个对象, 弱引用
const targetMap = new WeakMap();
function getDep(target, key) {
  // 1.根据对象(target)取出对应的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;
}


// 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();
    }
  })
}

// const proxy = reactive({name: "123"})
// proxy.name = "321";


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

// // watchEffect1
// watchEffect(function () {
//   console.log("effect1:", info.counter * 2, info.name);
// })

// // watchEffect2
// watchEffect(function () {
//   console.log("effect2:", info.counter * info.counter);
// })

// // watchEffect3
// watchEffect(function () {
//   console.log("effect3:", info.counter + 10, info.name);
// })

// watchEffect(function () {
//   console.log("effect4:", foo.height);
// })

// info.counter++;
// info.name = "why";

// foo.height = 2;

4、为什么Vue3选择Proxy呢?

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

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

2、修改对象的不同:

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

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

  • has:in操作符的捕获器
  • deleteProperty:delete 操作符的捕捉器

4、Proxy 作为新标准将受到浏览器厂商重点持续的性能优化
缺点:Proxy 不兼容IE,也没有 polyfill, defineProperty 能支持到IE9

4、mini-vue的具体实现

<!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>
  <script src="./renderer.js"></script>
  <script src="./reactive.js"></script>
  <script src="./index.js"></script>

  <script>
    // 1.创建根组件
    const App = {
      data: reactive({
        counter: 0
      }),
      render() {
        return h("div", null, [
          h("h2", null, `当前计数: ${this.data.counter}`),
          h("button", {
            onClick: () => {
              this.data.counter++
              console.log(this.data.counter);
            }
          }, "+1")
        ])
      }
    }

    // 2.挂载根组件
    const app = createApp(App);
    app.mount("#app");
  </script>

</body>
</html>
function createApp(rootComponent) {
  return {
    mount(selector) {
      const container = document.querySelector(selector);
      let isMounted = false;
      let oldVNode = null;

      watchEffect(function() {
        if (!isMounted) {
          oldVNode = rootComponent.render();
          mount(oldVNode, container);
          isMounted = true;
        } else {
          const newVNode = rootComponent.render();
          patch(oldVNode, newVNode);
          oldVNode = newVNode;
        }
      })
    }
  }
}
  JavaScript知识库 最新文章
ES6的相关知识点
react 函数式组件 & react其他一些总结
Vue基础超详细
前端JS也可以连点成线(Vue中运用 AntVG6)
Vue事件处理的基本使用
Vue后台项目的记录 (一)
前后端分离vue跨域,devServer配置proxy代理
TypeScript
初识vuex
vue项目安装包指令收集
上一篇文章      下一篇文章      查看所有文章
加:2022-06-08 18:57:17  更:2022-06-08 18:58:50 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 16:32:14-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码