龙空技术网

一文带你搞懂Vue3 底层源码

Echa攻城狮 633

前言:

如今看官们对“vue30项目源码”大概比较珍视,兄弟们都想要知道一些“vue30项目源码”的相关知识。那么小编同时在网络上收集了一些有关“vue30项目源码””的相关知识,希望同学们能喜欢,朋友们一起来学习一下吧!

作者:妹红大大

转发链接:

前言

vue3 出来有一段时间了。今天正式开始记录一下梗vue 3.0.0-beta 源码学习心得。

本文脚手架使用 vite-app 版本 0.20.0,内置 vue 3.0.0-beta.14。

ps: 可能大部分人都不清楚 vue3 的开发api,将源码之前先讲述 使用方法

环境搭建

最容易搭建 vue3 的方式就是使用作者的 vite《一个由 Vue 作者尤雨溪开发的 web 开发工具—vite》

通过 npm 安装

  $ npm init vite-app <project-name>  $ cd <project-name>  $ npm install  $ npm run dev

也可以通过 yarn 安装

  $ yarn create vite-app <project-name>  $ cd <project-name>  $ yarn  $ yarn dev

安装的过程中你可能遇到以下问题(反正本文遇到了)

异常1:No valid exports main found for' C:\xxx\xxx\node_ modules\@rollup\pluginutils'异常2:The engine "node" is incompatible with this module. Expected version ">= 10.16.0". Got "10.15.3

异常1:本菜翻阅了 vite 的 issue,然后 google + baidu 一无所获, 最后发现是因为本菜 node 版本为 13.5.0导致的(版本过高),

异常2:很明显啦,node 版本太低了。

最后的解决方式是:本菜通过 nvm 将 node 版本切换到 12.12.0,至于 nvm 没使用过的童鞋们可以去尝试下哦。特别好用

vite 原理解析

当浏览器识别 type="module" 引入js文件的时候,内部的 import 就会发起一个网络请求,尝试去获取这个文件。

