PS:对前端技术感兴趣的朋友们,可以关注下我的《致力于前端的技术博客》哦!如果对你有帮助,欢迎赠个⭐️,会常更新内容,敬请期待!❤️❤️
基础扫盲
vue-cli脚手架
安装脚手架
npm install -g @vue/cli
yarn global add @vue/cli
创建项目
vue create [name]
使用图形化界面
vue ui
调整 webpack 配置最简单的方式就是在 vue.config.js 中的 configureWebpack 选项提供一个对象.
基础指令
- {{}}:文本插值,用双大括号表示
- v-text:操作纯文本,会替代显示对应对象上的值
- v-html:输出html,会将其当html标签解析后输出
- v-on:监听事件指令,简写为
@click - v-bind:属性绑定指令 ,简写为
:xx - v-for:遍历指令
- v-model:表单输入绑定指令(可使用修饰符为 .lazy .trim .number)
- v-if 、v-show:条件渲染
v-if和v-show的区别
- v-if是真实的条件渲染,当进行条件切换时,它会销毁和重建条件块的内容,并且它支持
<template>语法; - v-show的条件切换时基于css的display属性,所以不会销毁和重建条件块的内容;
- 当你频繁需要切换条件时,推荐使用v-show;否则使用v-if;
样式设置
// 1.对象语法
v-bind:class="{ active: isActive, 'text-danger': hasError }"
data: {
isActive: true,
hasError: false
}
// 2.数组语法
<div v-bind:class="[activeClass, errorClass]"></div>
<div v-bind:class="[{ active: isActive }, errorClass]"></div>
<div v-bind:class="[isActive ? activeClass : '', errorClass]"></div>
// 3.Style绑定
v-bind:style="{ color: activeColor, fontSize: fontSize + 'px' }"
事件修饰符
vue还为v-on提供了事件修饰符
- .stop 阻止事件继续传播
- .prevent 提交的事件不再阻止页面
- .capture 添加事件监听器时使用事件捕获模式
- .self 只当在event.target是当前元素自身时触发处理函数
- .once 点击事件将只触发一次
- .passive 滚动事件的默认行为将会立即触发
<div v-on:click.prevent="greet">1</div><!-- 等价于event.preventDefault() -->
<div v-on:click.stop="greet">2</div><!-- 等价于event.stopPropagation() -->
<div v-on:click.capture="greet">3</div><!-- 等价于事件回调函数采用捕获阶段监听事件 -->
<div v-on:click.self="greet">4</div><!-- 等价于event.target -->
computed 和 watch
computed: 是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值;
watch: 更多的是「观察」的作用,类似于某些数据的监听回调 ,每当监听的数据变化时都会执行回调进行后续操作;
运用场景:
- 当我们需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时,都要重新计算;
- 当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许我们执行异步操作 ( 访问一个 API ),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。
nextTick机制
在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。 需要注意的是,在 created 和 mounted 阶段,如果需要操作渲染后的试图,也要使用 nextTick 方法。
// 这样可以,nextTick里面的代码会在DOM更新后执行
Vue.nextTick(function(){
console.log(vm.$el.textContent) //可以得到'changed'
})
// 注意 mounted 不会承诺所有的子组件也都一起被挂载。如果你希望等到整个视图都渲染完毕,可以用 vm.$nextTick 替换掉 mounted
mounted: function () {
this.$nextTick(function () {
// Code that will run only after the
// entire view has been rendered
})
}
vue-router
推荐阅读vue-router官方文档
- 全局前置守卫:当一个导航触发时,全局前置守卫按照创建顺序调用。守卫是异步解析执行,此时导航在所有守卫 resolve 完之前一直处于 等待中。
- 全局解析守卫:这和 router.beforeEach 类似,区别是在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后,解析守卫就被调用。
- 全局后置钩子:transition 可以定义路由过渡动画
vuex
npm install vuex –save
- state:定义全局状态属性 this.$store.state.showFooter
- getters:和vue计算属性computed一样,来实时监听state值的变化(最新状态),并把它也仍进Vuex.Store里面
- mutations:具体的用法就是给里面的方法传入参数state或额外的参数,然后利用vue的双向数据驱动进行值的改变,同样的定义好之后也把这个mutations扔进Vuex.Store里面 this.$store.commit('show')
- actions:通常用于异步操作或是mutations的封装,可以包含任意异步操作,这里面的方法是用来异步触发mutations里面的方法,actions里面自定义的函数接收一个context参数和要变化的形参,context与store实例具有相同的方法和属性,所以它可以执行context.commit(' '),然后也不要忘了把它也扔进Vuex.Store里面 this.$store.dispatch('showFooter')
举个例子:
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
const state={ //要设置的全局访问的state对象
showFooter: true,
changableNum:0
//要设置的初始属性值
};
const getters = { //实时监听state值的变化(最新状态)
isShow(state) { //承载变化的showFooter的值
return state.showFooter
},
getChangedNum(){ //承载变化的changebleNum的值
return state.changableNum
}
};
const mutations = {
show(state) { //自定义改变state初始值的方法,这里面的参数除了state之外还可以再传额外的参数(变量或对象);
state.showFooter = true;
},
hide(state) { //同上
state.showFooter = false;
},
newNum(state,sum){ //同上,这里面的参数除了state之外还传了需要增加的值sum
state.changableNum+=sum;
}
};
const actions = {
hideFooter(context) { //自定义触发mutations里函数的方法,context与store 实例具有相同方法和属性
context.commit('hide');
},
showFooter(context) { //同上注释
context.commit('show');
},
getNewNum(context,num){ //同上注释,num为要变化的形参
context.commit('newNum',num)
}
};
const store = new Vuex.Store({
state,
getters,
mutations,
actions
});
export default store;
modules 模块化 以及 组件中引入 mapGetters、mapActions 和 mapStates的使用
import {mapState,mapGetters,mapActions} from 'vuex';
computed:{
...mapState({ //这里的...是超引用,ES6的语法,意思是state里有多少属性值我可以在这里放多少属性值
isShow:state=>state.footerStatus.showFooter //注意这些与上面的区别就是state.footerStatus,
}),
...mapActions('collection',[ //collection是指modules文件夹下的collection.js
'invokePushItems' //collection.js文件中的actions里的方法,在上面的@click中执行并传入实参
]),
...mapGetters('collection',{ //用mapGetters来获取collection.js里面的getters
arrList:'renderCollects'
})
}
vue生命周期
vue生命周期:Vue 实例从创建到销毁的过程,就是生命周期。从开始创建、初始化数据、编译模板、挂载Dom→渲染、更新→渲染、销毁等一系列过程,称之为 Vue 的生命周期。它的生命周期中有多个事件钩子,让我们在控制整个Vue实例的过程时更容易形成好的逻辑。
总共分为 8 个阶段beforeCreate(创建前) created(创建后) beforeMount(载入前) mounted(载入后) beforeUpdate(更新前), updated(更新后) beforeDestroy(销毁前) destroyed(销毁后)。
- 创建前/后:在 beforeCreate 阶段,vue 实例的挂载元素 el 还没有。
- 载入前/后:在 beforeMount 阶段,vue 实例的$el 和 data 都初始化了,但还是挂载之前为虚拟的 dom 节点,data.message 还未替换。在 mounted 阶段,vue 实例挂载完成,data.message 成功渲染。
- 更新前/后:当 data 变化时,会触发 beforeUpdate 和 updated 方法。
- 销毁前/后:在执行 destroy 方法后,对 data 的改变不会再触发周期函数,说明此时 vue 实例已经解除了事件监听以及和 dom 的绑定,但是 dom 结构依然存在。
- 另外还有 keep-alive 独有的生命周期,分别为 activated 和 deactivated 。用 keep-alive 包裹的组件在切换时不会进行销毁,而是缓存到内存中并执行 deactivated 钩子函数,命中缓存渲染后会执行 activated钩子函数。
应用场景有哪些?
- beforeCreate 可以在此时加一些loading效果,在created时进行移除
- created 需要异步请求数据的方法可以在此时执行,完成数据的初始化
- mounted 当需要操作dom的时候执行,可以配合$.nextTick 使用进行单一事件对数据的更新后更新dom
- updated 当数据更新需要做统一业务处理的时候使用
父子组件通信中常用方法
- 在父组件中,使用component引用子组件,然后使用props属性:
<child-component :property="data"></child-component> - 使用Vuex状态管理进行父子组件通信,定义store.js,并定义state,在state中定义传递的属性比如叫childProperty。然后,在子组件中,使用
store.state.childProperty进行使用。 - 使用router中的Params进行传参(即路径传参),
设置路由
/child/:id,当访问到/child/1元素的时候,在子组件中,使用this.$route.params.id的方式进行使用
不推荐使用LocalStorage缓存传参,虽然使用缓存也可以获取到数据。但是,这不是推荐的做法,也不方便管理,容易丢失数据或者是数据紊乱(因为没有及时清理与回收)。
vue调试方法
vscode安装Debugger for Chrome。在vue.config.js中设置source-map:
module.exports = {
configureWebpack: {
devtool: 'source-map'
}
}
点击“调试”>“添加配置”,生成launch.json,注意url的端口要与项目运行的端口一致,点击“开始调试”即可。
{
"version": "0.2.0",
"configurations": [{
"type": "chrome",
"request": "launch",
"name": "vuejs: chrome",
"url": "http://localhost:8081",
"webRoot": "${workspaceFolder}/src",
"breakOnLoad": true,
"sourceMapPathOverrides": {
"webpack:///./src/*": "${webRoot}/*"
}
}]
}
直接在chrome中下载此插件即可。
对于Vue-cli创建的工程化项目,哪些方式可以调试应用?
- 使用vue官方推荐的devTools进行调试(官方推荐的dev-Tools是最方便去查看vue的状态管理、vue变量的工具)
- 在webpack配置代码中打开source-map,插入debugger,使用chrome的调试窗口(但是要注意这种方式,不方便查看vuex的状态变化,vuex的commit事件无法监听)
- 使用alert, console.log,JSON.stringfy打印相关的日志(这个是最大众,最简单,也是最普通的一种方式了)
hash模式 和 history模式
hash模式: 在浏览器中符号“#”,#以及#后面的字符称之为hash,用window.location.hash读取
特点: hash虽然在URL中,但不被包括在HTTP请求中;用来指导浏览器动作,对服务端安全无用,hash不会重加载页面。 hash 模式下,仅 hash 符号之前的内容会被包含在请求中,如 www.xxx.com,因此对于后端来说,即使没有做到对路由的全覆盖,也不会返回 404 错误。
history模式:history采用HTML5的新特性;且提供了两个新方法:pushState(),replaceState()可以对浏览器历史记录栈进行修改,以及popState事件的监听到状态变更。
history 模式下,前端的 URL 必须和实际向后端发起请求的 URL 一致,如 http://www.xxx.com/items/id。 后端如果缺少对 /items/id 的路由处理,将返回 404 错误。
特点:Vue-Router 官网里如此描述“不过这种模式要玩好,还需要后台配置支持……所以呢,你要在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面。”
keep-alive
keep-alive是 Vue 内置的一个组件,可以使被包含的组件保留状态,或避免重新渲染。 在vue 2.1.0 版本之后,keep-alive新加入了两个属性: include(包含的组件缓存) 与 exclude(排除的组件不缓存,优先级大于include)
vue项目性能优化
(1)代码层面的优化
- v-if 和 v-show 区分使用场景
- computed 和 watch 区分使用场景
- v-for 遍历必须为 item 添加 key,且避免同时使用 v-if
- 长列表性能优化
- 事件的销毁
- 图片资源懒加载
- 路由懒加载
- 第三方插件的按需引入
- 优化无限列表性能
- 服务端渲染 SSR or 预渲染
(2)Webpack 层面的优化
- Webpack 对图片进行压缩
- 减少 ES6 转为 ES5 的冗余代码
- 提取公共代码
- 模板预编译
- 提取组件的 CSS
- 优化 SourceMap
- 构建结果输出分析
- Vue 项目的编译优化
(3)基础的 Web 技术的优化
- 开启 gzip 压缩
- 浏览器缓存
- CDN 的使用
- 使用 Chrome Performance 查找性能瓶颈
vue-cli与elementui集成
// 安装element
vue add element
// main.js引入element
import Vue from 'vue';
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import App from './App.vue';
Vue.use(ElementUI);
new Vue({
el: '#app',
render: h => h(App)
});
// 实现按需引入
npm install babel-plugin-component -D
// .babelrc
{
"presets": [["es2015", { "modules": false }]],
"plugins": [
[
"component",
{
"libraryName": "element-ui",
"styleLibraryName": "theme-chalk"
}
]
]
}
// main.js
import Vue from 'vue';
import { Button, Select } from 'element-ui';
import App from './App.vue';
Vue.component(Button.name, Button);
Vue.component(Select.name, Select);
/* 或写为
* Vue.use(Button)
* Vue.use(Select)
*/
new Vue({
el: '#app',
render: h => h(App)
});
SSR
nuxt.js
客户端渲染和服务器端渲染的最重要的区别就是究竟是谁来完成html文件的完整拼接,如果是在服务器端完成的,然后返回给客户端,就是服务器端渲染,而如果是前端做了更多的工作完成了html的拼接,则就是客户端渲染 。 创建nuxt项目:
yarn create nuxt-app <项目名>
yarn install
cnpm run dev
// 如果要使用 sass 就必须要安装 node-sass和sass-loader
npm install --save-dev node-sass sass-loader
前端vue等框架打包的项目一般为SPA应用,而单页面是不利于SEO的,现在的解决方案有两种:
- SSR服务器渲染
- 预渲染模式(这比服务端渲染要简单很多,而且可以配合 vue-meta-info 来生成title和meta标签,基本可以满足SEO的需求 )
TIPS : 使用预渲染vue-router必须使用history模式。当然,有时候我们也可能会遇到让人头疼的SEO问题,那么使用此插件配合 prerender-spa-plugin 也是再合适不过了。
警惕内存泄漏
- beforeDestroy()、destroyed钩子清除出定时器、相关变量置为null
- 使用内建的 keep-alive组件,状态就会保留,因此就留在了内存里
要确保测试应用的内存泄漏问题并在适当的时机做必要的组件清理。
页面过渡动画
Vue 在插入、更新或者移除 DOM 时,提供多种不同方式的应用过渡效果。 包括以下工具:
- 在 CSS 过渡和动画中自动应用 class
- 可以配合使用第三方 CSS 动画库,如 Animate.css
- 在过渡钩子函数中使用 JavaScript 直接操作 DOM
- 可以配合使用第三方 JavaScript 动画库,如 Velocity.js
Vue 提供了 transition的封装组件,在下列情形中,可以给任何元素和组件添加进入/离开过渡
- 条件渲染 (使用 v-if)
- 条件展示 (使用 v-show)
- 动态组件
- 组件根节点
在进入/离开的过渡中,会有 6 个 class 切换。
对于 Vue 的过渡系统和其他第三方 CSS 动画库,如 Animate.css 结合使用十分有用。
可复用的过渡
过渡可以通过 Vue的组件系统实现复用。要创建一个可复用过渡组件,你需要做的就是将 或者 作为根组件,然后将任何子组件放置在其中就可以了。
Vue.component('my-special-transition', {
template: '\
<transition\
name="very-special-transition"\
mode="out-in"\
v-on:before-enter="beforeEnter"\
v-on:after-enter="afterEnter"\
>\
<slot></slot>\
</transition>\
',
methods: {
beforeEnter: function (el) {
// ...
},
afterEnter: function (el) {
// ...
}
}
})
vue3.0的改变
Vue 3.0 的目标是让 Vue 核心变得更小、更快、更强大,Vue 3.0 增加以下这些新特性:
(1)监测机制的改变
3.0 基于代理 Proxy 的 observer 实现,提供全语言覆盖的反应性跟踪。这消除了 Vue 2 当中基于 Object.defineProperty 的实现所存在的很多限制:
- 只能监测属性,不能监测对象
- 检测属性的添加和删除;
- 检测数组索引和长度的变更;
- 支持 Map、Set、WeakMap 和 WeakSet。 新的 observer 还提供了以下特性:
- 用于创建 observable 的公开 API。这为中小规模场景提供了简单轻量级的跨组件状态管理解决方案。
- 默认采用惰性观察。在 2.x 中,不管反应式数据有多大,都会在启动时被观察到。如果你的数据集很大,这可能会在应用启动时带来明显的开销。在 3.x 中,只观察用于渲染应用程序最初可见部分的数据。
- 更精确的变更通知。在 2.x 中,通过 Vue.set 强制添加新属性将导致依赖于该对象的 watcher 收到变更通知。在 3.x 中,只有依赖于特定属性的 watcher 才会收到通知。
- 不可变的 observable:我们可以创建值的“不可变”版本(即使是嵌套属性),除非系统在内部暂时将其“解禁”。这个机制可用于冻结 prop 传递或 Vuex 状态树以外的变化。
- 更好的调试功能:我们可以使用新的 renderTracked 和 renderTriggered 钩子精确地跟踪组件在什么时候以及为什么重新渲染。
(2)模板
模板方面没有大的变更,只改了作用域插槽,2.x 的机制导致作用域插槽变了,父组件会重新渲染,而 3.0 把作用域插槽改成了函数的方式,这样只会影响子组件的重新渲染,提升了渲染的性能。 同时,对于 render 函数的方面,vue3.0 也会进行一系列更改来方便习惯直接使用 api 来生成 vdom 。
(3)对象式的组件声明方式
vue2.x 中的组件是通过声明的方式传入一系列 option,和 TypeScript 的结合需要通过一些装饰器的方式来做,虽然能实现功能,但是比较麻烦。3.0 修改了组件的声明方式,改成了类式的写法,这样使得和 TypeScript 的结合变得很容易。
此外,vue 的源码也改用了 TypeScript 来写。其实当代码的功能复杂之后,必须有一个静态类型系统来做一些辅助管理。现在 vue3.0 也全面改用 TypeScript 来重写了,更是使得对外暴露的 api 更容易结合 TypeScript。静态类型系统对于复杂代码的维护确实很有必要。
(4)其它方面的更改
vue3.0 的改变是全面的,上面只涉及到主要的 3 个方面,还有一些其他的更改:
- 支持自定义渲染器,从而使得 weex 可以通过自定义渲染器的方式来扩展,而不是直接 fork 源码来改的方式。
- 支持 Fragment(多个根节点)和 Protal(在 dom 其他部分渲染组建内容)组件,针对一些特殊的场景做了处理。
- 基于 treeshaking 优化,提供了更多的内置功能。
Vue原理与进阶
MVVM手写实现
vue框架是典型的MVVM(Model-View-ViewModel)模式的前端框架,它最大的特点就是,View和ViewModel之间做了双向数据绑定,当Model发生变化的时候,绑定Model数据的View会随之发生变化,当View发生变化时,对应的Model也会随之变化。
为了更清楚地了解vue的源码设计,我们先来实现一个小巧简单的mvvm框架吧~ 初步的文件结构设计如下:
index.html
页面框架,创建MVVM实例,挂载页面元素和数据。分别引入watcher.js、observer.js、compile.js、mvvm.js文件,后面会介绍具体的作用。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>My MVVM</title>
</head>
<body>
<div id="app">
<!-- 双向数据绑定 -->
<input type="text" v-model='msg'> {{msg}}
</div>
</body>
<!-- <script src="node_modules/vue/dist/vue.min.js"></script> -->
<script src="./watcher.js"></script>
<script src="./observer.js"></script>
<script src="./compile.js"></script>
<script src="./mvvm.js"></script>
<script>
// mvvm如何实现?
// vue中实现双向绑定 1.模板编译 2.数据劫持 3.Watcher
let vm = new MVVM({
el: '#app', //el:document.getElementById('app')
data: {
msg: 'hello'
}
})
</script>
</html>
mvvm.js
mvvm.js定义了MVVM类,首先把可用的属性全部挂载到实例上。如果有要编译的模板就开始编译,涉及到数据劫持、数据代理、模板编译三个阶段。其中,数据劫持是把对象的所有属性改为get、set;代理数据阶段让this.$data下的数据都代理到this(MVVM)中,能让用户方便地从this.xx进行取值,而不是需要从this.$data.xx进行取值;模板编译阶段是使用数据和元素进行编译,返回含有完整数据内容的页面。
class MVVM {
constructor(options) {
// 先把可用的东西挂载到实例上
this.$el = options.el
this.$data = options.data
// 如果有要编译的模板就开始编译
if (this.$el) {
new Observer(this.$data) // 数据劫持,把对象的所有属性改为get、set
this.proxyData(this.$data)
new Compile(this.$el, this) // 用数据和元素进行编译
}
}
// 代理数据,因为用户可能要通过this.msg取值,而不是this.$data.msg取值
proxyData(data) {
Object.keys(data).forEach(key => {
Object.defineProperty(this, key, {
get() {
return data[key]
},
set(newVal) {
data[key] = newVal
}
})
})
}
}
compile.js
compile.js是将数据和页面元素组合起来,返回含有对应数据内容的完整页面,用于浏览器渲染。如果存在模板,则需要进行以下几步:
- 把真实的dom移入到内存,放到fragment(
node2Fragment(el)函数) - 进行编译(
compile(fragment)函数),提取元素节点和文本节点,针对元素节点和文本节点实行不同的编译方式 a)如果是元素节点,则需要先进行元素节点编译(compileElement(node)函数),再进行递归 b)如果是文本节点,则直接编译文本节点(compileText(node)函数),提取{{}}中的内容进行数据填充 - 把编译好的element放回页面
class Compile {
constructor(el, vm) {
// 判断el是否是元素节点,如果是html的元素节点则直接返回,否则使用document.querySelector找到el节点并返回
this.el = this.isElementNode(el) ? el : document.querySelector(el)
this.vm = vm
if (this.el) {
// 1. 先把真实的dom移入到内存,放到fragment
let fragment = this.node2Fragment(this.el)
// 2. 编译——提取想要的元素节点和文本节点 v-model {{}}
this.compile(fragment)
// 3. 把编译好的element放回页面
this.el.appendChild(fragment)
}
}
// 辅助方法
isElementNode(node) {
return node.nodeType === 1
}
isDirective(name) {
return name.includes('v-')
}
// 核心方法
// 将el元素内容全部放入内存
node2Fragment(el) {
let fragment = document.createDocumentFragment() //文档碎片
let firstChild
while (firstChild = el.firstChild) {
fragment.appendChild(firstChild)
}
return fragment
}
// 编译
compile(fragment) {
// childNodes拿不到嵌套子节点,需要使用递归
let childNodes = fragment.childNodes
Array.from(childNodes).forEach(node => {
// 元素节点
if (this.isElementNode(node)) {
this.compileElement(node)
this.compile(node) // 需要深入检查, 使用递归
} else {
// 文本节点
this.compileText(node)
}
})
}
// 编译元素 v-model、v-text等
compileElement(node) {
let attrs = node.attributes
Array.from(attrs).forEach(attr => {
// 判断属性名字是否包含v-
let attrName = attr.name
if (this.isDirective(attrName)) {
let expr = attr.value //expr是指令的值
// node this.vm.$data expr
//取到v-后面的名称,如v-model的model,v-text的text等等
// let type = attrName.slice(2)
let [, type] = attrName.split('-')
Compileutil[type](node, this.vm, expr)
}
})
}
// 编译文本,{{}}
compileText(node) {
let expr = node.textContent
let reg = /\{\{([^}]+)\}\}/g //匹配{{}}
if (reg.test(expr)) {
// node this.vm.$data expr
const type = 'text'
Compileutil[type](node, this.vm, expr)
}
}
}
Compileutil = {
// 获取实例上对应的数据,如msg.a.b=>'hello'
// msg.a.b=>this.$data.msg=>this.$data.msg.a=>this.$data.msg.a.b
getVal(vm, expr) {
expr = expr.split('.')
return expr.reduce((prev, next) => {
return prev[next]
}, vm.$data)
},
// 获取编译文本后的结果,如{{msg}}=>'hello'
getTextVal(vm, expr) {
return expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
// arguments[1]是正则匹配括号内容,如{{msg}}的msg
return this.getVal(vm, arguments[1])
})
},
// 赋值
// 例如给msg.a.b赋新值,则取到最后再赋value值
setVal(vm, expr, value) {
expr = expr.split('.')
return expr.reduce((prev, next, curIndex) => {
if (curIndex === expr.length - 1) {
return prev[next] = value
}
}, vm.$data)
},
// 文本处理
text(node, vm, expr) {
let updateFn = this.update['textUpdater']
// 拿到{{a}}{{b}}的a、b
expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
new Watcher(vm, arguments[1], newVal => {
// 如果数据变化了, 文本节点需要重新获取依赖的数据来更新文本节点
updateFn && updateFn(node, this.getTextVal(vm, expr))
})
})
let value = this.getTextVal(vm, expr)
updateFn && updateFn(node, value)
},
// 输入框处理
model(node, vm, expr) {
let updateFn = this.update['modelUpdater']
// 这里应该加一个监控,数据变化时,应该调用watcher的callback,将新值传递过来
new Watcher(vm, expr, newVal => {
updateFn && updateFn(node, this.getVal(vm, expr))
})
updateFn && updateFn(node, this.getVal(vm, expr))
node.addEventListener('input', e => {
let newVal = e.target.value
this.setVal(vm, expr, newVal)
})
},
update: {
// 文本更新
textUpdater(node, value) {
node.textContent = value
},
// 输入框更新
modelUpdater(node, value) {
node.value = value
},
}
}
observer.js
observer.js是将页面中绑定的数据全部变为响应式,即将data数据原有的属性改为get和set的形式,使用defineReactive函数进行数据劫持(这里要注意,如果劫持的是对象,还要对对象内的属性继续劫持)。在defineReactive函数中,我们针对每个数据都新建了Dep的实例,Dep是典型的用来发布订阅的类(见下文的watcher.js),可以用来添加订阅者信息和触发数据更新。在这个函数中,使用Object.defineProperty进行了数据劫持,在数据发生变化时(对应set),通知所有该数据的订阅者数据变化了,会让对应的订阅者进行更新操作。
class Observer {
constructor(data) {
this.observe(data)
}
// 将data数据原有的属性改为get和set的形式
observe(data) {
if (!data || typeof data !== 'object') return
Object.keys(data).forEach(key => {
// 开始劫持
this.defineReactive(data, key, data[key])
// 如果劫持的是对象,还要对对象内的属性继续劫持
this.observe(data[key])
})
}
// 定义响应式
defineReactive(data, key, value) {
let _this = this
let dep = new Dep() //每个变化的数据都会对应一个数组,这个数组是存放所有更新的操作
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
Dep.target && dep.addSub(Dep.target)
return value
},
set(newValue) {
if (newValue !== value) {
// 设置新值时,如果是对象仍然需要劫持
_this.observe(newValue)
value = newValue
dep.notify() //通知所有订阅者数据变化了
}
}
})
}
}
watcher.js
watcher.js定义了观察者类,用来给需要变化的dom元素增加观察者。使用新值和旧值进行比对,如果发生变化,执行对应的方法(如更新页面)
class Watcher {
constructor(vm, expr, cb) {
this.vm = vm
this.expr = expr
this.cb = cb
this.value = this.get()
}
// 获取实例上对应的数据,如msg.a.b=>'hello'
getVal(vm, expr) {
expr = expr.split('.')
return expr.reduce((prev, next) => {
return prev[next]
}, vm.$data)
}
get() {
Dep.target = this
let value = this.getVal(this.vm, this.expr)
Dep.target = null
return value
}
// 对外暴露的方法
update() {
let newVal = this.getVal(this.vm, this.expr)
let oldVal = this.value
if (newVal !== oldVal) {
this.cb(newVal)
}
}
}
// 发布订阅
class Dep {
constructor() {
// 订阅数组
this.subs = []
}
// 添加订阅
addSub(watcher) {
this.subs.push(watcher)
}
notify() {
this.subs.forEach(watcher => {
watcher.update()
})
}
}
测试html页面,可以成功实现mvvm,view和model已经完成了双向绑定。
下面让我们具体分析其中的细节:
- vue双向数据绑定原理
- vue响应式原理
- vue生命钩子函数执行原理
- vue模板编译渲染函数原理
双向数据绑定
一句话概括:Vue内部通过Object.defineProperty方法属性拦截的方式,把data对象里每个数据的读写转化成getter/setter,当数据变化时通知视图更新,如下图所示:
首先,如何让数据对象变得可观测?我们可以使用Object.defineProperty方法,使数据变得可观测。举个例子:
/**
* 把一个对象的每一项都转化成可观测对象
* @param { Object } obj 对象
*/
function observable(obj) {
if (!obj || typeof obj !== 'object') {
return;
}
let keys = Object.keys(obj);
keys.forEach((key) => {
defineReactive(obj, key, obj[key])
})
return obj;
}
/**
* 使一个对象转化成可观测对象
* @param { Object } obj 对象
* @param { String } key 对象的key
* @param { Any } val 对象的某个key的值
*/
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
console.log(`${key}属性被读取了`);
return val;
},
set(newVal) {
console.log(`${key}属性被修改了`);
val = newVal;
}
})
}
通过调用observable方法,可以使得传入的obj对象任何属性变得可观测。
完成了数据的'可观测',我们就知道了数据在什么时候被读写了,于是就可以在数据被读写的时候通知那些依赖该数据的视图更新。为了方便,我们需要先将所有依赖收集起来,一旦数据发生变化,就统一通知更新。其实,这就是典型的“发布订阅者”模式,数据变化为“发布者”,依赖对象为“订阅者”。
现在,我们需要创建一个依赖收集容器,也就是消息订阅器Dep,用来容纳所有的“订阅者”。订阅器Dep主要负责收集订阅者,然后当数据变化的时候后执行对应订阅者的更新函数。
创建消息订阅器Dep:
class Dep {
constructor() {
this.subs = []
}
//增加订阅者
addSub(sub) {
this.subs.push(sub);
}
//判断是否增加订阅者
depend() {
if (Dep.target) {
this.addSub(Dep.target)
}
}
//通知订阅者更新
notify() {
this.subs.forEach((sub) => {
sub.update()
})
}
}
Dep.target = null;
有了订阅器,再将defineReactive函数进行改造一下,向其写入订阅器:
function defineReactive(obj, key, val) {
let dep = new Dep();
Object.defineProperty(obj, key, {
get() {
dep.depend();
console.log(`${key}属性被读取了`);
return val;
},
set(newVal) {
val = newVal;
console.log(`${key}属性被修改了`);
dep.notify() //数据变化通知所有订阅者
}
})
}
我们设计了一个订阅器Dep类,该类里面定义了一些属性和方法,这里需要特别注意的是它有一个静态属性 target,这是一个全局唯一的Watcher。
我们将订阅器Dep添加订阅者的操作设计在getter里面,这是为了让Watcher初始化时进行触发,因此需要判断是否要添加订阅者。在setter函数里面,如果数据变化,就会去通知所有订阅者,订阅者们就会去执行对应的更新的函数。
接下来,我们设计订阅者Watcher:
class Watcher {
constructor(vm, exp, cb) {
this.vm = vm; // 一个Vue的实例对象;
this.exp = exp; //是node节点的v-model或v-on:click等指令的属性值。如v-model="name",exp就是name;
this.cb = cb; // 是Watcher绑定的更新函数;
this.value = this.get(); // 将自己添加到订阅器的操作
}
update() {
let value = this.vm.data[this.exp];
let oldVal = this.value;
if (value !== oldVal) {
this.value = value;
this.cb.call(this.vm, value, oldVal);
}
}
get() {
Dep.target = this; // 缓存自己
let value = this.vm.data[this.exp] // 强制执行监听器里的get函数
Dep.target = null; // 释放自己
return value;
}
}
开始测试:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
</head>
<body>
<h1 id="name"></h1>
<input type="text">
<input type="button" value="改变data内容" onclick="changeInput()">
<script src="observer.js"></script>
<script src="watcher.js"></script>
<script>
function myVue(data, el, exp) {
this.data = data;
observable(data); //将数据变的可观测
el.innerHTML = this.data[exp]; // 初始化模板数据的值
new Watcher(this, exp, function(value) {
el.innerHTML = value;
});
return this;
}
var ele = document.querySelector('#name');
var input = document.querySelector('input');
var myVue = new myVue({
name: 'hello world'
}, ele, 'name');
//改变输入框内容
input.oninput = function(e) {
myVue.data.name = e.target.value
}
//改变data内容
function changeInput() {
myVue.data.name = "难凉热血"
}
</script>
</body>
</html>
响应式原理
数据发生变化后,会重新对页面渲染,这就是Vue响应式,那么这一切是怎么做到的呢?想完成这个过程,我们需要:
- 侦测数据的变化
- 收集视图依赖了哪些数据
- 数据变化时,自动“通知”需要更新的视图部分,并进行更新
Vue 是如何将一个plain object(The PlainObject type is a JavaScript object containing zero or more key-value pairs)给处理成 reactive object 的,也就是说Vue 是如何拦截对象的 get/set 的呢?
追踪变化:
把一个普通JS对象传给Vue实例的data选项,Vue将遍历此对象所有的属性,并使用Object.defineProperty把这些属性全部转为getter/setter。每个组件实例都有相应的watcher实例对象,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的setter被调用时,会通知watcher重新计算,从而致使它关联的组件得以更新。如下图所示:
具体是执行过程如下:
-
- 在 new Vue() 后, Vue 会调用_init 函数进行初始化,也就是init 过程,在 这个过程Data通过Observer转换成了getter/setter的形式,来对数据追踪变化,当被设置的对象被读取的时候会执行getter 函数,而在当被赋值的时候会执行 setter函数。
-
- 当外界通过Watcher读取数据时,会触发getter从而将Watcher添加到依赖中。
-
- 在修改对象的值的时候,会触发对应的setter, setter通知之前依赖收集得到的 Dep 中的每一个 Watcher,告诉它们自己的值改变了,需要重新渲染视图。这时候这些 Watcher就会开始调用 update 来更新视图。
还有几个问题需要深入研究下:
1. 如何侦测数据的变化?
其实有两种办法可以侦测到变化:使用Object.defineProperty和ES6的Proxy,这就是进行数据劫持或数据代理。
Object.defineProperty方式
function observe(obj) { // 我们来用它使对象变成可观察的
// 判断类型
if (!obj || typeof obj !== 'object') return
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
function defineReactive(obj, key, value) {
// 递归子属性
observe(value)
Object.defineProperty(obj, key, {
enumerable: true, //可枚举(可以遍历)
configurable: true, //可配置(比如可以删除)
get: function reactiveGetter() {
console.log('get', value) // 监听
return value
},
set: function reactiveSetter(newVal) {
observe(newVal) //如果赋值是一个对象,也要递归子属性
if (newVal !== value) {
console.log('set', newVal) // 监听
render()
value = newVal
}
}
})
}
但是使用Object.defineProperty有几个问题:
- 无法检测到对象属性的添加或删除
- 不能监听数组的变化,需要进行数组方法的重写
let handler = {
get(target, key) {
// 如果取的值是对象就在对这个对象进行数据劫持
if (typeof target[key] == 'object' && target[key] !== null) {
return new Proxy(target[key], handler)
}
return Reflect.get(target, key)
},
set(target, key, value) {
if (key === 'length') return true
render()
return Reflect.set(target, key, value)
}
}
let proxy = new Proxy(obj, handler)
Proxy 的代理是针对整个对象的,而不是对象的某个属性,因此不同于 Object.defineProperty 的必须遍历对象每个属性,Proxy 只需要做一层代理就可以监听同级结构下的所有属性变化,当然对于深层结构,递归还是需要进行的。此外Proxy支持代理数组的变化。
2. 收集视图依赖了哪些数据?
收集依赖需要为依赖找一个存储依赖的地方,为此我们创建了Dep,它用来收集依赖、删除依赖和向依赖发送消息等。
先来实现一个订阅者 Dep 类,用于解耦属性的依赖收集和派发更新操作,说得具体点,它的主要作用是用来存放 Watcher 观察者对象。我们可以把Watcher理解成一个中介的角色,数据发生变化时通知它,然后它再通知其他地方。
class Dep {
constructor() {
/* 用来存放Watcher对象的数组 */
this.subs = [];
}
/* 在subs中添加一个Watcher对象 */
addSub(sub) {
this.subs.push(sub);
}
/* 通知所有Watcher对象更新视图 */
notify() {
this.subs.forEach((sub) => {
sub.update();
})
}
}
- 用
addSub方法可以在目前的Dep对象中增加一个Watcher的订阅操作; - 用
notify方法通知目前Dep对象的subs中的所有Watcher对象触发更新操作。
3. 数据变化时,如何自动“通知”需要更新的视图部分,并进行更新?
Vue 中定义一个 Watcher 类来表示观察订阅依赖。如下图所示:
当属性发生变化后,我们要通知用到数据的地方,而使用这个数据的地方有很多,而且类型还不一样,既有可能是模板,也有可能是用户写的一个watch,这时需要抽象出一个能集中处理这些情况的类。然后,我们在依赖收集阶段只收集这个封装好的类的实例进来,通知也只通知它一个,再由它负责通知其他地方。
class Watcher {
constructor(obj, key, cb) {
// 将 Dep.target 指向自己
// 然后触发属性的 getter 添加监听
// 最后将 Dep.target 置空
Dep.target = this
this.cb = cb
this.obj = obj
this.key = key
this.value = obj[key]
Dep.target = null
}
update() {
// 获得新值
this.value = this.obj[this.key]
// 我们定义一个 cb 函数,这个函数用来模拟视图更新,调用它即代表更新视图
this.cb(this.value)
}
}
vue生命周期钩子实现
实例生命周期钩子
Vue实例在创建之前需要经过初始化,也会运行一些生命周期函数,这给了用户在不同阶段添加自己的代码的机会。比如created钩子可以用来在一个实例被创建之后执行代码,也有一些其它的钩子,在实例生命周期的不同阶段被调用,如mounted、updated和destroyed。生命周期钩子的this上下文指 向调用它的Vue实例。
生命周期示意图
生命周期钩子如何执行
所有的生命周期函数都是需要调用callHook方法执行的。
export function callHook (vm: Component, hook: string) {
pushTarget()
const handlers = vm.$options[hook]
const info = `${hook} hook`
if (handlers) {
for (let i = 0, j = handlers.length; i < j; i++) {
invokeWithErrorHandling(handlers[i], vm, null, vm, info)
}
}
if (vm._hasHookEvent) {
vm.$emit('hook:' + hook)
}
popTarget()
}
有源码可以看出,调用callHook方法时,会传入Vue实例和hook名称,根据hook名称去vm.$options上获取该生命周期对应的数组,再依次执行这些数组中的操作。因为Vue中各个阶段的生命周期的函数被合并到 vm.$options 里,因此是可以根据 vm.$options[hook]获取到生命周期的函数数组的。
Vue中有这么多的生命周期,如:
- beforeCreate & created
- beforeMount & mounted
- beforeUpdate & updated
- beforeDestroy & destroyed
- activated & deactivated 它是在什么时候调用的呢?
beforeCreate & created
在Vue的初始化阶段(_init函数)就执行了beforeCreate和created两个钩子函数,分别是在initState函数的前后。也就是说,在初始化data、props、methods等之前,就已经执行了beforeCreate钩子。因此,如果我们需要获取data、props、methods等,那就需要考虑created钩子了。
Vue.prototype._init = function (options?: Object) {
// ...
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
// ...
}
beforeMount & mounted 在渲染vnode之前,就执行了beforeMount钩子,将vnode映射到真实DOM上时,再执行mounted钩子。源码如下所示:
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
// ...
callHook(vm, 'beforeMount')
// ...
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
beforeUpdate & updated beforeUpdate是在渲染Watcher的before函数中执行的
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
// ...
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
}, true)
// ...
}
updated是在flushSchedulerQueue函数调用的时候执行的。
function flushSchedulerQueue () {
// ...
callUpdatedHooks(updatedQueue)
}
function callUpdatedHooks (queue) {
let i = queue.length
while (i--) {
const watcher = queue[i]
const vm = watcher.vm
if (vm._watcher === watcher && vm._isMounted) {
callHook(vm, 'updated')
}
}
}