Vue.js全套面试题(进阶提升最佳选择)

在vue文件组件中,style标签的scoped有什么作用?

  • vue中有一种给组件提供样式作用域的方式,让组件和组件之间不会发生冲突,原理就是通过给元素添加data-v-hashid属性的形式,让样式通过属性表达式处理,达到样式隔离的目的,但是有时候也会出现问题,如果我们在一个组件中控制另一个组件的样式就不会成功,可以通过/deep/的形式突破样式作用域的隔离.

介绍一下vuex

Vuex是一个专为Vue.js应用程序开发的状态管理模式,就是一个全局的状态管理工具,和全局变量没什么区别,唯一区别就是vuex是有规则的存储,获取,并且可以适配vue的响应式规则,并且提供可供调试的devtools

vuex的规则就是

  1. state: 定义状态
  2. getters:类似与组件中的computed, 可以对state中定义的状态进行过滤,计算等操作
  3. mutations:更改Vuex的store中的状态的唯一方法是提交mutation,并在mutation中只能以同步的形式去修改state, mutations是一个对象,每一个mutaion是一个函数, 函数接受state,payload作为参数,
  4. actions:异步逻辑处理,actions是一 个对象 ,每一个action是一个函数, 函数接受context,payload作为参数,在内部写ajax异步逻辑,在ajax成功或者失败,通过context.commit()去提交mutation进行state的变更
  5. modules:如果状态特别的多的情况下,可以利用moudles进行模块的划分,每-个模块可以使用namespaced这个属性开启单独作用域,避免和其他模块发生冲突在组件内部如果要获取vuex的状态,以及mutation,action, getters的话,vuex提供了对应的辅助函数mapState, mapActions, mapMutaions, mapGetters如果遇到需要多页面,多组件共享的状态就需要使用vuex作为状态管还有就是强制刷新后数据重新初始化,可以接口数据持久化(本地存储)解决这个问题

Vue2.x和Vue3.x渲染器的diff算法分别说一下

  • 简单来说,dif算法有以下过程同级比较,再比较子节点先判断一方有子节点一方没有子节点的情况(如果新的children没有子节点,将旧的子节点移除)比较都有子节点的情况(核心diff)递归比较子节点正常Diff两个树的时间复杂度是O(n3),但实际情况下我们很少会进行跨层级的移动DOM,所以Vue将Diff进行了优化,从O(n3)-> O(n),只有当新旧children都为多个子节点时才需要用核心的Dif算法进行同层级比较。
  • Vue2的核心Diff算法采用了双端比较的算法,同时从新旧children的两端开始进行比较,借助key值找到可复用的节点,再进行相关操作。相比React的Diff算法, 同样情况下可以减少移动节点次数,减少不必要的性能损耗,更加的优雅。
  • Vue3.x借鉴了ivi算法和inferno算法在创建VNode时就确定其类型,以及在mount/ patch的过程中采用位运算来判断- -个VNode的类型,在这个基础之上再配合核心的Diff算法,使得性能上较Vue2.x有了提升

Vue组件中data为什么必须是函数

  • 在vue中我们使用组件进行开发,组件是复用的,一个组件如果多次复用的话就会创建多个实例,而这些实例都是同一个构造函数,如果data是对象的话,对象属于引用类型,数据之间共享,我们改变-个data值就会影响其他组件内的状态,这还是我们想要的结果,为了保证组件之间状态相互独立,所以data是必须是一个函数。

vue如何优化首屏加载速度?

优化首屏加载可以从这几个方面开始

  1. 请求优化: CDN将第三方的类库放到CDN上,能够大幅度减少生产环境中的项目体积,另外CDN能够实时地根据网络流量和各节点的连接、负载状况以及到用户的距离和响应时间等综合信息将用户的请求重新导向离用户最近的服务节点上,另外因为CDN和服务器的域名一般不是同- 一个,可以缓解同-域名并发http请 求的数量限制有效分流以及减少多余的cookie的发送(CDN上面的静态资源请求时不需要携带任何cookie)在webpack中可以通过externals配置项,将第三方的类库的弓|用地址从本地指向你提供的CDN地址
  2. 缓存:将长时间不会改变的第三方类库或者静态资源设置为强缓存,将max-age设置为一个非常长的时间,再将访问路径加上哈希达到哈希值变了以后保证获取到最新资源,个好的缓存策略,有助于减轻服务器的压力,并且显著的提升用户的体验
  3. gzip:开启gzip压缩,通常开启gzip压缩能够有效的缩小传输资源的大小
  4. http2:如果系统首屏同一时间需要加载的静态资源非常多,但是浏览器对同-域名的tcp连接数量是有限制的(chrome为6个)超过规定数量的tcp连接,则必须要等到之前的请求收到响应后才能继续发送,而http2则可以在- -个tcp连接中并发多个请求没有限制,在一些网络较差的环境开启http2性能提升尤为明显
  5. 懒加载:通过import(使得ES6的模块有了动态加载的能力,让ur|匹配到相应的路径时,会动态加载页面组件,这样首屏的代码量会大幅减少,webpack会把动态加载的页面组件分离成单独的一个chunkjs文件
  6. 预渲染:由于浏览器在渲染出页面之前,需要先加载和解析相应的html,css和js文件,为此会有一段白屏的时间, 可以添加loading,或者骨架屏幕尽可能的减少白屏对用户的影响体积优化
  • 合理使用第三方库:对于一些第三方ui框架,类库,尽量使用按需加载,减少打包体积
  • 使用可视化工具分析打包后的模块体积:过webpack-bundle- analyzer这个插件在每次打包后能够更加直观的分析打包后模块的体积,再对其中比较大的模块进行优化
  • 提高代码使用率:利用代码分割,将脚本中无需立即调用的代码在代码构建时转变为异步加载的过程
  • 封装:构建良好的项目架构,按照项目需求就行全局组件,插件,过滤器,指令, utils等做一 些公共封装,可以有效减少我们的代码量,而且更容易维护资源优化
  1. 图片懒力加载:使用图片懒加载可以优化同一时间减少http请求开销,避免显示图片导致的画面抖动,提高用户体验
  2. 使用svg图标:相对于用一张图片来表示图标,svg拥有 更好的图片质量,体积更小,并且不需要开启额外的http请求
  3. 压缩图片:可以使用image-webpack-loader,在用户肉眼分辨不清的情况下一定程度上压缩图片

new Vue时发生了什么?

创建Vue实例的,因此首先搜索Vue的定义

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);
}

