十万字详解Vue2全部Api原理(一)

257 阅读22分钟

没有文案,直接上正文吧

目前已经整理了三万多字,后面持续更新……

一、API概览

1.全局配置 9个

通过Vue.config.xxx进行配置

  1. silent -- 取消所有日志和警告
  2. optionMergeStrategies -- 自定义合并选项策略
  3. devtools -- 是否允许浏览器插件识别代码
  4. errorHandler -- 全局错误捕获
  5. warnHandler -- 全局警告捕获
  6. ignoredElements -- 忽略Vue之外的自定义元素
  7. keyCodes -- 给v-on添加键位别名,
  8. performance -- 允许浏览器开发工具性能追踪
  9. productionTip -- 阻止启动时生成生产提示 (控制台)

2.全局API 12个

通过Vue.xxx调用,一些api需要在new Vue之前调用,例如use

  1. Vue.extend -- Vue构造器
  2. Vue.nextTick -- 下次DOM更新循环之后的延迟回调
  3. Vue.set -- 增加响应式数据
  4. Vue.delete -- 删除响应式数据中的某个属性,会彻底解绑其所关联的watcher和dep等
  5. Vue.directive -- 注册或获取全局指令
  6. Vue.filter -- 注册或获取全局过滤器
  7. Vue.component -- 注册组件 及查找
  8. Vue.use -- 注入插件
  9. Vue.mixin -- 混入
  10. Vue.compile -- 模板编译
  11. Vue.observable -- 手动对某个对象进行观测
  12. Vue.version -- 版本号

3.数据选项 6个

  1. data -- 数据
  2. props -- 接收的数据
  3. propsData -- new 实例时传递的props,可在new的时候传递,等同于props
  4. computed -- 计算属性
  5. methods -- 方法集合
  6. watch -- 用户watcher , 原理也是new Watcher

4.DOM选项 4个

  1. el -- 根节点挂载的dom元素
  2. template -- 渲染模板,渲染优先级大于data,如果有则取它,没有则会取el对应的元素的innerHTML
  3. render -- 虚拟dom转换函数,将传入的数据转换成vnode 原理懒得写
  4. renderError -- render生效时调用的,可在做类似暂无数据提示的组件

5.生命周期选项 14个

8个组件钩子,2个keep-alive钩子,1个错误捕获钩子,

3个v3钩子:渲染追踪、渲染已触发、服务端渲染前

订阅:mergeOptions执行starts中的lifecycle_hooks策略,将对应的hook维护到options上

发布:通过callhook取出对应的handler,调用该handler,(在invokeWithErrorHandling中执行的,收集错误日志)

  1. beforeCreate -- 数据初始化前
  2. created -- 数据初始化完成
  3. beforeMount -- 开始挂载前
  4. mounted -- 挂载完成
  5. beforeUpdate -- 视图变更前
  6. updated -- 视图变更后
  7. activated -- keep-alive组件激活时
  8. deactivated -- keep-alive组件失活时
  9. beforeDestroy -- 实例销毁前
  10. destroyed -- 实例销毁后
  11. errorCaptured -- 捕获错误日志
  12. serverPrefetch --- 服务端渲染前
  13. renderTracked --- 渲染追踪
  14. renderTriggered --- 渲染已触发

6.资源选项 3个

  1. directives -- 自定义指令
  2. filters -- 过滤器
  3. components -- 组件注册及查找

7.组合选项 4个

  1. parent -- 认贼作父,强制和其它组件 建立父子关系
  2. mixins -- 其实就是调用mergeOptions把传进来的options合并到vm上
  3. extends -- 允许声明扩展另一个组件,原理是通过mergeOptions合并到parent中,类似mixin
  4. provide / inject -- 可以理解为上下文,可用于传递数据

8.其它选项 6个

  1. name -- 组件名称,没有实质作用,只是便于调试,更友好的警告信息以及跟语义化的组件构造函数名
  2. delimiters -- 自定义模板语法中的文本取值,例如{{}} 改写为[[]],原理是再createCompileToFunction时,改写正则匹配规则
  3. functional -- 标记是否为函数组件
  4. model -- v-model
  5. inheritAttrs -- 传递给组件的属性如果没有用props接收,会被添加到元素上面,设为false可隐藏属性
  6. comments -- 保留代码注释

9.实例属性 13个

  1. vm.$data -- 等同于data
  2. vm.$props -- 等同于props
  3. vm.$el -- 实例真实Dom根元素
  4. vm.$options -- 当前实例的全部选项
  5. vm.$parent -- 父节点
  6. vm.$root -- 根节点
  7. vm.$children -- 子节点
  8. vm.$slots -- 访问被具名插槽分发的内容
  9. vm.$scopedSlots -- 访问作用域插槽
  10. vm.$refs -- 用ref注册的dom对象
  11. vm.$isServer -- 是否运行于服务器
  12. vm.attrs父组件传递过来,但没有被子组件props接收的的属性,会被维护到子组件的attrs -- 父组件传递过来,但没有被子组件props接收的的属性,会被维护到子组件的attrs
  13. vm.$listeners -- 父组件中不含.native的事件监听器

10.实例数据方法 3个

  1. vm.watch选项watch调用的watch -- 选项watch调用的watch,里面new Watcher
  2. vm.$set -- 等同于Vue.set
  3. vm.$delete -- 等同于Vue.delete

11.实例事件方法 4个

  1. vm.$on -- 监听实例事件 ,eventMixin注入 > 将传入的函数添加到_events中
  2. vm.once派发一个只执行一次的实例事件,eventMixin注入>将一个执行fn的函数传events,该函数中执fn时先调用once -- 派发一个只执行一次的实例事件,eventMixin注入 > 将一个执行fn的函数传入_events,该函数中执fn时先调用off移除事件
  3. vm.$off -- 关闭监听事件,eventMixin注入 > 移除_events中对应的函数
  4. vm.$emit -- 触发监听事件,eventMixin注入 > 取出_event中对应的函数并执行

12.实例生命周期方法 4个

  1. vm.$mount -- 挂载实例,调用mountComponent方法,创建渲染watcher
  2. vm.$forceUpdate -- 强制渲染一次,很简单,调用vm._watcher.update()
  3. vm.$nextTick -- 调用nextTick方法
  4. vm.$destroy -- 销毁实例,两个callHook,将很多属性改为null

