Open maicFir opened 2 years ago
在 vue 中,我们知道它的核心思想是数据驱动视图,表现层我们知道在页面上,当数据发生变化,那么视图层也会发生变化。这种数据变化驱动视图背后依靠的是什么?
正文开始...
// 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来劫持对象。
vue2
Object.defineProperty
每劫持一组对象,首先会实例化一个Dep对象,每个拦截的对象属性都会动态添加get和set将传入的data或者prop变成响应式,在Object.defineProperty的get中,当我们访问对象的某个属性时,就会先调用get方法,依赖收集调用dep.depend(),当我们设置该属性值时就会调用set方法调用dep.notify()``派发更新所有的数据,在调用notify时会调用实例Watch的run,从而执行watch的回调方法。
Dep
get
set
data
prop
dep.depend()
dep.notify()``派发更新
notify
Watch
run
watch
在vue2源码中劫持对象实现数据驱动视图,那么我们依葫芦画瓢,化繁为简,实现一个自己的数据劫持吧。
新建一个index.js
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
html
<!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方法,从而进行更新页面数据操作。
index.html
options.age = 10
在源码里里面处理是相当复杂的,我们可以看到访问数据时,会先调用get方法,在dep.depend()进行依赖收集,然后再设置对象的值时,会调用set方法,派发更新操作。更多关于vue2响应式原理可以参考这篇文章响应式原理
vue3主要利用Proxy这个API来实现对象劫持的,关于Proxy可以看下阮一峰老师的 es6 教程proxy,全网讲解Proxy最好的教程了。
vue3
Proxy
API
继续用个例子来感受下
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)获取对应的属性值。
proxy.name = 111
new Proxy()
Reflect.set(target,key,val)
Relect.get(target, key)
这点与vue2中劫持数据的方式比较大,具体可以看下vue3源码响应式reactive实现
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来存储响应式数据的。
reative
proxy
WeakMap
相比较vue2的defineProperty,vue3的Proxy更加强大,因为代理对象对劫持的对象动态新增属性也一样有检测,而defineProperty就没有这种特性,它只能劫持已有的对象属性。
defineProperty
在vue2中数据劫持是用Object.defineProperty,当访问对象属性时会触发get方法进行依赖收集,当设置对象属性时会触发set方法进行派发更新操作。
在vue3中数据劫持时用new Proxy()来做的,可以动态的监测对象的属性新增与删除操作,效率高,实用简单。
本文示例code example
正文开始...
vue2 源码中的数据劫持
我们会发现其实在
vue2
源码中,本质上就是利用Object.defineProperty
来劫持对象。每劫持一组对象,首先会实例化一个
Dep
对象,每个拦截的对象属性都会动态添加get
和set
将传入的data
或者prop
变成响应式,在Object.defineProperty
的get
中,当我们访问对象的某个属性时,就会先调用get
方法,依赖收集调用dep.depend()
,当我们设置该属性值时就会调用set
方法调用dep.notify()``派发更新
所有的数据,在调用notify
时会调用实例Watch
的run
,从而执行watch
的回调方法。在
vue2
源码中劫持对象实现数据驱动视图,那么我们依葫芦画瓢,化繁为简,实现一个自己的数据劫持吧。新建一个
index.js
再新建一个
html
引入index.js
直接打开
index.html
当我们大开控制台时,我们直接修改options.age = 10
此时会触发拦截器的set
方法,从而进行更新页面数据操作。在源码里里面处理是相当复杂的,我们可以看到访问数据时,会先调用
get
方法,在dep.depend()
进行依赖收集,然后再设置对象的值时,会调用set
方法,派发更新操作。更多关于vue2
响应式原理可以参考这篇文章响应式原理vue3 是如何做数据劫持的
vue3
主要利用Proxy
这个API
来实现对象劫持的,关于Proxy
可以看下阮一峰老师的 es6 教程proxy,全网讲解Proxy
最好的教程了。继续用个例子来感受下
当我们在控制输入
proxy.name = 111
时,此时就会触发new Proxy()
内部的set
方法,而我们此时采用的是利用Reflect.set(target,key,val)
成功的设置了,在get
中,我们时用Relect.get(target, key)
获取对应的属性值。这点与
vue2
中劫持数据的方式比较大,具体可以看下vue3
源码响应式reactive
实现从源码中我们可以看出在
vue3
使用reative
初始化响应式数据时,实际上它就是就是一个被proxy
代理后的数据,并且使用WeakMap
来存储响应式数据的。相比较
vue2
的defineProperty
,vue3
的Proxy
更加强大,因为代理对象对劫持的对象动态新增属性也一样有检测,而defineProperty
就没有这种特性,它只能劫持已有的对象属性。总结
在
vue2
中数据劫持是用Object.defineProperty
,当访问对象属性时会触发get
方法进行依赖收集,当设置对象属性时会触发set
方法进行派发更新操作。在
vue3
中数据劫持时用new Proxy()
来做的,可以动态的监测对象的属性新增与删除操作,效率高,实用简单。本文示例code example