可以看到Vue构造函数的核心代码只有一行:this._init(options);因此搜索私有化_init方法。由于_init是作为this的一个方法,注意此处的this就是Vue。经过查找_init方法的定义如下:

Vue.prototype._init = function (options) {
    var vm = this;
    // a uid
    vm._uid = uid$3++;

    var 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
    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 {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      );
    }
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm);
    } else {
      vm._renderProxy = vm;
    }
    // expose real self
    vm._self = vm;
    debugger
    initLifecycle(vm);
    initEvents(vm);
    initRender(vm);
    callHook(vm, 'beforeCreate');
    initInjections(vm); // resolve injections before data/props
    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);
    }
  };
}

读源码需要注意的一点就是不相关的一定要忽略,一旦遵循深度遍历法则读下去,你是一定会失败的。如果方法不对,那还不如不读,睡会觉。可以将上面的代码简化为:

Vue.prototype._init = function (options) {
    var vm = this;
    ...
    vm.$options = mergeOptions(options || {}, vm);
    ...
    initState(vm);
    ...
    if (vm.$options.el) {
      vm.$mount(vm.$options.el);
    }
    ...
  };
}
_init方法总体上做的事情其实并不多,第一项就是合并配置项。比如路由,状态管理,渲染函数。

new Vue({
  store: store,
  router: router,
  render: h => h(App),
}).$mount('#app')
然后初始化状态,initState的方法定义如下:

function initState (vm) {
  vm._watchers = [];
  var opts = vm.$options;
  if (opts.props) { initProps(vm, opts.props); }
  if (opts.methods) { initMethods(vm, opts.methods); }
  if (opts.data) {
    initData(vm);
  } else {
    observe(vm._data = {}, true /* asRootData */);
  }
  if (opts.computed) { initComputed(vm, opts.computed); }
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch);
  }
}

从源码可以看出,initState就是将vue实例中的data,method,computed,watch等数据项做进一步得处理,其实就是做代理以及转化成可观测对象。
数据处理完成之后就将数据挂载到指定的钩子上:vm.options.el);

另外需要注意的是,_init方法中有一下一段代码,在上面我为来突出主线而省略了,这就是

    initLifecycle(vm);
    initEvents(vm);
    initRender(vm);
    callHook(vm, 'beforeCreate');
    initInjections(vm); // resolve injections before data/props
    initState(vm);
    initProvide(vm); // resolve provide after data/props
    callHook(vm, 'created');

可以看到在initState(vm)执行之前,我们执行了beforeCreate方法,在initState(vm)执行之后,我们执行了created方法。因此在beforeCreate方法中,我们无法直接引用data,method,computed,watch等在initState(vm)中才开始存在的属性。

  • 总结:1合并配置项。比如路由,状态管理,渲染函数 2初始化状态 3将数据挂载到指定的钩子上:vm.options.el)
    初始化生命周期,初始化事件中心,初始化渲染,初始化 data、props、computed、watcher

vue中keep-alive组件的作用

  • 在vue项目中,难免会有列表页面或者搜索结果列表页面,点击某个结果之后,返回回来时,如果不对结果页面进行缓存,那么返回列表页面的时候会回到初始状态但是我们想要的结果是返回时这个页面还是之前搜索的结果列表,这时候就需要用到vue的 了,keep-alive是Vue内置的一个组件,可以使被包含的组件保留状态,或避免重新渲染它的作用就是实现页面的缓存提高用户的体验。它主要是靠两个函数activated是 keep-alive组件激活时使用deactivated是keep-alive组件停用时调用;为了防止返回列表页面的时候回到初始状态,可以是被包含的组件保留状态,或者避免重新被渲染.keep - alive在vue中主要是用来缓存组件,优化性能,使用keep - alive的组件一定要name, 组件有两个属性,include匹配要缓存的组件,值是字符串或者数组,exclude匹配到的name命将不会缓存,如果一个组件使用了keep-alive,除非将从keep-alive实例里面找到这个组件实例并且删除,则这个组件会一直缓存.比如:有一个列表页面和-一个详情页面,那么用户就会经常执行打开详情= >返回列表= >打开详情这样的话列表和详情都是一个频率很高的页面,那么就可以对列表组件使用 进行缓存,这样用户每次返回列表的时候,都能从缓存中快速渲染,而不是重新渲染。

