Skip to content
大纲

Vue

参考题目

  1. Vue 双向绑定原理
  2. 描述下 vue 从初始化页面--修改数据--刷新页面 UI 的过程?
  3. 你是如何理解 Vue 的响应式系统的?
  4. 组件中写 name 选项有什么作用?
  5. vue 中怎么重置 data?
  6. vue 首屏加载优化
  7. vuex 是什么?怎么使用?哪种功能场景使用它?
  8. vuex 有哪几种属性?
  9. 虚拟 DOM 实现原理
  10. Vue 中 key 值的作用?
  11. 什么是 MVVM 框架?
  12. mvvm 和 mvc 区别?它和其它框架(jquery)的区别是什么?哪些场景适合?
  13. MVVM、MVC 和 MVP 的区别是什么?各自有什么应用场景?
  14. Vue 2.x 模板中的指令是如何解析实现的
  15. 简要说明 Vue 2.x 的全链路运作机制
  16. 简单介绍一下 Element UI 的框架设计
  17. 如何理解 Vue 是一个渐进式框架
  18. Vue 里实现跨组件通信的方式有哪些
  19. Vue 中响应式数据是如何做到对某个对象的深层次属性的监听的
  20. 简述一下$nextTick 的用法

watch 和 computed 区别

  • watch 是监听动作,computed 是计算属性
  • watch 没缓存,只要数据变化就执行。computed 有缓存,只在属性变化的时候才去计算。
  • watch 可以执行异步操作,而 computed 不能
  • watch 常用于一个数据影响多个数据,computed 则常用于多个数据影响一个数据

讲一下 Vue 的生命周期?

创建期间的生命周期函数:

  • beforeCreate:实例刚在内存中被创建出来,此时,还没有初始化好 data 和 methods 属性
  • created:实例已经在内存中创建 OK,此时 data 和 methods 已经创建 OK,此时还没有开始 编译模板
  • beforeMount:此时已经完成了模板的编译,但是还没有挂载到页面中。
    • 换句话说,此时页面中的类似 { { msg } } 这样的语法还没有被替换成真正的数据。
  • mounted:此时,已经将编译好的模板,挂载到了页面指定的容器中显示【可以获取 DOM 节点 | 发起异步请求】
    • 用户已经可以看到渲染好的页面了

运行期间的生命周期函数:

  • beforeUpdate:状态更新之前执行此函数,此时 data 中的状态值是最新的,但是界面上显示的 数据还是旧的,因为此时还没有开始重新渲染 DOM 节点
  • updated:实例更新完毕之后调用此函数,此时 data 中的状态值 和 界面上显示的数据,都已经完成了更新,界面已经被重新渲染好了!

销毁期间的生命周期函数:

  • beforeDestroy:实例销毁之前调用。在这一步,实例仍然完全可用。
  • destroyed:Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。

Vue 的父组件和子组件生命周期钩子执行顺序是什么

  1. 加载渲染过程 父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount->子mounted->父mounted
  2. 子组件更新过程 父beforeUpdate->子beforeUpdate->子updated->父updated
  3. 父组件更新过程 父beforeUpdate->父updated
  4. 销毁过程 父beforeDestroy->子beforeDestroy->子destroyed->父destroyed

总结:从外到内,再从内到外

Vue 中父组件如何监听子组件的生命周期?

v-on / $emit

html
<child-comp @child-event="handleChildEvent"></child-comp></div>
js
Vue.component('child-comp', {
  template: '<div></div>',
  data: function () {
    return {
      childMsg: 'Hello, I am Child'
    }
  },
  methods: {},
  mounted() {
    this.$emit('child-event')
  }
})
const app = new Vue({
  el: '#app',
  data: function () {
    return {
      parentData: 'parent Message'
    }
  },
  beforeCreate: function () {
    console.log('before created')
  },
  methods: {
    handleChildEvent() {
      console.log('child mounted')
    }
  }
})

在子组件中的 mounted 钩子函数中调用 this.$emit("child-event"); 向父组件发送 child-event 消息。父组件 @child-event="handleChildEvent" 监听了此消息。

@hook

假如我们这里的子组件是外部的,是不可更改的。那我们父组件监听这个外部子组件中的生命周期钩子函数怎么办呢?

html
<div id="app">
  <child-comp @hook:mounted="handleChildEvent"></child-comp>
</div>
js
Vue.component('child-comp', {
  template: '<div></div>',
  data: function () {
    return {
      childMsg: 'Hello, I am Child'
    }
  },
  methods: {},
  mounted() {
    //this.$emit("child-event");
  }
})
const app = new Vue({
  el: '#app',
  data: function () {
    return {
      parentData: 'parent Message'
    }
  },
  beforeCreate: function () {
    console.log('before created')
  },
  methods: {
    handleChildEvent() {
      console.log('child mounted')
    }
  }
})

把子组件中的 mounted 钩子函数中的 $emit 方法去掉,在父组件中使用 @hook:mounted

更多阅读

组件

现在有个父子组件,我希望在父级中给子组件绑定一个原生 click 事件,这个事件会被触发吗?

html
<div id="app">
  <my-button @click="change"></my-button>
</div>
<script>
  export default {
    methods: {
      change() {
        alert(1)
      }
    }
  }
</script>

答:不能,绑定的该 click 事件会被当做组件上的一个普通属性看待,如果想要使 click 事件生效,可以使用 @click.native='change' 的方式来实现。

为什么 Vue 实例对象中的 data 直接是个对象,而组件内的 data 是个函数,且返回一个对象?

因为组件中是 data:{} 的话,这个 {} 是个对象,引用类型。如果多处地方引用同一个组件的话,则共享同一个 data 对象,这是不合理的。所以需要每次使用组件时,return 一个新的对象,这样就不会共享了。

组件间如何通讯?

  • props/$emit+v-on: 通过 props 将数据自上而下传递,而通过$emit 和 v-on 来向上传递信息。
  • EventBus: 通过 EventBus 进行信息的发布与订阅
  • vuex: 是全局数据管理库,可以通过 vuex 管理全局的数据流
  • $attrs/$listeners: Vue2.4 中加入的$attrs/$listeners可以进行跨级的组件通信
  • provide/inject:以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效,这成为了跨组件通信的基础

Vue 组件间通信六种方式

this.$emit 的返回值是什么?如果需要返回值该怎么办?

this.$emit 的返回值就是 this,即当前子组件 VueComponent。

如果想要有返回值可以如下操作:

子组件

vue
<template>
  <input :value="name" @change="handleChange" />
</template>
<script>
export default {
  props: ['name'],
  methods: {
    handleChange(e) {
      const res = this.$emit('Echange', e.target.value, (val) => {
        console.log(val)
      })
      console.log(res, res === this)
    }
  }
}
</script>

父组件

html
<template>
  <Child :name="name" @Echange="handleEventChange" />
</template>
<script>
  export default {
    data() {
      return {
        name: '',
      }
    }
    methods: {
      handleEventChange(val, callback) {
        this.name = val
        callback("hello")
        return 'hello'
      }
    }
  }
</script>

filter 过滤器

filter 中的 this 是什么?

