赞
踩
我的开源库:
- fly-barrage 前端弹幕库,项目官网:https://fly-barrage.netlify.app/,可实现类似于 B 站的弹幕效果,并提供了完整的 DEMO,Gitee 推荐项目;
- fly-gesture-unlock 手势解锁库,项目官网:https://fly-gesture-unlock.netlify.app/,在线体验:https://fly-gesture-unlock-online.netlify.app/,可高度自定义锚点的数量、样式以及尺寸;
这篇博客主要讲解 Vue 是如何将数据转换成响应式的,将数据转换成响应式是进行依赖收集和变化侦测的基础。
第一小节是为了让大家了解响应式处理的代码在整体源码中的位置。
- function Vue (options) {
- // 执行 vm 原型上的 _init 方法,该方法在 initMixin 方法中定义
- this._init(options)
- }
-
- // 写入 vm._init
- initMixin(Vue)
vue 里面执行 _init 方法,该方法定义在 initMixin 方法中。
- export function initMixin (Vue: Class<Component>) {
- Vue.prototype._init = function (options?: Object) {
- // 初始化 state,包括 props、methods、data、computed、watch
- initState(vm)
-
- // 如果配置中有 el 的话,则自动执行挂载操作
- if (vm.$options.el) {
- vm.$mount(vm.$options.el)
- }
- }
- }
初始化数据的方法是 initState。
- 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)
- // 初始化监听属性
- // nativeWatch的作用:Firefox has a "watch" function on Object.prototype...
- if (opts.watch && opts.watch !== nativeWatch) {
- // 进行侦听属性的初始化过程
- initWatch(vm, opts.watch)
- }
- }
initState 方法内进行了一系列状态的初始化,我们以 initData 为例。
- // 初始化我们配置中写的 data 对象,传递的参数(vm)是当前 Vue 的实例
- function initData (vm: Component) {
- // observe data
- observe(data, true /* asRootData */)
- }
- export function observe (value: any, asRootData: ?boolean): Observer | void {
- // 如果这个值不是一个对象、或者这个值是一个虚拟节点实例的话,在这里直接 return
- if (!isObject(value) || value instanceof VNode) {
- return
- }
- // 声明要返回的 ob 变量
- let ob: Observer | void
- // 如果当前 value 有 __ob__ 属性,且这个属性是 Observer 类的实例的话
- // 直接将 value.__ob__ 赋值给 ob
- if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
- ob = value.__ob__
- } else if (
- observerState.shouldConvert &&
- !isServerRendering() &&
- (Array.isArray(value) || isPlainObject(value)) &&
- Object.isExtensible(value) &&
- !value._isVue
- ) {
- // 传递进来的 value 并不是响应式的,在这里。通过 new Observer(value) 将其转换成响应式的
- // 并且返回 new 出来的实例
- ob = new Observer(value)
- }
- if (asRootData && ob) {
- ob.vmCount++
- }
- return ob
- }
在这里通过执行 ob = new Observer(value) 将数据转换成响应式的,所谓响应式的数据是指 Vue 能够侦测到该数据的使用和变更。
响应式的数据是指 Vue 能够侦测到该数据的使用和变更。
侦测数据的使用是为了依赖收集。
侦测数据的变更是为了触发依赖更新
- export class Observer {
- // 被处理成响应式的数据,可以是对象类型或者是数组类型
- value: any;
- dep: Dep;
- // number of vms that has this object as root $data
- // 将此对象作为根 $data 的 vms 数量
- vmCount: number;
-
- constructor (value: any) {
- this.value = value
- this.dep = new Dep()
- this.vmCount = 0
- def(value, '__ob__', this)
- // 对象类型值和数组类型值有不同的处理,在这里进行 if else 判断。
- if (Array.isArray(value)) {
- const augment = hasProto
- ? protoAugment
- : copyAugment
- augment(value, arrayMethods, arrayKeys)
- this.observeArray(value)
- } else {
- // 用于将对象中的属性都转换成响应式的
- this.walk(value)
- }
- }
-
- /**
- * 该方法用于将 obj 中所有的 key 都转换成响应式的
- * 具体的做法是遍历 keys,每个 key 都执行 defineReactive 方法
- * defineReactive 方法用于将对象中具体的 key 转换成响应式的
- */
- walk (obj: Object) {
- const keys = Object.keys(obj)
- for (let i = 0; i < keys.length; i++) {
- defineReactive(obj, keys[i], obj[keys[i]])
- }
- }
-
- /**
- * 用于将数组中的元素都转换成响应式的
- */
- observeArray (items: Array<any>) {
- for (let i = 0, l = items.length; i < l; i++) {
- // 对数组中的每个元素都执行 observe 方法
- observe(items[i])
- }
- }
- }
Observer 类的构造方法中会判断处理的数据是不是数组类型,对象类型和数组类型的处理方式是不一样的,对象类型会执行 walk 方法。
walk 方法会遍历对象的 key,执行 defineReactive 方法,在 defineReactive 方法中将对象中的 key 都转换成 Object.defineProperty 的形式,这样 Vue 就能监控到对象属性的使用和变更了。
- export function defineReactive (
- // 对象
- obj: Object,
- // key
- key: string,
- // 值
- val: any,
- customSetter?: ?Function,
- // 浅的
- shallow?: boolean
- ) {
- // 如果 val 是一个对象类型的话,那么这个 dep 将用于保存 val 的依赖列表
- // 数组类型值的依赖列表保存在 observer.dep 中
- const dep = new Dep()
-
- const getter = property && property.get
- const setter = property && property.set
-
- // 这个 childOb(Observer类的实例)中的 dep 是用来保存数组类型值的依赖的
- let childOb = !shallow && observe(val)
- Object.defineProperty(obj, key, {
- enumerable: true,
- configurable: true,
- // 在此进行依赖收集
- get: function reactiveGetter () {
- // 触发执行上面拿到的 getter
- const value = getter ? getter.call(obj) : val
- / 下面是依赖收集的操作 /
- // 如果 Dep 上的静态属性 target 存在的话
- if (Dep.target) {
- // 向 dep 中添加依赖,依赖是 Watcher 的实例
- dep.depend()
- if (childOb) {
- // childOb.dep 用来存储数组类型值的依赖
- childOb.dep.depend()
- // 如果值是数组类型的话
- if (Array.isArray(value)) {
- dependArray(value)
- }
- }
- }
- // getter 返回值
- return value
- },
- // 在此进行派发更新
- set: function reactiveSetter (newVal) {
- // 触发依赖的更新
- dep.notify()
- }
- })
- }
在 defineReactive 方法的开头创建了一个 Dep 类的实例,这个 dep 实例是用于存储对象类型值依赖的,我们可以看到在下面的 get 中,执行 dep.depend() 进行依赖的收集,依赖收集的细节,下面再说。
如果数据是数组类型的话,还需要执行数组类型值对应 dep 实例的 depend 方法,数组类型的 dep 实例存储在 Observer 实例上面。关于数组类型值为什么要多这一步处理?以及 dep 为什么存储在 Observer 实例上面,我们在下一小节细讲一下,要不然容易引起困惑。
假设上面 defineReactive 方法的 obj 参数为:
- let obj = {
- names: ['tom', 'jack', 'rose']
- }
key 参数为 'names',value 参数为 ['tom', 'jack', 'rose']。
那么上面的这个 names 属性的变更有两种方式:
(1)obj.names = ['小明', '小红', '小山']
(2)obj.names.push('alice')
第一种方式是直接对 obj.names 重新设值,这种变更数据的方式 Object.defineProperty set 是能够侦测到变化的,这在 set 中执行 dep.notify() 触发依赖更新就可以了。
但是第二种方式使用数组原型上的方法变更数据,这种变更方法 Object.defineProperty set 是侦测不到的。所以 Vue 另辟新径,重写了数组的原型方法,如果用户执行数组的原型方法变更数组的话,Vue 就能够在重写的原型方法中执行依赖更新的操作。
Vue 在重写的原型方法中执行依赖更新操作的前提是:在重写的原型方法中能够拿到这个数组对应的 dep 实例,而在 defineReactive 方法中创建的 dep 实例,在重写的原型方法中是获取不到的。Vue 的解决方案是在 Observer 实例上创建一个专门服务于数组收集依赖的 dep 实例,因为Observer 的实例会被定义在值的 '__ob__' 属性上,所以在数组的原型方法中,可以通过 this.__ob__.dep 获取到数组对应的 Dep 实例,这就解决了问题。相关代码如下:
(1)将为数组类型值的 dep 设值到 Observer 实例上
- export class Observer {
- value: any;
- dep: Dep;
-
- constructor (value: any) {
- this.value = value
- this.dep = new Dep()
- def(value, '__ob__', this)
- }
- }
(2)在 get 中为数组类型值收集依赖,依赖存储到 childOb.dep 中
- // 这个 childOb(Observer类的实例)中的 dep 是用来保存数组类型值的依赖的
- let childOb = !shallow && observe(val)
-
- Object.defineProperty(obj, key, {
- enumerable: true,
- configurable: true,
- // 在此进行依赖收集
- get: function reactiveGetter () {
- // 触发执行上面拿到的 getter
- const value = getter ? getter.call(obj) : val
- / 下面是依赖收集的操作 /
- // 如果 Dep 上的静态属性 target 存在的话
- if (Dep.target) {
- // 向 dep 中添加依赖,依赖是 Watcher 的实例
- dep.depend()
- if (childOb) {
- // childOb.dep 用来存储数组类型值的依赖
- childOb.dep.depend()
- // 如果值是数组类型的话
- if (Array.isArray(value)) {
- dependArray(value)
- }
- }
- }
- // getter 返回值
- return value
- },
- // 在此进行派发更新
- set: function reactiveSetter (newVal) {
- ......
- }
- })
(3)在重写的原型方法中,获取 dep,并且触发依赖更新
- [
- 'push',
- 'pop',
- 'shift',
- 'unshift',
- 'splice',
- 'sort',
- 'reverse'
- ]
- .forEach(function (method) {
- def(arrayMethods, method, function mutator (...args) {
- const result = original.apply(this, args)
- const ob = this.__ob__
- // 通知 ob.dep 中的依赖
- ob.dep.notify()
- // 在最后,返回 Array 方法执行的结果
- return result
- })
- })
- export class Observer {
- constructor (value: any) {
- this.value = value
- this.dep = new Dep()
- def(value, '__ob__', this)
- if (Array.isArray(value)) {
- const augment = hasProto
- ? protoAugment
- : copyAugment
- augment(value, arrayMethods, arrayKeys)
- } else {
- // 用于将对象中的属性都转换成响应式的
- this.walk(value)
- }
- }
- }
hasProto 方法用于判断当前的浏览器环境支不支持 __proto__。
如果支持原型的话,就将重写的数组原型方法赋值到数组的 __proto__ 属性上。
- function protoAugment (target, src: Object, keys: any) {
- /* eslint-disable no-proto */
- target.__proto__ = src
- /* eslint-enable no-proto */
- }
如果不支持 __proto__ 的话,直接将重写的原型方法赋值到数组值上面。
- function copyAugment (target: Object, src: Object, keys: Array<string>) {
- for (let i = 0, l = keys.length; i < l; i++) {
- const key = keys[i]
- def(target, key, src[key])
- }
- }
加下来看看 Vue 是如何重写原型方法的,上面的 arrayMethods 就是包含重写原型方法的对象。
- // 拿到 Array 的 prototype 原型对象
- const arrayProto = Array.prototype
- // 利用 Object.create() 创建一个新的对象,并且这个新的对象的原型链(__proto__)指向 arrayProto。
- // 这样的话,我们只需要将一些需要改写的方法定义到 arrayMethods 对象中即可。
- // 这样的话,我们既可以访问到 arrayMethods 对象中已经改写了的方法,也能访问到 arrayProto 对象中未改写的方法
- // ^o^ 完美!
- export const arrayMethods = Object.create(arrayProto)
-
- // 能够改变数组内容方法的数组
- ;[
- 'push',
- 'pop',
- 'shift',
- 'unshift',
- 'splice',
- 'sort',
- 'reverse'
- ]
- // 进行遍历处理
- .forEach(function (method) {
- // 缓存原生的相应方法
- const original = arrayProto[method]
- // 定义该 method 对应的自定义方法
- def(arrayMethods, method, function mutator (...args) {
- // 执行原生方法拿到执行结果值,在最后将这个结果值返回
- const result = original.apply(this, args)
- // 这里的 this 是执行当前方法的数组的实例。在 Vue 中,每个数据都会有 __ob__ 属性,这个属性
- // 是 Observer 的实例,该实例有一个 dep 属性(Dep 的实例),该属性能够收集数组的依赖
- const ob = this.__ob__
- // 数组有三种新增数据的方法。分别是:'push','unshift','splice'
- // 这些新增的数据也需要变成响应式的,在这里,使用 inserted 变量记录新增的数据
- let inserted
- switch (method) {
- // 如果当前的方法是 push 或者 unshift 的话,新增的数据就是 args,将 args 设值给 inserted 即可
- case 'push':
- case 'unshift':
- inserted = args
- break
- // 如果当前的方法是 splice 的话,那么插入的数据就是 args.slice(2)
- case 'splice':
- inserted = args.slice(2)
- break
- }
- // 如果的确新增了数据的话,将 inserted 作为参数执行 observer.observeArray() 方法,把新增的每个元素都变成响应式的
- if (inserted) ob.observeArray(inserted)
- // 通知 ob.dep 中的依赖
- ob.dep.notify()
- // 在最后,返回 Array 方法执行的结果
- return result
- })
- })
(1)首先使用变量 arrayProto 保存数组原先的原型对象。
(2)然后创建对象 arrayMethods,原型链指向 arrayProto。
(3)最后在 arrayMethods 中重写那些能够改变数组本身的原型方法,在重写的原型函数中会执行 ob.dep.notify()
这样做的好处是:只重写了那些能够改变数组内容的原型方法,其余的原型方法借助于原型链还使用其原本实现。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。