写点什么

前端 vue 面试题(持续更新中)

作者:bb_xiaxia1998
  • 2022-12-12
    浙江
  • 本文字数:13065 字

    阅读完需:约 43 分钟

Watch 中的 deep:true 是如何实现的

当用户指定了 watch 中的 deep 属性为 true 时,如果当前监控的值是数组类型。会对对象中的每一项进行求值,此时会将当前 watcher存入到对应属性的依赖中,这样数组中对象发生变化时也会通知数据更新


源码相关


get () {     pushTarget(this) // 先将当前依赖放到 Dep.target上     let value     const vm = this.vm     try {         value = this.getter.call(vm, vm)     } catch (e) {         if (this.user) {             handleError(e, vm, `getter for watcher "${this.expression}"`)         } else {             throw e         }     } finally {         if (this.deep) { // 如果需要深度监控         traverse(value) // 会对对象中的每一项取值,取值时会执行对应的get方法     }popTarget() }
复制代码

什么是作用域插槽

插槽


  • 创建组件虚拟节点时,会将组件儿子的虚拟节点保存起来。当初始化组件时,通过插槽属性将儿子进行分类{a:[vnode],b[vnode]}

  • 渲染组件时会拿对应的 slot 属性的节点进行替换操作。(插槽的作用域为父组件)


<app>    <div slot="a">xxxx</div>    <div slot="b">xxxx</div></app> 
slot name="a" slot name="b"
复制代码


作用域插槽


  • 作用域插槽在解析的时候不会作为组件的孩子节点。会解析成函数,当子组件渲染时,会调用此函数进行渲染。(插槽的作用域为子组件)

  • 普通插槽渲染的作用域是父组件,作用域插槽的渲染作用域是当前子组件。



// 插槽
const VueTemplateCompiler = require('vue-template-compiler'); let ele = VueTemplateCompiler.compile(` <my-component> <div slot="header">node</div> <div>react</div> <div slot="footer">vue</div> </my-component> `)
// with(this) { // return _c('my-component', [_c('div', { // attrs: { "slot": "header" },// slot: "header" // }, [_v("node")] // _文本及诶点 )// , _v(" "), // _c('div', [_v("react")]), _v(" "), _c('div', { // attrs: { "slot": "footer" },// slot: "footer" }, [_v("vue")])]) // }
const VueTemplateCompiler = require('vue-template-compiler');let ele = VueTemplateCompiler.compile(` <div> <slot name="header"></slot> <slot name="footer"></slot> <slot></slot> </div> `);
with(this) { return _c('div', [_v("node"), _v(" "), _t(_v("vue")])]), _v(" "), _t("default")], 2) }// _t定义在 core/instance/render-helpers/index.js
复制代码


// 作用域插槽:let ele = VueTemplateCompiler.compile(` <app>        <div slot-scope="msg" slot="footer">{{msg.a}}</div>     </app> `);
// with(this) { // return _c('app', { scopedSlots: _u([{ // // 作用域插槽的内容会被渲染成一个函数 // key: "footer", // fn: function (msg) { // return _c('div', {}, [_v(_s(msg.a))]) } }]) // })// } // }
const VueTemplateCompiler = require('vue-template-compiler');VueTemplateCompiler.compile(` <div><slot name="footer" a="1" b="2"></slot> </div> `);
// with(this) { return _c('div', [_t("footer", null, { "a": "1", "b": "2" })], 2) }
复制代码

router-link 和 router-view 是如何起作用的

分析


vue-router中两个重要组件router-linkrouter-view,分别起到导航作用和内容渲染作用,但是回答如何生效还真有一定难度


回答范例


  1. vue-router中两个重要组件router-linkrouter-view,分别起到路由导航作用和组件内容渲染作用

  2. 使用中router-link默认生成一个a标签,设置to属性定义跳转path。实际上也可以通过custom和插槽自定义最终的展现形式。router-view是要显示组件的占位组件,可以嵌套,对应路由配置的嵌套关系,配合name可以显示具名组件,起到更强的布局作用。

  3. router-link组件内部根据custom属性判断如何渲染最终生成节点,内部提供导航方法navigate,用户点击之后实际调用的是该方法,此方法最终会修改响应式的路由变量,然后重新去routes匹配出数组结果,router-view则根据其所处深度deep在匹配数组结果中找到对应的路由并获取组件,最终将其渲染出来。

