前端面试总结-Vue

277 阅读18分钟

Vue的核心是什么

  • 数据驱动

    专注于View 层。它让开发者省去了操作DOM的过程,只需要改变数据。

  • 组件响应原理

    数据(model)改变驱动视图(view)自动更新

  • 组件化

    扩展HTML元素,封装可重用的代码。

MVC和MVVM的区别

MVC全名是 Model View Controller,时模型 - 视图 - 控制器的缩写,一种软件设计典范。

  • Model(模型):是用于处理应用程序数据逻辑部分。通常模型对象负责在数据库中存取数据。
  • View(视图):是应用程序中处理数据显示的本分。通常视图是依据模型数据创建的。
  • Controller(控制器):是应用程序处理用户交互的部分。通常控制器负责从视图读取数据,控制用户输入,并向模型发送数据。

MVC的思想:一句话描述就是Controller负责将Model的数据用View显示出来,换句话说就是在Controller里面把Model的数据赋值给View。

MVVM Model-View-ViewModel

新增了VM类

ViewModel层:做了两件事达到了数据的双向绑定,一是将模型转化成视图(数据绑定,数据劫持),二是将视图转化为模型,即将所看到的页面转化为后端的数据,实现方式是DOM事件监听

MVVM和MVC最大的区别就是:实现了View和Model的自动同步,也就是当Model的属性改变时,我们不用再手动操作Dom元素来改变View的显示,而是改变属性后该属性对应的View层会自动改变。整体来看,MVVM比MVC精简很多,不仅简化了业务与界面的依赖,还解决了数据频繁更新的问题,不用再用选择器选择DOM元素。因为在MVVM中,View不知道Model的存在,Model和ViewModel也察觉不到View,这种低耦合模式提高了代码的可重复性

!!!!Vue并未完全遵循MVVM的思想,严格的MVVM要求View不能和Model直接通信,而Vue提供了$refs这个属性,让Model可以直接操作View

为什么data是一个函数

组件的data写成一个函数,数据以函数返回值形式定义,这也每复用一次组件,就会返回一分新的的data,类似给每个组件实例创建了一个私有的数据空间,让各个组件实例维护各自的数据。如果写成对象形式,这就使得所有组件实例共用一份data,就会造成一个变了全都变得结果

new vue的时候发生了什么

Vue 初始化主要就干了几件事情,合并配置,初始化生命周期,初始化事件中心,初始化渲染,初始化 data、props、computed、watcher 等等,最后初始化完成检测到如果有 el 属性,则调用 vm.$mount 方法挂载 vm,挂载的目标就是把模板渲染成最终的 DOM;

Vue组件通信有哪些方式

1、props 和 emit。父组件向子组件传递数据是通过props传递的,子组件传递给父组件是通过emit。父组件向子组件传递数据是通过props传递的,子组件传递给父组件是通过emit触发事件来做到的。

2、parentparent 和 children 获取单签组件的父组件和当前组件的子组件。

3、attrsattrs 和 listeners A -> B -> C。Vue2.4开始提供了attrsattrs和listeners来解决这个问题。

4、父组件中通过 provide 来提供变量,然后在子组件中通过 inject 来注入变量。(官方不推荐在实际业务中适用,但是写组件库时很常用。)

5、$refs 获取组件实例。

6、envetBus 兄弟组件数据传递,这种情况下可以使用事件总线的方式。

7、vuex 状态管理。

Vue的生命周期方法有哪些?一般在哪一步发送请求?

beforeCreate 在实例初始化之后,此阶段为实例初始化之后,this指向创建的实例,此时数据观察事件机制都未形成,不能获得DOM节点,在当前阶段 data、methods、computed 以及 watch 上的数据和方法都不能被访问。

created 实例已经创建完成之后被调用。在这一步,实例已经完成以下的配置:数据观测(data observe ),属性和方法的运算,watch/event 事件回调。这里没有 el,如果非要想与DOM进行交互,可以通过vm.el,如果非要想与 DOM 进行交互,可以通过vm.nextTick 来访问 DOM。