r o u t e 和 route 和 routerouter的区别

  • r o u t e 和 route和 routerouter:
    • $route 获取当前页面匹配的路由相关信息包含了当前URL解析得到的信息。包含当前的路径,参数,query对象等。
    • r o t u e r 获 取 到 的 是 当 前 的 路 由 实 例 , 用 于 控 制 路 由 跳 转 当 导 航 到 不 同 的 u r l , 可 以 使 用 rotuer 获取到的是当前的路由实例,用于控制路由跳转 当导航到不同的 url,可以使用 rotuerurl,使router.push方法实现, 这个方法会向
      history里面添加一条记录,点击浏览器回退按钮的时候
      会会退到之前的url
  • $route 是 $router.currentRoute 的指针,目的就是为了方便开发者更加方便路由信息的获取 $route === $router.currentRoute

vue-router有几种导航钩子(导航守卫)?

  1. 第一种:
    全局导航钩子:一般用来做(登录,title改变,埋点,…)
    前置守卫
         //单独设置每个路由的属性:
         meta: { may: true }
         router.beforeEach((to, from, next) => {
             if (to.matched.some(item => item.meta.may)) {
                 let id = window.localStorage.getItem("id")
                 if (id) {
                     next()
                 } else {
                     next({ name: "login" })
                 }
             } else {
                 next()
             }
         })
    

注意:next 方法必须要调用,否则钩子函数无法 resolved

后置钩子

  router.afterEach((to,from) => {
      if(to.meta && to.meta.title){
          document.title = to.meta.title
      }else{
          document.title = "666"
      }
  })
  1. 第二种:单独路由独享钩子:不太常用
{
    path: '/home',
    name: 'home',
    component: Home,
    beforeEnter(to, from, next) {
        if (window.localStorage.getItem("id")) {
            next()
        } else {
            next({ name: "login" })
        }
    }
}
  1. 第三种:组件内的钩子:
    当前页面需要通过路由的进入离开做一些交互操作(表单页面放弃保存的提示)
    beforeRouteEnter(to, from, next) {
        // do someting
        // 在渲染该组件的对应路由被 confirm 前调用
    },
    beforeRouteUpdate(to, from, next) {
        // do someting
        // 在当前路由改变,但是依然渲染该组件时调用
    },
    beforeRouteLeave(to, from ,next) {
        // do someting
        // 导航即将离开该组件的对应路由时被调用
    }

全局解析守卫
router.beforeResolve 注册一个全局守卫,和 router.beforeEach 类似

vue-router 中路由的传参方式

(1)在路由中配置

{
  path : '/home/:id',
  name : 'Home',
  component
}

然后写调用的时候

this.$router.push({path : `/describle/${id}`})

取值:

$route.parms.id

(2)通过params传参,通过name配置路由

路由配置:

{
  path : '/home',
  name : 'Home',
  component : Home
}

this.$router.push({
  name : 'Home',
  params : {
    id : id
  }
})

获取

$route.params.id

(3)使用path来配置路由,通过query来传递参数,参数会在url后边的?id=?中显示
注意:path与params不能同时使用应用path+query
路由配置:

{
  path : '/home',
  name : 'Home',
  component : Home
}

调用:

this.$router.push({
  path : '/home',
  query : {
    id : id
  }
})

获取

this.$route.query.id

vue-router 实现路由懒加载(动态加载路由)

写法:component:()=>import('@/components/Home');

vue路由实现的原理?或vue-router原理?

原理核心就是 更新视图但不重新请求页面。

vue-router实现单页面路由跳转,提供了三种方式:hash模式、history模式、abstract模式,根据mode参数来决定采用哪一种方式。

