Vue2.0双向绑定核心实现

/**
 * author:Echonessy
 * des:
 * date:2020.07.24
 * target: Vue
 *  1.Vue
 *      把data中的成员注入到Vue实例中,并且把data中的成员转成getter/setter
 *  2.Observer
 *      能够对数据对象的所有属性进行监听,如果变动可拿到最新值并通知Dep(发布者-目标)
 *  3.Watcher
 *      定义观察者,定义update()函数,当数据发生变动,更新视图
 *  4.Dep
 *      添加观察者,当数据发生变化的时候,通知所有的观察者,执行观察者的update()函数
 *  5.Compiler
 *      负责编译模板,解析指令/差值表达式,负责页面的首次渲染,当数据变化后更新视图
 * */



/**
 *  1.Vue
 *      把data中的成员注入到Vue实例中,并且把data中的成员转成getter/setter
 *      功能:
 *          1.负责接受初始化的参数(选项)
 *          2.负责吧data中的属性注入到Vue实例,转换成getter/setter
 *          3.负责调用Observer监听data中所有属性的变化
 *          4.负责调用compiler解析指令/差值表达式
 *      结构:
 *          +$options :记录所有参数配置
 *          +$el :记录绑定的DOM Element
 *          +$data :记录响应式数据
 *          ---------------------
 *          -_proxyData()  私有成员,把data中的属性,转换成getter/setter注入到Vue实例中
 * */
class Vue {
    constructor(options) {
        // 1.通过属性保存选项的数据
        this.$options = options || Object.create(null);
        // data 必须是一个函数,为了防止与内部变量冲突
        if(typeof options.data !== 'function'){
            throw ('data must be a function')
            return
        }
        this.$data = options.data() || Object.create(null);
        this.$el = typeof options.el === 'string' ? document.querySelector(options.el):options.el;
        // 2.把data中的成员转换成getter/setter注入到Vue实例中
        this._proxyData(this.$data);
        // 3.调用Observer对象,监听数据的变化
        new Observer(this.$data)
        // 4.调用Compiler对象,解析指令和差值表达式
        new Compiler(this)
    }
    // 私有成员,把data中的属性,转换成getter/setter注入到Vue实例中
    _proxyData(data){
        // 1.遍历data中的所有属性,
        if(!data || typeof data !== 'object') return;
        Object.keys(data).forEach(key =>{
            Object.defineProperty(this,key,{
                configurable:true,
                enumerable:true,
                get() {
                    return data[key]
                },
                set(nv) {
                    if(data[key] == nv) return;
                    data[key] = nv;
                }
            })
        })
    }
}


/**
 *  2.Observer 核心
 *      数据响应式处理
 *      功能:
 *          1.负责编译模板,解析指令/差值表达式,
 *          2.负责页面的首次渲染
 *          3.当数据变化后更新视图
 *      结构:
 *          +go(data)
 *              负责遍历对象属性,对象拦截,只针对对象数据进行响应式处理,
 *          +proxyData(data)
 *              数据代理
 *              负责通过Object.defineProperty进行对象劫持,通过递归进行深度对象监听,
 *              针对新赋值属性值,如果是对象,同样进行数据拦截
 * */

//监听data
class Observer {
    constructor(data) {
        this.go(data)
    }
    go(data){
        if(typeof data !== 'object'){
            return
        }
        Object.keys(data).forEach(key =>{
            this.proxyData(data,key,data[key])
        })
    }
    proxyData(data,key,value){
        this.go(value);
        let that = this;
        //收集依赖,发送通知
        let dep = new Dep();
        Object.defineProperty(data,key,{
            configurable:true,
            enumerable:true,
            get() {
                // console.log('getter -> ' + value)
                Dep.target && dep.addSub(Dep.target)
                return value;
            },
            set(nv) {
                if(value == nv) return;
                console.log('数据变化'+value+'-->'+nv+',发送通知')
                value = nv;
                that.go(nv);
                dep.notify(key,nv)
                //    数据变化,发送通知
            }
        })
    }
}


/**
 *  3.Compiler 核心
 *      编译、更新视图
 *      功能:
 *          1.负责编译模板,解析指令和差值表达式
 *          2.负责页面的首次加载
 *          3.当数据变化时,更新视图
 *
 *      结构:
 *          +el
 *              Vue构造函数的options.el ,DOM对象
 *          +vm
 *              Vue实例
 *          ----------------------------------------------
 *          +compile(el)
 *              用于遍历DOM对象所有节点,如果是文本节点,解析差值表达式。如果是元素节点,解析指令。
 *          +compileText(node)
 *              解析差值表达式
 *          +compileElement(node)
 *              解析元素指令
 *          +isDirective(node)
 *              判断是否是指令
 *          +isTextNode(node)
 *              判断是否是文本节点
 *          +isElementNode(node)
 *              判断是否是元素节点
 *          +update(node,key,attrName)
 *              更新视图,执行指令,根据 attrName+Update 执行对应方法
 *          +textUpdate(node,key,attrName)
 *              更新文本,执行指令v-text
 *          +modelUpdate(node,key,attrName)
 *              更新表单value,执行指令v-model
 *
 *      nodeType:12种节点类型
 *      1   Element 代表元素
 *      2   Attr    代表属性
 *      3   Text    代表元素或属性中的文本内容。
 *      4   CDATASection    代表文档中的 CDATA 部分(不会由解析器解析的文本)。
 *      5   EntityReference 代表实体引用。
 *      6   Entity  代表实体。
 *      7   ProcessingInstruction   代表处理指令。
 *      8   Comment 代表注释。
 *      9   Document    代表整个文档(DOM 树的根节点)。
 *      10  DocumentType    向为文档定义的实体提供接口
 *      11  DocumentFragment    代表轻量级的 Document 对象,能够容纳文档的某个部分
 *      12  Notation    代表 DTD 中声明的符号。
 * */