双向绑定的原理是什么

我们都知道 Vue 是数据双向绑定的框架,双向绑定由三个重要部分构成


  • 数据层(Model):应用的数据及业务逻辑

  • 视图层(View):应用的展示效果,各类 UI 组件

  • 业务逻辑层(ViewModel):框架封装的核心,它负责将数据与视图关联起来


而上面的这个分层的架构方案,可以用一个专业术语进行称呼:MVVM这里的控制层的核心功能便是 “数据双向绑定” 。自然,我们只需弄懂它是什么,便可以进一步了解数据绑定的原理


理解 ViewModel


它的主要职责就是:


  • 数据变化后更新视图

  • 视图变化后更新数据


当然,它还有两个主要部分组成


  • 监听器(Observer):对所有数据的属性进行监听

  • 解析器(Compiler):对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数

Vue 中修饰符.sync 与 v-model 的区别

sync的作用


  • .sync修饰符可以实现父子组件之间的双向绑定,并且可以实现子组件同步修改父组件的值,相比较与v-model来说,sync修饰符就简单很多了

  • 一个组件上可以有多个.sync修饰符


<!-- 正常父传子 --><Son :a="num" :b="num2" />
<!-- 加上sync之后的父传子 --><Son :a.sync="num" :b.sync="num2" />
<!-- 它等价于 --><Son :a="num" :b="num2" @update:a="val=>num=val" @update:b="val=>num2=val" />
<!-- 相当于多了一个事件监听,事件名是update:a, --><!-- 回调函数中,会把接收到的值赋值给属性绑定的数据项中。 -->
复制代码



v-model的工作原理


<com1 v-model="num"></com1><!-- 等价于 --><com1 :value="num" @input="(val)=>num=val"></com1>
复制代码


  • 相同点

  • 都是语法糖,都可以实现父子组件中的数据的双向通信

  • 区别点

  • 格式不同:v-model="num", :num.sync="num"

  • v-model: @input + value

  • :num.sync: @update:num

  • v-model只能用一次;.sync可以有多个

实现双向绑定

我们还是以Vue为例,先来看看Vue中的双向绑定流程是什么的


  1. new Vue()首先执行初始化,对data执行响应化处理,这个过程发生Observe

  2. 同时对模板执行编译,找到其中动态绑定的数据,从data中获取并初始化视图,这个过程发生在Compile

  3. 同时定义⼀个更新函数和Watcher,将来对应数据变化时Watcher会调用更新函数

  4. 由于data的某个key在⼀个视图中可能出现多次,所以每个key都需要⼀个管家Dep来管理多个Watcher

  5. 将来 data 中数据⼀旦发生变化,会首先找到对应的Dep,通知所有Watcher执行更新函数


流程图如下:



先来一个构造函数:执行初始化,对data执行响应化处理


