今天看啥  ›  专栏  ›  影子同学

Vue 数据响应式源码分析笔记

影子同学  · 掘金  ·  · 2019-09-30 15:43
阅读 15

Vue 数据响应式源码分析笔记

一、前言

Vue2实现响应式的核心是利用了ES5的Object.defineProperty,这也是Vue不能兼容IE8及以下浏览器的原因

Object.defineProperty

会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象

可以在MDN看看关于它的使用介绍

在Vue中主要使用到的是descriptor中的get和set,get 是一个给属性提供的 getter 方法,当访问了该属性的时候会触发getter方法。set 是一个给属性提供的 setter 方法,当对该属性做修改的时候会触发setter方法

建议去下载一份Vue2代码,这样对代码结构以及思路会比较清晰。想了解的函数方法也可以及时查到。接下来提到的实现函数代码篇幅不会太多,仅作为引导作用

在Vue2中,只要是在data属性里声明的字段,都会变成响应式属性,在代码中修改值,会对应的更新到dom上(也涉及到数据驱动知识)或对该值作出响应

let vm = new Vue({
    data() {
        return {
            one: 'one'
        }
    },
    watch: {
        one: function() {
            console.log('one changed')
        }
    },
    computed: {
        two: function() {
            return this.one + 1
        }
    }
})
vm.one = '1'
// console.log: one changed
vm.two: '11'
// 此后系统会对这个修改作出一系列的响应如更新dom,更新有关数据等等
复制代码

二、初始化

在Vue的初始化_init函数执行时,其中会执行initState(vm)方法

// src/core/instance/state.js
export function initState (vm: Component) {
    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)
    }
}
复制代码

它主要是对props、methods、data、computed和 wathcer等属性做了初始化操作。这里我们重点分析props 和 data:

三、props初始化

// src/core/instance/state.js
function initProps (vm: Component, propsOptions: Object) {
    const propsData = vm.$options.propsData || {}
    const props = vm._props = {}
    const keys = vm.$options._propKeys = []
    const isRoot = !vm.$parent
    if (!isRoot) {
        toggleObserving(false)
    }
    for (const key in propsOptions) {
        keys.push(key)
        const value = validateProp(key, propsOptions, propsData, vm)
        if (process.env.NODE_ENV !== 'production') {
        ...
        } else {
        defineReactive(props, key, value)
        }
        if (!(key in vm)) {
        proxy(vm, `_props`, key)
        }
    }
    toggleObserving(true)
}
复制代码

props的初始化主要过程是遍历定义的props配置。遍历的过程主要做两件事情:

  • 调用defineReactive方法把每个prop对应的值变成响应式,可以通过 vm._props.xxx 访问到定义props中对应的属性(defineReactive方法稍后介绍
  • 另一个是通过proxy把vm._props.xxx的访问代理到vm.xxx上

四、data初始化

// src/core/instance/state.js
function initData (vm: Component) {
    let data = vm.$options.data
    data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {}
    if (!isPlainObject(data)) {
        data = {}
        ...
    }
    // proxy data on instance
    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') {
            // 判断在methods中是否声明过了
            if (methods && hasOwn(methods, key)) {
            ...
            }
        }
        // 判断在props中是否声明过了
        if (props && hasOwn(props, key)) {
        ...
        } else if (!isReserved(key)) {
        proxy(vm, `_data`, key)
        }
    }
    // 关键代码!!
    observe(data, true /* asRootData */)
}
复制代码

data 的初始化主要过程也是做两件事:

  • 对定义data函数返回对象的遍历,通过proxy把每一个值vm._data.xxx都代理到vm.xxx上
  • 另一个是调用observe方法观测整个data的变化,把data也变成响应式,可以通过vm._data.xxx访问到定义data 返回函数中对应的属性(observe稍后会介绍)

可以看到,无论是props还是data的初始化都是把它们变成响应式对象,其中主要函数有defineReactive,proxy,observe

五、此处有个小结

简单理解是:Vue在创建实例的时候,会拿到options中的data和props等字段,然后对他们进行响应式改造

进行响应式声明的入口只有data、props等几个属性,并且字段需要是显示声明的。所以在其他地方的字段声明就不会有响应式的效果,比如在执行代码中新加的属性,数组的部分操作(这个比较特殊)

如果在平时开发中遇到数据怎么怎么改都不会在页面上动的,可以先检查一下是否触及到上面的问题

let vm = new Vue({
    data () {
        return {
            one: 1,
            two: 2,
            arr: []
        }
    },
    created: {
        this.three = 3
    }
})
vm.one = 'one' // 会有对应的响应
vm.two = 'two' // 会有对应的响应
vm.three = 'three' // 不会有对应的响应!
vm.arr[0] = 'new one' // 不会有对应的响应!
复制代码

至于为什么,继续看下去便能知一二

六、proxy

proxy的作用是把对象上的属性代理到vm实例上

// src/core/instance/state.js
const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}

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
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}
复制代码

这也就是为什么我们定义了如下props、data,通过vm实例就能访问到它

