阅读框架源码方法
Vue 源码阅读
Vue 源码打包入口文件
首先前往Vue2的 github 下载源码项目,安装依赖。
然后从代码的目录结构入手,先分析每一个文件夹的功能以及包含的内容:
bechmarks:性能测试dist:最终打包出的结果examples:官方例子demoflow:类型检测,Vue2当初没采用TypeScript,而是采用这个微软出品的类型检测,不过目前没人使用了packages:一些写好的包(Vue 源码中包含weex)scripts:所有打包的脚本文件src:源代码目录一般情况下我们的代码都是放在
src文件夹下,因此它是重点文件夹。打开src文件夹,分析其下的每个文件夹的功用:compiler:专门用作模板编译platformserver:服务端渲染相关sfc:解析单文件组件,如样式style、模板template、脚本script等(要结合vue-loader使用)shared:模板之间的共享属性和方法
找到
package.json文件,查看命令找到打包的入口,在srcipts文件夹下的config.js文件。该文件最后的代码如下:
if (process.env.TARGET) {
module.exports = genConfig(process.env.TARGET)
} else {
exports.getBuild = getConfig
exports.getAllBuilds = () => Object.keys(build).map(genConfig)
}从代码可以看成它做了一个判断,是否有打包目标 TARGET ,如果有直接使用,没有才通过遍历对象寻找。
查看运行的命令,有两种:运行时如果无法解析 new Vue 传入的 template ,则会走 web-runtime ;否则会执行 web-full (web-runtime + 模板解析)。
它调用了 genConfig 方法,该方法会去 builds 对象中找到对象打包目标对象中找到入口文件,部分源码如下:
const builds = {
// runtime-only build (Browser)
'runtime-dev': {
entry: resolve('web/entry-runtime.ts'),
dest: resolve('dist/vue.runtime.js'),
format: 'umd',
env: 'development',
banner,
},
// Runtime+compiler development build (Browser)
'full-dev': {
entry: resolve('web/entry-runtime-with-compiler.ts'),
dest: resolve('dist/vue.js'),
format: 'umd',
env: 'development',
alias: { he: './entity-decoder' },
banner,
},
}其中 entry 是打包的文件文件路径,在 web 文件夹下的相应文件。但是 scripts 以及外部都没有这个 web 文件夹,因此还需要继续寻找。
调用了 resolve 函数,把路径传了过去,源码如下:
const aliases = require('./alias')
const resolve = (p) => {
const base = p.split('/')[0]
if (aliases[base]) {
return path.resolve(aliases[base], p.slice(base.length + 1))
} else {
return path.resolve(__dirname, '../', p)
}
}它拿到路径后和 aliases 做了拼接处理,因此根据路径查看 aliases.js 的代码,如下:
module.exports = {
vue: resolve('src/platforms/web/entry-runtime-with-compiler'),
compiler: resolve('src/compiler'),
core: resolve('src/core'),
shared: resolve('src/shared'),
web: resolve('src/platforms/web'),
server: resolve('packages/server-renderer/src'),
sfc: resolve('packages/compiler-sfc/src'),
}可以看到其 web 实际上是 src/platforms/web 路径,现在能够通过拼接,找到对应的打包文件的入口路径了。
查看源码,发现它们都使用了 runtime :
import Vue from './runtime/index'而 entry-runtime-with-compiler 的区别是多实现了一个 compile API,把 template 转化成 render 函数。
下面先来看看这个 compile API 做了什么处理。源代码如下所示:
// 函数劫持,将原来的mount函数获取到后重写mount函数
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
// 获取el元素
el = el && query(el)
/* istanbul ignore if */
// 挂载的流程是用一个新生成的dom替换掉老的dom元素
if (el === document.body || el === document.documentElement) {
__DEV__ &&
warn(
`Do not mount vue to <html> or <body> - mount to normal elements instead.`
)
return this
}
const options = this.$options
// resolve template/el and convert to render function
if (!options.render) {
// 如果有render函数则直接使用用户的render函数
let template = options.template
if (template) {
// 如果没有render,查看是否有模板
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
// 考虑到 {template: '#template'} 的情况
template = idToTemplate(template)
/* istanbul ignore if */
if (__DEV__ && !template) {
warn(
`Template element not found or is empty: ${options.template}`,
this
)
}
}
} else if (template.nodeType) {
// 如果给的模板是一个dom元素,则拿到模板中的内容
template = template.innerHTML
} else {
if (__DEV__) {
warn('invalid template option:' + template, this)
}
return this
}
} else if (el) {
// @ts-expect-error
// 如果没有模板,则使用el对应的template
template = getOuterHTML(el)
}
if (template) {
/* istanbul ignore if */
if (__DEV__ && config.performance && mark) {
mark('compile')
}
// 直接将模板变成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')
}
}
}
// 调用了挂载
return mount.call(this, el, hydrating)
}查看其源码不难发现,他只做了一件事情,就是重写了 $mount 函数,将 template 变成 render 函数。这就是 entry-runtime-width-compiler 比 entry-runtime 命令多出来的区别。
接着我们再往上面看,看一下双方都有的 runtime/index 文件内的方法做了什么处理。它是运行时,所谓运行时,就是提供一些DOM操作的 API ,比如属性操作、元素操作等,还有一些指令和组件。
其中,部分源码如下所示:
// install platform runtime directives & components
extend(Vue.options.directives, platformDirectives) // 添加平台对应的指令和组件
extend(Vue.options.components, platformComponents)
// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop // 渲染时会调用的方法,更新时会调用的方法。noop是一个空函数,如果在服务端渲染,则不需要调用方法,因此给一个空函数即可
// public mount method
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
// 组件的挂载
return mountComponent(this, el, hydrating)
}主要是为原型上安装一个 __patch__ 的方法,判断当前的渲染环境是否在服务器端,如果在服务器端,则执行空函数 noop 。
在原型上挂载一个 $mount 函数,最终 return 导出一个组件的挂载的方法。
但是这还不是入口文件,往上滑发现它从 core/index 文件导入方法,因此前往查看它做了什么处理。
打开 core/index.js 文件,发现它依旧不是入口文件,做了一个导入导出。该文件增加了两个标识:是否是服务端渲染、服务端渲染的上下文,并且初始化Vue的全局 API 。部分源码如下:
initGlobalAPI(Vue)
Object.defineProperty(Vue.prototype, '$isServer', {
get: isServerRendering,
})
Object.defineProperty(Vue.prototype, '$ssrContext', {
get() {
/* istanbul ignore next */
return this.$vnode && this.$vnode.ssrContext
},
})根据文件路径继续网上找,查看 core/instance/index 文件,发现他没有外部导入Vue方法,而是创建了一个Vue的构造函数。其源码如下所示:
function Vue(options) {
if (__DEV__ && !(this instanceof Vue)) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)可以看到其本质上在函数中调用了 _init() 初始化的方法,然后调用了几个函数,在Vue的原型 prototype 上挂载方法。作用分别是:
initMixin:挂载初始化方法_initstateMixin:挂载$set、$delete、$watch方法eventsMixin:挂载$on、$off、$emit方法lifecycleMixin:挂载_update、$forceUpdate强制刷新、$destory销毁方法、renderMixin:挂载$nextTick、_render方法
总结出一句就是在扩展原型上的方法。
一图流如下所示:

里面每一个具体怎么实现?
- 如果找了核心流程,可以单独打开源码查看
- 如果不清楚流程,可以写一些测试用例和案例来调试。通过为编译命令行添加
--sourcemap让编译后的源文件可以debuggr调试
Vue 全局 API 分析
看看它的全局 API 是怎么实现的。找到 src/core/global-api/index.js 文件,里面有一个 initClobalAPI() 函数。
首先它代理了 config 方法,用于配置信息,源码如下:
const configDef: Record<string, any> = {}
configDef.get = () => config
if (__DEV__) {
configDef.set = () => {
warn('Do not replace the Vue.config object, set individual fields instead.')
}
}
Object.defineProperty(Vue, 'config', configDef)然后配置了Vue中的工具方法,如合并 extend 、合并策略 mergeOptions 、定义响应式 defineReactive 等。还有一些常用的方法,如 set 、delete 、nextTick 等。源码如下:
Vue.util = {
warn,
extend,
mergeOptions,
defineReactive,
}
Vue.set = set
Vue.delete = del
Vue.nextTick = nextTick其中,关于 nextTick 可以查看我的博客文档 nextTick 。
再往下是让一个对象变成响应式的方法,源码如下:
Vue.observable = <T>(obj: T): T => {
observe(obj)
return obj
}最后在Vue方法上通过 Object.create 设置空对象 options ,并通过循环赋值空对象,并让 _base 一直指向Vue构造函数。
使用 extend 方法挂载 keep-alive 方法。
再从其他页面导入对应的方法注册,分别是:
- Vue.use
- Vue.mixin
- Vue.extend
- Vue.component、Vue.directive、Vue.filter
源码如下:
Vue.options = Object.create(null)
ASSET_TYPES.forEach((type) => {
Vue.options[type + 's'] = Object.create(null)
})
// this is used to identify the "base" constructor to extend all plain-object
// components with in Weex's multi-instance scenarios.
Vue.options._base = Vue
extend(Vue.options.components, builtInComponents)
initUse(Vue)
initMixin(Vue)
initExtend(Vue)
initAssetRegisters(Vue)Vue 响应式数组
响应式数据的理解
initData函数中调用observe方法回调,对数据作监听。调用defineReactive方法劫持代理(内部重写了所有属性)。递归增加对象中的对象增加geter和setter。如果层级过深,则需要考虑优化(递归代理消耗性能)。如果数据不是响应式的就不要放在
data中了。在取值的时候要避免多次取值,如果有些对象放到data内但不是响应式的,可以考虑Object.freeze()来冻结对象。检测数组变化
defineProperty ,因为修改索引的情况不多(如果直接采用 defineProperty 会浪费大量性能)。采用重写数组的变异方法来实现(函数劫持)。if (isArray(value)) {
// 如果是数组,则判断它有没有__proto__对应方法,如果有说明已经代理过了,没有则没代理过,通过拷贝为其赋值
if (!mock) {
if (hasProto) {
;(value as any).__proto__ = arrayMethods
} else {
for (let i = 0, l = arrayKeys.length; i < l; i++) {
const key = arrayKeys[i]
def(value, key, arrayMethods[key])
}
}
}
if (!shallow) {
this.observeArray(value)
}
} else {
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)
}
}通过阅读源码可以看出其步骤顺序如下:调用 initData 函数,调用 observe 回调,对传入的数组进行原型链修改,后续调用的方法都是重写后的方法,对数组中每个对象也再次代理。
Vue 依赖收集
所谓的依赖收集,实际上是观察者模式。被观察者指代的是数据(即观察数据的变化);观察者是 watcher ,里面可能有渲染逻辑、计算属性、用户 watcher 。
观察者要观察数据,则需要通过 dep 先收集数据。一个 watcher 中需要先可能对应多个数据,watcher 中还需要保存 dep 。重写渲染的时候可以让属性重新记录 watcher 。
dep与watcher是多对多的关系,一个dep对应多个watcher,一个watcher内有多个dep。默认渲染时会收集依赖,触发
get方法。数据更新了就找到属性对应的watcher去触发更新。数据更新了就找到对应的watcher触发更新。如果不触发
$mount,就不会render更新,也就不会收集依赖。
一图流如下:

