源码层面分析 Vue2 数据驱动的实现原理
数据驱动的基本思想是:使用数据来描述应用的状态,将界面的修改与数据的修改绑定起来,实现数据的任何修改都能直接实时的反映到界面上。
一 v-model(双向绑定) 的实现原理
双向绑定的方向之一:dom 变化 => 更新数据,这个过程就是 input / select 等元素的 value 改变,结合 change / click 等事件实现的,过程较为简单,不在此讨论。
下面介绍双向绑定的另一个方向:数据变化 => 更新 dom。
要实现这个过程需要解决两个关键问题:
- 如何知道数据更新。
- 数据更新后,如何通知变化。
1. 如何知道数据更新
js 的原生方法 Object.defineProperty(),具有监听对象属性的存取器属性,即对象属性 setter 和 getter。当访问一个属性或修改一个属性的时候,该属性的 set 和 get 方法会被触发,通过这一特性可以劫持该属性。
let data ={ name:'tcy' };
Object.defineProperty(data,'name',{
set: function(newValue) {
console.log('更新了data的name:' + newValue);
},
get: function() {
console.log('获取data数据name');
}
})
const name = data.name; // 访问,会触发代码里的 get 方法
data.name = "fyn"; // 修改,会触发代码里的 set 方法
2. 数据更新后,如何通知变化
vue 采用了观察者模式(发布/订阅模式)来收集和通知数据的变化。
- observe: 观察者,即监听数据,并为每个数据建立一个发布类(Dep)。
- Dep:发布类,维护数据与订阅者之间的关系,当数据发生更新时,通知该数据的订阅者(Watcher)。
- Watcher:订阅类,接受发布类的数据变更通知。
下面是一段从源码中拷出简化的代码,可在浏览前上直接运行调试:
/**
* 发布器
*/
let uid = 0; // 发布器的uid
class Dep{
constructor () {
this.id = uid++; // 发布器的标识,每次加 1 以确保唯一
this.subs = []; // 订阅者集合
}
// 添加订阅者实例对象
addSub(watcher){
this.subs.push(watcher);
}
//移除订阅者实例对象
removeSub (watcher) {
remove(this.subs, watcher)
}
// 依赖收集函数,在 getter 中执行,在 Dep.target 上找到当前 watcher,并添加依赖
depend() {
Dep.target && Dep.target.addDep(this)
}
//通知所有订阅者
notify () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update();//更新
}
}
}
//记录当前的watcher实例
Dep.target = null;
const targetStack = []
function pushTarget (_target) {
if (Dep.target) targetStack.push(Dep.target)
Dep.target = _target
}
function popTarget () {
Dep.target = targetStack.pop()
}
/**
* 订阅者 Watcher(core/observer/watcher.js)
* 在 vue 实例初始化(initState)的时候,执行到 initComputed(core/instance/state.js) / initWatch 时会创建 Watcher 实例
* 组件挂载时会创建 Watcher 实例(core/instance/lifecycle.js mountComponent)
*/
class Watcher{
constructor (
vm,//vm数据对象
expOrFn,//待监听的属性表达式
cb//监听到变化后的回调函数
){
this.vm=vm;
this.expOrFn = expOrFn;
this.cb = cb;
this.value= this.get();
}
// 添加自身订阅者到发布器
addDep (dep) {
dep.addSub(this)
}
// 通知更新
update(){
this.run();
}
//实现视图的更新
run(){
let oldValue = this.value//更新前数据
let value = this.get();//获取最新值
if(value != oldValue){
this.cb.call(this.vm);
}
}
//获取value值,并进行依赖收集
get(){
//将自身watcher订阅实例设置给Dep.target
pushTarget(this);
//这一步很重要,获取属性值,同时将订阅者实例添加到发布器中
let value = this.expOrFn.call(this.vm);
//将订阅实例从target栈中取出并设置给Dep.target
popTarget();
return value;
}
}
/**
* observe 方法位于源码中的 src/core/observe/index.js
* 在 vue 实例初始化(initState)的时候,执行到 initProvide,initProps,initData等方法时,会调用 observe 方法
* 在实际源码中 observe 方法经过一系列判断后,返回的是 Observer 类,
* Observer 类中的 walk 方法,等同于此处的 observe 方法
* 此处代码有所省略,但原理上等同。
*/
function observe(data){
//获取所有的属性进行遍历
const keys = Object.keys(data)
for (let i = 0; i < keys.length; i++) {
let val = data[keys[i]];
defineReactive(data, keys[i],val); // 创建监听
}
}
function defineReactive(obj, key, val){
// 为每个 key 都创建一个 dep
let dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true,//可枚举
configurable: true,//可配置
get: function reactiveGetter () {
// 判断是否有Watcher正在进行依赖收集
// 如果有的话,调用dep.depend(),表示“我被调用了,它依赖我,请记录”
if (Dep.target) {
dep.depend();
}
return val;
},
set: function reactiveSetter (newVal) {
val = newVal;
dep.notify(); // 通知所有订阅者
}
})
}
/**
* TEST
* 第一步,为每个属性创建一个发布器,并设置set,get劫持
* 实际中,执行到 initProvide,initProps,initData 等方法时,会调用 observe 方法
*/
//对象数据
let data ={name:'tcy',age:'20',sex:'male'};
observe(data);
/**
* TEST
* 第二步,创建监听并实现依赖收集
* 实际源码,new Watcher 接收的第一个参数一般是当前的 vue 实例,第二个参数是要监听的数据待或监听的属性表达式
* 从而将数据与当前vue实例绑定,Dep 发布类从而能收集数据与订阅者
*/
var watcher = new Watcher(data,() => {
"name"+data.name + "age"+data.age
},() => {
console.log("实现视图更新");
});
// 第三步,数据变化,触发视图更新
data.name = "fyn"; // 实现视图更新
data.sex = "female"; // 没有在watcher中,所以不会创建监听和收集依赖
二 props/data/computed等中的属性 为什么能用 this.xxx 直接访问?
vue 源码定义了一个 proxy 的代理方法。
src/core/instance/state.js:
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
/**
* target: 目标对象,一般就是 vm(vue 实例)
* sourceKey:数据来源 key,例如_props
* key: 被代理的 key, 就是实际的数据属性
*/
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
}
// 将 key 挂到 target 上
Object.defineProperty(target, key, sharedPropertyDefinition)
}
以 props 为例如:
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)
}
第二个参数为 vm.options.propsData,它包含了父组件传递进来的属性值,
defineReactive(props, key, value)方法运行,便将 vm.$options.props 中的属性值通过 Object.defineProperty() 方法处理后赋给了 props 变量(第3行:const props = vm._props = {}), props 和 vm._props 指向同一个引用,因此父组件传递进来的属性值实际上都赋给了vm._props。
后续执行 proxy(vm, _props, key),调用上述的代理方法,实现了把 vm._props 中的属性赋给了 vm 本身。
参考:恰恰虎