let comP = {
  props: {
    msg: 'hello'
  },
  data() {
    return {
        str: 'hi'
    }
  }
  methods: {
    say() {
      console.log(this.msg, this.str)
    }
  }
}
复制代码

proxy方法的实现很简单,通过Object.defineProperty把target[sourceKey][key]的读写变成了对target[key]的读写,对 props而言,vm._props.xxx的读写变成了vm.xxx的读写,所以我们就可以通过vm.xxx访问到定义在props中的xxx 属性了。同理data也如此

七、observe

observe是用来给值绑上监测数据的变化的功能

// src/core/observer/index.js
export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  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 方法的作用就是给非VNode的对象类型数据添加一个Observer,如果已经添加过则直接返回,否则在满足一定条件下去实例化一个Observer对象实例

八、Observer

Observer是一个类,它的作用是给对象的属性添加getter和setter,用于依赖收集和派发更新

export class Observer {
    value: any;
    dep: Dep;
    vmCount: number; 
    constructor (value: any) {
        this.value = value
        this.dep = new Dep()
        this.vmCount = 0
        def(value, '__ob__', this)
        if (Array.isArray(value)) {
         const augment = hasProto ? protoAugment : copyAugment
        augment(value, arrayMethods, arrayKeys)
        this.observeArray(value)
        } else {
        this.walk(value)
        }
    }
    walk (obj: Object) {
        const keys = Object.keys(obj)
        for (let i = 0; i < keys.length; i++) {
        defineReactive(obj, keys[i])
        }
    }
    observeArray (items: Array<any>) {
        for (let i = 0, l = items.length; i < l; i++) {
            observe(items[i])
        }
    }
}
复制代码

Observer的构造函数逻辑很简单,首先实例化Dep对象(后面讲),接着通过执行def函数把自身实例添加到数据对象value的 __ob__属性上,def的定义在 src/core/util/lang.js中

export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
    Object.defineProperty(obj, key, {
        value: val,
        enumerable: !!enumerable,
        writable: true,
        configurable: true
    })
}
复制代码

def函数是一个非常简单的Object.defineProperty的封装,这就是为什么我在开发中输出data 上对象类型的数据,会发现该对象多了一个__ob__的属性

回到Observer的构造函数,接下来会对value做判断,对于数组会调用observeArray方法,否则对纯对象调用walk方法(重要)可以看到 observeArray是遍历数组再次调用observe方法,而walk方法是遍历对象的key调用defineReactive方法

九、defineReactive

defineReactive的功能是定义一个响应式对象,给对象动态添加getter和setter

// src/core/observer/index.js
export function defineReactive (
    obj: Object,
    key: string,
    val: any,
    customSetter?: ?Function,
    shallow?: boolean
) {
    const dep = new Dep()
    const property = Object.getOwnPropertyDescriptor(obj, key)
    if (property && property.configurable === false) {
        return
    }

    const getter = property && property.get
    const setter = property && property.set
    if ((!getter || setter) && arguments.length === 2) {
        val = obj[key]
    }
    // 关键代码!如果是value是对象,进入下一层响应式绑定
    let childOb = !shallow && observe(val)
    // 这里这里这里是关键!对key进行绑定,而不是对value
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter () {
            const value = getter ? getter.call(obj) : val
            if (Dep.target) {
                dep.depend()
                if (childOb) {
                    childOb.dep.depend()
                if (Array.isArray(value)) {
                    dependArray(value)
                }
                }
            }
            return value
        },
        set: function reactiveSetter (newVal) {
            const value = getter ? getter.call(obj) : val
            /* eslint-disable no-self-compare */
            if (newVal === value || (newVal !== newVal && value !== value)) {
                return
            }
            if (process.env.NODE_ENV !== 'production' && customSetter) {
                customSetter()
            }
            if (getter && !setter) return
            if (setter) {
                setter.call(obj, newVal)
            } else {
                val = newVal
            }
            childOb = !shallow && observe(newVal)
            dep.notify()
        }
    })
}
复制代码

defineReactive函数最开始初始化Dep对象的实例,接着拿到obj的属性描述符,然后会对子对象递归调用observe 方法(observe函数会判断传入值的类型然后做对应操作),这样就保证了无论obj的结构多复杂,它的所有子属性也能变成响应式的对象,这样我们访问或修改 obj 中一个嵌套较深的属性,也能触发getter和setter。最后利用 Object.defineProperty去给obj的属性key添加getter和 setter

而关于getter和setter的具体实现,篇幅太多,下篇介绍

十、总结

响应式的声明大概就是这样:拿到配置中的data、props等属性,对里面的字段都进行响应式改造

  • 遇到值为object, arr的,对key进行响应式改造,然后遍历拿该值的内部字段继续进行响应式改造
  • 遇到值为基本类型的,对key进行响应式改造
  • 具体实现上面有说到,当然自己看一下源码最好啦

是不是其实没什么捏




原文地址:访问原文地址
快照地址: 访问文章快照