前端模块化

一、模块化的理解 1.什么是模块? 将一个复杂的程序依据一定的规则(规范)封装成几个块(文件), 并进行组合在一起 块的内部数据与实现是私有的, 只是向外部暴露一些接口(方法)与外部其它模块通信 2.模块化的进化过程 全局function模式 : 将不同的功能封装成不同的全局函数 编码: 将不同的功能封装成不同的全局函数 问题: 污染全局命名空间, 容易引起命名冲突或数据不安全,而且模块成员之间看不出直接关系 function m1(){ //... } function m2(){ //... } namespace模式 : 简单对象封装 作用: 减少了全局变量,解决命名冲突 问题: 数据不安全(外部可以直接修改模块内部的数据) let myModule = { data: 'www.baidu.com', foo() { console.log(`foo() ${this.data}`) }, bar() { console.log(`bar() ${this.data}`) } } myModule.data = 'other data' //能直接修改模块内部的数据 myModule.foo() // foo() other data 这样的写法会暴露所有模块成员,内部状态可以被外部改写。 IIFE模式:匿名函数自调用(闭包) 作用: 数据是私有的, 外部只能通过暴露的方法操作 编码: 将数据和行为封装到一个函数内部, 通过给window添加属性来向外暴露接口 问题: 如果当前这个模块依赖另一个模块怎么办? // index.html文件 <script type="text/javascript" src="module.js"></script> <script type="text/javascript"> myModule.foo() myModule.bar() console.log(myModule.data) //undefined 不能访问模块内部数据 myModule.data = 'xxxx' //不是修改的模块内部的data myModule.foo() //没有改变 </script> // module.js文件 (function(window) { let data = 'www.baidu.com' //操作数据的函数 function foo() { //用于暴露有函数 console.log(`foo() ${data}`) } function bar() { //用于暴露有函数 console.log(`bar() ${data}`) otherFun() //内部调用 } function otherFun() { //内部私有的函数 console.log('otherFun()') } //暴露行为 window.myModule = { foo, bar } //ES6写法 })(window) 最后得到的结果: ...

October 5, 2024

v-if/v-show

v-show原理 export const vShow: ObjectDirective<VShowElement> = { beforeMount(el, { value }, { transition }) { el._vod = el.style.display === 'none' ? '' : el.style.display if (transition && value) { transition.beforeEnter(el) } else { setDisplay(el, value) } }, mounted(el, { value }, { transition }) { if (transition && value) { transition.enter(el) } }, updated(el, { value, oldValue }, { transition }) { // ... }, beforeUnmount(el, { value }) { setDisplay(el, value) } } 有transition就执行transition,没有transition就直接设置display。不管初始条件是什么,元素总是会被渲染。 v-if原理 export const transformIf = createStructuralDirectiveTransform( /^(if|else|else-if)$/, (node, dir, context) => { return processIf(node, dir, context, (ifNode, branch, isRoot) => { // ... return () => { if (isRoot) { ifNode.codegenNode = createCodegenNodeForBranch( branch, key, context ) as IfConditionalExpression } else { // attach this branch's codegen node to the v-if root. const parentCondition = getParentCondition(ifNode.codegenNode!) parentCondition.alternate = createCodegenNodeForBranch( branch, key + ifNode.branches.length - 1, context ) } } }) } ) 返回一个node节点,render函数通过表达式的值来决定是否生成DOM ...

October 4, 2024

插槽

一、插槽的几种使用方式 1. 普通插槽 举例来说,这里有一个 <FancyButton> 组件,可以像这样使用: <FancyButton> Click me! <!-- 插槽内容 --> </FancyButton> 而 <FancyButton> 的模板是这样的: <button class="fancy-btn"> <slot></slot> <!-- 插槽出口 --> </button> <slot> 元素是一个 插槽出口 (slot outlet),标示了父元素提供的 插槽内容 (slot content) 将在哪里被渲染。 最终渲染出的 DOM 是这样: <button class="fancy-btn">Click me!</button> 2. 具名插槽 有时在一个组件中包含多个插槽出口是很有用的。举例来说,在一个 <BaseLayout> 组件中,有如下模板: <div class="container"> <header> <!-- 标题内容放这里 --> </header> <main> <!-- 主要内容放这里 --> </main> <footer> <!-- 底部内容放这里 --> </footer> </div> 对于这种场景, <slot> 元素可以有一个特殊的 attribute name,用来给各个插槽分配唯一的 ID,以确定每一处要渲染的内容: ...

October 4, 2024

组件通信

Vue3 组件通信方式 props $emit expose / ref $attrs v-model provide / inject Vuex mitt Vue3 通信使用写法 1. props 用 props 传数据给子组件有两种方法,如下 方法一,setup() 方法写法 // Parent.vue 传送 <child :msg1="msg1" :msg2="msg2"></child> <script> import child from "./child.vue" import { ref, reactive } from "vue" export default { data(){ return { msg1:"这是传级子组件的信息1" } }, setup(){ // 创建一个响应式数据 // 写法一 适用于基础类型 ref 还有其他用处,下面章节有介绍 const msg2 = ref("这是传级子组件的信息2") // 写法二 适用于复杂类型,如数组、对象 const msg2 = reactive(["这是传级子组件的信息2"]) return { msg2 } } } </script> // Child.vue 接收 <script> export default { props: ["msg1", "msg2"],// 如果这行不写,下面就接收不到 setup(props) { console.log(props) // { msg1:"这是传给子组件的信息1", msg2:"这是传给子组件的信息2" } }, } </script> 方法二,setup 语法糖 ...

October 4, 2024

生命周期

vue3生命周期 在Vue中,生命周期指的是Vue实例从创建到销毁的过程。在这个过程中,Vue实例会经历一系列的阶段,包括数据观测、模板编译、挂载DOM、更新DOM、卸载等等。在这些阶段,Vue提供了一些生命周期钩子函数,让我们可以在特定的时机执行某些代码。 为什么需要生命周期 生命周期钩子函数为我们提供了在Vue实例生命周期不同阶段执行代码的能力。这对于非响应式的操作非常有用,例如直接操作DOM、添加事件监听器或者创建定时器等。这些操作如果在数据变化时执行可能会导致不必要的性能开销,而生命周期钩子函数让我们可以更合理地安排这些操作的执行时机。一些常用的库,例如react,webpack都提供了生命周期的钩子,方便开发者理解整个运行流程。 Vue3 有哪些生命周期,分别在什么时候调用? Vue3的生命周期钩子函数主要包括: beforeCreate: 创建实例之前调用,此时实例的数据观测、事件等还未初始化。 created: 创建实例后调用,此时实例的数据观测、事件等已经初始化完成。 beforeMount: 挂载实例之前调用,此时模板已经编译完成,但是还未挂载到DOM。 mounted: 挂载实例后调用,此时实例已经挂载到DOM,可以进行DOM操作。 beforeUpdate: 数据更新之前调用,此时可以进行状态的最后更改。 updated: 数据更新后调用,此时DOM已经完成更新,可以进行DOM操作。 beforeUnmount: 卸载实例之前调用,此时实例仍然完全可用。 unmounted: 卸载实例后调用,此时实例已经完全卸载。 生命周期类型的枚举位于packages/runtime-core/src/enums.ts目录下: export const enum LifecycleHooks { BEFORE_CREATE = 'bc', // 创建之前 CREATED = 'c', // 创建 BEFORE_MOUNT = 'bm', // 挂载之前 MOUNTED = 'm', // 挂载 BEFORE_UPDATE = 'bu', // 更新之前 UPDATED = 'u', // 更新 BEFORE_UNMOUNT = 'bum', // 卸载之前 UNMOUNTED = 'um', // 卸载之后 DEACTIVATED = 'da', // 被 keep-alive 缓存的组件停用时调用。 ACTIVATED = 'a', // 被 keep-alive 缓存的组件激活时调用。 RENDER_TRIGGERED = 'rtg', // 依赖被收集时调用 RENDER_TRACKED = 'rtc', // 依赖被触发时调用 ERROR_CAPTURED = 'ec', // 错误捕获 SERVER_PREFETCH = 'sp' } 上面生命周期可以用一张图来表示: 选项式和组合式生命周期的区别 在Vue3中,我们可以使用选项式API和组合式API来定义组件。这两种API对应的生命周期钩子函数有一些区别。 选项式API的生命周期钩子函数定义在组件选项对象的methods中,例如: export default { methods: { created() { // do something } } } 组合式API的生命周期钩子函数定义在setup函数中,需要使用import从vue中引入,例如: import { onMounted } from 'vue'; ​ export default { setup() { onMounted(() => { // do something }); } } 需要注意的是,在组合式API中没有beforecate和created钩子,因为他们本身被setup这个函数替代了,直接把相关逻辑写在setup中就行了 ...

October 4, 2024

methods

Methods 的初始化 在 Vue.js 中,每个组件实例(vm)都会有一个 methods 属性,这个属性包含了组件中定义的所有方法。这些方法需要被初始化到实例上,以便可以在模板和生命周期钩子中被调用。初始化过程如下: function initMethods(vm, methods) { for (var key in methods) { vm[key] = methods[key] == null ? noop : bind(methods[key], vm); } } 这段代码遍历 methods 对象中的每个属性,并将其复制到 Vue 实例(vm)上。如果方法不存在(methods[key] == null),则赋值为一个空函数(noop),以避免对 undefined 的调用。否则,使用 bind 方法将每个方法的 this 绑定到 Vue 实例上。 Methods 作用域的固定 在 JavaScript 中,函数的 this 值是在函数被调用时确定的,而不是在定义时。这可能导致在 Vue 实例的方法中 this 并不指向实例本身,尤其是在方法被传递给其他函数或作为回调函数时。Vue 使用 bind 来解决这个问题,确保 this 总是指向 Vue 实例。 原生和兼容性 bind 实现 Vue.js 提供了两种 bind 的实现:原生的和兼容性的。兼容性实现是为了兼容那些不支持 Function.prototype.bind 的旧浏览器。 function polyfillBind(fn, ctx) { function boundFn(a) { var l = arguments.length; return l ? (l > 1 ? fn.apply(ctx, arguments) : fn.call(ctx, a)) : fn.call(ctx); } boundFn._length = fn.length; return boundFn; } function nativeBind(fn, ctx) { return fn.bind(ctx); } var bind = Function.prototype.bind ? nativeBind : polyfillBind; polyfillBind 函数创建了一个新的函数 boundFn,它在调用时使用 call 或 apply 将 fn 的 this 绑定到 ctx 上。 nativeBind 函数直接使用原生的 bind 方法。 bind 变量根据浏览器是否支持原生 bind 来选择使用哪种实现。 使用 bind 的好处 使用 bind 后,我们可以在组件的方法中直接使用 this 来访问实例的属性和方法,而不用担心 this 的指向问题。这使得代码更加简洁和安全。 ...

October 4, 2024

侦听器watch

一、 watch API 我们先看看 watch的使用例子,watch 有多种用法,可以接收多种类型参数: import { reactive, watch } from 'vue' // 1. 传入getter函数 const state = reactive({ count: 0 }) watch(() => state.count, (count, prevCount) => { // 当 state.count 更新,会触发此回调函数 }) // 2. 传入reactive对象 watch(state, (count, prevCount) => { // 当 state.count 更新,会触发此回调函数 }) // 3. 传入ref对象 const stateRef = ref(0) watch(stateRef, (count, prevCount) => { // 当 stateRef.value 更新,会触发此回调函数 }) // 4.监听多个数据源,回调函数接受两个数组,分别对应来源数组中的新值和旧值: watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => { /* ... */ }) 从上面的例子可以看到,对于 watch,它能接收的第一个参数类型非常多。 你可以传入一个ref对象、一个响应式对象、一个 getter 函数、甚至是一个数组。 我们在上一篇中知道了computed对象其内部是借助了 effect函数 创建了一个 reactiveEffect函数,在访问computed对象的值时,执行其 runner函数 求值。 ...

October 4, 2024

计算属性

前言 Vue 的计算属性(computed)是 Vue.js 中非常重要的一个概念。它允许你在 Vue 组件中定义一些计算逻辑,这些逻辑会依赖于 Vue 的响应式数据,并且具有缓存机制,只有在依赖数据发生改变时才会重新计算,从而提高了性能。 计算属性的主要作用包括: 数据转换 数据过滤 响应式依赖 缓存机制 简化模板 在本篇文章中,我们将详细探寻如何利用计算属性优化 Vue 组件的数据处理和渲染逻辑,深入了解 Vue 计算属性的精髓。 一. 认识计算属性 1. 什么是计算属性 Vue.js 中的计算属性(computed properties)是 Vue 实例中非常重要的一个特性。它们用于对 Vue 实例的数据进行动态计算,且具有缓存机制,只有在相关依赖发生改变时才会重新计算。这种特性使得计算属性非常适合用于处理模板中的逻辑。 2. 计算属性的定义 计算属性是在 Vue 实例的computed选项中定义的,可以是一个函数或具有get和set方法的对象。函数形式的计算属性会在调用时被执行,而对象形式的计算属性则可以提供自定义的get和set方法。 computed: { // 函数形式的计算属性 reversedMessage: function () { return this.message.split('').reverse().join(''); }, // 对象形式的计算属性 fullName: { get: function () { return this.firstName + ' ' + this.lastName; }, set: function (newValue) { var names = newValue.split(' '); this.firstName = names[0]; this.lastName = names[names.length - 1]; } } } 3. 计算属性的依赖 计算属性可以依赖于 Vue 实例的响应式数据。当依赖数据发生改变时,相关的计算属性会重新计算。 ...

October 4, 2024

vue指令

1.1 钩子函数 一个指令定义对象可以提供如下几个钩子函数 (均为可选): bind 只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。 inserted 被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。 update 所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。 componentUpdated 指令所在组件的 VNode 及其子 VNode 全部更新后调用。 unbind 只调用一次,指令与元素解绑时调用。 Vue.directive('gqs',{ bind() { // 当指令绑定到 HTML 元素上时触发.**只调用一次** console.log('bind triggerd') }, inserted() { // 当绑定了指令的这个HTML元素插入到父元素上时触发(在这里父元素是 `div#app`)**.但不保证,父元素已经插入了 DOM 文档.** console.log('inserted triggerd') }, updated() { // 所在组件的`VNode`更新时调用. console.log('updated triggerd') }, componentUpdated() { // 指令所在组件的 VNode 及其子 VNode 全部更新后调用。 console.log('componentUpdated triggerd') }, unbind() { // 只调用一次,指令与元素解绑时调用. console.log('unbind triggerd') } }) 1.2 钩子函数参数 指令钩子函数会被传入以下参数: ...

October 4, 2024

数据绑定

1. 定义 1.1 双向绑定 & 单向绑定 vue的双向绑定,即数据与视图的响应式设计。具体表现为:View的改变能实时让Model发生变化,而Model的变化也能实时更新View。 什么情况下用户可以更新View呢?如:填写表单。当用户填写表单时,View的状态就被更新了,如果此时MVVM框架可以自动更新Model的状态,那就相当于把Model和View做了双向绑定。 注意区别:单向数据绑定,所有数据只有一份,一旦数据变化,就去更新页面(只有data–>DOM,没有DOM–>data)。 用户在页面作出更新,需要用户手动收集(双向绑定是自动收集),在合并到原有的数据中。 Vue.js中的v-model主要用在表单的input输入框,完成视图和数据的双向绑定: <!DOCTYPE html> <html> <head></head> <body> <div id="app"> <input type="text" v-model="message"> <p>{{message}}</p> </div> <script> var app = new Vue({ el: '#app', data: { message: '' } }); </script> </body> </html> 1.2 v-model来添加双向绑定 <input v-model="xxx"> <!-- 上面的代码等价于 --> <input :value="xxx" @input="xxx = $event.target.value"> <!-- 双向绑定 = 单向绑定 + UI事件监听 --> 1.3 双向绑定、单向绑定的优缺点 单向绑定:数据流也是单向的,对于复杂应用来说是实施统一状态管理(如redux)的前提。 双向绑定:在一些需要实时反应用户输入的场合会非常方便(如多级联动菜单)。但常认为复杂应用中这种便利比不上引入状态管理带来的优势。因为不知道状态什么时候发生改变,是谁造成的改变,数据变更也不会通知。 响应式的思路:mvvm Model,View,View-Model 2. 原理概述 Vue 数据双向绑定原理是通过 数据劫持 + 发布者-订阅者模式 的方式来实现的,首先是通过 ES5 提供的 Object.defineProperty() 方法来劫持(监听)各属性的 getter、setter,并在当监听的属性发生变动时通知订阅者,是否需要更新,若更新就会执行对应的更新函数。详见 vue源码。 ...

October 4, 2024