Appearance
简易 vue2 响应、收集依赖、触发更新实现
前言
多年前学习 vue2 源码时,看到 vue2 的响应式实现,感觉很神奇,但是一直没有深入研究,最近在学习 vue3 源码,发现 vue3 的响应式实现和 vue2 的响应式实现有很大的不同,所以想着先从 vue2 的响应式实现开始,一步一步的实现一个简易的 vue2 响应式系统,以此来加深对 vue2 响应式原理的理解。
响应式实现
js
// watcher执行
class Dep {
constructor() {
// 这个方法就是用来保存当前watcher的
this.subs = [];
}
addSubs(sub) {
this.subs.push(sub);
}
notify() {
this.subs.forEach(watcher => {
watcher.update();
})
}
}
// 数据劫持做完需要进行观察
class Wtacher {
constructor(expr, vm, cb) {
this.expr = expr;
this.vm = vm;
this.cb = cb;
// 这个类需要进行数据观察 在第一次初始化的时候
// 三个值 expr 表达式 vm viewModel实体 cb回调 用来将新的值传递回去
// 设置新值的时候需要先获取一下旧值,这样才能进行对比
this.oldValue = this.getValue();
}
getValue() {
// 这里是关键 在我们进行观察的时候 将当前watcher实例绑定到全局Dep上
Dep.target = this;
let oldValue = CompilerUtils.getValue(this.expr, this.vm);
// 获取值完毕后,我们再讲全局Dep上的观察者去掉
Dep.target = null;
return oldValue;
}
update() {
// 拿取新值,如果两值不相等则通过回调返回
let newValue = this.getValue();
if (newValue !== this.oldValue) {
this.cb && this.cb(newValue);
}
}
}
// 数据劫持
class Observer {
constructor(data) {
this.observer(data);
}
observer(data) {
// 值劫持object类型的值
if (data && typeof data === 'object') {
for (const key in data) {
if (data.hasOwnProperty(key)) {
const value = data[key];
this.defineReactive(data, key, value);
}
}
}
}
defineReactive(target, key, value) {
// 如果值也是对象,则需要再次进行劫持
this.observer(value);
// 需要在数据劫持的时候给这个数据创建独一无二的观察者
let dep = new Dep();
Object.defineProperty(target, key, {
get() {
Dep.target && dep.addSubs(Dep.target);
// 这里是获取值时
return value;
},
set: (newValue) => {
// 这里是设置值时 在新值不相等的时候 设置
if (newValue !== value) {
value = newValue;
// 新设置的值也需要进行劫持
this.observer(value);
// 在值更新的时候我们执行发布
dep.notify();
}
}
})
}
}
// 编译类
class Compiler {
constructor(el, vm) {
// 如果el是元素 则设置 如果不是 则获取
this.el = this.isElementNode(el) ? el : document.querySelector(el);
this.vm = vm;
// 现在我们已经拿到元素了 现在拿到处理过后的元素
let fragment = this.node2fragment(this.el);
// 对文档碎片进行处理
this.compiler(fragment);
// 将碎片添加回源文档
this.el.appendChild(fragment);
}
isElementNode(el) {
return el.nodeType === 1;
}
isDirective(name) {
return name.startsWith('v-');
}
node2fragment(el) {
// 首先创建一个文档碎片
let fragment = document.createDocumentFragment();
// 将el中的元素append到文档碎片中
let firstChild;
while (firstChild = el.firstChild) {
// appendChild具有移动性,可以将文档已有元素移动
fragment.appendChild(firstChild);
}
// 添加进去后,我们返回文档碎片
return fragment;
}
compiler(fragment) {
// 这里进行文档碎片处理
let childNodes = fragment.childNodes;
[...childNodes].forEach(node => {
// 如果是元素节点,则进行元素节点编译,否则进行文本节点编译
if (this.isElementNode(node)) {
this.compilerElement(node);
// 如果是元素节点,我们需要再次进行编译
this.compiler(node);
} else {
this.compilerText(node);
}
})
}
compilerElement(node) {
// 如果是元素节点,我们需要拿到属性 attributes 类似于 v-model="myAdd"这样的
let attributes = node.attributes;
[...attributes].forEach(attr => {
let { name, value: expr } = attr; // name v-model name myAdd
// 拿到属性后,我们需要去判断 v- 开头的属性
if (this.isDirective(name)) {
let [, directive] = name.split('-');
// 因为v-on:click 格式不一样 所以要进一步进行处理
let [directiveName, eventName] = directive.split(':');
// 拿到之后我们使用编译工具进行编译
CompilerUtils[directiveName](node, expr, this.vm, eventName);
}
})
}
compilerText(node) {
// 拿到之后 我们需要 {{}} 这样格式的文本节点
let textContent = node.textContent;
if (/\{\{(.+?)\}\}/.test(textContent)) {
CompilerUtils["text"](node, textContent, this.vm);
}
}
}
// 编译工具
let CompilerUtils = {
getValue(expr, vm) {
return expr.split('.').reduce((data, cur, curIndex, arr) => {
return data[cur];
}, vm.$data);
},
setValue(expr, vm, newValue) {
return expr.split('.').reduce((data, cur, curIndex, arr) => {
if (curIndex === arr.length - 1) {
return data[cur] = newValue;
}
return data[cur];
}, vm.$data);
},
// 使用name作为key值
model(node, expr, vm) {
// 拿到之后 第一步进行 获取值
let fn = this.updater['model'];
new Wtacher(expr, vm, (newValue) => {
// 在这里再次调用设置方法
fn(node, newValue);
})
let value = this.getValue(expr, vm);
fn(node, value);
node.addEventListener('input', (e) => {
this.setValue(expr, vm, e.currentTarget.value);
})
},
getTextContent(expr, vm) {
return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
return this.getValue(args[1], vm);
})
},
text(node, expr, vm) {
// 一样 第一步 获取值 但是文本都是 {{a}}{{b}} 这样的,所以我们要去匹配出来
let fn = this.updater['text'];
let value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
new Wtacher(args[1], vm, () => {
// 设置文本不一样需要循环 需要对每一个值进行观察
fn(node, this.getTextContent(expr, vm));
});
return this.getValue(args[1], vm);
})
fn(node, value);
},
html(node, expr, vm) {
// 拿到之后 第一步进行 获取值
let fn = this.updater['html'];
new Wtacher(expr, vm, (newValue) => {
// 在这里再次调用设置方法
fn(node, newValue);
})
let value = this.getValue(expr, vm);
fn(node, value);
},
on(node, expr, vm, eventName) {
node.addEventListener(eventName, (e) => {
vm.$methods[expr].call(vm, e);
})
},
updater: {
model(node, value) {
node.value = value;
},
text(node, value) {
node.textContent = value;
},
html(node, value) {
node.innerHTML = value;
},
}
}
// 基类
class Vue {
constructor(options) {
this.$el = options.el;
this.$data = options.data;
this.$computed = options.computed;
this.$methods = options.methods;
// 如果存在el则进行编译处理
if (this.$el) {
// 在此做数据劫持
new Observer(this.$data);
// 代理$data到vm
this.proxyData();
// 计算属性
for (const key in this.$computed) {
Object.defineProperty(this.$data, key, {
get: () => {
return this.$computed[key].call(this);
}
})
}
new Compiler(this.$el, this);
}
}
proxyData() {
for (const key in this.$data) {
if (this.$data.hasOwnProperty(key)) {
const value = this.$data[key];
Object.defineProperty(this, key, {
get: () => {
return this.$data[key];
},
set: (newValue) => {
if (newValue !== value) {
this.$data[key] = newValue;
}
}
})
}
}
}
}
html 测试部分
html
<body>
<div class="container">
<input type="text" v-model="myAdd">
{{outProp}}
{{outAge}}
<div>{{company.name}}</div>
<div>{{company.age}} {{count}}</div>
<div>{{myName}}</div>
<ul>
<li>1</li>
<li>2</li>
</ul>
{{computedAdd}} {{count}}
<button v-on:click="change">更新</button>
<div v-html="message"></div>
</div>
<script>
var vm = new Vue({
el: '.container',
data: {
company: {
name: 'ABC',
age: 4
},
myName: 'lxh',
myAdd: '沙河西后街',
outProp: '外部属性',
outAge: 12,
count: 1,
message: "<h1>你好啊</h1>"
},
computed: {
computedAdd() {
return this.myAdd.split('').reverse().join('');
}
},
methods: {
change() {
this.count++;
this.myName = this.myName + this.myAdd + this.outProp;
}
},
})
console.log(vm);
</script>
</body>