13.原生指令 14个

  1. v-text -- 渲染指定文本
  2. v-html -- 渲染指定html
  3. v-show -- 显示隐藏
  4. v-if -- 是否渲染
  5. v-else -- 是否渲染
  6. v-else-if -- 是否渲染
  7. v-for -- 循环
  8. v-on -- 绑定事件$on
  9. v-bind -- 绑定属性
  10. v-model -- 双向绑定
  11. v-slot -- 具名插槽
  12. v-pre -- 跳过编译,标记静态节点可提高性能
  13. v-cloak -- 添加到实例属性上直到关联实例结束编译,例如{{ name }} name可能时异步取值,在防止取到值之前的错误渲染,添加了该指令后不会被渲染为‘{{ name }}’,同时结合css将其隐藏,[v-clock]:{ display:none }
  14. v-once -- 只渲染一次,被标记为静态节点,之后更新操作都不会被重新渲染

14.特殊属性 6个

  1. key -- 标记节点,vnode进行节点比对时大有用处
  2. ref -- 获取真实dom,将该节点的$el赋值给ref所指的变量名,并同步给vm
  3. is -- 用于动态组件,指向组件的key
  4. slot -- 废弃 被v-slot替代
  5. slot-scope -- 废弃 被v-slot替代
  6. scope -- 废弃 被v-slot替代

15.内置组件 5个

  1. component -- 渲染动态组件 通过is查找对应组件
  2. transition -- 动画组件
  3. transition-group 动画组组件
  4. keep-alive -- 缓存
  5. slot -- 插槽

二、全局配置原理详解

1. silent

取消所有日志和警告

原理:在warn方法和tip方法中对config.slient进行判断后,再选择性输出日志和警告

warn = (msg, vm = currentInstance) => {
  const trace = vm ? generateComponentTrace(vm) : ''
  if (config.warnHandler) {
    config.warnHandler.call(null, msg, vm, trace)
  } else if (hasConsole && !config.silent) { // silent为true时不打印错误信息
    console.error(`[Vue warn]: ${msg}${trace}`)
  }
}
tip = (msg, vm) => {
  if (hasConsole && !config.silent) { // silent为true时不打印警告信息
    console.warn(`[Vue tip]: ${msg}` + (vm ? generateComponentTrace(vm) : ''))
  }
}

2. optionMergeStrategies

自定义选项合并策略

