1、阶段一:数据劫持
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>1、双向绑定</title>
</head>
<body>
<div id="app">
<span>作者:{{ name }}</span>
<input type="text" v-model="name">
<span>更多:{{ more.like }}</span>
<input type="text" v-model="more.like">
</div>
<script src="./vue.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
name: 'Ethan',
more: {
like: '一键三连'
}
}
});
console.log(vm);
</script>
</body>
</html>
vue.js
class Vue{
constructor(obj_instance){
this.$data = obj_instance.data;
Observer(this.$data);
}
}
//数据劫持 -- 监听实例里的数据
function Observer(data_instance){
Object.keys(data_instance).forEach(key => {
let value = data_instance[key];
Object.defineProperty(data_instance,key,{
configurable:true,
enumerable:true,
get(){
console.log(`访问了属性:${key}->值:${value}`);
return value;
},
set(newValue){
console.log(`属性${key}的值${value}修改为->${newValue}`);
value = newValue;
}
})
});
}
2、阶段二:子属性劫持
//数据劫持 -- 监听实例里的数据
function Observer(data_instance){
//递归出口
if(!data_instance || typeof data_instance !=="object"){
return;
}
Object.keys(data_instance).forEach(key => {
let value = data_instance[key];
Observer(value);//递归 -- 子属性数据劫持
Object.defineProperty(data_instance,key,{
configurable:true,
enumerable:true,
get(){
console.log(`访问了属性:${key}->值:${value}`);
return value;
},
set(newValue){
console.log(`属性${key}的值${value}修改为->${newValue}`);
value = newValue;
}
})
});
}
3、阶段三:ui处理–fragment
这时我们需要处理ui部分,首先要做的就是将ui缓存到内存中
class Vue{
constructor(obj_instance){
this.$data = obj_instance.data;
Observer(this.$data);
Compile(obj_instance.el,this);
}
}
.........
function Compile(element,vm){
vm.$el = document.querySelector(element);
const fragment = document.createDocumentFragment();
let child;
while(child = vm.$el.firstChild){
fragment.append(child);
}
console.log(fragment);
console.log(fragment.childNodes)
}
将ui转换为fragment(dom片段内存缓存)后,就可以方便的在内存中对dom进行操作而不担心回流等副作用。
4、阶段四:ui处理–正则匹配{{}}中的内容
function Compile(element,vm){
vm.$el = document.querySelector(element);
const fragment = document.createDocumentFragment();
let child;
while(child = vm.$el.firstChild){
fragment.append(child);
}
fragment_compile(fragment)
function fragment_compile(node){
const pattern = /\{\{\s*(\S+)\s*\}\}/
if(node.nodeType ===3){
const result_regex = pattern.exec(node.nodeValue);
if(result_regex){
console.log(result_regex);
}
return;
}
node.childNodes.forEach(child=>fragment_compile(child))
}
}

function fragment_compile(node) {
const pattern = /\{\{\s*(\S+)\s*\}\}/;
if (node.nodeType === 3) {
const result_regex = pattern.exec(node.nodeValue);
if (result_regex) {
//当需要处理嵌套的属性时,使用reducer进行处理:more.like
const arr = result_regex[1].split(".");
const value = arr.reduce((total, current) => total[current], vm.$data);
console.log(value)
}
return;
}
node.childNodes.forEach((child) => fragment_compile(child));
}
当需要处理嵌套的属性时,使用reducer处理引用链。
function fragment_compile(node) {
const pattern = /\{\{\s*(\S+)\s*\}\}/;
if (node.nodeType === 3) {
const result_regex = pattern.exec(node.nodeValue);
if (result_regex) {
const arr = result_regex[1].split(".");
const value = arr.reduce((total, current) => total[current], vm.$data);
//这里巧妙的复用了pattern进行了{{}}的替换操作
node.nodeValue = node.nodeValue.replace(pattern,value);
console.log(node.nodeValue);
}
return;
}
node.childNodes.forEach((child) => fragment_compile(child));
}
然后复用pattern使用$data中对应数据替换掉{{more.like}}占位符
function Compile(element, vm) {
vm.$el = document.querySelector(element);
const fragment = document.createDocumentFragment();
let child;
while ((child = vm.$el.firstChild)) {
fragment.append(child);
}
fragment_compile(fragment);
function fragment_compile(node) {
const pattern = /\{\{\s*(\S+)\s*\}\}/;
if (node.nodeType === 3) {
const result_regex = pattern.exec(node.nodeValue);
if (result_regex) {
const arr = result_regex[1].split(".");
const value = arr.reduce((total, current) => total[current], vm.$data);
node.nodeValue = node.nodeValue.replace(pattern,value);
console.log(node.nodeValue);
}
return;
}
node.childNodes.forEach((child) => fragment_compile(child));
}
//将文档碎片应用到页面
vm.$el.appendChild(fragment);
}