那么就可以通过通过拦截路由 / 和 .js 结尾的请求。然后通过 node 去加载对应的 .js 文件

    const fs = require('fs')    const path = require('path')    const Koa = require('koa')    const app = new Koa()    app.use(async ctx=>{        const {request:{url} } = ctx        // 首页        if(url=='/'){n            ctx.type="text/html"            ctx.body = fs.readFileSync('./index.html','utf-8')        }else if(url.endsWith('.js')){            // js文件            const p = path.resolve(__dirname,url.slice(1))            ctx.type = 'application/javascript'            const content = fs.readFileSync(p,'utf-8')            ctx.body = content        }    })    app.listen(3001, ()=>{        console.log('听我口令,3001端口,起~~')    })

如果只是简单的代码,这样加载就可以了。完全是按需加载,比起 webpack 的语法解析性能当然会快非常多。

但是遇到第三方库以上代码就会找不到 .js 文件的位置了,此时 vite 会用 es-module-lexer 把文件解析成 ast,拿到 import 的地址。

通过分析 import 的内容,识别是不是第三方库(这个主要是看前面是不是相对路径)

如果是第三方库就去 node_modules 中查找,vite 中通过在第三方库中添加前缀 /@modules/,然后发现了 /@modules/ 后走 第三方库逻辑

    if(url.startsWith('/@modules/')){        // 这是一个node_module里的东西        const prefix = path.resolve(__dirname,'node_modules',url.replace('/@modules/',''))        const module = require(prefix+'/package.json').module        const p = path.resolve(prefix,module)        const ret = fs.readFileSync(p,'utf-8')        ctx.type = 'application/javascript'        ctx.body = rewriteImport(ret)    }

这样第三方库也可以解析了。然后是 .vue 单文件解析。

首先 xx.vue 返回的格式大概是这样的

const __script = {    setup() {        ...    }}import {render as __render} from "/src/App.vue?type=template&t=1592389791757"__script.render = __renderexport default __script

然后可以用 @vue/compiler-dom 把 html 解析成 render

解析 .css 就更加简单了。通过 document.createElement('style')然后再注入就好了

reactive

正式进入正题。

作为 vue2 的使用者最想知道的肯定是 vue3 的数据劫持和双向绑定了。在 vue3中,双向绑定和可选项,如果需要使用双向绑定的需要通过 reactive方法进万数据劫持。

在这之前你还需要知道一个函数 setup

setup 是使用 Composition API 的入口setup 可以返回一个对象,该对象的属性会被合并到渲染上下文,并可以在模板中直接使用setup 也可以返回 render 函数

现在开始写一个简单的 vue

  <template>    <div>      <div>{{ count }}</div>      <button @click="increment">count++</button>    </div>  </template>  <script>    import { reactive } from 'vue'    export default {      setup() {        let count = reactive({          num: 0        })        const increment = () => count.num++        return {          count,          increment        }      }    }  </script>

emmm。这样点击按钮就可以动态改变 dom 中的 count 值了。

现在开始解读 reactive 源码。

首先找到 reactivity.esm-browser.js 文件,找到 626 行。

function reactive(target) {  // if trying to observe a readonly proxy, return the readonly version.  if (target && target.__v_isReadonly) {      return target;  }  return createReactiveObject(target, false, mutableHandlers, mutableCollectionHandlers);}

上面的 __v_isReadonly 其实是一个 typescript 的枚举值

export const enum ReactiveFlags {  skip = '__v_skip',  isReactive = '__v_isReactive',  isReadonly = '__v_isReadonly',  raw = '__v_raw',  reactive = '__v_reactive',  readonly = '__v_readonly'}

不同的枚举值对应了不同的数据劫持方式,例如 reactive、 shallowReactive 、readonly、 shallowReadonly

然后进入 createReactiveObject 在 649 行,意思就是:「创建响应式对象」

function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers) {    // 略...        // 如果target已经代理了, 返回target    if (target.__v_raw && !(isReadonly && target.__v_isReactive)) {        return target;    }    // target already has corresponding Proxy    if (hasOwn(target, isReadonly ? "__v_readonly" /* readonly */ : "__v_reactive" /* reactive */)) {        return isReadonly ? target.__v_readonly : target.__v_reactive;    }        if (!canObserve(target)) {        return target;    }    // 重点...    // collectionHandlers:对引用类型的劫持,    // baseHandlers: 对进行基本类型的劫持    const observed = new Proxy(target, collectionTypes.has(target.constructor) ? collectionHandlers : baseHandlers);    def(target, isReadonly ? "__v_readonly" /* readonly */ : "__v_reactive" /* reactive */, observed);    return observed;}

createReactiveObject 做了以下几件事

防止重复劫持只读劫持根据不同类型选择不同的劫持方式(collectionHandlers 或 baseHandlers)

实现劫持的主要方法是通过 Proxy 方法,(Proxy 使用可以看看阮老师的博客),顺藤摸瓜找到 mutableHandlers 定义的地方。在 338 行

const mutableHandlers = {    get,    set,    deleteProperty,    has,    ownKeys};// 229行const get = /*#__PURE__*/ createGetter();// 251 行function createGetter(isReadonly = false, shallow = false) {    return function get(target, key, receiver) {        // 一些 __v_isReactive、__v_isReadonly、__v_raw的处理        // 略...        // 数组操作        const targetIsArray = isArray(target);        if (targetIsArray && hasOwn(arrayInstrumentations, key)) {            return Reflect.get(arrayInstrumentations, key, receiver);        }        // 非数组        const res = Reflect.get(target, key, receiver);                // 其他 调用 track 返回 res 的情况        // 略...                // 如果可写,那么会调用 track        !isReadonly && track(target, "get" /* GET */, key);        // 如果是对象呢。那么递归        return isObject(res)            ? isReadonly                ? // need to lazy access readonly and reactive here to avoid                    // circular dependency                    readonly(res)                : reactive(res)            : res;    };}

mutableHandlers 主要是一个含有 Proxy 各种方法的常量。

get 指向了方法 createGetter, 「创建 get 劫持」

createGetter 主要做了以下事情

异常处理如果是数组且hasOwn(arrayInstrumentations, key) 则调用 arrayInstrumentations 获取值调用 track对象迭代 reactive

那么数组的 arrayInstrumentations 是什么呢?我们来到源码的 第 234 行。

const arrayInstrumentations = {};['includes', 'indexOf', 'lastIndexOf'].forEach(key => {    arrayInstrumentations[key] = function (...args) {        //         const arr = toRaw(this);        for (let i = 0, l = this.length; i < l; i++) {            track(arr, "get" /* GET */, i + '');        }        // we run the method using the original args first (which may be reactive)        // 我们首先 以原始args 运行该方法(可能是反应性的)        const res = arr[key](...args);        if (res === -1 || res === false) {            // if that didn't work, run it again using raw values.            // 如果那不起作用,则使用原始值再次运行它。            return arr[key](...args.map(toRaw));        }        else {            return res;        }    };});

通过 arrayInstrumentations 得到 hasOwn(arrayInstrumentations, key) 就是指 ['includes', 'indexOf', 'lastIndexOf']

arrayInstrumentations 中还是调用了 track 方法,那么 track 方法就更加神秘了。来看看它的源码吧?源码在 126 行

function track(target, type, key) {    if (!shouldTrack || activeEffect === undefined) {        return;    }    let depsMap = targetMap.get(target);    if (!depsMap) {        targetMap.set(target, (depsMap = new Map()));    }    let dep = depsMap.get(key);    if (!dep) {        depsMap.set(key, (dep = new Set()));    }    if (!dep.has(activeEffect)) {        dep.add(activeEffect);        activeEffect.deps.push(dep);        if ( activeEffect.options.onTrack) {            activeEffect.options.onTrack({                effect: activeEffect,                target,                type,                key            });        }    }}

首先 track 需要 shouldTrack 和 activeEffect 为真。

在不考虑 activeEffect 的情况下。track 所做的事情就是

创建包含自身的 map将 activeEffect 赛道 map 中触发 onTrack

然后 activeEffect 又是什么呢?找到 3687 行,这里有个 createReactiveEffect 函数。

function createReactiveEffect(fn, options) {    const effect = function reactiveEffect(...args) {        return run(effect, fn, args);    };    effect._isEffect = true;    effect.active = true;    effect.raw = fn;    effect.deps = [];    effect.options = options;    return effect;}

createReactiveEffect 是在 effect 中被调用的

而 effect 分别在以下地方被使用了

trigger 通过 scheduleRun 调用 effect:源码 3756 行mountComponent 通过 setupRenderEffect 调用 effect:源码 6235 行PS 该阶段在 createComponentInstance 之后doWatch 通过 scheduler 调用 effect

先开始讲述 trigget 相关的代码(核心哦)

function trigger(target, type, key, extraInfo) {    const depsMap = targetMap.get(target);        // 略...    const effects = new Set();    const computedRunners = new Set();    if (type === "clear" /* CLEAR */) {      // collection being cleared, trigger all effects for target      depsMap.forEach(dep => {        addRunners(effects, computedRunners, dep);      });    }    // 略...     const run = (effect) => {      scheduleRun(effect, target, type, key, extraInfo);    };        computedRunners.forEach(run);    effects.forEach(run);  }

trigger 最终是在 set 函数中被使用,源码 3855 行,这个 set 就是数据劫持所用的 set

function set(target, key, value, receiver) {    value = toRaw(value);    const oldValue = target[key];    if (isRef(oldValue) && !isRef(value)) {        oldValue.value = value;        return true;    }    const hadKey = hasOwn(target, key);    const result = Reflect.set(target, key, value, receiver);        {        const extraInfo = { oldValue, newValue: value };        if (!hadKey) {            trigger(target, "add" /* ADD */, key, extraInfo);        }        else if (hasChanged(value, oldValue)) {            trigger(target, "set" /* SET */, key, extraInfo);        }        }    }    return result;}

在源码 3900 行中,被 mutableHandlers、readonlyHandlers 等函数中被使用。

还记得吗?mutableHandlers 是什么?可以回到文章开头部分 reactive源码讲解之初的 createReactiveObject 方法。在通过 Proxy 劫持数据的时候用的就是 mutableHandlers

reactive 总结

所以,这里就成环了。

其实 effect 才是响应式的核心,在 mountComponent、doWatch、reactive 中被调用。在 reactive 中 通过 Proxy 实现劫持。在 Proxy 劫持set时调用 trigger。然后在 targger 中清除收集并触发目标的所有 effects最终触发 patch 游戏结束。推荐Vue学习资料文章:

《9个优秀的 VUE 开源项目》

《细聊Single-Spa + Vue Cli 微前端落地指南「实践」》

《通俗易懂的Vue异步更新策略及 nextTick 原理》

《通俗易懂的Vue响应式原理以及依赖收集》

《原生JS +Vue实现框选功能》

《Vue.js轮播库热门精选》

《一文带你搞懂vue/react应用中实现ssr(服务端渲染)》

《Vue+CSS3 实现图片滑块效果》

《教你Vue3 Compiler 优化细节,如何手写高性能渲染函数(上)》

《教你Vue3 Compiler 优化细节,如何手写高性能渲染函数(下)》

《vue实现一个6个输入框的验证码输入组件》

《一用惊人的Vue实践技巧「值得推荐」》

《Vue常见的面试知识点汇总(上)「附答案」》

《Vue常见的面试知识点汇总(下)「附答案」》

《Kbone原理详解与小程序技术选型》

《为什么我不再用Vue,改用React?》

《让Jenkins自动部署你的Vue项目「实践」》

《20个免费的设计资源 UI套件背景图标CSS框架》

《Deno将停止使用TypeScript,并公布五项具体理由》

《前端骨架屏都是如何生成的》

《Vue原来可以这样写开发效率杠杠的》

《用vue简单写一个音乐播放组件「附源码」》

《为什么Vue3.0不再使用defineProperty实现数据监听?》

《「干货」学会这些Vue小技巧,可以早点下班和女神约会》

《探索 Vue-Multiselect》

《细品30张脑图带你从零开始学Vue》

《Vue后台项目中遇到的技术难点以及解决方案》

《手把手教你Electron + Vue实战教程(五)》

《手把手教你Electron + Vue实战教程(四)》

《手把手教你Electron + Vue实战教程(三)》

《手把手教你Electron + Vue实战教程(二)》

《手把手教你Electron + Vue实战教程(一)》

《收集22种开源Vue模板和主题框架「干货」》

《如何写出优秀后台管理系统?11个经典模版拿去不谢「干货」》

《手把手教你实现一个Vue自定义指令懒加载》

《基于 Vue 和高德地图实现地图组件「实践」》

《一个由 Vue 作者尤雨溪开发的 web 开发工具—vite》

《是什么让我爱上了Vue.js》

《1.1万字深入细品Vue3.0源码响应式系统笔记「上」》

《1.1万字深入细品Vue3.0源码响应式系统笔记「下」》

《「实践」Vue 数据更新7 种情况汇总及延伸解决总结》

《尤大大细说Vue3 的诞生之路「译」》

《提高10倍打包速度工具Snowpack 2.0正式发布,再也不需要打包器》

《大厂Code Review总结Vue开发规范经验「值得学习」》

《Vue3 插件开发详解尝鲜版「值得收藏」》

《带你五步学会Vue SSR》

《记一次Vue3.0技术干货分享会》

《Vue 3.x 如何有惊无险地快速入门「进阶篇」》

《「干货」微信支付前后端流程整理(Vue+Node)》

《带你了解 vue-next(Vue 3.0)之 炉火纯青「实践」》

《「干货」Vue+高德地图实现页面点击绘制多边形及多边形切割拆分》

《「干货」Vue+Element前端导入导出Excel》

《「实践」Deno bytes 模块全解析》

《细品pdf.js实践解决含水印、电子签章问题「Vue篇」》

《基于vue + element的后台管理系统解决方案》

《Vue仿蘑菇街商城项目(vue+koa+mongodb)》

《基于 electron-vue 开发的音乐播放器「实践」》

《「实践」Vue项目中标配编辑器插件Vue-Quill-Editor》

《基于 Vue 技术栈的微前端方案实践》

《消息队列助你成为高薪 Node.js 工程师》

《Node.js 中的 stream 模块详解》

《「干货」Deno TCP Echo Server 是怎么运行的?》

《「干货」了不起的 Deno 实战教程》

《「干货」通俗易懂的Deno 入门教程》

《Deno 正式发布,彻底弄明白和 node 的区别》

《「实践」基于Apify+node+react/vue搭建一个有点意思的爬虫平台》

《「实践」深入对比 Vue 3.0 Composition API 和 React Hooks》

《前端网红框架的插件机制全梳理(axios、koa、redux、vuex)》

《深入Vue 必学高阶组件 HOC「进阶篇」》

《深入学习Vue的data、computed、watch来实现最精简响应式系统》

《10个实例小练习,快速入门熟练 Vue3 核心新特性(一)》

《10个实例小练习,快速入门熟练 Vue3 核心新特性(二)》

《教你部署搭建一个Vue-cli4+Webpack移动端框架「实践」》

《2020前端就业Vue框架篇「实践」》

《详解Vue3中 router 带来了哪些变化?》

《Vue项目部署及性能优化指导篇「实践」》

《Vue高性能渲染大数据Tree组件「实践」》

《尤大大细品VuePress搭建技术网站与个人博客「实践」》

《10个Vue开发技巧「实践」》

《是什么导致尤大大选择放弃Webpack?【vite 原理解析】》

《带你了解 vue-next(Vue 3.0)之 小试牛刀【实践】》

《带你了解 vue-next(Vue 3.0)之 初入茅庐【实践】》

《实践Vue 3.0做JSX(TSX)风格的组件开发》

《一篇文章教你并列比较React.js和Vue.js的语法【实践】》

《手拉手带你开启Vue3世界的鬼斧神工【实践】》

《深入浅出通过vue-cli3构建一个SSR应用程序【实践】》

《怎样为你的 Vue.js 单页应用提速》

《聊聊昨晚尤雨溪现场针对Vue3.0 Beta版本新特性知识点汇总》

《【新消息】Vue 3.0 Beta 版本发布,你还学的动么?》

《Vue真是太好了 壹万多字的Vue知识点 超详细!》

《Vue + Koa从零打造一个H5页面可视化编辑器——Quark-h5》

《深入浅出Vue3 跟着尤雨溪学 TypeScript 之 Ref 【实践】》

《手把手教你深入浅出vue-cli3升级vue-cli4的方法》

《Vue 3.0 Beta 和React 开发者分别杠上了》

《手把手教你用vue drag chart 实现一个可以拖动 / 缩放的图表组件》

《Vue3 尝鲜》

《总结Vue组件的通信》

《Vue 开源项目 TOP45》

《2020 年,Vue 受欢迎程度是否会超过 React?》

《尤雨溪:Vue 3.0的设计原则》

《使用vue实现HTML页面生成图片》

《实现全栈收银系统(Node+Vue)(上)》

《实现全栈收银系统(Node+Vue)(下)》

《vue引入原生高德地图》

《Vue合理配置WebSocket并实现群聊》

《多年vue项目实战经验汇总》

《vue之将echart封装为组件》

《基于 Vue 的两层吸顶踩坑总结》

《Vue插件总结【前端开发必备】》

《Vue 开发必须知道的 36 个技巧【近1W字】》

《构建大型 Vue.js 项目的10条建议》

《深入理解vue中的slot与slot-scope》

《手把手教你Vue解析pdf(base64)转图片【实践】》

《使用vue+node搭建前端异常监控系统》

《推荐 8 个漂亮的 vue.js 进度条组件》

《基于Vue实现拖拽升级(九宫格拖拽)》

《手摸手,带你用vue撸后台 系列二(登录权限篇)》

《手摸手,带你用vue撸后台 系列三(实战篇)》

《前端框架用vue还是react?清晰对比两者差异》

《Vue组件间通信几种方式,你用哪种?【实践】》

《浅析 React / Vue 跨端渲染原理与实现》

《10个Vue开发技巧助力成为更好的工程师》

《手把手教你Vue之父子组件间通信实践讲解【props、$ref 、$emit】》

《1W字长文+多图,带你了解vue的双向数据绑定源码实现》

《深入浅出Vue3 的响应式和以前的区别到底在哪里?【实践】》

《干货满满!如何优雅简洁地实现时钟翻牌器(支持JS/Vue/React)》

《基于Vue/VueRouter/Vuex/Axios登录路由和接口级拦截原理与实现》

《手把手教你D3.js 实现数据可视化极速上手到Vue应用》

《吃透 Vue 项目开发实践|16个方面深入前端工程化开发技巧【上】》

《吃透 Vue 项目开发实践|16个方面深入前端工程化开发技巧【中】》

《吃透 Vue 项目开发实践|16个方面深入前端工程化开发技巧【下】》

《Vue3.0权限管理实现流程【实践】》

《后台管理系统,前端Vue根据角色动态设置菜单栏和路由》

作者:妹红大大

转发链接:

标签: #vue30项目源码