基于Vue2源码,寻找defineProperty To Proxy的原因

吐槽君 分类:javascript

前言

大家都知道,新推出Vue3的中采用Proxy API对数据进行响应式处理,而弃用了defineProperty API,至于为什么,我把答案放在了最后,感兴趣的朋友记得读下去喔,(✿◡‿◡)

严格来说,对于Object.defineProperty对应数据劫持方式,而Proxy对应数据代理,只是代理过程存在一层拦截处理

下载

如果你也对Vue2中这一部分的代码感兴趣,想尝试Vue2源码阅读,可以把源码下载到本地,进行代码阅读。

  • 输入命令
git clone https://github.com/vuejs/vue.git
 
  • 注意

Vue.js的源码利用了Flow做了静态类型检查,所以存在一些静态类型检查语法

源码目录介绍

src
├── compiler        # 编译相关 
├── core            # 核心代码 
├── platforms       # 不同平台的支持
├── server          # 服务端渲染
├── sfc             # .vue 文件解析
├── shared          # 共享代码(工具方法)
 

core目录: 是Vue.js的核心代码目录,包括内置组件、全局 API 封装,Vue 实例化、观察者、虚拟 DOM、工具函数等等。
当然这篇文张主要分析数据劫持的过程,当然自己感兴趣的话可以尝试阅读其他部分的内容。
我们从src入口开始,找到core下的observer文件,好,平常我们被问到最多的数据劫持,双向绑定(除模版解析外)的代码都在这个observer文件目录里面

  • 核心文件介绍
observer
├── array.js        # 手动为数组提供视图更新
├── dep.js          # 发布订阅器实现 
├── index.js        # 数据劫持主文件入口
├── watcher.js      # 模版解析过程,所需的订阅者
 

核心

对于这部分,我只梳理数据劫持部分的代码,所以如果对于vue响应式原理感兴趣的小伙伴们,可以尝试下这篇文章。
找到observer下的index.js文件正式开始我们的内容。

我们把observer/index.js打开,当然这里面的代码我们从哪里开始呢?
所以我们要找到开始数据Vue实例化过程中,数据劫持的入口。


1. 找到整个Vue的数据初始化入口

找到Vue实例文件,在src/core/instance/index.js中:

import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue

 

看一眼,有个init方法,所以是在数据劫持初始化肯定在这里,Vue本身没有这方法,所以应该是原型上方法,找下initMixin,...下面这几个中有没有init定义
good,initMixin方法中找到了。
看到instance/init.js中:

// expose real self
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
 

数据观测应该在created生命周期之前,beforeCreate之后,在看下函数命名,应该在initState中。

来到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)
  }
}
 

明显就是initData和observe那,同文件看到initData定义,好家伙终于看到了核心代码

function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  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
  observe(data, true /* asRootData */)
}
 

找到数据劫持关键observe(data, true /* asRootData */),我们回到observer/index.js文件下

2. 重回observer/index.js

2.1 observer方法:

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
}
 

上面代码比较简单,首先判断是否是不是对象或者是虚拟节点,不然直接退出,然后判断
自身属性__ob__,shouldObserve是否观测(默认true),isServerRendering是否服务端渲染,isExtensible属性是否还存在扩展性,等等,然后进入实例化内容new Observer(value)

2.2 Observer 实例对象

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
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods) // 自行阅读
      } else {
        copyAugment(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])
    }
  }
}
 

上面代码中protoAugment,copyAugment希望能自己阅读下哈,就不说了
不过,上面的源码中我们看到了一个问题,在Observer实例化的过程中,代码中判断是否为数组,然后进行分开处理,但是为什么要这样做呢?

  • Object.defineProperty对数组的不友好

来看个例子:

var list = ["name","age","sex"];

console.log(Object.keys(list));

Object.keys(list).forEach((key)=>{
    let value = list[key]
    Object.defineProperty(list, key,{
        get: function(){
            console.log("getting is running")
            return value
        },
        set: function(newValue){
            console.log("setting is running")
            value = newValue
        }
    })
})

console.log(list[0]);
list[0] = "name123";
console.log(list);
console.log(Array.isArray(list));  // true
 

上面的例子中,我们如果以index下标为key值,上面看着是正常的,但确有一个问题,打印list,会发现原来的数组list变成了[ [Getter/Setter], [Getter/Setter], [Getter/Setter] ],get、set的方式进行存储数据
当然这一点上没有什么问题,defineProperty可以实现对数组类型的数据劫持。
但是我们从另一个角度来考虑,数组如果很长,那就会产生频繁的defineProperty调用,这从性能角度来讲是不友好的,这也采用Proxy的重要原因。


  • 我们来看下Proxy怎么进行数组数据处理
