声明:本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!
vue
我们应该学什么?最近,发现掘金
vue3
的教程逐渐增多,让我感慨颇深,偶然间想起一句 至理名言
鄙视**vue
** 理解**vue
** 成为**vue
**
vue3
之所以能成为,三大框架之一,到底是为什么?
我记得我在之前的文章中说过,因为好用
可是,虽然好用,也得分什么人用,怎么用
不信?
我举几个例子
1、你有没有在某个深夜,用reactive
拉取了一个接口的然后赋值之后,发现视图并没有任何变化,然后加班加点排查
const obj = reactive({});
requst().then((res) => {
obj = res;
});
2、你有没有好奇过,为什么 我在模板语法中,不需要用.value
而在js 的代码中,有需要加上 .value
<script setup>
import { ref } from 'vue'
const count = ref(0)
function increment() {
count.value++
}
</script>
<template>
<button @click="increment">
{{ count }} // 不需要调用.value
</button>
</template>
3、你有没有疑问,别人都说vue3
比vue2
好用,到底好在哪? 不就是api、生命周期变化? 怎么我用着感觉一样呢, 甚至还不如2
4、你有没有在某一天学了es6解构赋值,突然想在vue3
里头用一次, 发现vue不让
,你必须这样,你有没有想过为啥吗?
<template>
<div>
<h1>{{ title }}</h1>
<p>{{ description }}</p>
</div>
</template>
<script>
import { ref } from 'vue';
export default {
props: {
title: String,
description: String
},
setup(props) {
// 解构赋值使用props中的数据,必须要用toRefs
const { title, description } = toRefs(props);
return {
title,
description
};
}
};
</script>
5、你有没有思考过,nextTick
和 updated
那个先执行?
所以,想要用好vue
你光看文档是不行的 就要鄙视他,理解他,成为他
说人话,就是你要去死记硬背文档的内容,不是不行,而是万万的不行,文档就静静的躺在那里,你可以随时去翻。
而我们要做的,就是深刻的理解,vue
底层的一些设计思路,从而推导出 ,上层的表现,
也就是所谓的授人以鱼不如授人以渔
所以,我们打通任督二脉的关键点就是,要理解哪些设计思路
,这才是关键。
骥某人不才,vue3
也写过两年,源码也看过一点, 略懂,略懂,
就献丑帮大家筛选下,我们到底要理解哪些原理/思想 ,其实说白了就是想通一些事!
说起响应式原理,其实就是一个老生常谈的问题,而且我之前也写过文章,文章在这,无非就是一个Proxy
简单的使用方式如下
// 使用Proxy实现完美劫持
const obj = { name: "vue", arr: [1, 2, 3] };
function proxyData(value) {
const proxy = new Proxy(value, {
get(target, key) {
console.log(`get key is ${key}`);
const val = target[key];
if (typeof val === "object") {
return proxyData(val);
}
return val;
},
set(target, key, value) {
console.log(`set key is ${key}, value is ${value}`);
return (target[key] = value);
},
deleteProperty(target, key) {
console.log(`delete key is ${key}`);
},
});
return proxy;
}
const proxy = proxyData(obj);
proxy.age = 18; // 可对新增属性进行劫持
delete proxy.name; // 可对删除属性进行劫持
proxy.arr.push(4); // 可对数组的push等方法进行劫持
proxy.arr[3] = 4; // 可对象数组的索引操作进行劫持
代码写完了,
然而我要说的是,这玩意除了面试的时候能在面试官
面前显摆一下,屁用没有,因为你依然不了解,模板
是怎么 跟响应式
有关系的,为啥响应式
的值改变了,会影响模重新渲染,这才是你理解整个vue3
的关键
说到这
我们就可以回答第一个问题用**reactive
** 拉取了一个接口的然后赋值之后,发现视图并没有任何变化
其实原因也很简单,我们看到的并不一定是我们看到的
,
很多人思考问题,容易陷入一种思维惯性,比如
let obj = reactive({});
在上述代码中,我们按照之前的思维逻辑思考问题,很容易陷入一种惯性,obj
就是响应式,我不论如何对obj
做更改,他都应该是响应式的
然而事实是, obj
并不是响应式,他存的地址对应的 对象才是响应式的,他存的仅仅是响应式的地址而已,引用类型
这是我们入行的时候必看的课题之一
所以,当我们对于obj 做更改
obj ={a:1}
其实此时地址被更换了,响应式的对象自然就没了, 而obj 就不再是响应式的了,那么请问,你还怎么能更新模板呢?
神奇不,真实的情况就这么绕!!
而我们正确的做法,不让这个响应式地址丢失
obj = Object.assign(obj,{a:1})
理解了这个问题,我们就能理解es6解构赋值
为什么会破坏响应式,其实本质上异曲同工
好,接下来,我们开始正题
响应式
页面联动说起这个问题,我们要先从模板的历史讲起。。。。
讲起模板语法的历史,其实其中蕴含着的互联网技术发展历史,如果我要从源头讲起,我相信,我会讲的很累,大家也会听的很累。
所以我决定,用一句话解决问题
所谓模板语法,本质就是在刀耕火种的互联网年代,想要**html
** 的数据能够变成动态的而产生的一种规范,仅此而以
既然是规范,那么就可以借鉴,于是vue
毫不犹豫,拿了过来
拿是拿过来了, 为什么要拿过来呢?vue借鉴项目github如下[github.com/fb55/htmlpa…]
我相信,很多人都没有想过这个问题,我想过
其实原因很简单,为了能快速上手
相信用过react
的人都知道,想要直接在浏览器引用一个react
的包就能跑起来,是相当费劲的 ,想要不学习就能凭借之前的经验和直觉,就能写出代码,是没指望的 ,想要不用脚手架,就能写个简单项目,是不可能的
不信?
请看代码:
<div id="root"></div>
<script src="https://cdn.bootcdn.net/ajax/libs/react/16.11.0/umd/react.production.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/react-dom/16.11.0/umd/react-dom.production.min.js"></script>
// 竟然还要用babel
<script src="https://unpkg.com/@babel/standalone@7.18.4/babel.min.js"></script>
<script type="text/babel">
class App extends React.Component {
constructor() {
super()
this.state = {
num: 1
}
}
add = () => {
this.setState({
num: this.state.num + 1
})
}
render() {
return <button onClick={this.add} >
{this.state.num}
</button>
}
}
window.onload = function () {
ReactDOM.render(<App />, document.querySelector('#root'))
}
</script>
接下来, 我们再来反观vue
就上手的难度来看,高下立判!
<div id="app">{{ message }}</div>
<script type="module">
import { createApp, ref } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js'
createApp({
setup() {
const message = ref('Hello Vue!')
return {
message
}
}
}).mount('#app')
</script>
这就是模板语法的优势,直接在html
中修改变量,更符合传统的编码直觉
接下来第二个问题来了,数据怎么跟模板实现联动的呢?
先上官网图,其实,之所以模板能够和数据联动,究其原因很简单, 模板被转换了,变成了一个函数,因为只有这样我们才能在数据变动的时候去执行函数、更新界面,从而实现联动
有了这个前提,我们不需要去看vue
源码就能很轻松的理解,
其实很简单,每一个响应式的值,保存着跟他相关的render
函数,仅此而已,当值被更新的时候,顺带的跟他相关的render函数
,也就是模板被重新执行,实现页面更新
理解了这个原理,我们就能从代码中打断点来解决业务中遇到的问题,因为浏览器中就是编译后的render
函数
我们在vite
项目中,利用浏览器来对,render函数打断点
如图:
我们只需要在需要拿到值的地方断点即可
这就是我们理解了所谓的模板,就是render函数之后的魔力
当然,你如果觉得render
断点之后,想要看到结果是非常麻烦的,我们依然可以利用render函数
的特性,既然他是个函数,那么当然可以console.log
啊
于是
<template>
<div>假设我们要看到数据并且打印出来,看看模板中的问题{{ log(loginMsg) }}</div>
</template>
<script lang="ts" setup>
import { ref } from "vue";
const loginMsg = ref('');
const log = (loginMsg) => {
console.log(loginMsg)
};
于是我们就能清晰的看到模板中的每一个值,并且从而更容易的排查问题!!
理解了render
函数的真面目,我们就能回答在文章开头疑惑的问题,为什么我们在模板中不需要使用.value
呢?
其实原因很简单,render
函数是编译来的,既然是编译,那就能包装,于是,就诞生了一个取值的逻辑,来处理模板中的.value
值
如图,有一个toDisplayString
方法
代码如下:
export const toDisplayString = (val: unknown): string => {
// 判断了值类型
return isString(val)
? val
: val == null
? ''
: isArray(val) ||
(isObject(val) &&
(val.toString === objectToString || !isFunction(val.toString)))
? // 保证他是一个字符串 第二个参数是为了过滤内容,最终拿到相对应的value的值
JSON.stringify(val, replacer, 2)
: String(val)
}
const replacer = (_key: string, val: any): any => {
if (val && val.__v_isRef) {
// 这个地方处理了.value
return replacer(_key, val.value)
} else if (isMap(val)) {
return {
[`Map(${val.size})`]: [...val.entries()].reduce(
(entries, [key, val], i) => {
entries[stringifySymbol(key, i) + ' =>'] = val
return entries
},
{} as Record<string, any>,
),
}
} else if (isSet(val)) {
return {
[`Set(${val.size})`]: [...val.values()].map(v => stringifySymbol(v)),
}
} else if (isSymbol(val)) {
return stringifySymbol(val)
} else if (isObject(val) && !isArray(val) && !isPlainObject(val)) {
// native elements
return String(val)
}
return val
}
hooks
在vue3
出来以前,是react
的特权,很多人只知道,要用hooks
却不知道,用他的目的,
其实他的目的很简单,代码逻辑复用
而vue2
,其实一直没有解决这个问题,所以vue3
迫切的需要解决这个问题 于是vue
版本的hooks
横空出世
当然,他还有一个更高端的名字,composition api
在开始讲 composition api
之前,我们先温习一下,vue2
是怎么实现逻辑复用的
vue2
逻辑服用,之前使用的是Mixin
混入 (mixin) 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。一个混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。
代码如下:
// 混入内容
export const mixins = {
data() {
return {
msg: "这是值",
};
},
computed: {},
created() {
console.log("我是mixin中的created生命周期函数");
},
mounted() {
console.log("我是mixin中的mounted生命周期函数");
},
methods: {
clickMe() {
console.log("我是mixin中的点击事件");
},
},
};
// 使用混入
// src/App.vue
<template>
<div id="app">
<img alt="Vue logo" src="./assets/logo.png" />
<button @click="clickMe">点击我</button>
</div>
</template>
<script>
import { mixins } from "./mixin/index";
export default {
name: "App",
mixins: [mixins],
components: {},
created(){
console.log("组件调用minxi数据",this.msg);
},
mounted(){
console.log("我是组件的mounted生命周期函数")
}
};
</script>
不知各位jym
看到上述代码,有没有什么想法?
所以,发现了吗,Mixin
他不好用,出现的问题比解决的问题还多,于是 composition api
横空出世
他是为了解决什么问题呢?
解决,**react
** 有,而**vue
**没有的问题!!
额!!!开玩笑!
他本质是为了解决mixin
的这些缺点,当然,也是为了看齐react hooks
的能力,毕竟,实现逻辑复用有很多种,他偏偏选了这种
自己去细品
在开始之前我们先了解一下composition api
怎样使用呢?
其实所谓composition api
就是一些响应式方法,比如ref
、computed
、ref
、watch
、reactive
等。。。 例如
import { ref, defineComponent, computed } from "vue";
export default defineComponent({
setup() {
//composition api ref
const count = ref(0);
const color = ref("red");
const changeColor = () =>
(color.value = color.value === "red" ? "green" : "red");
return () => (
<div>
<div style={{ color: color.value }}>{count.value}</div>
<button onClick={changeColor}>改变颜色</button>
</div>
);
},
});
那么composition api
怎么实现逻辑复用呢?
很简单,用一个方法封装,将composition api
导出来,不就可以了吗?
如此一来 我们每次在setup
中初始化当前方法,就相当于初始化一套新的composition api
的响应式结果
完美的解决了Mixin
的所有问题
例如:
// 封装axios 请求实现逻辑复用
// 每次请求
import { ref } from "vue";
import axios from "axios";
export default function useFetch(url) {
const data = ref(null);
const error = ref(null);
const isLoading = ref(true);
axios.get(url)
.then(response => {
data.value = response.data;
})
.catch(errorResponse => {
error.value = 'Error: ' + errorResponse.response.status + ' - ' + errorResponse.response.statusText;
})
.finally(() => {
isLoading.value = false;
});
return {
data,
error,
isLoading
};
}
从上述代码中我们可以看出useFetch
可以到处复用,并且不会产生污染,他只会在当前setup
中独一份的生效!!
这就是composition api
逻辑复用的魅力!!
composition api
的组合式 与传统声明式的对比vue3
的使用方式,俗称组合式
就是利用 composition api
的特点,在setup
中任意自由组合,来解决vue2
中规矩太多的问题,也就是达到react
灵活的特点
我们从一张图中可以看出
如上图所示
composition api
由于任意自由组合
的特点,我们可以将相同的逻辑可以写在同一个地方,这些逻辑甚至可以使用函数抽离出来,各个逻辑之间界限分明,方便维护
当然,这有一个前提,他是需要修炼的
而反观声明式
的Options API 由于限制和规矩,导致你需要严格的按照规则书写,这反倒成了新手的天堂,毕竟想象力,和归纳能力,不是初出茅庐
就能有的。
他需要大量的训练和总结
到了这,我们就能顺利的解释之前的第三个问题都说**vue3
**比**vue2
**好用,到底好在哪?
答案其实很简单,抛开性能的提升,vue3最大的提升
俩字自由
从此,你就可以不再受限制,展翅高飞,随意的提炼设计你的代码,自由这俩字其实就是程序员编程的乐趣,不是吗?
这也是很多技术大佬喜欢react
的原因,因为他实在太自由了。
翱翔!!!!
接下来,我们来探讨最后一个问题,我们发现几乎所有的框架都有生命周期,这是为什么呢?
要理解这个问题,我们就需要追本溯源,从框架为什么叫做框架说起
所谓框架,其实就是一套开发规范,我们按照规范写代码,框架将规范的代码转换执行组成我们的界面,如此而已
其实我们也可以,自己用原生写,只是他会很浪费时间,因为没有规范,代码就会很凌乱,所谓没有规矩,不成方圆,就是如此
所谓,vue
、react
、 anglar
都是如此
这么多年来,无数的人,比来比去,骂来骂去,本质上,就是比较那种规范更好用,仅此而已!!
而要实现这种规范,框架就需要给我们做大量的工作,来承接规范到可执行代码的中间地带,而生命周期
,就是这种承接所必备的模块
原因很简单,既然是规范就不能随心所欲,但又要让用户能完成功能,所以在每个时期就得能让用户操作自己的代码
在vue
中总的来说有那么几个生命周期
1、beforeCreate: 组件实例刚被创建,组件属性计算之前。
2、created: 组件实例刚被创建,属性已绑定,但 Dom 还未生成。
3、beforeMount: 模板编译/挂载之前。在挂载开始之前被调用,相关的 render 函数首次被调用,实例已完成以下的配置: 编译模板,把 data 里面的数据和模板生成 html,此时注意还没有挂载到 Html 页面上。
4、mounted: 模板编译/挂载之后。在 el 被新创建的 vm.el 替换,并挂载到实例上去之后被调用,实例已完成以下的配置:用上面编译好的的 html 内容替换 l 属性指向的 Dom 对象。完成模板中的 html 渲染到 html 页面中。此过程进行 ajax 交互。
5、beforeUpdate: 组件更新之前。在数据更新之前调用,发生在虚拟 Dom 重新渲染打补丁之前,可以在钩子函数中进一步的更改状态,不会出大附加的重渲染过程。
6、updated: 组件更新之后。在由于数据更改导致的虚拟 Dom 重新渲染和打补丁之后调用。调用是,组件 Dom 已经更新,所以可以执行依赖于 Dom 的操作,然而在大多数的情况下,应该避免在此期间更改状态,因为这可能会导致更新无限循环,该钩子函数在服务器端渲染期间不被调用。
7、beforeDestroy: 组件销毁前调用。在示例销毁之前调用,实例仍然完全可用。
8、destroyed: 组件销毁后调用。 在实例销毁之后调用。调用后,所有的时间监听会被移除,所有的子实例也会被销毁,该钩子函数在服务器端渲染器件不被调用
好了,片汤话讲完了,这些都是文档上的话,翻翻看看即可。
接下来,讲点正题, 还是之前的那个问题 nextTick
**和** updated
**那个先执行?**
这其实是一个非常有意思的问题,因为都是 dom 更新之后调用,那么顺序到底是什么呢?
我们可以用以下代码来佐证
<template>
<div >
<div @click="b">{{ a }}</div>
</div>
</template>
<script setup lang="ts">
import { ref, onUpdated, nextTick } from "vue";
let a = ref(1);
nextTick(() => {
console.log(444444444);
});
onUpdated(() => {
console.log(22222222);
});
nextTick(() => {
console.log(333333333);
});
const b = () => {
a.value++;
nextTick(() => {
console.log(1111);
});
};
</script>
// 444444444
// 333333333
// 点击之后
// 22222222
// 1111
我们知道,onUpdated
和nextTick
最后都会推入微任务队列 中,只是前后顺序的问题。
从以上代码可以看出,onUpdated
方法 在初始化之后,应该是没有被推入微任务队列
, 而是保存在了某个地方,
不然,初始化之后,应该是打印三个值,而现今只有两个,
而当我点击之后发现, onUpdated
先打印, nextTick
后打印,则可说明, 在点击的时候, onUpdated
的函数 先被推入微任务队列, nextTick
随后
这样,我们就能不通过,查看源码来合理的分析出,整个vue
底层生命周期的运行轨迹,并且熟练运用在自己的业务中