beforeMount 在挂载开始之前被调用:相关的 render 函数首次被调用。vue实例的$el和data都已初始化,此时为虚拟的DOM节点,data.message未替换(此阶段生成虚拟DOM

mounted 在挂载完成后发生,在当前阶段,真实的 Dom 挂载完毕,数据完成双向绑定,可以访问到 Dom节点。

beforeUpdate 数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁 (patch)之前。可以在这个钩子中进一步地更改状态,这不会触发附加的重渲染过程。

updated 发生在更新完成之后,当前阶段组件 Dom 已经完成更新。要注意的是避免在此期间更新数据,因为这个可能导致无限循环的更新,该钩子在服务器渲染期间不被调用。

beforeDestroy 实例销毁之前调用。在这一步,实例仍然完全可用。我们可以在这时进行 善后收尾工作,比如清除定时器。

destroy 组件销毁之后调用,对data的改变不会再触发周期函数,vue实例已解除事件监听和DOM绑定,但DOM结构依然存在

activated keep-alive 专属,组件被激活时调用

deactivated keep-alive 专属,组件被销毁时调用

第一次页面加载时触发:beforeCreate、created、beforeMount、mounted

异步请求在哪一步发起?

可以在钩子函数 created、beforeMount、mounted 中进行异步请求,因为在这三个钩子函数中,data已经创建,可以将服务器端返回的数据进行赋值。

如果有DOM操作,那么肯定在mounted时候才能操作

如果异步请求不需要依赖 DOM 推荐加载 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:

  • 能更快获取到服务端数据,减少页面loading时间;
  • ssr 不支持 beforeMount、mounted 钩子函数,所以放在 created 中有助于一致性。

Vue从后端获得的数据写在created里面比较好

  • created是发生在mounted之前的,template还没有被渲染成html,但是在这个阶段已经可以获取到props和data了,所以在这一步进行数据赋值是比较恰当的。
  • mounted就是页面已经渲染完成了,template已经被渲染成html,这个时候再进行数据请求,很有可能会造成页面闪屏。

$set的用法

受 ES5 的限制,Vue.js 不能检测到对象属性的添加或删除。因为 Vue.js 在初始化实例时将属性转为 getter/setter,所以属性必须在 data 对象上才能让 Vue.js 转换它,才能让它是响应的。

要处理这种情况,我们可以使用$set()方法,既可以给data对象新增属性,又可以触发视图更新。

用法:this.$set(this.data,”key”,value’)

因为响应式数据 我们给对象和数组本身新增了ob属性,代表的是 Observer 实例。当给对象新增不存在的属性,首先会把新的属性进行响应式跟踪 然后会触发对象 ob 的dep收集到的 watcher 去更新,当修改数组索引时我们调用数组本身的 splice 方法去更新数组。

v-if和v-show的区别

v-if 在编译过程中会被转化成三元表达式,条件不满足时不渲染此节点。

v-show 会被编译成指令,条件不满足时控制样式将此节点隐藏(display:none)

扩展补充:display:none 、 visibility:hidden 和 opacity:0 之间的区别?

三者公共点都是隐藏。不同点:

  • 一、是否占据空间。 display:none,隐藏之后不占位置;visibility:hidden、opacity:0,隐藏后任然占据位置。
  • 二、子元素是否继承。 display:none --- 不会被子元素继承,父元素都不存在了,子元素也不会显示出来。 visibility:hidden --- 会被子元素继承,通过设置子元素 visibility:visible 来显示子元素。 opacity:0 --- 会被子元素继承,但是不能设置子元素 opacity:0 来先重新显示。
  • 三、事件绑定。 display:none 的元素都已经不存在了,因此无法触发他绑定的事件。 visibility:hidden 不会触发他上面绑定的事件。 opacity:0 元素上面绑定的事件时可以触发的。
  • 四、过度动画。 transition对于display是无效的。 transition对于visibility是无效的。 transition对于opacity是有效的。

说说vue内置指令

  • v-once - 定义它的元素或组件只渲染一次,包括元素或组件的所有节点,首次渲染后,不再随数据的变化重新渲染,将被视为静态内容。
  • v-cloak - 这个指令保持在元素上直到关联实例结束编译 -- 解决初始化慢到页面闪动的最佳实践。
  • v-bind - 绑定属性,动态更新HTML元素上的属性。例如 v-bind:class。
  • v-on - 用于监听DOM事件。例如 v-on:click v-on:keyup
  • v-html - 赋值就是变量的innerHTML -- 注意防止xss攻击
  • v-text - 更新元素的textContent
  • v-model - 1、在普通标签。变成value和input的语法糖,并且会处理拼音输入法的问题。2、再组件上。也是处理value和input语法糖。
  • v-if / v-else / v-else-if。可以配合template使用;在render函数里面就是三元表达式。
  • v-show - 使用指令来实现 -- 最终会通过display来进行显示隐藏
  • v-for - 循环指令编译出来的结果是 -L 代表渲染列表。优先级比v-if高最好不要一起使用,尽量使用计算属性去解决。注意增加唯一key值,不要使用index作为key。
  • v-pre - 跳过这个元素以及子元素的编译过程,以此来加快整个项目的编译速度。

怎样理解Vue的单项数据流

数据总是从父组件传到子组件,子组件没有权利修改父组件传过来的数据,只能请求父组件对原始数据进行修改。这样会防止从子组件意外改变父组件的状态,从而导致你的应用的数据流向难以理解。

注意:在子组件直接用 v-model 绑定父组件传过来的 props 这样是不规范的写法,开发环境会报警告。

如果实在要改变父组件的 props 值可以再data里面定义一个变量,并用 prop 的值初始化它,之后用$emit 通知父组件去修改。

什么是SPA,优缺点是什么

SPA(single-page application),单页面应用,即一个web项目只有一个页面(即一个HTML文件,HTML内容的变换是利用路由机制实现的)

仅在Web页面初始化时加载相应的HTML、JavaScript和CSS,一旦页面加载完成,SPA不会因为用户的操作而进行页面的重新加载或跳转;取而代之的是利用路由机制实现HTML内容的变换,UI与用户的的交互,避免页面的重新加载

优点:

  • 用户体验好、块,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染
  • SPA相对来说对服务器压力小
  • 前后端职责分离,架构清晰,前端进行交互逻辑,后端负责数据处理

缺点:

  • 初次加载耗时多:为实现单页面web应用功能及显示效果,需要在加载页面的时候将js,css统一加载,部分页面按需加载
  • 前进后退路由管理:由于单页面应用在一个页面中显示所有的内容,所以不能使用浏览器的前进后退,开发的时候肯定可以,但是单页面一般放在移动端在中,页面的切换需要自己简历堆栈管理
  • SEO难度较大,由于所有的内容都在一个页面中动态替换显示,所以在SEO上其有着天然的弱势

computed和watch的区别和运用的场景

computed 是计算属性,依赖其它属性计算值,并且 computed 的值有缓存,只有计算值变化才会返回内容,他可以设置getter和setter。

watch 监听到值的变化就会执行回调,在回调中可以进行一系列的操作。

计算属性一般用在模板渲染中,某个值是依赖其它响应对象甚至是计算属性而来;而侦听属性适用于观测某个值的变化去完成一段复杂的业务逻辑。

v-if和v-for为什么不建议一起使用

v-for和v-if不要在同一标签中使用,因为解析时先解析v-for在解析v-if。如果遇到需要同时使用时可以考虑写成计算属性的方式。

eg:

有一个数组,只渲染里面大于0的项,如果用v-for+v-if的话,v-for的优先级更高,会造成每渲染一个dom之前都要判断一次浪费性能,可以将其用cumpute来进行过滤

Vue3.0用过吗?了解多少?

  • 响应式原理的改变 Vue3.x 使用 Proxy 取代 Vue2.x 版本的 Object.defineProperty。
  • 组件选项声明方式 Vue3.x 使用 Composition API setup是Vue3.x新增的一个选项,他是组件内使用Composition API 的入口。
  • 模板语法变化 slot 具名插槽语法,自定义指令v-model升级。
  • 其他方面的更改 Suspense支持Fragment(多个根节点)和 Protal(在dom其他部分渲染组件内容)组件,针对一些特殊的场景做了处理。基于 treeShaking 优化,提供了更多的内置功能。

Vue3.0和2.0的响应式原理区别

Vue3.x 改用 Proxy 替代 Object.defineProperty。因为 Proxy 可以直接监听对象和数组的变化,并且有多达13种拦截方法。

虚拟DOM是什么?有什么优缺点?

由于在浏览器中操作DOM是很昂贵的。频繁操作DOM,会产生一定性能问题。这就是虚拟Dom的产生原因。Vue2的Virtual DOM 借鉴了开源库 snabbdom 的实现。Virtual DOM本质就是用一个原生的JS对象去描述一个DOM节点,是对真实DOM的一层抽象。

优点: 1、保证性能下限:框架的虚拟DOM需要适配任何上层API可能产生的操作,他的一些DOM操作的实现必须是普适的,所以它的性能并不是最优的;但是比起粗暴的DOM操作性能要好很多,因此框架的虚拟DOM至少可以保证在你不需要手动优化的情况下,依然可以提供还不错的性能,既保证性能的下限。 2、无需手动操作DOM:我们不需手动去操作DOM,只需要写好 View-Model的 代码逻辑,框架会根据虚拟DOM和数据双向绑定,帮我们以可预期的方式更新视图,极大提高我们的开发效率。 3、跨平台:虚拟DOM本质上是JavaScript对象,而DOM与平台强相关,相比之下虚拟DOM可以进行更方便地跨平台操作,例如服务器端渲染、weex开发等等。 缺点: 1、无法进行极致优化:虽然虚拟DOM + 合理的优化,足以应对大部分应用的性能需要,但在一些性能要求极高的应用中虚拟DOM无法进行针对性的极致优化。 2、首次渲染大量DOM时,由于多了一层DOM计算,会比innerHTML插入慢。

Vue事件绑定原理

原生事件绑定是通过 addEventListener 绑定给真实元素的,组件事件绑定是通过Vue自定义的on实现的。如果要在组件上使用原生事件,需要加.native修饰符,这样就相当于在父组件中把子组件当做普通的HTML标签,然后加上原生事件。on实现的。如果要在组件上使用原生事件,需要加.native修饰符,这样就相当于在父组件中把子组件当做普通的HTML标签,然后加上原生事件。 on、$emit 是基于发布订阅模式的,维护一个事件中心,on的时候将事件按名称存在事件中心里,称之为订阅者,然后emit将对应的事件进行发布,去执行事件中心里的对应的监听器。

vue-router路由钩子函数是什么?执行顺序是什么?

路由钩子的执行流程,钩子函数种类有:全局守卫、路由守卫、组件守卫

全局路由:前置钩子

const router = new VueRouter({ ... });
router.beforeEach((to, from, next) => {
    // do someting
});

后置钩子,没有next

router.afterEach((to, from) => {
    // do someting
});

单独路由守卫,在路由配置上定义,其参数的使用和全局的前置守卫是一样的

const router = new VueRouter({
    routes: [
        {
            path: '/file',
            component: File,
            beforeEnter: (to, from ,next) => {
                // do someting
            }
        }
    ]
});

组件内的守卫

组件内的导航钩子主要有这三种:beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave。他们是直接在路由组件内部直接进行定义的

const File = {
    template: `<div>This is file</div>`,
    beforeRouteEnter(to, from, next) {
        // do someting
        // 在渲染该组件的对应路由被 confirm 前调用
    },
    beforeRouteUpdate(to, from, next) {
        // do someting
        // 在当前路由改变,但是依然渲染该组件是调用
    },
    beforeRouteLeave(to, from ,next) {
        // do someting
        // 导航离开该组件的对应路由时被调用
    }
}

www.jianshu.com/p/cebb8e135…

谈一下对vuex的个人理解

vuex 是专门为 vue 提供的全局状态管理系统,用于多个组件中数据共享、数据缓存等。(无法持久化、内部内心原理是通过创造一个全局实例 new Vue)

主要包括以下几个模块:

  • State:定义了应用状态的数据结构,可以在这里设置默认的初始化状态。
  • Getter:允许组件从Store中获取数据,mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性。
  • Mutation:是唯一更改 store 中状态的方法,且必须是同步函数。
  • Action:用于提交 mutation,而不是直接变更状态,可以包含任意异步请求。
  • Module:允许将单一的 Store 拆分更多个 store 且同时保存在单一的状态树中。

vuex页面刷新数据丢失怎么解决

需要做 vuex 数据持久化,一般使用本地储存的方案来保存数据,可以自己设计存储方案,也可以使用第三方插件。 推荐使用 vuex-persist 插件,它是为 Vuex 持久化储存而生的一个插件。不需要你手动存取 storage,而是直接将状态保存至 cookie 或者 localStorage中。

vuex为什么要分模块并且加命名空间?

模块: 由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能会变得相当臃肿。为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块。

命名空间: 默认情况下,模块内部的 action、mutation、getter是注册在全局命名空间的 --- 这样使得多个模块能够对同一 mutation 或 action 做出响应。如果希望你的模块具有更高的封装度和复用性,你可以通过添加 namespaced:true 的方式使其成为带命名的模块。当模块被注册后,他所有 getter、action、及 mutation 都会自动根据模块注册的路径调整命名。

使用过Vue SSR吗?说说SSR

SSR 也就是服务端渲染,也就是将 Vue 在客户端把标签渲染成 HTML 的工作放在服务端完成,然后再把 html 直接返回给客户端。

优点: SSR 有着更好的 SEO、并且首屏加载速度更快。 缺点: 开发条件会受限制,服务器端渲染只支持 beforeCreate 和 created 两个钩子,当我们需要一些外部扩展库时需要特殊处理,服务端渲染应用程序也需要处于 Node.js 的运行环境。 服务器会有更大的负载需求

Vue中使用了哪些设计模式

1、工厂模式 - 传入参数即可创建实例 虚拟 DOM 根据参数的不同返回基础标签的 Vnode 和组件 Vnode。

eg

//通过工厂函数动态创建对象并返回。返回一个对象的函数就是工厂函数
funtion createPerson(name, age){
    var obj = {name: name, age: age, setName: funtion(){}}
    return obj
}
var p = createPerson('tom', 12)

2、单例模式 - 整个程序有且仅有一个实例 vuex 和 vue-router 的插件注册方法 install 判断如果系统存在实例就直接返回掉。

3、发布-订阅模式。(vue 事件机制)

4、观察者模式。(响应式数据原理)

5、装饰器模式(@装饰器的用法)

6、策略模式,策略模式指对象有某个行为,但是在不同的场景中,该行为有不同的实现方案 - 比如选项的合并策略

你都做过哪些Vue的性能优化?

这里只列举针对 Vue 的性能优化,整个项目的性能优化是一个大工程。

  • 对象层级不要过深,否则性能就会差。
  • 不需要响应式的数据不要放在 data 中(可以使用 Object.freeze() 冻结数据)www.jianshu.com/p/a4c85bc51…
  • v-if 和 v-show 区分使用场景
  • computed 和 watch 区分场景使用
  • v-for 遍历必须加 key,key最好是id值,且避免同时使用 v-if
  • 大数据列表和表格性能优化 - 虚拟列表 / 虚拟表格
  • 防止内部泄露,组件销毁后把全局变量和时间销毁
  • 图片懒加载
  • 路由懒加载
  • 异步路由
  • 第三方插件的按需加载
  • 适当采用 keep-alive 缓存组件
  • 防抖、节流的运用
  • 服务端渲染 SSR or 预渲染

nextTick的使用场景和原理

nextTick 中的回调是在下次 DOM 更新循环结束之后执行的延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。主要思路就是采用微任务优先的方式调用异步方法去执行 nextTick 包装的方法

考虑兼容问题,vue 做了 microtask 向 macrotask 的降级方案

keep-alive使用场景和原理

keep-alive 是 Vue 内置的一个组件,可以实现组件缓存,当组件切换时不会对当前组件进行卸载。

  • 常用的两个属性 include/exclude,允许组件有条件的进行缓存。
  • 两个生命周期 activated/deactivated,用来得知当前组件是否处理活跃状态。
  • keep-alive 运用了 LRU 算法,选择最近最久未使用的组件予以淘汰。

扩展补充:LRU 算法是什么?

一个队列,将数据从尾部插入到this.keys中

每当缓存命中,则将数据移动到this.keys的尾部

当this.keys满的时候将头部的数据丢弃

Vue修饰符

事件修饰符

  • .stop 阻止事件继续传播
  • .prevent 阻止标签默认行为
  • .capture 使用事件捕获模式,即元素自身触发的事件先在此处处理,然后才交由内部元素进行处理
  • .self 只当在 event.target 是当前元素自身时触发处理函数
  • .once 事件只会触发一次
  • .passive 告诉浏览器你不想阻止事件的默认行为

Vue.set方法原理

了解 Vue 响应式原理的同学都知道在两种情况下修改 Vue 是不会触发视图更新的。 1、在实例创建之后添加新的属性到实例上(给响应式对象新增属性) 2、直接更改数组下标来修改数组的值。

Vue.set 或者说是 $set 原理如下

因为响应式数据 我们给对象和数组本身新增了ob属性,代表的是 Observer 实例。当给对象新增不存在的属性,首先会把新的属性进行响应式跟踪 然后会触发对象 ob 的dep收集到的 watcher 去更新,当修改数组索引时我们调用数组本身的 splice 方法去更新数组。

v-model的修饰符

  • .lazy 通过这个修饰符,转变为在 change 事件再同步
  • .number 自动将用户输入值转化为数值类型
  • .trim 自动过滤用户输入的收尾空格

Vue命名插槽

ListCard.vue

<template>
    <swiper>
                <swiper-slide v-for="(category,i) in categories" :key="i">
                    <slot name="items" :category="category"></slot>
                </swiper-slide>
    </swiper>               
</template>

向组件外暴露了category变量,在父组件中可以使用暴露的变量,父组件中我们将ListCard命名为m-list-card

父组件

<template>
        <m-list-card>
            <template #items="{category}">
                <router-link v-for="(news,index) in category.newsList" :key="index">
                </router-link>
            </template>
        </m-list-card>
</template>

父组件可以使用子组件暴露出来的数据,然后在父组件中定义这些数据的显示方式

为什么要用这个,比如在项目中,我们需要多次复用这个组件,但是组件中数据格式相似呈现的格式不一样,我们就需要通过这个命名插槽来暴露组件中传入的数据

键盘修饰符

  • .enter
  • .tab
  • .delete (捕获“删除”和“退格”键)
  • .esc
  • .space
  • .up
  • .down
  • .left
  • .right

监听图片加载完成

原生js监听:img.onload=function(){}

vue中监听图片:<img @load='方法名称'>

所有组件都有一个属性$el:用于获取组件中的元素

this.refs.tabControl.refs.tabControl.el.offsetTop(tabControl是一个组件对象)

能说一下vue-router中常用的路由模式和实现原理吗

hash 模式

1、location.hash 的值实际就是 URL 中 # 后面的东西。它的特点在于:hash虽然出现 URL 中,但不会被包含在 HTTP 请求中,对后端完全没有影响,因此改变 hash 不会重新加载页面。

2、可以为 hash 的改变添加监听事件 window.addEventListener("hashchange",funcRef,false) 每一次改变 hash (window.location.hash),都会在浏览器的访问历史中增加一个记录,利用hash的以上特点,就可以实现前端路由“更新视图但不重新请求页面”的功能了 特点:兼容性好但是不美观

history 模式

利用 HTML5 History Interface 中新增的 pushState() 和 replaceState() 方法。

这两个方法应用于浏览器的历史记录站,在当前已有的 back、forward、go 的基础上,他们提供了对历史记录进行修改的功能。这两个方法有个共同点:当调用他们修改浏览器历史记录栈后,虽然当前 URL 改变了,但浏览器不会刷新页面,这就为单页面应用前端路由“更新视图但不重新请求页面”提供了基础

触发场景:项目打包之后前端资源部署到了5000端口下,直接允许5000端口,会找到index.html,这个index是vue项目最终打包之后生成的首页,然后点击index.html进行相应跳转,注意:这里跳转并未触发对5000端口的请求,只是根据前端路由进行页面的跳转。比如路径变成了localhost:5000/user/info/1,直接点击这个是静态的跳转,如果一刷新,GET404,由于history会将整个地址栏的信息发往服务器去请求这个资源。。。。根本就没有这个资源,所以hitstory模式需要后端进行配合,后端对于前端传过来的请求进行解析,区分哪些路由是前端路由哪些是后端路由

后端如何实现:正则表达式验证/借助第三方插件/nginx反向代理

特点:虽然美观,但是刷新会出现 404 需要后端进行配置。

Vue中的虚拟DOM树

虚拟DOM树本质上就是一个普通的js对象,它是用来描述一段HTML片段的

   01 当页面渲染的时候Vue会创建一颗虚拟DOM树

02 当页面发生改变Vue会再创建一颗新的虚拟DOM树

03 diff算法:前后两颗新旧虚拟DOM树进行对比,Vue通过diff算法,去记录差异的地方

   04 patch算法:将有差异的地方更新到真实的DOM树中

虚拟DOM树的作用:

vue中的虚拟DOM树只会重新渲染页面修改的地方,大大减少了对真实DOM树的操作。

diff算法了解吗

diff算法采用同级比较。

  • 1、tag 标签不一致直接新节点替换旧节点。

  • 2、tag 标签一样。

    先替换属性

    对比子元素

    • 新老都有子元素,采用双指针方式进行对比

      sameVnode 判断tag和key完全相同为同一节点,进行节点复用

      指针比较过程:

      头和头相等对比

      尾和尾相等对比

      头和尾相等对比

      详细对比过程

      老节点数组:oldStartIndex(指向老节点数组头部) oldEndIndex 指向老节点数组尾部

      新节点数组: newStartIndex newEndIndex

      循环 :

      执行上面指针比较过程,End指针如果匹配到了就前移,Start指针匹配到了就后移,然后执行下一轮循环

      如果newEndIndex指向的元素在旧节点数组中找不到,那么dom队列新增一个,然后当oldStartIndex<oldEndIndex的时候或者newStartIndex<newEndIndex的时候退出循环

      这也是为什么列表要加上key了,通过key可以直接通过key去比较两个元素看看两个元素是否值得比较,如果用index的话,比如删除了队首的一个元素,新元素队列全部都要渲染,就因为key对应的元素不一样,而其实改动的只有一个


      sameVnode 的时候传入两个新老子节点patch(oldChild,newChild)

      乱序情况 -- 上面的都不符合,先遍历旧子节点数组形成 key值映射的map对象。

      然后根据新子节点数组循环 按照key值和位置关系移动以及新增节点 最后删除多余的旧子节点 如果移动旧节点同样需要patch(oldChild,newChild)

    • 新的有子元素,老的没有子元素。-- 直接将子元素虚拟节点转化成真实节点插入即可。

    • 新的没有子元素,老的有子元素。 -- 直接清空 innerHtml

  • 3、无 tag 标签 -- 文本节点直接比较内容是否一致

vue双向数据绑定原理

reduce

应用场景:下次操作的初始值,依赖于上一次操作的返回值

1.数组的累加计算

const newarr = arr.reduce((pre,cur)=>{return pre+cur},0)

2.链式获取对象属性的值

   const obj = {
       name:'zs',
       info:{
           address:{
               location:'北京'
           }
       }
   }
   const arrs = ['info','address','location']
   const zs_location = arrs.reduce((pre,cur)=>{return pre[cur]},obj)
   console.log(zs_location)

升级操作

const obj = {
       name:'zs',
       info:{
           address:{
               location:'北京'
           }
       }
   }
const arrStr = 'info.address.location'
const zs_location = arrStr.split('.').reduce((pre,cur)=>pre[cur],obj)
console.log(zs_location)

watch 监听对象的时候,不能直接使用变量,要加上引号

 'testdata.count'(){
            console.log(this.testdata.count)
            this.$store.dispatch('SetCount',this.testdata.count)
            this.$store.dispatch('PrintCount')
            
        },

发布订阅模式

1.Dep类:负责进行依赖收集

  • 首先,有一个数组,专门来存放所有的订阅信息
  • 其次,还要提供一个向数组中追加订阅信息的方法
  • 然后,还要提供一个循环,循环触发数组中的每个订阅信息

2.Watch类:负责订阅一些事件

dom相当于订阅者,当数据发生改变的时候,发布者会将消息发布给订阅者,订阅者拿到消息之后进行重新渲染

Object.defineProperty

const obj = {
    name:'zs'
}
Object.defineProperty(obj,'name',{
    enumerable:true,//当前属性允许被循环
    configurable:true,//当前属性,允许被配置 delete
    get(){
        return '我不是zs'
    },
    set(newVal){
        console.log(newVal)
    }
})
console.log(obj.name)//我不是zs,说明结果被拦截了,访问name会被get所拦截
obj.name='ls'//给name赋值的时候会被set所拦截

需要3个类

class Vue{}//通过实例化Vue创建vm对象
class Dep{}//依赖收集的类/收集watcher订阅者的类
class Watcher{}//订阅者的类

两个方法

function Observe(obj){}//定义一个数据劫持的方法
function Compile(el,vm){}//对HTML结构进行模板编译的方法

双向绑定实现:单向数据绑定(能够将vm实例中数据渲染到页面),单向数据动态绑定(更改vm实例中数据会重新渲染对应页面),双向数据(在单项数据绑定的基础上,实现修改页面对应vm的数据能够影响vm实例中的数据)

首先我们来考虑如何实现将vm实例中的数据渲染到页面中,像vue那样,在模板表达式中使用obj.name就可以访问vm实例中的数据,那我们就需要一个模板编译函数,来实现vm实例中数据到页面的渲染

function Compile(el, vm) {
  // 获取 el 对应的 DOM 元素
  vm.$el = document.querySelector(el)
​
  // 创建文档碎片,提高 DOM 操作的性能
  const fragment = document.createDocumentFragment()
​
  while ((childNode = vm.$el.firstChild)) {
    fragment.appendChild(childNode)
  }
​
  // 进行模板编译
  replace(fragment)
​
  vm.$el.appendChild(fragment)
​
  // 负责对 DOM 模板进行编译的方法
  function replace(node) {
    // 定义匹配插值表达式的正则
    const regMustache = /{{\s*(\S+)\s*}}/
​
    // 证明当前的 node 节点是一个文本子节点,需要进行正则的替换
    if (node.nodeType === 3) {
      // 注意:文本子节点,也是一个 DOM 对象,如果要获取文本子节点的字符串内容,需要调用 textContent 属性获取
      const text = node.textContent
      // 进行字符串的正则匹配与提取
      const execResult = regMustache.exec(text)
      console.log(execResult)
      if (execResult) {
        const value = execResult[1].split('.').reduce((newObj, k) => newObj[k], vm)
        node.textContent = text.replace(regMustache, value)
    
      }
      // 终止递归的条件
      return
    }
    
    }
​
    // 证明不是文本节点,可能是一个DOM元素,需要进行递归处理
    node.childNodes.forEach((child) => replace(child))
  }
}
​

