参考文档:https://www.cnblogs.com/zhenfei-jiang/p/7542900.html
Vue的数据双向绑定的基础是通过Object.defineProperty()劫持作用域中变量的get和set方法实现的,示例如下:
示例中数据双向绑定的实现:
js对象变更触发dom更新:当执行virtualData.data = “default”;时输入框的信息同步更新为default字符串;
dom变更触发js对象更新:当改变输入框内的信息时,控制台打印的virtualData.data同步更新为输入框变更后的内容。
<html lang="en">
<head>
<meta charset="UTF-8">
<title>definePropertytitle>
head>
<body>
<div>
<input type="text" id="data">
div>
body>
<script>
let val = "";
let virtualData = {};
let dataDom = document.getElementById("data");
Object.defineProperty(virtualData, "data", {
get: function () {
return val;
},
set: function (newVal) {
/**
* 在vue中,这里是一个订阅列表,页面上所有使用data变量的地方(如v-model、v-bind以及{{}})
* 在初始化时都会在这里产生订阅(每处使用都会生成一个订阅者),用于触发视图更新
*
* 产生订阅:在set方法中增加dataDom.value = newVal;语句的过程
* 订阅发布:data变更触发set方法执行dataDom.value = newVal;语句的过程
*
* 这里可以视为数据双向绑定中的js对象变更触发dom变更
*/
dataDom.value = newVal;
val = newVal;
}
});
//不论页面触发的变量变更还是js中触发的变量变更,都会通过劫持的set方法触发订阅发布
dataDom.addEventListener("input", function (e) {
//这里可以视为数据双向绑定中的dom变更触发js对象变更
virtualData.data = e.target.value;
console.log("virtualData.data is " + virtualData.data);
});
virtualData.data = "default";
script>
html>
注意:本文后续代码Vue版本为2.6.7,如发现代码不匹配或功能与本文描述不符,请优先检查Vue版本。
当然,Vue真正的实现不会像上例那么简单,下面来看下Vue关于Object.defineProperty()的代码实现:
/**
* Define a reactive property on an Object.
*/
function defineReactive$$1 (
obj,
key,
val,
customSetter,
shallow
) {
//作用域中变量的生命周期由Dep对象统一管理
var dep = new Dep();
var property = Object.getOwnPropertyDescriptor(obj, key);
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
var getter = property && property.get;
var setter = property && property.set;
if ((!getter || setter) && arguments.length === 2) {
val = obj[key];
}
var childOb = !shallow && observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
var value = getter ? getter.call(obj) : val;
//Dep.target用于验证当前查询的变量是否有页面dom在使用(如果有,那么Dep.target是一个Watcher对象,否则是undefined)
if (Dep.target) {
//产生订阅,这里可以通过shallow参数控制是浅订阅还是深订阅
dep.depend();
if (childOb) {
childOb.dep.depend();
if (Array.isArray(value)) {
dependArray(value);
}
}
}
return value
},
set: function reactiveSetter (newVal) {
var 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 (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);
//订阅发布,函数会遍历订阅列表(即Watcher列表,每个作用域变量都会生成一个Watcher),调用每个Wacher的update方法更新dom
dep.notify();
}
});
}
接下来看一下Watcher对象:
在组件的整个生命周期中,Watcher对象会在以下3个过程中生成:
Watcher的代码实现:
/**
* A watcher parses an expression, collects dependencies,
* and fires callback when the expression value changes.
* This is used for both the $watch() api and directives.
*/
var Watcher = function Watcher (
//vue对象
vm,
expOrFn,
//真实dom列表,元素由Vue对html模版解析后生成(例:如果一个变量在页面上有两处使用,则cb会有两个元素),这里的解析逻辑会随着Vue的版本更新而变化,这是由于不同版本的Vue所支持的指令会略有差异
cb,
options,
isRenderWatcher
) {
this.vm = vm;
if (isRenderWatcher) {
vm._watcher = this;
}
vm._watchers.push(this);
// options
if (options) {
this.deep = !!options.deep;
this.user = !!options.user;
this.lazy = !!options.lazy;
this.sync = !!options.sync;
this.before = options.before;
} else {
this.deep = this.user = this.lazy = this.sync = false;
}
this.cb = cb;
this.id = ++uid$2; // uid for batching
this.active = true;
this.dirty = this.lazy; // for lazy watchers
this.deps = [];
this.newDeps = [];
this.depIds = new _Set();
this.newDepIds = new _Set();
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: '';
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn;
} else {
this.getter = parsePath(expOrFn);
if (!this.getter) {
this.getter = noop;
process.env.NODE_ENV !== 'production' && warn(
"Failed watching path: \"" + expOrFn + "\" " +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
);
}
}
this.value = this.lazy
? undefined
: this.get();
};
在Watcher对象的run方法中会更新真实dom(Watcher对象的update方法最终会调用run方法完成更新动作,所以这里直接贴出run方法):
/**
* Scheduler job interface.
* Will be called by the scheduler.
*/
Watcher.prototype.run = function run () {
if (this.active) {
var value = this.get();
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
var oldValue = this.value;
this.value = value;
//更新dom
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue);
} catch (e) {
handleError(e, this.vm, ("callback for watcher \"" + (this.expression) + "\""));
}
} else {
this.cb.call(this.vm, value, oldValue);
}
}
}
};
Vue解析html模版的代码在源码中逻辑较为松散,下面只贴出构建虚拟dom的部分源码供参考:
function genElement (el, state) {
if (el.parent) {
el.pre = el.pre || el.parent.pre;
}
if (el.staticRoot && !el.staticProcessed) {
return genStatic(el, state)
} else if (el.once && !el.onceProcessed) {
return genOnce(el, state)
} else if (el.for && !el.forProcessed) {
return genFor(el, state)
} else if (el.if && !el.ifProcessed) {
return genIf(el, state)
} else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
return genChildren(el, state) || 'void 0'
} else if (el.tag === 'slot') {
return genSlot(el, state)
} else {
// component or element
var code;
if (el.component) {
code = genComponent(el.component, el, state);
} else {
var data;
if (!el.plain || (el.pre && state.maybeComponent(el))) {
data = genData$2(el, state);
}
var children = el.inlineTemplate ? null : genChildren(el, state, true);
code = "_c('" + (el.tag) + "'" + (data ? ("," + data) : '') + (children ? ("," + children) : '') + ")";
}
// module transforms
for (var i = 0; i < state.transforms.length; i++) {
code = state.transforms[i](el, code);
}
return code
}
}