class Vue {    constructor(options) {      this.$options = options;      this.$data = options.data;  
// 对data选项做响应式处理 observe(this.$data);
// 代理data到vm上 proxy(this);
// 执行编译 new Compile(options.el, this); } }
复制代码


data选项执行响应化具体操作


function observe(obj) {    if (typeof obj !== "object" || obj == null) {      return;    }    new Observer(obj);  }  
class Observer { constructor(value) { this.value = value; this.walk(value); } walk(obj) { Object.keys(obj).forEach((key) => { defineReactive(obj, key, obj[key]); }); } }
复制代码


编译Compile


对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数



class Compile {    constructor(el, vm) {      this.$vm = vm;      this.$el = document.querySelector(el);  // 获取dom      if (this.$el) {        this.compile(this.$el);      }    }    compile(el) {      const childNodes = el.childNodes;       Array.from(childNodes).forEach((node) => { // 遍历子元素        if (this.isElement(node)) {   // 判断是否为节点          console.log("编译元素" + node.nodeName);        } else if (this.isInterpolation(node)) {          console.log("编译插值⽂本" + node.textContent);  // 判断是否为插值文本 {{}}        }        if (node.childNodes && node.childNodes.length > 0) {  // 判断是否有子元素          this.compile(node);  // 对子元素进行递归遍历        }      });    }    isElement(node) {      return node.nodeType == 1;    }    isInterpolation(node) {      return node.nodeType == 3 && /\{\{(.*)\}\}/.test(node.textContent);    }  }  
复制代码


依赖收集


视图中会用到data中某key,这称为依赖。同⼀个key可能出现多次,每次都需要收集出来用⼀个Watcher来维护它们,此过程称为依赖收集多个Watcher需要⼀个Dep来管理,需要更新时由Dep统⼀通知



实现思路


  1. defineReactive时为每⼀个key创建⼀个Dep实例

  2. 初始化视图时读取某个key,例如name1,创建⼀个watcher1

  3. 由于触发name1getter方法,便将watcher1添加到name1对应的Dep

