Vue双向数据绑定 源码理解 Proxy到底有什么不一样

565 阅读4分钟

双向数据绑定

双向数据绑定就是 页面 和 数据 无论哪一方发生改变,都可以改变当前的数据。

Vue数据双向绑定是通过数据劫持结合发布者-订阅者模式的方式来实现的。

基础知识

体验Object.defineProperty

如果不了解Object.defineProperty 传送门

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <input type="text" id="input" />
    <p id="data"></p>
  </body>
  <script>
    const obj = {}
    // 获取 input标签原生dom
    const input = document.getElementById('input')
    // 数据劫持。
    // 当 obj.name 时必须触发get()才会返回出来。
    // 当 obj.name = "xx" 时,这里是 obj.name = input.value ,触发了set 进而将data中的值也赋值了。
    Object.defineProperty(obj, 'name', {
      configurable: true,
      enumerable: true,
      get() {
        console.log(`obj.name 也就是获取值时触发`);
        return input.value
      },
      set(newVal) {
        console.log(`obj.name='xx' 也就是设置值时触发`);
        input.value = newVal
        document.getElementById('data').innerHTML = newVal
      },
    })
    // 监听输入框,实现视图->数据的绑定
    input.addEventListener('keyup', () => {
      obj.name = input.value
    })

    // 这个domo是有问题了,get是无法触发,只有set触发了,只是一个deomo而已。
    // 后面的代码,get() 将是至关重要
  </script>
</html>

简易版.png

完整版

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title></title>
  </head>

  <body>
    <div id="app">
      <div>
        <input v-model="value" />
        <p v-text="value"></p>
      </div>
    </div>
  </body>

  <script>
    // 收集依赖
    class Dep {
      constructor() {
        this.subs = []
      }

      addSub(sub) {
        this.subs.push(sub)
      }

      removeSub(sub) {
        remove(this.subs, sub)
      }

      depend() {
        if (window.target) {
          this.addSub(window.target)
        }
      }

      notify() {
        const subs = this.subs.slice()
        for (let i = 0, l = subs.length; i < l; i++) {
          subs[i].update()
        }
      }
    }

    // 使数据变成setter getter
    /**
     * Observer类会附加到每一个被侦测的object上。
     * 一旦被附加上,Observer会将object的所有属性转换为getter/setter的形式
     * 来收集属性的依赖,并且当属性发生变化时会通知这些依赖
     */
    class Observer {
      constructor(value) {
        this.value = value

        if (!Array.isArray(value)) {
          this.walk(value)
        }
      }

      /**
       * walk会将每一个属性都转换成getter/setter的形式来侦测变化
       * 这个方法只有在数据类型为Object时被调用
       */
      walk(obj) {
        const keys = Object.keys(obj)
        for (let i = 0; i < keys.length; i++) {
          defineReactive(obj, keys[i], obj[keys[i]])
        }
      }
    }

    function defineReactive(data, key, val) {
      // 新增,递归子属性
      if (typeof val === 'object') {
        new Observer(val)
      }
      let dep = new Dep()
      Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function () {
          console.log('触发了get' + val)
          dep.depend()
          return val
        },
        set: function (newVal) {
          console.log('触发了set' + newVal)
          if (val === newVal) {
            return
          }
          val = newVal
          dep.notify()
        },
      })
    }

    class Watcher {
      constructor(vm, node, name) {
        this.vm = vm
        this.node = node
        this.name = name
        this.value = this.update()
      }

      get() {
        window.target = this
        // 收集依赖,触发get,
        let value = this.vm.data[this.name]
        window.target = undefined
        return value
      }

      update() {
        this.value = this.get()
        this.node.innerText = this.value
      }
    }

    // 模板解析
    function Compile(el, vue, data) {
      // 关联自定义特性
      if (el.attributes) {
        // [].forEach.call(this,()=>{}) ==> Array.prototype.forEach(()=>{})
        // 为什么不直接使用 el.attributes.forEach(()=>{}) , 因为这是dom结构不支持,使用原生for循环可以,但是采用下面方案比较
        [].forEach.call(el.attributes, (attribute) => {
          if (attribute.name.includes('v-')) {
            Update[attribute.name](el, vue, data, attribute.value)
          }
        })
      }

      // 递归解析所有DOM
      ;[].forEach.call(el.childNodes, (child) => Compile(child, vue, data))
    }

    // 自定义特性对应的事件
    const Update = {
      'v-text'(el, vue, data, key) {
        // 初始化DOM内容
        el.innerText = data[key]
        // 收集依赖
        new Watcher(vue, el, key) 
      },
      'v-model'(input, vue, data, key) {
        // 收集依赖
        new Watcher(vue, input, key)
        // 初始化Input默认值
        input.value = data[key]
        // 监听控件的输入事件,并更新数据
        input.addEventListener('keyup', (e) => {
          vue.data[key] = e.target.value
        })
      },
    }

    function Vue(options) {
      this.data = options.data
      new Observer(this.data) // 劫持数据
      let el = document.getElementById(options.el)
      let vue = this
      Compile(el, vue, this.data) // 编译模版
    }

    let vm = new Vue({
      el: 'app',
      data: {
        value: 'juice',
      },
    })
  </script>
</html>

完整版.png

proxy 和Object.definepropety的优缺点

vue2.0+把Object.definepropety用的出神入化,最终也没有把监听数组的操作「要么直接赋值,要么使用splice」,对象的增删也是无法检测,但是他提供了$set 以及 $delete 。最终遗憾的没有实现最直接的监听。

vue3.0 使用的是proxy 优点就是上面所说的他都能实现,而且还拓展了13种方法,性能也可以得到提升,传送门,缺点就是浏览器兼容性的,这也是尤大大考虑将proxy放在vue3.0吧。

Vue源码解读

入口文件

文件路径:core/instance/init

initMixin(){
	vm._self = vm;
 	initLifecycle(vm);
 	initEvents(vm);
 	initRender(vm); // vm.$slots
 	callHook(vm, "beforeCreate");
 	initInjections(vm); // resolve injections before data/props
 	initState(vm);
 	initProvide(vm); // resolve provide after data/props
 	callHook(vm, "created");
}

initState(vm) 初始化并监听数据的调用方法,这里设计 props watch computed data

文件路径:core/instance/state
export function initState(vm: Component) {
  vm._watchers = [];
  const 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 */); // 如果data为空 创建一个观察对象
  }
  if (opts.computed) initComputed(vm, opts.computed); // 初始化计算属性
  if (opts.watch && opts.watch !== nativeWatch) {
    // 初始化watch
    initWatch(vm, opts.watch); // 初始化watch
  }
}

initData(vm) 初始化监听data方法,数据双向数据绑定在data上面更加全面

判断对象中的是否存在重复的属性,比如 methods 、 computed、props 、data 在同一个实例上面是不能有重名的 
function initData(vm: Component) {
  let data = vm.$options.data;
  data = vm._data = typeof data === "function" ? getData(data, vm) : data || {};
  // 判断如果不是对象,警告
  if (!isPlainObject(data)) {
    data = {};
    process.env.NODE_ENV !== "production" &&
      warn(
        "data functions should return an object:\n" +
          "https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function",
        vm
      );
  }
  // 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 是否已经存在data中的key,存在,则警告
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        );
      }
    }
     // 判断 props 是否已经存在data中的key,存在,则警告
    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
        );
     } 
     // _ 或者 $ 开头的key是无法被代理的,因为他们都是私有的属性,比如$store、$router
    else if (!isReserved(key)) {
      // 这行代码的作用是为了把监听的数据代理到了 data 上面,方便我们使用this方法
      proxy(vm, `_data`, key);
    }
  }
  // observe data
  observe(data, true /* asRootData */);
}

observe(data,true) 是准备开始监听,到了另外一个 observe 模块

Observe

Observe 是主要用于监听数据,并收集依赖。 文件路径:core/observer/index

/**
 * 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.
 */
// 判断当前value是否需要监听,
// 需要则 new Observer(value)
// 不需要则 value.__ob__ 返回缓存
export function observe(value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return;
  }
  let ob: Observer | void;
  // 如果该值已经被 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) {
    // 监听的数量+1
    ob.vmCount++;
  }
  return ob;
}


/**
 * 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.
 */
// 这是一个监听数据的发起函数,通过 getter/setter 去处理
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(); // 给当前Observer 增加dep属性
    this.vmCount = 0; // 当前监听数据的数量
    //  Define a property,将数据都定义在 __ob__ 上
    def(value, "__ob__", this);
    if (Array.isArray(value)) {
      // 判断 能不能使用 __proto__
      if (hasProto) {
        // 将数组的方法进行监听,当用户使用数组改变data中的数组的时候就会触发 setter
        protoAugment(value, arrayMethods);
      } else {
        copyAugment(value, arrayMethods, arrayKeys);
      }
      // 监听数组的,这个方法的原理就是递归监听数据里面的每一项
      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.
   */
  // 如果是对象的话,走defineReactive,最后面都会走这里
  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]);
    }
  }
}

/**
 * Define a reactive property on an Object.
 */
