Vue2之面试题

Vue2最后一个正式版本2.7.16

https://github.com/vuejs/vue/tags

v-show和v-if的异同

共同点

  • 能控制元素在页面是否显示

区别

控制手段

  • v-show,当表达式都为false时,隐藏则是为该元素添加css–display:none,dom元素依旧还在
  • v-if显示隐藏是将dom元素整个添加或删除

编译层面

  • v-if在编译的时候会被转化为三元表达式,条件不满足则不渲染此节点

    • 源码位置:src\compiler\codegen\index.ts

    • function genIfConditions(
        conditions: ASTIfConditions,
        state: CodegenState,
        altGen?: Function,
        altEmpty?: string
      ): string {
        if (!conditions.length) {
          // 没条件返回空元素
          return altEmpty || '_e()'
        }
      
        const condition = conditions.shift()!
        // 如果有表达式
        if (condition.exp) {
          //  三元表达式
          return `(${condition.exp})?${genTernaryExp(
            condition.block
          )}:${genIfConditions(conditions, state, altGen, altEmpty)}`
        } else {
          return `${genTernaryExp(condition.block)}`
        }
      
        // v-if with v-once should generate code like (a)?_m(0):_m(1)
        function genTernaryExp(el) {
          return altGen
            ? altGen(el, state)
            : el.once
            ? genOnce(el, state)
            : genElement(el, state)
        }
      }
      
      export function genFor(
        el: any,
        state: CodegenState,
        altGen?: Function,
        altHelper?: string
      ): string {
        const exp = el.for
        const alias = el.alias
        const iterator1 = el.iterator1 ? `,${el.iterator1}` : ''
        const iterator2 = el.iterator2 ? `,${el.iterator2}` : ''
      
        if (
          __DEV__ &&
          state.maybeComponent(el) &&
          el.tag !== 'slot' &&
          el.tag !== 'template' &&
          !el.key
        ) {
          state.warn(
            `<${el.tag} v-for="${alias} in ${exp}">: component lists rendered with ` +
              `v-for should have explicit keys. ` +
              `See https://v2.vuejs.org/v2/guide/list.html#key for more info.`,
            el.rawAttrsMap['v-for'],
            true /* tip */
          )
        }
      
        el.forProcessed = true // avoid recursion
        return (
          `${altHelper || '_l'}((${exp}),` +
          `function(${alias}${iterator1}${iterator2}){` +
          `return ${(altGen || genElement)(el, state)}` +
          '})'
        )
      }
      
  • v-show原理是会被编译成一个指令,有transition就执行transition,没有就直接设置display属性

    • 源码位置:src\platforms\web\runtime\directives\show.ts

    • import VNode from 'core/vdom/vnode'
      import type { VNodeDirective, VNodeWithData } from 'types/vnode'
      import { enter, leave } from 'web/runtime/modules/transition'
      
      // recursively search for possible transition defined inside the component root
      function locateNode(vnode: VNode | VNodeWithData): VNodeWithData {
        // @ts-expect-error
        return vnode.componentInstance && (!vnode.data || !vnode.data.transition)
          ? locateNode(vnode.componentInstance._vnode!)
          : vnode
      }
      
      export default {
        bind(el: any, { value }: VNodeDirective, vnode: VNodeWithData) {
          vnode = locateNode(vnode)
          const transition = vnode.data && vnode.data.transition
          // 取原来的display值,保存起来
          const originalDisplay = (el.__vOriginalDisplay =
            el.style.display === 'none' ? '' : el.style.display)
          if (value && transition) {
            vnode.data.show = true
            enter(vnode, () => {
              el.style.display = originalDisplay
            })
          } else {
            // 如果传来的值,则把原来的值放上去,否则就是none
            el.style.display = value ? originalDisplay : 'none'
          }
        },
      
        update(el: any, { value, oldValue }: VNodeDirective, vnode: VNodeWithData) {
          /* istanbul ignore if */
          if (!value === !oldValue) return
          vnode = locateNode(vnode)
          const transition = vnode.data && vnode.data.transition
          if (transition) {
            vnode.data.show = true
            if (value) {
              enter(vnode, () => {
                el.style.display = el.__vOriginalDisplay
              })
            } else {
              leave(vnode, () => {
                el.style.display = 'none'
              })
            }
          } else {
            el.style.display = value ? el.__vOriginalDisplay : 'none'
          }
        },
      
        unbind(
          el: any,
          binding: VNodeDirective,
          vnode: VNodeWithData,
          oldVnode: VNodeWithData,
          isDestroy: boolean
        ) {
          if (!isDestroy) {
            el.style.display = el.__vOriginalDisplay
          }
        }
      }
      
      

生命周期触发

  • v-show 由false变为true的时候不会触发组件的生命周期
  • v-if由false变为true的时候,触发组件的beforeCreate、create、beforeMount、mounted钩子
    • 由true变为false的时候触发组件的beforeDestory、destoryed方法

性能消耗

  • v-show不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 进行切换,v-show 有更高的初始渲染开销
  • v-if 有更高的切换开销

使用场景

  • 如果需要非常频繁地切换,则使用 v-show 较好
  • 如果在运行时条件很少改变,则使用 v-if 较好

Vue2响应式数据的理解

https://blog.csdn.net/qq_40588441/article/details/140337629?spm=1001.2014.3001.5501

  • 对象内部通过defineReactive方法,使用Object.defineProperty将属性进行劫持(只会劫持已经存在的属性)
    • 消耗大性能差,不存在的属性,如果新增的话不能重新渲染视图
  • 多层对象是通过递归来实现劫持。(所以会有性能问题,建议减少嵌套层级)
  • 数组则是通过重写数组方法来实现。
    • 数组通过索引进行修改 或者 修改数组的长度,响应式不生效(可以通过$set解决[本质是splice方法])

源码位置:src\core\observer\index.ts

// 定义响应式数据
export function defineReactive(
  obj: object,
  key: string,
  val?: any,
  customSetter?: Function | null,
  shallow?: boolean,
  mock?: boolean,
  observeEvenIfShallow = false
) {
  const dep = new Dep()
  // 如果不可以配置直接return
  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) &&
    (val === NO_INITIAL_VALUE || arguments.length === 2)
  ) {
    val = obj[key]
  }
   // 对数据进行观测
  let childOb = shallow ? val && val.__ob__ : observe(val, false, mock)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    // 取数据时进行依赖收集
    get: function reactiveGetter() {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        if (__DEV__) {
          dep.depend({
            target: obj,
            type: TrackOpTypes.GET,
            key
          })
        } else {
          dep.depend()
        }
        // 让对象本身进行依赖收集
        if (childOb) {
          // {a:1}  => {} 外层对象
          childOb.dep.depend()
          // 如果是数组  {arr:[[],[]]} vm.arr取值只会让arr属性和外层数组进行收集
          if (isArray(value)) {
            dependArray(value)
          }
        }
      }
      return isRef(value) && !shallow ? value.value : value
    },
    set: function reactiveSetter(newVal) {
      const value = getter ? getter.call(obj) : val
      if (!hasChanged(value, newVal)) {
        return
      }
      if (__DEV__ && customSetter) {
        customSetter()
      }
      if (setter) {
        setter.call(obj, newVal)
      } else if (getter) {
        // #7981: for accessor properties without setter
        return
      } else if (!shallow && isRef(value) && !isRef(newVal)) {
        value.value = newVal
        return
      } else {
        val = newVal
      }
      childOb = shallow ? newVal && newVal.__ob__ : observe(newVal, false, mock)
      if (__DEV__) {
        dep.notify({
          type: TriggerOpTypes.SET,
          target: obj,
          key,
          newValue: newVal,
          oldValue: value
        })
      } else {
        dep.notify()
      }
    }
  })

  return dep
}

