详解vue中的数据初始化(initState)


本文转自PHP中文网,链接如下:详解vue中的数据初始化(initState)

数据初始化

Vue 实例在建立的时候会运行一系列的初始化操作,而在这些初始化操作里面,和数据绑定关联最大的是 initState

首先,来看一下他的代码:

function initState(vm) {
    vm._watchers = [];
    var opts = vm.$options;
    if(opts.props) {
        initProps(vm, opts.props); //初始化props
    }
    if(opts.methods) {
        initMethods(vm, opts.methods); //初始化methods
    }
    if(opts.data) {
        initData(vm); //初始化data
    } else {
        observe(vm._data = {}, true /* asRootData */ );
    }
    if(opts.computed) {
        initComputed(vm, opts.computed); //初始化computed
    }
    if(opts.watch && opts.watch !== nativeWatch) {
        initWatch(vm, opts.watch); //初始化watch
    }
}

在这么多的数据的初始化中, propsmethodsdata 是比较简单的,而 computedwatch 则相对较难,逻辑较复杂,所以我下面主要讲下 computedwatch (以下代码部分为简化后的)。

initState 里面主要是对vue实例中的 props , methods , data , computedwatch 数据进行初始化。

1.props初始化–initProps

在初始化 props 的时候( initProps ),会遍历 props 中的每个属性,然后进行类型验证,数据监测等(提供为 props 属性赋值就抛出警告的钩子函数)。

2.methods初始化–initMethods

在初始化 methods 的时候( initMethods ),主要是监测 methods 中的方法名是否合法。

3.data初始化–initData

在初始化 data 的时候( initData ),会运行observe函数深度遍历数据中的每一个属性,进行数据劫持。

4.computed初始化–initComputed

在初始化 computed 的时候( initComputed ),会监测数据是否已经存在 data 或 props 上,如果存在则抛出警告,否则调用defineComputed函数 ,监听数据,为组件中的属性绑定 getter 及 setter 。如果 computed 中属性的值是一个函数,则默认为属性的getter函数。此外属性的值还可以是一个对象,他只有三个有效字段 set 、 get 和 cache,分别表示属性的 setter 、 getter 和是否启用缓存,其中 get 是必须的,cache默认为true。

//代码选取部分
function initComputed(vm, computed) {
    var watchers = vm._computedWatchers = Object.create(null);
    for(var key in computed) {
        var userDef = computed[key];
        var getter = typeof userDef === 'function' ? userDef : userDef.get;
        //创建一个计算属性 watcher
        watchers[key] = new Watcher(
            vm,
            getter || noop,
            noop,
            computedWatcherOptions
        );

        if(!(key in vm)) {
            //如果定义的计算属性不在组件实例上,对属性进行数据劫持
            //defineComputed 很重要,下面我们再说
            defineComputed(vm, key, userDef);
        } else {
            //如果定义的计算属性在data和props有,抛出警告
        }
    }
}

5.methods初始化–initMethods

在初始化 watch 的时候( initWatch ),会调用vm.$watch函数为 watch 中的属性绑定 setter回调(如果组件中没有该属性则不能成功监听属性必须存在于 props 、 data 或 computed中)。如果 watch 中属性的值是一个函数,则默认为属性的setter回调函数,如果属性的值是一个数组 ,则遍历数组中的内容,分别为属性绑定回调,此外属性的值还可以是一个对象。此时,对象中的 handler 字段代表 setter回调函数,immediate代表是否立即先去执行里面的 handler方法,deep代表是否深度监听

vm.$watch 函数会直接使用 Watcher 构建观察者对象。 watch 中属性的值作为 watcher.cb 存在,在观察者 update 的时候,在 watcher.run函数中执行。

//代码选取部分
function initWatch(vm, watch) {
    //遍历watch,为每一个属性创建侦听器
    for(var key in watch) {
        var handler = watch[key];
        //如果属性值是一个数组,则遍历数组,为属性创建多个侦听器
        //createWatcher函数中封装了vm.$watch,会在vm.$watch中创建侦听器
        if(Array.isArray(handler)) {
            for(var i = 0; i < handler.length; i++) {
                createWatcher(vm, key, handler[i]);
            }
        } else {
            //为属性创建侦听器
            createWatcher(vm, key, handler);
        }
    }
}

function createWatcher(vm, expOrFn, handler, options) {
    //如果属性值是一个对象,则取对象的handler属性作为回调
    if(isPlainObject(handler)) {
        options = handler;
        handler = handler.handler;
    }
    //如果属性值是一个字符串,则从组件实例上寻找
    if(typeof handler === 'string') {
        handler = vm[handler];
    }
    //为属性创建侦听器
    return vm.$watch(expOrFn, handler, options)
}

6.computed属性

computed 本质是一个惰性求值的观察者,具有缓存性,只有当依赖变化后,第一次访问 computed 属性,才会计算新的值。

下面将围绕这一句话来做解释。

