Vue实例挂载

一、思考 我们都听过知其然知其所以然这句话 那么不知道大家是否思考过new Vue()这个过程中究竟做了些什么? 过程中是如何完成数据的绑定,又是如何将数据渲染到视图的等等 一、分析 首先找到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) } } 仔细阅读上面的代码,我们得到以下结论: ...

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

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

计算属性

前言 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

侦听器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

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

生命周期

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

组件通信

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

插槽

一、插槽的几种使用方式 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

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