写点什么

滴滴前端二面 vue 相关面试题

作者:bb_xiaxia1998
  • 2022-10-18
    浙江
  • 本文字数:20374 字

    阅读完需:约 1 分钟

computed 的实现原理

computed 本质是一个惰性求值的观察者。


computed 内部实现了一个惰性的 watcher,也就是 computed watcher,computed watcher 不会立刻求值,同时持有一个 dep 实例。


其内部通过 this.dirty 属性标记计算属性是否需要重新求值。


当 computed 的依赖状态发生改变时,就会通知这个惰性的 watcher,


computed watcher 通过 this.dep.subs.length 判断有没有订阅者,


有的话,会重新计算,然后对比新旧值,如果变化了,会重新渲染。 (Vue 想确保不仅仅是计算属性依赖的值发生变化,而是当计算属性最终计算的值发生变化时才会触发渲染 watcher 重新渲染,本质上是一种优化。)


没有的话,仅仅把 this.dirty = true。 (当计算属性依赖于其他数据时,属性并不会立即重新计算,只有之后其他地方需要读取属性的时候,它才会真正计算,即具备 lazy(懒计算)特性。)

Vue-router 跳转和 location.href 有什么区别

  • 使用 location.href= /url 来跳转,简单方便,但是刷新了页面;

  • 使用 history.pushState( /url ) ,无刷新页面,静态跳转;

  • 引进 router ,然后使用 router.push( /url ) 来跳转,使用了 diff 算法,实现了按需加载,减少了 dom 的消耗。其实使用 router 跳转和使用 history.pushState() 没什么差别的,因为 vue-router 就是用了 history.pushState() ,尤其是在 history 模式下。

$nextTick 原理及作用

Vue 的 nextTick 其本质是对 JavaScript 执行原理 EventLoop 的一种应用。


nextTick 的核心是利用了如 Promise 、MutationObserver、setImmediate、setTimeout 的原生 JavaScript 方法来模拟对应的微/宏任务的实现,本质是为了利用 JavaScript 的这些异步回调任务队列来实现 Vue 框架中自己的异步回调队列。


nextTick 不仅是 Vue 内部的异步队列的调用方法,同时也允许开发者在实际项目中使用这个方法来满足实际应用中对 DOM 更新数据时机的后续逻辑处理


nextTick 是典型的将底层 JavaScript 执行原理应用到具体案例中的示例,引入异步更新队列机制的原因∶


  • 如果是同步更新,则多次对一个或多个属性赋值,会频繁触发 UI/DOM 的渲染,可以减少一些无用渲染

  • 同时由于 VirtualDOM 的引入,每一次状态发生变化后,状态变化的信号会发送给组件,组件内部使用 VirtualDOM 进行计算得出需要更新的具体的 DOM 节点,然后对 DOM 进行更新操作,每次更新状态后的渲染过程需要更多的计算,而这种无用功也将浪费更多的性能,所以异步渲染变得更加至关重要


Vue 采用了数据驱动视图的思想,但是在一些情况下,仍然需要操作 DOM。有时候,可能遇到这样的情况,DOM1 的数据发生了变化,而 DOM2 需要从 DOM1 中获取数据,那这时就会发现 DOM2 的视图并没有更新,这时就需要用到了nextTick了。


由于 Vue 的 DOM 操作是异步的,所以,在上面的情况中,就要将 DOM2 获取数据的操作写在$nextTick中。


