贸易公司网站建设,南谯区住房和城乡建设局网站,旅者志 wordpress主题,网站宣传有文化事业建设费吗目录 前言一、创建一个 Vue 实例二、找到 Vue 构造函数三、源码分析 - Vue.prototype._init四、源码分析 - 调用 $mount 方法#xff0c;进入挂载阶段五、总结 前言
使用Vue也有一段时间了#xff0c;最近去阅读了Vue的源码#xff0c;想总结分享下学到的新东西。 如果觉得… 目录 前言一、创建一个 Vue 实例二、找到 Vue 构造函数三、源码分析 - Vue.prototype._init四、源码分析 - 调用 $mount 方法进入挂载阶段五、总结 前言
使用Vue也有一段时间了最近去阅读了Vue的源码想总结分享下学到的新东西。 如果觉得直接看源码很枯燥可以结合前人总结的文章或者视频来看相信会事半功倍。 源码这个东西一定要多看多思考要想精通一遍两遍肯定是不够的。有的时候可能看着一个问题就会想通之前看过但是不明白的另个问题。
打算出一个Vue源码系列性的文章算是我个人学习源码的一个历程。
首先找到Vue项目 github 地址vue2.x源码链接git clone xxx 下载源码。
一、创建一个 Vue 实例
新建一个 html 文件引入vue。
!DOCTYPE html
html langen
headmeta charsetUTF-8meta http-equivX-UA-Compatible contentIEedgemeta nameviewport contentwidthdevice-width, initial-scale1.0titleDocument/title
/head
bodydiv idapp/divscript src./vue-2.7.14/dist/vue.js/script/scriptscriptnew Vue({el:#app,})/script
/body
/htmlvue 初始化就从这里开始了。
二、找到 Vue 构造函数
// src/core/instance/index.tsimport { initMixin } from ./init
import { stateMixin } from ./state
import { renderMixin } from ./render
import { eventsMixin } from ./events
import { lifecycleMixin } from ./lifecycle
import { warn } from ../util/index
import type { GlobalAPI } from types/global-api// Vue构造函数的声明
function Vue(options) {if (__DEV__ !(this instanceof Vue)) {warn(Vue is a constructor and should be called with the new keyword)}// 初始化方法this._init(options)
}// 从文件中可以看出 上面的 _init() 是从下面的混入中获得的那么具体从哪个中得到的需要分析一下// 初始化混入
initMixin(Vue)
// state的混入
stateMixin(Vue)
// events的混入
eventsMixin(Vue)
// 生命周期的混入
lifecycleMixin(Vue)
// 渲染函数的混入
renderMixin(Vue)// 上面的这些混入其实就是初始化实例的方法和属性
// 其实通过名字不难发现 _init() 方法肯定是在初始化的混入中initMixinexport default Vue as unknown as GlobalAPI
其实通过名字不难发现 _init() 方法肯定是在 初始化的混入中 initMixin() 那就继续看 initMixin() 所在的文件。
三、源码分析 - Vue.prototype._init
// src/core/instance/init.tsexport function initMixin(Vue: typeof Component) {// 负责 Vue 的初始化过程接收用户传进来的选项optionsVue.prototype._init function (options?: Recordstring, any) {// vue的实例const vm: Component this// 每个 vue 实例都有一个 _uid并且是依次递增的vm._uid uidlet startTag, endTagif (__DEV__ config.performance mark) {startTag vue-perf-start:${vm._uid}endTag vue-perf-end:${vm._uid}mark(startTag)}// vue标志, 避免被 Observe 观察vm._isVue truevm.__v_skip truevm._scope new EffectScope(true)vm._scope._vm true// 选项合并用户选项和系统默认的选项需要合并// 处理组件的配置内容将传入的options与构造函数本身的options进行合并(插件的策略都是默认配置和传入配置进行合并)if (options options._isComponent) {// 子组件优化内部组件子组件实例化且动态的options合并相当慢,这里只有需要处理一些特殊的参数属性。减少原型链的动态查找提高执行效率initInternalComponent(vm, options as any)} else {// 根组件 将全局配置选项合并到根组件的配置上其实就是一个选项合并vm.$options mergeOptions(// 获取当前构造函数的基本optionsresolveConstructorOptions(vm.constructor as any),options || {},vm)}if (__DEV__) {initProxy(vm)} else {vm._renderProxy vm}vm._self vm// 下面的方法才是整个初始化最重要的核心代码initLifecycle(vm) // 初始化实例的属性、数据$parent, $children, $refs, $root, _watcher...等initEvents(vm) //初始化事件$on, $off, $emit, $onceinitRender(vm) // 初始化render渲染所需的slots、渲染函数等。其实就两件事1、插槽的处理、2、$createElm 也就是 render 函数中的 h 的声明callHook(vm, beforeCreate, undefined, false /* setContext */) // 调用生命周期的钩子函数在这里就能看出一个组件在创建之前和之后分别做了哪些初始化// provide/inject 隔代传参// provide在祖辈中可以直接提供一个数据 // inject在后代中可以通过inject注入后直接使用initInjections(vm) // 在 data/props之前执行隔代传参时 先inject。作为一个组件在要给后辈组件提供数据之前需要先把祖辈传下来的数据注入进来initState(vm) // 数据响应式的重点处理 props、methods、data、computed、watch初始化initProvide(vm) // 在 data/props之后执行在把祖辈传下来的数据注入进来以后 再provide// 总而言之上面的三个初始化其实就是对组件的数据和状态的初始化callHook(vm, created) // created 初始化完成可以执行挂载了if (__DEV__ config.performance mark) {vm._name formatComponentName(vm, false)mark(endTag)measure(vue ${vm._name} init, startTag, endTag)}// 如果发现配置项上有 el 选项则自动调用 $mount 方法也就是说有了 el 选项就不需要再手动调用 $mount反之没有 el 则必须手动调用 $mountif (vm.$options.el) {// 调用 $mount 方法进入挂载阶段vm.$mount(vm.$options.el)}}
}
四、源码分析 - 调用 $mount 方法进入挂载阶段
打开 $mount 看看它做了什么。把一些多余的 代码简化一下。
// src/platforms/web/runtime-with-compiler.tsimport config from core/config
import { warn, cached } from core/util/index
import { mark, measure } from core/util/perfimport Vue from ./runtime/index
import { query } from ./util/index
import { compileToFunctions } from ./compiler/index
import {shouldDecodeNewlines,shouldDecodeNewlinesForHref
} from ./util/compat
import type { Component } from types/component
import type { GlobalAPI } from types/global-api// 获取宿主元素的方法
const idToTemplate cached(id {const el query(id)return el el.innerHTML
})// 扩展 $mount 方法
const mount Vue.prototype.$mount
Vue.prototype.$mount function (el?: string | Element,hydrating?: boolean
): Component {el el query(el)// 获取选项 $options const options this.$options/*** 编译权重* 优先看有没有render函数如果有直接用* 如果没有render函数就看有没有template模板* 如果都没有就直接获取el的outerHTML作为渲染模板*/// 如果 render 选项不存在if (!options.render) {// 则查找 templatelet template options.template// 如果 template 存在if (template) {// 则判断一下 template 的写法if (typeof template string) { // 如果是字符串模板 例如div template /divif (template.charAt(0) #) { // 如果是宿主元素的选择器例如#app// 则调用上面的 idToTemplate() 方法查找template idToTemplate(template)if (__DEV__ !template) {warn(Template element not found or is empty: ${options.template},this)}}// 如果是一个dom元素} else if (template.nodeType) {// 则使用它的 innerHTMLtemplate template.innerHTML} else {if (__DEV__) {warn(invalid template option: template, this)}return this}// 如果设置了 el } else if (el) {// 则以 el 的 outerHTML 作为 templatetemplate getOuterHTML(el)}// 如果存在 template 选项则编译它获取 render 函数if (template) {// 编译的过程把 template 变为 render 函数const { render, staticRenderFns } compileToFunctions(template,{outputSourceRange: __DEV__,shouldDecodeNewlines,shouldDecodeNewlinesForHref,delimiters: options.delimiters,comments: options.comments},this)// 最终获得的 render 函数将赋值给 选项 optionsoptions.render renderoptions.staticRenderFns staticRenderFns// 执行默认的挂载return mount.call(this, el, hydrating)
}/*** 总结一下:* new Vue({* el: #app,* template: div template /div,* template: #app,* render(h){ return h(div, render)},* data: {}* })* 在用户同时设置了 el、template、render的时候优先级的判断为render template el*/ // 获取 outerHTML 的方法
function getOuterHTML(el: Element): string {if (el.outerHTML) {return el.outerHTML} else {const container document.createElement(div)container.appendChild(el.cloneNode(true))return container.innerHTML}
}Vue.compile compileToFunctionsexport default Vue as GlobalAPI上面代码 主要 实现了 vue 渲染过程中很重要的一步得到 render 函数。
如果我们使用的 template 进行编写 HTML 代码Vue 内部会把模板编译成 Vue 可识别的 render 函数如果有写 render 则可以省去编译过程。 直接写 render 函数对 vue 编译效率会更好 )
上面 entry-runtime-with-compiler.js 文件中的Vue来自于 ‘./runtime/index’那我们自己分析 ‘./runtime/index’ 文件。
// src/platforms/web/runtime/index.ts// 能看到 Vue也不是在这里定义的一样是导入的那么这个文件主要做了什么呢
import Vue from core/index
import config from core/config
import { extend, noop } from shared/util
import { mountComponent } from core/instance/lifecycle
import { devtools, inBrowser } from core/util/indeximport {query,mustUseProp,isReservedTag,isReservedAttr,getTagNamespace,isUnknownElement
} from web/util/indeximport { patch } from ./patch
import platformDirectives from ./directives/index
import platformComponents from ./components/index
import type { Component } from types/component//...// 安装了一个 patch 函数也可以叫补丁函数或者更新函数。主要的作用就是把虚拟dom 转化为真实的domvdom dom)
Vue.prototype.__patch__ inBrowser ? patch : noop// 实现了 $mount 方法其实就只调用了一个mountComponent()方法
// $mount的最终目的就是把虚拟dom 转化为真实的dom并且追加到宿主元素中去vdom dom append
Vue.prototype.$mount function (el?: string | Element,hydrating?: boolean
): Component {el el inBrowser ? query(el) : undefinedreturn mountComponent(this, el, hydrating)
}export default Vue
下面打开源码 src/core/instance/lifecycle.js 找到 mountComponent 方法
// src/core/instance/lifecycle.tsexport function mountComponent(...): Component {// 调用生命周期钩子函数callHook(vm, beforeMount)let updateComponent// 创建一个更新渲染函数; 调用 _update 对 render 返回的虚拟 DOM 进行 patch也就是 Diff )到真实DOM这里是首次渲染updateComponent () {vm._update(vm._render(), hydrating)}// 当触发更新的时候会在更新之前调用const watcherOptions: WatcherOptions {before() {// 判断 DOM 是否是挂载状态就是说首次渲染和卸载的时候不会执行if (vm._isMounted !vm._isDestroyed) {// 调用生命周期钩子函数callHook(vm, beforeUpdate)}}}//生成一个渲染 watcher 每次页面依赖的数据更新后会调用 updateComponent 进行渲染new Watcher(vm,updateComponent,noop,watcherOptions,true )// 没有老的 vnode说明是首次渲染if (vm.$vnode null) {vm._isMounted true// 渲染真实 dom 结束后调用 mounted 生命周期callHook(vm, mounted)}return vm
}
五、总结
到此整个Vue初始化就完成了具体的细化代码这里不展示了主要的就是做标注的代码这里再做个总结吧。 从上面的函数看来new Vue所做的事情就像一个流程图一样展开了分别是
选项合并处理组件的配置内容将传入的options与构造函数本身的options进行合并用户选项和系统默认的选项进行合并初始化 vue实例生命周期 相关的属性组件关系属性的初始化定义了比如 $parent、$children、$root、$refs 等。初始化 事件若存在父监听事件则添加到该实例上。初始化 render渲染 所需的slots、渲染函数等。其实就两件事插槽的处理 和 $createElm的声明也就是 render 函数中的 h 函数的声明。调用 beforeCreate 钩子函数在这里就能看出一个组件在创建前和后分别做了哪些初始化。初始化注入数据隔代传参时 先inject。作为一个组件在要给后辈组件提供数据之前需要先把祖辈传下来的数据注入进来。对props,methods,data,computed,watch进行初始化包括响应式的处理。再把祖辈传下来的数据注入进来以后 再初始化provide。调用 created 钩子函数初始化完成可以执行挂载了。挂载到对应DOM元素上。如果组件构造函数设置了el选项会自动挂载所以就不用再手动调用 $mount 去挂载。
可参考 Vue源码系列二Vue初始化都做了什么 vue源码阅读解析超详细