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;
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271