class Compiler {
    constructor(vm) {
        this.vm = vm;
        this.el = vm.$el;
        this.compile(this.el)
    }
    //编译模板,处理文本节点和元素节点
    compile(el){
        let childNodes = el.childNodes; // 所有节点,属于伪数组需要通过Array.from()转换成真实数组
        Array.from(childNodes).forEach(node =>{
            if(this.isTextNode(node)){
                // 处理文本节点
                this.compileText(node)
            } else if(this.isElementNode(node)){
                // 处理元素节点
                this.compileElement(node)
            }
            // 判断node节点,是否有子节点,如果有,递归深度遍历
            if(node.childNodes && node.childNodes.length) {
                this.compile(node)
            }
        })
    }
    //编译元素节点,处理指令
    compileElement(node){
        // v-text v-html
        // 1.遍历所有的属性节点
        // 2.判断是否是指令
        Array.from(node.attributes).forEach(attr=>{
            let attrName = attr.name;
            if(this.isDirective(attrName)){
                attrName = attrName.substr(2);
                let key = attr.value;
                // 如果当前元素含有指令,则需要首次渲染指令对应的内容
                this.update(node,key,attrName)
            }
        })
    }
    update(node,key,attrName){
        let updateFn = this[attrName+'Update'];
        updateFn && updateFn.call(this,node,this.vm[key],key);
    }
    // 处理v-for 指令
    forUpdate(node,value,key){
        let reg = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/;
        let list = this.vm[key.match(reg)[2]];
        list.forEach(item =>{
            // console.log(item)
        })
        // console.log(list)
    }
    // 处理v-text 指令
    textUpdate(node,value,key){
        node.textContent = value;
        new Watcher(this.vm,key,(k,nv) =>{
            console.log('创建Watcher ,当数据改变更新视图' + nv)
            node.textContent = nv;
        })
    }

    //编译文本节点,处理差值表达式
    compileText(node){
        // {{name}}
        // .  匹配除换行符 \n 之外的任何单字符。要匹配 . ,请使用 \.
        // \  将下一个字符标记为或特殊字符、或原义字符、或向后引用、或八进制转义符。例如, 'n' 匹配字符 'n'。'\n' 匹配换行符。序列 '\\' 匹配 "\",而 '\(' 则匹配 "("。
        // ?  匹配前面的子表达式零次或一次,或指明一个非贪婪限定符。要匹配 ? 字符,请使用 \?。
        // +  匹配前面的子表达式一次或多次。要匹配 + 字符,请使用 \+。
        let reg = /\{\{(.+?)\}\}/; // 匹配单个的{{key1}}
        let value = node.textContent;
        if(reg.test(value)){
            let key = RegExp.$1.trim();
            node.textContent = value.replace(reg,this.vm[key]);
            new Watcher(this.vm,key,(k,nv) =>{
                console.log('创建Watcher ,当数据改变更新视图' + nv)
                node.textContent = this.vm[key];
            })
        }
    }
    // 处理v-model 指令
    modelUpdate(node,value,key){
        node.value = value;
        new Watcher(this.vm,key,(k,nv) =>{
            console.log('创建Watcher ,当数据改变更新视图' + nv)
            node.value = nv;
        })
        //设置双向绑定事件
        node.addEventListener('input',e => this.vm[key] = node.value)
    }
    // 判断元素是否是指令
    isDirective(attrName){
        //判断属性是否是v-开头
        return attrName.startsWith('v-');
    }
    //判断是否是文本节点
    isTextNode(node){
        return node.nodeType === 3;
    }
    //判断是否是元素节点
    isElementNode(node){
        return node.nodeType === 1;
    }
}


/**
 *  4.Dep 核心 dependence
 *      目标(发布者)
 *      功能:
 *          1.收集依赖,添加观察者
 *          2.通知所有观察者
 *
 *      结构:
 *          +subs 数组:存储所有的观察者
 *          ---------------------------------
 *          +addSub():添加观察者
 *          +notify():当事件发生时,调用所有的观察者的update()方法
 * */

class Dep {
    constructor() {
        // 记录所有的(观察者/订阅者)
        this.subs = new Array(0);
    }
    addSub(sub){
        // 每一个观察者都必须包含一个update方法
        if(sub && sub.update) this.subs.push(sub);
    }
    notify(key,nv){
        this.subs.forEach(sub =>sub.update(key,nv))
    }
}



/**
 *  4.Watcher 核心
 *      观察者 ->update():当事件发生时,具体要做的事情
 *      功能:
 *          1.当数据变化触发依赖,dep通知所有的Watcher实例更新视图
 *          2.自身实例化的时候往dep对象中添加自己
 *
 *      结构:
 *          +vm Vue 实例
 *          +key data中的属性名称
 *          +cb 回调函数 负责更新视图
 *          +oldValue 记录数据变化之前的值
 *          ------------------------------------
 *          +update() 当数据发生变化的时候,更新视图
 * */

// 订阅者-观察者
class Watcher {
    constructor(vm,key,cb) {
        this.vm = vm;
        this.key = key;
        this.cb = cb;
        // 把Watcher对象记录到Dep类的静态属性target
        // 触发get方法,在get中会调用addSub
        Dep.target = this;
        // 当获取vm[key]的时候会执行getter
        this.oldValue = vm[key];
        // 当Watcher 添加到subs之后,我们要对Dep进行静态属性的重置
        Dep.target = null;
    }
    update(key,nv){
        if(nv == this.oldValue) return;
        this.cb(key,nv)
        this.oldValue = nv;
    }
}

你可能感兴趣的:(Vue2.0双向绑定核心实现)