Vue源码学习 - new Vue初始化都做了什么?

目录

  • 前言
  • 一、创建一个 Vue 实例
  • 二、找到 Vue 构造函数
  • 三、源码分析 - Vue.prototype._init
  • 四、源码分析 - 调用 $mount 方法,进入挂载阶段
  • 五、总结

前言

使用Vue也有一段时间了,最近去阅读了Vue的源码,想总结分享下学到的新东西。
如果觉得直接看源码很枯燥,可以结合前人总结的文章或者视频来看,相信会事半功倍。
源码这个东西,一定要多看多思考,要想精通,一遍两遍肯定是不够的。有的时候可能看着一个问题就会想通之前看过但是不明白的另个问题。

打算出一个Vue源码系列性的文章,算是我个人学习源码的一个历程。

首先找到Vue项目 github 地址:vue2.x源码链接,git clone xxx 下载源码。

一、创建一个 Vue 实例

新建一个 html 文件引入vue。

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head>
<body><div id="app"></div><script src="./vue-2.7.14/dist/vue.js"></script></script><script>new Vue({el:'#app',})</script>
</body>
</html>

vue 初始化就从这里开始了。

二、找到 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() 方法肯定是在初始化的混入中:initMixin()export 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?: Record<string, any>) {// vue的实例const vm: Component = this// 每个 vue 实例都有一个 _uid,并且是依次递增的vm._uid = uid++let 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/perf'import 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 </div>"if (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/index'import {query,mustUseProp,isReservedTag,isReservedAttr,getTagNamespace,isUnknownElement
} from 'web/util/index'import { patch } from './patch'
import platformDirectives from './directives/index'
import platformComponents from './components/index'
import type { Component } from 'types/component'//...// 安装了一个 patch 函数,也可以叫补丁函数或者更新函数。主要的作用就是把:虚拟dom 转化为真实的dom(vdom => 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所做的事情,就像一个流程图一样展开了,分别是

  1. 选项合并,处理组件的配置内容,将传入的options与构造函数本身的options进行合并(用户选项和系统默认的选项进行合并)
  2. 初始化 vue实例生命周期 相关的属性,组件关系属性的初始化,定义了比如 $parent$children$root$refs 等。
  3. 初始化 事件,若存在父监听事件,则添加到该实例上。
  4. 初始化 render渲染 所需的slots、渲染函数等。其实就两件事:插槽的处理 和 $createElm的声明,也就是 render 函数中的 h 函数的声明。
  5. 调用 beforeCreate 钩子函数,在这里就能看出一个组件在创建前和后分别做了哪些初始化。
  6. 初始化注入数据,隔代传参时 先inject。作为一个组件,在要给后辈组件提供数据之前,需要先把祖辈传下来的数据注入进来。
  7. props,methods,data,computed,watch进行初始化,包括响应式的处理。
  8. 再把祖辈传下来的数据注入进来以后 再初始化provide。
  9. 调用 created 钩子函数,初始化完成,可以执行挂载了。
  10. 挂载到对应DOM元素上。如果组件构造函数设置了el选项,会自动挂载,所以就不用再手动调用 $mount 去挂载。

可参考:
Vue源码系列(二):Vue初始化都做了什么?
vue源码阅读解析(超详细)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/7145.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Spring MVC学习】连接 接收请求参数 响应返回参数

目录 前言&#xff1a;认识Spring MVC &#x1f337;1、什么是MVC&#xff1f; 一、建立连接&#xff08;5个注解&#xff09; &#x1f337;1、RequestMapping注解:注册接⼝的路由映射&#xff08;默认返回页面&#xff09; &#x1f337;2、ResponseBody注解&#xff1a…

Java可变参数

一、基本概念 Java允许将同一个类中多个同名同功能但参数个数不同的方法&#xff0c;封装成一个方法。 二、基本语法 访问修饰符 返回类型方法名(数据类型... 形参名){ }三、注意事项 可变参数的实参可以为0个或者任意多个&#xff1b; 可变参数的实参可以为数据&#xff1…

unity调用c++dll时变量处理问题

在C中为了处理方便可以将有些局部变量变为全局变量。 以下面三个变量为例 如果还有新全局变量需要这其来赋值&#xff0c;就会有问题。 如下面的body1_ptr为了dll生成处理方便&#xff0c;变为全局变量后&#xff0c;这个指针变赋值就会有问题。 因为geometry_path这个只在调…

Ubuntu22.04上部署Lua开发环境

需求背景 想在Ubuntu22.04上搭建一下Lua的开发环境&#xff0c;其实步骤比较简单的&#xff0c;此文章也适用于Ubuntu主机环境搭建Lua,如果想在在Ubuntu内部署一个容器&#xff0c;然后在容器内搭建Lua的环境&#xff0c;可以先参考容器的创建过程 ubuntu22.04上如何创建有pri…

字节抖音小程序,使用 uniapp 调起内置支付

字节抖音小程序&#xff0c;使用 uniapp 调起内置支付 第一步&#xff1a;提交订单 后端通过抖音预下单接口&#xff0c;提交支付订单信息。 预下单接口_小程序_抖音开放平台预下单接口 提交支付订单信息。 ## 使用限制 无 ## 接口说明 预下单接口需要保证同一app_id下每笔订…

Linux6.15 Docker 私有仓库(harbor)

文章目录 计算机系统5G云计算第四章 LINUX Docker 私有仓库&#xff08;harbor&#xff09;一、搭建本地私有仓库二、Docker--harbor私有仓库部署与管理1.Harbor 简介1&#xff09;什么是Harbor2&#xff09;Harbor的特性3&#xff09;Harbor的构成 2.Harbor 部署1&#xff09;…