this 是 undefined,在 filter 中拿不到 vue 实例。filter 应该是个纯函数,不应该依赖外界或者对外界有所影响。如果需要用到 this,可以用 computed 或者 method 代替。

vue 指令

能讲下 v-if 和 v-show 的区别吗?

  • v-if: 是否加载这个元素(一次性的)
  • v-show:控制显示方式 block or none(需要切换的,侧边栏)

因此:如果需要频繁切换 v-show 较好,如果在运行时条件不大可能改变 v-if 较好

v-for 你使用过程中,有遇到什么问题或者关注点吗?

  1. 避免将 v-ifv-for 放在同一个元素上,因为 v-for 优先级比 v-if 更高。例如要渲染 todo 列表中未完成的任务,给 li 标签同时写上 v-for 和 v-if 后会导致每次重新渲染都得遍历整个列表。优化方案是把需要遍历的 todoList 更换为在计算属性上遍历过滤。(Vue 文档有详细说明)
  2. v-for 设置键绑定键值 key。理由见下。

在列表组件中添加 key 属性的作用?

key 的主要作用就是在更新组件时判断两个节点是否相同。相同就复用,不相同就删除旧的创建新的。这样可以更高效的更新虚拟 DOM。

另外如果给列表组件设置了过渡效果,不添加 key 属性会导致过渡效果无法触发。因为不添加 key 会导致 vue 无法区分它们,导致只会替换节点内部属性而不会触发过渡效果。

为什么不建议用 index 作为 key 呢?

更新 DOM 时会出现性能问题

例如我们在使用 index 作为 key 值时想要下面列表进行倒序排列:

html
<li key="0">React</li>
<li key="1">Vue</li>
<li key="2">Angular</li>
<!-- 倒序后↓↓↓↓↓↓↓↓↓↓ -->
<li key="0">Angular</li>
<li key="1">Vue</li>
<li key="2">React</li>

vue 这个时候仅会调换第一和第三项的文本,li 元素则不会调换顺序,因为 vue 发现 key 值也就是 index 没变化。而这会导致 li 里面的文本重新渲染,影响性能。

而如果采用 id 作为 key,则仅仅需要移动下 DOM 就 OK 了,并不需要重新渲染 DOM:

html
<li key="react">React</li>
<li key="vue">Vue</li>
<li key="angular">Angular</li>
<!-- 倒序后↓↓↓↓↓↓↓↓↓↓ -->
<li key="angular">Angular</li>
<li key="vue">Vue</li>
<li key="react">React</li>

会发生一些状态 bug

还是以上面例子为例,给每个 li 元素中加一个 checkbox:

html
<li key="react"><input type="checkbox" />React</li>
<li key="vue"><input type="checkbox" />Vue</li>
<li key="angular"><input type="checkbox" />Angular</li>

如果采用 index 作为 key,选中状态会出现 bug(左边列表采用 index,右边采用 id 形式):

index-vs-id

原因也是 index 没有变化被复用了,导致选中状态永远都是 index=0 的第一项。

阅读更多:

数据响应式(双向绑定)怎么做到的?

原理:Vue 采用 数据劫持 结合 发布者 - 订阅者 模式的方式,通过 Object.defineProperty() 来劫持各个属性的 setter 以及 getter,在数据变动时发布消息给订阅者,触发相应的监听回调。

  1. 第一步:需要 Observe 的数据对象进行递归遍历,包括子属性对象的属性,都加上 setter 和 getter。这样的话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变化。
  2. 第二步:Compile 解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新数据。
  3. 第三步:Watcher 订阅者是 Observer 和 Compile 之间通信的桥梁,主要做的事情有:
    1. 在自身实例化时往属性订阅器(dep)里面添加自己
    2. 自身必须有一个 update() 方法
    3. 待属性变动 dep.notice() 通知时,能调用自身的 update() 方法,并触发 Compile 中绑定的回调,则功成身退。
  4. 第四步:MVVM 作为数据绑定的入口,整合 Observer、Compile 和 Watcher 三者,通过 Observer 来监听自己的 model 数据变化,通过 Compile 来解析编译模板指令,最终利用 Watcher 搭起 Observer 和 Compile 之间的桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据 model 变更的双向绑定效果。

参考:

Vue.js 双向绑定的实现原理

vue 双向数据绑定简单实现 (一)

追问 1:那如果我要监听一个对象属性的删除或添加呢?

受 defineProperty 限制,Vue 无法检测对象属性的删除和添加。所以我们可以利用 Vue 提供的 Vue.set 来解决此问题。

例子:

js
// 有一个 obj:{a:1},想要 this.obj.b=233,不会触发视图更新

Vue.set(this.obj, 'b', 233) or this.$set(this.obj, 'b', 233)

追问 2:为什么对象属性的删除或添加无法触发页面更新

因为 vue 在实例化过程中,深度遍历了 data 下所有属性,把属性全转为 getter/setter。这样才能监听属性变化。所以属性必须在 data 对象上存在才能让 Vue 转换它,这样才能让它是响应的。

当你在对象上新加了一个属性 newProperty,当前新加的这个属性并没有加入 vue 检测数据更新的机制(因为是在初始化之后添加的), vue.$set 是能让 vue 知道你添加了属性,它会给你做处理。

js 实现简单的双向绑定

html
<body>
  <div id="app">
    <input type="text" id="txt" />
    <p id="show"></p>
  </div>

  <script>
    window.onload = function () {
      let obj = {}
      window.obj = obj
      Object.defineProperty(obj, 'txt', {
        get: function () {
          return obj
        },
        set: function (newValue) {
          document.getElementById('txt').value = newValue
          document.getElementById('show').innerHTML = newValue
        }
      })
      document.addEventListener('keyup', function (e) {
        obj.txt = e.target.value
      })
    }
  </script>
</body>

Vue 的响应式原理中 Object.defineProperty 有什么缺陷?为什么在 Vue3.0 采用了 Proxy,抛弃了 Object.defineProperty?

  1. Vue 中使用 Object.defineProperty 进行双向数据绑定时,告知使用者是可以监听数组的,但是只是监听了数组的 push()、pop()、shift()、unshift()、splice()、sort()、reverse() 这八种方法,其他数组的属性检测不到。
  2. Object.defineProperty 无法监控到数组下标的变化,导致通过数组下标添加元素,不能实时响应;
  3. Object.defineProperty 只能劫持对象的属性,因此对每个对象的属性进行遍历时,如果属性值也是对象需要深度遍历,那么就比较麻烦了,所以在比较 Proxy 能完整劫持对象的对比下,选择 Proxy。
  4. Proxy 不仅可以代理对象,还可以代理数组。还可以代理动态增加的属性。

答案参考:Daily-Interview-Question - 第 51 题 更多阅读:实现双向绑定 Proxy 比 defineproperty 优劣如何

Vue 模板渲染的原理是什么?

vue 中的模板 template 无法被浏览器解析并渲染,因为这不属于浏览器的标准,不是正确的 HTML 语法,所有需要将 template 转化成一个 JavaScript 函数,这样浏览器就可以执行这一个函数并渲染出对应的 HTML 元素,就可以让视图跑起来了,这一个转化的过程,就成为模板编译。