Vue2中如何检测数组变化?

  • 数组考虑性能原因没有用defineProperty对数组的每一项进行拦截,而是选择重写数组(push,shift,pop,splice,unshift,sort,reverse)方法。
  • 数组中如果是对象数据类型也会进行递归劫持
  • 数组的索引和长度变化是无法监控到的

源码位置:src\core\observer\array.ts

/*
 * not type checking this file because flow doesn't play well with
 * dynamically accessing methods on Array prototype
 */

import { TriggerOpTypes } from '../../v3'
import { def } from '../util/index'

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

/**
 * Intercept mutating methods and emit events
 */
methodsToPatch.forEach(function (method) {
  // cache original 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) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    // 新增的数据需要进行观测
    if (inserted) ob.observeArray(inserted)
    // notify change
    if (__DEV__) {
      ob.dep.notify({
        type: TriggerOpTypes.ARRAY_MUTATION,
        target: this,
        key: method
      })
    } else {
      ob.dep.notify()
    }
    return result
  })
})

Vue2中如何进行依赖收集?

  • 每个属性都拥有自己的dep属性,存放他所依赖的watcher,当属性变化后会通知自己对应的watcher去更新
  • 默认在初始化时会调用render函数,此时会触发属性依赖收集 dep.depend
  • 当属性发生修改时会触发watcher更新 dep.notify()

image-20240719155810384

如何理解Vue2中模板编译原理

问题核心:如何将template转换成render函数 ?

  • 1.将template模板转换成ast语法树 - parserHTML

  • 2.对静态语法做静态标记 - markUp diff来做优化的 静态节点跳过diff操作

  • 3.重新生成代码 - `codeGen` 
    
src/compiler/index.js:11
export const createCompiler = createCompilerCreator(function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  const ast = parse(template.trim(), options) // 1.解析ast语法树
  if (options.optimize !== false) {          
    optimize(ast, options)                    // 2.对ast树进行标记,标记静态节点
  }
  const code = generate(ast, options)         // 3.生成代码
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})

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

  • Vue的生命周期钩子就是回调函数而已,当创建组件实例的过程中会调用对应的钩子方法。
  • 内部会对钩子函数进行处理,将钩子函数维护成数组的形式

src/core/instance/init.js:38 初始化合并

src/core/util/options.js:388 合并选项

function mergeHook (
  parentVal: ?Array<Function>,
  childVal: ?Function | ?Array<Function>
): ?Array<Function> {
  const res = childVal // 儿子有
    ? parentVal 
      ? parentVal.concat(childVal) // 父亲也有,那就是合并
      : Array.isArray(childVal) // 儿子是数组
        ? childVal
        : [childVal] // 不是数组包装成数组
    : parentVal
  return res
    ? dedupeHooks(res)
    : res
}

Vue2的生命周期方法有哪些?一般在哪一步发送请求及原因

  • beforeCreate 在实例初始化之后,数据观测(data observer) 和 event/watcher 事件配置之前被调用。
  • created 实例已经创建完成之后被调用。在这一步,实例已完成以下的配置:数据观测(data observer),属性和方法的运算, watch/event 事件回调。这里没有$el
  • beforeMount 在挂载开始之前被调用:相关的 render 函数首次被调用。
  • mounted el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子。
  • beforeUpdate 数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。
  • updated 由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。
  • beforeDestroy 实例销毁之前调用。在这一步,实例仍然完全可用。
  • destroyed Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。 该钩子在服务器端渲染期间不被调用。
  • keep-alive (activated 和 deactivated)

在哪发送请求都可以,主要看具体你要做什么事

Vue.mixin的使用场景和原理

  • Vue.mixin的作用就是抽离公共的业务逻辑,原理类似“对象的继承”,当组件初始化时会调用mergeOptions方法进行合并,采用策略模式针对不同的属性进行合并。如果混入的数据和本身组件中的数据冲突,会采用“就近原则”以组件的数据为准。

  • mixin中有很多缺陷 “命名冲突问题”、“依赖问题”、“数据来源问题”

  • 源码位置:src\core\global-api\mixin.ts

    import type { GlobalAPI } from 'types/global-api'
    import { mergeOptions } from '../util/index'
    
    export function initMixin(Vue: GlobalAPI) {
      Vue.mixin = function (mixin: Object) {
        this.options = mergeOptions(this.options, mixin)
        return this
      }
    }
    
    

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

  • 每次使用组件时都会对组件进行实例化操作,并且调用data函数返回一个对象作为组件的数据源。这样可以保证多个组件间数据互不影响
function Vue() {}
function Sub() { // 会将data存起来
    this.data = this.constructor.options.data;
}
Vue.extend = function(options) {
    Sub.options = options;
    return Sub;
}
let Child = Vue.extend({
    data: { name: 'hs' }
});
// 两个组件就是两个实例, 希望数据互不干扰
let child1 = new Child();
let child2 = new Child();

console.log(child1.data.name);
child1.data.name = 'jw';
console.log(child2.data.name);

data的合并策略,源码位置:src\core\util\options.ts

export function mergeDataOrFn(
  parentVal: any,
  childVal: any,
  vm?: Component
): Function | null {
  if (!vm) {
    // in a Vue.extend merge, both should be functions
    if (!childVal) {
      return parentVal
    }
    if (!parentVal) {
      return childVal
    }
    // when parentVal & childVal are both present,
    // we need to return a function that returns the
    // merged result of both functions... no need to
    // check if parentVal is a function here because
    // it has to be a function to pass previous merges.
    return function mergedDataFn() {
      return mergeData(
        isFunction(childVal) ? childVal.call(this, this) : childVal,
        isFunction(parentVal) ? parentVal.call(this, this) : parentVal
      )
    }
  } else {
    return function mergedInstanceDataFn() {
      // instance merge
      const instanceData = isFunction(childVal)
        ? childVal.call(vm, vm)
        : childVal
      const defaultData = isFunction(parentVal)
        ? parentVal.call(vm, vm)
        : parentVal
      if (instanceData) {
        // 合并2个对象
        return mergeData(instanceData, defaultData)
      } else {
        return defaultData
      }
    }
  }
}

strats.data = function (
  parentVal: any,
  childVal: any,
  vm?: Component
): Function | null {
  // 组件在合并时并没有产生实例,所以会校验类型
  if (!vm) {
    if (childVal && typeof childVal !== 'function') {
      __DEV__ &&
        warn(
          'The "data" option should be a function ' +
            'that returns a per-instance value in component ' +
            'definitions.',
          vm
        )

      return parentVal
    }
    return mergeDataOrFn(parentVal, childVal)
  }

  return mergeDataOrFn(parentVal, childVal, vm)
}

nextTick在哪里使用?原理是?

  • nextTick中的回调是在下次 DOM 更新循环结束之后执行的延迟回调。
  • 可用于获取更新后的 DOM。
  • Vue中数据更新是异步的,使用nextTick方法可以保证用户定义的逻辑在更新之后执行。

源码位置:src\core\util\next-tick.ts

computed和watch区别

  • computed和watch都是基于Watcher来实现的
  • computed属性是具备缓存的,依赖的值不发生变化(dirty标识是否已经读取过改值),对其取值时计算属性方法不会重新执行
  • watch则是监控值的变化,当值发生变化时调用对应的回调函数

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

function createComputedGetter(key) {
  return function computedGetter() {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      /// 如果值是脏的 进行求值操作
      if (watcher.dirty) {
        watcher.evaluate()
      }
      if (Dep.target) {
        if (__DEV__ && Dep.target.onTrack) {
          Dep.target.onTrack({
            effect: Dep.target,
            target: this,
            type: TrackOpTypes.GET,
            key
          })
        }
        // 让计算属性所依赖的属性 收集渲染watcher
        watcher.depend()
      }
      return watcher.value
    }
  }
}

  Vue.prototype.$watch = function (
    expOrFn: string | (() => any),
    cb: any,
    options?: Record<string, any>
  ): Function {
    const vm: Component = this
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
    // 标记为用户watcher
    options.user = true
    const watcher = new Watcher(vm, expOrFn, cb, options)
    if (options.immediate) {
      const info = `callback for immediate watcher "${watcher.expression}"`
      pushTarget()
      invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)
      popTarget()
    }
    return function unwatchFn() {
      watcher.teardown()
    }
  }