this.$nextTick(() => {    // 获取数据的操作...})
复制代码


所以,在以下情况下,会用到 nextTick:


  • 在数据变化后执行的某个操作,而这个操作需要使用随数据变化而变化的 DOM 结构的时候,这个操作就需要方法在nextTick()的回调函数中。

  • 在 vue 生命周期中,如果在 created()钩子进行 DOM 操作,也一定要放在nextTick()的回调函数中。


因为在 created()钩子函数中,页面的 DOM 还未渲染,这时候也没办法操作 DOM,所以,此时如果想要操作 DOM,必须将操作的代码放在nextTick()的回调函数中。

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

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


相关代码如下


import { mutableHandlers } from "./baseHandlers"; // 代理相关逻辑import { isObject } from "./util"; // 工具方法
export function reactive(target) { // 根据不同参数创建不同响应式对象 return createReactiveObject(target, mutableHandlers);}function createReactiveObject(target, baseHandler) { if (!isObject(target)) { return target; } const observed = new Proxy(target, baseHandler); return observed;}
const get = createGetter();const set = createSetter();
function createGetter() { return function get(target, key, receiver) { // 对获取的值进行放射 const res = Reflect.get(target, key, receiver); console.log("属性获取", key); if (isObject(res)) { // 如果获取的值是对象类型,则返回当前对象的代理对象 return reactive(res); } return res; };}function createSetter() { return function set(target, key, value, receiver) { const oldValue = target[key]; const hadKey = hasOwn(target, key); const result = Reflect.set(target, key, value, receiver); if (!hadKey) { console.log("属性新增", key, value); } else if (hasChanged(value, oldValue)) { console.log("属性值被修改", key, value); } return result; };}export const mutableHandlers = { get, // 当获取属性时调用此方法 set, // 当修改属性时调用此方法};
复制代码

vue3 中 watch、watchEffect 区别

  • watch是惰性执行,也就是只有监听的值发生变化的时候才会执行,但是watchEffect不同,每次代码加载watchEffect都会执行(忽略watch第三个参数的配置,如果修改配置项也可以实现立即执行)

  • watch需要传递监听的对象,watchEffect不需要

  • watch只能监听响应式数据:ref定义的属性和reactive定义的对象,如果直接监听reactive定义对象中的属性是不允许的(会报警告),除非使用函数转换一下。其实就是官网上说的监听一个getter

  • watchEffect如果监听reactive定义的对象是不起作用的,只能监听对象中的属性


看一下watchEffect的代码


<template><div>  请输入firstName:  <input type="text" v-model="firstName"></div><div>  请输入lastName:  <input type="text" v-model="lastName"></div><div>  请输入obj.text:  <input type="text" v-model="obj.text"></div> <div> 【obj.text】 {{obj.text}} </div></template>
<script>import {ref, reactive, watch, watchEffect} from 'vue'export default { name: "HelloWorld", props: { msg: String, }, setup(props,content){ let firstName = ref('') let lastName = ref('') let obj= reactive({ text:'hello' }) watchEffect(()=>{ console.log('触发了watchEffect'); console.log(`组合后的名称为:${firstName.value}${lastName.value}`) }) return{ obj, firstName, lastName } }};</script>
复制代码




改造一下代码


watchEffect(()=>{  console.log('触发了watchEffect');  // 这里我们不使用firstName.value/lastName.value ,相当于是监控整个ref,对应第四点上面的结论  console.log(`组合后的名称为:${firstName}${lastName}`)})
复制代码



watchEffect(()=>{  console.log('触发了watchEffect');  console.log(obj);})
复制代码



稍微改造一下


let obj = reactive({  text:'hello'})watchEffect(()=>{  console.log('触发了watchEffect');  console.log(obj.text);})
复制代码



再看一下 watch 的代码,验证一下


let obj= reactive({  text:'hello'})// watch是惰性执行, 默认初始化之后不会执行,只有值有变化才会触发,可通过配置参数实现默认执行watch(obj, (newValue, oldValue) => {  // 回调函数  console.log('触发监控更新了new',  newValue);  console.log('触发监控更新了old',  oldValue);},{  // 配置immediate参数,立即执行,以及深层次监听  immediate: true,  deep: true})
复制代码



  • 监控整个reactive对象,从上面的图可以看到 deep 实际默认是开启的,就算我们设置为false也还是无效。而且旧值获取不到。

  • 要获取旧值则需要监控对象的属性,也就是监听一个getter,看下图




总结


  • 如果定义了reactive的数据,想去使用watch监听数据改变,则无法正确获取旧值,并且deep属性配置无效,自动强制开启了深层次监听。

  • 如果使用 ref 初始化一个对象或者数组类型的数据,会被自动转成reactive的实现方式,生成proxy代理对象。也会变得无法正确取旧值。

  • 用任何方式生成的数据,如果接收的变量是一个proxy代理对象,就都会导致watch这个对象时,watch回调里无法正确获取旧值。

  • 所以当大家使用watch监听对象时,如果在不需要使用旧值的情况,可以正常监听对象没关系;但是如果当监听改变函数里面需要用到旧值时,只能监听 对象.xxx`属性 的方式才行


watch 和 watchEffect 异同总结


体验


watchEffect立即运行一个函数,然后被动地追踪它的依赖,当这些依赖改变时重新执行该函数


const count = ref(0)watchEffect(() => console.log(count.value))// -> logs 0count.value++// -> logs 1
复制代码


watch侦测一个或多个响应式数据源并在数据源变化时调用一个回调函数


const state = reactive({ count: 0 })watch(  () => state.count,  (count, prevCount) => {    /* ... */  })
复制代码


回答范例


  1. watchEffect立即运行一个函数,然后被动地追踪它的依赖,当这些依赖改变时重新执行该函数。watch侦测一个或多个响应式数据源并在数据源变化时调用一个回调函数

  2. watchEffect(effect)是一种特殊watch,传入的函数既是依赖收集的数据源,也是回调函数。如果我们不关心响应式数据变化前后的值,只是想拿这些数据做些事情,那么watchEffect就是我们需要的。watch更底层,可以接收多种数据源,包括用于依赖收集的getter函数,因此它完全可以实现watchEffect的功能,同时由于可以指定getter函数,依赖可以控制的更精确,还能获取数据变化前后的值,因此如果需要这些时我们会使用watch

  3. watchEffect在使用时,传入的函数会立刻执行一次。watch默认情况下并不会执行回调函数,除非我们手动设置immediate选项

  4. 从实现上来说,watchEffect(fn)相当于watch(fn,fn,{immediate:true})


watchEffect定义如下


export function watchEffect(  effect: WatchEffect,  options?: WatchOptionsBase): WatchStopHandle {  return doWatch(effect, null, options)}
复制代码


watch定义如下


export function watch<T = any, Immediate extends Readonly<boolean> = false>(  source: T | WatchSource<T>,  cb: any,  options?: WatchOptions<Immediate>): WatchStopHandle {  return doWatch(source as any, cb, options)}
复制代码


很明显watchEffect就是一种特殊的watch实现。

nextTick 使用场景和原理

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


相关代码如下


let callbacks = [];let pending = false;function flushCallbacks() {  pending = false; //把标志还原为false  // 依次执行回调  for (let i = 0; i < callbacks.length; i++) {    callbacks[i]();  }}let timerFunc; //定义异步方法  采用优雅降级if (typeof Promise !== "undefined") {  // 如果支持promise  const p = Promise.resolve();  timerFunc = () => {    p.then(flushCallbacks);  };} else if (typeof MutationObserver !== "undefined") {  // MutationObserver 主要是监听dom变化 也是一个异步方法  let counter = 1;  const observer = new MutationObserver(flushCallbacks);  const textNode = document.createTextNode(String(counter));  observer.observe(textNode, {    characterData: true,  });  timerFunc = () => {    counter = (counter + 1) % 2;    textNode.data = String(counter);  };} else if (typeof setImmediate !== "undefined") {  // 如果前面都不支持 判断setImmediate  timerFunc = () => {    setImmediate(flushCallbacks);  };} else {  // 最后降级采用setTimeout  timerFunc = () => {    setTimeout(flushCallbacks, 0);  };}
export function nextTick(cb) { // 除了渲染watcher 还有用户自己手动调用的nextTick 一起被收集到数组 callbacks.push(cb); if (!pending) { // 如果多次调用nextTick 只会执行一次异步 等异步队列清空之后再把标志变为false pending = true; timerFunc(); }}
复制代码

vue2.x 详细

1. 分析


首先找到vue的构造函数


源码位置:src\core\instance\index.js


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)}
复制代码


options是用户传递过来的配置项,如data、methods等常用的方法


vue构建函数调用_init方法,但我们发现本文件中并没有此方法,但仔细可以看到文件下方定定义了很多初始化方法


initMixin(Vue);     // 定义 _initstateMixin(Vue);    // 定义 $set $get $delete $watch 等eventsMixin(Vue);   // 定义事件  $on  $once $off $emitlifecycleMixin(Vue);// 定义 _update  $forceUpdate  $destroyrenderMixin(Vue);   // 定义 _render 返回虚拟dom
复制代码


首先可以看initMixin方法,发现该方法在Vue原型上定义了_init方法


源码位置:src\core\instance\init.js


Vue.prototype._init = function (options?: Object) {    const vm: Component = this    // a uid    vm._uid = uid++    let 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 // 合并属性,判断初始化的是否是组件,这里合并主要是 mixins 或 extends 的方法 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 { // 合并vue属性 vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ) } /* istanbul ignore else */ if (process.env.NODE_ENV !== 'production') { // 初始化proxy拦截器 initProxy(vm) } else { vm._renderProxy = vm } // expose real self vm._self = vm // 初始化组件生命周期标志位 initLifecycle(vm) // 初始化组件事件侦听 initEvents(vm) // 初始化渲染方法 initRender(vm) callHook(vm, 'beforeCreate') // 初始化依赖注入内容,在初始化data、props之前 initInjections(vm) // resolve injections before data/props // 初始化props/data/method/watch/methods 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) } }
复制代码


仔细阅读上面的代码,我们得到以下结论:


  • 在调用beforeCreate之前,数据初始化并未完成,像dataprops这些属性无法访问到

  • 到了created的时候,数据已经初始化完成,能够访问dataprops这些属性,但这时候并未完成dom的挂载,因此无法访问到dom元素

  • 挂载方法是调用vm.$mount方法


initState方法是完成props/data/method/watch/methods的初始化


源码位置:src\core\instance\state.js


export function initState (vm: Component) {  // 初始化组件的watcher列表  vm._watchers = []  const opts = vm.$options  // 初始化props  if (opts.props) initProps(vm, opts.props)  // 初始化methods方法  if (opts.methods) initMethods(vm, opts.methods)  if (opts.data) {    // 初始化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)  }}
复制代码


我们和这里主要看初始化data的方法为initData,它与initState在同一文件上


function initData (vm: Component) {  let data = vm.$options.data  // 获取到组件上的data  data = vm._data = typeof data === 'function'    ? getData(data, vm)    : data || {}  if (!isPlainObject(data)) {    data = {}    process.env.NODE_ENV !== 'production' && warn(      'data functions should return an object:\n' +      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',      vm    )  }  // proxy data on instance  const keys = Object.keys(data)  const props = vm.$options.props  const methods = vm.$options.methods  let i = keys.length  while (i--) {    const key = keys[i]    if (process.env.NODE_ENV !== 'production') {      // 属性名不能与方法名重复      if (methods && hasOwn(methods, key)) {        warn(          `Method "${key}" has already been defined as a data property.`,          vm        )      }    }    // 属性名不能与state名称重复    if (props && hasOwn(props, key)) {      process.env.NODE_ENV !== 'production' && warn(        `The data property "${key}" is already declared as a prop. ` +        `Use prop default value instead.`,        vm      )    } else if (!isReserved(key)) { // 验证key值的合法性      // 将_data中的数据挂载到组件vm上,这样就可以通过this.xxx访问到组件上的数据      proxy(vm, `_data`, key)    }  }  // observe data  // 响应式监听data是数据的变化  observe(data, true /* asRootData */)}
复制代码


仔细阅读上面的代码,我们可以得到以下结论:


  • 初始化顺序:propsmethodsdata

  • data定义的时候可选择函数形式或者对象形式(组件只能为函数形式)


关于数据响应式在这就不展开详细说明


上文提到挂载方法是调用vm.$mount方法


源码位置:


Vue.prototype.$mount = function (  el?: string | Element,  hydrating?: boolean): Component {  // 获取或查询元素  el = el && query(el)
/* istanbul ignore if */ // vue 不允许直接挂载到body或页面文档上 if (el === document.body || el === document.documentElement) { process.env.NODE_ENV !== 'production' && warn( `Do not mount Vue to <html> or <body> - mount to normal elements instead.` ) return this }
const options = this.$options // resolve template/el and convert to render function if (!options.render) { let template = options.template // 存在template模板,解析vue模板文件 if (template) { if (typeof template === 'string') { if (template.charAt(0) === '#') { template = idToTemplate(template) /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && !template) { warn( `Template element not found or is empty: ${options.template}`, this ) } } } else if (template.nodeType) { template = template.innerHTML } else { if (process.env.NODE_ENV !== 'production') { warn('invalid template option:' + template, this) } return this } } else if (el) { // 通过选择器获取元素内容 template = getOuterHTML(el) } if (template) { /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { mark('compile') } /** * 1.将temmplate解析ast tree * 2.将ast tree转换成render语法字符串 * 3.生成render方法 */ const { render, staticRenderFns } = compileToFunctions(template, { outputSourceRange: process.env.NODE_ENV !== 'production', shouldDecodeNewlines, shouldDecodeNewlinesForHref, delimiters: options.delimiters, comments: options.comments }, this) options.render = render options.staticRenderFns = staticRenderFns
/* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { mark('compile end') measure(`vue ${this._name} compile`, 'compile', 'compile end') } } } return mount.call(this, el, hydrating)}
复制代码


阅读上面代码,我们能得到以下结论:


  • 不要将根元素放到body或者html

  • 可以在对象中定义template/render或者直接使用templateel表示元素选择器

  • 最终都会解析成render函数,调用compileToFunctions,会将template解析成render函数


template的解析步骤大致分为以下几步:


  • html文档片段解析成ast描述符

  • ast描述符解析成字符串

  • 生成render函数


生成render函数,挂载到vm上后,会再次调用mount方法


源码位置:src\platforms\web\runtime\index.js


// public mount methodVue.prototype.$mount = function (  el?: string | Element,  hydrating?: boolean): Component {  el = el && inBrowser ? query(el) : undefined  // 渲染组件  return mountComponent(this, el, hydrating)}
复制代码


调用mountComponent渲染组件


export function mountComponent (  vm: Component,  el: ?Element,  hydrating?: boolean): Component {  vm.$el = el  // 如果没有获取解析的render函数,则会抛出警告  // render是解析模板文件生成的  if (!vm.$options.render) {    vm.$options.render = createEmptyVNode    if (process.env.NODE_ENV !== 'production') {      /* istanbul ignore if */      if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||        vm.$options.el || el) {        warn(          'You are using the runtime-only build of Vue where the template ' +          'compiler is not available. Either pre-compile the templates into ' +          'render functions, or use the compiler-included build.',          vm        )      } else {        // 没有获取到vue的模板文件        warn(          'Failed to mount component: template or render function not defined.',          vm        )      }    }  }  // 执行beforeMount钩子  callHook(vm, 'beforeMount')
let updateComponent /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { updateComponent = () => { const name = vm._name const id = vm._uid const startTag = `vue-perf-start:${id}` const endTag = `vue-perf-end:${id}`
mark(startTag) const vnode = vm._render() mark(endTag) measure(`vue ${name} render`, startTag, endTag)
mark(startTag) vm._update(vnode, hydrating) mark(endTag) measure(`vue ${name} patch`, startTag, endTag) } } else { // 定义更新函数 updateComponent = () => { // 实际调⽤是在lifeCycleMixin中定义的_update和renderMixin中定义的_render vm._update(vm._render(), hydrating) } } // we set this to vm._watcher inside the watcher's constructor // since the watcher's initial patch may call $forceUpdate (e.g. inside child // component's mounted hook), which relies on vm._watcher being already defined // 监听当前组件状态,当有数据变化时,更新组件 new Watcher(vm, updateComponent, noop, { before () { if (vm._isMounted && !vm._isDestroyed) { // 数据更新引发的组件更新 callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */) hydrating = false
// manually mounted instance, call mounted on self // mounted is called for render-created child components in its inserted hook if (vm.$vnode == null) { vm._isMounted = true callHook(vm, 'mounted') } return vm}
复制代码


阅读上面代码,我们得到以下结论:


  • 会触发boforeCreate钩子

  • 定义updateComponent渲染页面视图的方法

  • 监听组件数据,一旦发生变化,触发beforeUpdate生命钩子


updateComponent方法主要执行在vue初始化时声明的renderupdate方法


render的作用主要是生成vnode


源码位置:src\core\instance\render.js


// 定义vue 原型上的render方法Vue.prototype._render = function (): VNode {    const vm: Component = this    // render函数来自于组件的option    const { render, _parentVnode } = vm.$options
if (_parentVnode) { vm.$scopedSlots = normalizeScopedSlots( _parentVnode.data.scopedSlots, vm.$slots, vm.$scopedSlots ) }
// set parent vnode. this allows render functions to have access // to the data on the placeholder node. vm.$vnode = _parentVnode // render self let vnode try { // There's no need to maintain a stack because all render fns are called // separately from one another. Nested component's render fns are called // when parent component is patched. currentRenderingInstance = vm // 调用render方法,自己的独特的render方法, 传入createElement参数,生成vNode vnode = render.call(vm._renderProxy, vm.$createElement) } catch (e) { handleError(e, vm, `render`) // return error render result, // or previous vnode to prevent render error causing blank component /* istanbul ignore else */ if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) { try { vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e) } catch (e) { handleError(e, vm, `renderError`) vnode = vm._vnode } } else { vnode = vm._vnode } } finally { currentRenderingInstance = null } // if the returned array contains only a single node, allow it if (Array.isArray(vnode) && vnode.length === 1) { vnode = vnode[0] } // return empty vnode in case the render function errored out if (!(vnode instanceof VNode)) { if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) { warn( 'Multiple root nodes returned from render function. Render function ' + 'should return a single root node.', vm ) } vnode = createEmptyVNode() } // set parent vnode.parent = _parentVnode return vnode}
复制代码


_update主要功能是调用patch,将vnode转换为真实DOM,并且更新到页面中


源码位置:src\core\instance\lifecycle.js


Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {    const vm: Component = this    const prevEl = vm.$el    const prevVnode = vm._vnode    // 设置当前激活的作用域    const restoreActiveInstance = setActiveInstance(vm)    vm._vnode = vnode    // Vue.prototype.__patch__ is injected in entry points    // based on the rendering backend used.    if (!prevVnode) {      // initial render      // 执行具体的挂载逻辑      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)    } else {      // updates      vm.$el = vm.__patch__(prevVnode, vnode)    }    restoreActiveInstance()    // update __vue__ reference    if (prevEl) {      prevEl.__vue__ = null    }    if (vm.$el) {      vm.$el.__vue__ = vm    }    // if parent is an HOC, update its $el as well    if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {      vm.$parent.$el = vm.$el    }    // updated hook is called by the scheduler to ensure that children are    // updated in a parent's updated hook.  }
复制代码


2. 结论


  • new Vue的时候调用会调用_init方法

  • 定义 $set$get$delete$watch 等方法

  • 定义 $on$off$emit$off等事件

  • 定义 _update$forceUpdate$destroy生命周期

  • 调用$mount进行页面的挂载

  • 挂载的时候主要是通过mountComponent方法

  • 定义updateComponent更新函数

  • 执行render生成虚拟DOM

  • _update将虚拟DOM生成真实DOM结构,并且渲染到页面中

created 和 mounted 的区别

  • created:在模板渲染成 html 前调用,即通常初始化某些属性值,然后再渲染成视图。

  • mounted:在模板渲染成 html 后调用,通常是初始化页面完成后,再对 html 的 dom 节点进行一些需要的操作。

Vue 中封装的数组方法有哪些,其如何实现页面更新

在 Vue 中,对响应式处理利用的是 Object.defineProperty 对数据进行拦截,而这个方法并不能监听到数组内部变化,数组长度变化,数组的截取变化等,所以需要对这些操作进行 hack,让 Vue 能监听到其中的变化。 那 Vue 是如何实现让这些数组方法实现元素的实时更新的呢,下面是 Vue 中对这些方法的封装:


// 缓存数组原型const arrayProto = Array.prototype;// 实现 arrayMethods.__proto__ === Array.prototypeexport const arrayMethods = Object.create(arrayProto);// 需要进行功能拓展的方法const methodsToPatch = [  "push",  "pop",  "shift",  "unshift",  "splice",  "sort",  "reverse"];
/** * Intercept mutating methods and emit events */methodsToPatch.forEach(function(method) { // 缓存原生数组方法 const original = arrayProto[method]; def(arrayMethods, method, function mutator(...args) { // 执行并缓存原生数组功能 const result = original.apply(this, args); // 响应式处理 const ob = this.__ob__; let inserted; switch (method) { // push、unshift会新增索引,所以要手动observer case "push": case "unshift": inserted = args; break; // splice方法,如果传入了第三个参数,也会有索引加入,也要手动observer。 case "splice": inserted = args.slice(2); break; } // if (inserted) ob.observeArray(inserted);// 获取插入的值,并设置响应式监听 // notify change ob.dep.notify();// 通知依赖更新 // 返回原生数组方法的执行结果 return result; });});
复制代码


简单来说就是,重写了数组中的那些原生方法,首先获取到这个数组的__ob__,也就是它的 Observer 对象,如果有新的值,就调用 observeArray 继续对新的值观察变化(也就是通过target__proto__ == arrayMethods来改变了数组实例的型),然后手动调用 notify,通知渲染 watcher,执行 update。


参考:前端vue面试题详细解答

vue 如何监听对象或者数组某个属性的变化

当在项目中直接设置数组的某一项的值,或者直接设置对象的某个属性值,这个时候,你会发现页面并没有更新。这是因为 Object.defineProperty()限制,监听不到变化。


解决方式:


  • this.$set(你要改变的数组/对象,你要改变的位置/key,你要改成什么 value)


this.$set(this.arr, 0, "OBKoro1"); // 改变数组this.$set(this.obj, "c", "OBKoro1"); // 改变对象
复制代码


  • 调用以下几个数组的方法


splice()、 push()、pop()、shift()、unshift()、sort()、reverse()
复制代码


vue 源码里缓存了 array 的原型链,然后重写了这几个方法,触发这几个方法的时候会 observer 数据,意思是使用这些方法不用再进行额外的操作,视图自动进行更新。 推荐使用 splice 方法会比较好自定义,因为 splice 可以在数组的任何位置进行删除/添加操作


vm.$set 的实现原理是:


  • 如果目标是数组,直接使用数组的 splice 方法触发相应式;

  • 如果目标是对象,会先判读属性是否存在、对象是否是响应式,最终如果要对属性进行响应式处理,则是通过调用 defineReactive 方法进行响应式处理( defineReactive 方法就是 Vue 在初始化对象时,给对象属性采用 Object.defineProperty 动态添加 getter 和 setter 的功能所调用的方法)

Vue template 到 render 的过程

vue 的模版编译过程主要如下:template -> ast -> render 函数


vue 在模版编译版本的码中会执行 compileToFunctions 将 template 转化为 render 函数:


// 将模板编译为render函数const { render, staticRenderFns } = compileToFunctions(template,options//省略}, this)
复制代码


CompileToFunctions 中的主要逻辑如下∶ (1)调用 parse 方法将 template 转化为 ast(抽象语法树)


constast = parse(template.trim(), options)
复制代码


  • parse 的目标:把 tamplate 转换为 AST 树,它是一种用 JavaScript 对象的形式来描述整个模板。

  • 解析过程:利用正则表达式顺序解析模板,当解析到开始标签、闭合标签、文本的时候都会分别执行对应的 回调函数,来达到构造 AST 树的目的。


AST 元素节点总共三种类型:type 为 1 表示普通元素、2 为表达式、3 为纯文本


(2)对静态节点做优化


optimize(ast,options)
复制代码


这个过程主要分析出哪些是静态节点,给其打一个标记,为后续更新渲染可以直接跳过静态节点做优化


深度遍历 AST,查看每个子树的节点元素是否为静态节点或者静态节点根。如果为静态节点,他们生成的 DOM 永远不会改变,这对运行时模板更新起到了极大的优化作用。


(3)生成代码


const code = generate(ast, options)
复制代码


generate 将 ast 抽象语法树编译成 render 字符串并将静态部分放到 staticRenderFns 中,最后通过 new Function(`` render``) 生成 render 函数。

Proxy 与 Object.defineProperty 优劣对比

Proxy 的优势如下:


  • Proxy 可以直接监听对象而非属性;

  • Proxy 可以直接监听数组的变化;

  • Proxy 有多达 13 种拦截方法,不限于 apply、ownKeys、deleteProperty、has 等等是 Object.defineProperty 不具备的;

  • Proxy 返回的是一个新对象,我们可以只操作新的对象达到目的,而 Object.defineProperty 只能遍历对象属性直接修改;


Proxy 作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利;


Object.defineProperty 的优势如下:


  • 兼容性好,支持 IE9,而 Proxy 的存在浏览器兼容性问题,而且无法用 polyfill 磨平,因此 Vue 的作者才声明需要等到下个大版本( 3.0 )才能用 Proxy 重写。

v-show 与 v-if 有什么区别?

v-if真正的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建;也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。


v-show 就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 的 “display” 属性进行切换。


所以,v-if 适用于在运行时很少改变条件,不需要频繁切换条件的场景;v-show 则适用于需要非常频繁切换条件的场景。

谈谈 Vue 和 React 组件化的思想

  • 1.我们在各个页面开发的时候,会产生很多重复的功能,比如 element 中的 xxxx。像这种纯粹非页面的 UI,便成为我们常用的 UI 组件,最初的前端组件也就仅仅指的是 UI 组件

  • 2.随着业务逻辑变得越来多是,我们就想要我们的组件可以处理很多事,这就是我们常说的组件化,这个组件就不是 UI 组件了,而是包具体业务的业务组件

  • 3.这种开发思想就是分而治之。最大程度的降低开发难度和维护成本的效果。并且可以多人协作,每个人写不同的组件,最后像撘积木一样的把它构成一个页面

Vue 中 computed 和 watch 有什么区别?

计算属性 computed: (1)支持缓存,只有依赖数据发生变化时,才会重新进行计算函数; (2)计算属性内不支持异步操作; (3)计算属性的函数中都有一个 get(默认具有,获取计算属性)和 set(手动添加,设置计算属性)方法; (4)计算属性是自动监听依赖值的变化,从而动态返回内容。


侦听属性 watch: (1)不支持缓存,只要数据发生变化,就会执行侦听函数; (2)侦听属性内支持异步操作; (3)侦听属性的值可以是一个对象,接收 handler 回调,deep,immediate 三个属性; (3)监听是一个过程,在监听的值变化时,可以触发一个回调,并做一些其他事情

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() }
复制代码

vue-router 中如何保护路由

分析


路由保护在应用开发过程中非常重要,几乎每个应用都要做各种路由权限管理,因此相当考察使用者基本功。


体验


全局守卫:


const router = createRouter({ ... })router.beforeEach((to, from) => {  // ...  // 返回 false 以取消导航  return false})
复制代码


路由独享守卫:


const routes = [  {    path: '/users/:id',    component: UserDetails,    beforeEnter: (to, from) => {      // reject the navigation      return false    },  },]
复制代码


组件内的守卫:


const UserDetails = {  template: `...`,  beforeRouteEnter(to, from) {    // 在渲染该组件的对应路由被验证前调用  },  beforeRouteUpdate(to, from) {    // 在当前路由改变,但是该组件被复用时调用  },  beforeRouteLeave(to, from) {    // 在导航离开渲染该组件的对应路由时调用  },}
复制代码


回答


  • vue-router中保护路由的方法叫做路由守卫,主要用来通过跳转或取消的方式守卫导航。

  • 路由守卫有三个级别:全局路由独享组件级。影响范围由大到小,例如全局的router.beforeEach(),可以注册一个全局前置守卫,每次路由导航都会经过这个守卫,因此在其内部可以加入控制逻辑决定用户是否可以导航到目标路由;在路由注册的时候可以加入单路由独享的守卫,例如beforeEnter,守卫只在进入路由时触发,因此只会影响这个路由,控制更精确;我们还可以为路由组件添加守卫配置,例如beforeRouteEnter,会在渲染该组件的对应路由被验证前调用,控制的范围更精确了。

  • 用户的任何导航行为都会走navigate方法,内部有个guards队列按顺序执行用户注册的守卫钩子函数,如果没有通过验证逻辑则会取消原有的导航。


原理


runGuardQueue(guards)链式的执行用户在各级别注册的守卫钩子函数,通过则继续下一个级别的守卫,不通过进入catch流程取消原本导航


// 源码runGuardQueue(guards)  .then(() => {    // check global guards beforeEach    guards = []    for (const guard of beforeGuards.list()) {      guards.push(guardToPromiseFn(guard, to, from))    }    guards.push(canceledNavigationCheck)
return runGuardQueue(guards) }) .then(() => { // check in components beforeRouteUpdate guards = extractComponentsGuards( updatingRecords, 'beforeRouteUpdate', to, from )
for (const record of updatingRecords) { record.updateGuards.forEach(guard => { guards.push(guardToPromiseFn(guard, to, from)) }) } guards.push(canceledNavigationCheck)
// run the queue of per route beforeEnter guards return runGuardQueue(guards) }) .then(() => { // check the route beforeEnter guards = [] for (const record of to.matched) { // do not trigger beforeEnter on reused views if (record.beforeEnter && !from.matched.includes(record)) { if (isArray(record.beforeEnter)) { for (const beforeEnter of record.beforeEnter) guards.push(guardToPromiseFn(beforeEnter, to, from)) } else { guards.push(guardToPromiseFn(record.beforeEnter, to, from)) } } } guards.push(canceledNavigationCheck)
// run the queue of per route beforeEnter guards return runGuardQueue(guards) }) .then(() => { // NOTE: at this point to.matched is normalized and does not contain any () => Promise<Component>
// clear existing enterCallbacks, these are added by extractComponentsGuards to.matched.forEach(record => (record.enterCallbacks = {}))
// check in-component beforeRouteEnter guards = extractComponentsGuards( enteringRecords, 'beforeRouteEnter', to, from ) guards.push(canceledNavigationCheck)
// run the queue of per route beforeEnter guards return runGuardQueue(guards) }) .then(() => { // check global guards beforeResolve guards = [] for (const guard of beforeResolveGuards.list()) { guards.push(guardToPromiseFn(guard, to, from)) } guards.push(canceledNavigationCheck)
return runGuardQueue(guards) }) // catch any navigation canceled .catch(err => isNavigationFailure(err, ErrorTypes.NAVIGATION_CANCELLED) ? err : Promise.reject(err) )
复制代码


源码位置(opens new window)

Vue data 中某一个属性的值发生改变后,视图会立即同步执行重新渲染吗?

不会立即同步执行重新渲染。Vue 实现响应式并不是数据发生变化之后 DOM 立即变化,而是按一定的策略进行 DOM 的更新。Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化, Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。


如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环 tick 中,Vue 刷新队列并执行实际(已去重的)工作。

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

为什么要使用异步组件

  1. 节省打包出的结果,异步组件分开打包,采用jsonp的方式进行加载,有效解决文件过大的问题。

  2. 核心就是包组件定义变成一个函数,依赖import() 语法,可以实现文件的分割加载。


components:{   AddCustomerSchedule:(resolve)=>import("../components/AddCustomer") // require([]) }
复制代码


原理


export function ( Ctor: Class<Component> | Function | Object | void, data: ?VNodeData, context: Component, children: ?Array<VNode>, tag?: string ): VNode | Array<VNode> | void {     // async component     let asyncFactory     if (isUndef(Ctor.cid)) {         asyncFactory = Ctor         Ctor = resolveAsyncComponent(asyncFactory, baseCtor) // 默认调用此函数时返回 undefiend         // 第二次渲染时Ctor不为undefined         if (Ctor === undefined) {             return createAsyncPlaceholder( // 渲染占位符 空虚拟节点                 asyncFactory,                 data,                 context,                 children,                 tag             )         }     } }function resolveAsyncComponent ( factory: Function, baseCtor: Class<Component> ): Class<Component> | void {     if (isDef(factory.resolved)) {         // 3.在次渲染时可以拿到获取的最新组件         return factory.resolved     }    const resolve = once((res: Object | Class<Component>) => {         factory.resolved = ensureCtor(res, baseCtor)         if (!sync) {             forceRender(true) //2. 强制更新视图重新渲染         } else {             owners.length = 0         }     })    const reject = once(reason => {         if (isDef(factory.errorComp)) {             factory.error = true forceRender(true)         }     })    const res = factory(resolve, reject)// 1.将resolve方法和reject方法传入,用户调用 resolve方法后     sync = false     return factory.resolved }
复制代码


用户头像

bb_xiaxia1998

关注

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

还未添加个人简介

评论

发布
暂无评论
滴滴前端二面vue相关面试题_Vue_bb_xiaxia1998_InfoQ写作社区