  4. name1更新,setter触发时,便可通过对应Dep通知其管理所有Watcher更新


// 负责更新视图  class Watcher {    constructor(vm, key, updater) {      this.vm = vm      this.key = key      this.updaterFn = updater  
// 创建实例时,把当前实例指定到Dep.target静态属性上 Dep.target = this // 读一下key,触发get vm[key] // 置空 Dep.target = null }
// 未来执行dom更新函数,由dep调用的 update() { this.updaterFn.call(this.vm, this.vm[this.key]) } }
复制代码


声明Dep


class Dep {    constructor() {      this.deps = [];  // 依赖管理    }    addDep(dep) {      this.deps.push(dep);    }    notify() {       this.deps.forEach((dep) => dep.update());    }  } 
复制代码


创建watcher时触发getter


class Watcher {    constructor(vm, key, updateFn) {      Dep.target = this;      this.vm[this.key];      Dep.target = null;    }  }  
复制代码


依赖收集,创建Dep实例


function defineReactive(obj, key, val) {    this.observe(val);    const dep = new Dep();    Object.defineProperty(obj, key, {      get() {        Dep.target && dep.addDep(Dep.target);// Dep.target也就是Watcher实例        return val;      },      set(newVal) {        if (newVal === val) return;        dep.notify(); // 通知dep执行更新方法      },    });  }  
复制代码


参考 前端进阶面试题详细解答

Vue 要做权限管理该怎么做?控制到按钮级别的权限怎么做?

分析


  • 综合实践题目,实际开发中经常需要面临权限管理的需求,考查实际应用能力。

  • 权限管理一般需求是两个:页面权限和按钮权限,从这两个方面论述即可。



思路


  • 权限管理需求分析:页面和按钮权限

  • 权限管理的实现方案:分后端方案和前端方案阐述

  • 说说各自的优缺点


回答范例


  1. 权限管理一般需求是页面权限和按钮权限的管理

  2. 具体实现的时候分后端和前端两种方案:


  • 前端方案 会把所有路由信息在前端配置,通过路由守卫要求用户登录,用户登录后根据角色过滤出路由表。比如我会配置一个asyncRoutes数组,需要认证的页面在其路由的meta中添加一个roles字段,等获取用户角色之后取两者的交集,若结果不为空则说明可以访问。此过滤过程结束,剩下的路由就是该用户能访问的页面,最后通过router.addRoutes(accessRoutes)方式动态添加路由即可

  • 后端方案 会把所有页面路由信息存在数据库中,用户登录的时候根据其角色查询得到其能访问的所有页面路由信息返回给前端,前端再通过addRoutes动态添加路由信息

  • 按钮权限的控制通常会实现一个指令,例如v-permission,将按钮要求角色通过值传给 v-permission指令,在指令的moutned钩子中可以判断当前用户角色和按钮是否存在交集,有则保留按钮,无则移除按钮


  1. 纯前端方案的优点是实现简单,不需要额外权限管理页面,但是维护起来问题比较大,有新的页面和角色需求就要修改前端代码重新打包部署;服务端方案就不存在这个问题,通过专门的角色和权限管理页面,配置页面和按钮权限信息到数据库,应用每次登陆时获取的都是最新的路由信息,可谓一劳永逸!


可能的追问


  1. 类似Tabs这类组件能不能使用v-permission指令实现按钮权限控制?


<el-tabs>   <el-tab-pane label="⽤户管理" name="first">⽤户管理</el-tab-pane>     <el-tab-pane label="⻆⾊管理" name="third">⻆⾊管理</el-tab-pane></el-tabs>
复制代码


  1. 服务端返回的路由信息如何添加到路由器中?


// 前端组件名和组件映射表const map = {  //xx: require('@/views/xx.vue').default // 同步的⽅式  xx: () => import('@/views/xx.vue') // 异步的⽅式}// 服务端返回的asyncRoutesconst asyncRoutes = [  { path: '/xx', component: 'xx',... }]// 遍历asyncRoutes,将component替换为map[component]function mapComponent(asyncRoutes) {  asyncRoutes.forEach(route => {    route.component = map[route.component];    if(route.children) {      route.children.map(child => mapComponent(child))    }    })}mapComponent(asyncRoutes)
复制代码

怎样理解 Vue 的单向数据流

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


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


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


有两种常见的试图改变一个 prop 的情形 :


  1. 这个 prop 用来传递一个初始值;这个子组件接下来希望将其作为一个本地的 prop 数据来使用。 在这种情况下,最好定义一个本地的 data 属性并将这个 prop用作其初始值


props: ['initialCounter'],data: function () {  return {    counter: this.initialCounter  }}
复制代码


  1. 这个 prop 以一种原始的值传入且需要进行转换。 在这种情况下,最好使用这个 prop 的值来定义一个计算属性


props: ['size'],computed: {  normalizedSize: function () {    return this.size.trim().toLowerCase()  }}
复制代码

了解 history 有哪些方法吗?说下它们的区别

history 这个对象在html5的时候新加入两个api history.pushState()history.repalceState() 这两个API可以在不进行刷新的情况下,操作浏览器的历史纪录。唯一不同的是,前者是新增一个历史记录,后者是直接替换当前的历史记录。


从参数上来说:


window.history.pushState(state,title,url)//state:需要保存的数据,这个数据在触发popstate事件时,可以在event.state里获取//title:标题,基本没用,一般传null//url:设定新的历史纪录的url。新的url与当前url的origin必须是一样的,否则会抛出错误。url可以时绝对路径,也可以是相对路径。//如 当前url是 https://www.baidu.com/a/,执行history.pushState(null, null, './qq/'),则变成 https://www.baidu.com/a/qq/,//执行history.pushState(null, null, '/qq/'),则变成 https://www.baidu.com/qq/
window.history.replaceState(state,title,url)//与pushState 基本相同,但她是修改当前历史纪录,而 pushState 是创建新的历史纪录
复制代码


另外还有:


  • window.history.back() 后退

  • window.history.forward()前进

  • window.history.go(1) 前进或者后退几步


从触发事件的监听上来说:


  • pushState()replaceState()不能被popstate事件所监听

  • 而后面三者可以,且用户点击浏览器前进后退键时也可以

Vue 组件渲染和更新过程

渲染组件时,会通过 Vue.extend 方法构建子组件的构造函数,并进行实例化。最终手动调用$mount() 进行挂载。更新组件时会进行 patchVnode 流程,核心就是diff算法


v-if 和 v-show 区别

  • v-show隐藏则是为该元素添加css--display:nonedom元素依旧还在。v-if显示隐藏是将dom元素整个添加或删除

  • 编译过程:v-if切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件;v-show只是简单的基于css切换

  • 编译条件:v-if是真正的条件渲染,它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。只有渲染条件为假时,并不做操作,直到为真才渲染

  • v-showfalse变为true的时候不会触发组件的生命周期

  • v-iffalse变为true的时候,触发组件的beforeCreatecreatebeforeMountmounted钩子,由true变为false的时候触发组件的beforeDestorydestoryed方法

  • 性能消耗:v-if有更高的切换消耗;v-show有更高的初始渲染消耗


v-show 与 v-if 的使用场景


  • v-ifv-show 都能控制dom元素在页面的显示

  • v-if 相比 v-show 开销更大的(直接操作dom节点增加与删除)

  • 如果需要非常频繁地切换,则使用 v-show 较好

  • 如果在运行时条件很少改变,则使用 v-if 较好


v-show 与 v-if 原理分析


  1. v-show原理


不管初始条件是什么,元素总是会被渲染


我们看一下在 vue 中是如何实现的


代码很好理解,有transition就执行transition,没有就直接设置display属性


// https://github.com/vuejs/vue-next/blob/3cd30c5245da0733f9eb6f29d220f39c46518162/packages/runtime-dom/src/directives/vShow.tsexport const vShow: ObjectDirective<VShowElement> = {  beforeMount(el, { value }, { transition }) {    el._vod = el.style.display === 'none' ? '' : el.style.display    if (transition && value) {      transition.beforeEnter(el)    } else {      setDisplay(el, value)    }  },  mounted(el, { value }, { transition }) {    if (transition && value) {      transition.enter(el)    }  },  updated(el, { value, oldValue }, { transition }) {    // ...  },  beforeUnmount(el, { value }) {    setDisplay(el, value)  }}
复制代码


  1. v-if原理


v-if在实现上比v-show要复杂的多,因为还有else else-if 等条件需要处理,这里我们也只摘抄源码中处理 v-if 的一小部分


返回一个node节点,render函数通过表达式的值来决定是否生成DOM


// https://github.com/vuejs/vue-next/blob/cdc9f336fd/packages/compiler-core/src/transforms/vIf.tsexport const transformIf = createStructuralDirectiveTransform(  /^(if|else|else-if)$/,  (node, dir, context) => {    return processIf(node, dir, context, (ifNode, branch, isRoot) => {      // ...      return () => {        if (isRoot) {          ifNode.codegenNode = createCodegenNodeForBranch(            branch,            key,            context          ) as IfConditionalExpression        } else {          // attach this branch's codegen node to the v-if root.          const parentCondition = getParentCondition(ifNode.codegenNode!)          parentCondition.alternate = createCodegenNodeForBranch(            branch,            key + ifNode.branches.length - 1,            context          )        }      }    })  })
复制代码

说说 vue 内置指令

$route$router的区别

  • $route是“路由信息对象”,包括pathparamshashqueryfullPathmatchedname等路由信息参数。

  • $router是“路由实例”对象包括了路由的跳转方法,钩子函数等

v-if 和 v-for 哪个优先级更高

  • 实践中不应该把v-forv-if放一起

  • vue2中,v-for的优先级是高于v-if,把它们放在一起,输出的渲染函数中可以看出会先执行循环再判断条件,哪怕我们只渲染列表中一小部分元素,也得在每次重渲染的时候遍历整个列表,这会比较浪费;另外需要注意的是在vue3中则完全相反,v-if的优先级高于v-for,所以v-if执行时,它调用的变量还不存在,就会导致异常

  • 通常有两种情况下导致我们这样做:

  • 为了过滤列表中的项目 (比如 v-for="user in users" v-if="user.isActive")。此时定义一个计算属性 (比如 activeUsers),让其返回过滤后的列表即可(比如users.filter(u=>u.isActive)

  • 为了避免渲染本应该被隐藏的列表 (比如 v-for="user in users" v-if="shouldShowUsers")。此时把 v-if 移动至容器元素上 (比如 ulol)或者外面包一层template即可

  • 文档中明确指出永远不要把 v-ifv-for 同时用在同一个元素上,显然这是一个重要的注意事项

  • 源码里面关于代码生成的部分,能够清晰的看到是先处理v-if还是v-for,顺序上vue2vue3正好相反,因此产生了一些症状的不同,但是不管怎样都是不能把它们写在一起的


vue2.x 源码分析


在 vue 模板编译的时候,会将指令系统转化成可执行的render函数


编写一个p标签,同时使用v-ifv-for


<div id="app">  <p v-if="isShow" v-for="item in items">    {{ item.title }}  </p></div>
复制代码


创建vue实例,存放isShowitems数据


const app = new Vue({  el: "#app",  data() {    return {      items: [        { title: "foo" },        { title: "baz" }]    }  },  computed: {    isShow() {      return this.items && this.items.length > 0    }  }})
复制代码


模板指令的代码都会生成在render函数中,通过app.$options.render就能得到渲染函数


ƒ anonymous() {  with (this) { return     _c('div', { attrs: { "id": "app" } },     _l((items), function (item)     { return (isShow) ? _c('p', [_v("\n" + _s(item.title) + "\n")]) : _e() }), 0) }}
复制代码


  • _lvue的列表渲染函数,函数内部都会进行一次if判断

  • 初步得到结论:v-for优先级是比v-if 高

  • 再将v-forv-if置于不同标签


<div id="app">  <template v-if="isShow">    <p v-for="item in items">{{item.title}}</p>  </template></div>
复制代码


再输出下render函数


ƒ anonymous() {  with(this){return     _c('div',{attrs:{"id":"app"}},    [(isShow)?[_v("\n"),    _l((items),function(item){return _c('p',[_v(_s(item.title))])})]:_e()],2)}}
复制代码


这时候我们可以看到,v-forv-if作用在不同标签时候,是先进行判断,再进行列表的渲染


我们再在查看下 vue 源码


源码位置:\vue-dev\src\compiler\codegen\index.js


export function genElement (el: ASTElement, state: CodegenState): string {  if (el.parent) {    el.pre = el.pre || el.parent.pre  }  if (el.staticRoot && !el.staticProcessed) {    return genStatic(el, state)  } else if (el.once && !el.onceProcessed) {    return genOnce(el, state)  } else if (el.for && !el.forProcessed) {    return genFor(el, state)  } else if (el.if && !el.ifProcessed) {    return genIf(el, state)  } else if (el.tag === 'template' && !el.slotTarget && !state.pre) {    return genChildren(el, state) || 'void 0'  } else if (el.tag === 'slot') {    return genSlot(el, state)  } else {    // component or element    ...}
复制代码


在进行if判断的时候,v-for是比v-if先进行判断


最终结论:v-for优先级比v-if

什么是递归组件?举个例子说明下?

分析


递归组件我们用的比较少,但是在TreeMenu这类组件中会被用到。


体验


组件通过组件名称引用它自己,这种情况就是递归组件


<template>  <li>    <div> {{ model.name }}</div>    <ul v-show="isOpen" v-if="isFolder">      <!-- 注意这里:组件递归渲染了它自己 -->      <TreeItem        class="item"        v-for="model in model.children"        :model="model">      </TreeItem>    </ul>  </li><script>export default {  name: 'TreeItem',  // ...}</script>
复制代码


回答范例


  1. 如果某个组件通过组件名称引用它自己,这种情况就是递归组件。

  2. 实际开发中类似TreeMenu这类组件,它们的节点往往包含子节点,子节点结构和父节点往往是相同的。这类组件的数据往往也是树形结构,这种都是使用递归组件的典型场景。

  3. 使用递归组件时,由于我们并未也不能在组件内部导入它自己,所以设置组件name属性,用来查找组件定义,如果使用SFC,则可以通过SFC文件名推断。组件内部通常也要有递归结束条件,比如model.children这样的判断。

  4. 查看生成渲染函数可知,递归组件查找时会传递一个布尔值给resolveComponent,这样实际获取的组件就是当前组件本身


原理


递归组件编译结果中,获取组件时会传递一个标识符 _resolveComponent("Comp", true)


const _component_Comp = _resolveComponent("Comp", true)
复制代码


就是在传递maybeSelfReference


export function resolveComponent(  name: string,  maybeSelfReference?: boolean): ConcreteComponent | string {  return resolveAsset(COMPONENTS, name, true, maybeSelfReference) || name}
复制代码


resolveAsset中最终返回的是组件自身:


if (!res && maybeSelfReference) {    // fallback to implicit self-reference    return Component}
复制代码

说说 Vue 的生命周期吧

什么时候被调用?


  • beforeCreate :实例初始化之后,数据观测之前调用

  • created:实例创建万之后调用。实例完成:数据观测、属性和方法的运算、 watch/event 事件回调。无 $el .

  • beforeMount:在挂载之前调用,相关 render 函数首次被调用

  • mounted:了被新创建的vm.$el替换,并挂载到实例上去之后调用改钩子。

  • beforeUpdate:数据更新前调用,发生在虚拟 DOM 重新渲染和打补丁,在这之后会调用改钩子。

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

  • beforeDestroy:实例销毁前调用,实例仍然可用。

  • destroyed:实例销毁之后调用,调用后,Vue 实例指示的所有东西都会解绑,所有事件监听器和所有子实例都会被移除


每个生命周期内部可以做什么?


  • created:实例已经创建完成,因为他是最早触发的,所以可以进行一些数据、资源的请求。

  • mounted:实例已经挂载完成,可以进行一些 DOM 操作。

  • beforeUpdate:可以在这个钩子中进一步的更改状态,不会触发重渲染。

  • updated:可以执行依赖于 DOM 的操作,但是要避免更改状态,可能会导致更新无线循环。

  • destroyed:可以执行一些优化操作,清空计时器,解除绑定事件。


ajax 放在哪个生命周期?:一般放在 mounted 中,保证逻辑统一性,因为生命周期是同步执行的, ajax 是异步执行的。单数服务端渲染 ssr 同一放在 created 中,因为服务端渲染不支持 mounted 方法。 什么时候使用 beforeDestroy?:当前页面使用 $on ,需要解绑事件。清楚定时器。解除事件绑定, scroll mousemove

请说明 Vue 中 key 的作用和原理,谈谈你对它的理解


  • key是为Vue中的VNode标记的唯一id,在patch过程中通过key可以判断两个虚拟节点是否是相同节点,通过这个key,我们的diff操作可以更准确、更快速

  • diff算法的过程中,先会进行新旧节点的首尾交叉对比,当无法匹配的时候会用新节点的key与旧节点进行比对,然后检出差异

  • 尽量不要采用索引作为key

  • 如果不加key,那么vue会选择复用节点(Vue 的就地更新策略),导致之前节点的状态被保留下来,会产生一系列的bug

  • 更准确 :因为带 key 就不是就地复用了,在 sameNode 函数 a.key === b.key 对比中可以避免就地复用的情况。所以会更加准确。

  • 更快速key的唯一性可以被Map数据结构充分利用,相比于遍历查找的时间复杂度O(n)Map的时间复杂度仅仅为O(1),比遍历方式更快。


源码如下:


function createKeyToOldIdx (children, beginIdx, endIdx) {  let i, key  const map = {}  for (i = beginIdx; i <= endIdx; ++i) {    key = children[i].key    if (isDef(key)) map[key] = i  }  return map}
复制代码


回答范例


分析


这是一道特别常见的问题,主要考查大家对虚拟DOMpatch细节的掌握程度,能够反映面试者理解层次


思路分析:


  • 给出结论,key的作用是用于优化patch性能

  • key的必要性

  • 实际使用方式

  • 总结:可从源码层面描述一下vue如何判断两个节点是否相同


回答范例:


  1. key的作用主要是为了更高效的更新虚拟DOM

  2. vuepatch过程中 判断两个节点是否是相同节点是key是一个必要条件 ,渲染一组列表时,key往往是唯一标识,所以如果不定义key的话,vue只能认为比较的两个节点是同一个,哪怕它们实际上不是,这导致了频繁更新元素,使得整个patch过程比较低效,影响性能

  3. 实际使用中在渲染一组列表时key必须设置,而且必须是唯一标识,应该避免使用数组索引作为key,这可能导致一些隐蔽的bugvue中在使用相同标签元素过渡切换时,也会使用key属性,其目的也是为了让vue可以区分它们,否则vue只会替换其内部属性而不会触发过渡效果

  4. 从源码中可以知道,vue判断两个节点是否相同时主要判断两者的key标签类型(如div)等,因此如果不设置key,它的值就是undefined,则可能永远认为这是两个相同节点,只能去做更新操作,这造成了大量的dom更新操作,明显是不可取的


如果不使用 keyVue 会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。key 是为 Vuevnode 的唯一标记,通过这个 key,我们的 diff 操作可以更准确、更快速



diff 程可以概括为:oldChnewCh各有两个头尾的变量 StartIdxEndIdx,它们的2个变量相互比较,一共有4种比较方式。如果4种比较都没匹配,如果设置了key,就会用key进行比较,在比较的过程中,变量会往中间靠,一旦StartIdx>EndIdx表明oldChnewCh至少有一个已经遍历完了,就会结束比较,这四种比较方式就是旧尾新头旧头新尾


相关代码如下


// 判断两个vnode的标签和key是否相同 如果相同 就可以认为是同一节点就地复用function isSameVnode(oldVnode, newVnode) {  return oldVnode.tag === newVnode.tag && oldVnode.key === newVnode.key;}
// 根据key来创建老的儿子的index映射表 类似 {'a':0,'b':1} 代表key为'a'的节点在第一个位置 key为'b'的节点在第二个位置function makeIndexByKey(children) { let map = {}; children.forEach((item, index) => { map[item.key] = index; }); return map;}// 生成的映射表let map = makeIndexByKey(oldCh);
复制代码

Vue computed 实现

  • 建立与其他属性(如:dataStore)的联系;

  • 属性改变后,通知计算属性重新计算


实现时,主要如下


  • 初始化 data, 使用 Object.defineProperty 把这些属性全部转为 getter/setter

  • 初始化 computed, 遍历 computed 里的每个属性,每个 computed 属性都是一个 watch 实例。每个属性提供的函数作为属性的 getter,使用 Object.defineProperty 转化。

  • Object.defineProperty getter 依赖收集。用于依赖发生变化时,触发属性重新计算。

  • 若出现当前 computed 计算属性嵌套其他 computed 计算属性时,先进行其他的依赖收集

既然 Vue 通过数据劫持可以精准探测数据变化,为什么还需要虚拟 DOM 进行 diff 检测差异

  • 响应式数据变化,Vue确实可以在数据变化时,响应式系统可以立刻得知。但是如果给每个属性都添加watcher用于更新的话,会产生大量的watcher从而降低性能

  • 而且粒度过细也得导致更新不准确的问题,所以vue采用了组件级的watcher配合diff来检测差异

生命周期钩子是如何实现的

Vue 的生命周期钩子核心实现是利用发布订阅模式先把用户传入的的生命周期钩子订阅好(内部采用数组的方式存储)然后在创建组件实例的过程中会一次执行对应的钩子方法(发布)


相关代码如下


export function callHook(vm, hook) {  // 依次执行生命周期对应的方法  const handlers = vm.$options[hook];  if (handlers) {    for (let i = 0; i < handlers.length; i++) {      handlers[i].call(vm); //生命周期里面的this指向当前实例    }  }}
// 调用的时候Vue.prototype._init = function (options) { const vm = this; vm.$options = mergeOptions(vm.constructor.options, options); callHook(vm, "beforeCreate"); //初始化数据之前 // 初始化状态 initState(vm); callHook(vm, "created"); //初始化数据之后 if (vm.$options.el) { vm.$mount(vm.$options.el); }};
复制代码


用户头像

bb_xiaxia1998

关注

还未添加个人签名 2022-09-01 加入

还未添加个人简介

评论

发布
暂无评论
前端vue面试题(持续更新中)_Vue_bb_xiaxia1998_InfoQ写作社区