模板编译又分三个阶段,解析 parse,优化 optimize,生成 generate,最终生成可执行函数 render。

  • parse 阶段:使用大量的正则表达式对 template 字符串进行解析,将标签、指令、属性等转化为抽象语法树 AST。
  • optimize 阶段:遍历 AST,找到其中的一些静态节点并进行标记,方便在页面重渲染的时候进行 diff 比较时,直接跳过这一些静态节点,优化 runtime 的性能。
  • generate 阶段:将最终的 AST 转化为 render 函数字符串。

来源:https://juejin.im/post/6870374238760894472

Vuex 用过吗?

Vuex 是专为 Vue 应用程序开发的状态管理工具,相当于共享仓库,方便任何组件直接获取和修改。

  • state - 数据【存项目共享状态,是响应式的,store 的数据改变,所有依赖此状态的组件会更新】
    • $store.state.count
  • mutations - 方法【同步函数】
    • inc(state, 参数唯一) {}
    • $store.commit('inc', 2)
  • getters - 包装数据【store 的计算属性,可缓存】
    • show: function(state) {}
    • this.$store.getters.show
    • 传参,返回函数:show(state) {return function(参数) {return ...}}【不会缓存数据】
  • actions -【异步操作】【提交的是 mutations,不直接修改状态】
    • increment(context, num) {context.commit()}
    • this.$store.dispatch('',arg)

使用 Vuex 管理数据,与直接在全局 window 下定义变量相比,有什么区别或者说优势?

全局作用域下定义的数据是静态的,只能通过手动修改,修改后数据变了,但使用这些数据的组件并不会重新渲染,也必须得手动渲染。而且全局作用域下定义太多变量还容易造成变量污染。

Vuex 只要 store 中的数据更新,就会立即渲染所有使用 store 数据的组件。Vuex 使用单向数据流,要想修改 store 数据需要经过 action 层,mutation 层,层次划分明确,便于管理。

Vuex 是通过什么方式提供响应式数据的?

在 Store 构造函数中通过 new Vue({}) 实现的。利用 Vue 来监听 state 下的数据变化,给状态添加 getter、setter。

Vuex 如何区分 state 是外部直接修改,还是通过 mutation 方法修改的?

Vuex 中修改 state 的唯一渠道就是执行 commit('xx', payload) 方法,其底层通过执行 this._withCommit(fn) 设置_committing 标志变量为 true,然后才能修改 state,修改完毕还需要还原_committing 变量。外部修改虽然能够直接修改 state,但是并没有修改_committing 标志位,所以只要 watch 一下 state,state change 时判断是否_committing 值为 true,即可判断修改的合法性。

Vuex 原理

vuex 仅仅是作为 vue 的一个插件而存在,不像 Redux,MobX 等库可以应用于所有框架,vuex 只能使用在 vue 上,很大的程度是因为其高度依赖于 vue 的 computed 依赖检测系统以及其插件系统,

vuex 整体思想诞生于 flux,可其的实现方式完完全全的使用了 vue 自身的响应式设计,依赖监听、依赖收集都属于 vue 对对象 Property set get 方法的代理劫持。最后一句话结束 vuex 工作原理,vuex 中的 store 本质就是没有 template 的隐藏着的 vue 组件。

VueRouter 是什么?你平常是怎么用的?

  • 是什么:Vue-Router 是 Vue 官方的路由管理器

  • 作用:为了页面跳转

  • 原理:监听锚点值改变,渲染指定页面

    js
    <div class="h">我是头部</div>
    <div id="content" class="b"></div>
    <div class="f">我是底部</div>
    <script type="text/javascript">
    //监视锚点值的改变
    window.addEventListener('hashchange', function() {
        var text = '';
        switch (location.hash) {
            case '#/music':
                text = '各种音乐的数据';
                break;
            case '#/movie':
                text = '各种电影的数据';
                break;
        }
        document.getElementById('content').innerHTML = text;
    })
    </script>

动态路由

查询字符串:

  1. 去哪(列表页传参) <router-link :to="{name:'detail',query:{id:1}}">xxx</router-link>
  2. 导航(router 中) { name:'detail' , path:'/detail', component: Detail }
  3. 去了干嘛(详情页接收参数)this.$route.query.id

path 方式:

  1. 去哪里 <router-link :to="{name:'detail',params:{name:1}}">xxx</router-link>
  2. 导航 { name:'detail' , path:'/detail/:name', component: Detail}
  3. 去了干嘛(获取路由参数)this.$route.params.name

编程式导航

js
// name 配 params:
this.$router.push({ name: 'Goods', params: { goodsId: id } })

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

// 参数接收匹配:
this.goodsId = this.$route.query.goodsId
this.goodsId = this.$route.params.goodsId

路由守卫

  • 全局守卫:beforeEach, beforeResolve, afterEach【没有 next】
  • 路由独享守卫:beforeEnter
  • 组件内守卫:beforeRouteEnter【唯一 next 有回调】, Update, Leave

应用

  • 全局守卫:beforeEach(用户登录以及权限判定)

    js
    router.beforeEach((to, from, next) => {
      const isLogin = localStroage.token
      // 个人中心需要登录
      if (to.name === 'Member') {
        isLogin ? next() : next('/login')
      } else {
        next()
      }
    })
  • 组件内守卫:beforeRouteEnter(根据用户从何而来,修改当前组件标题)

    js
    // 详情页组件
    beforeRouteEnter(to, from, next) {
        let title = ''
        title = from.name === 'news.list' ? '新闻详情' : '商品详情'
        // 一定要调用 next,否则无法从列表页跳转到详情页
        next(vm => vm.title = title) // 通过 vm 访问组件实例
    }

讲一下完整的 Vue 路由生命周期

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

实现一个简单路由

js
// hash 路由
class Route {
  constructor() {
    // 路由存储对象
    this.routes = {}
    // 当前 hash
    this.currentHash = ''
    // 绑定 this,避免监听时 this 指向改变
    this.freshRoute = this.freshRoute.bind(this)
    // 监听
    window.addEventListener('load', this.freshRoute, false)
    window.addEventListener('hashchange', this.freshRoute, false)
  }
  // 存储
  storeRoute(path, cb) {
    this.routes[path] = cb || function () {}
  }
  // 更新
  freshRoute() {
    this.currentHash = location.hash.slice(1) || '/'
    this.routes[this.currentHash]()
  }
}

vue 脚手架生成的 router.js 中,有一段 base: process.env.BASE_URL 配置,你知道它引用了谁的路径么?