例如:Vue.config.optionMergeStrategies.myMethods = { //...合并策略 },然后就可在选项中添加我们写的myMethods:{ onClick(){ //... } }

意思就是可以在options中写除了data props methods watch computed 生命周期等以外的,自定义的东西,并且让其挂载到vm

原理:它是选项合并策略的初始化对象,mergeOptions会从基于它扩展的starts中取出对应的策略,再对选项进行合并.我们在该对象上可以增加自己的策略

// config.js
export default {
  optionMergeStrategies: Object.create(null),
  // ……
}
// /src/core/util/options.ts
const strats = config.optionMergeStrategies // 1.基于该选项合并策略
LIFECYCLE_HOOKS.forEach(hook => {
  strats[hook] = mergeLifecycleHook // 2.扩展生命周期钩子函数合并策略
})

3. devtools

是否允许浏览器插件识别代码

原理:初始化devtools前先判断config.devtools是否为true,再执行devtools的钩子

// devtool hook
/* istanbul ignore if */
if (devtools && config.devtools) {
    devtools.emit('flush')
}

4. errorHandler

全局错误捕获

原理:执行某些try catch或reject的时候,会触发globalHandlerError函数,该函数会尝试调用config.errorHandler

function globalHandleError(err, vm, info) {
  if (config.errorHandler) {
    try {
      return config.errorHandler.call(null, err, vm, info) // 如果用户有配置全局错误捕获,就尝试执行用户的
    } catch (e: any) {
      if (e !== err) {
        logError(e, null, 'config.errorHandler') // 执行vue自己的错误日志函数
      }
    }
  }
  logError(err, vm, info) // 执行vue自己的错误日志函数
}

5. warnHandler

全局警告捕获

原理:执行某些try catch或reject的时候,会调用warn函数,该函数会调用config.warnHandler

warn = (msg, vm = currentInstance) => {
  const trace = vm ? generateComponentTrace(vm) : ''
  if (config.warnHandler) {
    config.warnHandler.call(null, msg, vm, trace) // 调用全局配置中的warnHandler
  } else if (hasConsole && !config.silent) {
    console.error(`[Vue warn]: ${msg}${trace}`)
  }
}

6. ignoredElements -- 忽略Vue之外的自定义元素

忽略Vue之外的自定义元素

例如使用Vue组件的同时,如果在app上插入了web components api创建的自定义web组件,则该web组件会被识别成vue组件,但由于没有继承Vue所以会报错.

web components api可参考Vue2架构(3)扩展中的描述 或自行百度

原理:createElm渲染真实dom时,会调用isUnknownElement方法,通过config.ignoredElements判断vnode是否为未知元素,如果是未知元素就采用类似静态节点的处理方式

function isUnknownElement(vnode, inVPre) {
  return (
    !inVPre &&
    !vnode.ns &&
    !(
      config.ignoredElements.length && // 比对tag和config配置中的忽略元素列表
      config.ignoredElements.some(ignore => {
        return isRegExp(ignore)
          ? ignore.test(vnode.tag)
          : ignore === vnode.tag
      })
    ) &&
    config.isUnknownElement(vnode.tag)
  )
}

7. keyCodes

给v-on添加键位别名

原理:events中预设了一些keyCodes,执行相关事件的时候会查找vm上有没有对应的事件回调,调用_k方法的时候会尝试从config.keyCodes中取值

events中的预设

// KeyboardEvent.keyCode aliases
const keyCodes: { [key: string]: number | Array<number> } = {
  esc: 27,
  tab: 9,
  enter: 13,
  space: 32,
  up: 38,
  left: 37,
  right: 39,
  down: 40,
  delete: [8, 46]
}

checkKeyCodes --- Vue.prototype._k

/**
 * Runtime helper for checking keyCodes from config.
 * exposed as Vue.prototype._k
 * passing in eventKeyName as last argument separately for backwards compat
 */
export function checkKeyCodes(
  eventKeyCode: number,
  key: string,
  builtInKeyCode?: number | Array<number>,
  eventKeyName?: string,
  builtInKeyName?: string | Array<string>
): boolean | null | undefined {
  const mappedKeyCode = config.keyCodes[key] || builtInKeyCode // 优先取config中的keyCodes
  // ……
}

8. performance

允许浏览器开发工具性能追踪

原理:初始化前、初始化后、更新前、更新后、编译前、编译后,分别调用mark(startTag | endTag),mark方法核心是调用pref,pref 相关api可自行百度

初始化时

export function initMixin(Vue: typeof Component) {
  Vue.prototype._init = function (options?: Record<string, any>) {
    // ……
    if (__DEV__ && config.performance && mark) { // 如果允许性能追踪
      startTag = `vue-perf-start:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
      mark(startTag) // 开始追踪
    }
    // ……
    callHook(vm, 'created') // 初始化完成钩子
    if (__DEV__ && config.performance && mark) { // 如果允许性能追踪
      vm._name = formatComponentName(vm, false)
      mark(endTag) // 结束追踪
      measure(`vue ${vm._name} init`, startTag, endTag) // 分析性能
    }
    // ……
  } // ……
}

更新时

// mountComponent中
callHook(vm, 'beforeMount') // 开始更新钩子
let updateComponent
if (__DEV__ && 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() // 更新vnode
    mark(endTag) // 结束vnode更新的性能追踪
    measure(`vue ${name} render`, startTag, endTag) // 分析能能

    mark(startTag) // 开始渲染性能追踪
    vm._update(vnode, hydrating) // 渲染完
    mark(endTag) // 结束渲染性能追踪
    measure(`vue ${name} patch`, startTag, endTag) // 性能分析
  }
} else {
  updateComponent = () => {
    vm._update(vm._render(), hydrating)
  }
}

编译时

// compile时,$mount方法中
if (template) {
  /* istanbul ignore if */
  if (__DEV__ && config.performance && mark) {
    mark('compile') // 开始追踪
  }
  // 模板编译 把template拆字符串转为ast语法树,再拼字符串生成render函数
  const { render, staticRenderFns } = compileToFunctions(
    template,
    {
      outputSourceRange: __DEV__,
      shouldDecodeNewlines,
      shouldDecodeNewlinesForHref,
      delimiters: options.delimiters,
      comments: options.comments
    },
    this
  )
  options.render = render
  options.staticRenderFns = staticRenderFns

  /* istanbul ignore if */
  if (__DEV__ && config.performance && mark) {
    mark('compile end') // 结束追踪
    measure(`vue ${this._name} compile`, 'compile', 'compile end') // 性能分析
  }
}

9. productionTip

阻止启动时生成生产提示

// productionTip为true时才打印
if (
  __DEV__ &&
  process.env.NODE_ENV !== 'test' &&
  config.productionTip !== false &&
  typeof console !== 'undefined'
) {
  console[console.info ? 'info' : 'log'](
    `You are running Vue in development mode.\n` +
      `Make sure to turn on production mode when deploying for production.\n` +
      `See more tips at https://vuejs.org/guide/deployment.html`
  )
}

三、全局API原理详解

1. Vue.extend

Vue构造器,把传入的选项转换成一个继承了Vue的构造函数,是组件实现的核心原理之一

原理:传入继承选项,返回一个继承了Vue的构造函数Sub,并将传入的选项通过mergeOptions合并到Sub.options上

Vue.extend = function (extendOptions: any): typeof Component {
  extendOptions = extendOptions || {} // 初始化合并选项
  const Super = this // 父级
  const SuperId = Super.cid // 父级组件id 
  const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {}) // 如果有_Ctor说明是组件
  if (cachedCtors[SuperId]) {
    return cachedCtors[SuperId] // 同一个组件直接返回
  }
  const name =
    getComponentName(extendOptions) || getComponentName(Super.options)
  if (__DEV__ && name) {
    validateComponentName(name)
  }
  // 创建一个子函数(子组件)
  const Sub = function VueComponent(this: any, options: any) {
    this._init(options) // 子函数继承Vue后也有了自己的init方法,开始初始化子组件
  } as unknown as typeof Component
  Sub.prototype = Object.create(Super.prototype) // 子组件继承父组件
  Sub.prototype.constructor = Sub // 修复子组件的constructor,否则子组件被后代组件继承的时候,继承的是Vue
  Sub.cid = cid++ // 子组件唯一标识
  Sub.options = mergeOptions(Super.options, extendOptions) // 合并父组件和子组件选项
  Sub['super'] = Super // super就是父组件,可通过super找到父级,类似$parent的作用
  if (Sub.options.props) {
    initProps(Sub) // 初始化props,这里不会再对props进行劫持
  }
  if (Sub.options.computed) {
    initComputed(Sub) // 初始化computed
  }
  Sub.extend = Super.extend // 传递extend方法,使其可被后代组件调用
  Sub.mixin = Super.mixin // 传递mixin,使其可被后代组件调用
  Sub.use = Super.use // 传递use,同上
  // 初始化生命周期策略
  ASSET_TYPES.forEach(function (type) {
    Sub[type] = Super[type] // 继承父组件的合并策略
  })
  // 把选项中的name进行拼接,子组件构造函数名变成了vue-component-xxx
  if (name) {
    Sub.options.components[name] = Sub
  }
  Sub.superOptions = Super.options // 记录父组件选项
  Sub.extendOptions = extendOptions // 记录自己的选项
  Sub.sealedOptions = extend({}, Sub.options) // 克隆一个
  cachedCtors[SuperId] = Sub // 缓存构造函数,keep-alive中会用
  return Sub
}

2. Vue.nextTick

下次DOM更新循环之后的延迟回调,在dom更新后尽早的拿到更新后的结果,基于js事件机制实现;

说明:

在某一个同步任务队列中,可能存在很多次赋值操作,例如this.name=xxx this.age=xxx...

每一次赋值操作都会触发set,set触发dep.notiyf,notify遍历watcher队列执行update,update通过queueWatcher执行run,run执行watcher收集的getter,

如果是渲染watcher则更新视图,那么每修改一次数据,就会触发一次视图更新,非常浪费性能.所以需要进行批处理,将视图更新放在同步任务队列之后去执行,

这样既避免了重复渲染,也确保了同步任务队列的每一个赋值操作触发的watcher都会被收集;

渲染watcher中的nextTick:

通过queueWatcher将watcher维护成一个队列, flushing状态判断和has去重后,将批处理函数传递给nextTick,

批处理函数flushSchedulerQueue中关键操作:queue队列排序\遍历queue,执行watcher.run

原理:将传入的回调函数维护到待执行队列callbacks中,通过pending状态判断执行timerFn函数,timerFn函数以向下兼容方式,依次尝试promise>MutationObserver>setImmediate>setTimeOut(原因:想要在上一个同步任务队列执行完后,尽快的执行传入的回调函数,微任务是最佳选择,但某些环境可能不支持微任务,最终的妥协就是开启一个宏任务,V3没有兼容直接用的promise),并将flushCallbacks作为回调函数传入timerFn,在flushCallbacks函数中遍历callbacks队列并依次执行,随后修改pending状态,清空callbacks

export let isUsingMicroTask = false // 是否正在使用微任务
// 假如用户连续调用1000次nextTick,如果不做批处理的话,每次都会开启一个异步任务,这样明显不合理;
// 假如nextTick后又改了状态,如果未做批处理,那么当前nextTick拿到的是上一个任务队列的结果,并不是最新的;
// 使用批处理后,当前任务队列无论多少次nextTick都会合并成一个异步任务队列执行,并且确保nextTick是在所有同步任务执行完毕之后再执行的。
const callbacks: Array<Function> = [] // 回调函数队列,会把其放入数组中,在本次任务队列中,一次性全部执行。
let pending = false // 执行状态

// 遍历执行nextTick接收的回调函数
function flushCallbacks() {
  pending = false // 重置执行状态
  const copies = callbacks.slice(0) // 依次取出回调函数
  callbacks.length = 0 // 重置回调函数列表
  for (let i = 0; i < copies.length; i++) {
    copies[i]() // 执行回调函数
  }
}

let timerFunc // 最终执行的函数
if (typeof Promise !== 'undefined' && isNative(Promise)) { // 如果当前环境支持promise
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    // IOS中的微任务队列可能不会被主动清空,也就是说可能不会执行上面的p.then
    if (isIOS) setTimeout(noop) // 需要开启一个空的宏任务确保微任务被清空
  }
  isUsingMicroTask = true // 修复状态
} else if (
  !isIE &&
  typeof MutationObserver !== 'undefined' &&
  (isNative(MutationObserver) ||
    MutationObserver.toString() === '[object MutationObserverConstructor]')
) {
  // 如果promise不支持,就尝试用用MutatiuonObserver,它的回调函数是微任务
  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)
  }
  isUsingMicroTask = true // 修复状态
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  // 用setImmediate,这个是IE上的,比setTimeout性能稍好
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  // 最终如果都不支持 就用定时器开启宏任务,这是最后的妥协
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