但是这样我们只能第一次刷新页面的时候才可以使用vm中的数据,后面我们修改vm实例中数据,页面并不会变化,也就是说我们只是实现了单项数据的渲染,但是没有动态的进行绑定,如何绑定喃?

这个时候就需要使用到上面所说的订阅发布机制了,当我们创建一个具有数据的DOM的时候,DOM对象可以理解为一个订阅者,我将我的信息存到发布者那里,当发布者监听到DOM对应的vm数据发生变化的时候就发送信息告诉DOM进行页面重新渲染,所以我们在创建一个DOM的时候就要创建一个Watcher类(订阅者类,里面需要拿到对应的vm、自己dom对应的数据key,以及如何更新的回调函数),每次创建一个DOM的时候就实例化一个Watcher类,并将其存储在发布者类里面,所以我们还需要有一个依赖收集类Dep类,里面存放着订阅的信息,这个类的功能:存放订阅信息(watcher实例)、一个添加watcher实例的方法、一个通知每个watcher进行更新的方法,这么看好像Dep要简单点,所以我们首先来实现Dep类 Dep

// 依赖收集的类/收集 watcher 订阅者的类
class Dep {
  constructor() {
    // 今后,所有的 watcher 都要存到这个数组中
    this.subs = []
  }
​
  // 向 subs 数组中,添加 watcher 的方法
  addSub(watcher) {
    this.subs.push(watcher)
  }
​
  // 负责通知每个 watcher 的方法
  notify() {
    this.subs.forEach((watcher) => watcher.update())
  }
}