js
const router = new Router({
  mode: 'history',
  base: process.env.BASE_URL, // <-
  ...

它会和 vue.config.js 中的 publicPath 选项相符,即你的应用会部署到的线上/开发环境的基础路径:

js
const path = require('path')
module.exports = {
  // 旧版叫做 baseURL
  // 线上/开发环境的路径配置
  publicPath: process.env.NODE_ENV === 'production' ? 'http://你的线上环境' : '/',

你是如何使用插槽的?

  • 默认插槽:父<child>html 模板</child> | 子<slot></slot>
  • 具名插槽:父<child><template slot="footer"></template></child> | 子 <slot name="footer"></slot>
  • 作用域插槽:父<child><template slot-scope="user"></template></child> | 子提供数据 <slot :data=data>

阅读更多:深入理解 vue 中的 slot 与 slot-scope

相同名称的插槽是合并还是替换?

  • Vue 2.5 版本,匿名和具名插槽都是合并,作用域插槽是替换
  • Vue 2.6 版本,都是替换(因为新版底层原理一样)

nextTick 的更新原理

Vue 在修改数据后,视图不会立刻更新,而是等同一事件循环中的所有数据变化完成之后,再统一进行视图更新。

nextTick 的回调函数会等到同步任务执行完毕,DOM 更新后才触发。

阅读更多:Vue.nextTick 的原理和用途

让你自己实现一个 nextTick,说说你的思路?(TODO)

待续...

在 Vue 中,子组件为何不可以修改父组件传递的 Prop

为了保证数据的单向流动,便于对数据进行追踪,避免数据混乱。官网有详细的信息 prop

所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。

设想一个场景,某个父组件下不只使用了一个子组件。而且都使用到了这份 prop 数据,那么一旦某个子组件更改了这个 prop 数据,会连带着其他子组件的 prop 数据也被更改。这会导致数据混乱,而且由于修改数据的源头不止一处,在出错后 debug 时难以定位错误原因。

所以我们需要将修改数据的源头统一为父组件,子组件想要改 prop 只能委托父组件帮它。从而保证数据修改源唯一

另外 props 传入的值如果对象的话,是可以直接在子组件里更改的,因为是同一个引用。

如果修改了,Vue 是如何监控到属性的修改并给出警告的

下面的代码就是实现 Vue 提示修改 props 的操作,在组件 initProps 方法的时候,会对 props 进行 defineReactive 操作,传入的第四个参数是自定义的 set 函数,该函数会在触发 props 的 set 方法时执行,当 props 修改了,就会运行这里传入的第四个参数,然后进行判断,如果不是 root 根组件,并且不是更新子组件,那么说明更新的是 props,所以会警告

js
// src/core/instance/state.js 源码路径
function initProps(vm: Component, propsOptions: Object) {
  const propsData = vm.$options.propsData || {}
  const props = (vm._props = {})
  // cache prop keys so that future props updates can iterate using Array
  // instead of dynamic object key enumeration.
  const keys = (vm.$options._propKeys = [])
  const isRoot = !vm.$parent
  // root instance props should be converted
  if (!isRoot) {
    toggleObserving(false)
  }
  for (const key in propsOptions) {
    keys.push(key)
    const value = validateProp(key, propsOptions, propsData, vm)
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      const hyphenatedKey = hyphenate(key)
      if (
        isReservedAttribute(hyphenatedKey) ||
        config.isReservedAttr(hyphenatedKey)
      ) {
        warn(
          `"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
          vm
        )
      }
      defineReactive(props, key, value, () => {
        if (!isRoot && !isUpdatingChildComponent) {
          warn(
            `Avoid mutating a prop directly since the value will be ` +
              `overwritten whenever the parent component re-renders. ` +
              `Instead, use a data or computed property based on the prop's ` +
              `value. Prop being mutated: "${key}"`,
            vm
          )
        }
      })
    } else {
      defineReactive(props, key, value)
    }
    // static props are already proxied on the component's prototype
    // during Vue.extend(). We only need to proxy props defined at
    // instantiation here.
    if (!(key in vm)) {
      proxy(vm, `_props`, key)
    }
  }
  toggleObserving(true)
}

// src/core/observer/index.js
/**
 * Define a reactive property on an Object.
 */
export function defineReactive(
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }

  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter(newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}

如果传入的 props 是基本数据类型,子组件修改父组件传的 props 会警告,并且修改不成功,如果传入的是引用数据类型,那么修改引用数据类型的某个属性值时,对应的 props 也会修改,并且 vue 不会报警告。

虚拟 DOM

什么是虚拟 DOM

Virtual DOM 是 DOM 节点在 JavaScript 中的一种抽象数据结构,之所以需要虚拟 DOM,是因为浏览器中操作 DOM 的代价比较昂贵,频繁操作 DOM 会产生性能问题。虚拟 DOM 的作用是在每一次响应式数据发生变化引起页面重渲染时,Vue 对比更新前后的虚拟 DOM,匹配找出尽可能少的需要更新的真实 DOM,从而达到提升性能的目的。

来源:https://juejin.im/post/6870374238760894472

虚拟 DOM 实现原理?

  • 虚拟 DOM 本质上是 JavaScript 对象,是对真实 DOM 的抽象
  • 状态变更时,记录新树和旧树的差异
  • 最后把差异更新到真正的 dom 中

详细实现见虚拟 DOM 原理?

虚拟 DOM 的优劣如何?

优点:

  • 保证性能下限:虚拟 DOM 可以经过 diff 找出最小差异,然后批量进行 patch,这种操作虽然比不上手动优化,但是比起粗暴的 DOM 操作性能要好很多,因此虚拟 DOM 可以保证性能下限
  • 无需手动操作 DOM: 虚拟 DOM 的 diff 和 patch 都是在一次更新中自动进行的,我们无需手动操作 DOM,极大提高开发效率
  • 跨平台:虚拟 DOM 本质上是 JavaScript 对象,而 DOM 与平台强相关,相比之下虚拟 DOM 可以进行更方便地跨平台操作,例如服务器渲染、移动端开发等等

缺点:

  • 无法进行极致优化:在一些性能要求极高的应用中虚拟 DOM 无法进行针对性的极致优化,比如 VScode 采用直接手动操作 DOM 的方式进行极端的性能优化

Vue 项目能进行哪些性能优化

链接:https://juejin.im/post/6857856269488193549

其它

vue 的优点是什么?

  • 低耦合。视图(View)可以独立于 Model 变化和修改,一个 ViewModel 可以绑定到不同的"View"上,当 View 变化的时候 Model 可以不变,当 Model 变化的时候 View 也可以不变。
  • 可重用性。你可以把一些视图逻辑放在一个 ViewModel 里面,让很多 view 重用这段视图逻辑。
  • 独立开发。开发人员可以专注于业务逻辑和数据的开发(ViewModel),设计人员可以专注于页面设计,使用 Expression Blend 可以很容易设计界面并生成 xml 代码。
  • 可测试。界面素来是比较难于测试的,而现在测试可以针对 ViewModel 来写。

当执行 import vue from‘vue’时发生了什么?

平时开发中,经常会用到这样一个语句:

js
import Vue from 'vue'

由于浏览器兼容性问题,通常这个语法是在 webpack 的构建流搭建的项目中执行的,那么这个语句到底做了什么呢?

其实在 nodejs 中,执行 import 就相当于执行了 require,而 require 被调用,其实会用到 require.resolve 这个函数来查找包的路径,而这个函数在 nodejs 中会有一个关于优先级的算法。先看一下 import Vue from 'vue' 这一句做了什么:

  1. import Vue from 'vue' 解析为 const Vue = require('vue')
  2. require 判断 vue 是否未 nodejs 核心包,如我们常用的:path,fs 等,是则直接导入,否则继续往下走。
  3. vue 非 nodejs 核心包,判断 vue 是否未 '/' 根目录开头,显然不是,继续往下走。
  4. vue 是否为 './'、'/' 或者 '../' 开头,显然不是,继续往下走。
  5. 以上条件都不符合,读取项目目录下 node_modules 包里的包。

