热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

有关Vue源码的简单实现实现一个属于自己的minvue

大厂技术高级前端Node进阶点击上方程序员成长指北,关注公众号回复1,加入高级Node交流群vue-study自己实现的mini-vue仓库࿱
大厂技术  高级前端  Node进阶

点击上方 程序员成长指北,关注公众号

回复1,加入高级Node交流群

vue-study

自己实现的mini-vue仓库:https://github.com/maolovecoding/mini-vue2-stage[1]建议克隆代码观看,效果更佳。

  1. 实现了Vue

  2. 实现了响应式数据

  3. 实现了模板编译

  4. 实现了ast转render

  5. render执行生成虚拟dom

  6. 虚拟dom转真实dom渲染页面

  7. 响应式数据和页面渲染结合 数据改变可自动更新视图

  8. 实现同步更新数据,异步更新视图

  9. 优雅降级

10.实现nextTick 11. 实现了mixin,目前只支持生命周期的合并

代码中注释很多,觉得不习惯的可以一边看一边删除多余的注释。 outside_default.png

outside_default.pngoutside_default.pngoutside_default.png

vue的常见源码实现

rollup环境搭建

安装rollup及其插件

npm i rollup rollup-plugin-babel @babel/core @babel/preset-env rollup-plugin-node-resolve -D

编写配置文件 rollup.config.js

这个可以直接使用es module

// rollup默认可以导出一个对象 作为打包的配置文件
import babel from "rollup-plugin-babel";
import resolve from 'rollup-plugin-node-resolve'
export default {// 入口input: "./src/index.js",// 出口output: {// 生成的文件file: "./dist/vue.js",// 全局对象 Vue 在global(浏览器端就是window)上挂载一个属性 Vuename: "Vue",// 打包方式 esm commonjs模块 iife自执行函数 umd 统一模块规范 -> 兼容cmd和amdformat: "umd",// 打包后和源代码做关联sourcemap: true,},plugins: [babel({// 排除第三方模块exclude: "node_modules/**",}),// 自动找文件夹下的index文件resolve()],
};

babel.config.js

// babel config
module.exports =  {// 预设presets: ["@babel/preset-env"],
};

编写脚本

"scripts": {"dev": "rollup -cw"}

-c表示使用配置文件,-w表示监控文件变化。

element.outerHTML

outerHTML属性获取描述元素(包括其后代)的序列化HTML片段。它也可以设置为用从给定字符串解析的节点替换元素。

{{name}}

{{age}}

核心流程

vue的核心流程:

  1. 创造响应式数据

  2. 模板编译 生成 ast

  3. ast 转为render函数 后续每次数据更新 只执行render函数(不需要再次进行ast的转换)

  4. render函数执行 生成 vNode节点(会使用到响应式数据)

  5. 根据vNode 生成 真实dom 渲染页面

  6. 数据更新 重新执行render

数据劫持

Vue2中使用的是Object.definedProperty,Vue3中直接使用Proxy了

模板编译为ast

vue2中使用的是正则表达式进行匹配,然后转换为ast树。

模板引擎 性能差 需要正则匹配 替换 vue1.0 没有引入虚拟dom的改变,vue2 采用虚拟dom 数据变化后比较虚拟dom的差异 最后更新需要更新的地方, 核心就是我们需要将模板变成我们的js语法 通过js语法生成虚拟dom,语法之间的转换 需要先变成抽象语法树AST 再组装为新的语法,这里就是把template语法转为render函数。

ast转render

把生成的ast语法树,通过字符串拼接等方式转为render函数。render函数内部主要用到:

  1. _c函数:创建元素虚拟dom节点

  2. _v函数:创建文本虚拟dom节点

  3. _s函数:将函数内的变量字符串化

render函数生成真实dom

调用render函数,会生成虚拟dom,然后把虚拟dom转为真实DOM,挂载到页面即可。

回忆流程

核心流程:

  1. 数据处理成响应式,在 initState中处理的(针对对象来说主要是definedProperty,数组则是重写七个方法)

  2. 模板编译:先把模板转成ast语法树,再把语法树生成render函数

  3. 调用render函数,可能会进行变量的取值操作(_s函数内有变量),产生对应的虚拟dom

  4. 虚拟dom渲染为真实dom,挂载到页面即可

完成了,虚拟和真实dom的渲染,也完成了响应式数据的处理,接下来需要进行视图和响应式数据的关联,在渲染页面的时候,收集依赖数据。

  1. 使用观察者模式实现依赖收集

  2. 异步更新策略

  3. mixin的实现原理

模板的依赖收集

要完成依赖的收集,很明显的就是,我们要如何得知,此模板在此次渲染的时候,用到了那些响应式数据。