为了方便起见,我们没有做到非常详细只更新某一部分,而是只要数据有更新我们就更新全部,中心在理解数据绑定上面 接下来是Watcher类

// 订阅者的类
class Watcher {
  // cb 回调函数中,记录着当前 Watcher 如何更新自己的文本内容
  //    但是,只知道如何更新自己还不行,还必须拿到最新的数据,
  //    因此,还需要在 new Watcher 期间,把 vm 也传递进来(因为 vm 中保存着最新的数据)
  // 除此之外,还需要知道,在 vm 身上众多的数据中,哪个数据,才是当前自己所需要的数据,
  //    因此,必须在 new Watcher 期间,指定 watcher 对应的数据的名字
  constructor(vm, key, cb) {
    this.vm = vm
    this.key = key
    this.cb = cb
​
    // ↓↓↓↓↓↓ 下面三行代码,负责把创建的 Watcher 实例存到 Dep 实例的 subs 数组中 ↓↓↓↓↓↓
    Dep.target = this
    key.split('.').reduce((newObj, k) => newObj[k], vm)
    Dep.target = null
  }
​
  // watcher 的实例,需要有 update 函数,从而让发布者能够通知我们进行更新!
  update() {
    const value = this.key.split('.').reduce((newObj, k) => newObj[k], this.vm)
    this.cb(value)
  }
}
 