const list = [];
const arr = new Proxy(list,{
    get(target,key, receiver){
        console.log(key);
        console.log(typeof key);
        console.log("getting is running");
        return Reflect.get(target,key)
    },
    set(target, key, value){
        console.log("setting is running");
        return Reflect.set(target, key, value)
    }
    
})

arr[0] = "min";  // setting is running
console.log(list); // ["min"]
console.log(arr);  // Proxy {0: "min"}
console.log(arr[0]);  // 0 ; string ;  getting is running ; min
 

上面看到使用Proxy,数组list还是原来的数组list,且仍然可以对数据进行劫持操作,而且并不会出现大面积的性能问题。

  • 代理对象为数组,且使用数组方法的情况
const list = [];
const arr = new Proxy(list,{
    get(target,key, receiver){
        console.log(`getting: key ${key}`);
        return Reflect.get(target,key)
    },
    set(target, key, value){
        console.log(`setting: key ${key}, value: ${value}`);
        return Reflect.set(target, key, value)
    }
    
})

arr.push("min");
 

想一想,上面的执行情况是怎么样的,自己打印下结果
是不是发现了Proxy的强大,四次操作分别对应执行取push, 取值length,赋值数组第0项,赋值length的操作。


我们继续回到observer/index.js的代码中

2.3 对于数组类型的处理
由于defineProperty的性能缺陷,Vue2中对数组的处理主要采用手动触发更新的方式来实现
找到observer/array.js文件:

import { def } from '../util/index'

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

methodsToPatch.forEach(function (method) {
  // cache original method
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // notify change
    ob.dep.notify()
    return result
  })
})
 

上面的代码中,将数组方法模拟,然后ob.dep.notify()实现手动触发订阅,来实现视图更新的过程
上面代码中,Object.create的使用值得我们思考,避免了对Array.prototype的污染。


2.4 对象的数据劫持
Observer class中看到,对于对象的处理采用defineReactive方法,我们来看下这个方法里面写了什么内容

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
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }

  let childOb = !shallow && observe(val)
  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
      }
      /* eslint-enable no-self-compare */
      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()
    }
  })
}
 

上面的代码中,涉及到dep.js,watcher.js中的内容,可以自己去找对应文件了解下
简单来说,dep.js:一个发布订阅器;watcher.js:视图依赖,模版解析时,实例化一个Watcher,Dep.target指向watcher实例,get时进行Watcher收集
Object.defineProperty进行get进行依赖收集,set进行触发更新。
上面的代码中是不是又看到了一个问题,我们来看看数据劫持中的这2行代码

let childOb = !shallow && observe(val)  
childOb = !shallow && observe(newVal)  //  set中处理新赋值的内容
 

是不是看出了什么,由于defineProperty只能依据进行key值处理,所以如果数据对象存在过深的结构层级,一开始我们就会执行大量的递归调用,来保证响应式。
所以这个问题上如果存在大量数据的初始化场景下,会存在极大的性能问题,相信这也是Vue团队废弃defineProperty而使用Proxy的原因之一。

  • 看下proxy对这方面的处理
function isObject (obj){
    return obj !== null && typeof obj === 'object'
}
const obj = {
    world: {
        person: "min"
    }
};
function reactive(obj){
    return new Proxy(obj,{
        get(target,key, receiver){
            console.log(`getting: key ${key}`);
            const res = Reflect.get(target,key);
            return isObject(res)? reactive(res): res
        },
        set(target, key, value){
            console.log(`setting: key ${key}, value: ${value}`);
            return Reflect.set(target, key, value)
        }
    })
}
const newObj = reactive(obj);
//已被注释 console.log(newObj.world.person);
newObj.world.person = {min: {age: 23}};
 

上面的过程是是惰性的,newObj.world.person = {min: {age: 23}};会先触发world属性的get,从而又会触发person的set。
区别defineProperty一开始就递归对子数据响应式定义,Proxy是获取到深层数据时,再利用reactive进一步定义响应式。

总结

先放上一张网上普遍存在的图:

20210411170904.png
(1)第一条不太正确: defineProperty可以监听数组变动,但存在性能风险
(2)第二条不太正确:defineProperty需要递归遍历实现整个对象劫持;Proxy采用惰性的方式进行劫持,也并非是一开始就劫持整个对象
(3)(4)(5):基本正确


谢谢你读完本篇文章,希望对你能有所帮助,如有问题欢迎各位指正。

我是him8(✿◡‿◡),如果觉得写得可以的话,请点个赞吧❤。

回复

我来回复
  • 暂无回复内容