我们可以给模板中的属性,增加一个收集器(dep)。这个收集器,是给每个属性单独增加的。页面渲染的时候,我们把渲染逻辑封装到watcher中。(其实就是手动更新视图的那两个方法app._update(app._render()))。让dep记住这个watcher即可,在属性变化了以后,可以找到对应的dep中存放的watcher,然后执行重新渲染页面。

这里面我们用到的方式其实就是观察者模式

/*** watcher 进行实际的视图渲染* 每个组件都有自己的watcher,可以减少每次更新页面的部分* 给每个属性都增加一个dep,目的就是收集watcher* 一个视图(组件)可能有很多属性,多个属性对应一个视图 n个dep对应1个watcher* 一个属性也可能对应多个视图(组件)* 所以 dep 和 watcher 是多对多关系* * 每个属性都有自己的dep,属性就是被观察者* watcher就是观察者(属性变化了会通知观察者进行视图更新)-> 观察者模式*/
class Watcher{}

先让watcher收集dep,如果dep已经收集过,则不会再次收集。当dep被收集的时候,我们也会让dep反向收集当前的watcher。实现二者的双向收集。

然后在响应式数据发送改变的时候,通知dep的观察者(watcher)进行视图更新。

outside_default.png
image-20220415105750259

视图同步渲染

此时,已经完成了响应式数据和视图的绑定,在数据发生改变的情况下,视图会同步更新。也就是说,我们更新了两次响应式数据,也会更新两次视图。

outside_default.png
image-20220415110028536

正常情况下,更新两次视图是没有问题的,但是此时两次数据的更新发生在一次同步代码中,我们应该让视图的更新是异步的,这样在一次操作更新多个数据的情况下,也只会渲染一次视图,提高渲染速率。

那么我们的想法就是合并更新,在所有的更新数据做完以后,在刷新页面。也就是批处理,事件环。

事件环

我们的期望就是,同步代码执行完毕之后,在执行视图的渲染(作为异步任务)。把更新操作延迟。

方法就是使用一个队列维护需要更新的watcher,第一次更新属性值的时候,就开启一个定时器,清空所有的watcher。后续的数据改变的操作,都不会再次开启定时器,只是会把需要更新的watcher再次入队列。(当然watcher我们会先去重)。

但是这个清空操作是在同步代码执行完毕后才会执行的。

// watcher queue 本次需要更新的视图队列
let queue = [];
// watcher 去重  {0:true,1:true}
let has = {};
// 批处理 也可以说是防抖
let pending = false;
/*** 不管执行多少次update操作,但是我们最终只执行一轮刷新操作* @param {*} watcher*/
function queueWatcher(watcher) {const id = watcher.id;// 去重if (!has[id]) {queue.push(watcher);has[id] = true;console.log(queue);if (!pending) {// 刷新队列 多个属性刷新 其实执行的只是第一次 合并刷新了setTimeout(flushSchedulerQueue, 0);pending = true;}}
}
/*** 刷新调度队列 且清理当前的标识 has pending 等都重置* 先执行第一批的watcher,如果刷新过程中有新的watcher产生,再次加入队列即可*/
function flushSchedulerQueue() {const flushQueue = [...queue];queue = [];has = {};pending = false;// 刷新视图 如果在刷新过程中 还有新的watcher 会重新放到queueWatcher中flushQueue.forEach((watcher) => watcher.run()); // run 就是执行render
}

nextTick

原理:

因为我们数据的更新和视图的更新不再是同步,导致我们在同步获取视图最新的dom元素时,可能出现获取的元素和视图实际显示的元素不一致的情况。于是出现了 nextTick方法

实际上:nextTick方法内部也是维护了一个异步回调队列,开启一个定时器,每次调用该方法传入回调,都是把回调函数放入队列,并不是每次调用nextTick方法都开启一个定时器(比较销毁性能)。再放入第一个回调函数的时候,开启定时器,后续的回调函数只放入队列而不会再次开启定时器了,。所以nextTick不是创建了异步任务,而是将这个任务维护到了队列而已。

nextTick方法是同步还是异步?

把任务(回调)放到队列是同步,实际执行任务是异步。

// 任务队列
let callbacks = [];
// 是否等待任务刷新
let waiting = false;
/*** 刷新异步回调函数队列*/
function flushCallbacks() {const cbs = [...callbacks];callbacks = [];waiting = false;cbs.forEach((cb) => cb());
}
/*** 异步批处理* 是先执行内部的回调 还是用户的? 用个队列 排序* @param {Function} cb 回调函数*/
export function nextTick(cb) {// 使用队列维护nextTick中的callback方法callbacks.push(cb);if (!waiting) {setTimeout(flushCallbacks, 0); // 刷新waiting = true;}
}