同时我们还要有一个数据劫持的方法,我们需要去劫持vm实例上data数据,比如某个数据访问的时候我们该做什么操作,修改某个数据的时候该进行什么操作,递归给data中每一个数据都进行数据劫持,我们编译类中只要使用到了vm中的数据,对应操作为get,那么我们就需要实例化一个watcher类,然后存在Dep里面,一旦vm数据发生变化,即触发set,然后dep通知watcher进行更新

// 定义一个数据劫持的方法
function Observe(obj) {
  // 这是递归的终止条件
  if (!obj || typeof obj !== 'object') return
  const dep = new Dep()
​
  // 通过 Object.keys(obj) 获取到当前 obj 上的每个属性
  Object.keys(obj).forEach((key) => {
    // 当前被循环的 key 所对应的属性值
    let value = obj[key]
    // 把 value 这个子节点,进行递归
    Observe(value)
    // 需要为当前的 key 所对应的属性,添加 getter 和 setter
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get() {
        // 只要执行了下面这一行,那么刚才 new 的 Watcher 实例,
        // 就被放到了 dep.subs 这个数组中了
        Dep.target && dep.addSub(Dep.target)
        return value
      },
      set(newVal) {
        value = newVal
        Observe(value)
        // 通知每一个订阅者更新自己的文本
        dep.notify()
      },
    })
  })
}

