「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!」
声明
? 本文是开始学习 Vue
源码的第三篇笔记,当前的版本是 2.6.14
。如果对你有一点点帮助,请点赞鼓励一下,如果有错误或者遗漏,请在评论区指出,非常感谢各位大佬。
? 代码基本上是逐行注释,由于本人的能力有限,很多基础知识也进行了注释和讲解。由于源码过长,文章不会贴出完整代码,所以基本上都是贴出部分伪代码然后进行分析,建议在阅读时对照源码,效果更佳。
? 从本篇文章开始,可能会出现暂时看不懂的地方,是因为还没有学习前置知识,不必惊慌,只需知道存在这样一个知识点,接着向下看,看完了前置知识,回过头来再看这里就一目了然了。
前言
先回顾一下上文,我们知道了 Vue
的初始化过程,在 Vue.prototype._init
中我们分成四个部分进行分析,其中第三部分做了一系列的初始化,本文继续学习其中的一个初始化过程,响应式原理的核心部分 initState
。也就是 data
,props
,methods
,watch
,computed
的初始化过程。
initState
代码注释
/**
* @description: 初始化数据 响应式原理的入口
* @param {*} vm 实例Vm
*/
export function initState (vm: Component) {
// 为当前组件创建了一个watchers属性,为数组类型 vm._watchers保存着当前vue组件实例的所有监听者(watcher)
vm._watchers = []
// 从实例上获取配置项
const opts = vm.$options
//如果vm.$options上面定义了props 初始化props 对props配置做响应式处理
//代理props配置上的key到vue实例,支持this.propKey的方式访问
if (opts.props) initProps(vm, opts.props)
//如果vm.$options上面定义了methods 初始化methods ,props的优先级 高于methods的优先级
//代理methods配置上的key到vue实例,支持this.methodsKey的方式访问
if (opts.methods) initMethods(vm, opts.methods)
//如果vm.$options上面定义了data ,初始化data, 代理data中的属性到vue实例,支持通过 this.dataKey 的方式访问定义的属性
if (opts.data) {
initData(vm)
} else {
//这里是data为空时observe 函数观测一个空对象:{}
observe(vm._data = {}, true /* asRootData */)
}
//如果vm.$options上面定义了computed 初始化computed
//computed 是通过watcher来实现的,对每个computedKey实例化一个watcher,默认懒执行.
//将computedKey代理到vue实例上,支持通过this.computedKey的方式来访问computed.key
if (opts.computed) initComputed(vm, opts.computed)
//如果vm.$options上面定义了watch 初始化watch
if (opts.watch && opts.watch !== nativeWatch) {
// 判断组件有watch属性 并且没有nativeWatch( 兼容火狐)
initWatch(vm, opts.watch)
}
}
代码解读
⭐ 为当前组件创建了一个 watchers
属性,为数组类型 vm._watchers
保存着当前 vue
组件实例的所有监听者(watcher)
⭐ 从代码中可以看出,初始化的顺序是 props
-> methods
-> data
-> computed
-> watch
⭐ initProps 如果 vm.$options
上面定义了 props
初始化 props
对 props
配置做响应式处理,代理 props
配置上的 key
到 vue
实例,支持 this.propKey
的方式访问。
⭐ initMethods 如果 vm.$options
上面定义了 methods
初始化 methods
, props
的优先级 高于 methods
的优先级,代理 methods
配置上的 key
到 vue
实例 , 支持 this.methodsKey
的方式访问。
⭐ initData 如果 vm.$options
上面定义了 data
,初始化 data
, 代理 data
中的属性到 vue
实例,支持通过 this.dataKey
的方式访问定义的属性。data
为空时 observe
函数观测一个空对象。
⭐ initComputed 如果 vm.$options
上面定义了 computed
初始化 computed
。computed
是通过watcher
来实现的,对每个 computedKey
实例化一个 watcher
,默认懒执行。将 computedKey
代理到 vue
实例上,支持通过 this.computedKey
的方式来访问 computed.key
。
⭐ initWatch 判断组件有 watch
属性,并且没有 nativeWatch
( 兼容火狐)。如果 vm.$options
上面定义了 watch
初始化 watch
。
proxy
代码注释
// 代理对象
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
/**
* 代理 通过sharedPropertyDefinition对象 给key添加一层getter和setter 将key代理到 vue 实例上
* 当我们访问this.key的时候,实际上就会访问 vm._data.key / vm._props.key
* @param {*} target 实例vm
* @param {*} sourceKey _data / _props
* @param {*} key data / props 中的属性
*/
export function proxy (target: Object, sourceKey: string, key: string) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val
}
// 拦截对 this.key的访问
Object.defineProperty(target, key, sharedPropertyDefinition)
}
代码解读
⭐ 通过 sharedPropertyDefinition
对象 给 key
添加一层 getter
和 setter
将 key
代理到 vue
实例上,当我们访问 this.key
的时候,实际上就会访问 vm._data.key / vm._props.key
。
initProps
代码注释
/**
* @description: 初始化props
* @param {*} vm 实例vm
* @param {*} propsOptions 配置对象上的props
*/
function initProps (vm: Component, propsOptions: Object) {
// 存放父组件传入子组件的props
const propsData = vm.$options.propsData || {}
// 存放经过转换后的最终的props的对象, props 与 vm._props 保持同一个引用,初始值为 {}
const props = vm._props = {}
// 缓存 props 的每个 key,性能优化, 一个存放props的key的数组,就算props的值是空的,key也会存在里面 ,keys 与 vm.$options._propKeys 保持同一个引用,初始值为 {}
const keys = vm.$options._propKeys = []
// 判断是不是根元素
const isRoot = !vm.$parent
//当组件不是根组件时,使用 toggleObserving(false) 取消对 Object Array 类型 Prop 深度观测,为什么这么做呢,因为 Object Array 在父组件中已经被深度观测过了。
if (!isRoot) {
toggleObserving(false)
}
// 遍历props配置对象
for (const key in propsOptions) {
// 向缓存键值数组中添加键名
keys.push(key)
/**
* 用validateProp校验是否为预期的类型值,然后返回相应 prop 值(或default值)
* 如果有定义类型检查,布尔值没有默认值时会被赋予false,字符串默认undefined
*/
const value = validateProp(key, propsOptions, propsData, vm)
//非生产环境
if (process.env.NODE_ENV !== 'production') {
// 进行键名的转换,将驼峰式转换成连字符式的键名
const hyphenatedKey = hyphenate(key)
// 校验prop是否为内置的属性, 内置属性:key,ref,slot,slot-scope,is
if (isReservedAttribute(hyphenatedKey) ||
config.isReservedAttr(hyphenatedKey)) {
warn(
`"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
vm
)
}
// 对属性建立观察,并在直接使用props属性时给予警告
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 {
// 生产环境下直接对属性进行存取器包装,建立依赖观察, 为 props 的每个 key 设置数据响应式
defineReactive(props, key, value)
}
// 当实例上没有同名属性时,对属性进行代理操作,将对键名的引用指向vm._props对象中
if (!(key in vm)) {
// 代理 key 到 vm 对象上
proxy(vm, `_props`, key)
}
}
// 开启观察状态标识, 重新打开观测开关,避免影响后续代码执行
toggleObserving(true)
}
代码解读
⭐ 初始化变量 propsData
存放父组件传入子组件的 props
。const props = vm._props = { }
存放经过转换后的最终的 props
的对象 , props
与 vm._props
保持同一个引用,初始值为 {}
。
const keys = vm.$options._propKeys = []
, keys
与 vm.$options._propKeys
保持同一个引用,初始值为 []
。isRoot
判断是不是根元素。
⭐ 当组件不是根组件时,使用 toggleObserving(false)
取消对 Object
Array
类型 Prop
深度观测。
⭐ 遍历 props
配置对象。缓存 props
的每个 key
,用以性能优化 。
⭐ 校验是否为预期的类型值,然后返回相应 prop
值(或 default
值),如果有定义类型检查,布尔值没有默认值时会被赋予 false
,字符串默认 undefined
。
⭐ defineReactive
,对属性建立观察。
⭐ 当实例上没有同名属性时,对属性进行代理操作 , 将对键名的引用指向 vm._props
对象中。
⭐ 开启观察状态标识,重新打开观测开关,避免影响后续代码执行 toggleObserving(true)
。
⭐ 本文对 initProps
掌握到这里即可,后面会详细分析 defineReactive
方法。
initMethods
代码注释
/**
* @description: 初始化methods
* @param {*} vm 实例vm
* @param {*} methods 实例配置项上面的methods vm.$options.methods
*/
function initMethods (vm: Component, methods: Object) {
// 获取实例配置上的props
const props = vm.$options.props
// 做一些检查 然后赋值给Vue实例
for (const key in methods) {
// 判断环境 只在非生产环境下起作用
if (process.env.NODE_ENV !== 'production') {
// 判断key是否是function类型
if (typeof methods[key] !== 'function') {
warn(
`Method "${key}" has type "${typeof methods[key]}" in the component definition. ` +
`Did you reference the function correctly?`,
vm
)
}
// 检测 methods 中的属性名是否与 props 冲突,由 initState 方法我们知道,props 是先与 methods 初始化的。
if (props && hasOwn(props, key)) {
warn(
`Method "${key}" has already been defined as a prop.`,
vm
)
}
// 检测 methods 是否使用了关键字保留字, 而且不允许以$ 或者 _ 开头。
if ((key in vm) && isReserved(key)) {
warn(
`Method "${key}" conflicts with an existing Vue instance method. ` +
`Avoid defining component methods that start with _ or $.`
)
}
}
/**
* 将 methods 中的所有方法赋值到 vue 实例上 ,支持通过 this.methodsKey 的方式访问定义的方法
* 如果 key 不是一个函数 则赋值为空函数
* 如果 key 是函数 则执行bind()函数
*/
vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm)
}
}
代码解读
⭐ 判断属性是否是 function
类型,检测 methods
中的属性名是否与 props
冲突,由 initState
方法我们知道,props
是先于 methods
初始化的。检测 methods
是否使用了关键字保留字,而且不允许以 $
或者 _
开头。
⭐ 将 methods
中的所有方法赋值到 vue
实例上 , 支持通过 this.methodsKey
的方式访问定义的方法。
initData
代码注释
/**
* @description: 初始化data
* @param {*} vm 实例vm
*/
function initData (vm: Component) {
//从vm.$options.data里面拿到data,就是我们在开发时候定义的data 赋值给data 还有vm._data
let data = vm.$options.data
/**
* 判断data是不是一个function 保证后续处理的data是一个对象
* 如果是 执行getData方法
* 如果不是 返回 data || {}
*/
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
//如果不是个对象的话,开发环境下会报一个警告
if (!isPlainObject(data)) {
//把data重置为一个空对象
data = {}
process.env.NODE_ENV !== 'production' && warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
)
}
//拿到data对象的key 组成一个数组
const keys = Object.keys(data)
//拿到props
const props = vm.$options.props
//拿到methods
const methods = vm.$options.methods
/**
* 循环判断data中的属性和props,methods中的属性是否冲突
* 因为所有的data,props,methods最终都会挂载到vm实例上
*/
let i = keys.length
while (i--) {
const key = keys[i]
//非生产环境
if (process.env.NODE_ENV !== 'production') {
//与methods判重
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
//与props判重
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== 'production' && warn(
`The data property "${key}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else if (!isReserved(key)) {
//判重通过,最终交给proxy做代理 ,代理data中的属性到vue实例,支持通过 this.dataKey 的方式访问定义的属性
proxy(vm, `_data`, key)
}
}
// 对data进行响应式处理
observe(data, true /* asRootData */)
}
//如果data是一个函数 那么会走这个方法
export function getData (data: Function, vm: Component): any {
// 收集依赖
pushTarget()
try {
// 调用call 返回的值就是这个对象
return data.call(vm, vm)
} catch (e) {
handleError(e, vm, `data()`)
return {}
} finally {
// 释放依赖
popTarget()
}
}
代码解读
⭐ data
为空,直接观测一个空对象 observe(vm._data = {} , true)
⭐ data
不为空,判断 data
是不是一个 function
,保证后续处理的 data
是一个对象。
⭐ 循环判断 data
中的属性和 props
, methods
中的属性是否冲突,由 initState
方法我们知道,props
,methods
是先于 methods
初始化的。
⭐ 对 data
进行响应式处理 observe(data , true)
⭐ 本文对 initData
掌握到这里即可,后面会详细分析 observe
方法。
initComputed
代码注释
//用于传入Watcher实例的一个对象 懒执行
const computedWatcherOptions = { lazy: true }
/**
* @description: 初始化computed
* @param {*} vm 实例vm
* @param {*} computed 定义的computed配置
*/
function initComputed (vm: Component, computed: Object) {
// 声明变量 watchers,与 vm._computedWatchers 保持同一个引用,并且初始化值为空对象。
const watchers = vm._computedWatchers = Object.create(null)
// 声明变量isSSR,判断是不是 ssr(服务端渲染)
const isSSR = isServerRendering()
// 遍历 computed 配置对象
for (const key in computed) {
// 获取 key 当次遍历对应的值.
const userDef = computed[key]
/**
* 使用过 computed 都知道,它有两种写法 函数写法以及对象写法
* computed: {
compA: function() { return this.a + 1 },
compB: {
get: function() { return this.b + 1 },
}
}
* 判断是不是函数,如果是函数 getter 就是函数本身,如果是对象,getter就用他的get属性
*/
const getter = typeof userDef === 'function' ? userDef : userDef.get
// 非开发环境下getter如果为null,警告
if (process.env.NODE_ENV !== 'production' && getter == null) {
warn(
`Getter is missing for computed property "${key}".`,
vm
)
}
// 如果不是SSR
if (!isSSR) {
/**
* 针对当次循环的 computed,实例化一个 Watcher , 所以computed其实就是通过Watcher来实现的
* watchers 保存了 vm._computedWatchers 的引用,所以这里同样会将该 watcher 保存到 vm._computedWatchers。
* 每一个 computed 的 key,都会生成一个 watcher 实例,并且保存到 vm._computedWatchers 这个对象上。
*/
watchers[key] = new Watcher(
vm, //实例vm
getter || noop, // getter
noop, // 空函数
computedWatcherOptions // 配置对象 懒执行(不可更改)
)
}
//if 语句用来检测 computed 的命名是否与 data,props 冲突,在非生产环境将会打印警告信息。
if (!(key in vm)) {
//不冲突时,调用 defineComputed 方法。
defineComputed(vm, key, userDef)
} else if (process.env.NODE_ENV !== 'production') {
if (key in vm.$data) {
//与data中的属性冲突
warn(`The computed property "${key}" is already defined in data.`, vm)
} else if (vm.$options.props && key in vm.$options.props) {
//与props中的属性冲突
warn(`The computed property "${key}" is already defined as a prop.`, vm)
} else if (vm.$options.methods && key in vm.$options.methods) {
//与methods中的属性冲突
warn(`The computed property "${key}" is already defined as a method.`, vm)
}
}
}
}
/**
* @description: 为 sharedPropertyDefinition 添加 get, set 属性,将该 computed 属性添加到 Vue 实例 vm 上,并使用 sharedPropertyDefinition 作为设置项。
* @param {*} target vm实例
* @param {*} key 当次循环的computedKey
* @param {*} userDef computed.key
*/
export function defineComputed (
target: any,
key: string,
userDef: Object | Function
) {
//
const shouldCache = !isServerRendering()
if (typeof userDef === 'function') {
// 如果computed.key是function类型走这里
//设置sharedPropertyDefinition配置对象的get方法
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: createGetterInvoker(userDef)
//设置sharedPropertyDefinition配置对象的set方法
sharedPropertyDefinition.set = noop
} else {
//如果computed.key不是function类型走这里
//设置sharedPropertyDefinition配置对象的get方法
sharedPropertyDefinition.get = userDef.get
? shouldCache && userDef.cache !== false
? createComputedGetter(key)
: createGetterInvoker(userDef.get)
: noop
//设置sharedPropertyDefinition配置对象的get方法
sharedPropertyDefinition.set = userDef.set || noop
}
//如果是非生产环境 并且sharedPropertyDefinition的set方法是noop
if (process.env.NODE_ENV !== 'production' &&
sharedPropertyDefinition.set === noop) {
//将sharedPropertyDefinition的set方法设置为警告
sharedPropertyDefinition.set = function () {
warn(
`Computed property "${key}" was assigned to but it has no setter.`,
this
)
}
}
//将computed配置项中的key,代理到vue实例上,支持通过this.computedKey的方式去访问 computed中的属性
Object.defineProperty(target, key, sharedPropertyDefinition)
}
/**
* @description: 在这里我们暂时只需要知道sharedPropertyDefinition的 get属性 被设置为这个方法的返回值就行
* @param {*} key computedKey
* @return {*} computedGetter
*/
function createComputedGetter (key) {
return function computedGetter () {
//拿到watcher
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
//执行watcher.evaluate方法
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
/**
* @description: 在这里我们暂时只需要知道sharedPropertyDefinition的 get属性 被设置为这个方法的返回值就行
* @param {*} fn userDef.get
* @return {*} computedGetter
*/
function createGetterInvoker(fn) {
return function computedGetter () {
return fn.call(this, this)
}
}
代码解读
⭐ 声明变量 watchers
,与 vm._computedWatchers
保持同一个引用,并且初始化值为空对象。
⭐ 声明变量 isSSR
, 判断是不是 ssr
(服务端渲染)。
⭐ 遍历 computed
配置对象,声明 userDef
变量存放当次遍历 key
对应的值 。 声明 getter
变量, 判断 userDef
是不是函数 , 如果是函数 getter
就是函数本身 , 如果是对象 getter
就用他的 get
属性 。非生产环境下 getter
如果为 null
, 发出警告。如果不是 SSR
,针对当次循环的 computed
,实例化一个 Watcher
。watchers
保存了 vm._computedWatchers
的引用,所以这里同样会将该 watcher
保存到 vm._computedWatchers
。每一个 computed
的 key
,都会生成一个 watcher
实例,并且保存到 vm._computedWatchers
这个对象上。检测 computed
的命名是否与 data
,props
冲突,在非生产环境将会打印警告信息。不冲突时,调用 defineComputed
方法。
⭐ 本文对 initComputed
掌握到这里即可,后面会详细分析 defineComputed
方法。
initWatch
代码注释
/**
* @description: 初始化watch
* @param {*} vm 实例vm
* @param {*} watch watch配置项 / vm.$options.watch
*/
function initWatch (vm: Component, watch: Object) {
//遍历watch配置项 从这可以看出 key 和 watcher 实例可能是 一对多 的关系
for (const key in watch) {
//获取当次遍历 key 对应的值
const handler = watch[key]
//如果是数组的话
if (Array.isArray(handler)) {
//循环数组 为数组的每一项调用createWatcher方法
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
// 如果不是数组 直接调用createWatcher方法
createWatcher(vm, key, handler)
}
}
}
/**
* @description: 兼容性处理,保证 handler 肯定是一个函数,调用 $watch
* @param {*} vm 实例vm
* @param {*} expOrFn watchKey
* @param {*} handler watch.key
* @param {*} options 配置选项
*/
function createWatcher (
vm: Component,
expOrFn: string | Function,
handler: any,
options?: Object
) {
//如果是对象 从 handler 属性中获取函数
if (isPlainObject(handler)) {
options = handler
handler = handler.handler
}
//如果是字符串 表示的是一个methods方法,直接通过 this.methodsKey的方式 拿到这个函数
if (typeof handler === 'string') {
handler = vm[handler]
}
//调用vm.$watch方法
return vm.$watch(expOrFn, handler, options)
}
代码解读
⭐ 遍历 watch
配置项 ,获取当次遍历 key
对应的值,如果是数组的话,循环数组,为数组的每一项调用 createWatcher
方法,如果不是数组,直接调用 createWatcher
方法。
⭐ 从这可以看出 key
和 watcher
实例可能是 一对多 的关系。
⭐ 本文对 initWatch
掌握到这里即可,后面会详细分析 createWatcher
方法。
总结
最后我们用一张思维导图总结一下
参考
Vue.js 技术揭秘
精通 Vue 技术栈的源码原理
本文由 李永宁 教程结合自己的想法整理而来,在此特别感谢前辈。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!