export function nextTick(cb?: (...args: any[]) => any, ctx?: object) {
  let _resolve
  // 放入回调队列中
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx) // 尝试call,如果失败说明可能传的不是函数
      } catch (e: any) {
        handleError(e, ctx, 'nextTick') // 抛出错误
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true // 修改执行状态,确保本轮任务队列执行完之前,只执行一次timerFunc
    timerFunc() // 执行timerFunc
  }
  // 如果没有传回调函数,就返回一个promise
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve // 赋值一个成功回调
    })
  }
}

3. Vue.set

添加响应式数据

data需要预先定义,未定义的数据默认是不会通过observe进行观测的,该方法将一个没有预定义的数据转变成响应式数据

原理:走了一遍defineReactive的set大致相同的逻辑,同时递归调用了defineReactive,如果是数组还调用了observe,默认再触发一次dep.notify通知视图更新

initGlobalApi

import { set, del } from '../observer/index'
import {
  warn,
  extend,
  nextTick,
  mergeOptions,
  defineReactive
} from '../util/index'
export function initGlobalAPI(Vue: GlobalAPI) {
  // ……
  Vue.util = {
    warn,
    extend,
    mergeOptions,
    defineReactive
  }
  Vue.set = set // 直接用的observer中的set方法
  Vue.delete = del // 直接用的observer中的del方法
  Vue.nextTick = nextTick 
  // ……
}

/src/core/observer/index.ts

export function set(
  target: any[] | Record<string, any>,
  key: any,
  val: any
): any {
  // 如果是开发环境 且目标未定义 或者 目标是原始值,就不能被劫持,只有对象才能被劫持
  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__ // ob是Observer的实例对象
  // 如果是数组,且数组索引有效
  if (isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key) // 删除无效的索引对应的数据
    target.splice(key, 1, val) // 挨个取出进行劫持
    // 模拟ssr时,数组方法不会被劫持
    if (ob && !ob.shallow && ob.mock) {
      observe(val, false, true) // 递归劫持,observer中会调defineReactive > get
    }
    return val
  }
  // 遍历对象中的每一项 排除原型属性
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  // isVue表示是vue的实例,ob表示已经被观测过,这两种都不能劫持
  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
  }
  // 如果没有ob属性,说明没有被劫持过
  if (!ob) {
    target[key] = val
    return val
  }
  // 调用defineReactive进行劫持
  defineReactive(ob.value, key, val, undefined, ob.shallow, ob.mock)
  if (__DEV__) { // dep通知watcher更新视图
    // notify接收的对象数据最终会传递给renderTracked生命周期钩子
    // renderTracked是v3开发环境下才有的,--- 渲染追踪钩子函数
    ob.dep.notify({
      type: TriggerOpTypes.ADD,
      target: target,
      key,
      newValue: val,
      oldValue: undefined
    })
  } else { // dep通知watcher更新视图
    ob.dep.notify()
  }
  return val
}

4. Vue.delete

删除响应式数据中的某个属性,会彻底解绑其所关联的watcher和dep等

原理:一堆条件判断后(是否存在 是否只读 是否属于该对象 是否还有ob...),移除属性,同时ob.dep.notify通知视图更新

export function initGlobalAPI(Vue: GlobalAPI) {
  Vue.delete = del
  // ……
}