Vue.set方法是如何实现的

  • 我们给对象和数组本身都增加了dep属性
  • 当给对象新增不存在的属性则触发对象依赖的watcher去更新
  • 当修改数组索引时我们调用数组本身的splice方法去更新数组

源码位置:src\core\observer\index.ts

export function set(
  target: any[] | Record<string, any>,
  key: any,
  val: any
): any {
  // 1.是开发环境 target 没定义或者是基础类型则报错
  if (__DEV__ && (isUndef(target) || isPrimitive(target))) {
    warn(
      `Cannot set reactive property on undefined, null, or primitive value: ${target}`
    )
  }
  if (isReadonly(target)) {
    __DEV__ && warn(`Set operation on key "${key}" failed: target is readonly.`)
    return
  }
  const ob = (target as any).__ob__
  // 2.如果是数组 Vue.set(array,1,100); 调用我们重写的splice方法 (这样可以更新视图)
  if (isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val)
    // when mocking for SSR, array methods are not hijacked
    if (ob && !ob.shallow && ob.mock) {
      observe(val, false, true)
    }
    return val
  }
   // 3.如果是对象本身的属性,则直接添加即可
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  // 4.如果是Vue实例 或 根数据data时 报错,(更新_data 无意义)
  if ((target as any)._isVue || (ob && ob.vmCount)) {
    __DEV__ &&
      warn(
        'Avoid adding reactive properties to a Vue instance or its root $data ' +
          'at runtime - declare it upfront in the data option.'
      )
    return val
  }
  // 5.如果不是响应式的也不需要将其定义成响应式属性
  if (!ob) {
    target[key] = val
    return val
  }
  // 6.将属性定义成响应式的
  defineReactive(ob.value, key, val, undefined, ob.shallow, ob.mock)
  if (__DEV__) {
    ob.dep.notify({
      type: TriggerOpTypes.ADD,
      target: target,
      key,
      newValue: val,
      oldValue: undefined
    })
  } else {
    // 通知视图更新
    ob.dep.notify()
  }
  return val
}

Vue为什么需要虚拟DOM

  • Virtual DOM就是用js对象来描述真实DOM,是对真实DOM的抽象
  • 由于直接操作DOM性能低但是js层的操作效率高,可以将DOM操作转化成对象操作,最终通过diff算法比对差异进行更新DOM(减少了对真实DOM的操作)。
  • 虚拟DOM不依赖真实平台环境从而也可以实现跨平台。

源码路径:src\core\vdom\create-element.ts

import config from '../config'
import VNode, { createEmptyVNode } from './vnode'
import { createComponent } from './create-component'
import { traverse } from '../observer/traverse'

import {
  warn,
  isDef,
  isUndef,
  isArray,
  isTrue,
  isObject,
  isPrimitive,
  resolveAsset,
  isFunction
} from '../util/index'

import { normalizeChildren, simpleNormalizeChildren } from './helpers/index'
import type { Component } from 'types/component'
import type { VNodeData } from 'types/vnode'

const SIMPLE_NORMALIZE = 1
const ALWAYS_NORMALIZE = 2

// wrapper function for providing a more flexible interface
// without getting yelled at by flow
export function createElement(
  context: Component,
  tag: any,
  data: any,
  children: any,
  normalizationType: any,
  alwaysNormalize: boolean
): VNode | Array<VNode> {
  if (isArray(data) || isPrimitive(data)) {
    normalizationType = children
    children = data
    data = undefined
  }
  if (isTrue(alwaysNormalize)) {
    normalizationType = ALWAYS_NORMALIZE
  }
  return _createElement(context, tag, data, children, normalizationType)
}

export function _createElement(
  context: Component,
  tag?: string | Component | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
  if (isDef(data) && isDef((data as any).__ob__)) {
    __DEV__ &&
      warn(
        `Avoid using observed data object as vnode data: ${JSON.stringify(
          data
        )}\n` + 'Always create fresh vnode data objects in each render!',
        context
      )
    return createEmptyVNode()
  }
  // object syntax in v-bind
  if (isDef(data) && isDef(data.is)) {
    tag = data.is
  }
  if (!tag) {
    // in case of component :is set to falsy value
    return createEmptyVNode()
  }
  // warn against non-primitive key
  if (__DEV__ && isDef(data) && isDef(data.key) && !isPrimitive(data.key)) {
    warn(
      'Avoid using non-primitive value as key, ' +
        'use string/number value instead.',
      context
    )
  }
  // support single function children as default scoped slot
  if (isArray(children) && isFunction(children[0])) {
    data = data || {}
    data.scopedSlots = { default: children[0] }
    children.length = 0
  }
  if (normalizationType === ALWAYS_NORMALIZE) {
    children = normalizeChildren(children)
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    children = simpleNormalizeChildren(children)
  }
  let vnode, ns
  if (typeof tag === 'string') {
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    if (config.isReservedTag(tag)) {
      // platform built-in elements
      if (
        __DEV__ &&
        isDef(data) &&
        isDef(data.nativeOn) &&
        data.tag !== 'component'
      ) {
        warn(
          `The .native modifier for v-on is only valid on components but it was used on <${tag}>.`,
          context
        )
      }
      vnode = new VNode(
        config.parsePlatformTagName(tag),
        data,
        children,
        undefined,
        undefined,
        context
      )
    } else if (
      (!data || !data.pre) &&
      isDef((Ctor = resolveAsset(context.$options, 'components', tag)))
    ) {
      // component
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      // unknown or unlisted namespaced elements
      // check at runtime because it may get assigned a namespace when its
      // parent normalizes children
      vnode = new VNode(tag, data, children, undefined, undefined, context)
    }
  } else {
    // direct component options / constructor
    vnode = createComponent(tag as any, data, context, children)
  }
  if (isArray(vnode)) {
    return vnode
  } else if (isDef(vnode)) {
    if (isDef(ns)) applyNS(vnode, ns)
    if (isDef(data)) registerDeepBindings(data)
    return vnode
  } else {
    return createEmptyVNode()
  }
}

function applyNS(vnode, ns, force?: boolean) {
  vnode.ns = ns
  if (vnode.tag === 'foreignObject') {
    // use default namespace inside foreignObject
    ns = undefined
    force = true
  }
  if (isDef(vnode.children)) {
    for (let i = 0, l = vnode.children.length; i < l; i++) {
      const child = vnode.children[i]
      if (
        isDef(child.tag) &&
        (isUndef(child.ns) || (isTrue(force) && child.tag !== 'svg'))
      ) {
        applyNS(child, ns, force)
      }
    }
  }
}

// ref #5318
// necessary to ensure parent re-render when deep bindings like :style and
// :class are used on slot nodes
function registerDeepBindings(data) {
  if (isObject(data.style)) {
    traverse(data.style)
  }
  if (isObject(data.class)) {
    traverse(data.class)
  }
}

src\core\vdom\vnode.ts

import type { Component } from 'types/component'
import type { ComponentOptions } from 'types/options'
import type { VNodeComponentOptions, VNodeData } from 'types/vnode'

/**
 * @internal
 */
export default class VNode {
  tag?: string
  data: VNodeData | undefined
  children?: Array<VNode> | null
  text?: string
  elm: Node | undefined
  ns?: string
  context?: Component // rendered in this component's scope
  key: string | number | undefined
  componentOptions?: VNodeComponentOptions
  componentInstance?: Component // component instance
  parent: VNode | undefined | null // component placeholder node

