步步向“前”——Vue

192 阅读7分钟

Vue相关

我的技术栈是vue,所以面试被问到vue的只是相对较多,发现其实问的很多文档里都有,但是平常很容易忽略,就“搬”出来备用。

vue的生命周期以及对应的钩子函数、各个周期通常做什么事情

每个 Vue 实例在被创建时都要经过一系列的初始化过程——例如,需要 设置数据监听编译模板将实例挂载到 DOM 并在数据变化时更新 DOM元素等。同时在这个过程中也会运行一些叫做生命周期钩子的函数,这给了用户在不同阶段添加自己的代码的机会。

经典官方文档图:

new Vue({}) 实力初始化

  • beforeCreate: 在实例初始化之后,数据观测 (data observer) 和 event/watcher 事件配置之前被调用。

  • created: 在实例创建完成后被立即调用。在这一步,实例已完成以下的配置:数据观测 (data observer),属性和方法的运算,watch/event 事件回调,(此时watchimmeditae: true的函数也会先于created执行)。然而,挂载阶段还没开始,$el 属性目前尚不可用

  • beforeMount: 在挂载开始之前被调用:相关的 render 函数首次被调用。该钩子在服务器端渲染期间不被调用。

  • mounted: 实例被挂载后调用,这时 el 被新创建的 vm.$el 替换了。如果根实例挂载到了一个文档内的元素上,当 mounted 被调用时 vm.$el 也在文档内。

    注意 mounted 不会保证所有的子组件也都一起被挂载。如果你希望等到整个视图都渲染完毕,可以在 mounted 内部使用 vm.$nextTick

    mounted: function () {
      this.$nextTick(function () {
        // Code that will run only after the
        // entire view has been rendered
      })
    }
    

    该钩子在服务器端渲染期间不被调用。

  • beforeUpdate: 数据更新时调用,发生在虚拟 DOM 打补丁之前。这里适合在更新之前访问现有的 DOM,比如手动移除已添加的事件监听器

    该钩子在服务器端渲染期间不被调用,因为只有初次渲染会在服务端进行

  • updated: 由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。

    当这个钩子被调用时,组件 DOM 已经更新,所以你现在可以执行依赖于 DOM的操作。然而在大多数情况下,你应该避免在此期间更改状态。如果要相应状态改变,通常最好使用计算属性或 watcher 取而代之

    注意 updated 不会保证所有的子组件也都一起被重绘。如果你希望等到整个视图都重绘完毕,可以在 updated 里使用 vm.$nextTick

    该钩子在服务器端渲染期间不被调用。

  • beforeDestroy: 实例销毁之前调用。在这一步,实例仍然完全可用。

    该钩子在服务器端渲染期间不被调用。 在这一步可以进行一些清除操作。比如定时器的清除:

    beforeDestroy: {
        //清除加载的定时器
        cleartInterval(this.timer)
    }
    

    这样的写法存在两个问题:

    • 需要在这个组件实例中保存这个 定时器,如果可以的话最好只有生命周期钩子可以访问到它。这并不算严重的问题,但是它可以被视为杂物。
    • 我们的建立代码独立于我们的清理代码,这使得我们比较难于程序化地清理我们建立的所有东西。

    所以vue文档给了更加“高级”的用法:

        mounted: function () {
          var picker = new Pikaday({
            field: this.$refs.input,
            format: 'YYYY-MM-DD'
          })
        
          this.$once('hook:beforeDestroy', function () {
            picker.destroy()
          })
        }
    
  • destroyed: 实例销毁后调用。该钩子被调用后,对应 Vue 实例的所有指令都被解绑,所有的事件监听器被移除,所有的子实例也都被销毁。

    该钩子在服务器端渲染期间不被调用。

vue的响应式实现原理

当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter

Object.defineProperty(obj, prop, descriptor)

  • obj 要定义属性的对象。
  • prop 要定义或修改的属性的名称或 Symbol 。
  • descriptor 要定义或修改的属性描述符。
  • 返回值 被传递给函数的对象。
Object.defineProperty(o, "b", {
  // 使用了方法名称缩写(ES2015 特性)
  // 下面两个缩写等价于:
  // get : function() { return bValue; },
  // set : function(newValue) { bValue = newValue; },
  get() { return bValue; },  //获取o['b']时的回调函数
  set(newValue) { bValue = newValue; },  //设置o['b']时的回调函数
  enumerable : true,
  configurable : true
});

通过在 Object.defineProperty 中自定义getset 函数,并在 get 中进行依赖收集,在 set 中派发更新。

但是这个方法需要指明对象键值,所以对于一些对象利用下标新增属性的操作无法做到实时监听,数组直接赋值同理,vue对数组的slicepush等能够触发更新是因为重写了数组的这些对应方法。

Vue3.0最新的实现方式,用ProxyReflect来替代Object.definePropertypry的方式。

proxy

