(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(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。
(1)实现原理
父组件提供数据,子组件通过props字段选择自己需要哪些内容,Vue.js内部通过子组件的props选项将需要的数据筛选出来之后添加到子组件的上下文中。
(2)Vue.js组件系统的运作原理
Vue.js中的所有组件都是Vue.js实例,组件在进行模板解析时,会将标签上的属性解析成数据,最终生成渲染函数。而渲染函数被执行时,会生成真实的DOM节点并渲染到视图中。但是这里有一个细节,如果某个节点是组件节点,也就是说模板中的、某个标签的名字是组件名,那么在虚拟DOM渲染的过程中会将子组件实例化,这会将模板解析时从标签属性上解析出的数据当作参数传递给子组件,其中就包含props数据。
(1)子组件被实例化时,会先对props进行规格化处理,规格化之后的props为对象格式。
(2)props可以通过数组指定需要哪些属性。但在Vue.js内部,数组格式的props将被规格化成对象格式。
(3)实现代码
function normalizeProps(options,vm){
const props = options.props;
if(!props) return;
const res = {};
let i,val,name;
if(Array.isArray(props)){
i = props.length;
while(i--){
val = props[i];
if(typeof val === 'string'){
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);
res[name] = isPlainObject(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是否有效时会用到。
初始化methods时,只需要循环选项中的methods对象,并将每个属性依次挂载到vm上即可。
function initMethods(vm,methods){
const props = vm.$options.props;
for(const key in methods){
if(process.env.NODE_ENV !=='production'){
if(methods[key]==null){
warn(
'Method "${key}" has an undefined value in the component definition.'+
'Did you reference the function correctly?',
vm
)
}
if(props && hasOwn(props,key)){
warn(
'Method "{key}" has already been defined as a prop.',
vm
)
}
if((key in vm)&&isReserved(key)){
warn(
'Method "${key}" conflicts with an existing Vue instance method.'+
'Avoid defining component that start with _or$.'
)
}
}
vm[key] = methods[key] === null ? noop : bind(methods[key],vm);
}
}
(1)原理
data中的数据最终会保存到vm._data中。然后在vm上设置一个代理,使得通过vm.x可以访问到vm._data中的x属性。最后由于这些数据并不是响应式数据,所以需要调用observe函数将data转换成响应式数据。于是,data就完成了初始化。
(2)代码
function initData(vm){
let data = vm.$options.data;
data = vm._data = typeof data === 'function'
? getData(data,vm)
: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
)
}
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'){
if(methods && hasOwn(methods,key)){
warn(
'Method "${key}" has already been defined as a data property.',
vm
)
}
}
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(vm,'_data',key);
}
}
observe(data,true/* asRootData */)
}
1、如果data中的某个key与methods发生了重复,依然会将data代理到实例中,但如果与props发生了重复,则不会将data代理到实例中。
(3)代码中调用了proxy函数实现代理功能。该函数的作用是在第一参数设置一个属性名为第三个参数的属性。这个属性的修改和获取操作实例上针对的是与第二个参数相同属性名的属性。
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);
}
通过这样的方法将vm._data中的方法代理到vm上。
(4)所有属性都代理后,执行observe函数将数据转换成响应式的。
(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