到了第五步,import Vue from 'vue' 就找到了 vue 所在的实际位置了,那么问题来了,node_modules 下的 vue 是一个文件夹,而引入的 Vue 是一个 javascript 对象,那它是怎么取到这个对象呢?

其实对于一个 npm 包,内部还有一个文件输出的规则,先看下 node_modules/vue 下的文件结构是怎么样的:

bash
├── LICENSE
├── README.md
├── dist
├── package.json
├── src
└── types

是不是看起来很笼统,其实对于 npm 包,require 的规则是这样的:

  1. 查找 package.json 下是否定义了 main 字段,是则读取 main 字段下定义的入口。
  2. 如果没有 package.json 文件,则读取文件夹下的 index.js 或者 index.node。
  3. 如果都 package.json、index.js、index.node 都找不到,抛出错误 Error: Cannot find module 'some-library'

那么看一下 vue 的 package.json 文件有这么一句:

json
{
  ...
  "main": "dist/vue.runtime.common.js",
  ...
}

到这里就很清晰了:

js
import vue from 'vue'

// 最后转换为
const vue = require('./node_modules/vue/dist/vue.runtime.common.js')

而 vue.runtime.common.js 文件的最后一行是:module.exports = Vue;,就正好跟我们平时使用时的 new Vue({}) 是一致的,这就是 import vue from 'vue' 的过程了。

当然,这个是我们平时使用得最多的导入方式,还有其他一些导入规则,都可以在 nodejs 的文档 中找到。

文章来源https://www.jianshu.com/p/fad3688cbd81

Vue 和 jQuery 有什么区别?