vue的nextTick

实际上,vue的nextTick方法,内部并没有直接使用原生的某一个异步api(比如promise,setTimeout等)。而是采用优雅降级的方法。

  1. 内部先采用的是promise(ie不兼容)。

  2. 有一个和Promise等价的 MutationObserve[2]。也是异步微任务。(此API是H5的,只能在浏览器中使用)

  3. 考虑ie浏览器专享的 setImmediate API。性能比settimeout好一些

  4. 最后直接上setTimeout

**采用优雅降级的目的,**还是为了用户可以尽快看见页面的渲染。

/*** 优雅降级  Promise -> MutationObserve -> setImmediate -> setTimeout(需要开线程 开销最大)*/
let timerFunc = null;
if (Promise) {timerFunc = () => Promise.resolve().then(flushCallbacks);
} else if (MutationObserver) {// 创建并返回一个新的 MutationObserver 它会在指定的DOM发生变化时被调用(异步执行callback)。const observer = new MutationObserver(flushCallbacks);// TODO 创建文本节点的API 应该封装 为了方便跨平台const textNode = document.createTextNode(1);console.log("observer-----------------")// 监控文本值的变化observer.observe(textNode, {characterData: true,});timerFunc = () => (textNode.textContent = 2);
} else if (setImmediate) {// IE平台timerFunc = () => setImmediate(flushCallbacks);
} else {timerFunc = () => setTimeout(flushCallbacks, 0);
}

对于vue3,肯定就不需要这种方式了,在不兼容ie的情况下,可以直接使用promise了。

outside_default.png
image-20220415150046818

经过一次次处理,现在是可以在视图更新以后再去拿最新的dom了。

当然:对于更改值放在取值的下面,那么获取到的肯定还是旧的dom值。vue也是如此的。

outside_default.png
image-20220415150347883

mixin的实现

Vue的mixin,可以实现全局混入和局部混入。

全局混入对所有组件实例都生效。

暂时我实现了生命周期的混入,对于data等其他特殊选项的合并还未处理。

对于混入的生命周期,无论是一个还是多个相同的生命周期,最终我们都转为使用数组包裹,每个数组元素都是混入进来的生命周期。在创建组件实例的时候,把传入的选项和全局的Vue.options选项进行合并到实例上,实现混入效果。

outside_default.png
image-20220415220542253

参考资料

[1]

https://github.com/maolovecoding/mini-vue2-stage

[2]

https://developer.mozilla.org/zh-CN/docs/Web/API/MutationObserver

关于本文

来自:codermao

https://juejin.cn/post/7105011877159108644

最后

Node 社群我组建了一个氛围特别好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你对Node.js学习感兴趣的话(后续有计划也可以),我们可以一起进行Node.js相关的交流、学习、共建。下方加 考拉 好友回复「Node」即可。如果你觉得这篇内容对你有帮助,我想请你帮我2个小忙:1. 点个「在看」,让更多人也能看到这篇文章2. 订阅官方博客 www.inode.club 让我们一起成长点赞和在看就是最大的支持❤️


