SOURCE

/** 
实现一个数据劫持 - Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者
实现一个模板编译 - Compiler,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数
实现一个 - Watcher,作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图
MVVM 作为入口函数,整合以上三者
**/

/**
// 数据劫持 - Observer 
    Observer 类主要目的就是给 data 数据内的所有层级的数据都进行数据劫持,让其具备监听对象属性变化的能力
【重点】:
1.当对象的属性值也是对象时,也要对其值进行劫持 --- 递归
2.当对象赋值与旧值一样,则不需要后续操作 --- 防止重复渲染
3.当模板渲染获取对象属性会调用get添加target,对象属性改动通知订阅者更新 --- 数据变化,视图更新

 */

class Observer {
    constructor(data) {
        this.observer(data)
    }
    observer(data) {
        // 判断是否是对象 是才观察
        if (typeof data === 'object' && data != null) {
            for (let key in data) {
                this.defineReactive(data, key, data[key])
            }
        }
    }
    defineReactive(data, key, value) {
        let dep = new Dep()
        this.observer(value) // 如果值也是对象,继续观察
        Object.defineProperty(data, key, {
            // 获取值
            get() {
                Dep.target && Dep.addSub(Dep.target)
                return value
            },
            // 设置值
            set(newValue) {
                // 新值与旧值不一样才替换,一样就不用替换
                if (newValue != value) {
                    this.observer(newValue) // 如果值是对象继续观察
                    value = newValue
                    dep.notify() // 通知所有订阅者更新了
                }
            }
        })

    }

}

/**
 * 模板编译 - Compiler
 * Compiler 是解析模板指令,将模板中的变量替换成数据,
 * 然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,
 * 添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
 * Compiler 主要做了三件事:
 * 1.将当前根节点所有子节点遍历放到内存中
   2.编译文档碎片,替换模板(元素、文本)节点中属性的数据
   3.将编译的内容回写到真实DOM上
【重点】:
    1.先把真实的 dom 移入到内存中操作 --- 文档碎片
    2.编译 元素节点 和 文本节点
    3.给模板中的表达式和属性添加观察者
 */

class Compiler {
    // el 为元素节点,el选项中有可能是‘#app’字符串也有可能是document.getElementById('#app')
    // vm 实列
    constructor(el, vm) {
        // 判断el是否是元素,不是就获取
        this.el = this.isElmentNode(el) ? el : document.querySelector(el)
        this.vm = vm
        // 把当前节点的元素获取放到内存中
        let fragment = this.nodeToFragment(this.el)
        // 1. 编译模板 用data中的数据编译, 2. 把内存中的内容进行替换
        this.compile(fragment)
        // 3. 再把替换后的内容回写到页面中
        this.el.appendChild(fragment)
    }
    // 判断是不是元素 
    isElmentNode(node) {
        return node.nodeType === 1
    }
    // 判断是否是指令
    isDirective(attrName) {
        return attrName.startsWith('v-') // 是否含有v-
    }
    // 将节点添加到内存中
    nodeToFragment(node) {
        // 创建一个文档片段接口 目的是为了将这个节点中的每个孩子都写到这个文档碎片中
        let fragment = document.createDocumentFragment()
        let firstChild // 这个节点中的第一个孩子

        while (firstChild = node.firstChild) {
            // appendChild具有移动性,每移动一个节点到内存中,页面上就会少一个节点
            fragment.appendChild(firstChild)
        }

        return fragment
    }
    // 编译内存中的dom节点
    compile(fragmentNode) {
        let childNodes = fragmentNode.childNodes  // 获取的是类数组NodeLis
        let childArr = [...childNodes] // 转化为真实数据, Array.from [...xxx]  [].slice.call
        childArr.forEach(child => {
            // 是否是元素节点
            if (this.isElmentNode(child)) {
                this.compileElement(child)
                this.compile(child)   // 如果是元素的话  需要把自己传进去  再去遍历子节点   递归
            } else {
                // 文本节点
                this.compileText(child)
            }
        })
    }
    // 文件节点的编译 
    compileText(node) {
        let content = node.textContent // 获取内容
        // 通过正则去匹配只需要含有{{}}大括号的,空的不需要 获取大括号中间的内容
        if (/\{\{(.+?)\}\}/.test(content)) {
            // 需要文本来处理
            CompilerUnit['text'](node, content, this.vm)
        }
    }
    // 元素节点的编译
    compileElement(node) {
        let attrbutes = node.attrbutes // 获取元素节点的属性
        let attrbutesArr = [...attrbutes]
        attrbutesArr.forEach(attr => {
            // attr格式:type="text"  v-model="obj.name"
            let [name, expr] = attr.split('=')
            // 判断是否是指令 v-model v-bind v-html等
            if (this.isDirective(name)) {
                let [, directive] = name.split('-')
                // 需要调用不同的指令来处理
                CompilerUnit[directive](node, expr, this.vm)
            }
        })
    }
}

