title: vue入门浅析
author: Sun-Wind
date: May 14,2022
写这篇博文的目的在于为初学vue的同学对vue有一些更进一步的了解
读这篇博文前,您应该至少安装了vue环境,能在本地运行一个简单的demo
本文将浅析vue项目工程的结构,以及用npm运行项目的过程中发生的一些事件
注明:该文本应在2022.5.14发表,由于博主有其他安排耽搁后面忘了,现在补上。
项目的文件结构
主文件结构
存放 vue 项目的源代码。其文件夹下的各个文件(文件夹)分别为:
- assets:资源文件,比如存放 css,图片等资源
- component:组件文件夹,用来存放 vue 的公共组件(注册于全局,在整个项目中通过关键词便可直接输出)。
- router:用来存放 index.js,这个 js 用来配置路由
- tool:用来存放工具类 js,将 js 代码封装好放入这个文件夹可以全局调用(比如常见的 api.js,http.js 是对 http 方法和 api 方法的封装)
- views:用来放主体页面,虽然和组件文件夹都是 vue 文件,但 views 下的 vue 文件是可以用来充当路由 view 的。
- main.js:是项目的入口文件,作用是初始化 vue 实例,并引入所需要的插件。
- app.vue:是项目的主组件,所有页面都是在该组件下进行切换的.
其他文件结构
- public:用于存放静态文件
- public/index.html:是一个模板文件,作用是生成项目的入口文件,webpack打包的js,css也会自动注入到该页面中。我们浏览器访问项目的时候就会默认打开生成好的index.html
- package.json: 模块基本信息项目开发所需要模块,版本,项目名称
- vue.config.js:包含vue项目的其他配置,包括端口等信息
- node_modules:项目的依赖模块
- dist:打包文件
npm run serve/dev浅析
我们通常会在package.json中配置 script 字段作为 NPM 的执行脚本。
以个人开发项目为例,Vue.js 源码构建的脚本如下:
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"stylelint": "stylelint src/css/*.* --fix",
"htmlhint": "htmlhint **.html",
"eslint": "eslint src/**/*.js src/**/*.vue",
"eslint-fix-js": "eslint src/**/*.js --fix",
"eslint-fix-vue": "eslint src/**/*.vue --fix"
},
所以当我们在终端运行npm run serve时,实际上运行的是vue-cli-service serve
通过这个脚本去构建整个vue项目
构建的过程中发生了什么
public/index.html
下面这些代码是个例子
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<link rel="icon" href="./icon.png">
<title></title>
</head>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
我们注意到一个特别的div块,它的id为app
src/main.js
import Vue from 'vue';
new Vue({
el: '#app',
render: h => h(app
};
我们都知道,new 关键字在 Javascript 语言中代表实例化是一个对象,而 Vue 实际上是一个类,类在 Javascript 中是用 Function 来实现的,在vue.js源码中是这样定义的
function Vue (options {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue
{
warn('Vue is a constructor and should be called with the `new` keyword'
}
this._init(options
}
可以看到vue只能通过关键字初始化,this._init函数这里就不再具体介绍
Vue 初始化主要就干了几件事情,合并配置,初始化生命周期,初始化事件中心,初始化渲染,初始化 data、props、computed、watcher 等等。
在初始化的最后,检测到如果有 el 属性,则调用 vm.$mount 方法挂载 vm,挂载的目标就是把模板渲染成最终的DOM
在compiler版本的$mount实现中,它对 el 做了限制,Vue 不能挂载在 body、html 这样的根节点上。
接下来的是很关键的逻辑 —— 如果没有定义 render 方法,则会把 el 或者 template 字符串转换成 render 方法。
render 方法,无论我们是用单文件 .vue 方式开发组件,还是写了 el 或者 template 属性,最终都会转换成 render 方法,那么这个过程是 Vue 的一个在线编译的过程。
结合之前public/index.html中的例子
<div id="app">
</div>
实际上是编写了如下render函数
render: function (createElement {
return createElement('div', {
attrs: {
id: 'app'
},
}
}
vm._render 最终是通过执行 createElement 方法并返回的是 vnode,它是一个虚拟 Node
Virtual DOM介绍
而 Virtual DOM 就是用一个原生的 JS 对象去描述一个 DOM 节点,所以它比创建一个 DOM 的代价要小很多。在 Vue.js 中,Virtual DOM 是用 VNode 这么一个 Class 去描述
在 Vue.js 中,VNode 的 create 是通过之前提到的 createElement 方法创建的。
生命周期
在开发过程中,我们会频繁地跟vue的生命周期打交道
在vue.js源码中 beforeCreate 和 created 的钩子调用是在 initState 的前后,initState 的作用是初始化 props、data、methods、watch、computed 等属性
beforeUpdate 和 updated 的钩子函数执行时机都应该是在数据更新的时候,比如双向绑定等等
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
: Component {
// ...
// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook, which relies on vm._watcher being already defined
new Watcher(vm, updateComponent, noop, {
before ( {
if (vm._isMounted {
callHook(vm, 'beforeUpdate'
}
}
}, true /* isRenderWatcher */
// ...
}
可以看到这里有一个vm._isMounted的判断,也就是说组件在mounted后才会去执行这个钩子函数
同时这里实例化了一个watcher去监听vm上的数据变化重新渲染
注意mounted和destroyed的执行过程都是先子后父
从下图可以看到初始化vue到最终渲染的整个过程
注册组件
组件注册的语法如下
Vue.component('my-component', {
// 选项
}
或
import HelloWorld from './components/HelloWorld'
export default {
components: {
HelloWorld
}
}
注册组件实际上是一个合并的过程,合并option再创建vnode。
下载插件
开发的过程中很可能需要一些其他的插件如Element等使用
一般来说通过Vue.use(来下载插件,并且会阻止多次注册相同的插件
export function initUse (Vue: GlobalAPI {
Vue.use = function (plugin: Function | Object {
const installedPlugins = (this._installedPlugins || (this._installedPlugins = []
if (installedPlugins.indexOf(plugin > -1 {
return this
}
const args = toArray(arguments, 1
args.unshift(this
if (typeof plugin.install === 'function' {
plugin.install.apply(plugin, args
} else if (typeof plugin === 'function' {
plugin.apply(null, args
}
installedPlugins.push(plugin
return this
}
}
这是use方法的源码,可以看到其参数只能是object或者function,然后判断其是否被注册过
然后再调用该插件的install方法
路由
路由的主要作用是根据不同的路径映射到不同的视图,一般我们用官方插件vue-router来解决路由的问题
Object.defineProperty(Vue.prototype, '$router', {
get ( { return this._routerRoot._router }
}
Object.defineProperty(Vue.prototype, '$route', {
get ( { return this._routerRoot._route }
}
在vue-router的类定义中有在vue原型上定义的 $router 和 $route 两个属性的get方法,这也是为什么可以在组件实例上访问 this.$router和this.$route
beforeCreate( {
if (isDef(this.$options.router {
// ...
this._router = this.$options.router
this._router.init(this
// ...
}
}
所以在执行该钩子函数时,如果有传入router实例,则会执行router.init方法
匹配是利用matcher匹配,并且会生成用户的路由表
(具体细节暂时不表)
当我们点击router-link的时候,会通过一系列函数找到完整的url,执行pushState方法
export function pushState (url?: string, replace?: boolean {
saveScrollPosition(
const history = window.history
try {
if (replace {
history.replaceState({ key: _key }, '', url
} else {
_key = genKey(
history.pushState({ key: _key }, '', url
}
} catch (e {
window.location[replace ? 'replace' : 'assign'](url
}
}
该方法会更新浏览器的url地址,并且把当前url压入历史栈中
有一个专门的监听器会监听历史栈的变化情况
setupListeners ( {
const router = this.router
const expectScroll = router.options.scrollBehavior
const supportsScroll = supportsPushState && expectScroll
if (supportsScroll {
setupScroll(
}
window.addEventListener(supportsPushState ? 'popstate' : 'hashchange', ( => {
const current = this.current
if (!ensureSlash( {
return
}
this.transitionTo(getHash(, route => {
if (supportsScroll {
handleScroll(this.router, route, current, true
}
if (!supportsPushState {
replaceHash(route.fullPath
}
}
}
}
当点击浏览器的返回按钮时,会触发popstate事件,通过同样的方法拿到当前要跳转的url并进行路径转换
data.routerView = true
// ...
while (parent && parent._routerRoot !== parent {
if (parent.$vnode && parent.$vnode.data.routerView {
depth++
}
if (parent._inactive {
inactive = true
}
parent = parent.$parent
}
const matched = route.matched[depth]
// ...
const component = cache[name] = matched.components[name]
这个循环就是从当前的
在router-view的最后有根据 component 渲染出对应的组件 vonde:
return h(component, data, children
- hash模式:单页应用标配,hash发生变化的url都会被浏览器记录下来
- history模式:可以进行切换和修改(历史状态),使得路由配置更自由
其他
vuex
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。
它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
该状态管理模式包含以下几个部分:
- state:驱动数据的应用源
- view:以声明方法将state映射到视图
- actions:响应在view上用户的输入导致的状态变化
以下是一个简单的数据流模式
- Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
- 你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit mutation。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。
参考书籍《vue.js技术揭秘》