当前还只是实现了单项数据绑定,即vm中data数据变化会引发页面中对应dom重新渲染,但是我们还没有实现双向数据绑定,双向数据绑定其实还较为简单,在编译方法中进行修改,在具有v-model属性的文本框上添加监听函数,即node.addEventListener,数据变化的时候把最新的数据更新到vm上即可

// 对 HTML 结构进行模板编译的方法
function Compile(el, vm) {
  // 获取 el 对应的 DOM 元素
  vm.$el = document.querySelector(el)
​
  // 创建文档碎片,提高 DOM 操作的性能
  const fragment = document.createDocumentFragment()
​
  while ((childNode = vm.$el.firstChild)) {
    fragment.appendChild(childNode)
  }
​
  // 进行模板编译
  replace(fragment)
​
  vm.$el.appendChild(fragment)
​
  // 负责对 DOM 模板进行编译的方法
  function replace(node) {
    // 定义匹配插值表达式的正则
    const regMustache = /{{\s*(\S+)\s*}}/
​
    // 证明当前的 node 节点是一个文本子节点,需要进行正则的替换
    if (node.nodeType === 3) {
      // 注意:文本子节点,也是一个 DOM 对象,如果要获取文本子节点的字符串内容,需要调用 textContent 属性获取
      const text = node.textContent
      // 进行字符串的正则匹配与提取
      const execResult = regMustache.exec(text)
      console.log(execResult)
      if (execResult) {
        const value = execResult[1].split('.').reduce((newObj, k) => newObj[k], vm)
        node.textContent = text.replace(regMustache, value)
        // 在这个时候,创建 Watcher 类的实例
        new Watcher(vm, execResult[1], (newValue) => {
          node.textContent = text.replace(regMustache, newValue)
        })
      }
      // 终止递归的条件
      return
    }
​
    // 判断当前的 node 节点是否为 input 输入框
    if (node.nodeType === 1 && node.tagName.toUpperCase() === 'INPUT') {
      // 得到当前元素的所有属性节点
      const attrs = Array.from(node.attributes)
      const findResult = attrs.find((x) => x.name === 'v-model')
      if (findResult) {
        // 获取到当前 v-model 属性的值   v-model="name"    v-model="info.a"
        const expStr = findResult.value
        const value = expStr.split('.').reduce((newObj, k) => newObj[k], vm)
        node.value = value
​
        // 创建 Watcher 的实例
        new Watcher(vm, expStr, (newValue) => {
          node.value = newValue
        })
​
        // 监听文本框的 input 输入事件,拿到文本框最新的值,把最新的值,更新到 vm 上即可
        node.addEventListener('input', (e) => {
          const keyArr = expStr.split('.')
          const obj = keyArr.slice(0, keyArr.length - 1).reduce((newObj, k) => newObj[k], vm)
          const leafKey = keyArr[keyArr.length - 1]
          obj[leafKey] = e.target.value
        })
      }
    }
​
    // 证明不是文本节点,可能是一个DOM元素,需要进行递归处理
    node.childNodes.forEach((child) => replace(child))
  }
}
​