jQuery 是使用选择器(DOMHTML便DOMlabel("label").val();,它还是依赖 DOM 元素的值。

Vue 则是通过 Vue 对象将数据和 View 完全分离开来了。对数据进行操作不再需要引用相应的 DOM 对象,可以说数据和 View 是分离的,他们通过 Vue 对象这个 vm 实现相互的绑定,也就是 MVVM。

谈谈你对 MVVM 开发模式的理解

  • Model 数据模型层,保存页面中的数据
  • View 视图层,页面数据的展示;
  • ViewModel M,V 调度者。双向数据绑定。通过监听 Model 数据的改变来控制 V 的更新,处理用户交互操作;

M 数据改变 =>VM => 触发 View 更新

V 用户操作 => VM => M 数据同步更新

开发者只需要专注对数据的维护操作即可,而不需要自己操作 dom。

请你讲一下 Vue 项目中使用 token 登录的具体流程

对 token 完全不了解的同学可以查看我博客中转载的 这篇文章

不想看长篇大论的我这里简单描述下基于 token 身份验证的整个流程:

  1. 用户通过账户名和密码发送登录请求
  2. 服务端对账户的有效性进行验证
  3. 验证成功后再利用「密钥」和「加密算法」(如:HMAC-SHA256)对「用户数据」(如账号信息)做一个签名的 token 返回给客户端
  4. 客户端本地存储 token,并在每次请求的 header 中带上 token
  5. 服务端验证 token 并返回数据

对于 Vue 项目来说,具体流程如下:

  1. 客户端:登录页带上用户名和密码请求登录接口
  2. 服务端:接收请求并在数据库中查询账户的有效性
  3. 服务端:查询通过后利用「密钥」和「加密算法」对「用户数据」做签名 token 并返回给客户端此 token(此 token 有时效性)
  4. 客户端:本地存储 token(如 localStorage + Vuex)
  5. 客户端:每次路由跳转前都要判断 localStorage 是否存在 token,有则正常跳转,无则跳转回登录页
  6. 客户端:每次发送请求时,在 Axios 请求头里携带上 token
  7. 服务端:接收请求并判断请求头有无 token,有且 token 没有过期,正常返回数据;无或 token 失效返回 401 状态码
  8. 客户端:一旦发现 401 则重定向到登录页

一般回答完毕后面试官还会追问一些细节,这里列举两个常问的:

  1. 你是如何利用 Axios 实现携带 token 以及 401 状态码判定的?

    利用 Axios 的请求/响应拦截。使用 axios.interceptors.request.use 进行请求拦截,判断 localStorage 是否有 token,有则在请求头里携带 token。使用 axios.interceptors.response.use 进行响应拦截,判断 response.status 是否为 401,是则代表 token 失效,清空本地 token,跳转登录页

  2. 你是如何监控路由跳转,并在没有 token 时跳转回登录页的?

    使用 Vue Router 的全局路由守卫 router.beforeEach,该方法接收三个参数:to、from 和 next,如果用户访问的是不需要登录就能访问的页面(如 to.path === '/login'),则直接跳转。否则判断本地是否有 token,有则调用 next() ,无则 next('/login') 跳转回登录页

SPA 的缺点有哪些,如何解决?

  • 不利于 SEO
  • 首屏渲染时间过长

vue 如何优化首页的加载速度?vue 首页白屏是什么问题引起的?如何解决呢?

首页白屏的原因:

单页面应用的 html 是靠 js 生成,因为首屏需要加载很大的 js 文件 (app.js vendor.js),所以当网速差的时候会产生一定程度的白屏

解决办法:

  • 1.将公用的 JS 库通过 script 标签外部引入,减小 app.bundle 的大小,让浏览器并行下载资源文件,提高下载速度;
  • 2.在配置路由时,页面和组件使用懒加载的方式引入,进一步缩小 app.bundle 的体积,在调用某个组件时再加载对应的 js 文件;
  • 3.上骨架屏或 loading 动画,提升用户体验;
  • 4.合理使用 web worker 优化一些计算
  • 5.缓存一定要使用,但是请注意合理使用
  • 6.最后可以借助一些工具进行性能评测,重点调优,例如 chrome 开发者工具的 performance 或 Google PageSpeed Insights 插件协助测试。

说说 Vue2.0 和 Vue3.0 有什么区别

  1. 重构响应式系统,使用 Proxy 替换 Object.defineProperty,使用 Proxy 优势:
  • 可直接监听数组类型的数据变化

  • 监听的目标为对象本身,不需要像 Object.defineProperty 一样遍历每个属性,有一定的性能提升

  • 可拦截 apply、ownKeys、has 等 13 种方法,而 Object.defineProperty 不行

  • 直接实现对象属性的新增/删除

  1. 新增 Composition API,更好的逻辑复用和代码组织

  2. 重构 Virtual DOM

  • 模板编译时的优化,将一些静态节点编译成常量
  • slot 优化,将 slot 编译为 lazy 函数,将 slot 的渲染的决定权交给子组件
  • 模板中内联事件的提取并重用(原本每次渲染都重新生成内联函数)
  1. 代码结构调整,更便于 Tree shaking,使得体积更小

  2. 使用 Typescript 替换 Flow

为什么要新增 Composition API,它能解决什么问题

Vue2.0 中,随着功能的增加,组件变得越来越复杂,越来越难维护,而难以维护的根本原因是 Vue 的 API 设计迫使开发者使用 watch,computed,methods 选项组织代码,而不是实际的业务逻辑。

另外 Vue2.0 缺少一种较为简洁的低成本的机制来完成逻辑复用,虽然可以 minxis 完成逻辑复用,但是当 mixin 变多的时候,会使得难以找到对应的 data、computed 或者 method 来源于哪个 mixin,使得类型推断难以进行。

所以 Composition API 的出现,主要是也是为了解决 Option API 带来的问题,第一个是代码组织问题,Compostion API 可以让开发者根据业务逻辑组织自己的代码,让代码具备更好的可读性和可扩展性,也就是说当下一个开发者接触这一段不是他自己写的代码时,他可以更好的利用代码的组织反推出实际的业务逻辑,或者根据业务逻辑更好的理解代码。

第二个是实现代码的逻辑提取与复用,当然 mixin 也可以实现逻辑提取与复用,但是像前面所说的,多个 mixin 作用在同一个组件时,很难看出 property 是来源于哪个 mixin,来源不清楚,另外,多个 mixin 的 property 存在变量命名冲突的风险。而 Composition API 刚好解决了这两个问题。

SSR 有了解吗?原理是什么?

在客户端请求服务器的时候,服务器到数据库中获取到相关的数据,并且在服务器内部将 Vue 组件渲染成 HTML,并且将数据、HTML 一并返回给客户端,这个在服务器将数据和组件转化为 HTML 的过程,叫做服务端渲染 SSR。

而当客户端拿到服务器渲染的 HTML 和数据之后,由于数据已经有了,客户端不需要再一次请求数据,而只需要将数据同步到组件或者 Vuex 内部即可。除了数据以外,HTML 也结构已经有了,客户端在渲染组件的时候,也只需要将 HTML 的 DOM 节点映射到 Virtual DOM 即可,不需要重新创建 DOM 节点,这个将数据和 HTML 同步的过程,又叫做客户端激活。

使用 SSR 的好处:

  • 有利于 SEO:其实就是有利于爬虫来爬你的页面,因为部分页面爬虫是不支持执行 JavaScript 的,这种不支持执行 JavaScript 的爬虫抓取到的非 SSR 的页面会是一个空的 HTML 页面,而有了 SSR 以后,这些爬虫就可以获取到完整的 HTML 结构的数据,进而收录到搜索引擎中。
  • 白屏时间更短:相对于客户端渲染,服务端渲染在浏览器请求 URL 之后已经得到了一个带有数据的 HTML 文本,浏览器只需要解析 HTML,直接构建 DOM 树就可以。而客户端渲染,需要先得到一个空的 HTML 页面,这个时候页面已经进入白屏,之后还需要经过加载并执行 JavaScript、请求后端服务器获取数据、JavaScript 渲染页面几个过程才可以看到最后的页面。特别是在复杂应用中,由于需要加载 JavaScript 脚本,越是复杂的应用,需要加载的 JavaScript 脚本就越多、越大,这会导致应用的首屏加载时间非常长,进而降低了体验感。

更多详情查看彻底理解服务端渲染 - SSR 原理

MVVM 和 MVC 的区别

  • MVC: MVC 是应用最广泛的软件架构之一,一般 MVC 分为:Model(模型),View(视图),Controller(控制器)。这主要是基于分层的目的,让彼此的职责分开.View 一般用过 Controller 来和 Model 进行联系。Controller 是 Model 和 View 的协调者,View 和 Model 不直接联系。基本都是单向联系。
  1. View 传送指令到 Controller。
  2. Controller 完成业务逻辑后改变 Model 状态。
  3. Model 将新的数据发送至 View,用户得到反馈。
  • MVVM: MVVM 是把 MVC 中的 Controller 改变成了 ViewModel。

View 的变化会自动更新到 ViewModel,ViewModel 的变化也会自动同步到 View 上显示,通过数据来显示视图层。

MVVM 和 MVC 的区别:

  • MVC 中 Controller 演变成 MVVM 中的 ViewModel
  • MVVM 通过数据来显示视图层而不是节点操作
  • MVVM 主要解决了 MVC 中大量的 dom 操作使页面渲染性能降低,加载速度变慢,影响用户体验

vue 的优点

  • 轻量级框架
  • 简单易学
  • 双向数据绑定
  • 组件化
  • 视图,数据,结构分离
  • 虚拟 DOM
  • 运行速度更快

vue 的响应式原理

数据发生变化后,会重新对页面渲染,这就是 Vue 响应式

想完成这个过程,我们需要:

  • 侦测数据的变化
  • 收集视图依赖了哪些数据
  • 数据变化时,自动“通知”需要更新的视图部分,并进行更新

对应专业俗语分别是:

数据劫持 / 数据代理 依赖收集 发布订阅模式

vue 双向数据绑定原理

vue 通过使用双向数据绑定,来实现了 View 和 Model 的同步更新。vue 的双向数据绑定主要是通过使用数据劫持和发布订阅者模式来实现的。

首先我们通过 Object.defineProperty() 方法来对 Model 数据各个属性添加访问器属性,以此来实现数据的劫持,因此当 Model 中的数据发生变化的时候,我们可以通过配置的 setter 和 getter 方法来实现对 View 层数据更新的通知。

数据在 html 模板中一共有两种绑定情况,一种是使用 v-model 来对 value 值进行绑定,一种是作为文本绑定,在对模板引擎进行解析的过程中。

如果遇到元素节点,并且属性值包含 v-model 的话,我们就从 Model 中去获取 v-model 所对应的属性的值,并赋值给元素的 value 值。然后给这个元素设置一个监听事件,当 View 中元素的数据发生变化的时候触发该事件,通知 Model 中的对应的属性的值进行更新。

如果遇到了绑定的文本节点,我们使用 Model 中对应的属性的值来替换这个文本。对于文本节点的更新,我们使用了发布订阅者模式,属性作为一个主题,我们为这个节点设置一个订阅者对象,将这个订阅者对象加入这个属性主题的订阅者列表中。当 Model 层数据发生改变的时候,Model 作为发布者向主题发出通知,主题收到通知再向它的所有订阅者推送,订阅者收到通知后更改自己的数据。

Object.defineProperty 介绍

Object.defineProperty 函数一共有三个参数,第一个参数是需要定义属性的对象,第二个参数是需要定义的属性,第三个是该属性描述符。

一个属性的描述符有一下属性,分别是 value 属性的值, writable 属性是否可写, enumerable 属性是否可枚举, configurable 属性是否可配置修改。 get 属性 当访问该属性时,会调用此函数 set 属性 当属性值被修改时,会调用此函数。

使用 Object.defineProperty() 来进行数据劫持有什么缺点

有一些对属性的操作,使用这种方法无法拦截,比如说通过下标方式修改数组数据或者给对象新增属性,vue 内部通过重写函数解决了这个问题。

在 Vue3.0 中已经不使用这种方式了,而是通过使用 Proxy 对对象进行代理,从而实现数据劫持。使用 Proxy 的好处是它可以完美的监听到任何方式的数据改变,唯一的缺点是兼容性的问题,因为这是 ES6 的语法。

v-if 和 v-show 的区别

  • v-if:每次都会重新删除或创建元素来控制 DOM 结点的存在与否

  • v-show:是切换了元素的样式 display:none,display: block

因而 v-if 有较高的切换性能消耗,v-show 有较高的初始渲染消耗

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

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

简而言之,就是 data 中数据可能会被复用,要保证不同组件调用的时候数据是相同的。

vue 的生命周期函数

  • beforeCreate:

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

    在 new 一个 vue 实例后,只有一些默认的生命周期钩子和默认事件,其他的东西都还没创建。在 beforeCreate 生命周期执行的时候,data 和 methods 中的数据都还没有初始化。不能在这个阶段使用 data 中的数据和 methods 中的方法

  • created:

    在实例创建完成后被立即调用。在这一步,实例已完成以下的配置:数据观测 (data observer),property 和方法的运算,watch/event 事件回调。然而,挂载阶段还没开始,$el property 目前尚不可用。

    data 和 methods 都已经被初始化好了,如果要调用 methods 中的方法,或者操作 data 中的数据,最早可以在这个阶段中操作

  • beforeMount:

    在挂载开始之前被调用:相关的 render 函数首次被调用。

    执行到这个钩子的时候,在内存中已经编译好了模板了,但是还没有挂载到页面中,此时,页面还是旧的

  • mounted:

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

    执行到这个钩子的时候,就表示 Vue 实例已经初始化完成了。此时组件脱离了创建阶段,进入到了运行阶段。如果我们想要通过插件操作页面上的 DOM 节点,最早可以在这个阶段中进行

  • beforeUpdate:

    当执行这个钩子时,页面中的显示的数据还是旧的,data 中的数据是更新后的,页面还没有和最新的数据保持同步

  • updated:

    页面显示的数据和 data 中的数据已经保持同步了,都是最新的

  • beforeDestroy:

    Vue 实例从运行阶段进入到了销毁阶段,这个时候上所有的 data 和 methods,指令,过滤器……都是处于可用状态,还没有真正被销毁

  • destroyed:

    这个时候上所有的 data 和 methods,指令,过滤器……都是处于不可用状态,组件已经被销毁了。

  • activated:

    keep-alive 缓存的组件激活时调用。

  • deactivated:

    keep-alive 缓存的组件停用时调用。

vue 的 activated 和 deactivated 钩子函数

html
<keep-alive>
  <component :is="view"></component>
</keep-alive>

keep-alive包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。

当组件在 <keep-alive> 内被切换,它的 activateddeactivated 这两个生命周期钩子函数将会被对应执行。

  • activatedkeep-alive组件激活时调用,该钩子函数在服务器端渲染期间不被调用。
  • deactivatedkeep-alive组件停用时调用,该钩子函数在服务端渲染期间不被调用。

Vue 中父子组件生命周期执行顺序

在单一组件中,钩子的执行顺序是 beforeCreate-> created -> mounted->... ->destroyed

父子组件生命周期执行顺序:

  • 加载渲染过程

    txt
    父 beforeCreate->父 created->父 beforeMount->子 beforeCreate->子 created->子 beforeMount->子 mounted->父 mounted
  • 更新过程

    txt
    父 beforeUpdate->子 beforeUpdate->子 updated->父 updated
  • 销毁过程

    txt
    父 beforeDestroy->子 beforeDestroy->子 destroyed->父 destroyed
  • 常用钩子简易版

    txt
    父 create->子 created->子 mounted->父 mounted

nextTick 用法

官网解释:

将回调延迟到下次 DOM 更新循环之后执行。在修改数据之后立即使用它,然后等待 DOM 更新。

html
<div class="app">
  <div ref="msgDiv">{{msg}}</div>
  <div v-if="msg1">Message got outside $nextTick: {{msg1}}</div>
  <div v-if="msg2">Message got inside $nextTick: {{msg2}}</div>
  <div v-if="msg3">Message got outside $nextTick: {{msg3}}</div>
  <button @click="changeMsg">Change the Message</button>
</div>
vue
new Vue({ el: '.app', data: { msg: 'Hello Vue.', msg1: '', msg2: '', msg3:'' },
methods: { changeMsg() { this.msg = "Hello world." this.msg1 =
this.$refs.msgDiv.innerHTML this.$nextTick(() => { this.msg2 =
this.$refs.msgDiv.innerHTML }) this.msg3 = this.$refs.msgDiv.innerHTML } } })

vue 中 key 属性的作用

一句话 key 的作用主要是为了高效的更新虚拟 DOM

key 的特殊 attribute 主要用在 Vue 的虚拟 DOM 算法,在新旧 nodes 对比时辨识 VNodes。如果不使用 key,Vue 会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。而使用 key 时,它会基于 key 的变化重新排列元素顺序,并且会移除 key 不存在的元素。

有相同父元素的子元素必须有独特的 key。重复的 key 会造成渲染错误。

Vue 中 key 属性用 index 为什么不行

这是由于 diff 算法的机制所决定的,话不多说,直接上反例:

当我们选中某一个(比如第 3 个),再添加或删除内容的时候就能发现 bug 了

html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  </head>
  <body>
    <div id="app">
      <span>ID:</span><input type="text" v-model="id" /> <span>Name:</span
      ><input type="text" v-model="name" />
      <button @click="handleClick">添加</button>

      <div v-for="(item, index) in list" :key="index">
        <input type="checkbox" />
        <span @click="handleDelete(index)">{{item.id}} --- {{item.name}}</span>
      </div>
    </div>
    <script>
      let vm = new Vue({
        el: '#app',
        data: {
          id: '',
          name: '',
          list: [
            { id: 1, name: '张三' },
            { id: 2, name: '李四' },
            { id: 3, name: '王五' },
            { id: 4, name: '赵六' }
          ]
        },
        methods: {
          handleClick() {
            this.list.unshift({
              id: this.id,
              name: this.name
            })
          },
          handleDelete(index) {
            this.list.splice(index, 1)
          }
        }
      })
    </script>
  </body>
</html>

Vue 的路由模式

hash 模式 与 history 模式

  • hash(即地址栏 URL 中的 # 符号)。
txt
比如这个 URL:www.123.com/#/test,hash 的值为 #/test。

特点:hash 虽然出现在 URL 中,但不会被包括在 HTTP,因为我们 hash 每次页面切换其实切换的是#之后的内容,而#后内容的改变并不会触发地址的改变,
所以不存在向后台发出请求,对后端完全没有影响,因此改变 hash 不会重新加载页面。

每次 hash 发生变化时都会调用 onhashchange 事件

优点:可以随意刷新
  • history(利用了浏览器的历史记录栈)
txt
特点:利用了 HTML5 History Interface 中新增的 pushState() 和 replaceState() 方法。(需要特定浏览器支持)

在当前已有的 back、forward、go 的基础之上,它们提供了对历史记录进行修改的功能。只是当它们执行修改时,虽然改变了当前的 URL,但浏览器不会立即向后端发送请求。

history:可以通过前进 后退控制页面的跳转,刷新是真是的改变 url。

缺点:不能刷新,需要后端进行配置。由于 history 模式下是可以自由修改请求 url,当刷新时如果不对对应地址进行匹配就会返回 404。
但是在 hash 模式下是可以刷新的,前端路由修改的是#中的信息,请求时地址是不会变的

vue 中$router 和$route 的区别

  • this.$route:当前激活的路由的信息对象。每个对象都是局部的,可以获取当前路由的 path, name, params, query 等属性。

  • this.$router:全局的 router 实例。通过 vue 根实例中注入 router 实例,然后再注入到每个子组件,从而让整个应用都有路由功能。其中包含了很多属性和对象(比如 history 对象),任何页面也都可以调用其 push(), replace(), go() 等方法。

Vue diff 算法详解

  • updateChildren

这个函数是用来比较两个结点的子节点

js
updateChildren(parentElm, oldCh, newCh) {
    let oldStartIdx = 0,
        newStartIdx = 0
    let oldEndIdx = oldCh.length - 1
    let oldStartVnode = oldCh[0]
    let oldEndVnode = oldCh[oldEndIdx]
    let newEndIdx = newCh.length - 1
    let newStartVnode = newCh[0]
    let newEndVnode = newCh[newEndIdx]
    let oldKeyToIdx
    let idxInOld
    let elmToMove
    let before
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { // 只有 oldS>oldE 或者 newS>newE 才会终止循环
        if (oldStartVnode == null) { // 对于 vnode.key 的比较,会把 oldVnode = null
            oldStartVnode = oldCh[++oldStartIdx]
        } else if (oldEndVnode == null) {
            oldEndVnode = oldCh[--oldEndIdx]
        } else if (newStartVnode == null) {
            newStartVnode = newCh[++newStartIdx]
        } else if (newEndVnode == null) { // 到这里是找到第一个不为 null 的 oldStartVnode oldEndVnode newStartVnode newEndVnode
            newEndVnode = newCh[--newEndIdx]
        } else if (sameVnode(oldStartVnode, newStartVnode)) { // oldS 指针和 newS 指针对应的结点相同时,将 oldS 和 newS 指针同时向后移一位
            patchVnode(oldStartVnode, newStartVnode)
            oldStartVnode = oldCh[++oldStartIdx]
            newStartVnode = newCh[++newStartIdx]
        } else if (sameVnode(oldEndVnode, newEndVnode)) { // oldE 指针和 newE 指针对应的结点相同时,将 oldE 和 newE 指针同时向前移一位
            patchVnode(oldEndVnode, newEndVnode)
            oldEndVnode = oldCh[--oldEndIdx]
            newEndVnode = newCh[--newEndIdx]
        } else if (sameVnode(oldStartVnode, newEndVnode)) { // oldS 指针和 newE 指针对应的结点相同时,将 oldS 指针对应结点移动到 oldE 指针之后,同时将 oldS 指针向后移动一位,newE 指针向前移动一位
            patchVnode(oldStartVnode, newEndVnode)
            api.insertBefore(parentElm, oldStartVnode.el, api.nextSibling(oldEndVnode.el))
            oldStartVnode = oldCh[++oldStartIdx]
            newEndVnode = newCh[--newEndIdx]
        } else if (sameVnode(oldEndVnode, newStartVnode)) { // oldE 指针和 newS 指针对应的结点相同时,将 oldE 指针对应的结点移动到 oldS 指针之前,同时将 oldE 指针向前移动一位,newS 指针想后移动一位
            patchVnode(oldEndVnode, newStartVnode)
            api.insertBefore(parentElm, oldEndVnode.el, oldStartVnode.el)
            oldEndVnode = oldCh[--oldEndIdx]
            newStartVnode = newCh[++newStartIdx]
        } else { // 使用 key 时的比较
            if (oldKeyToIdx === undefined) {
                oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) // 有 key 生成 index 表
            }
            idxInOld = oldKeyToIdx[newStartVnode.key]
            if (!idxInOld) {
                api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
                newStartVnode = newCh[++newStartIdx]
            } else {
                elmToMove = oldCh[idxInOld]
                if (elmToMove.sel !== newStartVnode.sel) {
                    api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
                } else {
                    patchVnode(elmToMove, newStartVnode)
                    oldCh[idxInOld] = null
                    api.insertBefore(parentElm, elmToMove.el, oldStartVnode.el)
                }
                newStartVnode = newCh[++newStartIdx]
            }
        }
    }
    if (oldStartIdx > oldEndIdx) { // oldVnode 遍历结束了,那就将 newVnode 里 newS 指针和 newE 指针之间的结点添加到 oldVnode 里
        before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].el
        addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx)
    } else if (newStartIdx > newEndIdx) { // newVnode 遍历结束了,那就将 oldVnonde 里 oldS 指针和 oldE 指针之间的结点删除
        removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
    }
}