  // strictly internal
  raw: boolean // contains raw HTML? (server only)
  isStatic: boolean // hoisted static node
  isRootInsert: boolean // necessary for enter transition check
  isComment: boolean // empty comment placeholder?
  isCloned: boolean // is a cloned node?
  isOnce: boolean // is a v-once node?
  asyncFactory?: Function // async component factory function
  asyncMeta: Object | void
  isAsyncPlaceholder: boolean
  ssrContext?: Object | void
  fnContext: Component | void // real context vm for functional nodes
  fnOptions?: ComponentOptions | null // for SSR caching
  devtoolsMeta?: Object | null // used to store functional render context for devtools
  fnScopeId?: string | null // functional scope id support
  isComponentRootElement?: boolean | null // for SSR directives

  constructor(
    tag?: string,
    data?: VNodeData,
    children?: Array<VNode> | null,
    text?: string,
    elm?: Node,
    context?: Component,
    componentOptions?: VNodeComponentOptions,
    asyncFactory?: Function
  ) {
    this.tag = tag
    this.data = data
    this.children = children
    this.text = text
    this.elm = elm
    this.ns = undefined
    this.context = context
    this.fnContext = undefined
    this.fnOptions = undefined
    this.fnScopeId = undefined
    this.key = data && data.key
    this.componentOptions = componentOptions
    this.componentInstance = undefined
    this.parent = undefined
    this.raw = false
    this.isStatic = false
    this.isRootInsert = true
    this.isComment = false
    this.isCloned = false
    this.isOnce = false
    this.asyncFactory = asyncFactory
    this.asyncMeta = undefined
    this.isAsyncPlaceholder = false
  }

  // DEPRECATED: alias for componentInstance for backwards compat.
  /* istanbul ignore next */
  get child(): Component | void {
    return this.componentInstance
  }
}

export const createEmptyVNode = (text: string = '') => {
  const node = new VNode()
  node.text = text
  node.isComment = true
  return node
}

export function createTextVNode(val: string | number) {
  return new VNode(undefined, undefined, undefined, String(val))
}

// optimized shallow clone
// used for static nodes and slot nodes because they may be reused across
// multiple renders, cloning them avoids errors when DOM manipulations rely
// on their elm reference.
export function cloneVNode(vnode: VNode): VNode {
  const cloned = new VNode(
    vnode.tag,
    vnode.data,
    // #7975
    // clone children array to avoid mutating original in case of cloning
    // a child.
    vnode.children && vnode.children.slice(),
    vnode.text,
    vnode.elm,
    vnode.context,
    vnode.componentOptions,
    vnode.asyncFactory
  )
  cloned.ns = vnode.ns
  cloned.isStatic = vnode.isStatic
  cloned.key = vnode.key
  cloned.isComment = vnode.isComment
  cloned.fnContext = vnode.fnContext
  cloned.fnOptions = vnode.fnOptions
  cloned.fnScopeId = vnode.fnScopeId
  cloned.asyncMeta = vnode.asyncMeta
  cloned.isCloned = true
  return cloned
}

Vue2中diff算法原理

  • Vue的diff算法是平级比较,不考虑跨级比较的情况。内部采用深度递归的方式 + 双指针的方式进行比较。
  • 1.先比较是否是相同节点 key tag
  • 2.相同节点比较属性,并复用老节点
  • 3.比较儿子节点,考虑老节点和新节点儿子的情况
  • 4.优化比较:头头、尾尾、头尾、尾头
  • 5.比对查找进行复用
  • Vue3中采用最长递增子序列来实现diff优化

源码位置

src/core/vdom/patch.js:700

src/core/vdom/patch.js`:501 比较两个虚拟节点 `patchVnode()
src/core/vdom/patch.js`:404 比较两个虚拟节点 `patchChildren()

vue-diff.4c21677d

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

  • 响应式数据变化,Vue确实可以在数据发生变化时,响应式系统可以立刻得知。但是如果给每个属性都添加watcher用于更新的话,会产生大量的watcher从而降低性能。
  • 而且粒度过细也会导致更新不精准的问题,所以vue采用了组件级的watcher配合diff来检测差异

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

  • Vue在patch过程中通过key可以判断两个虚拟节点是否是相同节点。 (可以复用老节点)
  • 无key会导致更新的时候出问题
  • 尽量不要采用索引作为key

image-20240719164807015

谈一谈对Vue组件化的理解

  • 组件化开发能大幅提高应用开发效率、测试性、复用性等;
  • 常用的组件化技术:属性、自定义事件、插槽等
  • 降低更新范围,只重新渲染变化的组件
  • 组件的特点:高内聚、低耦合、单向数据流

Vue的组件渲染流程

源码位置:src\core\vdom\patch.ts

  • 渲染的时候,先父后子
  • 卸载的时候,先子后父
  • 产生组件虚拟节点 -> 创建组件的真实节点 -> 插入到页面中

image-20240719164852873

1.在渲染父组件时会创建父组件的虚拟节点,其中可能包含子组件的标签
2.在创建虚拟节点时,获取组件的定义使用 Vue. extend
生成组件的构造函数。
3.将虚拟节点转化成真实节点时,会创建组件的实例并且调用组件的 Smount 方法。
4.所以组件的创建过程是先父后子

Vue组件更新流程

属性更新时会触发patchVnode方法 -> 组件虚拟节点会调用prepatch钩子 -> 更新属性 -> 组件更新

image-20240719164955061

Vue中异步组件原理

  • 默认渲染异步占位符节点 -> 组件加载完毕后调用forceUpdate强制更新

image-20240719165021173

函数组件的优势及原理

  • 函数式组件的特性:无状态、无生命周期、无this。但是性能高 正常组件是一个类继承了Vue, 函数式组件就是普通的函数,没有new的过程,也没有 initprepatch

    src/vdom/create-component.js:163

    if (isTrue(Ctor.options.functional)) { // 函数式组件
        return createFunctionalComponent(Ctor, propsData, data, context, children)
    }
    // extract listeners, since these needs to be treated as
    // child component listeners instead of DOM listeners
    const listeners = data.on // 处理事件
    // replace with listeners with .native modifier
    // so it gets processed during parent component patch.
    data.on = data.nativeOn // 处理原生事件
    
    // install component management hooks onto the placeholder node
    installComponentHooks(data) // 初始化组件钩子方法
    

Vue组件间传值的方式及之间区别

  • props$emit 父组件向子组件传递数据是通过prop传递的,子组件传递数据给父组件是通过$emit触发事件来做到的
  • $parent,$children 获取当前组件的父组件和当前组件的子组件
  • $attrs$listeners A->B->C。Vue 2.4 开始提供了$attrs$listeners来解决这个问题
  • 父组件中通过provide来提供变量,然后在子组件中通过inject来注入变量。
  • $refs 获取实例
  • envetBus 平级组件数据传递 这种情况下可以使用中央事件总线的方式
  • vuex状态管理

props实现原理

<my-component a="1" b="2" c="3" @xxx @qqq @click.native></my-component>
src\core\vdom\create-component.js:192
const vnode = new VNode( // 创建组件虚拟节点
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefined, undefined, undefined, context,
    { Ctor, propsData, listeners, tag, children }, // 包含组件的属性及事件
    asyncFactory
)
src\core\instance\init.js:36
export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
  const opts = vm.$options = Object.create(vm.constructor.options)
  // doing this because it's faster than dynamic enumeration.
  const parentVnode = options._parentVnode
  opts.parent = options.parent
  opts._parentVnode = parentVnode

  const vnodeComponentOptions = parentVnode.componentOptions
  opts.propsData = vnodeComponentOptions.propsData // 将属性添加到$options中
  opts._parentListeners = vnodeComponentOptions.listeners
  opts._renderChildren = vnodeComponentOptions.children
  opts._componentTag = vnodeComponentOptions.tag

  if (options.render) {
    opts.render = options.render
    opts.staticRenderFns = options.staticRenderFns
  }
}