上面代码中提到过,当计算属性中的数据存在与 data 和 props 中时,会被警告,也就是这种做法是错误的。所以一般的,我们都会直接在计算属性中声明数据。还是那个代码片段中,如果定义的计算属性不在组件实例上,会运行 defineComputed函数 对数据进行数据劫持。

下面我们来看下 defineComputed函数 中做了什么。

//代码选取部分
function defineComputed(target, key, userDef) {
    //是不是服务端渲染
    var shouldCache = !isServerRendering();
    //如果我们把计算属性的值写成一个函数,这时函数默认为计算属性的get
    if(typeof userDef === 'function') {
        sharedPropertyDefinition.get = shouldCache ?
        //如果不是服务端渲染,则默认使用缓存,设置get为createComputedGetter创建的缓存函数
        createComputedGetter(key) :
        //否则不使用缓存,直接设置get为userDef这个我们定义的函数
        createGetterInvoker(userDef);
        //设置set为空函数
        sharedPropertyDefinition.set = noop;
    } else {
        //如果我们把计算属性的值写成一个对象,对象中可能包含set、get和cache三个字段
        sharedPropertyDefinition.get = userDef.get ?
        shouldCache && userDef.cache !== false ?
        //如果我们传入了get字段,且不是服务端渲染,且cache不为false,
        //设置get为createComputedGetter创建的缓存函数
        createComputedGetter(key) : 
        //如果我们传入了get字段,但是是服务端渲染或者cache设为了false,设置get为userDef这个我们定义的函数
        createGetterInvoker(userDef.get)  :
        //如果没有传入get字段,设置get为空函数
        noop;
        //设置set为我们传入的传入set字段或空函数
        sharedPropertyDefinition.set = userDef.set || noop;
    }
    //虽然这里可以get、set都可以设置为空函数
    //但是在项目中,get为空函数对数据取值会报错,set为空函数对数据赋值会报错
    //而computed主要作用就是计算取值的,所以get字段是必须的
    //数据劫持
    Object.defineProperty(target, key, sharedPropertyDefinition);
}

计算属性 watcher 实例化的时候,会把 options.lazy 设置为 true ,这里是计算属性惰性求值,且可缓存的关键,当然前提是 cache 不为 false 。

cache 不为 false ,会调用 createComputedGetter函数 创建计算属性的 getter函数computedGetter

先来看一段代码

//代码选取部分
function createComputedGetter(key) {
    return function computedGetter() {
        var watcher = this._computedWatchers && this._computedWatchers[key];
        if(watcher) {
            if(watcher.dirty) {
            //watcher.evaluate中更新watcher的值,并把watcher.dirty设置为false
            //这样等下次依赖更新的时候才会把watcher.dirty设置为true,
            //然后进行取值的时候才会再次运行这个函数
                watcher.evaluate();
            }
            //依赖追踪
            if(Dep.target) {
                watcher.depend();
            }
            //返回watcher的值
            return watcher.value
        }
    }
}

//对于计算属性,当取值计算属性时,发现计算属性的watcher的dirty是true
//说明数据不是最新的了,需要重新计算,这里就是重新计算计算属性的值。
Watcher.prototype.evaluate = function evaluate() {
    this.value = this.get();
    this.dirty = false;
};

//当一个依赖改变的时候,通知它update
Watcher.prototype.update = function update() {
    //三种watcher,只有计算属性 watcher的lazy设置了true,表示启用惰性求值
    if(this.lazy) {
        this.dirty = true;
    } else if(this.sync) {
        //标记为同步计算的直接运行run,三大类型暂无,所以基本会走下面的queueWatcher
        this.run();
    } else {
        //将watcher推入观察者队列中,下一个tick时调用。
        //也就是数据变化不是立即就去更新的,而是异步批量去更新的
        queueWatcher(this);
    }
};

当 options.lazy 设置为 true 之后(仅计算属性 watcher 的 options.lazy 设置为 true ),每次依赖更新,都不会主动触发 run函数 ,而是把 watcher.dirty 设置为 true 。这样,当对计算属性进行取值时,就会运行 computedGetter函数 。 computedGetter函数 中有一个关于 watcher.dirty 的判断,当 watcher.dirty 为 true 时会运行 watcher.evaluate 进行值的更新,并把 watcher.dirty 设置为 false ,这样就完成了惰性求值的过程。后面只要依赖不更新,就不会运行 update ,就不会把 watcher.dirty 为 true ,那么再次取值的时候就不会运行 watcher.evaluate 进行值的更新,从而达到了缓存的效果。

综上,我们了解到 cache 不为 false 的时候,计算属性都是惰性求值且具有缓存性的,而 cache 默认是 true ,我们也大多使用这个默认值,所以我们说 computed 本质是一个惰性求值的观察者,具有缓存性,只有当依赖变化后,第一次访问 computed 属性,才会计算新的值


文章作者: 弈心
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 弈心 !
评论
  目录