「快速get」Vue响应式原理

429 阅读1分钟

前言

vue最大的特点就是数据响应式,可以实现数据模型和试图的统一,当修改数据时试图中对应位置会相应的改变,其实vue源码中对使用Object.defineProperty对进行了数据劫持和依赖收集。接下来来屡一屡源码是如何实现的? vue工作流程(简版)

使用方式

先来看一下熟悉的使用姿势: html模版

<div id="app">
        <div>{{name}}</div>
</div>

js

const app = new Vue({
        el:"#app",
        data:{
            name:"tongqi"
        },
        created(){
            setTimeout(() => {
                this.name="tong"
            }, 1500);
        }
})

源码简单实现:

class Vue{
    constructor(options){
        this.$options=options;
        this.$data=options.data;
        this.observe(this.$data); // 响应化
        new Compiler(options.el,this); // 编译
        if (options.created) {
            options.created.call(this); // 执行created
        }
    }
    observe(value){
        Object.keys(value).forEach(key=>{
            this.defineReactive(value,key,value[key]);
        })
    }

    defineReactive(obj,key,val){
        this.observe(val); // 递归
        const dep = new Dep(); // 创建dep和key对应
        Object.defineProperty(obj,key,{
            get(){
                Dep.target && dep.addDep(Dep.target); // 依赖收集,watcher实例被加入到deps中
                return val
            },
            set(newVal){
                if (newVal===val) {
                    return 
                }
                val=newVal;
                dep.notify();
            }
        })
    }
}
// dep -->和data中的每个key对应,管理watcher
class Dep{
    constructor(){
        this.deps=[]; // 其实是watcher数组
    }
    addDep(dep){
        this.deps.push(dep);
    }
    notify(){
        this.deps.forEach(dep=>{
            dep.update();
        })
    }
}

// watcher类:1.可以读取属性 2.拥有属性更新函数
class Watcher{
    constructor(vm,key,cb){
        this.vm = vm;
        this.key = key;
        this.cb =cb;
        Dep.target = this; // watcher实例赋值给Dep的target属性
        this.vm[this.key];// 触发依赖收集
        Dep.target = null;
    }
    update(){
        console.log(`${this.key}更新了`);
        this.cb(this.vm[this.key]); // 其实更新是执行了传入的回调函数
    }
}

Compiler的定义

// compiler 遍历模版,分析其中那些地方使用到了data中的key,以及事件等指令
// 有一个地方使用就创建一个watcher实例
class Compiler{
    constructor(el,vm){
        this.$vm=vm;
        this.$el=document.querySelector(el);
        this.compile(this.$el);
    }

    compile(el){
        const childNodes = el.childNodes;
        Array.from(childNodes).forEach(node=>{
            if (this.isElement(node)) {  // 元素
                this.compileElement(node);
                this.compile(node)
            } else if(this.isInter(node)) { // 插值文本
                this.compileText(node);
            }
        })
    }

    isElement(node){
        return node.nodeType===1;
    }

    isInter(node){
        return node.nodeType===3&&/\{\{(.*)\}\}/.test(node.textContent);
    }

    compileText(node){
        this.update(node,RegExp.$1,'text')
    }

    compileElement(node){
        const nodeAttrs = node.attributes;
        Array.from(nodeAttrs).forEach(attr=>{
            const attrName = attr.name;
            const exp = attr.value;
            if (this.isDirective(attrName)) {
                const dir = attrName.substring(2);
                this[dir] && this[dir](node,exp);
            }else if(this.isEvent(attrName)){
                const dir = attrName.substring(1);
                this.eventHandler(node,exp,dir);// exp 回调函数 dir事件名
            }
        })
    }

    isDirective(attr){
        return attr.indexOf("k-")==0;
    }

    // update 函数,负责更新dom,同时创建watcher实例,在两者之间挂钩
    update(node,exp,dir){
        // 初始化
        const updaterFn = this[dir+'Updater'];
        updaterFn && updaterFn(node,this.$vm[exp]);

        // 更新
        new Watcher(this.$vm,exp,function (value) {
            updaterFn && updaterFn(node,value);
        })
    }

    textUpdater(node,value){
        node.textContent=value;
    }

    text(node,exp){
        this.update(node,exp,'text')
    }

    htmlUpdater(node,value){
        node.innerHTML=value;
    }

    html(node,exp){
        this.update(node,exp,'html')
    }

    modelUpdater(node,value){
        
        node.value=value;
    }

    model(node,exp){
        this.update(node,exp,"model"); // 赋值
        // 事件
        node.addEventListener('input',e=>{
            this.$vm[exp]=e.target.value;
        })
    }

    isEvent(attr){
        return attr.indexOf("@") == 0;
    }

    eventHandler(node,exp,dir){ // exp 回调函数 dir事件名
        const fn = this.$vm.$options.methods && this.$vm.$options.methods[exp];
        if (fn) {
            node.addEventListener(dir,fn.bind(this.$vm));
        }
    }
}