一、初始化状态
(1)当我们使用Vue.js开发应用时,经常会使用一些状态,例如props、methods、data、computed和watch。在Vue.js内部,这些玩状态在使用之前需要进行初始化。
(2)initState函数
export function initState(vm){
vm._watchers = [];
const opts = vm.$options;
if(opts.props) initProps(vm,opts.props);
if(opts.methods) initMethods(vm,opts.methods);
if(opts.data){
initData(vm);
}else{
<!-- 不存在,则直接使用observe函数观察空对象 -->
<!-- observe函数的作用是将数据转换为响应式的 -->
observe(vm._data = {},true/*asRootData*/);
}
if(opts.computed) initComputed(vm,opts.computed);
if(opts.watch && opts.watch !== nativeWatch){
initWatch(vm,opts.watch);
}
}
1、首先在vm上新增一个属性_watchers,用来保存当前组件中所有的watcher实例。无论是使用vm.$watch注册的watcher还是使用watch选项添加的watcher实例,都会添加到vm._watchers中。
2、可以通过vm._watchers得到当前Vue.js实例中所注册的所有watcher实例,并将它们一次卸载。
3、用户在实例化Vue.js时使用了哪些状态,哪些状态就需要被初始化,没有用到的状态则不用初始化。例如,用户只使用了data,那么只需要初始化data即可。
4、初始化的顺序其实是精心安排的。先初始化props,后初始化data,这样就可以在data中使用props中的数据了。在watch中既可以观察props,也可以观察data,因为它是最后被初始化的。
5、初始化状态可以分为5个子项,分别是初始化props、初始化methods、初始化data、初始化computed和初始化watch。
二、初始化props
(1)实现原理
父组件提供数据,子组件通过props字段选择自己需要哪些内容,Vue.js内部通过子组件的props选项将需要的数据筛选出来之后添加到子组件的上下文中。
(2)Vue.js组件系统的运作原理
Vue.js中的所有组件都是Vue.js实例,组件在进行模板解析时,会将标签上的属性解析成数据,最终生成渲染函数。而渲染函数被执行时,会生成真实的DOM节点并渲染到视图中。但是这里有一个细节,如果某个节点是组件节点,也就是说模板中的、某个标签的名字是组件名,那么在虚拟DOM渲染的过程中会将子组件实例化,这会将模板解析时从标签属性上解析出的数据当作参数传递给子组件,其中就包含props数据。
-
规格化props
(1)子组件被实例化时,会先对props进行规格化处理,规格化之后的props为对象格式。
(2)props可以通过数组指定需要哪些属性。但在Vue.js内部,数组格式的props将被规格化成对象格式。
(3)实现代码
function normalizeProps(options,vm){
const props = options.props;
<!-- 判断是否有props属性,如果没有,说明用户没有使用props接收任何数据,那么不需要规格化,直接使用return语句退出即可。 -->
if(!props) return;
<!-- 声明了一个变量res,用来保存规格化后的结果 -->
const res = {};
let i,val,name;
<!-- 检查props是否是数组 -->
if(Array.isArray(props)){
<!-- 将Array类型的props规格化成Object类型 -->
i = props.length;
while(i--){
val = props[i];
<!-- 判断props名称的类型是否是String -->
if(typeof val === 'string'){
<!-- 将props名称驼峰化 -->
name = camelize(val);
res[name] = { type:null };
<!-- 如果不是在非生产环境下在控制台打印警告 -->
}else if(process.env.NODE_ENV !=='production'){
warn('props must be strings when using array syntax');
}
}
<!-- 检查是否为对象类型 -->
}else if(isPlainObject(props)){
for(const key in props){
val = props[key];
name = camelize(key);
<!-- 判断val是否是Object类型 -->
res[name] = isPlainObject(val)
<!-- 是则在res上设置key为名的属性 -->
? val
<!-- 不是则说明val的值可能是基础的类型函数或者是一个数组提供多个可能的类型 -->
:{type:val}
}
<!-- 不是数组或对象,在非生产环境下在控制台中打印警告 -->
}else if(process.env.NODE_ENV !=='production'){
warn(
'Invaild value for option "props" : expected an Array or an Object,'+
'but got ${toRawType(props)}.',
vm
)
}
options.props = res;
}
1、如果props的类型不是Array而是Object,那么根据props的语法可以得知,props对象中的值可以是一个基础的类型函数
{
propA:Number
}
也可能是一个数组
{
propB:[String,Number]
}
还可能是一个对象类型的高级选项
{
propC:{
type:String,
required:true
}
}
2、使用for...in语句循环props。
3、规格化之后的props的类型既可能是基础的类型函数,也有可能是数组。这在后面断言props是否有效时会用到。
-
初始化props
三、初始化methods
初始化methods时,只需要循环选项中的methods对象,并将每个属性依次挂载到vm上即可。
function initMethods(vm,methods){
<!-- props用来判断methods中的方法是否和props发生了冲突 -->
const props = vm.$options.props;
for(const key in methods){
<!-- 1、校验方法是否合法 -->
<!-- 在非生产环境下需要校验methods并在控制台发出警告 -->
if(process.env.NODE_ENV !=='production'){
<!-- 只有key没有value -->
if(methods[key]==null){
warn(
'Method "${key}" has an undefined value in the component definition.'+
'Did you reference the function correctly?',
vm
)
}
<!-- methods中的某个方法已经在props中声明过了 -->
if(props && hasOwn(props,key)){
warn(
'Method "{key}" has already been defined as a prop.',
vm
)
}
<!-- methods中的某个方法已经存在于vm中,并且方法名是以$或_开头的 -->
<!-- isReserved作用是判断字符串是否是以$或_开头 -->
if((key in vm)&&isReserved(key)){
warn(
'Method "${key}" conflicts with an existing Vue instance method.'+
'Avoid defining component that start with _or$.'
)
}
}
<!-- 2、将方法挂载到vm中 -->
<!-- 判断方法(methods[key])是否存在,如果不存在,则将noop赋值到vm[key]中,如果存在,则将该方法通过bind改写它的this后,再赋值到vm[key]中 -->
<!-- 这样就可以通过vm.x访问到methods中的x方法了 -->
vm[key] = methods[key] === null ? noop : bind(methods[key],vm);
}
}
四、初始化data
(1)原理
data中的数据最终会保存到vm._data中。然后在vm上设置一个代理,使得通过vm.x可以访问到vm._data中的x属性。最后由于这些数据并不是响应式数据,所以需要调用observe函数将data转换成响应式数据。于是,data就完成了初始化。
(2)代码
function initData(vm){
let data = vm.$options.data;
<!-- 判断data的类型,如果是函数,则需要执行函数并将返回值赋值给变量data和vm._data -->
<!-- getData中的逻辑也是调用data函数并将值返回,只不过getData中有一些细节处理,比如使用try...catch...语句捕获data函数中可能发生的错误等 -->
data = vm._data = typeof data === 'function'
? getData(data,vm)
:data || {}
<!-- 最终得到的data值应该是object类型,否则就在非生产环境下在控制台打印出警告,并为data设置默认值,也就是空对象 -->
if(!isPlainObject(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代理到Vue.js实例上 -->
const keys = Object.keys(data);
const props = vm.$options.props;
const methods = vm.$options.methods;
let i = keys.length;
while(i--){
const key = keys[i];
<!-- 非生产环境 -->
if(process.env.NODE_ENV !=== 'production'){
<!-- 判断当前循环的key是否存在于methods中,如果存在,则说明数据重复了,在控制台打印警告 -->
if(methods && hasOwn(methods,key)){
warn(
'Method "${key}" has already been defined as a data property.',
vm
)
}
}
<!-- 判断props中是否存在某个属性与key相同,若相同,在控制台打印警告 -->
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
)
<!-- 只有props中不存在当前与key相同的属性且属性名不以$或_开头,才会将属性代理到实例上 -->
}else if(!isReserved(key)){
proxy(vm,'_data',key);
}
}
<!-- 观察数据 -->
<!-- 执行observe函数将数据转换成响应式的 -->
observe(data,true/* asRootData */)
}
1、如果data中的某个key与methods发生了重复,依然会将data代理到实例中,但如果与props发生了重复,则不会将data代理到实例中。
(3)代码中调用了proxy函数实现代理功能。该函数的作用是在第一参数设置一个属性名为第三个参数的属性。这个属性的修改和获取操作实例上针对的是与第二个参数相同属性名的属性。
<!-- sharedPropertyDefinition默认属性描述符 -->
const sharedPropertyDefinition = {
enumerable : true,
configurable : true,
get:hoop,
set:hoop
}
export function proxy(target,sourceKey,key){
sharedPropertyDefinition.get = function proxyGetter(){
return this[sourceKey][key];
}
sharedPropertyDefinition.set = function proxySetter(val){
this[sourceKey][key] = val;
}
<!-- 使用Object.defineProperty方法为target定义一个属性,属性名为key,属性描述符为sharedPropertyDefinition默认属性描述符 -->
Object.defineProperty(target,key,sharedPropertyDefinition);
}
通过这样的方法将vm._data中的方法代理到vm上。
(4)所有属性都代理后,执行observe函数将数据转换成响应式的。
五、初始化computed
(1)computed是定义在vm上的一个特殊的getter方法。之所以说特殊,是因为在vm上定义getter方法时,get并不是用户提供的函数,而是Vue.js内部的一个代理函数。在代理函数中可以结合Watcher实现缓存与收集依赖等功能。
(2)计算属性的结果会被缓存,且只有在计算属性所依赖的响应式属性或者说计算属性的返回值发生变化时才会重新计算。
(3)如何知道计算属性的返回值是否发生了变化?
其实是结合Watcher的dirty属性来分辨的:当dirty属性为true时,说明需要重新计算“计算属性”的返回值;当dirty属性为false时,说明计算属性的值并没有变,不需要重新计算。
(4)当计算属性中的内容发生变化后,计算属性的Watcher与组件的Watcher都会得到通知。
(5)计算属性的Watcher会将自己的dirty属性设置为true,当下一次读取计算属性时,就会重新计算一次值。
(6)然后组件的Watcher也会得到通知,从而执行render函数进行重新渲染的操作。
(7)由于要重新执行render函数,所有会重新读取计算属性的值,这时候计算属性的Watcher已经把自己的dirty属性设置为true,所以会重新计算一次计算属性的值,用于本次渲染。
(8)简单来说,计算属性会通过Watcher来观察它所用到的所有属性的变化,当这些属性发生变化时,计算属性会将自身的Watcher的dirty属性数值为true,说明自身的返回值变了。
(9)在模板中使用了一个数据渲染视图时,如果这个数据恰好是计算属性,那么读取数据这个操作其实会触发计算属性的getter方法(初始化计算属性时在vm上设置的getter方法)。
六、 初始化watch