正所谓纸上得来终觉浅,绝知此事要躬行。我们看了vue的响应式实现源码,大致了解了vue的实现思路,那这一节我们就参考vue的实现写一个简易版的vue,我们命名为Lue。上一节我们说过vue响应式原理的核心实现是三个部分,分别是Observer,Dep和Watcher。那下面我们就针对这三个部分分别做一个简单的实现。
Observer
对传入的数据进行数据劫持。这里只简单考虑是对象的场景,对数组等复杂场景暂未考虑。
class Observer{
constructor(value){
this.walk(value);
}
walk(obj){
Object.keys(obj).forEach(key =>{
if(typeof obj[key] === 'object'){
this.walk(obj[key]);
}
defineReactive(obj,key,obj[key]);
})
}
}
function defineReactive(obj,key,value){
let dep = new Dep();
Object.defineProperty(obj,key,{
get(){
if(Dep.target){
dep.depend();
}
return value;
},
set(newValue){
if(newValue === value || (newValue !== newValue && value !== value)){
return;
}
value = newValue;
observe(newValue);
dep.notify();
}
})
}
Dep
Dep中有一个subs数组用于存放观察者(watcher),depend用于收集watcher,notify用于有变更时通知subs中的watcher进行update变更。
class Dep{
constructor(){
this.subs = [];
}
depend(){
if(Dep.target){
Dep.target.addDep(this);
}
}
addSub(sub){
this.subs.push(sub);
}
notify(){
this.subs.forEach(sub =>{
sub.update();
})
}
}
let targetStack = [];
Dep.target = null;
Watcher
Watcher的构造函数中传入vue实例,exp表达式和更新时的回调函数cb。get方法中pushTarget将当前watcher赋值给Dep.target,然后通过表达式添加依赖,再用popTarget 释放掉Dep.target。update方法获取变更后的新值,执行cb函数。
function pushTarget (target) {
targetStack.push(target)
Dep.target = target
}
function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
function parsePath (path) {
var segments = path.split('.');
return function (obj) {
for (var i = 0; i < segments.length; i++) {
if (!obj) { return }
obj = obj[segments[i]];
}
return obj
}
}
class Watcher{
constructor(vm,exp,cb){
this.vm = vm;
this.exp = parsePath(exp);
this.cb = cb;
this.get();
}
get(){
pushTarget(this);
this.value = this.exp.call(this.vm, this.vm._data)
popTarget();
}
update(){
let val = this.exp.call(this.vm, this.vm._data);
this.cb.call(this.vm, val, this.value)
}
addDep(dep){
dep.addSub(this);
}
}
到此三大模块实现。但我们这时还无法与页面Dom进行响应,我们还需实现一个compiler编译器,我们需要解析出页面上{{}}、v-model等等标识和指令,获取绑定的数据值和内容,并进行数据变化watcher监听。
compiler
options中的el 参数,为我们指定了我们需要编译哪些内容。这里简单对{{}}和l-model做了解析。对元素节点我们添加input事件,获取用户的输入变化,从而改变数据。到这边我们才真正实现了一个简单的双向数据邦定。
class compiler {
constructor(el,vm){
vm.$el = document.querySelector(el);
this.parse(vm.$el,vm);
}
parse(root,vm){
Array.from(root.childNodes).forEach(node =>{
if(node.nodeType === 3){
let reg = /\{\{(.*?)\}\}/g;
let txt = node.textContent.trim();
if(reg.test(txt)){
let val = parsePath(RegExp.$1)(vm._data);
node.textContent = txt.replace(reg,val);
new Watcher(vm,RegExp.$1,(newVal)=>{
node.textContent = txt.replace(reg,newVal);
})
}
}
if(node.nodeType === 1){
let nodeAttrs = node.attributes;
Array.from(nodeAttrs).forEach(attr =>{
let attrName = attr.name;
let attrValue = attr.value;
if(attrName === 'l-model'){
node.value = parsePath(attrValue)(vm._data);
}
new Watcher(vm,attrValue,(newVal =>{
node.value = newVal;
}))
node.addEventListener('input',e =>{
let newVal = e.target.value;
let arr = attrValue.split(".");
let val = vm._data;
arr.forEach((key,i)=>{
if(i === arr.length -1){
val[key] = newVal;
return;
}
val = val[key];
})
})
})
}
if(node.childNodes && node.childNodes.length>0){
this.parse(node,vm);
}
})
}
}
Lue
最后是Lue的类
class Lue{
constructor(options){
const vm = this;
vm.$options = options;
let data = vm._data = vm.$options.data;
observe(vm._data);
new compiler(vm.$options.el,vm);
}
}
function observe(value){
if(!value || typeof value !== 'object'){
return;
}
return new Observer(value);
}
附上html代码
<div id="app">
<h3>lue demo</h3>
<input l-model="a.b"/>
<p>a.b:{{a.b}}<p>
</div>
<script>
new Lue({
el:"#app",
data:{
a:{
b:"test"
}
}
})
</script>
最后看下实现效果,觉得还不错就点个赞吧