Vue数据双向绑定实现原理

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

在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

c25627255ba23fcacbaf9ba4919b6eb4.png

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


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


vue3是如何做数据劫持的


vue3主要利用Proxy这个API来实现对象劫持的,关于Proxy可以看下阮一峰老师的es6教程proxy[2],全网讲解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[3]
相关文章
|
5天前
|
JavaScript 前端开发
如何在 Vue 项目中配置 Tree Shaking?
通过以上针对 Webpack 或 Rollup 的配置方法,就可以在 Vue 项目中有效地启用 Tree Shaking,从而优化项目的打包体积,提高项目的性能和加载速度。在实际配置过程中,需要根据项目的具体情况和需求,对配置进行适当的调整和优化。
|
5天前
|
存储 缓存 JavaScript
在 Vue 中使用 computed 和 watch 时,性能问题探讨
本文探讨了在 Vue.js 中使用 computed 计算属性和 watch 监听器时可能遇到的性能问题,并提供了优化建议,帮助开发者提高应用性能。
|
5天前
|
存储 缓存 JavaScript
如何在大型 Vue 应用中有效地管理计算属性和侦听器
在大型 Vue 应用中,合理管理计算属性和侦听器是优化性能和维护性的关键。本文介绍了如何通过模块化、状态管理和避免冗余计算等方法,有效提升应用的响应性和可维护性。
|
5天前
|
存储 缓存 JavaScript
Vue 中 computed 和 watch 的差异
Vue 中的 `computed` 和 `watch` 都用于处理数据变化,但使用场景不同。`computed` 用于计算属性,依赖于其他数据自动更新;`watch` 用于监听数据变化,执行异步或复杂操作。
|
4天前
|
JavaScript 前端开发 UED
vue学习第二章
欢迎来到我的博客!我是一名自学了2年半前端的大一学生,熟悉JavaScript与Vue,目前正在向全栈方向发展。如果你从我的博客中有所收获,欢迎关注我,我将持续更新更多优质文章。你的支持是我最大的动力!🎉🎉🎉
|
6天前
|
存储 JavaScript 开发者
Vue 组件间通信的最佳实践
本文总结了 Vue.js 中组件间通信的多种方法,包括 props、事件、Vuex 状态管理等,帮助开发者选择最适合项目需求的通信方式,提高开发效率和代码可维护性。
|
4天前
|
JavaScript 前端开发 开发者
vue学习第一章
欢迎来到我的博客!我是瑞雨溪,一名热爱JavaScript和Vue的大一学生。自学前端2年半,熟悉JavaScript与Vue,正向全栈方向发展。博客内容涵盖Vue基础、列表展示及计数器案例等,希望能对你有所帮助。关注我,持续更新中!🎉🎉🎉
|
6天前
|
存储 JavaScript
Vue 组件间如何通信
Vue组件间通信是指在Vue应用中,不同组件之间传递数据和事件的方法。常用的方式有:props、自定义事件、$emit、$attrs、$refs、provide/inject、Vuex等。掌握这些方法可以实现父子组件、兄弟组件及跨级组件间的高效通信。
|
11天前
|
JavaScript
Vue基础知识总结 4:vue组件化开发
Vue基础知识总结 4:vue组件化开发
|
11天前
|
存储 JavaScript
Vue 状态管理工具vuex
Vue 状态管理工具vuex