// 定义响应式数据
export function defineReactive(
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep();
  // Object.getOwnPropertyDescriptor获取当前对象的描述
  // {
  //   configurable: true
  //   enumerable: true
  //   value: 1
  //   writable: true
  // }
  // 如果不可以配置直接return,property.configurable === false

  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 去监听数据的变化
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      // 取数据时进行依赖收集
      const value = getter ? getter.call(obj) : val;
      // 全局收集依赖对象
      if (Dep.target) {
        dep.depend(); // dep中收集的是watcher
        if (childOb) {
          // 让对象本身进行依赖收集
          childOb.dep.depend(); // {a:1}  => {} 外层对象
          if (Array.isArray(value)) {
            // 如果是数组  {arr:[[],[]]} vm.arr取值只会让arr属性和外层数组进行收集
            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();
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return;
      if (setter) {
        setter.call(obj, newVal);
      } else {
        val = newVal;
      }
      childOb = !shallow && observe(newVal);
      // getter收集到的状态,让watcher去更新
      dep.notify();
    },
  });
}

更新

由上面知道 dep.notify() ,是触发更新的,其实去走watch中的代码,下面来看一下具体流程。

文件路径:core/observer/dep
/**
 * A dep is an observable that can have multiple
 * directives subscribing to it.
 */
export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

  constructor() {
    this.id = uid++;
    this.subs = [];
  }

  addSub(sub: Watcher) {
    this.subs.push(sub);
  }

  removeSub(sub: Watcher) {
    remove(this.subs, sub);
  }

  depend() {
    if (Dep.target) {
      Dep.target.addDep(this);
    }
  }

  notify() {
    // stabilize the subscriber list first
    const subs = this.subs.slice();
    if (process.env.NODE_ENV !== "production" && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort((a, b) => a.id - b.id);
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update();
    }
  }
}


subs[i].update() 触发  watch中的 update方法

文件路径:core/server/watcher

 /**
   * Subscriber interface.
   * Will be called when a dependency changes.
   */
update() {
  /* istanbul ignore else */
  if (this.lazy) {
    this.dirty = true;
  } else if (this.sync) {
    this.run();
  } else {
    queueWatcher(this);
  }
}



/**
 * Push a watcher into the watcher queue.
 * Jobs with duplicate IDs will be skipped unless it's
 * pushed when the queue is being flushed.
 */
export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      queue.push(watcher)
    } else {
      // if already flushing, splice the watcher based on its id
      // if already past its id, it will be run next immediately.
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    // queue the flush
    if (!waiting) {
      waiting = true

      if (process.env.NODE_ENV !== 'production' && !config.async) {
        flushSchedulerQueue()
        return
      }
      nextTick(flushSchedulerQueue)
    }
  }
}


调用 flushSchedulerQueue 文件路径:core/observer/scheduler
/**
 * Flush both queues and run the watchers.
 */
function flushSchedulerQueue () {
  currentFlushTimestamp = getNow()
  flushing = true
  let watcher, id

  // Sort queue before flush.
  // This ensures that:
  // 1. Components are updated from parent to child. (because parent is always
  //    created before the child)
  // 2. A component's user watchers are run before its render watcher (because
  //    user watchers are created before the render watcher)
  // 3. If a component is destroyed during a parent component's watcher run,
  //    its watchers can be skipped.
  queue.sort((a, b) => a.id - b.id)

  // do not cache length because more watchers might be pushed
  // as we run existing watchers
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    watcher.run()
    // in dev build, check and stop circular updates.
    if (process.env.NODE_ENV !== 'production' && has[id] != null) {
      circular[id] = (circular[id] || 0) + 1
      if (circular[id] > MAX_UPDATE_COUNT) {
        warn(
          'You may have an infinite update loop ' + (
            watcher.user
              ? `in watcher with expression "${watcher.expression}"`
              : `in a component render function.`
          ),
          watcher.vm
        )
        break
      }
    }
  }

  // keep copies of post queues before resetting state
  const activatedQueue = activatedChildren.slice()
  const updatedQueue = queue.slice()

  resetSchedulerState()

  // call component updated and activated hooks
  callActivatedHooks(activatedQueue)
  callUpdatedHooks(updatedQueue)

  // devtool hook
  /* istanbul ignore if */
  if (devtools && config.devtools) {
    devtools.emit('flush')
  }
}


最后面执行 watch 中的run 方法
  /**
   * Scheduler job interface.
   * Will be called by the scheduler.
   */
  run() {
    if (this.active) {
      const value = this.get();
      if (
        value !== this.value ||
        // Deep watchers and watchers on Object/Arrays should fire even
        // when the value is the same, because the value may
        // have mutated.
        isObject(value) ||
        this.deep
      ) {
        // set new value
        const oldValue = this.value;
        this.value = value;
        if (this.user) {
          try {
            this.cb.call(this.vm, value, oldValue);
          } catch (e) {
            handleError(
              e,
              this.vm,
              `callback for watcher "${this.expression}"`
            );
          }
        } else {
          this.cb.call(this.vm, value, oldValue);
        }
      }
    }
  }

Proxy

在这里引用mdn的一句话,Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。原来的对象将不会受影响。

只接收两个参数。
target:代理的对象。
handler:13种方法,详见mdn。
const p = new Proxy(target, handler)

当我们代理数组的时候。
原数组
const arr = [1, 2, 3, 4]

代理数组
const newArr = new Proxy(arr, {
  get: function (target, key, receiver) {
    console.log('target:', target, 'key:', key, 'receiver:', receiver)
    return target[key]
  },
  set: function (target, key, value, receiver) {
    console.log('target:', target, 'key:', key, 'value:', value, 'receiver:', receiver)
    return (target[key] = value)
  },
})
console.log(newArr, 'newArr') //  Proxy {0: 1, 1: 2, 2: 3, 3: 4} "newArr" 所代理的新对象
console.log(newArr[0]) // 触发get target: (4) [1, 2, 3, 4] key: 0 receiver: Proxy {0: 1, 1: 2, 2: 3, 3: 4}  ,打印1
console.log((newArr[0] = 11)) // 触发set target: (4) [1, 2, 3, 4] key: 0 value: 11 receiver: Proxy {0: 1, 1: 2, 2: 3, 3: 4} ,打印11
console.log(newArr[0]) // 修改了newArr target: (4) [11, 2, 3, 4] key: 0 receiver: Proxy {0: 11, 1: 2, 2: 3, 3: 4}
console.log(newArr.push(5)) // 5
console.log(newArr) // Proxy {0: 11, 1: 2, 2: 3, 3: 4, 4: 5}

当我们代理对象的时候。
// 原对象
const obj = { name: 'juice', age: '18' }

// 代理的对象
const newObj = new Proxy(obj, {
  get: function (target, key, receiver) {
    console.log('target:', target, 'key:', key, 'receiver:', receiver)
    return target[key]
  },
  set: function (target, key, value, receiver) {
    console.log('target:', target, 'key:', key, 'value:', value, 'receiver:', receiver)
    return (target[key] = value)
  },
})
console.log(newObj, 'newObj') // Proxy {name: "juice", age: "23"} "newObj"
console.log(newObj.name) // juice
console.log((newObj.name = 'JUICE')) // JUICE
console.log(newObj.name) // JUICE
// 新增属性
console.log((newObj.hobby = '王者荣耀')) //  王者荣耀
console.log(newObj) //  Proxy {name: "JUICE", age: "23", hobby: "王者荣耀"}

   
那么按照代理的话,把Object.defindpropety 替换成proxy其实是特别简单的,基本Vue2.0差不多,而且新增数据,和数组都能监听得到。

Vue3.0中的Proxy

Vue2.0
对象:添加属性的时候 obj.a = 1 会无法被Vue2劫持,必须使用Vue2提供的$set方法来进行更新
因为defineProperty只能对当前对象中已有的属性进行监听一个个去监听,新增加进来的,需要重新使用defineProperty,所以Vue是不知道什么时候去使用defineProperty。

Vue3.0中,使用proxy来进行数据代理没有这个顾虑
proxy对于数据的代理,是能够响应新增的属性,当新增一个属性的时候,可以响应到get中,对当前对象进行代理
const a = new Proxy({
    a: 1,
    b: 2,
}, {
    get: function(target, value) {
        console.log('get', obj, value);
        return target[key]
    },
    set: function(target, prop, value) {
        console.log('set', obj, prop, value);
        return (target[key] = value)
    },
})


数组:Vue针对数组是多做了一层处理,代理了数组的7个方法,这是因为使用Object.defineProperty在数组上面无法监听数组的变化,需要通过方法去修改值。
const a = new Proxy([1, 2], {
  get: function(target, key) {
    console.log('get', 'target', target, 'key', key)
    return target[key]
  },
  set: function(target, key, value) {
    console.log('set', 'target', target, 'key', key, value)
    return (target[key] = value)
  }
})
console.log(a, 'a')
a.push(1)

get [1,2] push
get [1,2] length
set [1,2] 2 3
set [1,2, 3] length 3

由于proxy会触发两次,Vue3只会在prop为length值才进行更新

Vue2 如果没有设置Object.freeze默认递归data里面的数据做响应式处理,所以不建议在data中的数据定义嵌套太多层,Vue3的proxy是懒递归,不会一上来就递归,性能就相对好点

各位看官如遇上不理解的地方,或者我文章有不足、错误的地方,欢迎在评论区指出,感谢阅读。