热门标签 | 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 让我们一起成长点赞和在看就是最大的支持❤️


推荐阅读
  • VScode格式化文档换行或不换行的设置方法
    本文介绍了在VScode中设置格式化文档换行或不换行的方法,包括使用插件和修改settings.json文件的内容。详细步骤为:找到settings.json文件,将其中的代码替换为指定的代码。 ... [详细]
  • sklearn数据集库中的常用数据集类型介绍
    本文介绍了sklearn数据集库中常用的数据集类型,包括玩具数据集和样本生成器。其中详细介绍了波士顿房价数据集,包含了波士顿506处房屋的13种不同特征以及房屋价格,适用于回归任务。 ... [详细]
  • HDFS2.x新特性
    一、集群间数据拷贝scp实现两个远程主机之间的文件复制scp-rhello.txtroothadoop103:useratguiguhello.txt推pushscp-rr ... [详细]
  • [大整数乘法] java代码实现
    本文介绍了使用java代码实现大整数乘法的过程,同时也涉及到大整数加法和大整数减法的计算方法。通过分治算法来提高计算效率,并对算法的时间复杂度进行了研究。详细代码实现请参考文章链接。 ... [详细]
  • Go GUIlxn/walk 学习3.菜单栏和工具栏的具体实现
    本文介绍了使用Go语言的GUI库lxn/walk实现菜单栏和工具栏的具体方法,包括消息窗口的产生、文件放置动作响应和提示框的应用。部分代码来自上一篇博客和lxn/walk官方示例。文章提供了学习GUI开发的实际案例和代码示例。 ... [详细]
  • Go Cobra命令行工具入门教程
    本文介绍了Go语言实现的命令行工具Cobra的基本概念、安装方法和入门实践。Cobra被广泛应用于各种项目中,如Kubernetes、Hugo和Github CLI等。通过使用Cobra,我们可以快速创建命令行工具,适用于写测试脚本和各种服务的Admin CLI。文章还通过一个简单的demo演示了Cobra的使用方法。 ... [详细]
  • WhenIusepythontoapplythepymysqlmoduletoaddafieldtoatableinthemysqldatabase,itdo ... [详细]
  • 解决github访问慢的问题的方法集锦
    本文总结了国内用户在访问github网站时可能遇到的加载慢的问题,并提供了解决方法,其中包括修改hosts文件来加速访问。 ... [详细]
  • 本文介绍了使用Python编写购物程序的实现步骤和代码示例。程序启动后,用户需要输入工资,并打印商品列表。用户可以根据商品编号选择购买商品,程序会检测余额是否充足,如果充足则直接扣款,否则提醒用户。用户可以随时退出程序,在退出时打印已购买商品的数量和余额。附带了完整的代码示例。 ... [详细]
  • 用Vue实现的Demo商品管理效果图及实现代码
    本文介绍了一个使用Vue实现的Demo商品管理的效果图及实现代码。 ... [详细]
  • Vue3中setup函数的参数props和context配置详解
    本文详细介绍了Vue3中setup函数的参数props和context的配置方法,包括props的接收和配置声明,以及未通过props进行接收配置时的输出值。同时还介绍了父组件传递给子组件的值和模板的相关内容。阅读本文后,读者将对Vue3中setup函数的参数props和context的配置有更深入的理解。 ... [详细]
  • 本文讨论了将HashRouter改为Router后,页面全部变为空白页且没有报错的问题。作者提到了在实际部署中需要在服务端进行配置以避免刷新404的问题,并分享了route/index.js中hash模式的配置。文章还提到了在vueJs项目中遇到过类似的问题。 ... [详细]
  • 一、路由首先需要配置路由,就是点击good组件进入goodDetail组件配置路由如下{path:goodDetail,component:goodDetail}同时在good组件中写入如下点击事件,路由中加入 ... [详细]
  • 一:跨域问题1、同源策略(浏览器的安全策略)    只允许当前页面朝当前域下发请求,如果向其他域发请求,请求可以正常发送,数据也可以拿回,但是被浏览器拦截了  2、c ... [详细]
  • 媒介本文的前身是源自github上的项目awesome-github-vue,但由于该项目上次更新时候为2017年6月12日,许多内容早已逾期或是许多近期优异组件未被收录,所以小肆 ... [详细]
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社区 版权所有