【深度学习-神经网络架构-通俗易懂的入门课程】

文章目录 深度学习与AI的关系机器学习的流程机器学习的核心以及问题深度学习要解决的问题模型如何搭建&#xff1f;特征如何提取&#xff1f;为什么要深度学习&#xff1f; 深度学习的应用深度学习的问题计算机视觉任务分类与检索如何实现分类 神经网络基础线性函数损失函数防止…

树状数组笔记

数组、前缀和、树状数组的区别&#xff1a; 数组&#xff1a;修改某点O&#xff08;1&#xff09;&#xff0c;求区间O&#xff08;n&#xff09; 前缀和&#xff1a;修改某点O&#xff08;n&#xff09;&#xff0c;求区间O&#xff08;1&#xff09; 树状数组&#xff1a;修改…

ADO.NET访问数据库基础技术介绍

ADO.NET是.NET框架中用于访问数据库的一组类和技术。它提供了一种低级别的数据访问方式&#xff0c;允许开发人员直接使用SQL查询来与数据库交互。 ADO.NET主要由以下几个重要的组件组成&#xff1a; Connection&#xff08;连接&#xff09;&#xff1a; Connection类用于在…

YOLOv5(v7.0)网络修改实践二:把单分支head改为YOLOX的双分支解耦合head(DecoupleHead)

前面研究了一下YOLOX的网络结构&#xff0c;在YOLOv5(tag7.0)集成了yolox的骨干网络&#xff0c;现在继续下一步集成YOLOX的head模块。YOLOX的head模块是双分支解耦合网络&#xff0c;把目标置信度的预测和目标的位置预测分成两条支路&#xff0c;并验证双分支解耦合头性能要优…

力扣 56. 合并区间

题目来源&#xff1a;https://leetcode.cn/problems/merge-intervals/description/ C题解&#xff1a;根据左区间排序&#xff0c;更新每一段的右区间最大值&#xff0c;直到间断。 class Solution { public:static bool cmp(vector<int> & a, vector<int> &a…

Linux实训笔记~操作系统概述

1、操作系统 操作系统作为接口的示意图: 没有安装操作系统的计算机, 通常被称为裸机。 2、不同应用利于的主流操作系统 桌面操作系统 服务器操作系统 嵌入式操作系统 移动设备操作系统

多线程案例

多线程案例 1. 单例模式2. 阻塞式队列3. 定时器4. 线程池 1. 单例模式 单例模式能保证某个类在程序中只存在唯一一份实例, 而不会创建出多个实例.分为懒汉式和饿汉式两种 饿汉式: 类加载的同时, 创建实例. class Singleton {private static Singleton instance new Singlet…

计算机网络——网络层

文章目录 **1 网络层的功能****1.1 异构网络互连****1.2 路由与转发****1.3 SDN的基本概念****1.4 拥塞控制** **2 路由算法****2.1 静态路由和动态路由****2.2 距离-向量路由算法&#xff08;动态&#xff09;****2.3 链路状态路由算法&#xff08;动态&#xff09;****2.4 层…

技术速览|Meta Llama 2 下一代开源大型语言模型

AI 使用大型语言模型&#xff08;LLM&#xff09;来理解和生成自然语言。LLM 可以从大量文本中学习并创建有关各种主题的文本&#xff0c;并可以完成比如编写代码、生成歌词、总结文章等任务。但有些 LLM 相关课程成本高昂且封闭&#xff0c;而现有的开放课程数量十分有限。这就…

Python爬虫+数据可视化:分析唯品会商品数据

目录 前言数据来源分析1. 明确需求2. 抓包分析&#xff1a;通过浏览器自带工具: 开发者工具 代码实现步骤: 发送请求 -> 获取数据 -> 解析数据 -> 保存数据发送请求解析数据保存数据 数据可视化先读取数据泳衣商品性别占比商品品牌分布占比各大品牌商品售价平均价格各…

LINUX中的myaql(一)安装

目录 前言 一、概述 二、数据库类型 三、数据库模型 四、MYSQL的安装 &#xff08;一&#xff09;yum安装MYSQL &#xff08;二&#xff09;rpm安装MYSQL 五、MYSQL本地登录 rpm安装MYSQL本地登录 六、重置密码 总结 前言 MySQL是一种常用的开源关系型数据库管理系统&#xff…

MATLAB与ROS联合仿真——ROS环境搭建及相关准备工作(下)

本篇文章主要介绍在安装完ROS后&#xff0c;在进行MATLAB与ROS联合仿真之前&#xff0c;需要进行的一些环境搭建以及准备工作&#xff0c;主要分为 创建ROS工作空间及功能包、必备功能包安装、安装Gazebo11、导入实验功能包至工作空间、安装Visual_Studio_Code(选做)、常用便捷…

Mysql错误日志、通用查询日志、二进制日志和慢日志的介绍和查看

一.日志 1.日志和备份的必要性 日志刷新 2.mysql的日志类型 &#xff08;1&#xff09;错误日志 查看当前错误日志和是否记录警告设置 &#xff08;2&#xff09;通用查询日志 查看通用查询日志的设置 &#xff08;3&#xff09;二进制日志 查看二进制文件的设置&…

-Ddfs.checksum.combine.mode=COMPOSITE_CRC参数解析

-Ddfs.checksum.combine.modeCOMPOSITE_CRC -Ddfs.checksum.combine.modeCOMPOSITE_CRC是一个Hadoop配置参数&#xff0c;用于指定HDFS&#xff08;Hadoop分布式文件系统&#xff09;在计算文件校验和时使用的模式。 具体来说&#xff0c;dfs.checksum.combine.mode 参数决定…