Vue 源码解析 - 数据驱动与响应式原理

2020-04-12  本文已影响0人  Whyn

[TOC]

数据驱动与响应式原理

Vue 的一个核心思想就是 数据驱动,即数据会驱动界面,如果想修改界面,直接对相应的数据进行修改即可, DOM 元素会自动更新。

数据驱动 的思想,其实质是将界面 DOM 元素映射到数据上,解耦了业务与视图,这样可以让我们只专注于对数据的操作,而无须与视图进行交互,简化了业务逻辑,代码更加清晰。

:前端开发中,视图的展示本身就是由数据进行驱动的。传统前端开发过程中,一般都是先从后端获取数据,然后手动将数据渲染到前端 DOM 元素上,由于一个页面上可能存在很多 DOM 元素需要进行渲染,这样我们的业务代码中就充斥着许多与业务无关的 DOM 操作,代码会显得臃肿和混乱。而 数据驱动 思想可以自动完成渲染到 DOM 元素这个操作,对于我们的代码来说,只需进行数据获取即可,这才是更加纯粹的数据驱动模型。

举个栗子:一个最简单的数据驱动例子如下所示:

<div id="app">
    <h1>{{message}}</h1>
</div>

<script>
    const vm = new Vue({
        el: '#app',
        data: {
            message: 'Hello Vue!'
        }
    });
</script>

h1元素的内容由Vue实例的数据data.message进行驱动,并且,当我们手动更改message的值时,可以看到,视图同时也随之更新了。

由上,我们可以知道,数据驱动 其实包含两部分内容:组件挂载响应式

组件挂载 内容请参考:Vue 源码解析 - 组件挂载

以下我们主要对 Vue响应式原理 进行解析。

Vue 源码解析 - 主线流程 中,我们知道,当new Vue(Options)的时候,实际上调用的是_init函数:

// src/core/instance/index.js
function Vue(options) {
    ...
    this._init(options)
}

_init函数中, 会执行一系列的初始化,其中就包含有initState(vm)

// src/core/instance/init.js
export function initMixin(Vue: Class<Component>) {
    Vue.prototype._init = function (options?: Object) {
        ...
        // 初始化 props、methods、data、computed 与 watch
        initState(vm)
        ...
    }
}

initState函数主要对Vue组件的props,methods,data,computedwatch等状态进行初始化:

// src/core/instance/state.js
export function initState(vm: Component) {
    vm._watchers = []
    const opts = vm.$options
    //   初始化 options.props
    if (opts.props) initProps(vm, opts.props)
    //   初始化 options.methods
    if (opts.methods) initMethods(vm, opts.methods)
    if (opts.data) {
        // 初始化 options.data
        initData(vm)
    } else {
        // 没有 options.data 时,绑定为一个空对象
        observe(vm._data = {}, true /* asRootData */)
    }
    //   初始化 options.computed
    if (opts.computed) initComputed(vm, opts.computed)
    if (opts.watch && opts.watch !== nativeWatch) {
        // 初始化 options.watcher
        initWatch(vm, opts.watch)
    }
}

我们主要来看下initState函数中的initData(vm)操作:

// core/instance/state.js
function initData (vm: Component) {
    let data = vm.$options.data
    data = vm._data = typeof data === 'function'
        ? getData(data, vm)     // getData:解析出原本的 options.data
        : data || {}            // data 不是函数,直接使用
    if (!isPlainObject(data)) { // data 不是对象,开发环境会给出警告
        data = {}
        ...
    }
    // proxy data on instance
    const keys = Object.keys(data)
    ...
    let i = keys.length
    while (i--) {
        const key = keys[i]
        ...
        else if (!isReserved(key)) {
            // 为 vm 组件对象设置与 key 同名的访问器属性,作为 key 的代理,真实值存储于 vm._data 对象中
            // 这步操作过后,vm 就具备了与 options.data 所有的同名 key 的访问器属性,因此,使用 this.xxx 操作
            // data 中的数据就是操作组件对象 vm 的访问器属性,相当于 options.data 的数据被组件对象拦截了。 
            proxy(vm, `_data`, key)
        }
    }
    // observe data
    observe(data, true /* asRootData */)
}
export function getData (data: Function, vm: Component): any {
    ...
    return data.call(vm, vm)
    ...
}

// src/shared/util.js
/**
 * Strict object type check. Only returns true
 * for plain JavaScript objects.
 */
export function isPlainObject (obj: any): boolean {
    return _toString.call(obj) === '[object Object]'
}

// src/core/util/lang.js
/**
 * Check if a string starts with $ or _
 */
