1.最前端|详解VUE源码初始化流程以及响应式原理
2.Vue3 源码中创建应用实例(createApp)流程
3.vue打包后反编译到源代码详细步骤
4.Vue2.6x源码解析(一):Vue初始化过程
5.vue项目如何初始化?码地
6.每天学点Vue源码: 关于vm.$watch()内部原理
最前端|详解VUE源码初始化流程以及响应式原理
为大家分享一些实用内容,便于大家理解,码地希望对大家在 Vue 开发中有所助益,码地直接进入正题:
Vue 源码的码地入口是 src/core/instance/index.js,此文件负责在 Vue 的码地 prototype 上注册函数属性等,并执行 initMixin 中注册的码地hilog指标源码图解 _init 函数。
继续观察流程,码地_init 方法代表初始化流程,码地主要代码如下:
如果是码地组件,则 _isComponent 为真,码地其他情况下都会执行 resolveConstructorOptions,码地该函数将用户设置的码地 options 和默认 options 合并。随后执行一系列初始化函数,码地如 initLifecycle 初始化生命周期,码地initEvent 初始化事件处理机制,码地initRender 初始化 vnode、插槽及属性等。接下来调用 beforeCreate 钩子函数,然后是 initInjections 和 initProvide 两个与通信相关的组件。
这里涉及到两个熟悉的生命周期函数:beforeCreate 和 created。对比 Vue 流程图,可以明确这两个钩子函数的执行时机。
它们之间实际上差了三个初始化过程。重点是 initState 方法:
在此方法中,如果传入 data 则执行 initData,否则初始化一个空对象。接下来可以看到 computed 和 watch 也是在这里初始化的。
简化后的 initData 代码:
此方法首先判断 data 是否为函数,若是则执行,否则直接取值,因此我们的 data 既可以函数,也可以是对象。然后循环 data 的 key 值,通过 hasOwn 判断属性是否有重复。
isReserved 方法是判断变量名是否以 _ 或 $ 开头,这意味着我们不能使用 _ 和 $ 开头的属性名。然后进入 proxy 方法,该方法通过 Object.defineProperty 设置 get 和 set 将 data 的属性代理到 vm 上,使我们能够通过 this[propName] 访问到 data 上的属性,而无需通过 this.data[propName]。最后执行 observe,如下:
前面都是在做一些初始化等必要的判断,核心只有一句:
从这里开始,我们暂时中止 init 流程,开始响应式流程这条线。在阅读源码时,安卓开发源码你总会被各种支线打断,这是没有办法的事情,只要你还记得之前在做什么就好。
Observer 类是 Vue 实现响应式最重要的三环之一,代码如下:
这里介绍一下 def 函数,这是 Vue 封装的方法,在源码中大量使用,我们可以稍微分析一下,代码如下:
可以看到,也是使用了 Object.defineProperty 方法,上文提到过。这是一个非常强大的方法,可以说 Vue 的双向绑定就是通过它实现的。它有三个配置项:configurable 表示是否可以重新赋值和删除,writable 表示是否可以修改,enumerable 表示该属性是否会被遍历到。Vue 通过 def 方法定义哪些属性是不可修改的,哪些属性是不暴露给用户的。这里通过 def 方法将 Observer 类绑定到 data 的 __ob__ 属性上,有兴趣的同学可以去 debugger 查看 data 和 prop 中的 __ob__ 属性的格式。
再说回 Observer,如果传入的数据是数组,则会调用 observeArray,该函数会遍历数组,然后每个数组项又会去执行 observe 方法,这里显然是一个递归,目的是将所有的属性都调用 observe。这个 observe 方法实际上是 Vue 实现观察者模式的核心,不仅是在初始化 data 的时候用到。最终,data 上的每个属性都会走到 defineReactive 里面来,重点就在这里:
这个方法的作用是将普通数据处理成响应式数据,这里的 get 和 set 就是 Vue 中依赖收集和派发更新的源头。这里又涉及到了响应式另一个重要的类:Dep。
在这段代码中,通过 Object.getOwnPropertyDescriptor 获取对象的属性描述符,如果不存在,则通过 Object.defineProperty 创建。这里的 get 和 set 都是函数,因此 data 和 prop 中所有的值都会因为闭包而缓存在内存中,并且都关联了一个 Dep 对象。
当用户通过 this[propName] 访问属性时,就会触发 get,并调用 dep.depend 方法(下面的 dependArray 实际上就是递归遍历数组,然后去调用那个数据上的仿饿了么源码 __ob__.dep.depend 方法),当赋值更新时,则会触发 set,并调用 observe 对新的值创建 observer 对象,最后调用 dep.notify 方法。
总结起来就是,当赋值时调用 dep.notify;当取值时调用 dep.depend。这个方法的作用就在于此,剩下的工作交给了 Dep 类。
接下来我们可以看一下 Dep 类中做了什么。
这里多贴了一些代码,虽然不属于同一个类,但非常重要。这段代码初始化了一个 subs 数组,这个非常熟悉的数组就是我们经常在 Vue 的属性中看到的,它是一个观察者列表。
前文提到,当 key 的 getter 触发时会调用 depend,将 Dep.target 添加到观察者列表中。这样,在 set 的时候我们才能 notify 去通知 update。
另外,还要提一点,前面在设置 getter 时的代码中有这样一段:
那么既然已经执行了 dep.depend,为什么还要执行 childOb.dep.depend,这又是什么东西呢?
实际上,在数据的增删改查中,响应式的实现方式是不同的。setter 和 getter 只能检测到数据的修改和读取操作,因此这部分是由 dep.depend 来实现的。而 data 的新增删除的属性,并不能直接实现响应式,这部分是由 childOb.dep.depend 来完成的,这就是我们常用的 Vue.set 和 Vue.delete 的实现方式。
接着往下看,我们发现 depend 方法将 Dep.target 推入 subs 中。在上面定义中可以看到,它是一个 Watcher 类的实例,这个类就是响应式系统中的最后一环。
不过,我们暂时不管它,在这里还有一个重要的点:targetStack。可以看到有 pushTarget 和 popTarget 这两个方法,它们遵循着栈的原则,后进先出。因此,外链网盘源码Vue 中的更新也是按照这个原则进行的。另外,大家可能注意到,这里似乎没有实例化 Watcher 对象,那么它是在什么地方执行的呢?下文会提到。
Watcher 的代码很长,我们这里只看一小段。当 notify 被触发时,会调用 update 方法。需要注意的是,这部分已经不是在 init 的流程中了,而是在数据更新时调用的。
这里正常情况下会执行 queueWatcher:
可以看到,当 data 更新时会将 watcher push 到 queue 中,然后等到 nextTick 执行 flushSchedulerQueue,nextTick 也是一个大家很熟悉的东西,Vue 当然不会蠢到每有一个更新就更新一遍 DOM。它就是通过 nextTick 来实现优化的,所有的改动都会被 push 到一个 callbacks 队列中,然后等待全部完成之后一次清空,一起更新。这就是一轮 tick。
言归正传,接着来看 flushSchedulerQueue:
实际核心代码就是遍历所有的 queue,然后执行 watcher.run,最后发出 actived 和 updated 两个 hook。
watcher.run 会更新值然后调用 updateComponent 方法去更新 DOM。至此,响应式原理的主体流程结束。说了这么多,其实下面这个流程图就能完整概括。
我们回到 init 的流程,上文中 init 的流程并没有执行完,还差这最后一句:
即通过传入的 options 将 DOM 给渲染出来,我们来看 $mount 的代码。
前面是在获取元素以及进行一系列的类型检查判断,核心就在 compileToFunctions 这个方法上。
看到这个 ast 我们就应该知道这个函数的作用了,通过 template 获取 AST 抽象语法树,然后根据定义的模板规则生成 render 函数。
这个方法执行完之后返回了 render 函数,之后被赋值在了 options 上,最后调用了 mount.call(this, el, hydrating)。
这个方法很简单,就是微信表白墙源码调用 mountComponent 函数。
这里的流程很容易理解。首先触发 beforeMount 钩子函数,然后通过 vm._render 生成虚拟 DOM(vnode)。这个 vnode 就是常说的虚拟 DOM。生成 vnode 后,再调用 update 方法将其更新为真实的 DOM。在 update 方法中,会实现 diff 算法。最后执行 mounted 钩子函数。需要注意的是,这里的 updateComponent 只是定义出来了,然后将其作为参数传递给了 Watcher。之前提到的 Watcher 就是在这个地方实例化的。
至此,init 的主体流程也结束了。当然,其中还有很多细节没有提到。我也还没有深入研究这些细节,之后有时间会进一步理解和梳理。这篇文章主要是为了自己做个笔记,也分享给大家,希望能有所帮助。如果文中有任何错误之处,请大家指正。
版权声明:本文由神州数码云基地团队整理撰写,若转载请注明出处。
公众号搜索神州数码云基地,了解更多技术干货。
Vue3 源码中创建应用实例(createApp)流程
Vue3的核心应用实例创建过程主要由createAppAPI驱动,这个过程涉及到了关键函数如beforeCreateRender和createApp。createApp位于/vue-core/vue-next/packages/runtime-dom/src/index.ts中,它是项目构建的起点,功能包括组件实例的构建和页面挂载。
首先,createApp通过ensureRenderer函数来构建组件实例,这个过程涉及虚拟节点的操作,如更新和挂载。ensureRenderer会返回createRenderer,进一步生成baseCreateRenderer,最终返回createAppAPI。这个函数的主要任务是为虚拟节点添加如mixin、use、mount、props和emits等功能。
在beforeCreateRender中,主要负责创建render和hydrate渲染器,这些渲染器负责DOM操作,如节点的更新和挂载。虽然这部分内容详细,但略过了具体的实现细节,有兴趣的话,可以参考vue-core/vue-next/packages/runtime-core/src/renderer.ts文件。
createAppAPI函数的核心是返回createApp,这个函数接收根组件和其props作为输入,用于生成Vue应用程序实例。至此,组件实例app已经创建完成,但挂载到页面的过程将在后续内容中深入讨论。
vue打包后反编译到源代码详细步骤
若仅持有编译后的Vue前端文件,且原始文件夹丢失,还原项目源代码的步骤如下: 使用反编译库 reverse-sourcemap 借助此库,可从.map文件还原编译前的Vue文件。安装
执行命令,生成对应源文件至src文件夹
在dist/static/js下,找到大量xxxxx.js.map文件。使用Python脚本统一导出。
执行后,获取源代码文件,位于dist/src/static/js/webpack/src(根据原始编译路径)。
删除/static/js下编译过的js文件,保留正常js文件。
还原的node_modules目录位于dist/src/static/js/webpack。
调整项目目录结构复制反编译得到的src、node_modules文件夹,替换原代码目录。
替换static文件夹至原代码static目录。
删除编译后的index.html中引入的css、js代码,检查静态js、css文件,确保未误删。
管理依赖包信息进入备份的反编译node_modules目录。
执行npm shrinkwrap,生成npm-shrinkwrap.json文件。
文件记录项目所用npm包,但不包括版本号和编译库信息。
检查node_modules目录中的库信息,确认重要库如vue、npm的版本号。
启动项目回到构建项目目录。
修改原package.json,保留编译所需库,如本地使用webpack。
根据需求调整本地package.json,执行npm run start。
耐心查找依赖,根据报错提示逐个安装。
查看源代码引入的库,推测内容。
生成package.json项目启动成功后,执行npm shrinkwrap生成新的npm-shrinkwrap.json文件。
对照第3步得到的npm-shrinkwrap.json文件,确认库版本和信息。
额外提示复制项目,删除node_modules,新建目录,执行npm run install后,重新npm run start。
根据报错提示逐个安装依赖库,直至项目启动。
注意,执行npm run start时需删除npm-shrinkwrap.json文件。
完成上述步骤后,可成功还原Vue项目源代码。祝您反编译成功!Vue2.6x源码解析(一):Vue初始化过程
Vue2.6x源码解析(一):Vue初始化过程
Vue.js的核心代码在src/core目录,它在任何环境都能运行。项目入口通常在src/main.js,引入的Vue构造函数来自dist/vue.runtime.esm.js,这个文件导出了Vue构造函数,允许我们在创建Vue实例前预置全局API和原型方法。
初始化前,Vue构造函数在src/core/instance/index.js中定义,它预先挂载了全局API如set、delete等。即使不通过new Vue初始化,Vue本身已具备所需功能。
当执行new Vue时,实际上是调用了_init方法,这个过程会在src/core/index.js的initGlobalAPI(Vue)中初始化全局API和原型方法。接着,组件实例的初始化与根实例基本一致,包括组件构造函数的定义,以及组件的生命周期、渲染和挂载。
组件初始化过程中,关键步骤包括数据转换为响应式、事件注册和watcher的创建。例如,组件的渲染函数会触发渲染方法,而watcher的更新则通过异步更新队列机制确保性能。
在开发环境,Vue-template-compiler插件负责模板编译,然后runtime中的$mount方法负责实际的渲染和挂载。整个过程涉及组件的构建、渲染函数生成、依赖响应式数据的更新和异步调度。
vue项目如何初始化?
在Vue项目中初始化的过程,从源码的入口src/core/instance/index.js开始。在Vue的prototype上注册函数属性等,并执行initMixin中注册的_init函数,这是初始化流程的核心。
初始化流程主体代码在_init方法中。对于组件,_init方法执行resolveConstructorOptions函数,将用户设置的options与默认options合并,然后调用一系列初始化函数,如initLifecycle初始化生命周期、initEvent初始化事件处理机制、initRender初始化vnode、插槽和属性等。
initLifecycle函数处理生命周期方法,initEvent处理事件,initRender处理渲染过程。初始化流程中涉及到beforeCreate和created两个生命周期函数,它们之间差了三个初始化过程,重点在于initState方法。initState根据传入的data执行initData,处理data的函数或对象,循环data的key值,通过hasOwn判断属性是否有重复。isReserved方法检查属性名是否以_或$开头,然后进入proxy方法,通过Object.defineProperty将data属性代理到vm上,使可以通过this[propName]访问data属性。
Observer类是实现响应式的重要组件,它通过observe方法观察数据变化,当data更新时,每个属性都会通过defineReactive变为响应式。defineReactive方法使用Object.defineProperty设置get和set,实现双向绑定。Dep类在数据变化时收集依赖,当访问或更新属性时,触发通知更新。
Watcher类是响应式系统中的最后一环,负责执行更新操作。当数据变化时,Watcher实例化并调用update方法,更新DOM。在初始化流程的最后,通过传入的options渲染DOM,使用$mount方法实现。
整个初始化流程包括组件的生命周期、事件、渲染和DOM更新等关键步骤,Vue通过这些机制实现了数据与DOM的响应式更新。
每天学点Vue源码: 关于vm.$watch()内部原理
深入探讨Vue源码,解析vm.$watch()的内部原理,让我们从整体结构入手。使用vm.$watch()时,首先数据属性被整个对象a进行观察,这个过程产生一个名为ob的Observe实例。在该实例中,存在dep,它代表依赖关系,而依赖关系在Observe实例内部进行存储。接下来,我们聚焦于内部实现细节,深入理解vm.$watch()在源码中的运作机制。
在Vue的源代码中,实现vm.$watch()功能的具体位置位于`vue/src/core/instance/state.js`文件。从这里开始,我们移步至`vue/src/core/observer/watcher.js`文件,探寻更深入的实现逻辑。此文件内,watcher.js承担了关键角色,管理着观察者和依赖关系的关联。
在深入解析源码过程中,我们发现,当使用vm.$watch()时,Vue会创建一个Watcher实例,这个实例负责监听特定属性的变化。每当被观察的属性值发生变化时,Watcher实例就会触发更新,确保视图能够相应地更新。这一过程通过依赖的管理来实现,即在Observe实例内部,依赖关系被封装并存储,确保在属性变化时能够准确地通知相关的Watcher实例。
总的来说,vm.$watch()的内部实现依赖于Vue框架的观察者模式,通过创建Observe实例和Watcher实例来实现数据变化的监听和响应。这一机制保证了Vue应用的响应式特性,使得开发者能够轻松地在数据变化时触发视图更新,从而构建动态且灵活的应用程序。
每天学点Vue源码: 关于vm.$delete()/Vue.use() 内部原理
vm.$delete() 方法的使用请参考官网文档。为何需要Vue.delete()?在ES6之前,JavaScript并未提供检测属性被删除的机制。通过delete操作删除属性后,Vue无法检测到变动,导致数据响应式机制失效。举例如下。
深入源码分析内部实现,vm.$delete()相关操作在vue/src/core/instance/state.js中的stateMixin方法以及vue/src/core/observer/index.js的del函数中进行。重要的是,一旦数据被observe,会创建一个私有的ob属性,这是在Observer类的构造器内完成的。
探讨Vue.use()方法,它是全局API,用于安装插件。具体用法可参阅官网。Vue.use()何时绑定至Vue原型?源码位于vue/src/core/index.js的Vue对象内部。
初始化全局API的源码在vue/src/core/global-api/index.js的initGlobalAPI()方法中,而绑定Vue.use()的逻辑在vue/src/core/global-api/use.js的initUse()方法中。
关于Vuex的源码,我们知道创建Vue.js插件时应公开一个install方法。此方法接收Vue构造器与可选选项对象作为参数。让我们以Vuex为例,看看其install方法的工作原理。
Vuex的install方法实现逻辑位于vuex-dev/src/store.js。它通过applyMixin()方法来实现,此方法位于vuex/src/mixin.js中。Vuex的install方法何时执行?在创建Vue实例时。