一个视图渲染的属性有可能不一样,因此每次都会先清理,然后重新收集依赖。如果不清理,改旧的、现在没用到的属性也会造成视图更新。每次重新渲染都会触发 getter 方法,在这个方法收集依赖。
Vue 模板编译
用户传递的是 template 属性,需要将这个 template 编译成 render 函数。
- 首先转换成 AST 语法树
- 对语法树进行标记(静态节点)
- AST 语法树生成
render函数
Vue 生命周期
在 src/core/util/options.js 文件中的 444 行开始,循环父亲和儿子的所有属性,然后调用 mergeField 函数做合并操作。源码如下:
const options: ComponentOptions = {} as any
let key
// 循环父亲所有属性
for (key in parent) {
mergeField(key)
}
// 循环儿子所有属性
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key)
}
}
// 策略模式
function mergeField(key: any) {
const strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key)
}而策略模式中默认如果有儿子的属性方法则采用儿子的,没有才使用父亲的属性方法,源码如下:
const defaultStrat = function (parentVal: any, childVal: any): any {
// 以儿子为主
return childVal === undefined ? parentVal : childVal
}接下来看看 strats 的 hook 内封装了啥方法,源码如下:
export function mergeLifecycleHook(
parentVal: Array<Function> | null,
childVal: Function | Array<Function> | null
): Array<Function> | null {
// 获取父亲和儿子的
const res = childVal
? parentVal
? parentVal.concat(childVal) // 都有就合并
: isArray(childVal) // 否则看儿子是不是数组,是数组直接用,不是则用数组包裹
? childVal
: [childVal]
: parentVal
return res ? dedupeHooks(res) : res
}去到 src/core/instance/lifecycle.js 文件,找到生命周期的钩子回调函数,源码如下:
export function callHook(
vm: Component,
hook: string,
args?: any[],
setContext = true
) {
// #7573 disable dep collection when invoking lifecycle hooks
pushTarget()
const prev = currentInstance
setContext && setCurrentInstance(vm)
const handlers = vm.$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()
}实现原理是利用发布订阅模式,将用户写的钩子维护成一个数组,后续依次调用 callHook 。主要依靠的是 mergeOptions 方法。
为什么有些钩子是先子后父,有些是先父后子?
主要是看组件是如何渲染的。看下方代码:
<div id="app">
<my-button></my-button>
</div>遇到父组件 #app ,开始渲染父组件,触发其 beforeCreate 钩子函数。然后遇到子组件 my-button ,开始渲染子组件,触发子组件的 beforeCreate 钩子函数。
渲染完毕后触发子组件的 mounted 钩子函数,继续往下,渲染父组件,渲染完毕后触发父组件的 mounted 。
Vue 题外话
一般在哪里发送请求?
beforeCreate 生命周期的数据还未实现响应式,从源码下手证明这一点。
在 src/core/instance/init.js 文件的 59 行左右,函数调用顺序如下:
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate', undefined, false /* setContext */)
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')首先调用 initLifecycle 函数,该方法源码如下:
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
vm.$root = parent ? parent.$root : vm
vm.$children = []
vm.$refs = {}
vm._provided = parent ? parent._provided : Object.create(null)
vm._watcher = null
vm._inactive = null
vm._directInactive = false
vm._isMounted = false
vm._isDestroyed = false
vm._isBeingDestroyed = false
}总结下来就是处理组件父子关系,如 $parent 、$children 等。
然后调用了 initEvents 方法函数,该方法主要初始化一些全局方法,如 $on 、$off 、$emit 等。源码如下所示:
export function eventsMixin(Vue: typeof Component) {
const hookRE = /^hook:/
Vue.prototype.$on = function () {}
Vue.prototype.$once = function () {}
Vue.prototype.$off = function () {}
Vue.prototype.$emit = function () {}
}然后调用了 beforeCreate 生命周期钩子函数,该函数无法获取数据,作用不大,因此Vue3把它剔除了。
然后调用了 initInjections 方法,该方法主要是注册初始化 resolve 、injections 、before 、data/props 、inject 等方法。紧接着调用 initState 函数方法,此时劫持数据并做响应式处理。执行完毕后调用 initProvide 函数,主要实现 provide 方法。
再然后才调用 created 生命函数,此时拿到的是响应式的属性(不涉及DOM渲染),这个 API 可以在服务端渲染中使用。在Vue3中改为 setup 。
然后代码执行到 src/core/instance/lifecycle.js 文件中的 146 行,触发 beforeMount 生命周期回调。源码如下:
vm.$el = el
if (!vm.$options.render) {
// @ts-expect-error invalid type
vm.$options.render = createEmptyVNode
if (__DEV__) {
/* istanbul ignore if */
if (
(vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
vm.$options.el ||
el
) {
warn(
'You are using the runtime-only build of vue where the template ' +
'compiler is not available. Either pre-compile the templates into ' +
'render functions, or use the compiler-included build.',
vm,
)
} else {
warn(
'Failed to mount component: template or render function not defined.',
vm,
)
}
}
}
callHook(vm, 'beforeMount')该方法的意义不大,逻辑可以写到 create 钩子函数内,获取DOM元素并操作的方法可以写到 mounted 钩子函数内。
等到DOM元素渲染完毕,判断通过,才调用 mounted 回调函数。源码如下:
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}当有数据更新时,就会重新渲染,此时会被 Watcher 监听到,调用 beforeUpdate 钩子函数。源码如下:
const watcherOptions: WatcherOptions = {
before() {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
},
}执行 watcher.before() 方法调用更新,源码如下:
watcher = queue[index]
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
watcher.run()组件更新完毕后调用 updated 钩子函数,源码如下:
function callUpdatedHooks(queue: Watcher[]) {
let i = queue.length
while (i--) {
const watcher = queue[i]
const vm = watcher.vm
if (vm && vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'updated')
}
}
}如果销毁组件,最好在 beforeDestory 钩子函数上移除定时器等,此时数据还是响应式的。清理完之后调用 destoryed 钩子函数,此时数据没有响应式了,但是真实DOM还在。源码如下:
Vue.prototype.$destroy = function () {
const vm: Component = this
if (vm._isBeingDestroyed) {
return
}
// 调用销毁前
callHook(vm, 'beforeDestroy')
vm._isBeingDestroyed = true
// 移除 $children
const parent = vm.$parent
if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
remove(parent.$children, vm)
}
vm._scope.stop()
// 清除数据的响应式
if (vm._data.__ob__) {
vm._data.__ob__.vmCount--
}
vm._isDestroyed = true
// 把最新的虚拟节点标记为null
vm.__patch__(vm._vnode, null)
callHook(vm, 'destroyed')
// 关闭所有事件
vm.$off()
if (vm.$el) {
vm.$el.__vue__ = null
}
if (vm.$vnode) {
vm.$vnode.parent = null
}
}从上所述,一般最多在 mounted 上发请求。代码是同步执行的,请求是异步的,都会等待 mounted 之后才获取数据,因此不差 created 那点时间。
服务端渲染不都是在 created 中(服务端没有 DOM,也没有 mounted 钩子)。在哪发请求主要看需要做什么操作。
Vue mixin 原理及作用
通过 Vue.mixin 实现逻辑的复用,代码如下:
const request = () => {}
Vue.mixin({
beforeCreate() {
this.$request = request()
},
beforeDestory() {
// ...
},
})
new Vue({
el: '#app',
components: {
my: {
template: '<button @click="handle"></button>',
methods: {
handle() {
console.log(this.$request)
},
},
},
},
})虽然实现了复用,但还是有一点问题:
- 数据来源不明确。全局
mixin每个组件都可调用,项目大时后续的维护会比较诧异变量的来源 - 声明的时候可能会导致命名冲突
在Vue3中,采用高阶组件的形式,规避这一缺点。
从源码入手分析,在 src/core/global-api/mixin.js 文件内,源码如下所示:
export function initMixin(Vue: GlobalAPI) {
Vue.mixin = function (mixin: Object) {
this.options = mergeOptions(this.options, mixin)
return this
}
}这里的 this 是谁调用就指向谁,由于该 mixin 方法是Vue调用的,因此 this 实际上是 Vue。最终将 mixin 对象和 Vue.options 合并在一起。
查看 mergeOptions 函数的源码,如下所示:
export function mergeOptions(
parent: Record<string, any>,
child: Record<string, any>,
vm?: Component | null
): ComponentOptions {
if (__DEV__) {
checkComponents(child)
}
// 合并的是一个组件的构造函数
if (isFunction(child)) {
child = child.options
}
normalizeProps(child, vm)
normalizeInject(child, vm)
normalizeDirectives(child)
if (!child._base) {
// 针对组件的 extends 属性来进行合并
if (child.extends) {
parent = mergeOptions(parent, child.extends, vm)
}
// 针对组件内部的 mixins 来进行合并,即组件通过 mixins: [xxx] 来注册局部 mixin
if (child.mixins) {
for (let i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm)
}
}
}
const options: ComponentOptions = {} as any
let key
for (key in parent) {
mergeField(key)
}
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key)
}
}
function mergeField(key: any) {
const strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key)
}
return options
}总结下来,mixin 核心就是合并属性(内部采用策略模式进行合并),全局 mixin 与局部 mixin 针对不同的属性有不同的合并策略。
Vue data 为什么是函数
Vue 内部调用的是 Vue.extend() 方法,会将用户的选项放到子类上。代码如下所示:
const<word text="Vue" />= {};
Vue.extend = function (options) {
function Sub() {
this.data = this.constructor.options.data;
}
Sub.options = options;
return Sub;
};
let Child = Vue.extend({
data: {
a: 1,
},
});
let c1 = new Child();
let c2 = new Child();
console.log(c1.data.a, c2.data.a);但是如果多次调用 Vue.extend 方法,不可能每次都返回全新的,应该有一个缓存处理。到源码 src/core/global-api/extend.js 文件 20 行处查看,如下:
extendOptions = extendOptions || {}
const Super = this
const SuperId = Super.cid
const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
if (cachedCtors[SuperId]) {
return cachedCtors[SuperId]
}它通过 ID 查找对应的缓存的值,如果有则取缓存的值。没有才赋新值。
现在返回刚刚我们写的代码,此时输出两个构造函数的 a 都是 1,但是会有一个问题,他们共享=了同一个数据对象 data ,因此修改后会干扰到其他构造函数内的数据。
修改为函数返回对象的形式,确保每一个构造函数的 data 都是独立的、互不干扰的数据对象,修改不会造成数据干扰。
Vue 对此做了校验处理,如果是 newVue 说明是根组件,可以用对象的形式;如果是组件的形式,则只能是函数的形式。下面看看他的源码:
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
}
return function mergedDataFn() {
return mergeData(
isFunction(childVal) ? childVal.call(this, this) : childVal,
isFunction(parentVal) ? parentVal.call(this, this) : parentVal
)
}
} else {
return function mergedInstanceDataFn() {
const instanceData = isFunction(childVal)
? childVal.call(vm, vm)
: childVal
const defaultData = isFunction(parentVal)
? parentVal.call(vm, vm)
: parentVal
if (instanceData) {
return mergeData(instanceData, defaultData)
} else {
return defaultData
}
}
}
}这是最开始 newVue 时初始化 data 数据的方法,会判断是对象形式还是函数形式。而组件会判断其 data 的数据形式,源码如下:
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)
}由此可以总结一下,对于根实例而言,new Vue 组件是通过同一个构造函数多次创建实例,如果是同一个对象的话那么会被互相影响。每个组件的数据源都是独立的,则每次都调用 data 返回一个新的对象。
Vue nextTick 实现原理
nextTick 内部采用了异步任务进行包装(多个 nextTick 调用会被合并成一次,内部会合并回调)。最后在异步任务中批次处理。
主要应用场景就是异步更新(默认调度的时候,就会添加一个 nextTick 任务)用户为了获取最终的渲染结果需要在内部任务执行之后再执行,这时用户需要把对应的逻辑放到 nextTick 中。
每次触发更新都会执行 Watcher 类的 update 方法,源码如下:
update() {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}而方法 queueWatcher 则负责过滤同名 watcher ,并将多个渲染 watcher 去重后放到队列中。源码如下:
export function queueWatcher(watcher: Watcher) {
// 负责过滤同名 `watcher`
const id = watcher.id
if (has[id] != null) {
return
}
if (watcher === Dep.target && watcher.noRecurse) {
return
}
has[id] = true
if (!flushing) {
// 多个渲染 `watcher` 去重后放到队列
queue.push(watcher)
} else {
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
if (!waiting) {
waiting = true
if (__DEV__ && !config.async) {
flushSchedulerQueue()
return
}
// 每一次更新视图后内部会开启一个nextTick
nextTick(flushSchedulerQueue)
}
}前往文件 src/core/util/next-tick.js 文件中,找到 necttick 函数,源码如下:
export function nextTick(cb?: (...args: any[]) => any, ctx?: object) {
let _resolve
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e: any) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
pending = true
timerFunc()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}它还对当前浏览器能否使用 Promise 做了判断,如果可以使用则为微任务,不可使用则改为宏任务。源码如下:
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
// In problematic UIWebViews, Promise.then doesn't completely break, but
// it can get stuck in a weird state where callbacks are pushed into the
// microtask queue but the queue isn't being flushed, until the browser
// needs to do some other work, e.g. handle a timer. Therefore we can
// "force" the microtask queue to be flushed by adding an empty timer.
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (
!isIE &&
typeof MutationObserver !== 'undefined' &&
(isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]')
) {
// Use MutationObserver where native Promise is not available,
// e.g. PhantomJS, iOS7, Android 4.4
// (#6466 MutationObserver is unreliable in IE11)
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)) {
// Fallback to setImmediate.
// Technically it leverages the (macro) task queue,
// but it is still a better choice than setTimeout.
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// Fallback to setTimeout.
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}了解完源码后下方这道题就浅显易懂了,先看代码:
const vm = new Vue({
data() {
return {
a: 1,
}
},
})
vm.$nextTick(() => {
console.log(vm.a)
})
vm.a = 100
vm.$nextTick(() => {
console.log(vm.a)
})根据源码可知,他会开辟一个数组队列,一次把任务推送到数组队列中。首先推送第一次的 nextTick ,然后推送更新操作,最后推送第二次的 nextTick 。
因此先打印 1,然后修改值为 100,再打印 100。
Vue watch 与 computed
Vue 原理
下面来看看 computed 和 watch 的源码。首先来到 src/core/instance/state.js 文件内,找到 initState 函数,里面有计算属性和侦听器的挂载,源码如下:
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}可以看到计算属性的方法是在侦听器前挂载的,因为侦听器能够侦听计算属性变量的变化,因此需要计算属性方法先执行。
接着执行 initComputed 方法,源码如下:
function initComputed(vm: Component, computed: Object) {
const watchers = (vm._computedWatchers = Object.create(null))
const isSSR = isServerRendering()
for (const key in computed) {
const userDef = computed[key]
const getter = isFunction(userDef) ? userDef : userDef.get
if (__DEV__ && getter == null) {
warn(`Getter is missing for computed property "${key}".`, vm)
}
if (!isSSR) {
// create internal watcher for the computed property.
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}
if (!(key in vm)) {
defineComputed(vm, key, userDef)
} else if (__DEV__) {
if (key in vm.$data) {
warn(`The computed property "${key}" is already defined in data.`, vm)
} else if (vm.$options.props && key in vm.$options.props) {
warn(`The computed property "${key}" is already defined as a prop.`, vm)
} else if (vm.$options.methods && key in vm.$options.methods) {
warn(
`The computed property "${key}" is already defined as a method.`,
vm
)
}
}
}
}可以看到它首先创建一个空对象 watchers ,用于后续保存每一项计算属性。循环遍历 computed 计算属性对象,判断其类型是函数型还是对象型,获取相对应的值。
紧接着判断其是否是 dirty ,如果是 dirty 说明该属性已经被修改了,走正常流程更新数据;否则值没变化,用之前缓存的数据即可。源码如下:
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.depend()
}
return watcher.value
}
}
}判断当前的变量是计算属性还是 data 内的变量则通过传值处理,如果是计算属性,底层会传递一个 lazy 的参数,判断 lazy 是否为 true ,如果为真说明当前的变量是计算属性。源码如下:
// 为真说明是计算属性,不立即执行;为假说明是data内的数据,调用get方法劫持代理
this.value = this.lazy ? undefined : this.get()
// ...
update() {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}计算属性的底层逻辑梳理了一遍,现在看看侦听器 watch 的底层逻辑。同样在 state.js 文件中,找到 initWatch 方法,源码如下:
function initWatch(vm: Component, watch: Object) {
for (const key in watch) {
const handler = watch[key]
if (isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
createWatcher(vm, key, handler)
}
}
}
function createWatcher(
vm: Component,
expOrFn: string | (() => any),
handler: any,
options?: Object
) {
if (isPlainObject(handler)) {
options = handler
handler = handler.handler
}
if (typeof handler === 'string') {
handler = vm[handler]
}
return vm.$watch(expOrFn, handler, options)
}它也是循环遍历 watch 侦听器对象,如果是数组,循环创建 watcher ;如果是一个则直接创建 watcher 。
把每一项侦听器方法判断其类型,获取到对应的值后最终走 vm.$watch() 方法。方法源码如下:
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 || {}
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()
}
}可以看到,首先调用 Watcher 类收集依赖,然后判断是否需要立即执行 immediate ,最后返回一个销毁侦听的事件。deep 属性的实现原理源码在 src/core/observe/watcher.js 文件内,源码如下:
if (this.deep) {
traverse(value)
}通过递归实现深度监听。
Vue 异同点
相同点:二者底层都会创建一个 watcher (用法的区别,computed 定义的属性可以在模板中使用,watch 不能在视图中使用)
不同点:
computed默认不会立即执行,只有取值的时候才会执行。内部会有唯一一个dirty属性,来控制依赖的值是否发生变化,默认计算属性需要同步返回结果(有个包就是让computed变成异步的)watch默认用户会提供一个回调函数,数据变化了就调用这个回调。通过监控某个数据的变化,数据变化执行某些操作。
Vue set 的实现
Vue.set 方法是Vue中的一个补丁方法,正常情况下添加属性是不会触发更新的,数组无法监控到索引和长度的变化。
底层给每个对象都添加了一个 Dep 属性,触发更新时会调用 dep.notify() 方法,更新数据,代码如下:
const vm = new Vue({
data() {
return {
obj: {
a: 1,
},
}
},
})
vm.obj.b = 100 // js可以获取,视图不会更新
vm.obj.__ob__.dep.notify() // 实现视图更新因此 Vue.set 方法无非时通过上方的逻辑思路实现的。源码也是这么处理的,如下所示:
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__
// 如果是数组的修改,如 vm.arr[0] = 100 ,修改为 vm.arr.splice(0,1,100) 因为 splice 方法是重写过的了
if (isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
target.splice(key, 1, val)
if (ob && !ob.shallow && ob.mock) {
observe(val, false, true)
}
return val
}
// 已有的属性直接修改即可
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
// 不建议重写整个data对象,如 $set(vm, '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
}
// 不是响应式数据,直接添加
if (!ob) {
target[key] = val
return val
}
// 变成响应式数据
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
}Vue3不需要此方法了,修改为 proxy 代理对象本身。Vue 虚拟 DOM 的意义
实际业务中可能会针对不同的平台来使用不同的标签文本(weex 、web 、小程序),虚拟DOM可以跨平台,不需要考虑平台问题。
不用关心兼容性,可以在上层将对应的渲染方法传递过来,通知虚拟DOM渲染即可。
Diff 算法针对更新的时候,有了虚拟DOM之后可以通过 Diff 算法来找到最后的差异进行修改真实DOM。(类似于在真实DOM之间做了一个缓存)
Vue diff 算法
Diff 算法特点就是平级比较,内部采用了双指针方式进行优化,优化了常见的操作。采用了递归比较的方式。
Vue 针对一个节点的 diff 算法
先拿出根节点来进行比较,如果是同一个节点则比较属性,如果不是同一个节点则直接换成最新的即可。
同一个节点比较属性后,复用老节点。
Vue 比较儿子
一方有儿子,另一方没有,则作删除或添加的操作。
两方都有儿子:
- 优先比较头头、尾尾、头尾尾头的交叉对比
- 做一个映射表,用心的去映射表中查找此元素是否存在,存在则移动不存在则插入,最后删除多余的
- 会有多移动的情况。O(n) 复杂度的比较
源码如下:
const oldCh = oldVnode.children // 老的儿子
const ch = vnode.children // 新儿子
// 调用一系列的更新方法
if (isDef(data) && isPatchable(vnode)) {
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
if (isDef((i = data.hook)) && isDef((i = i.update))) i(oldVnode, vnode)
}
// vnode不是文本
if (isUndef(vnode.text)) {
// 两边都有儿子
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch)
updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {
if (__DEV__) {
checkDuplicateKeys(ch)
}
// 新的有,老的没有,添加节点
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
// 如果老的有,新的没有,则移除
removeVnodes(oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
// 如果老的是文本,则清空文本
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
// 文本不一致,则设置文本
nodeOps.setTextContent(elm, vnode.text)
}
if (isDef(data)) {
if (isDef((i = data.hook)) && isDef((i = i.postpatch))) i(oldVnode, vnode)
}Vue 题外话
既然数据劫持能够保持数据的响应式,那么为什么还需要虚拟DOM进行 Diff 检测差异呢?
如果给每一个属性都增加 watcher ,粒度太小不好控制;降低 watcher 数量(每一个组件都有一个 watcher ,一个变量变化更新该组件的视图)。再通过 Diff 算法优化渲染过程,减少视图更新的消耗,通过 Diff 算法和响应式原理折中处理。
key 的原理和作用?
方法 isSameVnode 中会根据 key 来判断两个元素是否是同一个元素,key 不同说明不是同一个 元素。key 在动态列表中不要使用索引,使用 key 尽量保证其唯一性,可以优化 diff 算法。
Vue 组件化
Vue 特点
组件的优点:组件的复用可以根据数据渲染对应的组件,把组件相关的内容放在一起(方便复用)。合理规划组件,可以做到更新的时候是组件级更新(特性、属性、事件、插槽)。
Vue 中怎么样处理组件?
Vue.extend根据用户传入的对象生成一个组件的构造函数- 根据组件产生对应的虚拟节点
- 做组件初始化,将虚拟节点转化成真实节点(组件的
init方法)
Vue 渲染流程
先写一段组件化的Vue代码,如下:
<div id="app">
<my></my>
</div>
<script>
const vm = new Vue({
el: '#app',
// vm.$options.components = {my: 模板}
components: {
my: {
template: '<div>my-component</div>',
},
},
})
</script>vm.$options.components = {my: 模板}- 创建对应的虚拟节点,例如
{tag: 'my', data: {}, componentOptions: {Ctor:Vue.extend({my: 模板})}} - 调用
createComponent方法函数创建真实节点,然后调用init方法挂载 vm.$el插入到父元素中
Vue 更新流程
更新的几种情况:
- 父传子数据更新,收集依赖
- 属性更新,给组件传入属性,属性变化后触发更新
- 插槽变化
重点聊聊属性更新,更新逻辑都在 patch 函数中。去到 src/core/vdom/patch.js 文件,找到 patch 函数,部分源码如下:
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
}调用 patchVnode 方法判断新旧子节点的异同,如果父传子的数据发生变化,,则调用 prepatch 方法。源码如下所示:
function isDef<T>(v: T): v is NonNullable<T> {
return v !== undefined && v !== null
}
let i
const data = vnode.data
if (isDef(data) && isDef((i = data.hook)) && isDef((i = i.prepatch))) {
i(oldVnode, vnode)
}而 prepatch 方法则非常暴力的做一件事情:把旧的实例赋值给新的实例,实例复用了也就意味着 componsnetInstance.$el 也复用了。其本质的作用就是复用组件的实例,并且可以去更新属性、事件、插槽。
复用完之后把对应的孩子、数据、事件等传参调用 updateChildComponent 方法。源码如下:
prepatch(oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
const options = vnode.componentOptions
const child = (vnode.componentInstance = oldVnode.componentInstance)
updateChildComponent(
child,
options.propsData, // updated props
options.listeners, // updated listeners
vnode, // new parent vnode
options.children // new children
)
},updateChildComponent 方法主要做以下几件事情:
- 把变量的响应式先去掉,因为属性是在父组件中定义的,传递给子组件。父组件定义的数据已经是响应式了,数据变化会自动更新
- 获取到父传子的数据对象
- 循环遍历,一一校验所传的属性名和值
- 更新完毕后恢复响应式
总结
- 组件更新会触发组件的
prepatch方法,会复用组件,并且比较组件的属性、事件、插槽- 父组件给子组件传递的属性
prop是响应式的,模板中使用会做依赖收集,手机自己的组件watcher- 稍后组件更新了会重新给
props赋值,赋值完成后会触发watcher重新更新
Vue 异步组件原理
Vue 中异步组件写法有很多,主要用作大的组件可以异步加载比较大的组件,如 markdown 组件、editor 组件等。
先渲染一个注释标签,等组件加载完毕,最后再重新渲染 forceUpdate (图片懒加载)。使用异步组件会配合 webpack 。
Vue.component('async-example', function (resolve, reject) {
setTimeout(function () {
resolve({
template: `<div>I am async!</div>`,
})
}, 1000)
})可以看到,其参数二不再是一个对象,而是一个工厂函数,收到一个 resolve 回调,也可以调用 reject 表示调用失败。
也可以在工厂函数中返回一个 Promise ,写法如下:
Vue.component('async-example', () => {
component: import('./my-async-example'),
loading: {
template: '<div>loading</div>'
}, // 加载中的组件
error: ErrorComponent, // 加载失败的组件
delay: 200, // 加载时间
timeout: 3000 // 超时时间
})从源码入手,梳理整个流程。前面介绍到,Vue 创建组件时,会调用 Vue.extend 方法,那是因为底层代码有做了一个类型判断,如果是对象类型,直接调用 extend 方法。但异步组件是函数型,因此不会走这一步。源码如下:
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor as typeof Component)
}它会继续往下走,判断是否有 cid ,没有 cid 说明是异步组件,调用 resolveAsyncComponent 方法,返回值赋值给 Ctor 。然后再做判断,如果此时 Ctor 还是 undefined ,则调用 createAsyncPlaceholder 方法。源码如下:
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor
Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
if (Ctor === undefined) {
return createAsyncPlaceholder(asyncFactory, data, context, children, tag)
}
}resolveAsyncComponent 函数源码如下所示:
export function resolveAsyncComponent(
factory: { (...args: any[]): any, [keye: string]: any },
baseCtor: typeof Component
): typeof Component | void {
// 如果factory.error,就直接渲染错误组件
if (isTrue(factory.error) && isDef(factory.errorComp)) {
return factory.errorComp
}
// 如果factory.resolved,就直接渲染成功
if (isDef(factory.resolved)) {
return factory.resolved
}
const owner = currentRenderingInstance
if (owner && isDef(factory.owners) && factory.owners.indexOf(owner) === -1) {
factory.owners.push(owner)
}
// 如果factory.loading,就直接渲染loading组件
if (isTrue(factory.loading) && isDef(factory.loadingComp)) {
return factory.loadingComp
}
if (owner && !isDef(factory.owners)) {
const owners = (factory.owners = [owner])
let sync = true
let timerLoading: number | null = null
let timerTimeout: number | null = null
owner.$on('hook:destroyed', () => remove(owners, owner))
// 强制更新
const forceRender = (renderCompleted: boolean) => {
for (let i = 0, l = owners.length; i < l; i++) {
owners[i].$forceUpdate()
}
if (renderCompleted) {
owners.length = 0
if (timerLoading !== null) {
clearTimeout(timerLoading)
timerLoading = null
}
if (timerTimeout !== null) {
clearTimeout(timerTimeout)
timerTimeout = null
}
}
}
// 传递给用户的resolve
const resolve = once((res: Object | Component) => {
// cache resolved
factory.resolved = ensureCtor(res, baseCtor)
// invoke callbacks only if this is not a synchronous resolve
// (async resolves are shimmed as synchronous during SSR)
if (!sync) {
forceRender(true)
} else {
owners.length = 0
}
})
// 传递给用户的reject
const reject = once((reason) => {
__DEV__ &&
warn(
`Failed to resolve async component: ${String(factory)}` +
(reason ? `\nReason: ${reason}` : '')
)
if (isDef(factory.errorComp)) {
factory.error = true
forceRender(true)
}
})
// 让函数执行,返回resolve或reject
const res = factory(resolve, reject)
if (isObject(res)) {
if (isPromise(res)) {
// 是promise就调用promise.then。如果成功则将获取到的结果赋予给 factory.resolved,即对应的组件
if (isUndef(factory.resolved)) {
res.then(resolve, reject)
}
} else if (isPromise(res.component)) {
// 用户自己写的特殊的组件,是一个promise
res.component.then(resolve, reject)
if (isDef(res.error)) {
factory.errorComp = ensureCtor(res.error, baseCtor)
}
if (isDef(res.loading)) {
// 将组件的loading赋予给factory.loadingComp
factory.loadingComp = ensureCtor(res.loading, baseCtor)
if (res.delay === 0) {
factory.loading = true
} else {
// @ts-expect-error NodeJS timeout type
timerLoading = setTimeout(() => {
timerLoading = null
if (isUndef(factory.resolved) && isUndef(factory.error)) {
factory.loading = true
forceRender(false)
}
}, res.delay || 200)
}
}
if (isDef(res.timeout)) {
timerTimeout = setTimeout(() => {
timerTimeout = null
if (isUndef(factory.resolved)) {
reject(__DEV__ ? `timeout (${res.timeout}ms)` : null)
}
}, res.timeout)
}
}
}
sync = false
// 默认返回一个loading
return factory.loading ? factory.loadingComp : factory.resolved
}
}总结
异步组件默认不会调用
Vue.extend方法,所有Ctor上没有cid属性,没有cid属性的就是异步组件。先渲染一个占位符组件,但是如果有
loading会先渲染loading,第一轮就结束了。如果用户调用了
resolve,会将结果赋予给factory.resolved上面,强制重新渲染。重新渲染时候再次进入到resolveAsyncComponent中,会直接到factory.resolved结果来渲染。
Vue 函数式组件优势及原理
函数组件与类组件相比,没有 this ,没有所谓的状态,没有 sub 实例,没有生命周期 beforeCreate 、created 等等。优点在于性能好,不需要创建 watcher 。
由上方的描述不难看出,函数式组件是单纯的无状态组件,没有其他的操作,因此其性能高。源码如下:
export function createFunctionalComponent(
Ctor: typeof Component,
propsData: Object | undefined,
data: VNodeData,
contextVm: Component,
children?: Array<VNode>
): VNode | Array<VNode> | void {
const options = Ctor.options
const props = {}
const propOptions = options.props
if (isDef(propOptions)) {
for (const key in propOptions) {
props[key] = validateProp(key, propOptions, propsData || emptyObject)
}
} else {
if (isDef(data.attrs)) mergeProps(props, data.attrs)
if (isDef(data.props)) mergeProps(props, data.props)
}
// 创建属性上下文
const renderContext = new FunctionalRenderContext(
data,
props,
children,
contextVm,
Ctor
)
// 函数式组件对应的vnode
const vnode = options.render.call(null, renderContext._c, renderContext)
// 如果符合,直接返回
if (vnode instanceof VNode) {
return cloneAndMarkFunctionalResult(
vnode,
data,
renderContext.parent,
options,
renderContext
)
} else if (isArray(vnode)) {
const vnodes = normalizeChildren(vnode) || []
const res = new Array(vnodes.length)
for (let i = 0; i < vnodes.length; i++) {
res[i] = cloneAndMarkFunctionalResult(
vnodes[i],
data,
renderContext.parent,
options,
renderContext
)
}
return res
}
}总结
函数式组件就是调用
render拿到返回结果渲染,所以性能高。
Vue 组件传值的方式及之间区别
props父传子props会分类,在父组件传递的参数中,只有子组件内props声明了的才会归为props一类,剩下的归为$attrs一类。如下:vue<div id="app"> <son a=1 b=2 c=3 /> </div> <script> new Vue({ el: '#app', components: { son: { props: ['a', 'b'], template: '<div>son</div>', }, }, }) </script>上方示例代码中
a和b是props一类,c为$attes一类。源码如下所示:jsexport function extractPropsFromVNodeData( data: VNodeData, Ctor: typeof Component, tag?: string ): object | undefined { const propOptions = Ctor.options.props if (isUndef(propOptions)) { return } const res = {} const { attrs, props } = data if (isDef(attrs) || isDef(props)) { for (const key in propOptions) { const altKey = hyphenate(key) if (__DEV__) { const keyInLowerCase = key.toLowerCase() if ( key !== keyInLowerCase && attrs && hasOwn(attrs, keyInLowerCase) ) { tip( `Prop "${keyInLowerCase}" is passed to component ` + `${formatComponentName( // @ts-expect-error tag is string tag || Ctor )}, but the declared prop name is` + ` "${key}". ` + `Note that HTML attributes are case-insensitive and camelCased ` + `props need to use their kebab-case equivalents when using in-DOM ` + `templates. You should probably use "${altKey}" instead of "${key}".` ) } } checkProp(res, props, key, altKey, true) || checkProp(res, attrs, key, altKey, false) } } return res }拿到父传子的数据后循环遍历校验(如转为小写后查看是否存在该属性等),然后分类出
props和attrs。接着创建好组件的虚拟节点,有一个属性
{componentOptions: {propsData}}。初始化的时候就要对propsData做处理,挂载到vm.$options.propsData。源码如下:jsconst name = getComponentName(Ctor.options) || tag const vnode = new VNode( `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`, data, undefined, undefined, undefined, context, { Ctor, propsData, listeners, tag, children }, asyncFactory, ) return vnode初始化属性时会走到
initProps方法,拿到propsData,并声明一个vm._props,该属性类似于vm._data。声明一个数组存放所有的key。判断是不是根属性,只有根属性会被进行观测,其他父组件传递给的不需要观测。循环遍历子组件接收父组件的传值
props对象,把所有的键名push放到存放key的数组内,校验值是否合法。源码如下:jsfunction initProps(vm: Component, propsOptions: Object) { const propsData = vm.$options.propsData || {} const props = (vm._props = shallowReactive({})) // keys 数组用于存放 props 对象的键名key const keys: string[] = (vm.$options._propKeys = []) const isRoot = !vm.$parent // 判断是不是根属性 if (!isRoot) { toggleObserving(false) } // 循环遍历propsOptions对象 for (const key in propsOptions) { // 保存键名 keys.push(key) // 校验键值是否合法 const value = validateProp(key, propsOptions, propsData, vm) /* istanbul ignore else */ if (__DEV__) { 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) } if (!(key in vm)) { proxy(vm, `_props`, key) } } toggleObserving(true) }校验成功后调用
defineReactive方法,该方法把该属性转为响应式。源码如下:jsexport function defineReactive( obj: object, key: string, val?: any, customSetter?: Function | null, shallow?: boolean, mock?: boolean ) { const dep = new Dep() // ... 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) { childOb.dep.depend() 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) { 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 }设为响应式完毕后把所有属性定义到
vm._props上,并通过映射实现vm.b => vm._props.b,因此可以通过vm.b获取props内b的变量数据。父组件的值发生了变化,则去更新
vm._props,然后更新视图。emit子传父子传父通过自定义事件传参来实现,代码如下:
html<son @xxx="handler" />该方法实际上是给子组件绑定一个方法,如下:
jsson.$on('xxx', handler)这是典型的发布订阅模式。源码中会给组件虚拟节点
data上的on属性绑定该函数,效果如下图所示:
还有另外一种写法:为组件绑定原生事件,在Vue2中,组件想要实现原生事件,需要添加
.native修饰符,代码如下:html<son @click.native="handler" />底层逻辑会给组件虚拟节点
data对象中添加一个nativeOn属性,并绑定相对应的click函数,如下图所示:
ref获取组件实例通过
this.$refs.xxx可以获取DOM元素和组件实例(虚拟DOM没有DOM元素,也没有组件实例,因此没有处理ref)。底层逻辑会走
createPatchFunction函数,形参接收到的是节点操作和属性操作,如下图所示:
里面的属性包含了针对浏览器的属性和虚拟DOM内核中定义的逻辑。把
ref属性保存到data中,代码与效果如下:jsexport function createPatchFunction(backend) { let i, j const cbs: any = {} // ... const data = vnode.data }
调用
invokeCreateHooks函数方法,把相应的数据传过去,jsif (isDef(data)) { invokeCreateHooks(vnode, insertedVnodeQueue) }该方法会循环遍历,找到对应有
ref相关方法的属性,代码如下:jsfunction invokeCreateHooks(vnode, insertedVnodeQueue) { for (let i = 0; i < cbs.create.length; ++i) { cbs.create[i](emptyNode, vnode) } i = vnode.data.hook // Reuse variable if (isDef(i)) { if (isDef(i.create)) i.create(emptyNode, vnode) if (isDef(i.insert)) insertedVnodeQueue.push(vnode) } }
ref对应执行的方法是registerRef方法,在src/core/vdom/modules/ref.js文件内,做如下操作:- 取出
ref - 拿到实例
- 如果是组件,那么
ref是组件的实例;如果是元素,ref是真实 DOM - 获取
vm.$refs - 判断,如果是删除方法,则把它从数组中删除;否则在
refs对象内把用户设置的实例名称作为对象的键名,对应的DOM作为键值
源码如下:
jsexport function registerRef(vnode: VNodeWithData, isRemoval?: boolean) { const ref = vnode.data.ref if (!isDef(ref)) return const vm = vnode.context const refValue = vnode.componentInstance || vnode.elm const value = isRemoval ? null : refValue const $refsValue = isRemoval ? undefined : refValue if (isFunction(ref)) { invokeWithErrorHandling(ref, vm, [value], vm, `template ref function`) return } const isFor = vnode.data.refInFor const _isString = typeof ref === 'string' || typeof ref === 'number' const _isRef = isRef(ref) const refs = vm.$refs if (_isString || _isRef) { if (isFor) { const existing = _isString ? refs[ref] : ref.value if (isRemoval) { isArray(existing) && remove(existing, refValue) } else { if (!isArray(existing)) { if (_isString) { refs[ref] = [refValue] setSetupRef(vm, ref, refs[ref]) } else { ref.value = [refValue] } } else if (!existing.includes(refValue)) { existing.push(refValue) } } } else if (_isString) { if (isRemoval && refs[ref] !== refValue) { return } refs[ref] = $refsValue setSetupRef(vm, ref, value) } else if (_isRef) { if (isRemoval && ref.value !== refValue) { return } ref.value = value } else if (__DEV__) { warn(`Invalid template ref type: ${typeof ref}`) } } }- 取出
props总结
子组件更新原理,即父组件的属性修改后会调用子组件的
prepatch方法,接收到后更新vm._props.变量 = 新值。组件重新更新时都会执行
prepatch方法,在更新时如果是开发环境还会把标识变为true,源码如下:jsif (__DEV__) { isUpdatingChildComponent = true }在调用
defineReactive方法时会做判断,如果是开发环境则报错,不允许直接通过props修改变量,保持单项数据的规范。源码如下:jsdefineReactive(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, ) } })属性的原理就是把解析后的
props,验证后就会将属性定义在当前的实例vm._props上(这个对象上的属性都是通过defineReactive来定义的响应式数据。组件在渲染的过程中会去vm上取值,_props属性会被代理到vm上)
emit总结在创建虚拟节点的时候将所有事件绑定到
listeners,通过$on方法绑定事件,通过$emit解绑事件。源码如下:jsVue.prototype.$on = function ( event: string | Array<string>, fn: Function ): Component { // vm._events.xxx = [fn] const vm: Component = this if (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 }
ref总结创建
ref时,会将用户所有的DOM操作以及属性,都维护到一个cbs属性中(create、update、insert、destory等等)。依次调用cbs中create方法,这里就包含ref相关的操作,会操作ref并且赋值。
Vue provide 与 inject
provide 就是在当前组件上通过一个 _provide 属性,对应的就是提供的对象。源码在 src/core/instance/inject.js 文件内,代码如下所示:
export function initProvide(vm: Component) {
const provideOption = vm.$options.provide
if (provideOption) {
const provided = isFunction(provideOption)
? provideOption.call(vm)
: provideOption
if (!isObject(provided)) {
return
}
const source = resolveProvided(vm)
// IE9 doesn't support Object.getOwnPropertyDescriptors so we have to
// iterate the keys ourselves.
const keys = hasSymbol ? Reflect.ownKeys(provided) : Object.keys(provided)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
Object.defineProperty(
source,
key,
Object.getOwnPropertyDescriptor(provided, key)!
)
}
}
}inject 写法为 inject: ['a'] 。在底层逻辑中,以以下步骤实现业务功能:
- 在父组件内找到对应的属性
- 把这个属性定义到自己的身上
源码如下所示:
export function initInjections(vm: Component) {
const result = resolveInject(vm.$options.inject, vm)
if (result) {
toggleObserving(false)
Object.keys(result).forEach((key) => {
/* istanbul ignore else */
if (__DEV__) {
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)
}
}上述源码中,方法 resolveInject 就是实现查找父组件上的属性,它的步骤为:
- 声明一个空对象
- 拿着
key,遍历对象一个一个查找 - 将当前实例作为开头,拿到结果保存至
result,找到一个就停止 - 向上查找父组件
源码如下:
export function resolveInject(
inject: any,
vm: Component
): Record<string, any> | undefined | null {
if (inject) {
// inject is :any because flow is not smart enough to figure out cached
const result = Object.create(null)
const keys = hasSymbol ? Reflect.ownKeys(inject) : Object.keys(inject)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
// #6574 in case the inject object is observed...
if (key === '__ob__') continue
const provideKey = inject[key].from
if (provideKey in vm._provided) {
result[key] = vm._provided[provideKey]
} else if ('default' in inject[key]) {
const provideDefault = inject[key].default
result[key] = isFunction(provideDefault)
? provideDefault.call(vm)
: provideDefault
} else if (__DEV__) {
warn(`Injection "${key as string}" not found`, vm)
}
}
return result
}
}总结
provide在父组件中将属性暴露出来,在父组件中提供数据;inject在后代组件中注入属性,通过递归向上查找。
Vue $attrs与$listeners
在实际业务中,$attrs 可以接收父组件通过 v-bind 传输的所有变量,$listeners 可以接收父组件通过 v-on 传递的所有方法。
从源码下手查看其底层逻辑,在 src/core/instance/render.js 文件中,代码如下:
if (__DEV__) {
defineReactive(
vm,
'$attrs',
(parentData && parentData.attrs) || emptyObject,
() => {
!isUpdatingChildComponent && warn(`$attrs is readonly.`, vm)
},
true,
)
defineReactive(
vm,
'$listeners',
options._parentListeners || emptyObject,
() => {
!isUpdatingChildComponent && warn(`$listeners is readonly.`, vm)
},
true,
)
} else {
defineReactive(
vm,
'$attrs',
(parentData && parentData.attrs) || emptyObject,
null,
true,
)
defineReactive(
vm,
'$listeners',
options._parentListeners || emptyObject,
null,
true,
)
}源码主要区分了开发环境与生产环境,把从父节点那里接收到的值分别保存。这就是它的底层逻辑。
下面来看实际业务中它是如何使用的。父组件传递参数,子组件接收数据,此时父组件传递的参数会都保存在子组件的根标签。如果想要在二级以下的标签使用,则需要先设置 inheritAttrs 为 false ,然后再需要使用的标签上获取对应的值。代码如下:
<div id="app">
<son :a="1" @click="handler" />
</div>
<script>
new Vue({
el: '#app',
components: {
inheritAttrs: false,
son: {
template: `<div><input v-bind="${$attrs.a}" /></div>`,
},
},
})
</script>Vue 组件中 name 属性的好处与作用
在Vue中有 name 属性的组件可以被递归调用(在写模板语法的时候,可以通过 name 属性来递归调用自己)。
在声明组件时通过 Sub.options.components[name] = 组件 的形式,把自己的构造函数放上去。
该属性还可以用来标识组件,通过 name 来找到对应的组件,自己封装跨级通信。
父组件调用子组件的方法
思路如下:
- 遍历组件的
name是否等于需要的name组件 - 如果相等,则调用其相应的函数方法
代码如下:
jsVue.prototype.$dispatch = function (componentName, data, fnName) { let vm = this while (vm) { if (vm.$options.name === componentName) { vm[fnName](data) break } vm = vm.$parent } }- 遍历组件的
子组件给父组件传自定义事件
jsVue.prototype.$broadcast = function (conponentName, data, fnName) { let components = [] let vm = this function findChildren(children) { children.forEach((child) => { // 找到符合名称的组件 if (child.$options.name === componentName) { components.push(child) } child.$children && findChildren(child.$children) }) } findChildren(vm.$children) // 遍历符合条件的组件数组,依次调用 emit 方法 components.forEach((child) => { child.$emit(fnName, data) }) }
name 属性还可以作 devtool 调试工具来标明具体的组件。
Vue v-if、v-for、v-model
Vue v-if 和 v-for 优先级
前往 template 源码翻译的在线网站,输入对应的 template 结构,翻译成源码如下所示:

由此可以推断出 v-for 的优先级高于 v-if 。在编译的时候,会将 v-for 渲染成 _l 函数,v-if 会渲染成三元表达式。
从源码下手,去往 src/compiler/codegen/index.js 文件,该部分的源码如下所示:
else if (el.for && !el.forProcessed) {
return genFor(el, state)
} else if (el.if && !el.ifProcessed) {
return genIf(el, state)
}可以发现,它是先处理 el.for 循环,然后再执行 el.if 判断。
先来看看 v-for 调用的 genFor 方法的源码,代码如下所示:
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)}` +
'})'
)
}以上方截图为例,可以看到,该方法首先获取到循环的次数 el.for ,然后获取到循环的每一项 i 、每一项的索引 index 、第三个循环参数。
接着判断是否有 key 变量,没有则弹出错误提示,最后转成 AST 树并返回。
el 变量内拥有的属性如下图所示:

接下来看看 v-if 的源码,调用了 genIf 方法,代码如下:
export function genIf(
el: any,
state: CodegenState,
altGen?: Function,
altEmpty?: string
): string {
el.ifProcessed = true // avoid recursion
return genIfConditions(el.ifConditions.slice(), state, altGen, altEmpty)
}它调用了 genIfConditions 方法,其源码如下:
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)
}
}该方法的本质是截取出判断的条件,再放到 AST 树内,最后返回出去。
Vue v-if 和 v-show 区别
面试时经常会有人问到,v-if 和 v-show 的区别。网上很多回答都是:v-if 控制组件是否创建,v-show 控制组件 display 属性是否显隐。那么,如果 v-show 从 false 改为 true ,<span> 标签的 display 属性会设置为 block 么?我们从源码入手。
从底层逻辑上我们已经知道了 v-if 实际上是通过三元表达式判断它需不需要编译,而 v-show 则是在编译的时候添加一条指令。如下图所示:

接下来看看 v-show 的源码,在 sec/platforms/web/runtime/directives/show.js 文件内,代码如下所示:
bind(el: any, { value }: VNodeDirective, vnode: VNodeWithData) {
vnode = locateNode(vnode)
const transition = vnode.data && vnode.data.transition
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 {
el.style.display = value ? originalDisplay : 'none'
}
},可以看到,他会先获取到原来标签的 display 属性,保存到 originalDisplay 变量中,后续切换 v-show 的值为真时则拿到 originalDisplay 变量内的 display 属性值。
题外话
为什么用
display,不用visbility: hidden或opacity: 0呢?这两个方法都会占位,可以触发相应的事件。
Vue v-if 实现原理
总结一下:v-if 会编译成三元表达式,如果为真则编译渲染,如果为假则不编译。
Vue v-for 实现原理
v-for 是通过 _l AST 语法树来实现的。下面查看一下 _l 底层源码。来到 src/core/instance/render-helpers/index.js 文件中,代码如下所示:
export function installRenderHelpers(target: any) {
target._o = markOnce
target._n = toNumber
target._s = toString
target._l = renderList
target._t = renderSlot
target._q = looseEqual
target._i = looseIndexOf
target._m = renderStatic
target._f = resolveFilter
target._k = checkKeyCodes
target._b = bindObjectProps
target._v = createTextVNode
target._e = createEmptyVNode
target._u = resolveScopedSlots
target._g = bindObjectListeners
target._d = bindDynamicKeys
target._p = prependModifier
}可以看到,它每一个 AST 树的语法标识都有对应的方法。下面查看其对应方法 renderList 。代码如下所示:
export function renderList(
val: any,
render: (val: any, keyOrIndex: string | number, index?: number) => VNode
): Array<VNode> | null {
let ret: Array<VNode> | null = null,
i,
l,
keys,
key
if (isArray(val) || typeof val === 'string') {
ret = new Array(val.length)
for (i = 0, l = val.length; i < l; i++) {
ret[i] = render(val[i], i)
}
} else if (typeof val === 'number') {
ret = new Array(val)
for (i = 0; i < val; i++) {
ret[i] = render(i + 1, i)
}
} else if (isObject(val)) {
if (hasSymbol && val[Symbol.iterator]) {
ret = []
const iterator: Iterator<any> = val[Symbol.iterator]()
let result = iterator.next()
while (!result.done) {
ret.push(render(result.value, ret.length))
result = iterator.next()
}
} else {
keys = Object.keys(val)
ret = new Array(keys.length)
for (i = 0, l = keys.length; i < l; i++) {
key = keys[i]
ret[i] = render(val[key], key, i)
}
}
}
if (!isDef(ret)) {
ret = []
}
;(ret as any)._isVList = true
return ret
}它做的事情很简单,判断循环条件的类型,如果是数值型,直接循环即可;如果是数组型,则循环其长度;如果是对象型,则通过迭代遍历循环。最后返回出去。
Vue v-model 实现原理
绑定到普通元素上
v-model 放在不同的元素上会编译出不同的结果,针对文本来说会处理文本,编译成 value + input + 指令处理。
代码源码翻译图如下所示:

接下来看它的底层逻辑,源码在 src/platforms/web/compiler/directives/model.js 文件中,代码如下所示:
export default function model(
el: ASTElement,
dir: ASTDirective,
_warn: Function
): boolean | undefined {
warn = _warn
const value = dir.value
const modifiers = dir.modifiers
const tag = el.tag
const type = el.attrsMap.type
if (__DEV__) {
// inputs with type="file" are read only and setting the input's
// value will throw an error.
if (tag === 'input' && type === 'file') {
warn(
`<${el.tag} v-model="${value}" type="file">:\n` +
`File inputs are read only. Use a v-on:change listener instead.`,
el.rawAttrsMap['v-model']
)
}
}
if (el.component) {
genComponentModel(el, value, modifiers)
// component v-model doesn't need extra runtime
return false
} else if (tag === 'select') {
genSelect(el, value, modifiers)
} else if (tag === 'input' && type === 'checkbox') {
genCheckboxModel(el, value, modifiers)
} else if (tag === 'input' && type === 'radio') {
genRadioModel(el, value, modifiers)
} else if (tag === 'input' || tag === 'textarea') {
genDefaultModel(el, value, modifiers)
} else if (!config.isReservedTag(tag)) {
genComponentModel(el, value, modifiers)
// component v-model doesn't need extra runtime
return false
} else if (__DEV__) {
warn(
`<${el.tag} v-model="${value}">: ` +
`v-model is not supported on this element type. ` +
"If you are working with contenteditable, it's recommended to " +
'wrap a library dedicated for that purpose inside a custom component.',
el.rawAttrsMap['v-model']
)
}
// ensure runtime directive metadata
return true
}可以看到,该方法做了以下几件事情:
- 获取
v-model双向绑定的内容、标签、类型等 - 判断是否是文件上传组件
<input type="file" />,如果是则返回报错 - 根据
v-model绑定的DOM元素的类型,调用不同的方法 - 如果不是组件、
input类型,则报错
下拉框、单选框、复选框的事件都是为元素绑定一个 change 事件,这里不做过多赘述,着重看一下 text 文本框的事件处理。源码如下:
const directive = {
inserted(el, binding, vnode, oldVnode) {
if (vnode.tag === 'select') {
// #6903
if (oldVnode.elm && !oldVnode.elm._vOptions) {
mergeVNodeHook(vnode, 'postpatch', () => {
directive.componentUpdated(el, binding, vnode)
})
} else {
setSelected(el, binding, vnode.context)
}
el._vOptions = [].map.call(el.options, getValue)
} else if (vnode.tag === 'textarea' || isTextInputType(el.type)) {
el._vModifiers = binding.modifiers
if (!binding.modifiers.lazy) {
el.addEventListener('compositionstart', onCompositionStart)
el.addEventListener('compositionend', onCompositionEnd)
// Safari < 10.2 & UIWebView doesn't fire compositionend when
// switching focus before confirming composition choice
// this also fixes the issue where some browsers e.g. iOS Chrome
// fires "change" instead of "input" on autocomplete.
el.addEventListener('change', onCompositionEnd)
/* istanbul ignore if */
if (isIE9) {
el.vmodel = true
}
}
}
},
componentUpdated(el, binding, vnode) {
if (vnode.tag === 'select') {
setSelected(el, binding, vnode.context)
// in case the options rendered by v-for have changed,
// it's possible that the value is out-of-sync with the rendered options.
// detect such cases and filter out values that no longer has a matching
// option in the DOM.
const prevOptions = el._vOptions
const curOptions = (el._vOptions = [].map.call(el.options, getValue))
if (curOptions.some((o, i) => !looseEqual(o, prevOptions[i]))) {
// trigger change event if
// no matching option found for at least one value
const needReset = el.multiple
? binding.value.some((v) => hasNoMatchingOption(v, curOptions))
: binding.value !== binding.oldValue &&
hasNoMatchingOption(binding.value, curOptions)
if (needReset) {
trigger(el, 'change')
}
}
}
},
}
function onCompositionEnd(e) {
// prevent triggering an input event for no reason
if (!e.target.composing) return
e.target.composing = false
trigger(e.target, 'input')
}其中,它会监听中文输入完毕的事件,中文输入完毕后,再把标识变为 false ,触发 input 事件,手动更新。
总结一下, value 和 input 实现双向绑定阻止中文的触发,指令的作用就是处理中文输入完毕后手动触发更新。
绑定到组件上
v-model 绑定到组件上会被编译成一个 model 对象,组件在创建虚拟节点的时候会有这个对象。逻辑如下图所示:

可以看出来,它实质上是 value 和 input 的语法糖。
Vue 修饰符 .sync 的作用、用法及原理
Vue 作用
一个组件如果想要实现多个组件响应式数据,但是在Vue2中不允许一个组件使用多个 v-model ,因此推出 .sync 语法糖,实现子组件改变父组件的变量。
在Vue3中该修饰符废除。
Vue 用法
父组件在给子组件传递参数时使用 .sync 修饰符;子组件给父组件传递自定义事件时传递 update:父组件传递的变量名称, 需要传递的变量 实现子组件改变父组件的变量。
代码如下所示:
<div id="app">
<my :xx.sync="info" />
</div>
<script>
Vue.component('my', {
props: [xx],
template: `<div>{{xx}}<button @click="$emit('update:xx', world)">click me change xx</button></div>`,
})
const vm = new Vue({
el: '#app',
data() {
return {
info: 'hello',
}
},
})
</script>Vue 原理
具体原理如下图所示:

绑定一个属性 xx ,并且生成一个事件 update:xx 。
Vue Vue.use 原理
首先需要知道 Vue.use 方法是干嘛用的,该方法主要用于注册插件,其使用方法代码如下所示:
// 写法1
let plugin1 = function (Vue, options) {
console.log(Vue, options)
}
Vue.use(plugin1, { a: 1, b: 2 })
// 写法2
let plugin2 = {
install: function (Vue, options) {
console.log(Vue, options)
},
}
Vue.use(plugin2, { a: 1, b: 2 })接受两个参数,参数一是Vue实例,参数二是传的数据参数,如下图所示:

其底层逻辑主要做了以下几步的处理:
- 判断缓存中是否有该插件,如果有则直接使用缓存中的插件
- 调用内部的
toArray方法把传递的参数从索引为 1 开始提取出来作为一个数组(即不要索引为 0 的Vue参数) - 在该数组中第一项添加
this - 判断写法,如果是函数写法直接调用,如果是
install写法则通过apply把this指向改为plugin - 添加到缓存的数组中
- 返回
源码在文件 src/core/global-api/use.js 文件中,代码如下所示:
export function initUse(Vue: GlobalAPI) {
Vue.use = function (plugin: Function | any) {
const installedPlugins =
this._installedPlugins || (this._installedPlugins = [])
if (installedPlugins.indexOf(plugin) > -1) {
return this
}
// additional parameters
const args = toArray(arguments, 1)
args.unshift(this)
if (isFunction(plugin.install)) {
plugin.install.apply(plugin, args)
} else if (isFunction(plugin)) {
plugin.apply(null, args)
}
installedPlugins.push(plugin)
return this
}
}
export function toArray(list: any, start?: number): Array<any> {
start = start || 0
let i = list.length - start
const ret: Array<any> = new Array(i)
while (i--) {
ret[i] = list[i + start]
}
return ret
}对于第一个参数要传Vue实例或许会有疑问,它是为了处理版本问题而存在的。以刚刚的案例为例,假设 plugin 的Vue版本是 1.4, 而 Vue.use 的版本是 2.4,此时版本不一致。
因此传Vue的目的就是将Vue的构造函数传递给插件中,让所有插件依赖的Vue是同一个版本。
总结一下:
use方法目的是将Vue的构造函数传递给插件中,让所有插件依赖的Vue是同一个版本- 默认调用插件或插件的
install方法 vuex和vue-router里package.json的依赖里面没有Vue,是通过参数传进去的
复习一下
源码中
this的指向是调用了use()方法的Vue构造实例。this的指向分以下几种:
- 对象.xxx() ,this 指代对象
- xxxx() ,this 指代全局
- new Xxx() ,this 指代的是实例或者构造函数的返回结果
- call、bind 改 this 指向
Vue 插槽
Vue 如何实现
Vue 何时使用
普通插槽
普通插槽的使用方式如下所示:
<div id="app">
<my>
<div>{{msg}}</div>
</my>
</div>
<script>
new Vue({
el: '#app',
data: {
msg: 'outer',
},
components: {
my: {
data() {
return {
msg: 'inner',
}
},
template: `<div class="my"><slot></slot></my>`,
},
},
})
</script>此时页面显示的字段是 outer 父组件的变量,可以总结出一点:普通插槽渲染数据采用的是父组件。
为什么会产生这种效果呢?下面我们从原理下手。引入第三方库 vue-template-compiler ,调用其 compile 方法查看 template 模板编译后的 render 长什么样。代码如下:
const templateCompiler. = require('vue-template-compiler')
let result = templateCompiler.compile(`<my>
<div>{{msg}}</div>
</my>`)
console.log(result)最终打印出来的 render 函数为 _c('my', [_c('div', [_v(_s(msg))])]) 。此代码会立即执行,因此会使用父组件的 data 内的变量。
接着拿到组件的孩子,也就是插槽,调用 Vnode 构造函数,代码如下:
new Vnode = {
'tag': 'my',
'componentOptions': {
children: {
tag: 'div', 'hello'
}
}
}注意
组件的孩子叫插槽,元素的孩子就是孩子
然后创建组件的真实节点,走组件的初始化流程,调用 resolveSlots() 方法,把最终返回结果保存到 vm.$slots 上。该方法的源码如下所示:
export function resolveSlots(
children: Array<VNode> | null | undefined,
context: Component | null
): { [key: string]: Array<VNode> } {
// 判断它有没有孩子
if (!children || !children.length) {
return {}
}
const slots: Record<string, any> = {}
// 循环所有孩子
for (let i = 0, l = children.length; i < l; i++) {
// 拿到每个孩子和其属性
const child = children[i]
const data = child.data
// remove slot attribute if the node is resolved as a vue slot node
if (data && data.attrs && data.attrs.slot) {
delete data.attrs.slot
}
// named slots should only be respected if the vnode was rendered in the. same context.
// 查看孩子的上下文
if (
(child.context === context || child.fnContext === context) &&
data &&
data.slot != null
) {
const name = data.slot
const slot = slots[name] || (slots[name] = [])
if (child.tag === 'template') {
slot.push.apply(slot, child.children || [])
} else {
slot.push(child)
}
} else {
;(slots.default || (slots.default = [])).push(child)
}
}
// ignore slots that contains only whitespace
// 循环遍历,查看当前插槽是否有名字
for (const name in slots) {
if (slots[name].every(isWhitespace)) {
delete slots[name]
}
}
return slots
}最终返回一个映射关系 this.$slots = {default: [儿子虚拟节点]} 。
在渲染组件真实节点的时候,会采用组件的模板 _t('default') ,源码如下,看看他具体做了什么:
// 传了名字 name 为 default
export function renderSlot(
name: string,
fallbackRender: ((() => Array<VNode>) | Array<VNode>) | null,
props: Record<string, any> | null,
bindObject: object | null
): Array<VNode> | null {
// 查找作用域插槽的名字,默认default
const scopedSlotFn = this.$scopedSlots[name]
let nodes
if (scopedSlotFn) {
// scoped slot
props = props || {}
if (bindObject) {
if (__DEV__ && !isObject(bindObject)) {
warn('slot v-bind without argument expects an Object', this)
}
props = extend(extend({}, bindObject), props)
}
nodes =
scopedSlotFn(props) ||
(isFunction(fallbackRender) ? fallbackRender() : fallbackRender)
} else {
nodes =
this.$slots[name] ||
(isFunction(fallbackRender) ? fallbackRender() : fallbackRender)
}
// 判断有没有slot,有则调用方法,没有直接返回node
const target = props && props.slot
if (target) {
return this.$createElement('template', { slot: target }, nodes)
} else {
return nodes
}
}总结下来,就是在渲染组件时关联插槽。渲染到插槽后,会转为 _t('default') 之类的 AST 树,再去到 this.$slots 对象内,通过名称 name 找到对应的变量,即儿子的虚拟节点。
总结一下
在解析组件时,会将组件的
children放到componentOptions上作为虚拟节点的属性。将
children取出来放到组件的vm.$options._renderChildren中。做出一个映射表放到
vm.$slots上,将结果放到vm.$scopedSlots上。vm.$scopedSlots = {a:fn(),b:fn,default:fn()}。渲染组件的时候会调用
_t方法。此时会去vm.$scopedSlots找到对应的函数来渲染内容。
具名插槽
把上方案例修改一下,代码如下所示:
<div id="app">
<my>
<div>
<p slot="a">{{msg}}</p>
<p slot="b">{{msg}}</p>
{{msg}}
</div>
</my>
</div>
<script>
new Vue({
el: '#app',
data: {
msg: 'outer',
},
components: {
my: {
data() {
return {
msg: 'inner',
}
},
template: `<div class="my"><slot name="a"></slot><slot name="b"></slot></my>`,
},
},
})
</script>解析 slot 时,会根据标签的 slot 属性来设置对应的对象键名,没有设置的则放到 default 中,如下图所示:

返回之后在 vm.$scopedSlots 对象内就能拿到对应的函数,如下图所示:

后续再去渲染的话就能找到对应的函数。
总结一下
与普通插槽操作一样,不同之处在于多了一个名称
name。
作用域插槽
总结一下
作用域插槽渲染的时候不会作为
children,将作用域插槽做成一个属性scopedSlots。制作一个映射关系
$scopedSlots = {default:fn:function({msg}) {return _c('div', {}, [_v(_s(msg))])}}。即把渲染逻辑放到一个函数中。稍后渲染组件模板的时候,会通过
name找到对应的函数,将数据传入到函数中,此时才渲染虚拟节点,用这个虚拟节点替换_t('default')。
普通插槽会在父组件内渲染;作用域插槽会在子组件内渲染。
Vue keep-alive 使用与原理
Vue 使用
下面看一个场景:
<div id="app">
<button @click="component = 'aa'">切换组件a</button>
<button @click="component = 'bb'">切换组件b</button>
<keep-alive>
<component :is="component"></component>
</keep-alive>
</div>
<script>
new Vue({
el: '#app',
data: {
component: 'a',
},
components: {
aa: {
template: `<div>a</div>`,
},
bb: {
template: `<div>b</div>`,
},
},
})
</script>Vue 原理
其源码如下所示:
export default {
name: 'keep-alive', // 组件名
abstract: true, // 抽象组件,即不会被记录到 $children 和 $parent 上
props: {
include: patternTypes, // 可以缓存哪些组件 ['a','b']
exclude: patternTypes, // 可以排除哪些组件 ['c']
max: [String, Number], // 最大缓存个数 6
},
methods: {
cacheVNode() {
const { cache, keys, vnodeToCache, keyToCache } = this
if (vnodeToCache) {
const { tag, componentInstance, componentOptions } = vnodeToCache
// 缓存中,放置需要缓存的组件。存放组件的实例
cache[keyToCache] = {
name: _getComponentName(componentOptions),
tag,
componentInstance, // 组件实例渲染过会有 $el 属性,下次可以直接复用 $el 属性
}
keys.push(keyToCache) // 放入key
// 超过最大长度会移出第一个
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
this.vnodeToCache = null
}
},
},
created() {
this.cache = Object.create(null) // 弄一个缓存区 {}
this.keys = [] // 缓存组件的名字有哪些 []
},
destroyed() {
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys)
}
},
mounted() {
// 缓存实例
this.cacheVNode()
this.$watch('include', (val) => {
pruneCache(this, (name) => matches(val, name))
})
this.$watch('exclude', (val) => {
pruneCache(this, (name) => !matches(val, name))
})
},
updated() {
this.cacheVNode()
},
// 走完created接下来走render,先渲染才能挂载
render() {
const slot = this.$slots.default // 取得是默认插槽
const vnode = getFirstComponentChild(slot) // 获取插槽中第一个虚拟节点
const componentOptions = vnode && vnode.componentOptions
if (componentOptions) {
// 该方法返回 opts.Ctor.options.name ,获取组件的名字
const name = _getComponentName(componentOptions)
// 校验是否需要缓存
const { include, exclude } = this
// 以下这些情况不复用
if (
// not included 不包含
(include && (!name || !matches(include, name))) ||
// excluded 排除的
(exclude && name && matches(exclude, name))
) {
return vnode
}
// cache是对象;keys是数组
const { cache, keys } = this
// 生成一个唯一的key
const key =
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]) {
// 获取缓存的实例
vnode.componentInstance = cache[key].componentInstance
// make current key freshest 把当前的key作为最新的(缓存策略)
remove(keys, key)
keys.push(key)
} else {
// 没缓存过的则缓存当前的vnode和key
this.vnodeToCache = vnode
this.keyToCache = key
}
// 给虚拟节点添加 data: keep-alive
vnode.data.keepAlive = true
}
// 最终返回vnode。vnode上有data.keepAlive 和componentInstance,说明组件是缓存过的
return vnode || (slot && slot[0])
},
}其中组件缓存的思路如下图所示:

有 a、b、c、d 四个组件,最大缓存数是 3:
- 先访问 a、b、c 三个组件,依次缓存,最久的是 a,最近一次的是 c
- 接下来访问 d 组件,最久的 a 被销毁,新增 d
- 然后再次访问 c,则原先的 c 销毁,往后新增 c,此时从旧到近依次为 b、d、c
- 再次访问 d,则原先的 d 销毁,后面新增 d
当一个组件被 keep-alive 包裹,说明该组件需要被缓存,则其 data 对象内会有一个 keepAlive 属性为 true 。此时 render 方法执行完毕。
接着走到 mounted 方法,缓存组件与其 $el 属性,如果缓存过,后续则直接获取对应的 $el 属性与 vnode 使用。
最后在创建组件时会判断,如果实例上有老的属性,说明该实例是可复用的,直接将缓存的 dom 元素插入,代码如下:
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)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}被 keep-alive 缓存过的组件有两个新的生命周期,源码如下:
insert(vnode: MountedComponentVNode) {
const { context, componentInstance } = vnode
if (!componentInstance._isMounted) {
componentInstance._isMounted = true
callHook(componentInstance, 'mounted')
}
if (vnode.data.keepAlive) {
if (context._isMounted) {
queueActivatedComponent(componentInstance)
} else {
activateChildComponent(componentInstance, true /* direct */)
}
}
},
destroy(vnode: MountedComponentVNode) {
const { componentInstance } = vnode
if (!componentInstance._isDestroyed) {
if (!vnode.data.keepAlive) {
// 之前是销毁组件
componentInstance.$destroy()
} else {
// 现在调用deactived钩子
deactivateChildComponent(componentInstance, true /* direct */)
}
}
}总结一下
keep-alive的原理是默认缓存加载过的组件对应的实例,内部采用了 LRU 算法。下次组件切换加载的时候,此时会找到对应缓存的节点来进行初始化,但是会采用上次缓存
$el来触发。(不需要虚拟节点转化成真实节点)更新和销毁会触发
actived和deactived。
Vue 自定义指令
源码如下:
function _update(oldVnode, vnode) {
const isCreate = oldVnode === emptyNode // 根据虚拟节点来判断是创建还是删除
const isDestroy = vnode === emptyNode
// 拿到老的指令和新的指令
const oldDirs = normalizeDirectives(
oldVnode.data.directives,
oldVnode.context
)
const newDirs = normalizeDirectives(vnode.data.directives, vnode.context)
// 将插入的钩子缓存,等待元素插入后调用
const dirsWithInsert: any[] = []
// 缓存componentUpdated
const dirsWithPostpatch: any[] = []
let key, oldDir, dir
for (key in newDirs) {
oldDir = oldDirs[key]
dir = newDirs[key]
if (!oldDir) {
// 如果没有老的指令,调用bind方法
callHook(dir, 'bind', vnode, oldVnode)
if (dir.def && dir.def.inserted) {
dirsWithInsert.push(dir) // 将instarted钩子存到队列中
}
} else {
// existing directive, update
dir.oldValue = oldDir.value
dir.oldArg = oldDir.arg
callHook(dir, 'update', vnode, oldVnode) // 调用更新
if (dir.def && dir.def.componentUpdated) {
dirsWithPostpatch.push(dir) // 缓存componentUpdated钩子
}
}
}
if (dirsWithInsert.length) {
const callInsert = () => {
for (let i = 0; i < dirsWithInsert.length; i++) {
callHook(dirsWithInsert[i], 'inserted', vnode, oldVnode)
}
}
if (isCreate) {
mergeVNodeHook(vnode, 'insert', callInsert)
} else {
callInsert()
}
}
if (dirsWithPostpatch.length) {
mergeVNodeHook(vnode, 'postpatch', () => {
for (let i = 0; i < dirsWithPostpatch.length; i++) {
callHook(dirsWithPostpatch[i], 'componentUpdated', vnode, oldVnode)
}
})
}
if (!isCreate) {
// 删除的话调用unbind方法
for (key in oldDirs) {
if (!newDirs[key]) {
// no longer present, unbind
callHook(oldDirs[key], 'unbind', oldVnode, oldVnode, isDestroy)
}
}
}
}总结一下
自定义指令就是用户定义好对应的钩子,当元素在不同的状态时会调用对应的钩子(所有钩子会被合并到
cbs对应的方法上,到时候依次调用)
Vue 事件修饰符
修饰符实现主要靠的是模板编译原理,如 .stop 、.prevent 等为 addEventListener( stop, defaultPrevent ) 等。编译的时候直接编译到事件内部中。
而 .passive 、.capture 、one 编译的时候会添加标识符。
键盘事件通过源码翻译如下图所示:

可以看出它在编译的时候做了处理,源码如下所示:
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],
}
// KeyboardEvent.key aliases
const keyNames: { [key: string]: string | Array<string> } = {
// #7880: IE11 and Edge use `Esc` for Escape key name.
esc: ['Esc', 'Escape'],
tab: 'Tab',
enter: 'Enter',
// #9112: IE11 uses `Spacebar` for Space key name.
space: [' ', 'Spacebar'],
// #7806: IE11 uses key names without `Arrow` prefix for arrow keys.
up: ['Up', 'ArrowUp'],
left: ['Left', 'ArrowLeft'],
right: ['Right', 'ArrowRight'],
down: ['Down', 'ArrowDown'],
// #9112: IE11 uses `Del` for Delete key name.
delete: ['Backspace', 'Delete', 'Del'],
}
// #4868: modifiers that prevent the execution of the listener
// need to explicitly return null so that we can determine whether to remove
// the listener for .once
const genGuard = (condition) => `if(${condition})return null;`
const modifierCode: { [key: string]: string } = {
stop: '$event.stopPropagation();',
prevent: '$event.preventDefault();',
self: genGuard(`$event.target !== $event.currentTarget`),
ctrl: genGuard(`!$event.ctrlKey`),
shift: genGuard(`!$event.shiftKey`),
alt: genGuard(`!$event.altKey`),
meta: genGuard(`!$event.metaKey`),
left: genGuard(`'button' in $event && $event.button !== 0`),
middle: genGuard(`'button' in $event && $event.button !== 1`),
right: genGuard(`'button' in $event && $event.button !== 2`),
}Vue 阅读方法
如果是初次阅读,则先了解其目录结构,知道哪个文件夹主要负责什么功能。
然后通过 debugger 打断点,了解 API 执行时走了哪些方法。