export function isReserved (str: string): boolean {
    const c = (str + '').charCodeAt(0)
    return c === 0x24 || c === 0x5F
}

initData函数会获取我们在new Vue(Options)时传递进去的Options.data数据,然后进行遍历,对其进行proxy操作,最后会为Options.data进行observe操作。

下面我们先对proxy进行分析,其源码如下:

// core/instance/state.js
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
    }
    // 为 target 添加访问器属性 key
    Object.defineProperty(target, key, sharedPropertyDefinition)
}

const sharedPropertyDefinition = {
    enumerable: true,
    configurable: true,
    get: noop,
    set: noop
}

proxy函数的功能就是通过Object.definePropertytarget对象添加字段为key的访问器属性,并设置其get/set的具体操作。

当我们调用proxy(vm, '_data', key)时,就是为vm添加与Options.data相同键值的访问器属性,这些属性的get/set方法内部实现为:this['_data'],也即:当调用this.xxxthisVue实例,xxxOptions.data中的某个key)时,会被代理到sharedPropertyDefinition.get / sharedPropertyDefinition.set,其结果为:this._data.xxx

简而言之,proxy函数的作用就是让 Vue 实例创建与Options.data相同键值的访问器属性,使得在源码中可以使用this(即Vue实例)访问Options.data中的同名key的值,相当于代理了Options.data

initData的最后,还为Options.data做了observe操作,我们接下来查看下observe函数源码:

// src/core/observer/index.js
/**
 * Attempt to create an observer instance for a value,
 * returns the new observer if successfully observed,
 * or the existing observer if the value already has one.
 */
export function observe(value: any, asRootData: ?boolean): Observer | void {
    // value 必须为对象(且非 VNode),否则就无须观察
    if (!isObject(value) || value instanceof VNode) {
        return
    }
    let ob: Observer | void
    // 有 __ob__ 属性表示 value 已经被 Observer 了
    if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
        ob = value.__ob__
    } else if (
        shouldObserve &&
        !isServerRendering() &&
        (Array.isArray(value) || isPlainObject(value)) &&
        Object.isExtensible(value) &&
        !value._isVue
    ) {
        // 对被观察的数据创建一个观察者
        ob = new Observer(value)
    }
    if (asRootData && ob) {
        ob.vmCount++
    }
    return ob
}

observe函数的注释可以知道,observe函数会为要被观察的数据对象(即Options.data等)创建一个观察者实例Observer,该函数主要做了如下几件事:

这里进行判断主要是因为Observer会对被观察对象的每一个key都进行监控,代码编写上涉及一个递归过程(见后文),停止的条件就是非对象类型或是VNode类型。

我们主要来看下Observer实例的创建过程,即:new Observer(value)

// src/core/observer/index.js
/**
 * Observer class that is attached to each observed
 * object. Once attached, the observer converts the target
 * object's property keys into getter/setters that
 * collect dependencies and dispatch updates.
 */
export class Observer {
    value: any;
    dep: Dep;
    vmCount: number; // number of vms that have this object as root $data

    constructor(value: any) {
        this.value = value
        this.dep = new Dep()
        this.vmCount = 0
        // 为 value 对象添加 __ob__ 属性,其值指向当前 Observer
        def(value, '__ob__', this)
        if (Array.isArray(value)) {
            ...
            this.observeArray(value)
        } else {
            this.walk(value)
        }
    }

    /**
     * Walk through all properties and convert them into
     * getter/setters. This method should only be called when
     * value type is Object.
     */
    walk(obj: Object) {
        const keys = Object.keys(obj)
        for (let i = 0; i < keys.length; i++) {
            defineReactive(obj, keys[i])
        }
    }

    /**
     * Observe a list of Array items.
     */
    observeArray(items: Array<any>) {
        for (let i = 0, l = items.length; i < l; i++) {
            observe(items[i])
        }
    }
}

// src/core/util/lang.js
/**
* Define a property.
*/
export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
 Object.defineProperty(obj, key, {
   value: val,
   enumerable: !!enumerable,
   writable: true,
   configurable: true
 })
}

Observer的注释可以看到,每个需要被观察的数据对象都会被Observer实例监控,Observer实例会把被观察数据对象的所有键值属性转换为getter / setter函数(即将被观察数据对象的所有键值属性转换为访问器属性),从而可以进行 依赖收集派发更新 操作。其具体的操作细节如下:

到这里,数据驱动与响应式原理的分析过程就大致结束了。

参考

上一篇 下一篇

猜你喜欢

热点阅读