移动端适配的方法

起因:手机设备屏幕尺寸不一,做移动端的 Web 页面,需要考虑安卓/IOS 的各种尺寸设备上的兼容,针对移动端设备的页面,设计与前端实现怎样做能更好地适配不同屏幕宽度的移动设备;

  1. flex 弹性布局

  2. viewport 适配

    html
    <meta name="viewport" content="width=750,initial-scale=0.5" />

    initial-scale = 屏幕的宽度 / 设计稿的宽度

  3. rem 弹性布局

  4. rem + viewport 缩放

这也是淘宝使用的方案,根据屏幕宽度设定 rem 值,需要适配的元素都使用 rem 为单位,不需要适配的元素还是使用 px 为单位。(1em = 16px)

rem 原理

rem 布局的本质是等比缩放

rem 是(根)字体大小相对单位,也就是说跟当前元素的 font-size 没有关系,而是跟整个 body 的 font-size 有关系。

rem 和 em 的区别

一句话概括:em 相对于父元素,rem 相对于根元素。

  • em

    css
    子元素字体大小的 em 是相对于父元素字体大小
    元素的 width/height/padding/margin 用 em 的话是相对于该元素的 font-size
  • rem

    js
    rem 是全部的长度都相对于根元素,根元素是谁?<html>元素。
    通常做法是给 html 元素设置一个字体大小,然后其他元素的长度单位就为 rem。