hash: 使用 URL hash 值来作路由。默认模式。
history: 依赖 HTML5 History API 和服务器配置。查看 HTML5 History 模式。
abstract: 支持所有 JavaScript 运行环境,如 Node.js 服务器端
  • hash(#)是URL 的锚点,代表的是网页中的一个位置,单单改变#后的部分(/#/…),浏览器只会加载相应位置的内容,不会重新加载网页,也就是说 #是用来指导浏览器动作的,对服务器端完全无用,HTTP请求中不包括#;同时每一次改变#后的部分,都会在浏览器的访问历史中增加一个记录,使用”后退”按钮,就可以回到上一个位置;所以说Hash模式通过锚点值的改变,根据不同的值,渲染指定DOM位置的不同数据。

  • HTML5 History API提供了一种功能,能让开发人员在不刷新整个页面的情况下修改站点的URL,就是利用 history.pushState API 来完成 URL 跳转而无须重新加载页面;
    由于hash模式会在url中自带#,如果不想要很丑的 hash,我们可以用路由的 history 模式,只需要在配置路由规则时,加入"mode: ‘history’",这种模式充分利用 history.pushState API 来完成 URL 跳转而无须重新加载页面。

  • abstract模式是使用一个不依赖于浏览器的浏览历史虚拟管理后端。
    根据平台差异可以看出,在 Weex 环境中只支持使用 abstract 模式。 不过,vue-router 自身会对环境做校验,如果发现没有浏览器的 API,vue-router 会自动强制进入 abstract 模式,所以 在使用 vue-router 时只要不写 mode 配置即可,默认会在浏览器环境中使用 hash 模式,在移动端原生环境中使用 abstract 模式。
    参考地址:https://juejin.im/post/5bc6eb875188255c9c755df2

vue中computed的原理是什么?

要讲清楚,computed原理,首先得讲vue响应式原理,因为computed的实现是基于Watcher对象的。那么vue的响应式原理是什么呢,众所周知,vue是基于Object.defineProperty实现监听的。在vue初始化数据data和computed数据过程中。会涉及到以下几个对象:

  1. Observe对象
  2. Dep对象
  3. Watch对象
  • Observe对象是在data执行响应式时候调用,因为computed属性基于响应式属性,所以其不需要创建Observe对象。

  • Dep对象主要功能是做依赖收集,有个属性维护多个

  • Watch对象,当更新时候循环调用每个Watch执行更新。Watch对象主要是用于更新,而且是收集的重点对象。

    这里谈到computed计算属性,首先要知道,其有两种定义方式,-种是方法,另-种是get, set属性。而且,其内部监听的对象必须是已经定义响应式的属性,比如data的属性vuex的属性。vue在创建computed属性时候,会循环所有计算属性,每一个计算属性会创建一个watch, 并且在通过defineProperty定义监听,在get中,计算属性工作是做依赖收集,在set中, 计算属性重要工作是重新执行计算方法,这里需要多补充- -句,因为computed是懒执行,也就是说第一-次初始化之后,怀会执行计算,下一次变更执行重新计算是在set中。

另一个补充点是依赖收集的时机,computed收集时机和data一样,是在组件挂载前,但是其收集对象是自己属性对应的watch,而data本身所有数据对应一个watch

vue-loader 的实现原理是什么?

官方说明
vue-loader is a loader for Webpack that can transform Vue components written in the following 
format into a plain JavaScript module

简单来说就是:将 *.vue 文件变成 *.bundle.js,然后放入浏览器运行。

  1. vue-loader简介
    vue-loader是一个webpack的loader;可以将vue文件转换为JS模块;

  2. vue-loader特性
    (1)ES2015默认支持
    (2)允许对VUE组件的组成部分使用其他webpack loader;比如对< style >使用SASS(编译CSS语言),对< template >使用JADE(jade是一个高性能的模板引擎,用JS实现,也有其他语言的实现—php,scala,yuby,python,java,可以供给node使用)
    (3).vue文件中允许自定义节点,然后使用自定义的loader处理他们
    (4)对< style >< template >中的静态资源当做模块来对待,并且使用webpack loaders进行处理
    (5)对每个组件模拟出CSS作用域
    (6)支持开发期组件的热重载
    在编写vue应用程序时,组合使用webpack跟vue-loader能带来一个现代。灵活并且非常强大的前端工作流程;

  3. webpack简介
    (1)webpack是一个模块打包工具,他可以将一堆文件中的每个文件都作为一个模块;找出他们的依赖关系,将他们打包为可部署的静态资源;
    (2)使用webpack的loaders,我们可以配置webpack以任何方式去转换所有类型的文件;例如
    A:转换ES2015,CoffeeScript或者TypeScript模块为普通的ES5 CommonJs模块;
    B:可以选择在编译之前检验你的源代码;
    C:将jade模板转换为纯HTML并且嵌入JS字符串中
    D:将SASS文件转换为纯CSS,然后将其转换成JS片段,将生成的CSS作为< style >标签插入页面;
    E:处理html或者CSS中引用的图片。移动到配置的路径中,并且使用MD5 hash重命名;
    (3)当你理解webpack原理后会感觉到它是这么强大,可以大大的优化你的前端工作流程;缺点是配置比较复杂;

  4. VUE组件细则
    .vue文件是一个自定义的文件类型,用类HTML语法描述一个vue组件,每个.vue组件包含三种类型的顶级语言快< template>< script>< style>,还允许添加自定义的模块;

<template>
  <div class="example">{{ msg }}</div>
</template>

<script>
export default {
  data () {
    return {
      msg: 'Hello world!'
    }
  }
}
</script>

<style>
.example {
  color: red;
}
</style>

<custom1>
  This could be e.g. documentation for the component.
</custom1>

vue-loader会解析文件,提取出每个语言块,如果有必要会通过其他loader处理,最后将他们组装成一个commonjs模块;module.exports出一个vue.js组件对象;
vue-loader支持使用非默认语言,比如CSS预处理器,预编译的HTML模板语言,通过设置语言块的lang属性;例如

<style lang='sass'>
    /*sass*/
</style>

vue3.0中为什么要使用Proxy,他相比以前的实现方式有什么改进?

  1. Object.defineProperty只能劫持对象的属性,而Proxy是直接代理对象。
    由于 Object.defineProperty 只能对属性进行劫持,需要遍历对象的每个属性。而 Proxy 可以直接代理对象。

  2. Object.defineProperty对新增属性需要手动进行Observe。
    由于 Object.defineProperty 劫持的是对象的属性,所以新增属性时,需要重新遍历对象,对其新增属性再使用 Object.defineProperty 进行劫持。

也正是因为这个原因,使用vue给 data 中的数组或对象新增属性时,需要使用 vm.$set 才能保证新增的属性也是响应式的。
在vue的 set 方法中,对 target 是数组和对象做了分别的处理,
target 是数组时,会调用重写过的 splice 方法进行手动 Observe 。
对于对象,如果 key 本来就是对象的属性,则直接修改值触发更新,否则调用 defineReactive方法重新定义响应式对象。

(1)如果采用 proxy 实现,Proxy 通过 set(target, propKey, value, receiver) 拦截对象属性的设置,是可以拦截到对象的新增属性的。

(2)Proxy 对数组的方法也可以监测到,不需要像上面vue2.x源码中那样进行 hack。
(3) Proxy支持13种拦截操作,这是defineProperty所不具有的
get(target, propKey, receiver):拦截对象属性的读取,比如 proxy.foo 和 proxy[‘foo’]。

set(target, propKey, value, receiver):拦截对象属性的设置,比如 proxy.foo = v 或 proxy[‘foo’] = v ,返回一个布尔值。

has(target, propKey):拦截 propKey in proxy 的操作,返回一个布尔值。

deleteProperty(target, propKey):拦截 delete proxy[propKey] 的操作,返回一个布尔值。

ownKeys(target):拦截 Object.getOwnPropertyNames(proxy) 、 Object.getOwnPropertySymbols(proxy) 、Object.keys(proxy) 、for…in 循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而 Object.keys() 的返回结果仅包括目标对象自身的可遍历属性。

getOwnPropertyDescriptor(target, propKey):拦截 Object.getOwnPropertyDescriptor(proxy, propKey) ,返回属性的描述对象。

defineProperty(target, propKey, propDesc):拦截 Object.defineProperty(proxy, propKey, propDesc) 、Object.defineProperties(proxy, propDescs) ,返回一个布尔值。

preventExtensions(target):拦截 Object.preventExtensions(proxy) ,返回一个布尔值。

getPrototypeOf(target):拦截 Object.getPrototypeOf(proxy) ,返回一个对象。

isExtensible(target):拦截 Object.isExtensible(proxy) ,返回一个布尔值。

setPrototypeOf(target, proto):拦截 Object.setPrototypeOf(proxy, proto) ,返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。

apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如 proxy(…args)、proxy.call(object, …args) 、proxy.apply(…) 。

construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如 new proxy(…args) 。

(4)Proxy 作为新标准,长远来看,JS引擎会继续优化 Proxy ,但 getter 和 setter 基本不会再有针对性优化。
proxy缺点:
Proxy 对于IE浏览器来说简直是灾难。(不兼容)

并且目前并没有一个完整支持 Proxy 所有拦截方法的Polyfill方案,有一个google编写的 proxy-polyfill 也只支持了 get,set,apply,construct 四种拦截,可以支持到IE9+和Safari 6+。

总结:
Object.defineProperty 对数组和对象的表现一直,并非不能监控数组下标的变化,vue2.x中无法通过数组索引来实现响应式数据的自动更新是vue本身的设计导致的,不是 defineProperty 的锅。

Object.defineProperty 和 Proxy 本质差别是,defineProperty 只能对属性进行劫持,新增属性需要手动 Observe 的问题。

Proxy 作为新标准,浏览器厂商势必会对其进行持续优化,但它的兼容性也是块硬伤,并且目前还没有完整的polifill方案。

参考链接:https://blog.csdn.net/weixin_40687883/article/details/102565285

vue 中computed和watch的区别在哪里?

计算属性computed :

  1. 支持缓存,只有依赖数据发生改变,才会重新进行计算
  2. 不支持异步,当computed内有异步操作时无效,无法监听数据的变化
    3.computed 属性值会默认走缓存,计算属性是基于它们的响应式依赖进行缓存的,也就是基于data中声明过或者父组件传递的props中的数据通过计算得到的值
  3. 如果一个属性是由其他属性计算而来的,这个属性依赖其他属性,是一个多对一或者一对一,一般用computed
    5.如果computed属性属性值是函数,那么默认会走get方法;函数的返回值就是属性的属性值;在computed中的,属性都有一个get和一个set方法,当数据变化时,调用set方法。
    https://img2018.cnblogs.com/blog/1402448/201908/1402448-20190809154932198-1444047098.png

侦听属性watch:

  1. 不支持缓存,数据变,直接会触发相应的操作;
    2.watch支持异步;
    3.监听的函数接收两个参数,第一个参数是最新的值;第二个参数是输入之前的值;
  2. 当一个属性发生变化时,需要执行对应的操作;一对多;
  3. 监听数据必须是data中声明过或者父组件传递过来的props中的数据,当数据变化时,触发其他操作,函数有两个参数,
      immediate:组件加载立即触发回调函数执行,
      deep: 深度监听,为了发现对象内部值的变化,复杂类型的数据时使用,例如数组中的对象内容的改变,注意监听数组的变动不需要这么做。注意:deep无法监听到数组的变动和对象的新增,参考vue数组变异,只有以响应式的方式触发才会被监听到。
    https://img2018.cnblogs.com/blog/1402448/201908/1402448-20190809160441362-1201017336.png

监听的对象也可以写成字符串的形式
https://img2018.cnblogs.com/blog/1402448/201908/1402448-20190809160648619-505189772.png
当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的。这是和computed最大的区别

Vue的双向数据绑定原理?

  • vue实现数据双向绑定主要是:采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应监听回调。当把一个普通 Javascript 对象传给 Vue 实例来作为它的 data 选项时,Vue 将遍历它的属性,用 Object.defineProperty 将它们转为 getter/setter。用户看不到 getter/setter,但是在内部它们让 Vue 追踪依赖,在属性被访问和修改时通知变化。

  • vue的数据双向绑定 将MVVM作为数据绑定的入口,整合Observer,Compile和Watcher三者,通过Observer来监听自己的model的数据变化,通过Compile来解析编译模板指令(vue中是用来解析 {{}}),最终利用watcher搭起observer和Compile之间的通信桥梁,达到数据变化 —>视图更新;视图交互变化(input)—>数据model变更双向绑定效果。

nextTick实现原理

在DOM更新完毕之后执行一个回调

Vue.nextTick(function () {
    // DOM 更新了
})

尽管MVVM框架并不推荐访问DOM,但有时候确实会有这样的需求,尤其是和第三方插件进行配合的时候,免不了要进行DOM操作。而nextTick就提供了一个桥梁,确保我们操作的是更新后的DOM。

vue就是这样的思路,并不是用MO进行DOM变动监听,而是用队列控制的方式达到目的。

vue的数据响应过程包含:数据更改->通知Watcher->更新DOM。而数据的更改不由我们控制,可能在任何时候发生。如果恰巧发生在repaint之前,就会发生多次渲染。这意味着性能浪费,是vue不愿意看到的。

还需了解event loop的另一个重要概念,microtask.我们可以把它称为微任务

每一次事件循环都包含一个microtask队列,在循环结束后会依次执行队列中的microtask并移除,然后再开始下一次事件循环。

在执行microtask的过程中后加入microtask队列的微任务,也会在下一次事件循环之前被执行。也就是说,macrotask总要等到microtask都执行完后才能执行,microtask有着更高的优先级。

microtask的这一特性,简直是做队列控制的最佳选择啊!vue进行DOM更新内部也是调用nextTick来做异步队列控制。而当我们自己调用nextTick的时候,它就在更新DOM的那个microtask后追加了我们自己的回调函数,从而确保我们的代码在DOM更新后执行,同时也避免了setTimeout可能存在的多次执行问题。
常见的microtask有:Promise、MutationObserver、Object.observe(废弃),以及nodejs中的process.nextTick.

队列控制的最佳选择是microtask,而microtask的最佳选择是Promise.但如果当前环境不支持Promise,vue就不得不降级为macrotask来做队列控制了。

在vue2.5的源码中,macrotask降级的方案依次是:setImmediate、MessageChannel、setTimeout.

setImmediate是最理想的方案了,可惜的是只有IE和nodejs支持。

MessageChannel的onmessage回调也是microtask,但也是个新API,面临兼容性的尴尬…

所以最后的兜底方案就是setTimeout了,尽管它有执行延迟,可能造成多次渲染,算是没有办法的办法了。

总结
以上就是vue的nextTick方法的实现原理了,总结一下就是:

vue用异步队列的方式来控制DOM更新和nextTick回调先后执行
microtask因为其高优先级特性,能确保队列中的微任务在一次事件循环前被执行完毕
因为兼容性问题,vue不得不做了microtask向macrotask的降级方案

vue组件的生命周期

** 初始化阶段的4个钩子: beforeCreate、created、beforeMount、mounted;**
beforeCreate:
1、组件创建前触发,目的是为组件的 生命周期和组件中的事件做准备;
2、没有获得数据,真实dom也没有渲染出来
3、在此阶段内可以进行数据请求,提供一次修改数据的机会
4、此阶段执行一次
Created:
1、组件创建结束
2、数据可以拿到了
3、可以进行数据请求,提供一次修改数据的机会
4、该过程执行一次
beforeMount:
1、组件挂载前
2、该阶段任务:
判断el,判断template;
如果el没有,那么我们需要手动挂载,如果有,那麽判断template;
如果template有,那么进行render函数,如果template没有,那么通过outHTML手动书写模板;
3、数据可以获得,但是真实dom还没有渲染
4、可以进行数据请求,也提供了一次数据修改的机会
Mounted:
1、组件挂载结束
2、数据获得,真实dom也获得了
3、可以进行数据请求,也可以修改数据
4、执行一次
5、可以进行真实dom的操作了
总结:数据请求我们一般写在created钩子中,第三方库的实例化我们一般写在mounted钩子中;
运行中阶段有两个钩子:beforeUpdate、updated;
beforeUpdate:
① 更新前
② 重新渲染 VDOM , 然后通过diff算法比较两次vdom,生成patch 补丁对象,还未渲染到页面
③ 这个钩子函数更多的是内部进行一些操作
④ 可以触发多次,只要数据更新就会触发
Updated:
① 数据更新结束
② 真实dom得到了,数据也得到了( 更新后的 )
③ 可以用于动态数据获取( 第三方库实例化 )
④ 可以触发多次,只要数据更新就会触发
销毁阶段也是有两个钩子:beforeDestroy、destroyed.这两个钩子用法基本相同。
beforeDestroy
Destroyed

  methods:{
            //内部通过$destroy方法删除自身组件,触发destroy生命钩子函数
               clear() {
                 this.$destroy();
               }
         },
          beforeDestroy () {
                console.log('beforeDestroy');
            		//组件已经销毁,但是渲染出的真实dom结构未被销毁,手动销毁
              document.querySelector('#app').remove()
           },
           
            destroyed () {
               console.log('destroyed')
            }

Vue的路由实现原理

一般源码中,都会用到 window.history 和 location.hash

history 实现

window.history 对象包含浏览器的历史,window.history 对象在编写时可不使用 window 这个前缀。history是实现SPA前端路由是一种主流方法,它有几个原始方法:

  • history.back()
    与在浏览器点击后退按钮相同
  • history.forward()
    与在浏览器中点击按钮向前相同
  • history.go(n)
    接受一个整数作为参数,移动到该整数指定的页面,比如go(1)相当于forward(),go(-1)相当于back(),go(0)相当于刷新当前页面

如果移动的位置超出了访问历史的边界,以上三个方法并不报错,而是静默失败

在HTML5,history对象提出了 pushState() 方法和 replaceState() 方法,这两个方法可以用来向历史栈中添加数据,就好像 url 变化了一样(过去只有 url 变化历史栈才会变化),这样就可以很好的模拟浏览历史和前进后退了,现在的前端路由也是基于这个原理实现的。

history.pushState

pushState(stateObj, title, url) 方法向历史栈中写入数据,其第一个参数是要写入的数据对象(不大于640kB),第二个参数是页面的 title, 第三个参数是 url (相对路径)。

stateObj :一个与指定网址相关的状态对象,popstate事件触发时,该对象会传入回调函数。如果不需要这个对象,此* 处可以填null。

title:新页面的标题,但是所有浏览器目前都忽略这个值,因此这里可以填null。

url:新的网址,必须与当前页面处在同一个域。浏览器的地址栏将显示这个网址。

关于pushState,有几个值得注意的地方:

pushState方法不会触发页面刷新,只是导致history对象发生变化,地址栏会有反应,只有当触发前进后退等事件(back()和forward()等)时浏览器才会刷新

这里的 url 是受到同源策略限制的,防止恶意脚本模仿其他网站 url 用来欺骗用户,所以当违背同源策略时将会报错

history.replaceState

replaceState(stateObj, title, url) 和pushState的区别就在于它不是写入而是替换修改浏览历史中当前纪录,其余和 pushState一模一样。

popstate事件

定义:每当同一个文档的浏览历史(即history对象)出现变化时,就会触发popstate事件。

注意:仅仅调用pushState方法或replaceState方法 ,并不会触发该事件,只有用户点击浏览器倒退按钮和前进按钮,或者使用JavaScript调用back、forward、go方法时才会触发。另外,该事件只针对同一个文档,如果浏览历史的切换,导致加载不同的文档,该事件也不会触发。

用法:使用的时候,可以为popstate事件指定回调函数。这个回调函数的参数是一个event事件对象,它的state属性指向pushState和replaceState方法为当前URL所提供的状态对象(即这两个方法的第一个参数)。

HISTORY实现SPA前端路由代码

<a class="spa">abc.html</a>
<a class="spa">123.html</a>
<a href="/rdhub" class="spa ">rdhub</a>
 // 注册路由
 document.querySelectorAll('.spa').forEach(item => {
 item.addEventListener('click', e => {
 e.preventDefault();
 let link = item.textContent;
 if (!!(window.history && history.pushState)) {
 // 支持History API
 window.history.pushState({name: 'history'}, link, link);
 } else {
 // 不支持,可使用一些Polyfill库来实现
 }
 }, false)
 });
 // 监听路由
 window.addEventListener('popstate', e => {
 console.log({
 location: location.href,
 state: e.state
 })
 }, false)
popstate监听函数里打印的e.state便是history.pushState()里传入的第一个参数,在这里即为{name: 'history'}

hash

hash基本介绍

url 中可以带有一个 hash http://localhost:9000/#/rdhub.html

window 对象中有一个事件是 onhashchange,以下几种情况都会触发这个事件:

直接更改浏览器地址,在最后面增加或改变#hash;
通过改变location.href或location.hash的值;
通过触发点击带锚点的链接;
浏览器前进后退可能导致hash的变化,前提是两个网页地址中的hash值不同。
hash实现SPA前端路由代码

<a href="/rdhub" class="spa">rdhub</a>
<a href="/abc" class="spa">abc</a>
<a href="/123" class="spa">123</a>
<a href="/hash" class="spa">hash</a>
 document.querySelectorAll('.spa').forEach(item => {
 item.addEventListener('click', e => {
 e.preventDefault();
 let link = item.textContent;
 location.hash = link;
 }, false)
 });
 // 监听路由
 window.addEventListener('hashchange', e => {
 console.log({
 location: location.href,
 hash: location.hash
 })//欢迎加入全栈开发交流群一起学习交流:864305860
 }, false)

hash模式与history模式,这两种模式都是通过浏览器接口实现的,除此之外vue-router还为非浏览器环境准备了一个abstract模式,其原理为用一个数组stack模拟出浏览器历史记录栈的功能。当然,以上只是一些核心逻辑,为保证系统的鲁棒性源码中还有大量的辅助逻辑,也很值得学习。

两种模式比较

pushState设置的新URL可以是与当前URL同源的任意URL;而hash只可修改#后面的部分,故只可设置与当前同文档的URL

pushState设置的新URL可以与当前URL一模一样,这样也会把记录添加到栈中;而hash设置的新值必须与原来不一样才会触发记录添加到栈中

pushState通过stateObject可以添加任意类型的数据到记录中;而hash只可添加短字符串

pushState可额外设置title属性供后续使用

history模式的一个问题

我们知道对于单页应用来讲,理想的使用场景是仅在进入应用时加载index.html,后续在的网络操作通过Ajax完成,不会根据URL重新请求页面,但是难免遇到特殊情况,比如用户直接在地址栏中输入并回车,浏览器重启重新加载应用等。

hash模式仅改变hash部分的内容,而hash部分是不会包含在HTTP请求中的:

http://rdhub.cn/#/user/id // 如重新请求只会发送http://rdhub.cn/故在hash模式下遇到根据URL请求页面的情况不会有问题。而history模式则会将URL修改得就和正常请求后端的URL一样http://rdhub.cn/user/id

在此情况下重新向后端发送请求,如后端没有配置对应/user/id的路由处理,则会返回404错误。

官方推荐的解决办法是在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面。同时这么做以后,服务器就不再返回 404 错误页面,因为对于所有路径都会返回 index.html 文件。为了避免这种情况,在 Vue 应用里面覆盖所有的路由情况,然后在给出一个 404 页面。或者,如果是用 Node.js 作后台,可以使用服务端的路由来匹配 URL,当没有匹配到路由的时候返回 404,从而实现 fallback。

Vuex的实现原理

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式,可以帮助我们管理共享状态。
在这里插入图片描述
如何在Vue中使用Vuex?
如下先来回顾一下使用Vuex的正确姿势:

  1. 引入Vuex插件;
// store.js
Vue.use(Vuex);
  1. 将Vuex.Store这个类实例化,并传入一些配置,这里以计数器作为一个例子;
// store.js
const store = new Vuex.Store({
    state:{
        count:0
    },
    mutations:{
        increment(state){
            state.count++;
        },
        del(state){
            state.count--;
        },
    },
    actions:{
        asyncAdd({commit}){
            setTimeout(() => {
                commit("increment");
            }, 2000);
        }
    }
})
  1. 将store的实例配置给Vue
new Vue({
  store,
  render: h => h(App),
}).$mount('#app')
  1. 组件中使用时
<template>
    <div>计数器
        <span>{{$store.state.count}}</span>
        <br/>
        <button @click="this.add">+</button>
        <button @click="this.del">-</button>
        <button @click="this.asyncAdd">异步+</button>
    </div>
</template>
<script>
export default {
    methods:{
        add(){
            this.$store.commit('increment');
        },
        del(){
            this.$store.commit('del');
        },
        asyncAdd(){
            this.$store.dispatch('asyncAdd');
        }
    }
}
</script>
  1. 效果
    页面上点击+时,调用this. s t o r e . c o m m i t ( " x x x " ) 方 法 , 实 现 t h i s . store.commit("xxx")方法,实现this. store.commit("xxx")this.store.state.count的修改

Vuex的核心源码解析:
目标:

  1. 作为插件一定有install方法,可以在其中进行混入,当Vue实例化后挂载前拿到给其配置的store实例,把store放在原型上,以便全局可用;
  2. 持有基本的state,保存实例化router时配置的mutations,actions对象;
  3. 实现commit及dispatch等方法,可对state进行一定的修改;

Vuex的核心源码简版:

let Vue;
class Store {
    // 持有state,并使其响应化
    constructor(options){
        this.state = new Vue({
            data:options.state
        })
        this.mutations = options.mutations;// mutations 是对象
        this.actions = options.actions;// mutations 是对象
        // 绑定this
        this.commit=this.commit.bind(this);
        this.dispatch=this.dispatch.bind(this);
    }
    // 实现commit和dispatch方法
    commit(type,arg){
        this.mutations[type](this.state,arg);
    }
    dispatch(type,arg){
        console.log(this.actions[type])
        return this.actions[type](this,arg)
    }
}
function install(_vue){
    Vue = _vue;
    Vue.mixin({// 为什么用混入?use是先执行,而this指向的是vue实例,是在main.js中后创建的,使用混入才能在vue实例的指定周期里拿到store实例并做些事情
        beforeCreate(){
            if (this.$options.store) {
                Vue.prototype.$store=this.$options.store;
            }
        }
    })
}
export default {
    Store,
    install
}

其实,Vuex.Store是个类,使用他的时候,你给他传入了参数(state,mutations,actions)并让他实例化。你把这个实例配置给了Vue,Vuex帮你把他给了Vue原型上的$store。

Vuex还送给你个commit和dispatch方法让你能有办法改 s t o r e . s t a t e , 当 然 你 也 能 通 过 store.state,当然你也能通过 store.statestore.state方法到你要的状态。

加油,陌生人!(来来往往不陌生,希望对您有所帮助)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值