系统网站建设需求分析,浅析图书馆门户网站建设,医疗网站建设效果,网站制作建设公司Vue3相比大家也都有所了解#xff0c;即使暂时没有使用上#xff0c;但肯定也学习过#xff01;Vue3是使用TS进行重写#xff0c;采用了MonoRepo的管理方式进行管理#xff0c;本篇文章我们一起来看看 Vue3的使用#xff0c;与Vue2有什么区别#xff0c;以及我们该如何优… Vue3相比大家也都有所了解即使暂时没有使用上但肯定也学习过Vue3是使用TS进行重写采用了MonoRepo的管理方式进行管理本篇文章我们一起来看看 Vue3的使用与Vue2有什么区别以及我们该如何优雅的去使用【中】篇会从源码的角度去学习【下】篇主要是讲解Vue3的高频面试题开始正文吧 文章目录一、Vue2 与 Vue3响应式对比1. Vue2 的 Object.defineProperty2. Vue.set() 为什么可以解决上述问题他具体经历了那些步骤你知道吗3. 如何实现一个简单的 Vue2 响应式 2. Vue3 的 Proxy二、Vue3 新特性Composition API1. 如何理解 setup 2. 多根节点单文件的多根节点项目的多根节点——多个应用实例3. reactive() 与 shallowReactive()4. ref()、isRef() 、toRef()、toRefs()5. readOnly()、isReadonly() 、shallowReadonly()6. 生命周期7. 全局配置8. 异步组件9. Teleport9. 自定义 Hook举个栗子自定义一个 Hook 来记录鼠标的位置一、Vue2 与 Vue3响应式对比
Vue2 与 Vue3 最显著的差别就是响应式的差别那么是什么原因导致 Vue3 的双向绑定原理采用了Proxy我们下面来由浅入深的去了解一下。
1. Vue2 的 Object.defineProperty
基础使用
const initData { value: 1 };
const data {};
Object.keys(initData).forEach(key {Object.defineProperty(data, key, {get() {console.log(访问了, key);},set(v) {console.log(修改了, key);data[key] v;}})
})data.value;
data.value 2;
data;
initData.value2 2;
data.value2;以上就是最基础的使用 但是我们一起来看一下下面几个问题会输出什么
直接访问 data.value 访问了 value 改变 data.value 修改了 value 直接输出 data 空对象: { } 给 initData 添加一个新值 输出新值结果2 data.value2 又会输出什么 undefined 总结一下 Vue2 响应式弊端给对象加属性和删除属性响应式会检测不到。通常我们是使用 Vue.set( ) 来解决那么面试官问 Vue.set( ) 为什么可以解决他具体经历了那些步骤你知道吗 2. Vue.set() 为什么可以解决上述问题他具体经历了那些步骤你知道吗
Vue.set(target, key, value)
// target 必须是一个响应式的数据源在下面步骤会讲到会经历一下三个步骤
对 target 进行数据校验 ① 数据是 undefined、null或其他基本数据类型会报错 ② 数据是 数组则会取出当前数组的长度与当前 key 值的位置进行一个对比取两者最大值作为新数组的长度 - max(target.length, key) 然后使用 splice(key, 1, value); 当使用 splice 的时候会自动遍历设置响应式。 ③ 数据是 对象key 是否在对象里如果在则直接替换如果不在则直接判断 target 是不是响应式对象然后判断是不是 Vue 实例或者根的数据对象如果是 throw error。如果不是直接给 target 的 key 赋值如果 target 是响应式使用 defineReactive 将新的属性添加到 target进行依赖收集
Vue.set( ) 源码
// example :
this.$set(data, a, 1);
function set(target: Arrayany object, key: any, val: any): any {// isUndef 是判断 target 是不是等于 undefined 或者 nul1// isPrimitive 是判断 target 的数据类型是不是 string、number、symbol、boolean 中的一种if (process.env.NODE ENV ! production (isUndef(target) isPrimitive(target))) {warn(Cannot set readtive property on undefined, null, or primitive value: $((target: any)))}// 数组的处理if (Array.isArray(target) isValidArrayIndex(key)) {target.length Math .max(target .length, key)target.splice(key1, val)return val}// 对象并且该属性原来已存在于对象中则直接更新if (key in target !(key in object.prototype)) {target[key] valreturn val}// vue给响应式对象(比如 data 里定义的对象)都加了一个 ob 属性,// 如果一个对象有这个 ob属性那么就说明这个对象是响应式对象修改对象已有属性的时候就会触发页面渲染// 非 data 里定义的就不是响应式对象。const ob (target: any).__ob__if (target. isVue (ob ob.vmCount)) {process.env.NODE ENV ! production warn(Avoid adding reactive properties to a Vue instance or its root $data at runtime - declare it upfront in the data option.!return val}// 不是响应式对象if (!ob) {target[key] valreturn val}// 是响应式对象进行依赖收集defineReactive(ob.value, key val)// 触发更新视图ob.dep.notify()return val
}3. 如何实现一个简单的 Vue2 响应式
export function Vue(options) {this.__init(options);
}// initMixin
Vue.prototype.__init function (options) {this.$options options;// 假如这里是一个字符串就需要使用 document.querySelector 去获取this.$el options.el;this.$data options.data;this.$methods options.methods;// beforeCreate -- initState -- initDataproxy(this, this.$data);// Object.definePropertyobserver(this.$data);new Compiler(this);
};// this.$data.message --- this.message
function proxy(target, data) {let that this;Object.keys(data).forEach((key) {Object.defineProperty(target, key, {enumerable: true,configurable: true,get() {return data[key];},set(newVal) {// 考虑 NaN 的情况// this 指向已经改变if (!isSameVal(data[key], newVal)) {data[key] newVal;}},});});
}function observer(data) {new Observer(data);
}class Observer {constructor(data) {this.walk(data);}walk(data) {if (data typeof data object) {Object.keys(data).forEach((key) this.defineReactive(data, key, data[key]));}}//要把 data 里面的数据收集起来defineReactive(obj, key, value) {let that this;this.walk(value);let dep new Dep();Object.defineProperty(obj, key, {enumerable: true,configurable: true,get() {// get时 Dep 收集依赖// 4. 对于 num 来说就要执行这一句// 5. num 中的 dep就有了这个 watcherDep.target dep.add(Dep.target);return value;},set(newVal) {if (!isSameVal(value, newVal)) {//赋值进来的新值是没有响应式的所以我要在 walk 一次添加响应式value newVal;that.walk(newVal);// 重新 set时notify 通知更新// 6.dep.notify();}},});}
}// 视图怎么更新
// 数据改变视图才会更新。需要去观察
// 1. new Watcher(vm, num, (){ 更新视图上的 num 显示 })
class Watcher {constructor(vm, key, callback) {this.vm vm; // VUE 的一个实例this.key key;this.callback callback;// 2. 此时 Dep.target 作为一个全局变量理解放的就是就是 watcherDep.target this;// 3. 一旦进行了这一句赋值是不是就触发了这个值的 getter 函数this.__old vm[key];Dep.target null;}// 8. 执行所有的 callback 函数update() {let newVal this.vm[this.key];if (!isSameVal(newVal, this.__old)) this.callback(newVal);}
}// 每一个数据都要有一个 Dep 依赖
class Dep {constructor() {this.watchers new Set();}add(watcher) {if (watcher watcher.update) this.watchers.add(watcher);}// 7. 让所有的 watcher 执行 update 方法notify() {this.watchers.forEach((watch) watch.update());}
}class Compiler {constructor(vm) {this.vm vm;this.el vm.$el;this.methods vm.$methods;this.compile(vm.$el);}// 这里是递归编译 #app 下面的所有的节点内容compile(el) {let childNodes el.childNodes;// childNodes 为类数组Array.from(childNodes).forEach((node) {// 判断如果是文本节点if (node.nodeType 3) {this.compileText(node);}// 判断如果是元素节点else if (node.nodeType 1) {this.compileElement(node);}// 判断如果还有子节点就递归下去if (node.childNodes node.childNodes.length) this.compile(node);});}compileText(node) {// 匹配出来 messagelet reg /\{\{(.?)\}\}/;let value node.textContent;if (reg.test(value)) {let key RegExp.$1.trim();// 开始时赋值node.textContent value.replace(reg, this.vm[key]);// 给 message 添加观察者new Watcher(this.vm, key, (val) {// 数据改变时更新node.textContent val;});}}compileElement(node) {if (node.attributes.length) {Array.from(node.attributes).forEach((attr) {let attrName attr.name;if (attrName.startsWith(v-)) {// v- 指定匹配成功可能是 v-on:click 或者 v-model// 假设我们这里就处理两个指令Vue源码对这一块是有特殊处理的attrName attrName.indexOf(:) -1? attrName.substr(5): attrName.substr(2);let key attr.value;this.update(node, key, attrName, this.vm[key]);}});}}update(node, key, attrName, value) {if (attrName model) {node.value value;new Watcher(this.vm, key, (val) (node.value val));node.addEventListener(input, () {this.vm[key] node.value;});} else if (attrName click) {node.addEventListener(attrName, this.methods[key].bind(this.vm));}}
}function isSameVal(a, b) {return a b || (Number.isNaN(a) Number.isNaN(b));
} Vue2 的响应式我们简单介绍一下下来一起来看 Vue3 的 Proxy 2. Vue3 的 Proxy Proxy代理或拦截器Proxy 可以理解成在目标对象之前架设一层“拦截”外界对该对象的访问都必须先通过这层拦截因此提供了一种机制可以对外界的访问进行过滤和改写Proxy的思路和 React 的 HOC 很像组件外面包裹一层对外界的访问进行过滤和改写 const initData {value:1};
const proxy new Proxy(initData, {get(target, key, receiver) {console.log(访问了, key);return Reflect.get(target, key, receiver);},set(target, key, value, receiver){console.log(修改了, key);return Reflect.set(target, key, value, receiver);}
})proxy.value;
proxy.value 2;
proxy;
proxy.value2 2;
proxy.value2;具体这里就不详细说了感兴趣的大家可以移步下面链接 Vue3中的响应式原理为什么使用Proxy(代理) 与 Reflect(反射)
二、Vue3 新特性
Composition API composition api : 组合式 api,通过组合式 API我们可以使用导入的 API 函数来描述组件逻辑。在单文件组件中组合式 API 通常会与 script setup 搭配使用。这个 setup attribute 是一个标识告诉 Vue 需要在编译时进行一些处理让我们可以更简洁地使用组合式 API。比如 script setup 中的导入和顶层变量/函数都能够在模板中直接使用。 1. 如何理解 setup 通俗一点的讲setup 可以把他理解为 Vue3 组件模块的入口文件Vue3 中组件的新特性 作为组件统一的入口支持 未使用 setup 语法糖的写法 (了解即可,实际开发还是使用语法糖写法更加便捷):
setup(props, context){context.attrs -- this.$attrscontext.slot -- this.$slotcontext.emit -- this.$emitcontext.expose
}使用 setup 语法糖写法
script setup
// 变量
const msg Hello!// 函数
function log() {console.log(msg)
}
/scripttemplatebutton clicklog{{ msg }}/button
/template为什么推荐使用 setup 语法糖
更少的样板内容更简洁的代码。能够使用纯 TypeScript 声明 props 和自定义事件。更好的运行时性能 (其模板会被编译成同一作用域内的渲染函数避免了渲染上下文代理对象)。更好的 IDE 类型推导性能 (减少了语言服务器从代码中抽取类型的工作)。
setup 是在 beforeCreate 和 created 之前去执行
2. 多根节点 什么是多根节点呢看下图代码 单文件的多根节点
vue3 中之所以可以有多个节点是因为引入了Fragment的概念这是一个抽象的节点如果发现组件有多个根就创建一个Fragment节点把多个根节点作为它的children,将来path的时候如果发现是一个Fragement节点则直接遍历children创建或更新。
项目的多根节点——多个应用实例 应用实例并不只限于一个。createApp API 允许你在同一个页面中创建多个共存的 Vue 应用而且每个应用都拥有自己的用于配置和全局资源的作用域。 如果你正在使用 Vue 来增强服务端渲染 HTML并且只想要 Vue 去控制一个大型页面中特殊的一小部分应避免将一个单独的 Vue 应用实例挂载到整个页面上而是应该创建多个小的应用实例将它们分别挂载到所需的元素上去。
3. reactive() 与 shallowReactive() reactive通过 proxy 声明一个深层的响应式对象响应式是深层次的会影响所有嵌套。 等同于 Vue2 的 Vue.observable() const person {name: Barry,age: 18,contacts: {phone: 1873770}
}const personReactive reactive(person);
console.log(personReactive); // proxy
const contacts personReactive.contacts;
console.log(contacts); // proxyshallowReactive和 reactive() 不同这里没有深层级的转换一个浅层响应式对象里只有根级别的属性是响应式的。
const person {name: Barry,age: 18,contacts: {phone: 1873770}
}const personshallowReactive shallowReactive(person);
console.log(personshallowReactive); // proxy
const contactsShallowReactive personshallowReactive.contacts
console.log(contactsShallowReactive); // no proxy4. ref()、isRef() 、toRef()、toRefs() ref返回一个响应式的、可更改的 ref 对象此对象只有一个指向其内部值的属性 .value 所以我们想要修改 ref 声明的响应式数据需要带上 .value ; 如果将一个对象赋值给 ref 那么这个对象将通过 reactive() 转为具有深层次响应式的对象。这也意味着如果对象中包含了嵌套的 ref 它们将被深层地解包。 const count ref(10);
const cObj reactive({a: 100,count
})console.log(cObj.count);
console.log(cObj.count count.value);
count.value 20;
console.log(count.value, cObj.count);
cObj.count 30;
console.log(count.value, cObj.count);isRef检查某个值是否为 ref。 使用 ref 或者 reactive 声明的响应式数据通过 结构会失去响应式 解决办法 1. 使用 ref 声明的响应式可以通过 toRef() API 2. 使用 reactive 声明的响应式可以通过 toRefs API toRef基于响应式对象上的一个属性创建一个对应的 ref。这样创建的 ref 与其源属性保持同步改变源属性的值将更新 ref 的值 const state reactive({foo: 1,bar: 2
})const fooRef toRef(state, foo)// 更改该 ref 会更新源属性
fooRef.value
console.log(state.foo) // 2// 更改源属性也会更新该 ref
state.foo
console.log(fooRef.value) // 3 toRefs将一个响应式对象转换为一个普通对象这个普通对象的每个属性都是指向源对象相应属性的 ref。每个单独的 ref 都是使用 toRef() 创建的。 const state reactive({foo: 1,bar: 2
})const stateAsRefs toRefs(state)
/*
stateAsRefs 的类型{foo: Refnumber,bar: Refnumber
}
*/// 这个 ref 和源属性已经“链接上了”
state.foo
console.log(stateAsRefs.foo.value) // 2stateAsRefs.foo.value
console.log(state.foo) // 3
5. readOnly()、isReadonly() 、shallowReadonly() readOnly类似于 Object.freeze() 的效果把一个响应式对象变成一个只读的对象只读代理是深层的对任何嵌套属性的访问都将是只读的。会递归的去阻止 Proxy set 的触发【中】篇会从源码的角度去学习 中间会讲到 readOnly 源码是如何处理的 const original reactive({ count: 0 })const copy readonly(original)watchEffect(() {// 用来做响应性追踪console.log(copy.count)
})// 更改源属性会触发其依赖的侦听器
original.count// 更改该只读副本将会失败并会得到一个警告
copy.count // warning! isReadonly检查传入的值是否为只读对象。只读对象的属性可以更改但他们不能通过传入的对象直接赋值。 通俗一点讲就是检查你传入的值是否由 readonly 创建出来的 function isReadonly(value: unknown): booleanshallowReadonly readonly() 的浅层作用形式 这里没有深层级的转换只有根层级的属性变为了只读。属性的值都会被原样存储和暴露这也意味着 值为 ref 的属性不会被自动解包了 。 const state shallowReadonly({foo: 1,nested: {bar: 2}
})// 更改状态自身的属性会失败
state.foo// ...但可以更改下层嵌套对象
isReadonly(state.nested) // false// 这是可以通过的
state.nested.bar
6. 生命周期 新版的生命周期函数可以按需导入到组件中且只能在 setup 函数中使用但是也可以在 setup 外定义在 setup 中使用 setup 是围绕 beforeCreate 和 created 生命周期钩子运行的所以不需要显式的定义它们。换句话说在这些钩子中编写的任何代码都应该直接在 setup 函数中编写。 选项式 API组合式 APIbeforeCreateNot needed*createdNot needed*beforeMountonBeforeMountmountedonMountedbeforeUpdateonBeforeUpdateupdatedonBeforeUpdatebeforeUnmountonBeforeUnmountummountedonUnmountederrorCapturedonErrorCapturedrenderTrackedonRenderTrackedrenderTriggeredonRenderTriggeredactivatedonActivateddeactivatedonDeactived
注意若要在 setup 中引入需要 vue 中引入对应 hook 官网生命周期钩子地址
7. 全局配置 Vue2 中我们通常是使用 Vue.property.xxx xxx 的方式定义 第一个弊端 全局配置很容易意外的污染其他测试用例 第二个弊端 全局配置使得同一个页面上的多个“应用”在全局配置不同时共享同一个 Vue 副本非常困难 Vue3 我们是利用 app.config.globalProperties.xxx xxx 的方式来实现的。通过 Vue 实例上 config 来配置包含Vue应用程序全局配置的对象您可以在挂载应用程序之前修改对应的属性 具体点击右侧链接查看Vue3中全局配置 axios 的两种方式 可以在应用程序内的任何组件实例中访问的全局属性组件的属性将具有优先权。同时可以在组件通过 getCurrentInstance() 来获取全局 globalProperties 中配置的信息 getCurrentInstance 用于获取当前的组件实例然后通过 ctx 属性获得当前上下文这样我们就可以在 setup 中使用。 const app Vue.createApp({});
app.config {......}
app.config.globalProperties.$htpp xxx;app.config.errorhandler (err, vm, info) {}const { ctx } getCurrentInstance();
ctx.$http8. 异步组件 异步组件在大型项目中我们可能需要拆分应用为更小的块并仅在需要时再从服务器加载相关组件。Vue 提供了 defineAsyncComponent 方法来实现此功能 全局注册
// 可以利用返回值的实例去自定义异步组件在那个应用里生效在那个应用里注册
const AsyncComp defineAsyncComponent(() import(./components/AsyncComp.vue));
app.component(async-comp, AsyncComp)局部注册
// main.js
const AsyncComp defineAsyncComponent(() import(./components/AsyncComp.vue));
// app.vue
import AsyncComp from ./components/AsyncComp.vue;
{
components: async-comp, AsyncComp
}异步组件的作用
打包后不会集成在 index.js 中会单独进行打包方便后续操作可以进行缓存如多个页面都使用一个相同的组件可以将打包文件缓存下来如果组件包过大可以使用 loading 代替显示 Vue3 支持 suspense Suspense 是一个内置组件用来在组件树中协调对异步依赖的处理。它让我们可以在组件树上层等待下层的多个嵌套异步依赖项解析完成并可以在等待时渲染一个加载状态。 Suspense 组件有两个插槽#default 和 #fallback。两个插槽都只允许一个直接子节点。在可能的时候都将显示默认槽中的节点。否则将显示后备槽中的节点。 在 React V16.6.0 中官方提出了lazy 和 suspense 组件 Suspense!-- 具有深层异步依赖的组件 --Dashboard /!-- 在 #fallback 插槽中显示 “正在加载中” --template #fallbackLoading.../template
/Suspense
9. Teleport Teleport 是一个内置组件可以将子节点渲染到存在于父组件以外的 DOM 节点的方案 当处理某些类型的组件(如模式通知或提示) 时模板HTML的逻辑可能位于与我们希望染元素的位置不同的文件中。 很多时候与我们的 Vue 应用程序的 DOM 完全分开处理时这些元素的管理要容易得多。所有这些都是因为处理嵌套组件的位置z-index 和样式可能由于处理其所有父对象的范围而变得棘手。这种情况就是 Teleport 派上用场的地方。我们可以在逻辑所在的组件中编写模板代码这意味着我们可以使用组件的数据或 props。 但是然后完全将其渲染到我们Vue应用程序的范围之外。
button clickopen trueOpen Modal/buttonTeleport tobodydiv v-ifopen classmodalpHello from the modal!/pbutton clickopen falseClose/button/div
/Teleport
9. 自定义 Hook Vue3 的 hooks 其实可以参考 React 的自定义 hooks 的定义在 React 中在函数组件中保留 state 数据的同时融入生命周期函数将组件整体作为一个钩子函数。 当组件复杂时多个组件中一些重复的逻辑可以被抽象出来。在 Hook 诞生之前React 和 Vue 都拥有高阶组件的设计模式在 React 使用到 HOC在 Vue 2 中使用到 mixin。为什么要舍弃它们而使用 Hook使用自定义 Hook又有哪些优点我们先简单了解一下 HOC 和 mixin 对比后便知。 HOC 的原理是把组件作为参数传入一个函数加入复用部分后将新的组件作为返回值使用了装饰器模式。mixin 像是把复用的部分拆解成一个个小零件某个组件需要时就拼接进去。 在实践中mixin 有如下缺点: 1.引入了隐式依赖关系。 2不同 mixins之间可能会有先后顺序甚至代码冲突覆盖的问题 3.mixin 代码会导致滚雪球式的复杂性 4多个 mixin 导致合并项不明来源 为了避开这些问题React 采用 HOC但它依然存在缺陷 1.一个组件的state影响许多组件的props 2.造成地狱嵌套 不过使用全新的 Hook 组件结构可以实现平铺式调用组件的复用部分解决了 mixin 的来源不明和 HOC 的地狱嵌套问题。
举个栗子自定义一个 Hook 来记录鼠标的位置 Tips: 一般我们的自定义 Hook 都需要使用 use 开头 // src/hooks/useMousePosition. ts
import { ref, onMounted, onUnmounted, Ref } from vue
function useMousePosition() {const x ref(0)const y ref(0)const updateMouse (e) {x.value e.pageXy.value e.pageY}onMounted(() {document.addEventListener(click, updateMouse)})onUnmounted(() {document.removeEventListener(click, updateMouse)})return { x, y }
}
export default useMousePosition
templatedivpX: {{ x }}/ppY: {{ y }}/p I/div
/template
script langts
import { defineComponent} from vue
//引入hooks
import useMousePosition from ../ ../hooks/useMousePosition
export default defineComponent({setup () {//使用hooks功能const { x, y} useMousePosition()return {X,}}
})
/script 结语【Vue3 核心模块源码解析(上)】到此结束此篇还是以Vue2的部分回顾加上Vue3的新特性的基础使用以及部分见解有不对的地方欢迎大家及时指出本文到此结束