移动端 300ms 延迟的原因以及解决方案

移动端 300ms 点击延迟和点击穿透

移动端点击有 300ms 的延迟是因为移动端会有双击缩放的这个操作,因此浏览器在 click 之后要等待 300ms,看用户有没有下一次点击,来判断这次操作是不是双击。

有三种办法来解决这个问题:

  1. 通过 meta 标签禁用网页的缩放。

    html
    <meta name="viewport" content="user-scalable=no" />
  2. 更改默认的视口宽度

    html
    <meta name="viewport" content="width=device-width" />
  3. 调用一些 js 库,比如 FastClick

    FastClick 是 FT Labs 专门为解决移动端浏览器 300 毫秒点击延迟问题所开发的一个轻量级的库。

    FastClick 的实现原理是在检测到 touchend 事件的时候,会通过 DOM 自定义事件立即出发模拟一个 click 事件,并把浏览器在 300ms 之后的 click 事件阻止掉。

Vue 和 React 数据驱动的区别

在数据绑定上来说,vue 的特色是双向数据绑定,而在 react 中是单向数据绑定。

vue 中实现数据绑定靠的是数据劫持(Object.defineProperty())+ 发布 - 订阅模式

vue 中实现双向绑定

html
<input v-model="msg" />

react 中实现双向绑定

html
<input value={this.state.msg} onChange={() => this.handleInputChange()} />

参考