const CompilerUnit = {
    // 根据表达式获取对应数据
    getVal(vm, expr) {
        return expr.split('.').reduce((data, current) => {
            return data[current]
        }, vm.$data)
    },
    // 设置值
    setVal(vm, expr, value) {
        expr.split('.').reduce((data, current, index, arr) => {
            if (index === arr.length - 1) {
                return data[current] = value
            }
            return data[current]
        }, vm.$data)
    },
    // 文本节点内容
    getContentValue(vm, expr) {
        return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
            return this.getVal(vm, args[1].trim())
        })
    },
    // 处理v-model  node 节点 expr表达式 vm当前实列
    model(node, expr, vm) {
        // 给输入框赋予value属性 node.value = xxx
        let fn = this.updater['modelUpdater']
        //给输入框加一个观察者 数据更新会触发此方法 会拿新值给 输入框赋值
        new Watcher(vm, expr, newValue => {
            fn(node, newValue)
        })
        node.addEventListener('input', e => {
            let value = e.target.value
            this.setVal(vm, expr, value)
        })
        let value = this.getVal(vm, expr)
        fn(node, value)
    },
    // 处理文本 node 节点, expr表达式 vm 实列
    text(node, expr, vm) {
        let fn = this.updater['textUpdater']
        let content = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
            // 给表达式 {{}} 加上观察者
            new Watcher(vm, args[1], () => {
                fn(node, this.getContentValue(vm, expr)) // 返回了一个新的字符串
            })
            return this.getVal(vm, args[1].trim())
        })
        fn(node, content)
    },
    updater: {
        // 把数据插入到节点中
        modelUpdater(node, value) {
            node.value = value
        },
        //    处理文本
        textUpdater(node, value) {
            node.textContent = value
        }
    }
}

/**
 * 发布订阅 - Watcher
 * Watcher 订阅者作为 Observer 和 Compile 之间通信的桥梁,主要做的事情是:
 *  1.在自身实例化时往属性订阅器(dep)里面添加自己
    2.自身必须有一个update()方法
    3.待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功成身退。
 * 
 * Dep 和 Watcher 是简单的观察者模式的实现,Dep 即订阅者,它会管理所有的观察者,并且有给观察者发送消息的能力。
 * Watcher 即观察者,当接收到订阅者的消息后,观察者会做出自己的更新操作。
 */
// Dep 即订阅者,它会管理所有的观察者,并且有给观察者发送消息的能力。
class Dep {
    constructor() {
        this.subs = []
    }
    // 订阅
    addSub(watcher) {
        this.subs.push(watcher)
    }
    // 发布
    notify() {
        this.subs.forEach(watcher => watcher.update())
    }
}

// 观察者 当接收到订阅者的消息后,观察者会做出自己的更新操作
class Watcher {
    constructor(vm, expr, cb) {
        this.vm = vm
        this.expr = expr // 添加到订约中
        this.cb = cb
        this.oldValue = this.get() // 存放一个旧值
    }
    get() {
        Dep.target = this // 将当前的watcher放入Dep.target
        let value = CompilerUnit.getVal(this.vm, this.expr)
        Dep.target = null
        return value
    }
    update() {
        let newValue = CompilerUnit.getVal(this.vm, this.expr)
        if (newValue != this.oldValue) {
            this.cb(newValue)
        }
    }
}

/**
 * 整合 - MVVM
 * MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,
 * 通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,
 * 达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果。
 * 
 */

class MVVM {
    constructor(options) {
        // 当new该类时,参数就会传到构造函数中 options就是el  data computed ...
        this.$el = options.el
        this.$data = options.data
        // 判断根元素是否存在 <div id='app'></div> =>  编译模板
        if (this.$el) {
            // 把data里的数据 全部转化成用Object.defineProperty来定义
            new Observer(this.$data)
            new Compiler(this.$el, this)
        }
    }
}

// 扩展 - 实现computed
// computed 具有缓存功能,当依赖的属性发送变化,才会更新视图变化

function initComputed() {
    let vm = this // 将当前this挂载到vm
    let computed = this.$options.computed // 从options上拿到computed属性
    // 得到的都是对象的key可以通过Object.keys转化为数组
    Object.keys(computed).forEach(key => {
        Object.defineProperty(vm, key, {
            // 判断是computed里的key是对象还是函数
            // 若是函数,则直接就调get方法
            // 若是对象,则需要手动调一下get方法
            // 因为computed只根据依赖的属性进行触发,当获取依赖属性时,系统会自动的去调用get方法,所以就不要用Watcher去监听变化了

            get() {
                return typeof computed[key] === 'function' ? computed[key] : computed[key].get
            },
            set() {

            }
        })
    })
}
console 命令行工具 X clear

                    
>
console