思路分析:
首先思考vue
路由要解决的问题:用户点击跳转链接内容切换,页面不刷新。
hash
或者history api
实现url
跳转页面不刷新hashchange
事件或者popstate
事件处理跳转hash
值或者state
值从routes
表中匹配对应component
并渲染回答范例:
一个SPA
应用的路由需要解决的问题是 页面跳转内容改变同时不刷新 ,同时路由还需要以插件形式存在,所以:
createRouter
函数,返回路由器实例,实例内部做几件事hash
或者popstate
事件path
匹配对应路由router
定义成一个Vue
插件,即实现install
方法,内部做两件事router-link
和router-view
,分别实现页面跳转和内容显示$route
和$router
,组件内可以访问当前路由和路由器实例Vue3.x 改用 Proxy 替代 Object.defineProperty。因为 Proxy 可以直接监听对象和数组的变化,并且有多达 13 种拦截方法。
相关代码如下
import { mutableHandlers } from "./baseHandlers"; // 代理相关逻辑
import { isObject } from "./util"; // 工具方法
export function reactive(target) {
// 根据不同参数创建不同响应式对象
return createReactiveObject(target, mutableHandlers);
}
function createReactiveObject(target, baseHandler) {
if (!isObject(target)) {
return target;
}
const observed = new Proxy(target, baseHandler);
return observed;
}
const get = createGetter();
const set = createSetter();
function createGetter() {
return function get(target, key, receiver) {
// 对获取的值进行放射
const res = Reflect.get(target, key, receiver);
console.log("属性获取", key);
if (isObject(res)) {
// 如果获取的值是对象类型,则返回当前对象的代理对象
return reactive(res);
}
return res;
};
}
function createSetter() {
return function set(target, key, value, receiver) {
const oldValue = target[key];
const hadKey = hasOwn(target, key);
const result = Reflect.set(target, key, value, receiver);
if (!hadKey) {
console.log("属性新增", key, value);
} else if (hasChanged(value, oldValue)) {
console.log("属性值被修改", key, value);
}
return result;
};
}
export const mutableHandlers = {
get, // 当获取属性时调用此方法
set, // 当修改属性时调用此方法
};
优点:
缺点:
computed
Computed
本质是一个具备缓存的watcher
,依赖的属性发生变化就会更新视图。 适用于计算比较消耗性能的计算场景。当表达式过于复杂时,在模板中放入过多逻辑会让模板难以维护,可以将复杂的逻辑放入计算属性中处理
<template>{{fullName}}</template>
export default {
data(){
return {
firstName: 'zhang',
lastName: 'san',
}
},
computed:{
fullName: function(){
return this.firstName + ' ' + this.lastName
}
}
}
watch
用于观察和监听页面上的vue实例,如果要在数据变化的同时进行异步操作或者是比较大的开销,那么watch
为最佳选择
Watch
没有缓存性,更多的是观察的作用,可以监听某些数据执行回调。当我们需要深度监听对象中的属性时,可以打开deep:true
选项,这样便会对对象中的每一项进行监听。这样会带来性能问题,优化的话可以使用字符串形式监听,如果没有写到组件中,不要忘记使用unWatch
手动注销
<template>{{fullName}}</template>
export default {
data(){
return {
firstName: 'zhang',
lastName: 'san',
fullName: 'zhang san'
}
},
watch:{
firstName(val) {
this.fullName = val + ' ' + this.lastName
},
lastName(val) {
this.fullName = this.firstName + ' ' + val
}
}
}
computed:
computed
是计算属性,也就是计算值,它更多用于计算值的场景computed
具有缓存性,computed
的值在getter
执行后是会缓存的,只有在它依赖的属性值改变之后,下一次获取computed
的值时才会重新调用对应的getter
来计算computed
适用于计算比较消耗性能的计算场景watch:
props
$emit
或者本组件的值,当数据变化时来执行回调进行后续操作小结:
computed
和watch
都是基于watcher
来实现的computed
属性是具备缓存的,依赖的值不发生变化,对其取值时计算属性方法不会重新执行watch
是监控值的变化,当值发生变化时调用其对应的回调函数computed
watch
来观察这个数据变化回答范例
思路分析
computed
, watch
两者定义,列举使用上的差异vue3
变化computed
特点:具有响应式的返回值
const count = ref(1)
const plusOne = computed(() => count.value + 1)
watch
特点:侦测变化,执行回调
const state = reactive({ count: 0 })
watch(
() => state.count,
(count, prevCount) => {
/* ... */
}
)
回答范例
computed
和methods
的差异是它具备缓存性,如果依赖项不变时不会重新计算。侦听器可以侦测某个响应式数据的变化并执行副作用,常见用法是传递一个函数,执行副作用,watch没有返回值,但可以执行异步操作等复杂逻辑watch
可以传递对象,设置deep
、immediate
等选项vue3
中watch
选项发生了一些变化,例如不再能侦测一个点操作符之外的字符串形式的表达式; reactivity API
中新出现了watch
、watchEffect
可以完全替代目前的watch
选项,且功能更加强大基本使用
// src/core/observer:45;
// 渲染watcher / computed watcher / watch
const vm = new Vue({
el: '#app',
data: {
firstname:'张',
lastname:'三'
},
computed:{ // watcher => firstname lastname
// computed 只有取值时才执行
// Object.defineProperty .get
fullName(){ // firstName lastName 会收集fullName计算属性
return this.firstname + this.lastname
}
},
watch:{
firstname(newVal,oldVal){
console.log(newVal)
}
}
});
setTimeout(() => {
debugger;
vm.firstname = '赵'
}, 1000);
相关源码
// 初始化state
function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
// 初始化计算属性
if (opts.computed) initComputed(vm, opts.computed)
// 初始化watch
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
// 计算属性取值函数
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) { // 如果值依赖的值发生变化,就会进行重新求值
watcher.evaluate(); // this.firstname lastname
}
if (Dep.target) { // 让计算属性所依赖的属性 收集渲染watcher
watcher.depend()
}
return watcher.value
}
}
}
// watch的实现
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
const vm: Component = this
debugger;
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
options.user = true
const watcher = new Watcher(vm, expOrFn, cb, options) // 创建watcher,数据更新调用cb
if (options.immediate) {
try {
cb.call(vm, watcher.value)
} catch (error) {
handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
}
}
return function unwatchFn () {
watcher.teardown()
}
}
Vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。主要分为以下几个步骤:
数据总是从父组件传到子组件,子组件没有权利修改父组件传过来的数据,只能请求父组件对原始数据进行修改。这样会 防止从子组件意外改变父级组件的状态 ,从而导致你的应用的数据流向难以理解
注意 :在子组件直接用 v-model
绑定父组件传过来的 prop
这样是不规范的写法 开发环境会报警告
如果实在要改变父组件的 prop
值,可以在 data
里面定义一个变量 并用 prop
的值初始化它 之后用$emit
通知父组件去修改
有两种常见的试图改变一个 prop 的情形 :
prop
用来传递一个初始值;这个子组件接下来希望将其作为一个本地的 prop
数据来使用。 在这种情况下,最好定义一个本地的 data
属性并将这个 prop
用作其初始值props: ['initialCounter'],
data: function () {
return {
counter: this.initialCounter
}
}
prop
以一种原始的值传入且需要进行转换。 在这种情况下,最好使用这个 prop
的值来定义一个计算属性props: ['size'],
computed: {
normalizedSize: function () {
return this.size.trim().toLowerCase()
}
}
出现该问题是因为在 Vue 代码尚未被解析之前,尚无法控制页面中 DOM 的显示,所以会看见模板字符串等代码。
解决方案是,在 css 代码中添加 v-cloak 规则,同时在待编译的标签上添加 v-cloak 属性:
[v-cloak] { display: none; }
<div v-cloak>
{{ message }}
</div>
nextTick
中的回调是在下次 DOM
更新循环结束之后执行延迟回调,用于获得更新后的 DOM
DOM
微任务优先
的方式调用异步方法去执行 nextTick
包装的方法
nextTick
方法主要是使用了宏任务和微任务,定义了一个异步方法.多次调用nextTick
会将方法存入队列中,通过这个异步方法清空当前队列。所以这个nextTick
方法就是异步方法
根据执行环境分别尝试采用
Promise
Promise
不支持,再采用MutationObserver
MutationObserver
不支持,再采用setImmediate
setTimeout
flushCallbacks
,把callbacks
里面的数据依次执行回答范例
nextTick
中的回调是在下次 DOM
更新循环结束之后执行延迟回调,用于获得更新后的 DOM
Vue
有个异步更新策略,意思是如果数据变化,Vue
不会立刻更新DOM,而是开启一个队列,把组件更新函数保存在队列中,在同一事件循环中发生的所有数据变更会异步的批量更新。这一策略导致我们对数据的修改不会立刻体现在DOM上,此时如果想要获取更新后的DOM状态,就需要使用nextTick
nextTick
created
中想要获取DOM
时DOM
更新后的状态,比如希望获取列表更新后的高度nextTick
签名如下:function nextTick(callback?: () => void): Promise<void>
所以我们只需要在传入的回调函数中访问最新DOM状态即可,或者我们可以await nextTick()
方法返回的Promise
之后做这件事
Vue
内部,nextTick
之所以能够让我们看到DOM更新后的结果,是因为我们传入的callback
会被添加到队列刷新函数(flushSchedulerQueue
)的后面,这样等队列内部的更新函数都执行完毕,所有DOM操作也就结束了,callback
自然能够获取到最新的DOM值基本使用
const vm = new Vue({
el: '#app',
data() {
return { a: 1 }
}
});
// vm.$nextTick(() => {// [nextTick回调函数fn,内部更新flushSchedulerQueue]
// console.log(vm.$el.innerHTML)
// })
// 是将内容维护到一个数组里,最终按照顺序顺序。 第一次会开启一个异步任务
vm.a = 'test'; // 修改了数据后并不会马上更新视图
vm.$nextTick(() => {// [nextTick回调函数fn,内部更新flushSchedulerQueue]
console.log(vm.$el.innerHTML)
})
// nextTick中的方法会被放到 更新页面watcher的后面去
相关代码如下
// src/core/utils/nextTick
let callbacks = [];
let pending = false;
function flushCallbacks() {
pending = false; //把标志还原为false
// 依次执行回调
for (let i = 0; i < callbacks.length; i++) {
callbacks[i]();
}
}
let timerFunc; //定义异步方法 采用优雅降级
if (typeof Promise !== "undefined") {
// 如果支持promise
const p = Promise.resolve();
timerFunc = () => {
p.then(flushCallbacks);
};
} else if (typeof MutationObserver !== "undefined") {
// MutationObserver 主要是监听dom变化 也是一个异步方法
let counter = 1;
const observer = new MutationObserver(flushCallbacks);
const textNode = document.createTextNode(String(counter));
observer.observe(textNode, {
characterData: true,
});
timerFunc = () => {
counter = (counter + 1) % 2;
textNode.data = String(counter);
};
} else if (typeof setImmediate !== "undefined") {
// 如果前面都不支持 判断setImmediate
timerFunc = () => {
setImmediate(flushCallbacks);
};
} else {
// 最后降级采用setTimeout
timerFunc = () => {
setTimeout(flushCallbacks, 0);
};
}
export function nextTick(cb) {
// 除了渲染watcher 还有用户自己手动调用的nextTick 一起被收集到数组
callbacks.push(cb);
if (!pending) {
// 如果多次调用nextTick 只会执行一次异步 等异步队列清空之后再把标志变为false
pending = true;
timerFunc();
}
}
数据更新的时候内部会调用nextTick
// src/core/observer/scheduler.js
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// queue the flush
if (!waiting) {
waiting = true
if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
// 把更新方法放到数组中维护[nextTick回调函数,更新函数flushSchedulerQueue]
/**
* vm.a = 'test'; // 修改了数据后并不会马上更新视图
vm.$nextTick(() => {// [fn,更新]
console.log(vm.$el.innerHTML)
})
*/
nextTick(flushSchedulerQueue)
}
}
}
首屏时间(First Contentful Paint
),指的是浏览器从响应用户输入网址地址,到首屏内容渲染完成的时间,此时整个网页不一定要全部渲染完成,但需要展示当前视窗需要的内容
首屏加载可以说是用户体验中最重要的环节
关于计算首屏时间
利用performance.timing
提供的数据:
通过DOMContentLoad
或者performance
来计算出首屏时间
// 方案一:
document.addEventListener('DOMContentLoaded', (event) => {
console.log('first contentful painting');
});
// 方案二:
performance.getEntriesByName("first-contentful-paint")[0].startTime
// performance.getEntriesByName("first-contentful-paint")[0]
// 会返回一个 PerformancePaintTiming的实例,结构如下:
{
name: "first-contentful-paint",
entryType: "paint",
startTime: 507.80000002123415,
duration: 0,
};
在页面渲染的过程,导致加载速度慢的因素可能如下:
常见的几种SPA首屏优化方式
1. 减小入口文件体积
常用的手段是路由懒加载,把不同路由对应的组件分割成不同的代码块,待路由被请求的时候会单独打包路由,使得入口文件变小,加载速度大大增加
在vue-router
配置路由的时候,采用动态加载路由的形式
routes:[
path: 'Blogs',
name: 'ShowBlogs',
component: () => import('./components/ShowBlogs.vue')
]
以函数的形式加载路由,这样就可以把各自的路由文件分别打包,只有在解析给定的路由时,才会加载路由组件
2. 静态资源本地缓存
后端返回资源问题:
HTTP
缓存,设置Cache-Control
,Last-Modified
,Etag
等响应头Service Worker
离线缓存前端合理利用localStorage
3. UI框架按需加载
在日常使用UI
框架,例如element-UI
、或者antd
,我们经常性直接引用整个UI
库
import ElementUI from 'element-ui'
Vue.use(ElementUI)
但实际上我用到的组件只有按钮,分页,表格,输入与警告 所以我们要按需引用
import { Button, Input, Pagination, Table, TableColumn, MessageBox } from 'element-ui';
Vue.use(Button)
Vue.use(Input)
Vue.use(Pagination)
4. 组件重复打包
假设A.js
文件是一个常用的库,现在有多个路由使用了A.js
文件,这就造成了重复下载
解决方案:在webpack
的config
文件中,修改CommonsChunkPlugin
的配置
minChunks: 3
minChunks
为3表示会把使用3次及以上的包抽离出来,放进公共依赖文件,避免了重复加载组件
5. 图片资源的压缩
图片资源虽然不在编码过程中,但它却是对页面性能影响最大的因素
对于所有的图片资源,我们可以进行适当的压缩
对页面上使用到的icon
,可以使用在线字体图标,或者雪碧图,将众多小图标合并到同一张图上,用以减轻http
请求压力。
6. 开启GZip压缩
拆完包之后,我们再用gzip
做一下压缩 安装compression-webpack-plugin
cnmp i compression-webpack-plugin -D
在vue.congig.js
中引入并修改webpack
配置
const CompressionPlugin = require('compression-webpack-plugin')
configureWebpack: (config) => {
if (process.env.NODE_ENV === 'production') {
// 为生产环境修改配置...
config.mode = 'production'
return {
plugins: [new CompressionPlugin({
test: /\.js$|\.html$|\.css/, //匹配文件名
threshold: 10240, //对超过10k的数据进行压缩
deleteOriginalAssets: false //是否删除原文件
})]
}
}
在服务器我们也要做相应的配置 如果发送请求的浏览器支持gzip
,就发送给它gzip
格式的文件 我的服务器是用express
框架搭建的 只要安装一下compression
就能使用
const compression = require('compression')
app.use(compression()) // 在其他中间件使用之前调用
7. 使用SSR
SSR(Server side ),也就是服务端渲染,组件或页面通过服务器生成html字符串,再发送到浏览器
从头搭建一个服务端渲染是很复杂的,vue
应用建议使用Nuxt.js
实现服务端渲染
减少首屏渲染时间的方法有很多,总的来讲可以分成两大部分 :资源加载优化
和 页面渲染优化
下图是更为全面的首屏优化的方案
大家可以根据自己项目的情况选择各种方式进行首屏渲染的优化
虚拟节点就是用一个对象来描述一个真实的DOM元素。首先将template
(真实DOM)先转成ast
,ast
树通过codegen
生成render
函数,render
函数里的_c
方法将它转为虚拟dom
首页可以控制导航跳转,
beforeEach
,afterEach
等,一般用于页面title
的修改。一些需要登录才能调整页面的重定向功能。
beforeEach
主要有3个参数to
,from
,next
。to
:route
即将进入的目标路由对象。from
:route
当前导航正要离开的路由。next
:function
一定要调用该方法resolve
这个钩子。执行效果依赖next
方法的调用参数。可以控制网页的跳转v-if 是真正的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建;也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。
v-show 就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 的 “display” 属性进行切换。
所以,v-if 适用于在运行时很少改变条件,不需要频繁切换条件的场景;v-show 则适用于需要非常频繁切换条件的场景。
=> 相同点:
1. 数据驱动页面,提供响应式的试图组件
2. 都有virtual DOM,组件化的开发,通过props参数进行父子之间组件传递数据,都实现了webComponents规范
3. 数据流动单向,都支持服务器的渲染SSR
4. 都有支持native的方法,react有React native, vue有wexx
=> 不同点:
1.数据绑定:Vue实现了双向的数据绑定,react数据流动是单向的
2.数据渲染:大规模的数据渲染,react更快
3.使用场景:React配合Redux架构适合大规模多人协作复杂项目,Vue适合小快的项目
4.开发风格:react推荐做法jsx + inline style把html和css都写在js了
vue是采用webpack + vue-loader单文件组件格式,html, js, css同一个文件
scoped
虽然避免了组件间样式污染,但是很多时候我们需要修改组件中的某个样式,但是又不想去除scoped
属性
/deep/
<!-- Parent -->
<template>
<div class="wrap">
<Child />
</div>
</template>
<style lang="scss" scoped>
.wrap /deep/ .box{
background: red;
}
</style>
<!-- Child -->
<template>
<div class="box"></div>
</template>
style
标签<!-- Parent -->
<template>
<div class="wrap">
<Child />
</div>
</template>
<style lang="scss" scoped>
/* 其他样式 */
</style>
<style lang="scss">
.wrap .box{
background: red;
}
</style>
<!-- Child -->
<template>
<div class="box"></div>
</template>
SPA
( single-page application )仅在Web
页面初始化时加载相应的HTML
、JavaScript
和CSS
。一旦页面加载完成,SPA
不会因为用户的操作而进行页面的重新加载或跳转;取而代之的是利用路由机制实现HTML
内容的变换,UI
与用户的交互,避免页面的重新加载
优点:
SPA
相对对服务器压力小;缺点:
Web
应用功能及显示效果,需要在加载页面的时候将 JavaScript
、CSS
统一加载,部分页面按需加载;SEO
难度较大:由于所有的内容都在一个页面中动态替换显示,所以在 SEO
上其有着天然的弱势单页应用与多页应用的区别
单页面应用(SPA) | 多页面应用(MPA) | |
---|---|---|
组成 | 一个主页面和多个页面片段 | 多个主页面 |
刷新方式 | 局部刷新 | 整页刷新 |
| 哈希模式 | 历史模式 |
| 难实现,可使用SSR方式改善 | 容易实现 |
数据传递 | 容易 | 通过 |
页面切换 | 速度快,用户体验良好 | 切换加载资源,速度慢,用户体验差 |
维护成本 | 相对容易 | 相对复杂 |
实现一个SPA
hash
变化驱动界面变化pushsate
记录浏览器的历史,驱动界面发送变化url
中的hash
来进行路由跳转// 定义 Router
class Router {
constructor () {
this.routes = {}; // 存放路由path及callback
this.currentUrl = '';
// 监听路由change调用相对应的路由回调
window.addEventListener('load', this.refresh, false);
window.addEventListener('hashchange', this.refresh, false);
}
route(path, callback){
this.routes[path] = callback;
}
push(path) {
this.routes[path] && this.routes[path]()
}
}
// 使用 router
window.miniRouter = new Router();
miniRouter.route('/', () => console.log('page1'))
miniRouter.route('/page2', () => console.log('page2'))
miniRouter.push('/') // page1
miniRouter.push('/page2') // page2
history
模式核心借用 HTML5 history api
,api
提供了丰富的 router
相关属性先了解一个几个相关的apihistory.pushState
浏览器历史纪录添加记录history.replaceState
修改浏览器历史纪录中当前纪录history.popState
当 history
发生变化时触发// 定义 Router
class Router {
constructor () {
this.routes = {};
this.listerPopState()
}
init(path) {
history.replaceState({path: path}, null, path);
this.routes[path] && this.routes[path]();
}
route(path, callback){
this.routes[path] = callback;
}
push(path) {
history.pushState({path: path}, null, path);
this.routes[path] && this.routes[path]();
}
listerPopState () {
window.addEventListener('popstate' , e => {
const path = e.state && e.state.path;
this.routers[path] && this.routers[path]()
})
}
}
// 使用 Router
window.miniRouter = new Router();
miniRouter.route('/', ()=> console.log('page1'))
miniRouter.route('/page2', ()=> console.log('page2'))
// 跳转
miniRouter.push('/page2') // page2
题外话:如何给SPA做SEO
将组件或页面通过服务器生成html
,再返回给浏览器,如nuxt.js
目前主流的静态化主要有两种:
URL Rewrite
的方式,它的原理是通过web服务器内部模块按一定规则将外部的URL请求转化为内部的文件地址,一句话来说就是把外部请求的静态地址转化为实际的动态页面地址,而静态页面实际是不存在的。这两种方法都达到了实现URL静态化的效果Phantomjs
针对爬虫处理原理是通过Nginx
配置,判断访问来源是否为爬虫,如果是则搜索引擎的爬虫请求会转发到一个node server
,再通过PhantomJS
来解析完整的HTML
,返回给爬虫。下面是大致流程图
提示:vue3.2
版本开始才能使用语法糖!
在 Vue3.0
中变量必须 return
出来, template
中才能使用;而在 Vue3.2
中只需要在 script
标签上加上 setup
属性,无需 return
, template
便可直接使用,非常的香啊!
1. 如何使用setup语法糖
只需在 script
标签上写上 setup
<template>
</template>
<script setup>
</script>
<style scoped lang="less">
</style>
2. data数据的使用
由于 setup
不需写 return
,所以直接声明数据即可
<script setup>
import {
ref,
reactive,
toRefs,
} from 'vue'
const data = reactive({
patternVisible: false,
debugVisible: false,
aboutExeVisible: false,
})
const content = ref('content')
//使用toRefs解构
const { patternVisible, debugVisible, aboutExeVisible } = toRefs(data)
</script>
3. method方法的使用
<template >
<button @click="onClickHelp">帮助</button>
</template>
<script setup>
import {reactive} from 'vue'
const data = reactive({
aboutExeVisible: false,
})
// 点击帮助
const onClickHelp = () => {
console.log(`帮助`)
data.aboutExeVisible = true
}
</script>
4. watchEffect的使用
<script setup>
import {
ref,
watchEffect,
} from 'vue'
let sum = ref(0)
watchEffect(()=>{
const x1 = sum.value
console.log('watchEffect所指定的回调执行了')
})
</script>
5. watch的使用
<script setup>
import {
reactive,
watch,
} from 'vue'
//数据
let sum = ref(0)
let msg = ref('hello')
let person = reactive({
name:'张三',
age:18,
job:{
j1:{
salary:20
}
}
})
// 两种监听格式
watch([sum,msg],(newValue,oldValue)=>{
console.log('sum或msg变了',newValue,oldValue)
},
{immediate:true}
)
watch(()=>person.job,(newValue,oldValue)=>{
console.log('person的job变化了',newValue,oldValue)
},{deep:true})
</script>
6. computed计算属性的使用
computed
计算属性有两种写法(简写和考虑读写的完整写法)
<script setup>
import {
reactive,
computed,
} from 'vue'
// 数据
let person = reactive({
firstName:'poetry',
lastName:'x'
})
// 计算属性简写
person.fullName = computed(()=>{
return person.firstName + '-' + person.lastName
})
// 完整写法
person.fullName = computed({
get(){
return person.firstName + '-' + person.lastName
},
set(value){
const nameArr = value.split('-')
person.firstName = nameArr[0]
person.lastName = nameArr[1]
}
})
</script>
7. props父子传值的使用
父组件代码如下(示例):
<template>
<child :name='name'/>
</template>
<script setup>
import {ref} from 'vue'
// 引入子组件
import child from './child.vue'
let name= ref('poetry')
</script>
子组件代码如下(示例):
<template>
<span>{{props.name}}</span>
</template>
<script setup>
import { defineProps } from 'vue'
// 声明props
const props = defineProps({
name: {
type: String,
default: 'poetries'
}
})
// 或者
//const props = defineProps(['name'])
</script>
8. emit子父传值的使用
父组件代码如下(示例):
<template>
<AdoutExe @aboutExeVisible="aboutExeHandleCancel" />
</template>
<script setup>
import { reactive } from 'vue'
// 导入子组件
import AdoutExe from '../components/AdoutExeCom'
const data = reactive({
aboutExeVisible: false,
})
// content组件ref
// 关于系统隐藏
const aboutExeHandleCancel = () => {
data.aboutExeVisible = false
}
</script>
子组件代码如下(示例):
<template>
<a-button @click="isOk">
确定
</a-button>
</template>
<script setup>
import { defineEmits } from 'vue';
// emit
const emit = defineEmits(['aboutExeVisible'])
/**
* 方法
*/
// 点击确定按钮
const isOk = () => {
emit('aboutExeVisible');
}
</script>
9. 获取子组件ref变量和defineExpose暴露
即vue2
中的获取子组件的ref
,直接在父组件中控制子组件方法和变量的方法
父组件代码如下(示例):
<template>
<button @click="onClickSetUp">点击</button>
<Content ref="content" />
</template>
<script setup>
import {ref} from 'vue'
// content组件ref
const content = ref('content')
// 点击设置
const onClickSetUp = ({ key }) => {
content.value.modelVisible = true
}
</script>
<style scoped lang="less">
</style>
子组件代码如下(示例):
<template>
<p>{{data }}</p>
</template>
<script setup>
import {
reactive,
toRefs
} from 'vue'
/**
* 数据部分
* */
const data = reactive({
modelVisible: false,
historyVisible: false,
reportVisible: false,
})
defineExpose({
...toRefs(data),
})
</script>
10. 路由useRoute和useRouter的使用
<script setup>
import { useRoute, useRouter } from 'vue-router'
// 声明
const route = useRoute()
const router = useRouter()
// 获取query
console.log(route.query)
// 获取params
console.log(route.params)
// 路由跳转
router.push({
path: `/index`
})
</script>
11. store仓库的使用
<script setup>
import { useStore } from 'vuex'
import { num } from '../store/index'
const store = useStore(num)
// 获取Vuex的state
console.log(store.state.number)
// 获取Vuex的getters
console.log(store.state.getNumber)
// 提交mutations
store.commit('fnName')
// 分发actions的方法
store.dispatch('fnName')
</script>
12. await的支持
setup
语法糖中可直接使用await
,不需要写async
,setup
会自动变成async setup
<script setup>
import api from '../api/Api'
const data = await Api.getData()
console.log(data)
</script>
13. provide 和 inject 祖孙传值
父组件代码如下(示例):
<template>
<AdoutExe />
</template>
<script setup>
import { ref,provide } from 'vue'
import AdoutExe from '@/components/AdoutExeCom'
let name = ref('py')
// 使用provide
provide('provideState', {
name,
changeName: () => {
name.value = 'poetries'
}
})
</script>
子组件代码如下(示例):
<script setup>
import { inject } from 'vue'
const provideState = inject('provideState')
provideState.changeName()
</script>
子组件不可以直接改变父组件的数据。这样做主要是为了维护父子组件的单向数据流。每次父级组件发生更新时,子组件中所有的 prop 都将会刷新为最新的值。如果这样做了,Vue 会在浏览器的控制台中发出警告。
Vue提倡单向数据流,即父级 props 的更新会流向子组件,但是反过来则不行。这是为了防止意外的改变父组件状态,使得应用的数据流变得难以理解,导致数据流混乱。如果破坏了单向数据流,当应用复杂时,debug 的成本会非常高。
只能通过 $emit
派发一个自定义事件,父组件接收到后,由父组件修改。
事件修饰符
v-model 的修饰符
键盘事件的修饰符
系统修饰键
鼠标按钮修饰符
1. 分析
首先找到vue
的构造函数
源码位置:src\core\instance\index.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)
}
options
是用户传递过来的配置项,如data、methods
等常用的方法
vue
构建函数调用_init
方法,但我们发现本文件中并没有此方法,但仔细可以看到文件下方定定义了很多初始化方法
initMixin(Vue); // 定义 _init
stateMixin(Vue); // 定义 $set $get $delete $watch 等
eventsMixin(Vue); // 定义事件 $on $once $off $emit
lifecycleMixin(Vue);// 定义 _update $forceUpdate $destroy
renderMixin(Vue); // 定义 _render 返回虚拟dom
首先可以看initMixin
方法,发现该方法在Vue
原型上定义了_init
方法
源码位置:src\core\instance\init.js
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid
vm._uid = uid++
let startTag, endTag
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}
// a flag to avoid this being observed
vm._isVue = true
// merge options
// 合并属性,判断初始化的是否是组件,这里合并主要是 mixins 或 extends 的方法
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else { // 合并vue属性
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
// 初始化proxy拦截器
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
// 初始化组件生命周期标志位
initLifecycle(vm)
// 初始化组件事件侦听
initEvents(vm)
// 初始化渲染方法
initRender(vm)
callHook(vm, 'beforeCreate')
// 初始化依赖注入内容,在初始化data、props之前
initInjections(vm) // resolve injections before data/props
// 初始化props/data/method/watch/methods
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${vm._name} init`, startTag, endTag)
}
// 挂载元素
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
仔细阅读上面的代码,我们得到以下结论:
beforeCreate
之前,数据初始化并未完成,像data
、props
这些属性无法访问到created
的时候,数据已经初始化完成,能够访问data
、props
这些属性,但这时候并未完成dom
的挂载,因此无法访问到dom
元素vm.$mount
方法initState
方法是完成props/data/method/watch/methods
的初始化
源码位置:src\core\instance\state.js
export function initState (vm: Component) {
// 初始化组件的watcher列表
vm._watchers = []
const opts = vm.$options
// 初始化props
if (opts.props) initProps(vm, opts.props)
// 初始化methods方法
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
// 初始化data
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
我们和这里主要看初始化data
的方法为initData
,它与initState
在同一文件上
function initData (vm: Component) {
let data = vm.$options.data
// 获取到组件上的data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
if (!isPlainObject(data)) {
data = {}
process.env.NODE_ENV !== 'production' && warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
)
}
// proxy data on instance
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
const key = keys[i]
if (process.env.NODE_ENV !== 'production') {
// 属性名不能与方法名重复
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
// 属性名不能与state名称重复
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== 'production' && warn(
`The data property "${key}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else if (!isReserved(key)) { // 验证key值的合法性
// 将_data中的数据挂载到组件vm上,这样就可以通过this.xxx访问到组件上的数据
proxy(vm, `_data`, key)
}
}
// observe data
// 响应式监听data是数据的变化
observe(data, true /* asRootData */)
}
仔细阅读上面的代码,我们可以得到以下结论:
props
、methods
、data
data
定义的时候可选择函数形式或者对象形式(组件只能为函数形式)关于数据响应式在这就不展开详细说明
上文提到挂载方法是调用vm.$mount
方法
源码位置:
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
// 获取或查询元素
el = el && query(el)
/* istanbul ignore if */
// vue 不允许直接挂载到body或页面文档上
if (el === document.body || el === document.documentElement) {
process.env.NODE_ENV !== 'production' && warn(
`Do not mount Vue to <html> or <body> - mount to normal elements instead.`
)
return this
}
const options = this.$options
// resolve template/el and convert to render function
if (!options.render) {
let template = options.template
// 存在template模板,解析vue模板文件
if (template) {
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
template = idToTemplate(template)
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !template) {
warn(
`Template element not found or is empty: ${options.template}`,
this
)
}
}
} else if (template.nodeType) {
template = template.innerHTML
} else {
if (process.env.NODE_ENV !== 'production') {
warn('invalid template option:' + template, this)
}
return this
}
} else if (el) {
// 通过选择器获取元素内容
template = getOuterHTML(el)
}
if (template) {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile')
}
/**
* 1.将temmplate解析ast tree
* 2.将ast tree转换成render语法字符串
* 3.生成render方法
*/
const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile end')
measure(`vue ${this._name} compile`, 'compile', 'compile end')
}
}
}
return mount.call(this, el, hydrating)
}
阅读上面代码,我们能得到以下结论:
body
或者html
上template/render
或者直接使用template
、el
表示元素选择器render
函数,调用compileToFunctions
,会将template
解析成render
函数对template
的解析步骤大致分为以下几步:
html
文档片段解析成ast
描述符ast
描述符解析成字符串render
函数生成render
函数,挂载到vm
上后,会再次调用mount
方法
源码位置:src\platforms\web\runtime\index.js
// public mount method
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
// 渲染组件
return mountComponent(this, el, hydrating)
}
调用mountComponent
渲染组件
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
// 如果没有获取解析的render函数,则会抛出警告
// render是解析模板文件生成的
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
if (process.env.NODE_ENV !== 'production') {
/* istanbul ignore if */
if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
vm.$options.el || el) {
warn(
'You are using the runtime-only build of Vue where the template ' +
'compiler is not available. Either pre-compile the templates into ' +
'render functions, or use the compiler-included build.',
vm
)
} else {
// 没有获取到vue的模板文件
warn(
'Failed to mount component: template or render function not defined.',
vm
)
}
}
}
// 执行beforeMount钩子
callHook(vm, 'beforeMount')
let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = () => {
const name = vm._name
const id = vm._uid
const startTag = `vue-perf-start:${id}`
const endTag = `vue-perf-end:${id}`
mark(startTag)
const vnode = vm._render()
mark(endTag)
measure(`vue ${name} render`, startTag, endTag)
mark(startTag)
vm._update(vnode, hydrating)
mark(endTag)
measure(`vue ${name} patch`, startTag, endTag)
}
} else {
// 定义更新函数
updateComponent = () => {
// 实际调⽤是在lifeCycleMixin中定义的_update和renderMixin中定义的_render
vm._update(vm._render(), hydrating)
}
}
// 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 && !vm._isDestroyed) {
// 数据更新引发的组件更新
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
阅读上面代码,我们得到以下结论:
boforeCreate
钩子updateComponent
渲染页面视图的方法beforeUpdate
生命钩子updateComponent
方法主要执行在vue
初始化时声明的render
,update
方法
render的作用主要是生成
vnode
源码位置:src\core\instance\render.js
// 定义vue 原型上的render方法
Vue.prototype._render = function (): VNode {
const vm: Component = this
// render函数来自于组件的option
const { render, _parentVnode } = vm.$options
if (_parentVnode) {
vm.$scopedSlots = normalizeScopedSlots(
_parentVnode.data.scopedSlots,
vm.$slots,
vm.$scopedSlots
)
}
// set parent vnode. this allows render functions to have access
// to the data on the placeholder node.
vm.$vnode = _parentVnode
// render self
let vnode
try {
// There's no need to maintain a stack because all render fns are called
// separately from one another. Nested component's render fns are called
// when parent component is patched.
currentRenderingInstance = vm
// 调用render方法,自己的独特的render方法, 传入createElement参数,生成vNode
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e) {
handleError(e, vm, `render`)
// return error render result,
// or previous vnode to prevent render error causing blank component
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) {
try {
vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)
} catch (e) {
handleError(e, vm, `renderError`)
vnode = vm._vnode
}
} else {
vnode = vm._vnode
}
} finally {
currentRenderingInstance = null
}
// if the returned array contains only a single node, allow it
if (Array.isArray(vnode) && vnode.length === 1) {
vnode = vnode[0]
}
// return empty vnode in case the render function errored out
if (!(vnode instanceof VNode)) {
if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {
warn(
'Multiple root nodes returned from render function. Render function ' +
'should return a single root node.',
vm
)
}
vnode = createEmptyVNode()
}
// set parent
vnode.parent = _parentVnode
return vnode
}
_update
主要功能是调用patch
,将vnode
转换为真实DOM
,并且更新到页面中
源码位置:src\core\instance\lifecycle.js
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
const prevEl = vm.$el
const prevVnode = vm._vnode
// 设置当前激活的作用域
const restoreActiveInstance = setActiveInstance(vm)
vm._vnode = vnode
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
if (!prevVnode) {
// initial render
// 执行具体的挂载逻辑
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
restoreActiveInstance()
// update __vue__ reference
if (prevEl) {
prevEl.__vue__ = null
}
if (vm.$el) {
vm.$el.__vue__ = vm
}
// if parent is an HOC, update its $el as well
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el
}
// updated hook is called by the scheduler to ensure that children are
// updated in a parent's updated hook.
}
2. 结论
new Vue
的时候调用会调用_init
方法$set
、$get
、$delete
、$watch
等方法$on
、$off
、$emit
、$off
等事件_update
、$forceUpdate
、$destroy
生命周期$mount
进行页面的挂载mountComponent
方法updateComponent
更新函数render
生成虚拟DOM
_update
将虚拟DOM
生成真实DOM
结构,并且渲染到页面中原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。