Proxy 对象用于定义基本操作的自定义行为(如属性查找、赋值、枚举、函数调用等)。

new Proxy(target, handler) 参数

  • target 要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
  • handler 一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。
et validator = {
  set: function(obj, prop, value) {
    if (prop === 'age') {
      if (!Number.isInteger(value)) {
        throw new TypeError('The age is not an integer');
      }
      if (value > 200) {
        throw new RangeError('The age seems invalid');
      }
    }

    // The default behavior to store the value
    obj[prop] = value;

    // 表示成功
    return true;
  },
  get (obj, prop) {
    return prop in obj ? obj[prop] : 37;
  }
};

let person = new Proxy({}, validator);

person.age = 100;

console.log(person.age); 

proxyObject.defineProperty的直观区别就是 前者少了对象的key参数, 也正是由于参数key的未知性限制了后者对对象下标新增属性的setter/getter的处理,前者是可以做到对对象的基本定义行为进行拦截,而不用不受限于参数key。

Reflect

Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与proxy handlers的方法相同。Reflect不是一个函数对象,因此它是不可构造的。

  • 描述 与大多数全局对象不同,Reflect不是一个构造函数。你不能将其与一个new运算符一起使用,或者将Reflect对象作为一个函数来调用。Reflect的所有属性和方法都是静态的(就像Math对象)。

  • 方法 Reflect对象提供以下静态函数,它们具有与处理器对象方法相同的名称。这些方法中的一些与 Object 上的对应方法相同。如ReflecthasapplydefineProperty等。

data增加响应式属性

data

  • 类型:Object | Function

  • 限制:组件的定义只接受 function

详细:

Vue 实例的数据对象。Vue 将会递归将 data 的属性转换为 getter/setter,从而让 data 的属性能够响应数据变化。对象必须是纯粹的对象 (含有零个或多个的 key/value 对):浏览器 API 创建的原生对象,原型上的属性会被忽略。大概来说,data 应该只能是数据 - 不推荐观察拥有状态行为的对象。

一旦观察过,你就无法在根数据对象上添加响应式属性。因此推荐在创建实例之前,就声明所有的根级响应式属性。

如果需要,可以通过将vm.$data 传入 JSON.parse(JSON.stringify(...)) 得到深拷贝的原始数据对象。

对于新增的对象属性要做到响应式的方法:

Vue.set( target, propertyName/index, value )
vm.$set( target, propertyName/index, value ) //别名

参数:

  • {Object | Array} target
  • {string | number} propertyName/index
  • {any} value 返回值:设置的值。

用法:

向响应式对象中添加一个属性,并确保这个新属性同样是响应式的,且触发视图更新。它必须用于向响应式对象上添加新属性,因为 Vue 无法探测普通的新增属性 (比如 this.myObject.newProperty = 'hi')

注意对象不能是 Vue 实例,或者 Vue 实例的根数据对象

对应的删除属性触发更新的方法 vm.$deleteVue.delete

为什么组件内的data要以函数形式返回

当一个组件被定义,data 必须声明为返回一个初始数据对象的函数,因为组件可能被用来创建多个实例。如果 data 仍然是一个纯粹的对象,则所有的实例将共享引用同一个数据对象!通过提供 data 函数,每次创建一个新实例后,我们能够调用 data 函数,从而返回初始数据的一个全新副本数据对象。

vue-router的实现原理

vue-router浅析

基本原理:视图渲染而不刷新,两种模式hashhistoryhash模式利用监听浏览器锚点变化获取相应视图,不触发其他请求,缺点是只可修改#后面的部分,故只可设置与当前同文档的urlhistory模式则借助浏览器html5historyAPI实现,能让开发人员在不刷新整个页面的情况下修改站点的URL,就是利用 history.pushState API 来完成 URL 跳转而无须重新加载页面,但它可将url修改的就和正常请求后端的url一样(history不带#),需要在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面。

abstract模式是使用一个不依赖于浏览器的浏览历史虚拟管理后端。 vue-router 自身会对环境做校验,如果发现没有浏览器的 API,vue-router 会自动强制进入 abstract 模式,所以 在使用 vue-router 时只要不写 mode 配置即可,默认会在浏览器环境中使用 hash 模式,在移动端原生环境中使用 abstract 模式。 (当然,你也可以明确指定在所有情况下都使用 abstract 模式)

vue-router的路由守卫

完整的导航解析流程

  1. 导航被触发。
  2. 在失活的组件里调用离开守卫beforeRouteLeave
  3. 调用全局的 beforeEach 全局前置守卫
  4. 在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。
  5. 在路由配置里调用 beforeEnter独享守卫
  6. 解析异步路由组件。
  7. 在被激活的组件里调用 beforeRouteEnter
  8. 调用全局的 beforeResolve 全局解析守卫 (2.5+)。
  9. 导航被确认。
  10. 调用全局的 afterEach 全局后置守卫钩子。
  11. 触发 DOM 更新。
  12. 用创建好的实例调用 beforeRouteEnter 守卫中传给 next 的回调函数。