完整代码

<div id="app">
        {{name}}
        <input type="text" v-model='name'>
</div>
<script>
    const vm = new Vue({
    el:'#app',
    data:{
        name:'zs',
        age:20,
        info:{
            a:'a1'
        }
    }
})
</script>

vue.js

class Vue {
  constructor(options) {
    this.$data = options.data
​
    // 调用数据劫持的方法
    Observe(this.$data)
​
    // 属性代理
    Object.keys(this.$data).forEach((key) => {
      Object.defineProperty(this, key, {
        enumerable: true,
        configurable: true,
        get() {
          return this.$data[key]
        },
        set(newValue) {
          this.$data[key] = newValue
        },
      })
    })
​
    // 调用模板编译的函数
    Compile(options.el, this)
  }
}
​
// 定义一个数据劫持的方法
function Observe(obj) {
  // 这是递归的终止条件
  if (!obj || typeof obj !== 'object') return
  const dep = new Dep()
​
  // 通过 Object.keys(obj) 获取到当前 obj 上的每个属性
  Object.keys(obj).forEach((key) => {
    // 当前被循环的 key 所对应的属性值
    let value = obj[key]
    // 把 value 这个子节点,进行递归
    Observe(value)
    // 需要为当前的 key 所对应的属性,添加 getter 和 setter
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get() {
        // 只要执行了下面这一行,那么刚才 new 的 Watcher 实例,
        // 就被放到了 dep.subs 这个数组中了
        Dep.target && dep.addSub(Dep.target)
        return value
      },
      set(newVal) {
        value = newVal
        Observe(value)
        // 通知每一个订阅者更新自己的文本
        dep.notify()
      },
    })
  })
}
​
// 对 HTML 结构进行模板编译的方法
function Compile(el, vm) {
  // 获取 el 对应的 DOM 元素
  vm.$el = document.querySelector(el)
​
  // 创建文档碎片,提高 DOM 操作的性能
  const fragment = document.createDocumentFragment()
​
  while ((childNode = vm.$el.firstChild)) {
    fragment.appendChild(childNode)
  }
​
  // 进行模板编译
  replace(fragment)
​
  vm.$el.appendChild(fragment)
​
  // 负责对 DOM 模板进行编译的方法
  function replace(node) {
    // 定义匹配插值表达式的正则
    const regMustache = /{{\s*(\S+)\s*}}/
​
    // 证明当前的 node 节点是一个文本子节点,需要进行正则的替换
    if (node.nodeType === 3) {
      // 注意:文本子节点,也是一个 DOM 对象,如果要获取文本子节点的字符串内容,需要调用 textContent 属性获取
      const text = node.textContent
      // 进行字符串的正则匹配与提取
      const execResult = regMustache.exec(text)
      console.log(execResult)
      if (execResult) {
        const value = execResult[1].split('.').reduce((newObj, k) => newObj[k], vm)
        node.textContent = text.replace(regMustache, value)
        // 在这个时候,创建 Watcher 类的实例
        new Watcher(vm, execResult[1], (newValue) => {
          node.textContent = text.replace(regMustache, newValue)
        })
      }
      // 终止递归的条件
      return
    }
​
    // 判断当前的 node 节点是否为 input 输入框
    if (node.nodeType === 1 && node.tagName.toUpperCase() === 'INPUT') {
      // 得到当前元素的所有属性节点
      const attrs = Array.from(node.attributes)
      const findResult = attrs.find((x) => x.name === 'v-model')
      if (findResult) {
        // 获取到当前 v-model 属性的值   v-model="name"    v-model="info.a"
        const expStr = findResult.value
        const value = expStr.split('.').reduce((newObj, k) => newObj[k], vm)
        node.value = value
​
        // 创建 Watcher 的实例
        new Watcher(vm, expStr, (newValue) => {
          node.value = newValue
        })
​
        // 监听文本框的 input 输入事件,拿到文本框最新的值,把最新的值,更新到 vm 上即可
        node.addEventListener('input', (e) => {
          const keyArr = expStr.split('.')
          const obj = keyArr.slice(0, keyArr.length - 1).reduce((newObj, k) => newObj[k], vm)
          const leafKey = keyArr[keyArr.length - 1]
          obj[leafKey] = e.target.value
        })
      }
    }
​
    // 证明不是文本节点,可能是一个DOM元素,需要进行递归处理
    node.childNodes.forEach((child) => replace(child))
  }
}
​
// 依赖收集的类/收集 watcher 订阅者的类
class Dep {
  constructor() {
    // 今后,所有的 watcher 都要存到这个数组中
    this.subs = []
  }
​
  // 向 subs 数组中,添加 watcher 的方法
  addSub(watcher) {
    this.subs.push(watcher)
  }
​
  // 负责通知每个 watcher 的方法
  notify() {
    this.subs.forEach((watcher) => watcher.update())
  }
}
​
// 订阅者的类
class Watcher {
  // cb 回调函数中,记录着当前 Watcher 如何更新自己的文本内容
  //    但是,只知道如何更新自己还不行,还必须拿到最新的数据,
  //    因此,还需要在 new Watcher 期间,把 vm 也传递进来(因为 vm 中保存着最新的数据)
  // 除此之外,还需要知道,在 vm 身上众多的数据中,哪个数据,才是当前自己所需要的数据,
  //    因此,必须在 new Watcher 期间,指定 watcher 对应的数据的名字
  constructor(vm, key, cb) {
    this.vm = vm
    this.key = key
    this.cb = cb
​
    // ↓↓↓↓↓↓ 下面三行代码,负责把创建的 Watcher 实例存到 Dep 实例的 subs 数组中 ↓↓↓↓↓↓
    Dep.target = this
    key.split('.').reduce((newObj, k) => newObj[k], vm)
    Dep.target = null
  }
​
  // watcher 的实例,需要有 update 函数,从而让发布者能够通知我们进行更新!
  update() {
    const value = this.key.split('.').reduce((newObj, k) => newObj[k], this.vm)
    this.cb(value)
  }
}

