vue源码分析(三)

7 篇文章 0 订阅
5 篇文章 0 订阅

正所谓纸上得来终觉浅,绝知此事要躬行。我们看了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>

最后看下实现效果,觉得还不错就点个赞吧
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值