【源码学习】Vue中的props、methods、data、computed、watch初始化的顺序
声明
🔊 本文是开始学习 Vue 源码的第三篇笔记,当前的版本是 2.6.14 。
🔊 代码基本上是逐行注释,由于本人的能力有限,很多基础知识也进行了注释和讲解。由于源码过长,文章不会贴出完整代码,所以基本上都是贴出部分伪代码然后进行分析,建议在阅读时对照源码,效果更佳。
🔊 从本篇文章开始,可能会出现暂时看不懂的地方,是因为还没有学习前置知识,不必惊慌,只需知道存在这样一个知识点,接着向下看,看完了前置知识,回过头来再看这里就一目了然了。
本文代码所在路径:\vue-dev\src\core\instance\state.js
前言
先回顾一下上文,我们知道了 Vue 的初始化过程,在 Vue.prototype._init 中我们分成四个部分进行分析,其中第三部分做了一系列的初始化,本文继续学习其中的一个初始化过程,响应式原理的核心部分 initState 。也就是props、methods、data、computed、watch 的初始化过程。
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 方法。
总结
最后我们用一张思维导图总结一下
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· DeepSeek 开源周回顾「GitHub 热点速览」