src\core\instance\state.js属性的初始化

function initProps (vm: Component, propsOptions: Object) { // propsOptions 校验属性
  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) {// 用户用户的 props:{}
    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) // 定义到_props中
    }
    // 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) // 将_props代理到实例上
    }
  }
  toggleObserving(true)
}

$on , $emit

<my-component @change="fn" @change="fn" @change="fn"></my-component> // this.$on('change')
<script>
	this.$emit('change')
</script>
opts._parentListeners = vnodeComponentOptions.listeners // 用户在组件上定义的事件
src\core\instance\events.js:12
export function initEvents (vm: Component) {
  vm._events = Object.create(null)
  vm._hasHookEvent = false
  // init parent attached events
  const listeners = vm.$options._parentListeners
  if (listeners) {
    updateComponentListeners(vm, listeners) // 更新组件的事件
  }
}
export function updateComponentListeners (
  vm: Component,
  listeners: Object,
  oldListeners: ?Object
) {
  target = vm // 更新事件,采用add 、 remove方法
  updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm)
  target = undefined
}
function add (event, fn) {
  target.$on(event, fn)
}
function remove (event, fn) {
  target.$off(event, fn)
}

内部采用的就是发布订阅模式来进行实现

p a r e n t , parent, parent,children

src\core\instance\lifecycle.js:32
export function initLifecycle (vm: Component) {
  const options = vm.$options
  // locate first non-abstract parent
  let parent = options.parent
  if (parent && !options.abstract) { // 排除抽象组件
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent
    }
    parent.$children.push(vm) // 让父实例记住当前组件实例
  }

  vm.$parent = parent // 增加$parent属性 指向父实例
  vm.$root = parent ? parent.$root : vm
  // ...
}

$attrs, $listeners

<my-component a="1" b="2"></my-component>  => $vnode.data.attrs = {a:1,b:2}
export function initRender (vm: Component) {
  vm._vnode = null // the root of the child tree
  vm._staticTrees = null // v-once cached trees
  const options = vm.$options 
  const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree 获取占位符节点
  // ...

  const parentData = parentVnode && parentVnode.data // 占位符节点上的数据
  defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
  defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
}

provide & inject

src\core\instance\inject.js:7
export function initProvide (vm: Component) {
  const provide = vm.$options.provide
  if (provide) { // 将用户定义的provide 挂载到_provided
    vm._provided = typeof provide === 'function'
      ? provide.call(vm)
      : provide
  }
}
export function initInjections (vm: Component) { // inject:[a,b,c]
  const result = resolveInject(vm.$options.inject, vm) // 不停的向上查找 inject的属性
  if (result) {
    toggleObserving(false)
    Object.keys(result).forEach(key => {
      /* istanbul ignore else */
      if (process.env.NODE_ENV !== 'production') {
        defineReactive(vm, key, result[key], () => {
          warn(
            `Avoid mutating an injected value directly since the changes will be ` +
            `overwritten whenever the provided component re-renders. ` +
            `injection being mutated: "${key}"`,
            vm
          )
        })
      } else {
        defineReactive(vm, key, result[key])
      }
    })
    toggleObserving(true)
  }
}

$ref

src\core\vdom\modules\ref.js:20
export function registerRef (vnode: VNodeWithData, isRemoval: ?boolean) {
  const key = vnode.data.ref // 获取ref
  if (!isDef(key)) return

  const vm = vnode.context
  const ref = vnode.componentInstance || vnode.elm // 当前组件的实例 或者 组件的真实节点
  const refs = vm.$refs
  if (isRemoval) { // 删除ref
    if (Array.isArray(refs[key])) {
      remove(refs[key], ref)
    } else if (refs[key] === ref) {
      refs[key] = undefined
    }
  } else {
    if (vnode.data.refInFor) {
      if (!Array.isArray(refs[key])) {  // 在v-for中是数组 
        refs[key] = [ref]
      } else if (refs[key].indexOf(ref) < 0) {
        // $flow-disable-line
        refs[key].push(ref)
      }
    } else {
      refs[key] = ref
    }
  }
}

$attrs是为了解决什么问题出现的,provide和inject不能解决它能解决的问题吗? v-bind=“ a t t r s " v − o n = " attrs" v-on=" attrs"von="listeners”

$attrs主要的作用就是实现批量传递数据。provide/inject更适合应用在插件中,主要是实现跨级数据传递

v-ifv-for哪个优先级更高?

  • 在V2当中,v-for的优先级更⾼,⽽在V3当中,则是v-if的优先级更⾼。在V3当中,做了v-if的提 升优化,去除了没有必要的计算,但同时也会带来⼀个⽆法取到v-for当中遍历的item问题,这就需要 开发者们采取其他灵活的⽅式去解决这种问题

  • v-for和v-if不要在同一个标签中使用,因为解析时先解析v-for在解析v-if。如果遇到需要同时使用时可以考虑写成计算属性的方式。

src/compiler/index.js:19

src/compiler/codegen/index.js::56 解析v-if 和 v-for

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) { // 处理v-for
    return genFor(el, state) 
} else if (el.if && !el.ifProcessed) { // 处理v-if
    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)
}

v-ifv-modelv-for的实现原理

v-for实现原理

v-for`实现原理 `src/compiler/codegen/index.js:187
export function genFor(
    el: any,
    state: CodegenState,
    altGen ? : Function,
    altHelper ? : string
): string {
    const exp = el.for // 拿到表达式arr
    const alias = el.alias
    const iterator1 = el.iterator1 ? `,${el.iterator1}` : ''
    const iterator2 = el.iterator2 ? `,${el.iterator2}` : ''

    if (process.env.NODE_ENV !== 'production' &&
        state.maybeComponent(el) && // slot 和 template不能进行v-for操作
        el.tag !== 'slot' &&
        el.tag !== 'template' &&
        !el.key
    ) {
        state.warn(
            `<${el.tag} v-for="${alias} in ${exp}">: component lists rendered with ` +
            `v-for should have explicit keys. ` +
            `See https://vuejs.org/guide/list.html#key for more info.`,
            el.rawAttrsMap['v-for'],
            true /* tip */
        )
    }
    el.forProcessed = true // avoid recursion 生成循环函数
    const r = `${altHelper || '_l'}((${exp}),` +
        `function(${alias}${iterator1}${iterator2}){` +
        `return ${(altGen || genElement)(el, state)}` +
        '})'

    return r;
}

v-if实现原理

src/compiler/codegen/index.js:147

function genIfConditions(
    conditions: ASTIfConditions,
    state: CodegenState,
    altGen ? : Function,
    altEmpty ? : string
): string {
    if (!conditions.length) {
        return altEmpty || '_e()'
    }
    const condition = conditions.shift()
    if (condition.exp) { // 如果有表达式
        return `(${condition.exp})?${ // 将表达式拼接起来
      genTernaryExp(condition.block)
    }:${ // v-else-if
      genIfConditions(conditions, state, altGen, altEmpty) 
    }`
    } else {
        return `${genTernaryExp(condition.block)}` // 没有表达式直接生成元素 像v-else
    }

    // v-if with v-once should generate code like (a)?_m(0):_m(1)
    function genTernaryExp(el) {
        return altGen ?
            altGen(el, state) :
            el.once ?
            genOnce(el, state) :
            genElement(el, state)
    }
}

v-model实现原理

普通元素上的v-model指令

src/compiler/codegen/index.js:310