// core/observer/index.js
export function del(target: any[] | object, key: any) {
  // 开发环境 目标未定义 或者 是原始值,就抛出错误
  if (__DEV__ && (isUndef(target) || isPrimitive(target))) {
    warn(
      `Cannot delete reactive property on undefined, null, or primitive value: ${target}`
    )
  }
  // 如果是数组,且数组索引有效
  if (isArray(target) && isValidArrayIndex(key)) {
    target.splice(key, 1) // 删除key对应的数据
    return
  }
  const ob = (target as any).__ob__ // Observer实例对象
  // isVue表示是vue的实例,ob表示已经被观测过,这两种都不能直接删除,只需要设置为null
  if ((target as any)._isVue || (ob && ob.vmCount)) {
    __DEV__ &&
      warn(
        'Avoid deleting properties on a Vue instance or its root $data ' +
          '- just set it to null.'
      )
    return
  }
  // 如果目标对象只读  也无法删除
  if (isReadonly(target)) {
    __DEV__ &&
      warn(`Delete operation on key "${key}" failed: target is readonly.`)
    return
  }
  if (!hasOwn(target, key)) { // 如果目标对象上不存在key  直接return
    return
  }
  delete target[key] // 删除目标对象上的属性
  if (!ob) { // 如果ob不存在,则该属性是静态属性,没有被劫持过
    return
  }
  // 如果ob存在 就会走这里,dep通知watcheer更新视图
  if (__DEV__) {
    // notify接收的对象数据最终会传递给renderTriggered生命周期钩子
    // renderTriggered是v3开发环境下才有的,--- 渲染已触发钩子函数
    ob.dep.notify({
      type: TriggerOpTypes.DELETE, // 标记本次操作时删除属性
      target: target, 
      key
    })
  } else {
    ob.dep.notify()
  }
}

5. Vue.directive

注册或获取全局指令

说明:v-xxx在parse解析时,会被收录进ast的diractive属性中,创建vnode时会取出diractive和选项中的同名directives进行比对,并且调用其钩子函数,将关键信息传递过去;

原理:通过判断vnode的渲染状态,执行一些钩子函数,在这些钩子函数中,可获取指令绑定的参数以及vnode信息,用户可通过这些参数,做一些公共逻辑处理。

全局方法注入

import { initAssetRegisters } from './assets'
export function initGlobalAPI(Vue: GlobalAPI) {
  // ……
  initAssetRegisters(Vue) // 创建资源注册方法
}

遍历ASSET_TYPES策略添加全局资源注册方法

// shared/constants.js中
export const ASSET_TYPES = ['component', 'directive', 'filter'] as const

import { ASSET_TYPES } from 'shared/constants'
export function initAssetRegisters(Vue: GlobalAPI) { // 创建资源注册方法
  ASSET_TYPES.forEach(type => {
    Vue[type] = function (
      id: string,
      definition?: Function | Object
    ): Function | Object | void {
      if (!definition) {
        // 如果没传definition就通过id取值,取出带s的同名属性
        // 例如 使用directives就能获取到自定义的指令集合
        return this.options[type + 's'][id] 
      } else {
        if (__DEV__ && type === 'component') {
          validateComponentName(id) // 检测组件名称是否合规
        }
        if (type === 'component' && isPlainObject(definition)) {
          definition.name = definition.name || id 
          definition = this.options._base.extend(definition) // 注册组件 _base就是前面说的Vue
        }
        if (type === 'directive' && isFunction(definition)) {
          definition = { bind: definition, update: definition } // 注册指令方法
        }
        this.options[type + 's'][id] = definition // 带s的就是属性,放在options上
        return definition
      }
    }
  })
}

vdom/modules/directives.js 核心就是这个_update方法

// …… 核心就是这个_update方法
function _update(oldVnode, vnode) {
  const isCreate = oldVnode === emptyNode // 旧节点是否是空节点
  const isDestroy = vnode === emptyNode // 新节点是否是空节点
  const oldDirs = normalizeDirectives( // 规范化指令,保证其def有值
    oldVnode.data.directives,
    oldVnode.context
  )
  const newDirs = normalizeDirectives(vnode.data.directives, vnode.context) // 同上
  const dirsWithInsert: any[] = [] // 收集的inserted回调 --- 订阅事件
  const dirsWithPostpatch: any[] = [] // 收集的componentUpdate回调 --- 订阅事件
  let key, oldDir, dir
  for (key in newDirs) { // 遍历新指令
    oldDir = oldDirs[key] // 通过同名key找到旧指令
    dir = newDirs[key] // 新指令
    if (!oldDir) { // 如果没有旧的 那一定是第一次绑定
      callHook(dir, 'bind', vnode, oldVnode) // 执行传入的bind函数,只会执行一次 --- 发布事件
      if (dir.def && dir.def.inserted) { // 如果传入了inserted,则将其收集起来
        dirsWithInsert.push(dir) // 订阅,等会发布执行
      }
    } else { // 如果旧的有值,说明是修改
      dir.oldValue = oldDir.value // 赋值
      dir.oldArg = oldDir.arg // 标记根
      callHook(dir, 'update', vnode, oldVnode) // 执行传入的update函数 --- 发布事件
      if (dir.def && dir.def.componentUpdated) { // 如果传入了该函数 就收集起来
        dirsWithPostpatch.push(dir) // 订阅,等会发布执行
      }
    }
  }
  if (dirsWithInsert.length) { 
    const callInsert = () => { // 遍历执行inserted函数
      for (let i = 0; i < dirsWithInsert.length; i++) {
        callHook(dirsWithInsert[i], 'inserted', vnode, oldVnode) // 执行函数
      }
    }
    if (isCreate) { // 旧节点是空的 说明是第一次绑定
      mergeVNodeHook(vnode, 'insert', callInsert) // 合并hook 并执行inserted函数
    } else {
      callInsert() // 直接执行
    }
  }
  if (dirsWithPostpatch.length) {
    mergeVNodeHook(vnode, 'postpatch', () => {
      for (let i = 0; i < dirsWithPostpatch.length; i++) { // 遍历执行componentUpdate函数
        callHook(dirsWithPostpatch[i], 'componentUpdated', vnode, oldVnode)
      }
    })
  }
  if (!isCreate) { // 如果旧节点为空 不是第一次绑定,已经绑定好了
    for (key in oldDirs) {
      if (!newDirs[key]) { // 已经绑定好了就触发unding
        callHook(oldDirs[key], 'unbind', oldVnode, oldVnode, isDestroy)
      }
    }
  }
}

6. Vue.filter

注册或获取全局过滤器

原理:

解析阶段:parse解析阶段的parseFilters函数中,会把|前后解析为表达式和过滤器,warpFilter函数拼接_f函数时,将表达式作为过滤器的参数传递进去,并将其作为返回值;

执行阶段:_f函数对应的resolveFilter函数中,通过resolveAsset函数找到options上同名的过滤函数,执行该函数并传递参数

function wrapFilter(exp: string, filter: string): string {
  const i = filter.indexOf('(')
  if (i < 0) {
    // _f: resolveFilter
    return `_f("${filter}")(${exp})` // 拼接render函数,将表达式作为参数传递给过滤函数
  } else {
    const name = filter.slice(0, i)
    const args = filter.slice(i + 1) // 串联多个过滤函数
    return `_f("${name}")(${exp}${args !== ')' ? ',' + args : args}`
  }
}

执行阶段

// core/index.js中
import { renderMixin } from './render'
function Vue(options) {
  if (__DEV__ && !(this instanceof Vue)) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}
renderMixin(Vue)

// render.js中
import { installRenderHelpers } from './render-helpers/index'
export function renderMixin(Vue: typeof Component) {
  installRenderHelpers(Vue.prototype)
  // ……
}

// render-helpers/index.js中
import { resolveFilter } from './resolve-filter'
export function installRenderHelpers(target: any) {
  target._f = resolveFilter
  // ……
}

// resolveFilter
export function resolveFilter(id: string): Function {
  return resolveAsset(this.$options, 'filters', id, true) || identity
}

7. Vue.component

注册组件 及查找

原理:调用_base.extend方法,_base是根 也就是Vue

代码参考前面的directive注解 和 extend注解

8. Vue.use

注入插件

原理:高阶函数,拿到传入的插件和插件配置参数,显示去重判断,然后判断插件是否有install方法,将Vue传给插件,插件中接收Vue并在其原型链上扩展相应功能.

export function initUse(Vue: GlobalAPI) {
  Vue.use = function (plugin: Function | any) {
    // 记录注入了哪些插件
    const installedPlugins =
      this._installedPlugins || (this._installedPlugins = [])
    if (installedPlugins.indexOf(plugin) > -1) {
      return this
    }
    // 取出传进来的参数,并转成数组,从1开始截取,是因为第0个是插件本身
    const args = toArray(arguments, 1)
    args.unshift(this) // 参数前面插入this,这个this就是Vue实例,把vue作为插件的第一个参数传给插件
    if (isFunction(plugin.install)) { // 如果插件有install方法,且是一个函数
      plugin.install.apply(plugin, args) // 将参数传给插件,参数第1个是vue,后面的是use接收的选项
    } else if (isFunction(plugin)) { // 如果插件本身就是一个函数
      plugin.apply(null, args) // 直接执行这个函数,并传递参数
    }
    installedPlugins.push(plugin) // 维护进插件列表中
    return this
  }
}

9. Vue.mixin

选项混入

原理:利用mergerOptions往实例上合并数据,子组件会继承这个方法

export function initMixin(Vue: GlobalAPI) {
  Vue.mixin = function (mixin: Object) {
    this.options = mergeOptions(this.options, mixin)
    return this
  }
}
// extend中,详细注解参考extend
Vue.extend = function (extendOptions: any): typeof Component {
    const Sub = function VueComponent(this: any, options: any) {
        this._init(options)
    } as unknown as typeof Component
    // ……
    Sub.mixin = Super.mixin   // 子组件继承父组件发mixin方法
    // ……
    return Sub
}

10. Vue.compile

模板编译 --- 等于compileToFunctions

实现原理:

  1. 通过createCompilerCreator高阶函数传入baseCompile函数,
  2. createCompilerCreator中处理最终配置finalOptions后.调用baseCompile函数并返回执行结果,
  3. baseCompiler函数中通过parse方法创建ast语法树,再将ast传入generate函数中生成rende函数,
  4. 最终返回包含ast\render\staticRenderFns(静态渲染)的对象

tuntime-with-compiler.js中

import { compileToFunctions } from './compiler/index'
import Vue from './runtime/index'
Vue.compile = compileToFunctions

compiler/index.js中

// createCompilerCreator是一个高阶函数,里面还有很多不同配置不同环境的处理,最终会执行传入的这个函数
export const createCompiler = createCompilerCreator(function baseCompile(
  template: string,
  options: CompilerOptions
): CompiledResult {
  const ast = parse(template.trim(), options) // 生成ast语法树
  if (options.optimize !== false) { // 如果没有标记过
    optimize(ast, options)  // 各种标记,例如静态节点
  }
  const code = generate(ast, options) // 生成render函数
  return {
    ast,
    render: code.render, // 渲染函数
    staticRenderFns: code.staticRenderFns // 静态渲染函数
  }
})

11. Vue.observable

手动对某个对象进行观测

原理:直接调用observe方法,啥也没做,observe方法中先是一堆可不可以被观测的判断,然后通过new Observer对数据进行观测

Observer中…创建dep > 观测对象 > 观测数组 切片数组7个方法 …… 递归观测 defineReactive …… 这一块太熟悉了懒得整理了

import { observe } from 'core/observer/index'
export function initGlobalAPI(Vue: GlobalAPI) {
  // ……
  Vue.observable = <T>(obj: T): T => {
    observe(obj) // 调用observer进行观测
    return obj
  }
}

observer方法

export function observe(
  value: any,
  shallow?: boolean,
  ssrMockReactivity?: boolean
): Observer | void {
  // 如果存在__ob__属性,且该属性是Observer的实例,说明已经被观测过,直接返回该属性,否则会陷入回调地狱
  if (value && hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    return value.__ob__
  }
  // 这里就是一堆能不能被观测的判断
  if (
    shouldObserve && // 允许观测,这个参数用于后期手动设置
    (ssrMockReactivity || !isServerRendering()) && // 是服务端渲染 且ssrMockReactivity为true
    (isArray(value) || isPlainObject(value)) && // 是数组或者对象
    Object.isExtensible(value) && // 可以扩展 比如添加修改等
    !value.__v_skip /* ReactiveFlags.SKIP */ && // 跳过
    !isRef(value) && // 排除undefined或者null
    !(value instanceof VNode) // 排除vnode
  ) {
    // 通过Observer类观测,并返回一个实例,该实例上有__ob__属性指向实例自身
    return new Observer(value, shallow, ssrMockReactivity)
  }
}

Observer类

export class Observer {
  constructor(public value: any, public shallow = false, public mock = false) {
    this.dep = mock ? mockDep : new Dep() // 创建dep
    def(value, '__ob__', this) // defineProperty劫持一个__ob__属性,值是自身实例
    if (isArray(value)) {
      // 改写数组7个方法,切片重写的,新增时调用observer,修改时调用dep.notify
      ;(value as any).__proto__ = arrayMethods
      // ……这里我删掉了一些不重要的代码
      if (!shallow) { // 如果不是浅数组,其中可能包含对象
        this.observeArray(value) // 递归进行观测
      }
    } else {
      // 对象直接遍历,调用defineReactive进行观测就行了
      const keys = Object.keys(value)
      for (let i = 0; i < keys.length; i++) {
        const key = keys[i]
        defineReactive(value, key, NO_INITIAL_VALUE, undefined, shallow, mock)
      }
    }
  }
  // 递归观测数组
  observeArray(value: any[]) {
    for (let i = 0, l = value.length; i < l; i++) {
      observe(value[i], false, this.mock)
    }
  }
}

