响应式原理 - 学习vue源码系列4.2
决定跟着黄轶老师的 vue2 源码课程好好学习下vue2
的源码,学习过程中,尽量输出自己的所得,提高学习效率,水平有限,不对的话请指正~
将vue 的源码clone 到本地,切换到分支2.6
。
Introduction
前面说了,vue 是怎么实现初始化数据渲染和组件化的,但当数据变动的时候,dom 又是怎么更新的呢。
先看个 demo
数据变更:
<div id="app">hello</div>
假设希望点击之后能将hello
变成hello world
的话,寻常怎么操作。
// 1.修改数据 const text = "hello world"; // 2.获取dom,监听事件 document.querySelector("#app").onclick = () => { // 3.手动操作dom重新渲染 app.innerHTML = text; };
如果需要多次修改的话,就不得不每次都手动操作 dom,重新渲染。
而 vue,去掉了手动操作dom
,根据数据变化自动操作 dom。
<div id="app" @click="changeMsg">{{ message }}</div> <script src="/Users/zhm/mygit/vue/dist/vue.js"></script> <script> var app = new Vue({ el: "#app", data: { message: "Hello Vue!", }, methods: { changeMsg() { // 这边只是 改变了数据,其余的由vue本身去更新dom,重新渲染 this.message = "Hello World!"; }, }, }); </script>
响应式对象
其实Object.defineProperty
应该都听过了,嗯,主要就是用这个属性。
普通获取对象属性,设置对象属性都可以用这个,但是比较麻烦,所以一般分场景使用。
/** 普通创建属性的方式: **/ var obj = { a: 1 }; /** defineProperty创建、获取和设置属性的方式: **/ var obj = {}; // !注意需要另设一个变量,存储属性的值。 let value = 1; Object.defineProperty(obj, "a", { get() { console.log("get"); return value; }, set(newValue) { console.log("set"); value = newValue; }, }); /* 当然获取属性和设置的话,是一样的 */ // 获取 console.log(obj.a); // 设置 obj.a = 2;
Object.defineProperty
的核心就是get
和set
,因为是函数,所以能做很多事。
所谓的劫持,每次获取/设置属性的时候,都会执行get/set
函数,既然是函数,自然能做一些别的事情。
所谓的响应式对象,当对象的某属性有
get/set
,就称为响应式对象。
简版的响应式Vue
先写个简版的响应式Vue
,大致有个印象,真实的源码处理的情景比较多,看懂简版的,再看源码,不容易迷路。。。
主要做了以下几件事:
- 用
vm._data
指向options.data
- 将
vm._data
上面的属性都代理到vm
上 vm._data.__ob__
指向Observer实例,而这个实例的value是data的响应式
const vm = new Vue({ data: { a: 1 } }); console.dir(vm); function Vue(options) { this.vm = this; this.$options = options; /* this._init() initState()开始 */ initData(this); /* this._init() initState()结束 */ } function initData(vm) { let data = vm.$options.data; vm._data = data; // 将data上面的属性直接挂在vm上 Object.keys(data).forEach((key) => { /* proxy(vm, "_data", key) 开始 */ Object.defineProperty(vm, key, { enumerable: true, configurable: true, get() { return vm._data[key]; }, set(newValue) { vm.data[key] = newValue; }, }); /* proxy(vm, "_data", key) 结束 */ }); /* observe(data) 开始 */ data.__ob__ = data.__ob__ || new Observer(data); /* observe(data) 结束 */ } function Observer(data) { this.value = data; // this.dep = new Dep(); data.__ob__ = this; Object.keys(data).forEach((key) => { defineReactive(data, key); }); } // defineReactive将 obj.x 这种定义属性的方式 变成Object.defineProperty(obj, 'x' , {get(){}} function defineReactive(data, key) { // const dep = new Dep(); let value = data[key]; Object.defineProperty(data, key, { enumerable: true, configurable: true, get: function reactiveGetter() { // dep.depend(); return value; }, set: function reactiveSetter(newValue) { // dep.notify(); value = newValue; }, }); }
initState
Vue 的构造函数,开始就是this._init(..)
,而Vue.prototype._init = function(){initState(this)}
。
这里的initState
就是让Vue
的实例变成响应式对象的关键,这个方法就是对options
进行各种初始化的操作,而本文的重点是对data/props
的处理。
// src/core/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); } }
props 的处理
先看initProps
,主要其实就是
- 遍历
options.props
- 每个 prop 变成响应式(set/get),且每个 prop,也同步到
vm._props.xx
- 通过
proxy
把vm._props.xxx
的访问代理到vm.xxx
上
// src/core/instance/state.js function initProps(vm: Component, propsOptions: Object) { const propsData = vm.$options.propsData || {}; const props = (vm._props = {}); // cache prop keys so that future props updates can iterate using Array // instead of dynamic object key enumeration. const keys = (vm.$options._propKeys = []); const isRoot = !vm.$parent; // root instance props should be converted if (!isRoot) { toggleObserving(false); } for (const key in propsOptions) { keys.push(key); const value = validateProp(key, propsOptions, propsData, vm); defineReactive(props, key, value); // static props are already proxied on the component's prototype // during Vue.extend(). We only need to proxy props defined at // instantiation here. if (!(key in vm)) { proxy(vm, `_props`, key); } } toggleObserving(true); }
data 的处理
data 的初始化也做了两件事:
- 遍历
data
对象,每一个vm._data.xx
通过proxy
代理到vm._data
- 调用
observe
观察data
变化,将其也变成响应式
// src/core/instance/state.js function initData(vm: Component) { let data = vm.$options.data; data = vm._data = typeof data === "function" ? getData(data, vm) : data || {}; if (!isPlainObject(data)) { data = {}; } // 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 (!isReserved(key)) { proxy(vm, `_data`, key); } } // observe data observe(data, true /* asRootData */); }
proxy 代理
初始化 data 和 props 一个关键操作,就是让他们变成响应式,然后代理到vm
上。
代理?其实就是就是一个中介。本身只是一个线索,会连接到真实的资源。
举个例子,看下面的obj
,有个data
属性,现在想obj.a
也可以访问到 a 属性,怎么办?
此时obj.a
就是一个中介,其真实连接的资源是obj.data.a
。
const obj = { data: { a: 1 } };
其实还是利用上面的Object.defineProperty
Object.defineProperty(obj, "a", { get() { // 这里的obj.data.a就相当于存储变量 return obj.data.a; }, set(newValue) { obj.data.a = newValue; }, }); // 1 console.log(obj.a);
现在看源码里,对于proxy
的实现:
// src/core/instance/state.js // const noop = function empty(){} const sharedPropertyDefinition = { enumerable: true, configurable: true, get: noop, set: noop, }; // target相当于obj,sourceKey相当于data,key相当于a // proxy就是可以让vm.x 代理到 vm.data.x export function proxy(target: Object, sourceKey: string, key: string) { sharedPropertyDefinition.get = function proxyGetter() { return this[sourceKey][key]; }; sharedPropertyDefinition.set = function proxySetter(val) { this[sourceKey][key] = val; }; Object.defineProperty(target, key, sharedPropertyDefinition); }
observe:给增加一个 Observer 实例
observe
的功能就是用来监测数据的变化,给非 VNode
的对象类型数据添加一个 Observer
,如果已经添加过则直接返回,否则在满足一定条件下去实例化一个 Observer
对象实例。
// src/core/observer/index.js 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; }
Observer 类:给对象的每个属性添加 getter 和 setter
Observer
是一个类,它的作用是给对象的属性添加 getter 和 setter,用于依赖收集和派发更新:
/** * 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. */ export class Observer { value: any; dep: Dep; vmCount: number; // number of vms that has this object as root $data constructor(value: any) { this.value = value; // 实例化 Dep 对象 this.dep = new Dep(); this.vmCount = 0; def(value, "__ob__", this); if (Array.isArray(value)) { const augment = hasProto ? protoAugment : copyAugment; augment(value, arrayMethods, arrayKeys); this.observeArray(value); } else { this.walk(value); } } /** * Walk through each property and convert them into * getter/setters. This method should only be called when * value type is Object. */ 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]); } } }
Observer
的构造函数逻辑很简单:
- 实例化
Dep
对象, - 通过执行
def
函数把自身实例添加到数据对象value
的__ob__
属性上
def
就是给一个对象,定义一个属性,设置一个属性值,默认此属性是不可遍历的。
// def的定义 // src/core/util/lang.js export function def(obj: Object, key: string, val: any, enumerable?: boolean) { Object.defineProperty(obj, key, { value: val, enumerable: !!enumerable, writable: true, configurable: true, }); }
def
相当于Object.defineProperty
的简单封装。
defineReactive:给对象的某个属性动态添加 getter 和 setter
defineReactive
的功能就是给对象动态添加 getter
和 setter
,让对象成为响应式对象:
// src/core/observer/index.js /** * Define a reactive property on an Object. */ export function defineReactive( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean ) { // 初始化 Dep 对象的实例 const dep = new Dep(); // 拿到 obj 的属性描述符 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 (setter) { setter.call(obj, newVal); } else { val = newVal; } childOb = !shallow && observe(newVal); dep.notify(); }, }); }
defineReactive
函数:
- 最开始初始化
Dep
对象的实例, - 接着拿到
obj
的属性描述符, - 然后对子对象递归调用
observe
方法
这样就保证了无论 obj
的结构多复杂,它的所有子属性也能变成响应式的对象, 这样我们访问或修改 obj
中一个嵌套较深的属性,也能触发 getter
和 setter
。
最后利用 Object.defineProperty
去给 obj
的属性 key
添加 getter
和 setter
。
总结
响应式对象,核心就是利用 Object.defineProperty
给数据添加了 getter
和 setter
。 这样在访问数据以及写数据的时候能自动执行一些逻辑:
getter
做的事情是依赖收集setter
做的事情是派发更新