function genDirectives(el: ASTElement, state: CodegenState): string | void {
    const dirs = el.directives // 获取所有指令
    if (!dirs) return
    let res = 'directives:['
    let hasRuntime = false
    let i, l, dir, needRuntime
    for (i = 0, l = dirs.length; i < l; i++) {
        dir = dirs[i]
        needRuntime = true
        const gen: DirectiveFunction = state.directives[dir.name]
        if (gen) {
            // compile-time directive that manipulates AST.
            // returns true if it also needs a runtime counterpart.
            needRuntime = !!gen(el, dir, state.warn) // 添加input事件 和 value属性
        }
        if (needRuntime) {
            hasRuntime = true // 是否需要运行时
            res += `{name:"${dir.name}",rawName:"${dir.rawName}"${
        dir.value ? `,value:(${dir.value}),expression:${JSON.stringify(dir.value)}` : ''
      }${
        dir.arg ? `,arg:${dir.isDynamicArg ? dir.arg : `"${dir.arg}"`}` : ''
      }${
        dir.modifiers ? `,modifiers:${JSON.stringify(dir.modifiers)}` : ''
      }},`
        }
    }
    if (hasRuntime) { // directives:[{name:"model",rawName:"v-model",value:(msg),expression:"msg"}] 生成对应指令
        let result = res.slice(0, -1) + ']'
        return result;
    }
}

组件上的v-model指令

image-20240719171531374

function transformModel (options, data: any) { 
  const prop = (options.model && options.model.prop) || 'value' // 默认采用value属性
  const event = (options.model && options.model.event) || 'input' // 默认采用input事件
  ;(data.attrs || (data.attrs = {}))[prop] = data.model.value  // 绑定属性
  const on = data.on || (data.on = {})  // 绑定事件
  const existing = on[event]
  const callback = data.model.callback
  if (isDef(existing)) {
    if (
      Array.isArray(existing)
        ? existing.indexOf(callback) === -1
        : existing !== callback
    ) {
      on[event] = [callback].concat(existing) 
    }
  } else {
    on[event] = callback
  }
}

Vue中slot是如何实现的?什么时候使用它?

  • 普通插槽(模板传入到组件中,数据采用父组件数据),普通插槽是在父组件中渲染的
  • 作用域插槽(在父组件中访问子组件数据),作用域插槽是在子组件中渲染的

n-slot.91d66406

const templateCompiler = require('vue-template-compiler');
let r = templateCompiler.compile(`
<div>
    <slot name="title"></slot>
    <slot name="content"></slot>
</div>`);
// with(this){return _c('div',[_t("title"),_v(" "),_t("content")],2)}
console.log(r.render)


let r1 = templateCompiler.compile(`
<my>
    <h1 slot="title">标题</h1>
    <div slot="content">内容</div>
</my>`)
/**
with(this){ 
    return _c('my',[
        _c('h1',{attrs:{"slot":"title"},slot:"title"},[_v("标题")]),_v(" "),
        _c('div',{attrs:{"slot":"content"},slot:"content"},[_v("内容")])
    ])
}
**/
console.log(r1.render)

slot-scope.c373a157

et r3 = templateCompiler.compile(`
<div>
    <slot :article="{title:'标题',content:'内容'}"></slot>
</div>`);
// with(this){return _c('div',[_t("default",null,{"article":{title:'标题',content:'内容'}})],2)}
console.log(r3.render)

let r4 = templateCompiler.compile(`
<my>
    <template slot-scope="{article}">
        <h1 slot="article.title">标题</h1>
        <div slot="article.content">内容</div>
    </template>
</my>`)
/**
with(this){return _c('my',
    {scopedSlots:_u([
        {key:"default",fn:function({article}){
                return [
                    _c('h1',{attrs:{"slot":"article.title"},slot:"article.title"},[_v("标题")]),
                    _v(" "),
                    _c('div',{attrs:{"slot":"article.content"},slot:"article.content"},[_v("内容")])
                ]
            }
        }
    ])
})}
 */
console.log(r4.render)

通插槽,渲染在父级, 作用域插槽在组件内部渲染!

Vue.use是干什么的?原理是什么?

  • Vue.use是用来使用插件的,我们可以在插件中扩展全局组件、指令、原型方法等。
  • 会调用插件的install方法,将Vue的构造函数默认传入,这样在插件中可以使用Vue无需依赖Vue
src/core/global-api/use.js
Vue.use = function (plugin: Function | Object) {
    // 插件缓存
    const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
    if (installedPlugins.indexOf(plugin) > -1) { // 如果已经有插件 直接返回
        return this
    }

    // additional parameters
    const args = toArray(arguments, 1) // 除了第一项其他的参数整合成数组
    args.unshift(this) // 将Vue 放入到数组中
    if (typeof plugin.install === 'function') { // 调用install方法
        plugin.install.apply(plugin, args)
    } else if (typeof plugin === 'function') { // 直接调用方法
        plugin.apply(null, args)
    }
    installedPlugins.push(plugin) // 缓存插件
    return this
}

组件中写name选项有哪些好处及作用?

  • 增加name选项会在components属性中增加组件本身,实现组件的递归调用。
  • 可以标识组件的具体名称方便调试和查找对应组件。
src/core/global-api/extend.js:67
Sub.options.components[name] = Sub

Vue事件修饰符有哪些?其实现原理是什么?

  • .stop、.prevent、.capture!、.self、.once~、.passive&
src\compiler\helpers.js:69
export function addHandler (
  el: ASTElement,
  name: string,
  value: string,
  modifiers: ?ASTModifiers,
  important?: boolean,
  warn?: ?Function,
  range?: Range,
  dynamic?: boolean
) {
  modifiers = modifiers || emptyObject
  // warn prevent and passive modifier
  /* istanbul ignore if */
  if (
    process.env.NODE_ENV !== 'production' && warn &&
    modifiers.prevent && modifiers.passive
  ) {
    warn(
      'passive and prevent can\'t be used together. ' +
      'Passive handler can\'t prevent default event.',
      range
    )
  }
  if (modifiers.right) {
    if (dynamic) {
      name = `(${name})==='click'?'contextmenu':(${name})`
    } else if (name === 'click') {
      name = 'contextmenu'
      delete modifiers.right
    }
  } else if (modifiers.middle) {
    if (dynamic) {
      name = `(${name})==='click'?'mouseup':(${name})`
    } else if (name === 'click') {
      name = 'mouseup'
    }
  }

  // check capture modifier
  if (modifiers.capture) { // 如果capture 用!标记
    delete modifiers.capture
    name = prependModifierMarker('!', name, dynamic)
  }
  if (modifiers.once) { // 如果是once 用~ 标记
    delete modifiers.once
    name = prependModifierMarker('~', name, dynamic)
  }
  /* istanbul ignore if */
  if (modifiers.passive) { // 如果是passive 用 &标记
    delete modifiers.passive
    name = prependModifierMarker('&', name, dynamic)
  }

  let events
  if (modifiers.native) {
    delete modifiers.native
    events = el.nativeEvents || (el.nativeEvents = {})
  } else {
    events = el.events || (el.events = {})
  }

  const newHandler: any = rangeSetItem({ value: value.trim(), dynamic }, range)
  if (modifiers !== emptyObject) {
    newHandler.modifiers = modifiers
  }

  const handlers = events[name]
  /* istanbul ignore if */
  if (Array.isArray(handlers)) {
    important ? handlers.unshift(newHandler) : handlers.push(newHandler)
  } else if (handlers) {
    events[name] = important ? [newHandler, handlers] : [handlers, newHandler]
  } else {
    events[name] = newHandler
  }

  el.plain = false
}

src\compiler\codegen\events.js:42

