Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vue数据双向绑定实现原理 #46

Open
maicFir opened this issue Aug 11, 2022 · 0 comments
Open

Vue数据双向绑定实现原理 #46

maicFir opened this issue Aug 11, 2022 · 0 comments

Comments

@maicFir
Copy link
Owner

maicFir commented Aug 11, 2022

在 vue 中,我们知道它的核心思想是数据驱动视图,表现层我们知道在页面上,当数据发生变化,那么视图层也会发生变化。这种数据变化驱动视图背后依靠的是什么?

正文开始...

vue2 源码中的数据劫持

// src/core/instance/observer/index.js
/**
 * Define a reactive property on an Object.
 */
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();
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return;
      if (setter) {
        setter.call(obj, newVal);
      } else {
        val = newVal;
      }
      childOb = !shallow && observe(newVal);
      dep.notify();
    }
  });
}

我们会发现其实在vue2源码中,本质上就是利用Object.defineProperty来劫持对象。

每劫持一组对象,首先会实例化一个Dep对象,每个拦截的对象属性都会动态添加getset将传入的data或者prop变成响应式,在Object.definePropertyget中,当我们访问对象的某个属性时,就会先调用get方法,依赖收集调用dep.depend(),当我们设置该属性值时就会调用set方法调用dep.notify()``派发更新所有的数据,在调用notify时会调用实例Watchrun,从而执行watch的回调方法。

vue2源码中劫持对象实现数据驱动视图,那么我们依葫芦画瓢,化繁为简,实现一个自己的数据劫持吧。

新建一个index.js

// index.js
var options = {
  name: 'Maic',
  age: 18,
  from: 'china'
};
const renderHtml = (data, key) => {
  const appDom = document.getElementById('app');
  appDom.innerHTML = `<div>
    <p>options:${JSON.stringify(options)}</p>
    <p>${key}: ${JSON.stringify(data)}</p>
  </div>`;
};
const defineReactive = (target, key) => {
  let val = target[key];
  Object.defineProperty(target, key, {
    enumerable: true,
    configurable: true,
    get: function () {
      return val;
    },
    set: function (nval) {
      console.log(nval, '==nval');
      val = nval;
      renderHtml(nval, key);
    }
  });
};
Object.keys(options).forEach((key) => {
  defineReactive(options, key);
});
renderHtml(options, 'name');

再新建一个html引入index.js

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>vue2-reactive</title>
  </head>
  <body>
    <div id="app"></div>
    <script src="./index.js"></script>
  </body>
</html>

直接打开index.html

当我们大开控制台时,我们直接修改options.age = 10此时会触发拦截器的set方法,从而进行更新页面数据操作。

在源码里里面处理是相当复杂的,我们可以看到访问数据时,会先调用get方法,在dep.depend()进行依赖收集,然后再设置对象的值时,会调用set方法,派发更新操作。更多关于vue2响应式原理可以参考这篇文章响应式原理

vue3 是如何做数据劫持的

vue3主要利用Proxy这个API来实现对象劫持的,关于Proxy可以看下阮一峰老师的 es6 教程proxy,全网讲解Proxy最好的教程了。

继续用个例子来感受下

var options = {
  name: 'Maic',
  age: 18,
  from: 'china'
};
const renderHtml = (data, key) => {
  const appDom = document.getElementById('app');
  appDom.innerHTML = `<div>
    <p>options:${JSON.stringify(options)}</p>
    <p>${key}: ${JSON.stringify(data)}</p>
  </div>`;
};
renderHtml(options, 'name');
var proxy = new Proxy(options, {
  get: function (target, key, receiver) {
    console.log(key, receiver);
    return Reflect.get(target, key);
  },
  set: function (target, key, val, receiver) {
    console.log(key, val, receiver);
    renderHtml(val, key);
    return Reflect.set(target, key, val);
  }
});

当我们在控制输入proxy.name = 111时,此时就会触发new Proxy()内部的set方法,而我们此时采用的是利用Reflect.set(target,key,val)成功的设置了,在get中,我们时用Relect.get(target, key)获取对应的属性值。

这点与vue2中劫持数据的方式比较大,具体可以看下vue3源码响应式reactive实现

// package/reactivity/src/reactive.ts
function createReactiveObject(target: Target, isReadonly: boolean, baseHandlers: ProxyHandler<any>, collectionHandlers: ProxyHandler<any>, proxyMap: WeakMap<Target, any>) {
  if (!isObject(target)) {
    if (__DEV__) {
      console.warn(`value cannot be made reactive: ${String(target)}`);
    }
    return target;
  }
  // target is already a Proxy, return it.
  // exception: calling readonly() on a reactive object
  if (target[ReactiveFlags.RAW] && !(isReadonly && target[ReactiveFlags.IS_REACTIVE])) {
    return target;
  }
  // target already has corresponding Proxy
  const existingProxy = proxyMap.get(target);
  if (existingProxy) {
    return existingProxy;
  }
  // only a whitelist of value types can be observed.
  const targetType = getTargetType(target);
  if (targetType === TargetType.INVALID) {
    return target;
  }
  const proxy = new Proxy(target, targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers);
  proxyMap.set(target, proxy);
  return proxy;
}

从源码中我们可以看出在vue3使用reative初始化响应式数据时,实际上它就是就是一个被proxy代理后的数据,并且使用WeakMap来存储响应式数据的。

相比较vue2defineProperty,vue3Proxy更加强大,因为代理对象对劫持的对象动态新增属性也一样有检测,而defineProperty就没有这种特性,它只能劫持已有的对象属性。

总结

  • vue2中数据劫持是用Object.defineProperty,当访问对象属性时会触发get方法进行依赖收集,当设置对象属性时会触发set方法进行派发更新操作。

  • vue3中数据劫持时用new Proxy()来做的,可以动态的监测对象的属性新增与删除操作,效率高,实用简单。

  • 本文示例code example

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant