Vue入门浅析

写这篇博文的目的在于为初学vue的同学对vue有一些更进一步的了解
读这篇博文前,您应该至少安装了vue环境,能在本地运行一个简单的demo
本文将浅析vue项目工程的结构,以及用npm运行项目的过程中发生的一些事件
注明:该文本应在2022.5.14发表,由于博主有其他安排耽搁后面忘了,现在补上。

项目的文件结构

主文件结构

一般的vue工程项目核心部分都在src里
存放 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浅析

我们在本地运行vue项目,常见的指令就是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

之前我们提到过,这个文件作为项目的入口文件,首先加载这个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

这里的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 方法。

这里我们要牢记,在 Vue 2.0 版本中,所有 Vue 的组件的渲染最终都需要 render 方法,无论我们是用单文件 .vue 方式开发组件,还是写了 el 或者 template 属性,最终都会转换成 render 方法,那么这个过程是 Vue 的一个在线编译的过程。

最后,调用原先原型上的 $mount 方法挂载。

结合之前public/index.html中的例子

<div id="app">
</div>

实际上是编写了如下render函数

render: function (createElement) {
  return createElement('div', {
     attrs: {
        id: 'app'
      },
  })
}

vm._render 最终是通过执行 createElement 方法并返回的是 vnode,它是一个虚拟 Node

Virtual DOM介绍

浏览器真正的DOM通常是非常庞大的,因为浏览器产生DOM的标准本身就比较复杂,当我们频繁地进行DOM更新,就会产生一系列的性能问题
而 Virtual DOM 就是用一个原生的 JS 对象去描述一个 DOM 节点,所以它比创建一个 DOM 的代价要小很多。在 Vue.js 中,Virtual DOM 是用 VNode 这么一个 Class 去描述
在 Vue.js 中,VNode 的 create 是通过之前提到的 createElement 方法创建的。

生命周期

img

这也是一张比较经典的图了
在开发过程中,我们会频繁地跟vue的生命周期打交道

beforeCreate 和 created 函数都是在实例化 Vue 的阶段
在vue.js源码中 beforeCreate 和 created 的钩子调用是在 initState 的前后,initState 的作用是初始化 props、data、methods、watch、computed 等属性

在执行 vm._render() 函数渲染 VNode 之前,执行了 beforeMount 钩子函数,在执行完 vm._update() 把 VNode patch 到真实 DOM 后,执行 mouted 钩子。
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上的数据变化重新渲染

beforeDestroy 和 destroyed 钩子函数的执行时机在组件销毁的阶段
注意mounted和destroyed的执行过程都是先子后父
从下图可以看到初始化vue到最终渲染的整个过程
img

注册组件

在开发一个组件的过程中往往会用到其他的组件
组件注册的语法如下

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 提供的插件注册机制很简单,每个插件都需要实现一个静态的 install 方法,当我们执行 Vue.use 注册插件的时候,就会执行这个 install 方法,并且在这个 install 方法的第一个参数我们可以拿到 Vue 对象,这样的好处就是作为插件的编写方不需要再额外去import Vue 了。

路由

路由的主要作用是根据不同的路径映射到不同的视图,一般我们用官方插件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

在new一个vueRouter后会返回它的实例,在beforecreate()中有这样一段代码

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并进行路径转换

在router-view中

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]

这个循环就是从当前的的父节点向上找,一直找到根节点(vue实例),遍历完成后,就根据当前遍历的深度和路径找到对应的组件并进行渲染
在router-view的最后有根据 component 渲染出对应的组件 vonde:

return h(component, data, children)
  • hash模式:单页应用标配,hash发生变化的url都会被浏览器记录下来
  • history模式:可以进行切换和修改(历史状态),使得路由配置更自由

其他

vuex

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。
它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
该状态管理模式包含以下几个部分:

  • state:驱动数据的应用源
  • view:以声明方法将state映射到视图
  • actions:响应在view上用户的输入导致的状态变化
    以下是一个简单的数据流模式
    img

需要注意以下两点

  • Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
  • 你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。
    img

参考书籍《vue.js技术揭秘》

posted @ 2023-05-15 17:14  Sun-Wind  阅读(156)  评论(0编辑  收藏  举报