function genHandler (handler: ASTElementHandler | Array<ASTElementHandler>): string {  
    let code = ''
    let genModifierCode = ''
    const keys = []
    for (const key in handler.modifiers) {
      if (modifierCode[key]) {
        genModifierCode += modifierCode[key]
        // left/right
        if (keyCodes[key]) {
          keys.push(key)
        }
      } else if (key === 'exact') {
        const modifiers: ASTModifiers = (handler.modifiers: any)
        genModifierCode += genGuard(
          ['ctrl', 'shift', 'alt', 'meta']
            .filter(keyModifier => !modifiers[keyModifier])
            .map(keyModifier => `$event.${keyModifier}Key`)
            .join('||')
        )
      } else {
        keys.push(key) // modifiers中表达式存起来 
      }
    }
    if (keys.length) {
      code += genKeyFilter(keys)
    }
    // Make sure modifiers like prevent and stop get executed after key filtering
    if (genModifierCode) {
      code += genModifierCode
    }
    const handlerCode = isMethodPath
      ? `return ${handler.value}.apply(null, arguments)`
      : isFunctionExpression
        ? `return (${handler.value}).apply(null, arguments)`
        : isFunctionInvocation
          ? `return ${handler.value}`
          : handler.value
    /* istanbul ignore if */
    if (__WEEX__ && handler.params) {
      return genWeexHandler(handler.params, code + handlerCode)
    }
    return `function($event){${code}${handlerCode}}`
}

src\platforms\web\runtime\modules\events.js:105

export function updateListeners (
  on: Object,
  oldOn: Object,
  add: Function,
  remove: Function,
  createOnceHandler: Function,
  vm: Component
) {
  let name, def, cur, old, event
  for (name in on) { // 循环on中的 即事件
    def = cur = on[name]
    old = oldOn[name]
    event = normalizeEvent(name) // 事件修饰符
    /* istanbul ignore if */
    if (__WEEX__ && isPlainObject(def)) {
      cur = def.handler
      event.params = def.params
    }
    if (isUndef(cur)) {
      process.env.NODE_ENV !== 'production' && warn(
        `Invalid handler for event "${event.name}": got ` + String(cur),
        vm
      )
    } else if (isUndef(old)) {
      if (isUndef(cur.fns)) {
        cur = on[name] = createFnInvoker(cur, vm)
      }
      if (isTrue(event.once)) {
        cur = on[name] = createOnceHandler(event.name, cur, event.capture)
      }
      add(event.name, cur, event.capture, event.passive, event.params)
    } else if (cur !== old) {
      old.fns = cur
      on[name] = old
    }
  }
  for (name in oldOn) {
    if (isUndef(on[name])) {
      event = normalizeEvent(name)
      remove(event.name, oldOn[name], event.capture)
    }
  }
}

Vue中.sync修饰符的作用,用法及实现原理

src\compiler\parser\index.js:798

 if (modifiers.sync) {
     syncGen = genAssignmentCode(value, `$event`) // 转.async 改成 ${value} = xxx
     if (!isDynamic) {
         addHandler( // 添加update事件
             el,
             `update:${camelize(name)}`, 
             syncGen,
             null,
             false,
             warn,
             list[i]
         )
         if (hyphenate(name) !== camelize(name)) {
             addHandler(
                 el,
                 `update:${hyphenate(name)}`,
                 syncGen,
                 null,
                 false,
                 warn,
                 list[i]
             )
         }
     } else {
         // handler w/ dynamic event name
         addHandler(
             el,
             `"update:"+(${name})`,
             syncGen,
             null,
             false,
             warn,
             list[i],
             true // dynamic
         )
     }
 }
}
let r5 = templateCompiler.compile(`
    <my :value.sync="xxxx"></my>
`);

// with(this){return _c('my',{attrs:{"value":xxxx},on:{"update:value":function($event){xxxx=$event}}})}
console.log(r5.render)

如何理解自定义指令

  • 1.在生成ast语法树时,遇到指令会给当前元素添加directives属性

  • 2.通过genDeirectives生成指令代码

  • 3.在patch前将指令的钩子提取到cbs中,在patch过程中调用对应的钩子

  • 4.当执行cbs对应的钩子时,调用对应指令定义的方法

    src/vdom/patch.js:77 提取钩子函数

    const hooks = ['create', 'activate', 'update', 'remove', 'destroy']
    const { modules, nodeOps } = backend
    for (i = 0; i < hooks.length; ++i) {
        cbs[hooks[i]] = []
        for (j = 0; j < modules.length; ++j) {
            if (isDef(modules[j][hooks[i]])) {
                cbs[hooks[i]].push(modules[j][hooks[i]]); // 收集hook,patch过程中调用
                // {create:[fn,fn],activate:[fn,fn]...}
            }
        }
    }
    

    src/vdom/modules/directives.js:7 指令钩子

    export default { // 指令的钩子, 在创建和更新过程中会调用 create、update、destroy钩子
      create: updateDirectives,
      update: updateDirectives,
      destroy: function unbindDirectives (vnode: VNodeWithData) {
        updateDirectives(vnode, emptyNode)
      }
    }
    function updateDirectives (oldVnode: VNodeWithData, vnode: VNodeWithData) { 
      if (oldVnode.data.directives || vnode.data.directives) { // 如果有指令
        _update(oldVnode, vnode)
      }
    }
    

keep-alive平时在哪里使用?原理是?

  • 使用keep-alive包裹动态组件时, 会对组件进行缓存。避免组件的重新创建
<keep-alive :include="whiteList" :exclude="blackList" :max="count">
     <component :is="component"></component>
</keep-alive>
<keep-alive :include="whiteList" :exclude="blackList" :max="count">
    <router-view></router-view>
</keep-alive>
  • 实现原理(lRU 算法)

    export default {
      name: 'keep-alive',
      abstract: true, // 不会放到对应的lifecycle
    
      props: {
        include: patternTypes, // 白名单
        exclude: patternTypes, // 黑名单
        max: [String, Number] // 缓存的最大个数
      },
    
      created () {
        this.cache = Object.create(null) // 缓存列表
        this.keys = []  // 缓存的key列表
      },
    
      destroyed () {
        for (const key in this.cache) { // keep-alive销毁时 删除所有缓存
          pruneCacheEntry(this.cache, key, this.keys)
        }
      },
    
      mounted () { // 监控缓存列表
        this.$watch('include', val => { 
          pruneCache(this, name => matches(val, name))
        })
        this.$watch('exclude', val => {
          pruneCache(this, name => !matches(val, name))
        })
      },
    
      render () {
        const slot = this.$slots.default
        const vnode: VNode = getFirstComponentChild(slot)// 获得第一个组件
        const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
        if (componentOptions) {
          // check pattern
          const name: ?string = getComponentName(componentOptions)
          const { include, exclude } = this
          if ( // 获取组件名 看是否需要缓存,不需要缓存则直接返回
            // not included
            (include && (!name || !matches(include, name))) ||
            // excluded
            (exclude && name && matches(exclude, name))
          ) {
            return vnode
          }
    
          const { cache, keys } = this
          const key: ?string = vnode.key == null
            // same constructor may get registered as different local components
            // so cid alone is not enough (#3269)
            ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
            : vnode.key // 生成缓存的key
          if (cache[key]) { // 如果有key 将组件实例直接复用
            vnode.componentInstance = cache[key].componentInstance
            // make current key freshest
            remove(keys, key)
            keys.push(key) // lru算法
          } else {
            cache[key] = vnode // 缓存组件
            keys.push(key)
            // prune oldest entry
            if (this.max && keys.length > parseInt(this.max)) {
              pruneCacheEntry(cache, keys[0], keys, this._vnode) // 超过最大限制删除第一个
            }
          }
    
          vnode.data.keepAlive = true // 在firstComponent的vnode中增加keep-alive属性
        }
        return vnode || (slot && slot[0])
      }
    }
    
    • keep-alive第一次渲染的时候,会将其第一个子组件,缓存起来。
    • 当组件后续在次被激活时,会复用上一次缓存的实例进行渲染。

    src\core\vdom\patch.js:210

    function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
        let i = vnode.data
        if (isDef(i)) {
            const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
            if (isDef(i = i.hook) && isDef(i = i.init)) {
                i(vnode, false /* hydrating */)
            }
            if (isDef(vnode.componentInstance)) {
                initComponent(vnode, insertedVnodeQueue)
                insert(parentElm, vnode.elm, refElm) // 将原来的elm,插入到页面中
                if (isTrue(isReactivated)) {
                    reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
                }
                return true
            }
        }
    }
    

    src\core\vdom\create-component.js:36

    const componentVNodeHooks = {
        init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
            if ( 
            vnode.componentInstance &&
            !vnode.componentInstance._isDestroyed &&
            vnode.data.keepAlive // 有keepAlive, 不在执行组件的初始化流程
            ) {
                // kept-alive components, treat as a patch
                const mountedNode: any = vnode // work around flow
                componentVNodeHooks.prepatch(mountedNode, mountedNode)
            } else {
                const child = vnode.componentInstance = createComponentInstanceForVnode(
                    vnode,
                    activeInstance
                )
                //  组件挂载 当前组件实例中 包含$el属性
                child.$mount(hydrating ? vnode.elm : undefined, hydrating)
            }
        }
    }
    

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 的回调函数,创建好的组件实例会作为回调函数的参数传入。
const queue: Array<?NavigationGuard> = [].concat(
    // in-component leave guards
    extractLeaveGuards(deactivated), // 离开钩子
    // global before hooks
    this.router.beforeHooks, // 全局before钩子
    // in-component update hooks
    extractUpdateHooks(updated), // 更新钩子 beforeRouteUpdate
    // in-config enter guards
    activated.map(m => m.beforeEnter), // beforeEnter钩子
    // async components
    resolveAsyncComponents(activated) // 异步组件
)
runQueue(queue, iterator, () => {
    // wait until async components are resolved before
    // extracting in-component enter guards
    const enterGuards = extractEnterGuards(activated) // beforeRouteEnter
    const queue = enterGuards.concat(this.router.resolveHooks) // beforeResolve
    runQueue(queue, iterator, () => {
        afterEachs.forEach(fn=>fn())
    })
})
}