defineReactive方法 --- 劫持数据

export function defineReactive(
  obj: object,
  key: string,
  val?: any,
  customSetter?: Function | null,
  shallow?: boolean,
  mock?: boolean
) {
  const dep = new Dep() // 创建一个属性dep
  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return // 如果该属性不可配置(不能添加不能删除不能修改),停止劫持
  }
  const getter = property && property.get // 预定义get
  const setter = property && property.set // 预定义set
  // 如果不是浅数据,就递归继续观测,并拿到子属性的ob,childOb可能是数组,需要递归让dep和watcher相互关联
  let childOb = !shallow && observe(val, false, mock) 
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      const value = getter ? getter.call(obj) : val // 如果本身有get 就取get返回值
      if (Dep.target) { // target是watcher取值是pushTarget添加的 如果Dep上面有watcher
        if (__DEV__) {
          // 关联watcher的时候 执行v3生命周期的数据追踪钩子
          dep.depend({
            target: obj,
            type: TrackOpTypes.GET,
            key
          })
        } else {
          dep.depend() // 直接开始关联watcher
        }
        if (childOb) {
          childOb.dep.depend() // 关联子属性watcher
          if (isArray(value)) {
            dependArray(value) // 递归关联数组watcher
          }
        }
      }
      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 && observe(newVal, false, mock)
      if (__DEV__) {
        dep.notify({
          type: TriggerOpTypes.SET,
          target: obj,
          key,
          newValue: newVal,
          oldValue: value
        })
      } else {
        dep.notify()
      }
    }
  })
  return dep
}

12. Vue.version

版本号

没啥原理,就是构造函数上做个标记而已

四、数据选项原理详解

new Vue执行_init函数,会调用initState初始化数据

export function initState(vm: Component) {
  const opts = vm.$options // 取出options上的配置
  if (opts.props) initProps(vm, opts.props) // 初始化props
  initSetup(vm) // v3的 Composition API
  if (opts.methods) initMethods(vm, opts.methods) // 初始化methods
  if (opts.data) { 
    initData(vm) // 初始化data
  } else {
    const ob = observe((vm._data = {})) // 如果没有传data,则_data默认是空对象
    ob && ob.vmCount++ // 这个属性用于统计root 中 $data虚拟机的数量
  }
  if (opts.computed) initComputed(vm, opts.computed) // 初始化计算属性
  if (opts.watch && opts.watch !== nativeWatch) { // 火狐浏览器的Object.prototype上默认有一个watch
    initWatch(vm, opts.watch) // 初始化用户watcher
  }
}

1. data

自身数据

原理:initData > 取出options上的data > 调用observe > observe判断是否可以被观测后,return new Observe

function initData(vm: Component) {
  let data: any = vm.$options.data // 取出options中传入的data
  // 如果是函数就用函数返回值,否则默认是对象
  data = vm._data = isFunction(data) ? getData(data, vm) : data || {}
  if (!isPlainObject(data)) { // data如果不是对象
    data = {} // 改为对象
    __DEV__ && // 开发环境下报错
      warn(
        'data functions should return an object:\n' +
          'https://v2.vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
        vm
      )
  }
  // 把data代理到实例上
  const keys = Object.keys(data) // 取出key
  const props = vm.$options.props // 取出props 和key进行比对,如果同名就报错
  const methods = vm.$options.methods // 同上,props、methods和data中的属性名不能有冲突
  let i = keys.length
  while (i--) {
    const key = keys[i]
    if (__DEV__) {
      // 如果methods和data中的属性名冲突就报错,否则代理到this上时不知道应该访问哪个属性
      if (methods && hasOwn(methods, key)) {
        warn(`Method "${key}" has already been defined as a data property.`, vm)
      }
    }
    // props也是和methods一样的,不能重名
    if (props && hasOwn(props, key)) {
      __DEV__ &&
        warn(
          `The data property "${key}" is already declared as a prop. ` +
            `Use prop default value instead.`,
          vm
        )
    } else if (!isReserved(key)) { // 同时属性不能以$开头,因为$开头的会被识别成内部属性
      proxy(vm, `_data`, key) // 将key代理到_data,访问vm._data.xxx的时候,可用vm.xxx
    }
  }
  const ob = observe(data) // 观测数据,劫持
  ob && ob.vmCount++ // 统计虚拟机数量
}

2. props

接收的数据

原理:

  1. initProps > 通过$parent判断当前是否为根节点,如果是根节点则修改shouldObserve为false(是否允许观测)
  2. 取出options上面的propsData > 遍历propsData并调用validateProp进行属性校验 > 调用defineReactive进行数据劫持,重置shouldObserve

3. propsData

new 实例时传递和接收的数据,等同于props

原理:new 实例时传递的props,可在new的时候传递,等同于props

4. computed

计算属性

原理:

  1. initComputed > 取出options上的computed > 遍历并同步一份到vm._computedWatchers > new Watcher > dedineComputed
  2. defineComputed > 判断是否缓存过 > 劫持get判断:缓存过走createComputedGetter\否则createGetterInvoker > defineProperty将对key的访问指向劫持get
  3. createComputedGetter > 高阶函数,返回一个computedGetter函数 > 取出_computedWatchers中key对应的watcher,判断dirty > watcher.deped(watcher关联dep) > return watcher.value

5. methods

方法合集

原理:mergeoptions合并到$options, 初始化时遍历取出methods,并bind改变this指向vm

6. watch

用户watcher

原理:

  1. initWatch > 判断是否数组 是则遍历数组调用createWatcher,否则直接调用createWatcher
  2. createWatcher > 一堆判断后调用vm.$watch
  3. $watch > new Watcher > this.get() > 如果immdiate为true 立即pushTarget 取值watcher.value popTarget > 如果deep为true,则调用_traverse递归取值 > while循环 > 遇到取过值的会通过__ob__.dep.id标记

五、DOM选项原理详解

1. el

根节点挂载的dom元素

2. template