最终将文档碎片应用到页面后,可以看到占位符已经被data中的数据替换掉了,至此我们解决了”数据->ui“的流程。
示例:http://alaya.zone:50001/vue/%E5%8F%8C%E5%90%91%E7%BB%91%E5%AE%9A/
5、阶段五:数据更新后对{{}}也进行更新
class Vue {
constructor(obj_instance) {
this.$data = obj_instance.data;
Observer(this.$data);
Compile(obj_instance.el, this);
}
}
//观察者(对属性的监听)
function Observer(data_instance) {
if (!data_instance || typeof data_instance !== "object") {
return;
}
//TODO 难点:需要对data_instance的属性进行监听。
const dependency = new Dependency();
Object.keys(data_instance).forEach((key) => {
let value = data_instance[key];
Observer(value);
Object.defineProperty(data_instance, key, {
configurable: true,
enumerable: true,
get() {
// TODO 难点:需要对属性进行依赖收集。(需要注意TODO 【1】的触发行为触发get方法的流程是同步的)
Dependency.temp && dependency.addSub(Dependency.temp);
return value;
},
set(newValue) {
value = newValue;
Observer(newValue); //当现有属性的值被重新赋值时,对新赋值进行监听
// TODO 难点:需要对属性进行依赖通知。
dependency.notify();
},
});
});
}
function Compile(element, vm) {
vm.$el = document.querySelector(element);
const fragment = document.createDocumentFragment();
let child;
while ((child = vm.$el.firstChild)) {
fragment.append(child);
}
fragment_compile(fragment);
function fragment_compile(node) {
const pattern = /\{\{\s*(\S+)\s*\}\}/;
if (node.nodeType === 3) {
//TODO 难点:需要将插值暂存。
const xxx = node.nodeValue;
const result_regex = pattern.exec(node.nodeValue);
if (result_regex) {
const arr = result_regex[1].split(".");
const value = arr.reduce((total, current) => total[current], vm.$data);
node.nodeValue = xxx.replace(pattern, value);
// TODO 难点:需要对插值进行监听。
new Watcher(vm, result_regex[1], (newValue) => {
node.nodeValue = xxx.replace(pattern, newValue);
});
}
return;
}
//对带有v-model属性的input进行绑定操作
if (node.nodeType === 1 && node.nodeName) {
const attrs = [...node.attributes];
attrs.forEach((attr) => {
if (attr.nodeName === "v-model") {
const key = attr.nodeValue;
const value = key
.split(".")
.reduce((total, current) => total[current], vm.$data);
node.value = value;
new Watcher(vm, key, (newValue) => {
node.value = newValue;
});
node.addEventListener("input", (e) => {
const newValue = e.target.value;
const keys = key.split(".");
const lastKey = keys.pop();
const data = keys.reduce(
(total, current) => total[current],
vm.$data
);
data[lastKey] = newValue; // 更新 Vue 实例的数据
});
}
});
}
node.childNodes.forEach((child) => fragment_compile(child));
}
vm.$el.appendChild(fragment);
}
//发布者
class Dependency {
constructor() {
this.subscribers = [];
}
addSub(sub) {
this.subscribers.push(sub);
}
notify() {
this.subscribers.forEach((sub) => {
sub.update();
});
}
}
//订阅者
class Watcher {
constructor(vm, key, callback) {
this.vm = vm;
this.key = key;
this.callback = callback;
Dependency.temp = this;
// TODO 【1】难点:需要对属性进行依赖收集。(在初始化订阅者的时候进行一次主动get操作触发搜集操作)
key.split(".").reduce((total, current) => total[current], vm.$data);
Dependency.temp = null;
}
update() {
const value = this.key
.split(".")
.reduce((total, current) => total[current], this.vm.$data);
console.log("#### Watcher.update()");
this.callback(value);
}
}
关键代码:
//发布者
class Dependency {
constructor() {
this.subscribers = [];
}
addSub(sub) {
this.subscribers.push(sub);
}
notify() {
this.subscribers.forEach((sub) => {
sub.update();
});
}
}
//订阅者
class Watcher {
constructor(vm, key, callback) {
this.vm = vm;
this.key = key;
this.callback = callback;
Dependency.temp = this;
// TODO 【1】难点:需要对属性进行依赖收集。(在初始化订阅者的时候进行一次主动get操作触发搜集操作)
key.split(".").reduce((total, current) => total[current], vm.$data);
Dependency.temp = null;
}
update() {
const value = this.key
.split(".")
.reduce((total, current) => total[current], this.vm.$data);
console.log("#### Watcher.update()");
this.callback(value);
}
}
设置一个订阅/观察者,旨在对数据进行监听。

- watcher:
- 初始化:watcher在fragment_compile中初始化,表明:”对于某些dom进行某个字段xxxx的监听“,这里是dom和属性字段产生交集的地方。
- nodeType===3: 对文本dom上的插值进行更新(当值变化时,更新插值占位的dom)
- nodeType===1:对绑定了v-model的指令的input组件的value属性进行更新(当值变化时,更新input的value属性)
- dependency:
- dependency是在Observer中进行初始化的,一个observer对应一个dependency,表达了”每个object下的属性都由一个发布者进行监听“的意图
- 在get中:进行watcher注册,利用了Dependency的静态属性进行watcher的暂存,这里比较巧妙的是watcher初始化的时候会触发一下get方法将自己注册到Dependency上去,避免了复杂的寻找和调用。
- 在set中:进行notify响应。目的是通知对应的字段监听watcher
- input:
- 在fragment_compile中对input的”input“事件进行监听,newValue将在这时被赋值给v-model的属性值对应的属性。这也会导致触发属性变化及后续的一系列行为。
- fragment_compile:这是一个处理Vue模板的函数。它的主要目标是将模板编译成一颗反映数据变化的动态DOM树。具体的功能主要分为两个部分:对文本节点进行插值绑定和对元素节点(具有
v-model
属性的输入元素)进行双向数据绑定。