Vue-Router的两种模式的区别

  • Vue-Router 有三种模式 hashhistoryabstract
  • abstract模式是在不支持浏览器API环境使用,不依赖于浏览器历史
  • hash模式:hash + popState/hashChange 兼容性好但是不够美观,hash服务端无法获取。不利于seo优化
  • history模式: historyApi+ popState 美观,刷新会出现404 -> CLI webpack history-fallback

谈一下你对vuex的个人理解

  • vuex是专门为vue提供的全局状态管理系统,用于多个组件中数据共享、数据缓存等。(无法持久化、内部核心原理是通过创造一个全局实例 new Vue

image-20240719172843495

方法: replaceStatesubscriberegisterModulenamespace(modules)、辅助函数…

mutationaction的区别

  • mutation: 主要在于修改状态,必须同步执行
  • action: 执行业务代码,方便复用,逻辑可以为异步,不能直接修改状态
function enableStrictMode (store) {
  store._vm.$watch(function () { return this._data.$$state }, function () {
    if ((process.env.NODE_ENV !== 'production')) {
      assert(store._committing, "do not mutate vuex store state outside mutation handlers.");
    }
  }, { deep: true, sync: true }); // 同步watcher监控状态变化
}

Vue中的性能优化有哪些?

  • 数据层级不易过深,合理设置响应式数据
  • 使用数据时缓存值的结果,不频繁取值。
  • 合理设置Key属性
  • v-show和v-if的选取
  • 控制组件粒度 -> Vue采用组件级更新
  • 采用函数式组件 -> 函数式组件开销低
  • 采用异步组件 -> 借助webpack分包的能力
  • 使用keep-alive缓存组件
  • 虚拟滚动、时间分片等策略…
  • 打包优化

Vue中使用了哪些设计模式?

  • 单例模式 - 单例模式就是整个程序有且仅有一个实例
export function install (_Vue) {
  if (Vue && _Vue === Vue) {
    if (__DEV__) {
      console.error(
        '[vuex] already installed. Vue.use(Vuex) should be called only once.'
      )
    }
    return
  }
  Vue = _Vue
  applyMixin(Vue)
}
  • 工厂模式 - 传入参数即可创建实例 (createElement)
export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
  // ...
  if (typeof tag === 'string') {
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    if (config.isReservedTag(tag)) {
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
    } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
      )
    }
  } else {
    vnode = createComponent(tag, data, context, children)
  }
  // ....
}
  • 发布订阅模式 - 订阅者把自己想订阅的事件注册到调度中心,当该事件触发时候,发布者发布该事件到调度中心,由调度中心统一调度订阅者注册到调度中心的处理代码。
Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
    const vm: Component = this
    if (Array.isArray(event)) {
        for (let i = 0, l = event.length; i < l; i++) {
            vm.$on(event[i], fn)
        }
    } else {
        (vm._events[event] || (vm._events[event] = [])).push(fn)
        if (hookRE.test(event)) {
            vm._hasHookEvent = true
        }
    }
    return vm
}
Vue.prototype.$emit = function (event: string): Component {
    const vm: Component = this
    let cbs = vm._events[event]
    if (cbs) {
      cbs = cbs.length > 1 ? toArray(cbs) : cbs
      const args = toArray(arguments, 1)
      const info = `event handler for "${event}"`
      for (let i = 0, l = cbs.length; i < l; i++) {
        invokeWithErrorHandling(cbs[i], vm, args, vm, info)
      }
    }
    return vm
}
  • 观察者模式 - watcher&dep的关系
  • 代理模式 - 代理模式给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用。

_data属性、proxy、防抖、节流 let p = new Proxy

  • 装饰模式 - Vue2装饰器的用法 (对功能进行增强 @)
  • 中介者模式 - 中介者是一个行为设计模式,通过提供一个统一的接口让系统的不同部分进行通信。 Vuex
  • 策略模式 - 策略模式指对象有某个行为,但是在不同的场景中,该行为有不同的实现方案。 mergeOptions
  • 外观模式 - 提供了统一的接口,用来访问子系统中的一群接口
  • 5
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
vue2和vue3在面试中常见的问包括以下几个方面的不同: 1. 响应式原理: vue2使用Object.defineProperty实现响应式,而vue3使用Proxy对象实现更高效的响应式系统。 2. 响应性判断函数: vue3引入了一些新的响应性判断函数,如isRef、isReactive和isReadonly等。 3. setup函数: vue3中引入了setup函数,它是组件中的一个新选项,用于替代vue2中的created和mounted钩子函数。 4. Composition API: vue3中引入了Composition API,它是一种新的组件组织形式,类似于React Hook。它相比于vue2的Options API具有更好的可读性和组合性。 5. ref和reactive: ref和reactive是vue3中用于数据管理的两种不同的响应式API。ref适用于单个简单的数据,而reactive适用于复杂的对象。 6. watch和watchEffect: vue3中的watch和watchEffect函数与vue2中的watch不同,它们提供了更简洁和灵活的方式来监听数据的变化。 7. SSR(服务端渲染): SSR是指在服务器端生成最终的HTML页面并将其发送给客户端,以提高首屏加载速度和SEO友好性。vue3对SSR提供了更好的支持。 除了以上的主要区别之外,还有一些细节上的差异,如生命周期函数的变化等。总的来说,vue3相比于vue2在性能、开发体验和功能上都有不少的改进和优化。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [【前端vue3面试】2023最新面试实录vue3.0,高频10问(持续更新...)](https://blog.csdn.net/wzySan/article/details/129091719)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* *3* [vue2/3面试](https://blog.csdn.net/weixin_57547803/article/details/129619964)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值