vue-router传参

  • $route.params

类型: Object

一个 key/value 对象,包含了动态片段和全匹配片段,如果没有路由参数,就是一个空对象。

  • $route.query

类型: Object

一个 key/value 对象,表示 URL 查询参数。例如,对于路径 /foo?user=1,则有 $route.query.user == 1,如果没有查询参数,则是个空对象。

const User = {
  template: '<div>User {{ $route.params.id }}</div>'
}
const router = new VueRouter({
  routes: [
    { path: '/user/:id', component: User }
  ]
})

编程式导航:

// 命名的路由
router.push({ name: 'user', params: { userId: '123' }})

// 带查询参数,变成 /register?plan=private
router.push({ path: 'register', query: { plan: 'private' }})  //通过$route.query获取

注意:如果提供了 pathparams 会被忽略,上述例子中的 query 并不属于这种情况。你需要提供路由的 name 或手写完整的带有参数的 path

const userId = '123'
router.push({ name: 'user', params: { userId }}) // -> /user/123
router.push({ path: `/user/${userId}` }) // -> /user/123
// 这里的 params 不生效
router.push({ path: '/user', params: { userId }}) // -> /user

上述例子获取传参时都要使用$route.query/params,这样在组件中使用 $route会使之与其对应路由形成高度耦合,从而使组件只能在某些特定的 URL 上使用,限制了其灵活性。

vue官网推荐使用 props 将组件和路由解耦,即在路由声明组件中声明props,从而是参数以props的形式传入组件内:

props?: boolean | Object | Function

  • 如果 props 被设置为 trueroute.params 将会被设置为组件属性
const User = {
  props: ['id'],
  template: '<div>User {{ id }}</div>'
}
const router = new VueRouter({
  routes: [
    //这里规定参数以props的形式传入组件 组件内获取以props获取
    { path: '/user/:id', component: User, props: true },

    // 对于包含命名视图的路由,你必须分别为每个命名视图添加 `props` 选项:
    {
      path: '/user/:id',
      components: { default: User, sidebar: Sidebar },
      props: { default: true, sidebar: false }
    }
  ]
})
  • props如果是一个对象,它会被按原样设置为组件属性(props接收)。当 props 是静态的时候有用。跟上述例子获取方法一样。
const router = new VueRouter({
  routes: [
    { path: '/promotion/from-newsletter', component: Promotion, props: { newsletterPopup: false } }
  ]
})

<!--组件内以props: ['newsletterPopup']获取-->
  • 可以创建一个函数返回props。这样你便可以将参数转换成另一种类型,将静态值与基于路由的值结合等等。
const router = new VueRouter({
  routes: [
    { path: '/search', component: SearchUser, props: (route) => ({ query: route.query.q }) }
  ]
})

URL /search?q=vue 会将 {query: 'vue'} 作为属性props传递给 SearchUser 组件,组件内以props: ['query']获取。

请尽可能保持 props 函数为无状态的,因为它只会在路由发生变化时起作用。如果你需要状态来定义 props,请使用包装组件,这样 Vue 才可以对状态变化做出反应。

$route与$router的区别

$route只读

一个路由对象 (route object) 表示当前激活的路由的状态信息,包含了当前 URL 解析得到的信息,还有 URL 匹配到的路由记录 (route records)。

路由对象是不可变 (immutable) 的,每次成功的导航后都会产生一个新的对象。

路由对象出现在多个地方:

  • 在组件内,即 this.$route

  • $route 观察者回调内

  • router.match(location) 的返回值

  • 导航守卫的参数:

    router.beforeEach((to, from, next) => {
      // `to` 和 `from` 都是路由对象
    })
    
  • scrollBehavior 方法的参数:

    const router = new VueRouter({
      scrollBehavior(to, from, savedPosition) {
        // `to` 和 `from` 都是路由对象
      }
    })
    

常用api: .path .name .hash .params

$router

router 实例。常用方法:go push pop replace

vue的组件间数据通信的方式(祖父子孙)

  • 父传子: props

  • 子传父: this.$emit([EVENT_NAME], params) 父组件监听@[EVENT_NAME]

  • 兄弟之间: new Vue通过这个实例 结合$emit() $on()

  • 获取父(祖)$parent(.$parent)的方法属性都可以

  • 获取子(孙)$children(.$children)的方法属性都可以

  • provide/inject(需要注意数据并非响应的,按照文档所说如果是一个可监听对象还是可以的,建议在provide提供更新方法,inject页面注入这个实例,并调用这个刷新状态值,vue.js组件精讲小册给的思路)

  • vuex管理一个全局state对象各页面统一数据获取和修改入口

虚拟DOM原理,for循环key的作用

参考:

vueApi

vue生命周期

深入理解MVVM