推荐阅读
  • 深入解析 Vue 中的 Axios 请求库
    本文深入探讨了 Vue 中的 Axios 请求库,详细解析了其核心功能与使用方法。Axios 是一个基于 Promise 的 HTTP 客户端,支持浏览器和 Node.js 环境。文章首先介绍了 Axios 的基本概念,随后通过具体示例展示了如何在 Vue 项目中集成和使用 Axios 进行数据请求。无论你是初学者还是有经验的开发者,本文都能为你解决 Vue.js 相关问题提供有价值的参考。 ... [详细]
  • 深入解析:React与Webpack配置进阶指南(第二部分)
    在本篇进阶指南的第二部分中,我们将继续探讨 React 与 Webpack 的高级配置技巧。通过实际案例,我们将展示如何使用 React 和 Webpack 构建一个简单的 Todo 应用程序,具体包括 `TodoApp.js` 文件中的代码实现,如导入 React 和自定义组件 `TodoList`。此外,我们还将深入讲解 Webpack 配置文件的优化方法,以提升开发效率和应用性能。 ... [详细]
  • 深入探索Node.js新框架:Nest.js第六篇
    在本文中,我们将深入探讨Node.js的新框架Nest.js,并通过一个完整的示例来展示其强大功能。我们将使用多个装饰器创建一个基本控制器,该控制器提供了多种方法来访问和操作内部数据,涵盖了常见的CRUD操作。此外,我们还将详细介绍Nest.js的核心概念和最佳实践,帮助读者更好地理解和应用这一现代框架。 ... [详细]
  • React组件是构成用户界面的基本单元,每个组件都封装了特定的功能和逻辑,具备高度的独立性和可复用性。通过将不同大小和功能的组件组合在一起,可以构建出复杂且功能丰富的页面,类似于拼图游戏中的各个部分,最终形成一个完整的视觉效果。 ... [详细]
  • 本文深入探讨了 Vue.js 中异步组件的应用与优化策略。首先,文章介绍了异步组件的基本概念及其在现代前端开发中的重要性。为了确保最佳实践,建议使用 Webpack 作为模块打包工具,因为 Browserify 默认不支持异步组件的加载。接着,详细解释了异步组件的使用方法,并提供了官方文档的相关链接以供参考。此外,文章还讨论了多种优化技巧,包括代码分割、懒加载和性能调优,以提升应用的整体性能和用户体验。 ... [详细]
  • 深入浅出 webpack 系列(二):实现 PostCSS 代码的编译与优化
    在前一篇文章中,我们探讨了如何通过基础配置使 Webpack 完成 ES6 代码的编译。本文将深入讲解如何利用 Webpack 实现 PostCSS 代码的编译与优化,包括配置相关插件和加载器,以提升开发效率和代码质量。我们将详细介绍每个步骤,并提供实用示例,帮助读者更好地理解和应用这些技术。 ... [详细]
  • 本文详细介绍了在Linux系统上编译安装MySQL 5.5源码的步骤。首先,通过Yum安装必要的依赖软件包,如GCC、GCC-C++等,确保编译环境的完备。接着,下载并解压MySQL 5.5的源码包,配置编译选项,进行编译和安装。最后,完成安装后,进行基本的配置和启动测试,确保MySQL服务正常运行。 ... [详细]
  • 如何使用ES6语法编写Webpack配置文件? ... [详细]
  • Vue应用预渲染技术详解与实践 ... [详细]
  • 本文详细介绍了在 Vue.js 前端框架中集成 vue-i18n 插件以实现多语言支持的方法。通过具体的配置步骤和示例代码,帮助开发者快速掌握如何在项目中实现国际化功能,提升用户体验。同时,文章还探讨了常见的多语言切换问题及解决方案,为开发人员提供了实用的参考。 ... [详细]
  • Node.js 教程第五讲:深入解析 EventEmitter(事件监听与发射机制)
    本文将深入探讨 Node.js 中的 EventEmitter 模块,详细介绍其在事件监听与发射机制中的应用。内容涵盖事件驱动的基本概念、如何在 Node.js 中注册和触发自定义事件,以及 EventEmitter 的核心 API 和使用方法。通过本教程,读者将能够全面理解并熟练运用 EventEmitter 进行高效的事件处理。 ... [详细]
  • 深入解析 Vue 中通过 $route.params 实现参数传递的方法与技巧
    本文深入探讨了在 Vue 框架中利用 `$route.params` 进行参数传递的方法和技巧。通过详细解析 `$route.params` 的工作机制及其与 `$route.query` 的区别,帮助开发者更好地理解和应用这一功能。文章不仅涵盖了基本的使用方法,还提供了实际案例和最佳实践,以便读者能够灵活运用这些技术,提升开发效率和代码质量。 ... [详细]
  • 【前端开发】深入探讨 RequireJS 与性能优化策略
    随着前端技术的迅速发展,RequireJS虽然不再像以往那样吸引关注,但其在模块化加载方面的优势仍然值得深入探讨。本文将详细介绍RequireJS的基本概念及其作为模块加载工具的核心功能,并重点分析其性能优化策略,帮助开发者更好地理解和应用这一工具,提升前端项目的加载速度和整体性能。 ... [详细]
  • 基于Node.js、EJSExcel、Express与Vue.js构建Excel转JSON工具:首阶段——Vue.js项目初始化及开发环境配置
    在近期的一个H5游戏开发项目中,需要将Excel数据转换为JSON格式。经过调研,市面上缺乏合适的工具满足需求。因此,决定利用Node.js、EJSExcel、Express和Vue.js自行构建这一工具。本文主要介绍项目的第一阶段,即Vue.js项目的初始化及开发环境的配置过程,详细阐述了如何搭建高效的前端开发环境,确保后续功能开发的顺利进行。 ... [详细]
  • Vue.js 2.0 生命周期详解与应用实例分析
    一、声明周期图例   图片来源:https:www.jianshu.compd61f55da98fb?fromtimeline   二、分析1、newVue()创建vue实例,其实 ... [详细]
author-avatar
大鱼小鱼比目鱼
这个家伙很懒,什么也没留下!
Tags | 热门标签
RankList | 热门文章
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有