Vue入门浅析

科技资讯 投稿 7300 0 评论

Vue入门浅析

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]

这个循环就是从当前的的父节点向上找,一直找到根节点(vue实例),遍历完成后,就根据当前遍历的深度和路径找到对应的组件并进行渲染
在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技术揭秘》

编程笔记 » Vue入门浅析

赞同 (38) or 分享 (0)
游客 发表我的评论   换个身份
取消评论

表情
(0)个小伙伴在吐槽