渲染模板,渲染优先级大于data,如果有则取它,没有则会取el对应的元素的innerHTML

3. render

虚拟dom转换函数,将传入的数据转换成vnode

4. renderError

render生效前调用的,可在做类似暂无数据提示的组件

六、生命周期选项14个

原理:

  1. mergeOptions执行starts中的lifecycle_hooks策略,将对应的hook维护到options上,
  2. 通过callhook取出对应的handler,调用该handler,就是一个发布订阅而已,下面只列举部分钩子,其他都一样
callHook(vm, 'beforeCreate', undefined, false) // 数据初始化前执行beforeCreate
initInjections(vm) // 初始化injections
initState(vm) // 初始话数据 > props>methods>data>computed>watch
initProvide(vm) // 初始化provide
callHook(vm, 'created') // 数据初始化完成,执行created

callhook函数

export function callHook(
  vm: Component,
  hook: string,
  args?: any[],
  setContext = true
) {
  // 禁用深度收集
  pushTarget() // dep收集watcher watcher进栈
  const prev = currentInstance // 当前实例
  setContext && setCurrentInstance(vm)
  const handlers = vm.$options[hook] // 从options上取出对应hook
  const info = `${hook} hook`
  if (handlers) {
    for (let i = 0, j = handlers.length; i < j; i++) {
      // 带错误处理的调用,尝试执行函数,执行失败会抛出错误信息
      invokeWithErrorHandling(handlers[i], vm, args || null, vm, info) 
    }
  }
  if (vm._hasHookEvent) {
    vm.$emit('hook:' + hook)
  }
  setContext && setCurrentInstance(prev)
  popTarget() // watcher出栈
}

七、资源选项原理详解

1. directives

自定义指令

2. filters

过滤器

3. components

组件注册及查找

八、组合选项原理详解

1. parent

认贼作父,强制和其它组件 建立父子关系

2. mixins

混入,其实就是调用mergeOptions把传进来的options合并到vm上

3. extends

允许声明扩展另一个组件,原理是通过mergeOptions合并到parent中,类似mixin

4. provide / inject

可以理解为上下文,可用于传递数据

原理:

  • provide注入依赖:provide在mergePorions时,会通过mergeData添加一个form属性;initProvide时,遍历$options.provide > Object.defineProperty + Object.getOwnPropertyDescriptor 劫持_provide,getOwnPropertyDescriptor中有属性的所有描述(可读可写可枚举可扩展 get set等),原来就有get set就继续使用
  • inject引用依赖:initInjections时,遍历inject,不断通过form往上找父节点上的同名属性,找到后先取消观测,再用defineReactive观测一下,就近原则,先找到谁就是谁,所以并不安全,存在被覆盖可能性。

九、其它选项原理详解

1. name

组件名称,没有实质作用,只是便于调试,更友好的警告信息以及跟语义化的组件构造函数名

2. delimiters

自定义模板语法中的文本取值,例如{{}} 改写为[[]],原理是再createCompileToFunction时,改写正则匹配规则

3.functional

标记是否为函数组件

4.model

v-model的自定义

5.inheritAttrs

传递给组件的属性如果没有用props接收,会被添加到元素上面,设为false可隐藏属性

6.comments

保留代码注释

十、实例属性原理详解

1. vm.$data

等同于data

2. vm.$props

等同于props

3. vm.$el

实例真实Dom根元素

4. vm.$options

当前实例的全部选项

5. vm.$parent

父节点

6. vm.$root

根节点

7. vm.$children

子节点

8. vm.$slots

访问被具名插槽分发的内容

9. vm.$scopedSlots

访问作用域插槽

10. vm.$refs

用ref注册的dom对象

11. vm.$isServer

是否运行于服务器

12. vm.$attrs

父组件传递过来,但没有被子组件props接收的的属性,会被维护到子组件的$attrs

13. vm.$listeners

父组件中不含.native的事件监听器

十一、实例数据方法原理详解

1. vm.$on

监听实例事件 ,eventMixin注入 > 将传入的函数添加到_events中

2. vm.$once

派发一个只执行一次的实例事件,eventMixin注入 > 将一个执行fn的函数传入_events,该函数中执fn时先调用$off移除事件

3. vm.$off

关闭监听事件,eventMixin注入 > 移除_events中对应的函数

4. vm.$emit

触发监听事件,eventMixin注入 > 取出_event中对应的函数并执行

十二、实例生命周期方法原理详解

1. vm.$mount

挂载实例,调用mountComponent方法,创建渲染watcher

2. vm.$forceUpdate

强制渲染一次,很简单,调用vm._watcher.update()

3. vm.$nextTick

调用nextTick方法

4. vm.$destroy

销毁实例,两个callHook,将很多属性改为null

十三、原生指令原理详解

1. v-text

渲染指定文本

2. v-html

渲染指定html

3. v-show

显示隐藏

4. v-if

是否渲染

5. v-else

是否渲染

6. v-else-if

是否渲染

7. v-for

循环

8. v-on

绑定事件$on

9. v-bind

绑定属性

10. v-model

双向绑定

input --- :value+@input

select --- :value+@change

checkbox --- checked+@change

11. v-slot

具名插槽

12. v-pre

跳过编译,标记静态节点可提高性能

13. v-cloak

添加到实例属性上直到关联实例结束编译,例如{{ name }} name可能时异步取值,在防止取到值之前的错误渲染,添加了该指令后不会被渲染为‘{{ name }}’,同时结合css将其隐藏,[v-clock]:{ display:none }

14. v-once

只渲染一次,被标记为静态节点,之后更新操作都不会被重新渲染

十四、特殊属性原理详解

1. key

标记节点,vnode进行节点比对时大有用处

2. ref

获取真实dom,将该节点的$el赋值给ref所指的变量名,并同步给vm

3. is

用于动态组件,指向组件的key

4. slot

废弃 被v-slot替代

5. slot-scope

废弃 被v-slot替代

6. scope

废弃 被v-slot替代

十五、内置组件原理详解

1. component

渲染动态组件 通过is查找对应组件

2. transition

动画组件

3. transition-group

动画组组件

4. keep-alive

视图缓存

3个属性:include-对哪些缓存 exclude-对哪些不缓存 max-最大缓存数量

2个生命周期钩子:activated-缓存组件生效时,deactivated-缓存组件失效时,createLifcycle进行收集,cacheVnode时触发activated,remove时触发deactivated

缓存策略:LRU最近最久未使用淘汰法

5. slot

插槽