今天用react的同事问到vue的watch功能实现,想搬到react里做一个hook, 在网上搜索了一圈竟然没发现一个实现vue并带有watch,computed功能的。我一时兴起,抽三天时间晚上撸了一个,希望能帮到需要的朋友。
1.html模板,最终实现的MyVue需要实现的功能。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="app">
<p>名称:{{name}}</p>
<p v-text="name"></p>
<p>年龄:{{age}}</p>
<input type="text" v-model="name">
<br>
<label> 综合计算:【{{info}}】 </label>
<button @click="changeName">按钮事件</button>
<div v-html="html"></div>
</div>
<!-- 存入依赖的地方 -->
<script src='./dep.js'></script>
<!-- Watcher是连接Observer和Compile的桥梁,通过它修改dom -->
<script src='./watcher.js'></script>
<!-- 把模板编译,生成watcher,注意这个不是代码编译器 -->
<script src='./compile.js'></script>
<!-- 手动实现的简单版本vue -->
<script src='./myVue.js'></script>
<script>
const stan = new MyVue({
el: '#app',
data: {
name: "Stanley",
age: 12,
html: '<button>注入的html按钮</button>'
},
created() {
console.log('created生命周期')
},
computed: { //值有可能是get/set对象,但这里todo
info() {
return this.name + ': ' + this.age;
}
},
watch: {
age(newV, old) {
console.log('WATCH:', newV, old);
}
},
methods: {
changeName() {
this.name = '大神'
this.age = 18
}
}
})
</script>
</body>
</html>
2. Dep.js是用来存入依赖watcher的地方
// Dep:管理若干watcher实例,它和key一对一关系
class Dep {
constructor() {
this.deps = [];
}
addDep(watcher) {
this.deps.push(watcher);
}
notify(v) {
this.deps.forEach(watcher => watcher.update(v));
}
}
3.watcher.js 它的实例闭包了vnode,key,dom,各种需要的变量,回调中修改dom或者实现计算属性
// 保存ui中依赖,实现update函数可以更新之
class Watcher {
constructor(vm, key, cb, name) {
this.name = name;
this.vm = vm;
this.key = key;
this.cb = cb;
// 将当前实例指向Dep.target
Dep.target = this;
// 读一次key触发getter,这里很重要
this.vm[this.key];
Dep.target = null;
console.log('set watcher:' + this.name, this.key);
}
update(newV) {
// 更新,返回新值,watch时才会用到newV
this.cb.call(this.vm, newV, this.vm[this.key]);
}
}
4.compile.js html模板处理函数,把{{}}内的值v-model,@click等属性处理,完成从vue实现到dom同步的过程,生成watch,这就是值与dom的依赖关系。
// 遍历模板,将里面的插值表达式处理
// 另外如果发现v-xx, @xx做内部协议事件处理
class Compile {
constructor(el, vm) {
this.$vm = vm;
this.$el = document.querySelector(el);
if (this.$el) {
// 1.$el中的内容搬家到一个fragment,提高操作效率
this.$fragment = this.node2Fragment(this.$el);
// 2.编译fragment
this.compile(this.$fragment);
// 3.将编译结果追加至宿主中
this.$el.appendChild(this.$fragment);
}
}
// 遍历el,把里面内容搬到新创建fragment中
node2Fragment(el) {
const fragment = document.createDocumentFragment();
let child;
while ((child = el.firstChild)) {
// 由于appenChild是移动操作
fragment.appendChild(child);
}
return fragment;
}
// 把动态值替换,把指令和事件做处理
compile(el) {
// 遍历el
const childNodes = el.childNodes;
Array.from(childNodes).forEach(node => {
if (this.isElement(node)) {
// 如果是元素节点,我们要处理指令v-xx,事件@xx
this.compileElement(node);
} else if (this.isInterpolation(node)) {
this.compileText(node);
}
// 递归子元素
if (node.childNodes && node.childNodes.length > 0) {
this.compile(node);
}
});
}
isElement(node) {
return node.nodeType === 1;
}
// 插值表达式判断
isInterpolation(node) {
// 是文本节点,并且 需要满足{{xx}}
return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent);
}
compileElement(node) {
// 查看node的特性中是否有v-xx,@xx
const nodeAttrs = node.attributes;
Array.from(nodeAttrs).forEach(attr => {
// 获取属性名称和值 v-text="abc"
const attrName = attr.name; // v-text
const exp = attr.value; // abc
// 指令:v-xx
if (attrName.indexOf("v-") === 0) {
const dir = attrName.substring(2); // text
// 执行指令
this[dir] && this[dir](node, this.$vm, exp);
} else if(attrName.indexOf('@') === 0) {
// 事件 @click="handlClick"
const eventName = attrName.substring(1); // click
this.eventHandler(node, this.$vm, exp, eventName);
}
});
}
text(node, vm, exp) {
this.update(node, vm, exp, "text");
}
// 双向数据绑定
model(node, vm, exp) {
// update是数据变了改界面
this.update(node, vm, exp, "model");
// 界面变了改数值
node.addEventListener("input", e => {
vm[exp] = e.target.value;
});
}
modelUpdator(node, value) {
node.value = value;
}
html(node, vm, exp) {
this.update(node, vm, exp, "html");
}
htmlUpdator(node, value) {
node.innerHTML = value;
}
eventHandler(node, vm, exp, eventName){
// 获取回调函数
const fn = vm.$options.methods && vm.$options.methods[exp];
if(eventName && fn) {
// 函数柯理化,把事件的this绑定到了vm
node.addEventListener(eventName, fn.bind(vm))
}
}
// 把插值表达式替换为实际内容
compileText(node) {
// {{xxx}}
// RegExp.$1是匹配分组部分
// console.log(RegExp.$1);
const exp = RegExp.$1;
this.update(node, this.$vm, exp, "text");
}
// 编写update函数,它可复用
// exp是表达式, dir是具体操作:text,html,model
update(node, vm, exp, dir) {
const fn = this[dir + "Updator"];
const plainValue = vm[exp];// todo 没做子元素.操作
const templateText = node.textContent;
fn && fn(node, plainValue,templateText);
new Watcher(vm, exp, function(newV,old) {
// 这里执行的是,例如compile.textUpdator(node, value)
// 本函匿名数调用时的this是vm
// node被闭包了;vm也闭包在了watchre实例内,所以下面代码也可以写成fn(node, this.vm[exp])
fn && fn(node, vm[exp],templateText);
},'compile:'+ node.tagName);
}
textUpdator(node, value,templateText) {
// 这里出了问题,text里已经没有表达式了,todo
if(!templateText){
node.textContent = value;
}else{
node.textContent=templateText.replace(/\{\{(.*)\}\}/,value);
}
}
}
5 myVue.js 手动实现的简单版本vue,实现了watch,computed,state的初始化。
class MyVue {
constructor(options) {
this.$options = options;
this._state = { data: options.data };
// 响应化
this.initState(this._state.data);
this.initComputed(options.computed);
this.initWatch(options.watch);
// 创建编译器
new Compile(options.el, this);
if (options.created) {
options.created.call(this);
}
}
initState(state) {
// 递归遍历,使传递进来的对象响应化
this.defineReactive(state);
Object.keys(state).forEach((key) => {
// 把state里的变量代理到this
this.proxy(key, this._state);
});
}
initComputed(computedCfg) {
if(!computedCfg){
return;
}
Object.keys(computedCfg).forEach((key) => {
// 把state里的变量代理到this
this.defineComputed(key, computedCfg[key]);
});
}
initWatch(watchCfg) {
if(!watchCfg){
return;
}
Object.keys(watchCfg).forEach((key) => {
// 把state里的变量代理到this
new Watcher(this,key,watchCfg[key],'user watch');
});
}
// 在vue根上定义属性代理data中的数据
proxy(key, value) {
Object.defineProperty(this, key, {
get() {
return value.data[key];
},
set(newVal) {
// console.log(key,':',newVal)
value.data[key] = newVal;
},
});
}
// computeFunc 值有可能是get/set对象,但这里todo
// 实际上defineComputed是一个类似于initState,的依赖注入,
// state 是单个值的注入同步dom元素;computed是一组值依赖,同步一个值
defineComputed(key, computeFunc) {
let cacheVal; // 这个就是闭包缓存的值
const dep = new Dep();// 所有用到这个计算属性的值的地方,都要把watcher存进来
const onDependencyUpdated = ()=>{
// 再次计算 计算属性的值
cacheVal = computeFunc.call(this);
dep.notify();
};
// 将onDependencyUpdated 这个函数传给Dep.target
Dep.target = new Watcher(this, '', onDependencyUpdated,'computed');
// 很重要,收集计算属性的依赖值,get把target放到了成员的dep里
cacheVal = computeFunc.call(this);
Dep.target = null;
Object.defineProperty(this, key, {
get: function () {
Dep.target && dep.addDep(Dep.target);
return cacheVal;
},
set: function () {
// 什么也不做,不需要设定计算属性的值,也可以使用用户传入的 todo
},
});
}
defineReactive = (value) => {
if (typeof value !== 'object') {
return;
}
Object.keys(value).forEach((key) => {
let val = value[key];
// 递归
this.defineReactive(val);
// 创建Dep实例:Dep和key一对一对应
// 它会放多个watcher,每个watcher对应这个key被用在了不同的模板dom
const dep = new Dep();
// 给obj定义属性
Object.defineProperty(value, key, {
get() {
Dep.target && console.log(key,'pushed',Dep.target.name)
// 将Dep.target指向的Watcher实例加入到Dep中
Dep.target && dep.addDep(Dep.target);
return val;
},
set: (newVal) => {
if (newVal === val) {
return;
}
this.defineReactive(newVal);
// 只通知观察了这个变量的watcher(Dep.target)
// watcher会更新Compile时闭包的dom元素
dep.notify(newVal);
val = newVal;
console.log('RE:', key, ':', newVal, dep);
},
});
});
};
}