1、vue的全家桶
vue vue-cli vuex vue-router
2、创建vue项目
参考:创建Vue项目的3种方式 - 掘金 (juejin.cn)
2.1、CLI2初始化项目过程 vue init webpack "项目名称"
第一代官方项目脚手架Vue cli版本 < 3(基于Webpack),安装Vue cli2前要先安装nodejs,npm。安装cli2的命令是npm install -g @vue/cli-init,只能创建vue2的项目。
Vue CLI2是什么?
- Vue CLI是一个官方发布vue.js项目脚手架。
- 使用vue cli可以快速搭建Vue开发环境以及对应的webpack配置。搭建vue项目结构,同时可帮助配置各种文件目录及项目打包。
- vue cli2实现原理,执行npm run dev,实际是在node环境执行build/dev-server.js, dev-server.js会去拿到config中的端口等配置,通过express起一个服务,通过插件自动打开浏览器,加载webpack编译后放在内存的bundle。执行npm run build,实际上执行了build/build.js,通过webpack的一系列配置及插件,将文件打包合并丑化,并创建dist目录,放置编译打包后的文件,这将是未来用在生产环境的包。参考:简述vue-cli 2.x和vue-cli 3+在项目构建、运行、编译执行时的区别 - Echoyya、 - 博客园 (cnblogs.com)
配置环境变量参考:vue-cli2 关于 开发环境 、测试环境、 生产环境的配置_cli2配置开发环境与生产环境-优快云博客
2.2、 cli3初始化项目过程(基于Webpack)
第二代官方项目脚手架 cli版本 >= 3(基于Webpack),安装cli3的命令是npm install -g @vue/cli创建项目命令vue create project-name 。Vue3、Vue2都可以创建
cli3与cli2目录结构对比
参考:简述vue-cli 2.x和vue-cli 3+在项目构建、运行、编译执行时的区别 - Echoyya、 - 博客园 (cnblogs.com)
-
vue-cli 3+的项目摒弃了 config 、 build 、 static 目录,新增了
public
目录,将根目录下的 index.html 放置在 public 目录下。 -
新增
vue.config.js
(需手动创建)配置文件,可以在该文件中进行webpack的相关配置,例如 loader、开发环境等 -
新增
babel.config.js
替代原先的.babelrc,具备和原先.babelrc一样的作用。 -
src文件夹中多了
views
文件夹
vue.config.js是vue-cli3之后新增的一个功能,如果要配置相关属性,就需要自己在项目根目录新建vue.config.js这个文件.
vue.config.js配置
vue2之vue.config.js配置 - 掘金 (juejin.cn)
2.3、Vue 官方的项目脚手架工具(基于Vite)
创建项目命令npm create vite@latest project-name
配置文件 vite.config.ts
参考:vite 搭建vue3项目(一) - 掘金 (juejin.cn)
3、理解mvvm
Vue采用的是MVVM架构,数据变化更新视图,视图变化更新数据。
3.1、响应式原理(双向数据绑定)
参考:Vue双向数据绑定原理(面试必问) - 掘金 (juejin.cn)
双向绑定除了数据驱动 DOM 之外, DOM 的变化反过来影响数据,是一个双向的关系。Vue双向数据绑定是通过数据劫持+发布订阅者模式来实现的。
Vue 采用的是数据劫持结合发布和-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时,发布消息给订阅者(存放他所依赖的 watcher(依赖收集)),通知观察者触发相应的监听回调。
当页面使用对应属性时,每个属性都拥有自己的 dep 属性,存放他所依赖的 watcher(依赖收集),当属性变化后会通知自己对应的 watcher 去更新(派发更新)。
- 每当new一个Vue,主要做了两件事情:第一是监听数据:observe(data)。执行劫持监听data中每一个属性,如果给这个对象的某个值赋值,就会触发setter,notify通知订阅者watcher,那么就能监听到了数据变化。第二是编译HTML:nodeToFragment(id)。compile解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并为每一个与数据绑定相关的节点生成一个订阅者 watcher,一旦数据有变动,收到通知,更新视图。
- 监听器Observer:,会其生成一个消息订阅器dep(依赖收集器)。如果属性发生变化了,就通知订阅者Watcher看是否需要更新。
- Watcher订阅者是Observer和Compile之间通信的桥梁,主要做的事情是:
(1)在自身实例化时往属性订阅器(dep)里面添加自己
(2)自身必须有一个update()方法
(3)待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功成身退。 - MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果。
补充:数据流单项,不会冲突吗?为什么?
答:不冲突,在全局性数据流使用单项,方便跟踪;例如vuex,父子组件通信。
局部性数据流使用双向,简单易操作。例如某个组件中的数据data
3.2、数据劫持(给对象添加属性或方法)
Object.defineProperty(obj, prop, descriptor)
obj:必需。目标对象
prop:必需。需定义或修改的属性的名字
descriptor:必需。给对象的属性添加特性描述
descriptor中利用get,set进行双向数据绑定
var obj = { foo: 'foo' }
Object.defineProperty(obj, 'foo', {
get: function () {
console.log('将要读取obj.foo属性'); },
set: function (newVal) {
console.log('当前值为', newVal);
}
});
obj.foo; // 将要读取obj.foo属性
obj.foo = 'name'; // 当前值为 name
4、v-model 原理
v-model 只是语法糖而已。原理: 在input框里面通过v-bind动态绑定一个value,然后在input框里面通过@input方法去动态获取input输入的值,然后重新给变量赋值就可以了
v-model 在内部为不同的输入元素使用不同的 property 并抛出不同的事件:
- text 和 textarea 元素使用 value和 input 事件;
- checkbox 和 radio 使用 checked和 change 事件;
- select 字段将 value 作为 prop 并将 change 作为事件。
5、发布订阅模式
class EventEmitter {
constructor() {
this.event = {
}
}
on(type, cb) {
if (!this.event[type]) {
this.event[type] = [cb]
} else {
this.event[type].push(cb)
}
}
once(type, cb) {
// 绑定的时fn, 执行的时候会触发fn函数
let fn = () => {
cb(); // fn函数中调用原有的callback
this.off(type, fn); // 删除fn, 再次执行的时候之后执行一次
}
this.on(type, fn)
}
emit(type, ...args) {
if (!this.event[type])
return
else {
this.event[type].forEach(cb => {
cb(...args)
});
}
}
off(type, cb) {
if (!this.event[type])
return
else {
this.event[type] = this.event[type].filter(item => item !== cb)
}
}
}
// 运行示例
let ev = new EventEmitter();
const fun1 = (str) => {
console.log(str);
}
ev.on('say', fun1);
ev.once('say', fun1)
ev.emit('say', 'visa');
ev.off('say', fun1);
6、vue的指令
6.1、内容指令
参考:Vue3.x全家桶之Vue基础指令(一)_零基础玩转vue3.x+全家桶 资料-优快云博客
<div id="app">
// 1.Mustache 语法
<h1>{{message}}</h1>
<h1>{{message+site}}</h1>
//2.初次渲染message='你好Vue3!'后,之后修改message='Vue3!',视图不会发生变化
<h1 v-once>不会变化{{message}}</h1>
//3.变量放在标签属性中。v-text 指令会覆盖元素内默认的值。
<h1 v-text="message"></h1>
//4.v-html按照HTML格式进行解析,并且显示对应的内容
<h1 v-html="site"></h1>
//5.v-pre不解析Mustache 语法
<h1 v-pre>{{message}}</h1>
//v-clock vue解析之前有 v-clock 属性时,让其不显示,Vue解析之后没有v-clock 属性,再让其显示
<h1 v-cloak>{{message}}</h1>
</div>
<script src="../js/vue.js"></script>
<script>
// 1.创建Vue的实例对象
const app = Vue.createApp({
data(){
return {
message: '你好Vue3!',
site: '<a href="www.qindalin.com">大林博客</a>'
}
}
}).mount('#app');
</script>
<style>
[v-cloak] {
display: none;
}
</style>
6.2、属性与样式绑定指令 v-bind (:)
属性与样式我们也希望动态来绑定。比如动态绑定a元素的 href 属性,img元素的 src 属性,动态绑定style或class样式。
参考:【精选】vue中,动态绑定样式——动态绑定style写法 & 动态class写法_vue动态样式绑定的写法_viceen的博客-优快云博客
<div id="app">
<h1>{{message}}</h1>
//1.动态绑定属性
<a :href="url">百度</a>
//2.动态绑定style 驼峰式 (camelCase) 或 短横线分隔 (kebab-case)
<h2 style="color: red;">{{message}}</h2>
<div v-bind:style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
<div v-bind:style="styleObject"></div>
<div :style="{color:(index == 0 ? conFontColor : '#000')}"></div>
//3.动态绑定class
<div :class="{'active' : isActive == -2}" >{{name}}</div>
<div :class="{'active': isActive, 'user': isUser }"></div>
<div :class="classObject">{{name}}</div>
:class="[isActive?‘active’:’’]"
:class="[{ active: isActive }, ‘sort’]"
</div>
<script src="../js/vue.js"></script>
<script>
// 1.创建Vue的实例对象
const app = Vue.createApp({
data(){
return {
message: '你好Vue3!',
url: 'http://www.baidu.com',
activeColor: 'red',
fontSize: 30,
styleObject: {
color: 'red',
fontSize: '13px'
},
classObject:{ active: true, user:false },
}
}
}).mount('#app');
</script>
6.3、事件绑定指令 v-on (@)
Vue事件符.capture的含义用法参考:Vue事件符.capture的含义用法_vue capture-优快云博客
Vue事件符.stop、.prevent、.self、.once参考:v-on指令修饰符大全_v-on的事件修饰符有哪些-优快云博客
v-on:click.prevent.self //会阻止所有的点击的默认事件
v-on:click.self.prevent //只会阻止元素自身点击的默认事件
<body>
<div id="app">
<!--1.阻止冒泡-->
<div class="box" @click="boxClick1">
<button @click.stop="btnClick2">点我</button>
</div>
<!--2.阻止默认事件-->
<form action="http://www.jd.com">
<input type="submit" value="提交" @click.prevent="doSubmit">
</form>
<!--3.只想让其响应一次,需要@click.once-->
<button @click.once="btnClick">按钮</button>
<!--4.@keydown.enter ,意思是只有按下enter回车键才会触发-->
<input type="text" @keydown.enter="getMsg">
<!--5.只有点击非孩子部分才会触发fatherClick-->
<div :style="{ backgroundColor: '#f88', width:'100px',height:'100px'}" @click.self='fatherClick'>
<button @click='childClick'>click</button>
</div>
</div>
<script src="../js/vue.js"></script>
<script>
// 1.创建Vue的实例对象
const app = Vue.createApp({
data(){
return {
count: 0,
}
},
methods:{
boxClick1(){
console.log('点击了1盒子')
},
btnClick2(event){
console.log('点击了2按钮')
//2.阻止冒泡
event.stopPropagation();
},
doSubmit(){
console.log('表单提交')
//阻止默认事件,如表单提交,a连接跳转。处理我们其他数据等完了,再手动提交
event.preventDefault()
}
}
}).mount('#app');
</script>
</body>
6.4、条件渲染指令
- 当需要在显示与隐藏之间切片很频繁时,使用
v-show
- 当只有一次切换时,通过
v-if
<div id="app">
<--v - if、v-else-if、v-else-->
<p v-if="flag==1">今天要下雨</p>
<p v-else-if="flag==2">今天不要下雨</p>
<p v-else>今天天气很怪</p>
<p v-show="!flag">今天不要下雨</p>
</div>
<script src="../js/vue.js"></script>
<script>
// 1.创建Vue的实例对象
const app = Vue.createApp({
data(){
return{
flag: 1
}
}
}).mount('#app');
</script>
</body>
6.5、列表渲染指令
遍历对象、数组
2.x 版本中在一个元素上同时使用 v-if
和 v-for
时,v-for
会优先作用
3.x 版中 v-if
总是优先于 v-for
生效。
官方推荐我们在使用 v-for时,给对应的元素或组件加上一个 :key属性,key的作用主要是为了高效的更新虚拟DOM
当列表的数据变化时,默认情况下, vue 会尽可能的复用已存在的 DOM 元素,从而提升渲染的性能 。但这种默认的性能优化策略,会导致有状态的列表无法被正确更新 。为了给 vue 一个提示,以便它能跟踪每个节点的身份, 从而在保证有状态的列表被正确更新的前提下, 提升渲染的性能 。此时,需要为每项提供一个唯一的 key 属性
<div id="app">
<ul>
<li v-for="(item,index) in personArr">{{index}} -- {{item}}</li>
</ul>
<ul>
<--遍历对象-->
<li v-for="(item,key,index) in person">{{index}} -- {{key}}:{{item}}</li>
</ul>
</div>
<script src="../js/vue.js"></script>
<script>
// 1.创建Vue的实例对象
const app = Vue.createApp({
data(){
return{
personArr: ['秦晓',20,'Fighting'],
person: {
name: '秦',
age: 20,
friends: ['张三','李四']
}
}
}
}).mount('#app');
</script>
</body>
Diff算法
参考:15张图,20分钟吃透Diff算法核心原理,我说的!!! - 掘金 (juejin.cn)
Diff算法是一种对比算法。对比两者是旧虚拟DOM和新虚拟DOM
,对比出是哪个虚拟节点
更改了,找出这个虚拟节点
,并只更新这个虚拟节点所对应的真实节点
,而不用更新其他数据没发生改变的节点,实现精准
地更新真实DOM,进而提高效率
- 当数据发生改变时,订阅者
watcher
就会调用patch
给真实的DOM
打补丁- 通过
isSameVnode
进行判断,相同则调用patchVnode
方法patchVnode
做了以下操作:
- 找到对应的真实
dom
,称为el
- 如果都有都有文本节点且不相等,将
el
文本节点设置为Vnode
的文本节点- 如果
oldVnode
有子节点而VNode
没有,则删除el
子节点- 如果
oldVnode
没有子节点而VNode
有,则将VNode
的子节点真实化后添加到el
- 如果两者都有子节点,则执行
updateChildren
函数比较子节点updateChildren
主要做了以下操作:
- 设置新旧
VNode
的头尾指针- 新旧头尾指针进行比较,循环向中间靠拢,根据情况调用
patchVnode
进行patch
重复流程、调用createElem
创建一个新节点,从哈希表寻找key
一致的VNode
节点再分情况操作
1、Diff算法是:深度优先算法
。数据改变,会触发setter
,并且通过Dep.notify
去通知所有订阅者Watcher
,订阅者们就会调用patch方法
,给真实DOM打补丁,更新相应的视图。
2、这个方法作用就是,对比当前同层的虚拟节点是否为同一种类型的标签(同一类型的标准,下面会讲)
:是:继续执行patchVnode方法
进行深层比对;否:没必要比对了,直接整个节点替换成新虚拟节点。
sameVnode方法:patch关键的一步就是sameVnode方法判断是否为同一类型节点
,
function sameVnode(oldVnode, newVnode) {
return (
oldVnode.key === newVnode.key && // key值是否一样
oldVnode.tagName === newVnode.tagName && // 标签名是否一样
oldVnode.isComment === newVnode.isComment && // 是否都为注释节点
isDef(oldVnode.data) === isDef(newVnode.data) && // 是否都定义了data
sameInputType(oldVnode, newVnode) // 当标签为input时,type必须是否相同
)
}
3、patchVnode方法做了以下事情:
- 找到对应的
真实DOM
,称为el
- 判断
newVnode
和oldVnode
是否指向同一个对象,如果是,那么直接return
- 如果他们都有文本节点并且不相等,那么将
el
的文本节点设置为newVnode
的文本节点。 - 如果
oldVnode
有子节点而newVnode
没有,则删除el
的子节点 - 如果
oldVnode
没有子节点而newVnode
有,则将newVnode
的子节点真实化之后添加到el
- 如果两者都有子节点,则执行
updateChildren
函数比较子节点,这一步很重要
4、updateChildren方法
这是patchVnode
里最重要的一个方法,新旧虚拟节点的子节点对比,就是发生在updateChildren方法
中,就是首尾指针法
,新的子节点集合和旧的子节点集合,各有首尾两个指针。
然后会进行互相进行比较,总共有五种比较情况:
- 1、
oldS 和 newS
使用sameVnode方法
进行比较,sameVnode(oldS, newS)
- 2、
oldS 和 newE
使用sameVnode方法
进行比较,sameVnode(oldS, newE)
- 3、
oldE 和 newS
使用sameVnode方法
进行比较,sameVnode(oldE, newS)
- 4、
oldE 和 newE
使用sameVnode方法
进行比较,sameVnode(oldE, newE)
- 5、如果以上逻辑都匹配不到,再把所有旧子节点的
key
做一个映射到旧节点下标的key -> index
表,然后用新vnode
的key
去找出在旧节点中可以复用的位置。
v-for循环为什么不建议用index做key?
在进行子节点的 diff算法
过程中,会进行 旧首节点和新首节点的sameNode
对比,这一步命中了逻辑,因为现在新旧两次首部节点
的 key
都是 0
了,同理,key为1和2的也是命中了逻辑,导致相同key的节点
会去进行patchVnode
更新文本,而原本就有的c节点
,却因为之前没有key为4的节点,而被当做了新节点,所以很搞笑,使用index做key,最后新增的居然是本来就已有的c节点。所以前三个都进行patchVnode
更新文本,最后一个进行了新增
,那就解释了为什么所有li标签都更新了。
为什么用了id来当做key就实现了我们的理想效果呢,因为这么做的话,a,b,c节点
的key
就会是永远不变的,更新前后key都是一样的,并且又由于a,b,c节点
的内容本来就没变,所以就算是进行了patchVnode
,也不会执行里面复杂的更新操作,节省了性能,而林三心节点,由于更新前没有他的key所对应的节点,所以他被当做新的节点,增加到真实DOM上去了。
6.6、双向绑定指令
- text 和 textarea 元素使用 value和 input 事件;
- checkbox 和 radio 使用 checked和 change 事件;checkbox分为单选与多选
- select 字段将使用 value 和 change 作为事件。select 分为单选与多选
<body>
<div id="app">
<input type="text" v-model="message">
<!-- 1.等同于 input-->
<input type="text" v-bind:value="message" v-on:input="message = $event.target.value">
<!-- 2.radio单选-->
<label><input name="sex" type="radio" value="男" v-model="sex">男</label>
<label><input name="sex" type="radio" value="女" v-model="sex">女</label>
<!-- 3.checkbox单选 true/false-->
<label><input type="checkbox" v-model="isAgree">同意会员协议</label>
<!-- 4.checkbox多选 value值-->
<input type="checkbox" value="篮球" v-model="hobbies">篮球
<input type="checkbox" value="足球" v-model="hobbies">足球
<input type="checkbox" value="乒乓球" v-model="hobbies">乒乓球
<input type="checkbox" value="羽毛球" v-model="hobbies">羽毛球
<h2>您的爱好是: {{hobbies}}</h2>
<!-- 5.select 单选 value值-->
<select name="city" v-model="city">
<option value="上海">上海</option>
<option value="北京">北京</option>
<option value="天津">天津</option>
</select>
<!-- 6.select 多选 value值-->
<select name="cities" v-model="cities" multiple>
<option value="上海">上海</option>
<option value="北京">北京</option>
<option value="天津">天津</option>
</select>
<p>选择的城市是:{{cities}}</p>
</div>
<script src="../js/vue.js"></script>
<script>
// 1.创建Vue的实例对象
const app = Vue.createApp({
data(){
return {
message: '你好Vue3!',
sex: '男',
isAgree: 'false',
hobbies: [], // 多选框,
city: '上海',
cities: []
}
}
}).mount('#app');
</script>
</body>
v-model修饰符
7、定义指令
定义的指令都要按规定去创建 在bind
和inserted
还有 updated
中去创建。
Vue.directive('focus'{
//每当指令绑定到元素上的时候,会立即执行bind 函数,只执行一次,
注意:在元素刚绑定元素的时候,还没有插入到dom中去,这时候,调用focus方法没有作用,即放在focus 放在bind中是不起作用 的
bind:function(el,binding){
el.style.color=binding.value
},
//表示元素插入到dom中的时候,只执行一次
inserted:function(){
el.focus() js行为放在这里去创建
},
//当组件更新的时候,可能会触发多次
updated:function(){},
})
//局部
directives:{
'指令名':{
bind:function( el,b){
}
}
}
8、Vue 生命周期
从开始创建、初始化数据、编译模版、挂载 Dom -> 渲染、更新 -> 渲染、卸载等一系列过程。
生命周期 | 描述 |
---|---|
beforeCreate | 组件实例被创建之初,组件的属性生效之前 |
created | 组件实例已经完全创建,属性也绑定,但真实 dom 还没有生成,$el 还不可用 |
beforeMount | 在挂载开始之前被调用:相关的 render 函数首次被调用 |
mounted | el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子(处理请求回来的数据) |
beforeUpdate | 组件数据更新之前调用,发生在虚拟 DOM 打补丁之前 |
update | 组件数据更新之后 |
activited | keep-alive 专属,组件被激活时调用 |
deactivated | keep-alive 专属,组件被销毁时调用 |
beforeDestory | 组件销毁前调用 |
destoryed | 组件销毁后调用 |
Vue 的父组件和子组件生命周期钩子函数执行顺序?
加载渲染过程: 父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created ->子 beforeMount -> 子 mounted -> 父 mounted
子组件更新过程: 父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated
销毁过程 : 父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroye
9、computed、methods和 watch 的区别和运用的场景?
1、计算属性会生成一个具有响应式作用的值,具有有缓存效果。初始时,计算属性值会把函数执行一次,把结果存起来,依赖的data改变,函数才会自动被重新执行,否则使用之前的结果。
2.computed计算属性中的函数必须有return返回值,不支持异步,methods函数是每次模板编译都会执行。只要有响应式属性改变,视图刷新,函数就执行。不是必须要用return。一般手动触发。
3.watch是一个观察动作.可以侦听指定名称属性值(data/computed)的变化,属性值一旦发生变化时就会自动触发侦听器,然后侦听器执行相应的业务代码.使用 watch 选项允许我们执行异步操作 ( 访问一个 API )。watch中的函数不是必须要用return。
watch: {
name(newVal, oldVal){
console.log(newVal, oldVal);
}
}
watch: {
"要侦听的属性名": {
immediate: true, // 立即执行
deep: true, // 深度侦听复杂类型内变化
handler (newVal, oldVal) {
}
}}
10、为什么 vue 组件中的 data 是函数而不是对象
- 如果data是函数,每次创建一个新实例后,调用data函数,从而返回初始数据的一个全新副本数据对象。这样每复用一次组件,会返回一份新的data数据,类似于给每个组件实例创建一个私有的数据空间,让各个组件的实例各自独立,互不影响,保持低耦合
- 如果data是一个纯碎的对象,则所有的实例将共享引用同一份data数据对象,无论在哪个组件实例中修改data,都会影响到所有的组件实例
- 根实例对象data可以是对象也可以是函数(根实例是单例),不会产生数据污染情况
- 组件实例对象data必须为函数,目的是为了防止多个组件实例对象之间共用一个data,产生数据污染。采用函数的形式,initData时会将其作为工厂函数都会返回全新data对象
11、nextick原理
11.1、为什么使用nextTick?
因为 vue 采用的异步更新策略,当监听到数据发生变化的时候不会立即去更新DOM,
Vue就会开启一个队列,然后把在同一个事件循环 (event loop) 当中观察到数据变化的watcher推送进这个队列。如果这个watcher被触发多次,只会被推送到队列一次。这种做法带来的好处就是可以可以有效的去掉重复数据与将多次数据更新合并成一次,减少操作DOM的次数。如果不采用这种方法,假设数据改变100次就要去更新100次DOM,而频繁的DOM更新是很耗性能的。
11.2、nextTick 作用?
nextTick 接收一个回调函数作为参数,并将这个回调函数延迟到DOM更新后才执行;
使用场景:想要操作 基于最新数据生成的DOM 时,就将这个操作放在 nextTick 的回调中;
new Vue({
data() {
return {
message: 'Hello, Vue!'
}
},
mounted() {
this.message = 'Modified message'
this.$nextTick(() => {
// 在DOM更新之后执行的操作
console.log(this.$el.textContent) // 输出:"Modified message"
})
}
})
11.3、原理(源码解读???)
nextTick 之所以能够使我们看到 DOM 更新后的结果,是因为我们传入的 callback 会被添加到队列刷新函数(flushSchedulerQueue)的后面,这样等队列内部的更新函数都执行完毕,所有 DOM 操作也就结束了,callback 自然能够获取到最新的 DOM 值。
12、对数组的单个项或对象新属性进行修改
- 数组考虑性能原因没有用 defineProperty 对数组的每一项进行拦截,所以不能使用索引修改。但是使用这 7 种数组(push,shift,pop,splice,unshift,sort,reverse)方法进行进行修改数组数据。
- 给对象直接添加属性this.obj.newproperty="新属性",数据虽然更新了(
console
打印出了新属性),但页面并没有更新。vue2
是用过Object.defineProperty
实现数据响应式,为obj
添加新属性的时候,却无法触发事件属性的拦截。应创建一个新的对象,合并原对象和混入对象的属性。
this.someObject = Object.assign({},this.someObject,{newProperty1:1,newProperty2:2 ...})
13、组件通信
13.1、父组件向子组件通过props传递数据
总结:父组件通过props向下传递数据给子组件。注:组件中的数据共有三种形式:data、props、computed
13.2、子组件通过$emit触发父组件方法来改变数据
//App.vue父组件
<template>
<div id="app">
<users v-bind:users="users"></users>//前者自定义名称便于子组件调用,后者要传递数据名
<users v-on:titleChanged="updateTitle"/>
</div>
</template>
<script>
import Users from "./components/Users"
export default {
name: 'App',
data(){
return{
users:["Henry","Bucky","Emily"],
title:'',
}
},
methods:{
updateTitle(e){ //声明这个函数
this.title = e;
}
},
components:{
"users":Users
}
}
//users子组件
<template>
<div class="hello">
<ul>
<li v-for="user in users">{{user}}</li>//遍历传递过来的值,然后呈现到页面
</ul>
<header>
<h1 @click="changeTitle">{{title}}</h1>//绑定一个点击事件
</header>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props:{
users:{ //这个就是父组件中子标签自定义名字
type:Array,
required:true
},
methods:{
changeTitle() {
this.$emit("titleChanged","子向父组件传值");//自定义事件 传递值“子向父组件传值”
}
},
}
}
</script>
13.3、给组件打上节点,获取组件形式
给子组件使用 ref="box1" 绑定ID,,在父组件里面使用 this.$refs.box1 就可以拿到该组件
子组件访问父组件:使用$parent,如果想访问根组件,可以通过 $root
13.4、借助EventBus事件总线实现组件通信,
通过on来接受消息,emit来推送消息,实现组件通信。最后还要移除总线
- var Event=new Vue();emit事件将数据发送给on事件,
- Event.$emit(事件名,数据);
- Event.$on(事件名,data => {});
- Event.$off('aMsg', {})
13.5、$attrs $listeners跨级组件之间的通讯
参考:Vue 组件间通信六种方式(完整版) - 掘金 (juejin.cn)
-
$attrs
:包含了父作用域中不被 prop 所识别 (且获取) 的特性绑定 (class 和 style 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过 v-bind="$attrs" 传入内部组件。通常配合 inheritAttrs 选项一起使用。 -
$listeners
:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件
//父组件中 A组件(App.vue)
<child1 :pchild1="child1" :pchild2="child2" :pchild3="child3" @refresh="handleRefresh"></child1>
methods: {
handleRefresh(data) {
console.log('刷新', data)
}
}
//B组件(Child1.vue)
<!-- 通过 v-bind 绑定$attrs属性,C组件可以直接获取到A组件中传递下来的props(除了B组件中props声明的) -->
<!-- C组件中能直接触发test的原因在于 B组件调用C组件时 使用 v-on 绑定了$listeners 属性 -->
<child2 v-bind="$attrs" v-on="$listeners"></child2>
props: {
pchild1:{
type:String
}
},
inheritAttrs: false,//默认为true
//C 组件 (Child2.vue)
<button @click="handleBtn">按钮</button>
<p>$attrs: {{ $attrs }}</p>
<p>pchild3Name: {{ $attrs.pchild3.name }}</p>
methods: {
handleBtn() {
this.$emit('refresh', 'CCC')
}
},
13.6、provide/inject
在父组件中通过provider来提供属性,然后在子组件中通过inject来注入变量,不论子组件有多深,只要调用了inject那么就可以注入在provider中提供的数据
// provide提供数据
// 祖父组件
<template>
<div>
...
</div>
</template>
<script>
provide: {
message: '依赖注入'
},
methods: {
...
}
</script>
// inject注入数据
// 孙子组件
<template>
<div>
{{ message }} // 显示依赖注入
</div>
</template>
<script>
inject: ['message'],
methods: {
...
}
</script>
13.7、vuex(vue3再补充pinia)
import Vuex from 'vuex'
import Vue from 'vue'
// 1.安装插件
Vue.use(Vuex)
// 2.创建对象
const store = new Vuex.Store({
state: {
count: 0
},
//当给 state 中的对象添加新属性时,使用下面的方式
mutations: { //通过 mutation 更新
increment(state){
state.count++
},
decrement(state,counter){
state.count += counter
}
// 给 info 赋值一个新的对象
crement(state,payload){
state.count += payload.cCount
}
},
actions: {
//,比如网络请求,必然是异步的,Action 类似于 Mutation,但是是用来代替
//Mutation 进行异步操作的。
crement(context,payload){
setTimeout(() => {
context.commit('crement',payload)
},1000)
}
},
getters: { //Getters作为参数和传递参数
// 获取年龄大于20的学生个数
more20stuLength(state,getters) {
return getters.more20stu.length
},
},
modules: {
b: moduleB
}
// store.state.b -> moduleB 的状态
}
})
// 导出store对象
export default store
<template>
<div id="app">
<p>
<button @click="increment">+1</button>
<button @click="decrement(5))">-1<</button>
</div>
</template>
<script>
export default {
name: 'App',
components: {
},
computed: {
count: function() {
return this.$store.state.count
}
},
methods: {
increment: function() {
this.$store.commit('increment')
},
decrement: function() {
this.$store.commit('decrement',counter)
}
crement(){
this.$store.dispatch('crement', {cCount: 5})
}
}
}
</script>
13.8 、mixin(需要进一步学习)
在日常的开发中,我们经常会遇到在不同的组件中经常会需要用到一些相同或者相似的代码,这些代码的功能相对独立,可以通过 Vue 的 mixin 功能抽离公共的业务逻辑,原理类似“对象的继承”,当组件初始化时会调用 mergeOptions 方法进行合并,采用策略模式针对不同的属性进行合并。当组件和混入对象含有同名选项时,这些选项将以恰当的方式进行“合并”。
- mixin中的生命周期函数会和组件的生命周期函数一起合并执行。
- mixin中的data数据在组件中也可以使用。
- mixin中的方法在组件内部可以直接调用。
- 生命周期函数合并后执行顺序:先执行mixin中的,后执行组件的。
定义:混入 (mixin) 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。一个混入对象可以包含任意组件选项。
使用场景:多个组件中都用到了一些公用的方法、数据时。
// src/components/demo.vue
<template>
<div>mixin中的数据:{{ msg }}</div>
</template>
<script>
import { mixins } from "../mixin/index";
export default {
mixins: [mixins],
};
//引入之后可以当本地方法或数据使用
</script>
Mixin和Vuex的区别
- Vuex公共状态管理,如果在一个组件中更改了Vuex中的某个数据,那么其它所有引用了Vuex中该数据的组件也会跟着变化。
- Mixin中的数据和方法都是独立的,组件之间使用后是互相不影响的。
14、vue的单向数据流
Vue 在数据操作上支持单向绑定和双向绑定:
- 单向绑定:例如 Mustache 插值语法,v-bind 等;
- 双向绑定:即表单的 v-model。它实际上是一个语法糖,背后包括两步操作:
- v-bind:value:model 层的更改同步到 view 层
- v-on:input:view 层的更改同步到 model 层
单向数据流
单向数据流指只能从一个方向来修改状态。Vue 是单向数据流 —— 也就是说,数据总是从父组件传到子组件,子组件没有权利修改父组件传过来的数据,只能请求父组件对原始数据进行修改。
15、SPA单页面的理解
SPA
是一种网站的模型,只有一张Web页面的应用,是加载单个HTML 页面,并在用户与应用程序交互时动态更新该页面的Web应用程序。公共资源(js、css等)仅需加载一次,单页面跳转仅刷新局部资源 ,常用于PC端官网、购物等网站,页面在任何时间点都不会重新加载。首次渲染速度相对较慢。
多页面跳转刷新所有资源,每个公共资源(js、css等)需选择性重新加载,常用于 app 或 客户端等
单页面应用(SPA) | 多页面应用(MPA) | |
---|---|---|
组成 | 一个主页面和多个页面片段 | 多个主页面 |
刷新方式 | 局部刷新 | 整页刷新 |
url模式 | 哈希模式 | 历史模式 |
SEO搜索引擎优化 | 难实现,可使用SSR方式改善 | 容易实现 |
数据传递 | 容易 | 通过url、cookie、localStorage等传递 |
页面切换 | 速度快,用户体验良好 | 切换加载资源,速度慢,用户体验差 |
维护成本 | 相对容易 | 相对复杂 |
实现一个SPA
监听地址栏中hash
变化驱动界面变化
参考:面试官:你对SPA单页面的理解,它的优缺点分别是什么?如何实现SPA应用呢 | web前端面试 - 面试官系列 (vue3js.cn)
// 定义 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
16、SSR解决了什么问题?
传统web开发
网页内容在服务端渲染完成,⼀次性传输到浏览器。浏览器拿到的是全部的dom
结构
单页应用
单页应用优秀的用户体验,使其逐渐成为主流,页面内容由JS
渲染出来,这种方式称为客户端渲染
服务端渲染SSR
后端渲染出完整的首屏的dom
结构返回,前端拿到的内容包括首屏及完整单页面结构,应用激活后依然按照单页面方式运行
SSR主要解决了以下两种问题:
- seo:搜索引擎优先爬取页面
HTML
结构,使用ssr
时,服务端已经生成了和业务想关联的HTML
,有利于seo
- 首屏呈现渲染:用户无需等待页面所有
js
加载完成就可以看到页面视图(压力来到了服务器,所以需要权衡哪些用服务端渲染,哪些交给客户端)
如何实现(需进一步学习)
对于同构开发,我们依然使用webpack
打包,我们要解决两个问题:服务端首屏渲染和客户端激活
这里需要生成一个服务器bundle
文件用于服务端首屏渲染和一个客户端bundle
文件用于客户端激活