伪代码

//vue数据双向绑定伪代码
class Vue{
    constructor(){
        //定义data,存放vm数据
        //调用Observe函数对vm数据进行劫持
        //调用Compile函数对vm数据进行编译
    }
}
class Dep{
    constructor(){
        //定义一个数组,存放watcher
        this.subs = []
    }
    //定义一个向数组中添加watcher的方法
    addSub(watcher){}
    notify(){//通知自己数组中watcher去更新
        //调用每个watcher的update进行更新
    }
}
class Watcher{
    constructor(vm,key,cb){//知道数据从哪拿,拿那个,怎么更新
​
        //创建一个Watcher实例,就应该将其放入Dep中,所以应该在构造函数中将这个实例加入到Dep中
    }
    //需要有一个update函数,方便让发布者能够在通知函数中调用这个函数来对其进行更新
    update(){
​
    }
    
}
​
function Oberve(obj){
    //递归给传入的对象中每一个属性都进行数据劫持
    //通过Object.defineProperty来进行实现
    Object.defineProperty(obj,{
        get(){
            //数据被访问的时候即创建一个watcher实例放入Dep的数组中
        },
        set(){
            //数据被修改的时候新值赋给vm,发布修改信息,通知订阅者进行修改
            dep.notify()
        }
    })
}
function Compile(el,vm){//将el与vm进行对应,渲染vm数据到el上
    //一旦创建一个带数据的dom就实例化一个watcher
    //如果是input输入框,添加addEventListener,监听其变化,如果有变化就将新数据重新赋给vm
}