理解mvvm原理

通过以下demo,深入理解mvvm的原理,实现数据劫持,数据双向绑定,数据驱动页面,数据双向绑定,计算属性computed!
注意:为了方便理解,我会在每个函数内部把执行顺序和执行思路用文字注释,望知晓...

基本文档结构

  • HTML

    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>vm的demo</title>
    </head>
    <body>
    <div id="app">
        <div>a的值是:{{a.a}}</div>
        <p>b的值是:{{b}}</p>
        <input type="text" v-model="b">
        <p>computed: {{hello}}</p>
    </div>
    <script src="./lvm2.js"></script>
    <script>
    var lvm = new Lvm({
        el: '#app',
        data: {
            a: {a:3},
            b: '2',
            c: 10
        },
        computed: {
            hello (){
                console.log('computed')
                return this.c + this.b
            }
        }
    })
    </script>
    </body>
    </html>

  • 新建js文件

    //新建一个vm对象
    function Lvm (opt = {}) {}

初始化对象实现

  • 1.对象初始化

  • 2.数据劫持

  • 3.this代理data对象

    function Lvm (opt = {}) {
    //1.拿到opt对象,保存到this下边
    this.$options = opt;
    var data = this._data = this.$options.data;
    //2.进行数据劫持
    createObj (data)
    //3.this代理this._data对象
    for (let key in data) {
        Object.defineProperty(this, key, {
            enumerable: true,
            get: () => this._data[key],
            set: (newVal) =>this._data[key] = newVal
        })
    }
    }
    /**
    * @desc 数据劫持方法
    * 为了方便递归调用Objserver方法封装,返回值是Objserver的一个实例
    */
    function createObj (data) {
    if (typeof data !== 'object') return;
    return new Objserver(data)
    }
    
    /**
    * @desc 创建对象函数
    * @param {object} data 创建对象时传入的data对象
    * 1.遍历data对象,拿到左右的对象名和属性值
    * 2.使用Object.defineProperty创建对象
    * 3.加入遍历出来的val是一个对象,调用自己,实现递归调用
    * Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。
    */
    function Objserver (data) {
    for(let key in data) {
        let val  = data[key];
        createObj(val) 
        //使用Object.defineProperty 进行数据劫持
        Object.defineProperty(data, key, {
            enumerable: true,
            get: function () {
                return val
            },
            set: function (newVal) {
                //如果用户输入的新值和原先的值完全相同,则不进行操作直接返回
                if (newVal === val) return;
                val = newVal
                //跟新属性值以后重新监听数据
                createObj(val) 
            }
        })
    }
    }

实现页面渲染

  • 1.dom移动到内存
  • 2.对字符模板替换
  • 3.页面重绘
function Lvm (opt = {}) {
    //1.拿到opt对象,保存到this下边
    this.$options = opt;
    var data = this._data = this.$options.data;
    //2.进行数据劫持
    createObj (data)
    //3.this代理this._data对象
    for (let key in data) {
        Object.defineProperty(this, key, {
            enumerable: true,
            get: () => this._data[key],
            set: newVal =>this._data[key] = newVal
        })
    }
    //4.编译页面
    new Compile(this.$options.el, this);
}
/**
 * @desc 数据劫持方法
 */
function createObj (data) {
    if (typeof data !== 'object') return;
    return new Objserver(data)
}

/**
 * @desc 创建对象函数
 * @param {object} data 创建对象时传入的data对象
 * 1.遍历data对象,拿到左右的对象名和属性值
 * 2.使用Object.defineProperty创建对象
 * 3.加入遍历出来的val是一个对象,调用自己,实现递归调用
 * Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。
 */
function Objserver (data) {

    for(let key in data) {
        let val  = data[key];

        createObj(val) 

        //使用Object.defineProperty 进行数据劫持
        Object.defineProperty(data, key, {
            enumerable: true,
            get: function () {
                return val
            },
            set: function (newVal) {
                //如果用户输入的新值和原先的值完全相同,则不进行操作直接返回
                if (newVal === val) return;
                val = newVal
                //跟新属性值以后重新监听数据
                createObj(val) 
            }
        })
    }
}

/**
 * @desc 编译模板
 * @param {string} el 包裹元素选择器
 * @param {object} vm 我们的lvm实例对象
 * 
 * 1.拿到我们初始化的el元素
 * 2.新建一个文档随便,用来缓存dom
 * 3.遍历el元素的所有子元素节点,添加到文档碎片
 * 4.进行表达式里的值替换
 * 5.添加到dom,更新页面
 */
function Compile (el, vm) {
    vm.$el = document.querySelector(el);
    var frag = document.createDocumentFragment(), node = null;
    //把app的dom移动到内存中进行操作
    while(node = vm.$el.firstChild) {
        frag.appendChild(node)
    }
    //替换变量
    replice (frag)


    /**
     * @descrtion 替换内容
     * @param  {dom} node html dom标签
     * @return {null}   没有返回值
     *
     * 1.传入所有的dom的元素节点
     * 2.使用Array.form转换为数组以后遍历处理
     * 3.新建reg对象,匹配和捕获表达式里的
     * 4.拿到每一个节点的文本节点(如果是元素,则递归调用自己)
     * 5.通过nodeType判断是不是文本节点,并且使用正则对象验证是否包含大括号表达式
     * 6.如果有大括号表达式,则进行替换赋值操作
     * 7.数据监听(接下来实现)
     * 8.数据双向绑定(稍后实现)
     * 9.添加到dom
     */
    function replice (node) {
        // 把node类数组转换为数组遍历
        Array.from(node.childNodes).forEach(item => {
            var reg = /\{\{(.*)\}\}/,
                text = item.textContent;//拿到文本节点
            if(item.nodeType == 3 && reg.test(text)) {
                reg.exec(text)
                var key, arr = RegExp.$1.split('.'), //如果拿到的是对象的深层属性(a.a.b),在下边进行遍历 
                    val = vm;//这里设置为vm => this 对象以后就可以直接访问得到数据

                arr.forEach(key => {
                    //得到数据 遍历拿到对象属性的属性,主要操作多层数据
                    val = val[key]
                })
                //替换赋值操作
                item.textContent = text.replace(/\{\{(.*)\}\}/, val)
                val = null;
            }
            //如果node不是文本节点,继续循环
            if (item.childNodes) {
                replice (item)
            }
        })
    }
    //把操作过的数据添加到dom
    vm.$el.appendChild(frag)
    frag = null
}

实现数据监听动态更新页面

  • 模拟监听事件池
  • 监听函数weacher

function Lvm (opt = {}) {
    //1.拿到opt对象,保存到this下边
    this.$options = opt;
    var data = this._data = this.$options.data;
    //2.进行数据劫持
    createObj (data)
    //3.this代理this._data对象
    for (let key in data) {
        Object.defineProperty(this, key, {
            enumerable: true,
            get: () => this._data[key],
            set: newVal =>this._data[key] = newVal
        })
    }
    //4.编译页面
    new Compile(this.$options.el, this);
}
/**
 * @desc 数据劫持方法
 */
function createObj (data) {
    if (typeof data !== 'object') return;
    return new Objserver(data)
}

/**
 * @desc 创建对象函数
 * @param {object} data 创建对象时传入的data对象
 * 1.遍历data对象,拿到左右的对象名和属性值
 * 2.使用Object.defineProperty创建对象
 * 3.加入遍历出来的val是一个对象,调用自己,实现递归调用
 * 4.新建一个事件池dep
 * Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。
 */
function Objserver (data) {
    let dep = new Dep();
    for(let key in data) {
        let val  = data[key];

        createObj(val) 

        //使用Object.defineProperty 进行数据劫持
        Object.defineProperty(data, key, {
            enumerable: true,
            get: function () {
                //如果存在target属性的话添加到事件池dep
                Dep.target && dep.addSub(Dep.target)
                return val
            },
            set: function (newVal) {
                //如果用户输入的新值和原先的值完全相同,则不进行操作直接返回
                if (newVal === val) return;
                val = newVal
                //跟新属性值以后重新监听数据
                createObj(val) 
                //调用事件池通知方法 提醒更新时视图
                dep.notify()
            }
        })
    }
}

/**
 * @desc 编译模板
 * @param {string} el 包裹元素选择器
 * @param {object} vm 我们的lvm实例对象
 * 
 * 1.拿到我们初始化的el元素
 * 2.新建一个文档随便,用来缓存dom
 * 3.遍历el元素的所有子元素节点,添加到文档碎片
 * 4.进行表达式里的值替换
 * 5.添加到dom,更新页面
 */
function Compile (el, vm) {
    vm.$el = document.querySelector(el);
    var frag = document.createDocumentFragment(), node = null;
    //把app的dom移动到内存中进行操作
    while(node = vm.$el.firstChild) {
        frag.appendChild(node)
    }
    //替换变量
    replice (frag)


    /**
     * @descrtion 替换内容
     * @param  {dom} node html dom标签
     * @return {null}   没有返回值
     *
     * 1.传入所有的dom的元素节点
     * 2.使用Array.form转换为数组以后遍历处理
     * 3.新建reg对象,匹配和捕获表达式里的
     * 4.拿到每一个节点的文本节点(如果是元素,则递归调用自己)
     * 5.通过nodeType判断是不是文本节点,并且使用正则对象验证是否包含大括号表达式
     * 6.如果有大括号表达式,则进行替换赋值操作
     * 7.数据监听(接下来实现)
     * 8.数据双向绑定(稍后实现)
     * 9.添加到dom
     */
    function replice (node) {

        // 把node类数组转换为数组遍历
        Array.from(node.childNodes).forEach(item => {
            var reg = /\{\{(.*)\}\}/,
                text = item.textContent;//拿到文本节点
            if(item.nodeType == 3 && reg.test(text)) {
                reg.exec(text)
                var key, arr = RegExp.$1.split('.'), //如果拿到的是对象的深层属性(a.a.b),在下边进行遍历 
                    val = vm;//这里设置为vm => this 对象以后就可以直接访问得到数据

                arr.forEach(key => {
                    //得到数据 遍历拿到对象属性的属性,主要操作多层数据
                    val = val[key]
                })
                //创建监听小函数
                new Wetcher(vm, arr, function(newVal){
                    item.textContent = text.replace(/\{\{(.*)\}\}/, newVal)
                })
                //替换赋值操作
                item.textContent = text.replace(/\{\{(.*)\}\}/, val)
                val = null;
            }
            //如果node不是文本节点,继续循环
            if (item.childNodes) {
                replice (item)
            }
        })
    }
    //把操作过的数据添加到dom
    vm.$el.appendChild(frag)
    frag = null
}

/**
 * 新建一个dep 实现模拟事件池
 * subs 保存所有需要监听的对象
 * addSub 将监听事件添加到事件池
 * notify当数据发生变化的时候调用,通知更新数据
 * notify触发的时候,遍历执行subs里的所有事件
 */
function Dep () {
    this.subs = [];
}

/**
 * @desc 事件池添加方法
 * @param {Function} fn 添加事件到事件池
 */
Dep.prototype.addSub = function (fn) {
    this.subs.push(fn)
}
/**
 * @desc 数据更新时刷新页面
 * @return {[type]} [description]
 */
Dep.prototype.notify = function () {
    this.subs.forEach(fn => {
        fn.update()
    })
}

/**
 * @desc 监听小函数 每个需要监听的对象的实例对象
 * @param {object}   vm  当前this的实例
 * @param {string}   exp 监听的属性 a|a.a 
 * @param {Function} fn  数据更改以后执行的回调函数
 *
 * 1.我们在创建一个wecher监听对象的实例,传入上述的参数
 * 2.为了方便加入事件池,在初始化函数的过程中给Dep一个target属性,
 * 然后在触发属性的get方法,在哪里判断是否存在target属性,存在就加入,不存在则不操作
 * 3.在操作完成以后Dep.target属性制空
 */
function Wetcher (vm, exp, fn){
    this.fn = fn;
    this.vm = vm;
    this.exp = exp;
    Dep.target = this;
    let val = this.vm;
    let arr = this.exp;

    //获取值 会调用get方法
    arr.forEach(k => {
        val = val[k]
    })

    Dep.target = null;
}
/**
 * @description 监听小函数在数据变更以后调用
 * @return {[type]} [description]
 * 1.在这里我们拿到更新后的数据
 * 2.调用调用回调函数fn,并将新值传入
 */
Wetcher.prototype.update = function () {
    let val = this.vm
    let arr = this.exp
    //获取值 会调用get方法
    arr.forEach(k => {
        val = val[k]
    })
    //执行创建实例时的回调函数,并且传入新值替换
    this.fn(val)
}

实现数据双向绑定

  • input赋值
  • input绑定事件
  • input的value更新的时候更新数据

function Lvm (opt = {}) {
    //1.拿到opt对象,保存到this下边
    this.$options = opt;
    var data = this._data = this.$options.data;
    //2.进行数据劫持
    createObj (data)
    //3.this代理this._data对象
    for (let key in data) {
        Object.defineProperty(this, key, {
            enumerable: true,
            get: () => this._data[key],
            set: newVal =>this._data[key] = newVal
        })
    }
    //4.编译页面
    new Compile(this.$options.el, this);
}
/**
 * @desc 数据劫持方法
 */
function createObj (data) {
    if (typeof data !== 'object') return;
    return new Objserver(data)
}

/**
 * @desc 创建对象函数
 * @param {object} data 创建对象时传入的data对象
 * 1.遍历data对象,拿到左右的对象名和属性值
 * 2.使用Object.defineProperty创建对象
 * 3.加入遍历出来的val是一个对象,调用自己,实现递归调用
 * 4.新建一个事件池dep
 * Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。
 */
function Objserver (data) {
    let dep = new Dep();
    for(let key in data) {
        let val  = data[key];

        createObj(val) 

        //使用Object.defineProperty 进行数据劫持
        Object.defineProperty(data, key, {
            enumerable: true,
            get: function () {
                //如果存在target属性的话添加到事件池dep
                Dep.target && dep.addSub(Dep.target)
                return val
            },
            set: function (newVal) {
                //如果用户输入的新值和原先的值完全相同,则不进行操作直接返回
                if (newVal === val) return;
                val = newVal
                //跟新属性值以后重新监听数据
                createObj(val) 
                //调用事件池通知方法 提醒更新时视图
                dep.notify()
            }
        })
    }
}

/**
 * @desc 编译模板
 * @param {string} el 包裹元素选择器
 * @param {object} vm 我们的lvm实例对象
 * 
 * 1.拿到我们初始化的el元素
 * 2.新建一个文档随便,用来缓存dom
 * 3.遍历el元素的所有子元素节点,添加到文档碎片
 * 4.进行表达式里的值替换
 * 5.添加到dom,更新页面
 */
function Compile (el, vm) {
    vm.$el = document.querySelector(el);
    var frag = document.createDocumentFragment(), node = null;
    //把app的dom移动到内存中进行操作
    while(node = vm.$el.firstChild) {
        frag.appendChild(node)
    }
    //替换变量
    replice (frag)


    /**
     * @descrtion 替换内容
     * @param  {dom} node html dom标签
     * @return {null}   没有返回值
     *
     * 1.传入所有的dom的元素节点
     * 2.使用Array.form转换为数组以后遍历处理
     * 3.新建reg对象,匹配和捕获表达式里的
     * 4.拿到每一个节点的文本节点(如果是元素,则递归调用自己)
     * 5.通过nodeType判断是不是文本节点,并且使用正则对象验证是否包含大括号表达式
     * 6.如果有大括号表达式,则进行替换赋值操作
     * 7.数据监听(接下来实现)
     * 8.数据双向绑定(稍后实现)
     * 9.添加到dom
     */
    function replice (node) {

        // 把node类数组转换为数组遍历
        Array.from(node.childNodes).forEach(item => {
            var reg = /\{\{(.*)\}\}/,
                text = item.textContent;//拿到文本节点
            if(item.nodeType == 3 && reg.test(text)) {
                reg.exec(text)
                var key, arr = RegExp.$1.split('.'), //如果拿到的是对象的深层属性(a.a.b),在下边进行遍历 
                    val = vm;//这里设置为vm => this 对象以后就可以直接访问得到数据

                arr.forEach(key => {
                    //得到数据 遍历拿到对象属性的属性,主要操作多层数据
                    val = val[key]
                })
                //创建监听小函数
                new Wetcher(vm, arr, function(newVal){
                    item.textContent = text.replace(/\{\{(.*)\}\}/, newVal)
                })
                //替换赋值操作
                item.textContent = text.replace(/\{\{(.*)\}\}/, val)
                val = null;
            }
            //实现数据双向绑定 
            //思路:
            //首先判断如果是元素,是否存在v-model属性,(拿到所有的attribute,然后遍历判断)
            //如果不存在就不操作,如果存在(v-model)属性则进行如下操作
            //拿到属性,遍历对象拿到val,赋值给元素
            //给input元素绑定事件,在数据变化的时候那导致,然后重新赋值
            //因为这里赋值如果说嵌套层级较多的时候需要进行特殊处理,我偷懒,没有做,只做了一级的,
            if (item.nodeType == 1) {
                let attrs = item.attributes;
                let reg = /v\-model/;
                //循环遍历属性 看有没有v-model
                Array.from(attrs).forEach(attr => {
                    let name = attr.name;
                    let exp = attr.value.split('.');
                    if (reg.test(name)) {
                        let val = vm;

                        exp.forEach(k => {
                                val = val[k]
                            })
                        item.value = val;

                        //监听数据变化 更新input的值
                        new Wetcher(vm, exp, function(newVal){
                            item.value = newVal
                        })

                        item.addEventListener('input', function (e) {
                            let val = e.target.value;
                            //还没有想到多层级的数据怎么设置,后期学了再补
                            vm[exp] = val
                        })
                    }
                })
            }
            //如果node不是文本节点,继续循环
            if (item.childNodes) {
                replice (item)
            }
        })
    }
    //把操作过的数据添加到dom
    vm.$el.appendChild(frag)
    frag = null
}

/**
 * 新建一个dep 实现模拟事件池
 * subs 保存所有需要监听的对象
 * addSub 将监听事件添加到事件池
 * notify当数据发生变化的时候调用,通知更新数据
 * notify触发的时候,遍历执行subs里的所有事件
 */
function Dep () {
    this.subs = [];
}

/**
 * @desc 事件池添加方法
 * @param {Function} fn 添加事件到事件池
 */
Dep.prototype.addSub = function (fn) {
    this.subs.push(fn)
}
/**
 * @desc 数据更新时刷新页面
 * @return {[type]} [description]
 */
Dep.prototype.notify = function () {
    this.subs.forEach(fn => {
        fn.update()
    })
}

/**
 * @desc 监听小函数 每个需要监听的对象的实例对象
 * @param {object}   vm  当前this的实例
 * @param {string}   exp 监听的属性 a|a.a 
 * @param {Function} fn  数据更改以后执行的回调函数
 *
 * 1.我们在创建一个wecher监听对象的实例,传入上述的参数
 * 2.为了方便加入事件池,在初始化函数的过程中给Dep一个target属性,
 * 然后在触发属性的get方法,在哪里判断是否存在target属性,存在就加入,不存在则不操作
 * 3.在操作完成以后Dep.target属性制空
 */
function Wetcher (vm, exp, fn){
    this.fn = fn;
    this.vm = vm;
    this.exp = exp;
    Dep.target = this;
    let val = this.vm;
    let arr = this.exp;
    //获取值 会调用get方法
    arr.forEach(k => {
        val = val[k]
    })

    Dep.target = null;
}
/**
 * @description 监听小函数在数据变更以后调用
 * @return {[type]} [description]
 * 1.在这里我们拿到更新后的数据
 * 2.调用调用回调函数fn,并将新值传入
 */
Wetcher.prototype.update = function () {
    let val = this.vm
    let arr = this.exp
    //获取值 会调用get方法
    arr.forEach(k => {
        val = val[k]
    })
    //执行创建实例时的回调函数,并且传入新值替换
    this.fn(val)
}

实现computed

  • 新建一个initComputed函数
  • 获取computed的的值进行操作

function Lvm (opt = {}) {
    //1.拿到opt对象,保存到this下边
    this.$options = opt;
    var data = this._data = this.$options.data;
    //2.进行数据劫持
    createObj (data)
    //3.this代理this._data对象
    for (let key in data) {
        Object.defineProperty(this, key, {
            enumerable: true,
            get: () => this._data[key],
            set: newVal =>this._data[key] = newVal
        })
    }
    //计算属性初始化 必须在compile之前调用 否则无效 
    initComputed.call(this)

    //5.编译页面
    new Compile(this.$options.el, this);
}
/**
 * @desc 数据劫持方法
 */
function createObj (data) {
    if (typeof data !== 'object') return;
    return new Objserver(data)
}

/**
 * @desc 创建对象函数
 * @param {object} data 创建对象时传入的data对象
 * 1.遍历data对象,拿到左右的对象名和属性值
 * 2.使用Object.defineProperty创建对象
 * 3.加入遍历出来的val是一个对象,调用自己,实现递归调用
 * 4.新建一个事件池dep
 * Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。
 */
function Objserver (data) {
    let dep = new Dep();
    for(let key in data) {
        let val  = data[key];

        createObj(val) 

        //使用Object.defineProperty 进行数据劫持
        Object.defineProperty(data, key, {
            enumerable: true,
            get: function () {
                //如果存在target属性的话添加到事件池dep
                Dep.target && dep.addSub(Dep.target)
                return val
            },
            set: function (newVal) {
                //如果用户输入的新值和原先的值完全相同,则不进行操作直接返回
                if (newVal === val) return;
                val = newVal
                //跟新属性值以后重新监听数据
                createObj(val) 
                //调用事件池通知方法 提醒更新时视图
                dep.notify()
            }
        })
    }
}

/**
 * @desc 编译模板
 * @param {string} el 包裹元素选择器
 * @param {object} vm 我们的lvm实例对象
 * 
 * 1.拿到我们初始化的el元素
 * 2.新建一个文档随便,用来缓存dom
 * 3.遍历el元素的所有子元素节点,添加到文档碎片
 * 4.进行表达式里的值替换
 * 5.添加到dom,更新页面
 */
function Compile (el, vm) {
    vm.$el = document.querySelector(el);
    var frag = document.createDocumentFragment(), node = null;
    //把app的dom移动到内存中进行操作
    while(node = vm.$el.firstChild) {
        frag.appendChild(node)
    }
    //替换变量
    replice (frag)


    /**
     * @descrtion 替换内容
     * @param  {dom} node html dom标签
     * @return {null}   没有返回值
     *
     * 1.传入所有的dom的元素节点
     * 2.使用Array.form转换为数组以后遍历处理
     * 3.新建reg对象,匹配和捕获表达式里的
     * 4.拿到每一个节点的文本节点(如果是元素,则递归调用自己)
     * 5.通过nodeType判断是不是文本节点,并且使用正则对象验证是否包含大括号表达式
     * 6.如果有大括号表达式,则进行替换赋值操作
     * 7.数据监听(接下来实现)
     * 8.数据双向绑定(稍后实现)
     * 9.添加到dom
     */
    function replice (node) {

        // 把node类数组转换为数组遍历
        Array.from(node.childNodes).forEach(item => {
            var reg = /\{\{(.*)\}\}/,
                text = item.textContent;//拿到文本节点
            if(item.nodeType == 3 && reg.test(text)) {
                reg.exec(text)
                var key, arr = RegExp.$1.split('.'), //如果拿到的是对象的深层属性(a.a.b),在下边进行遍历 
                    val = vm;//这里设置为vm => this 对象以后就可以直接访问得到数据

                arr.forEach(key => {
                    //得到数据 遍历拿到对象属性的属性,主要操作多层数据
                    val = val[key]
                })
                //创建监听小函数
                new Wetcher(vm, arr, function(newVal){
                    item.textContent = text.replace(/\{\{(.*)\}\}/, newVal)
                })
                //替换赋值操作
                item.textContent = text.replace(/\{\{(.*)\}\}/, val)
                val = null;
            }
            //实现数据双向绑定 
            //思路:
            //首先判断如果是元素,是否存在v-model属性,(拿到所有的attribute,然后遍历判断)
            //如果不存在就不操作,如果存在(v-model)属性则进行如下操作
            //拿到属性,遍历对象拿到val,赋值给元素
            //给input元素绑定事件,在数据变化的时候那导致,然后重新赋值
            //因为这里赋值如果说嵌套层级较多的时候需要进行特殊处理,我偷懒,没有做,只做了一级的,
            if (item.nodeType == 1) {
                let attrs = item.attributes;
                let reg = /v\-model/;
                //循环遍历属性 看有没有v-model
                Array.from(attrs).forEach(attr => {
                    let name = attr.name;
                    let exp = attr.value.split('.');
                    if (reg.test(name)) {
                        let val = vm;

                        exp.forEach(k => {
                                val = val[k]
                            })
                        item.value = val;

                        //监听数据变化 更新input的值
                        new Wetcher(vm, exp, function(newVal){
                            item.value = newVal
                        })

                        item.addEventListener('input', function (e) {
                            let val = e.target.value;
                            //还没有想到多层级的数据怎么设置,后期学了再补
                            vm[exp] = val
                        })
                    }
                })
            }
            //如果node不是文本节点,继续循环
            if (item.childNodes) {
                replice (item)
            }
        })
    }
    //把操作过的数据添加到dom
    vm.$el.appendChild(frag)
    frag = null
}

/**
 * 新建一个dep 实现模拟事件池
 * subs 保存所有需要监听的对象
 * addSub 将监听事件添加到事件池
 * notify当数据发生变化的时候调用,通知更新数据
 * notify触发的时候,遍历执行subs里的所有事件
 */
function Dep () {
    this.subs = [];
}

/**
 * @desc 事件池添加方法
 * @param {Function} fn 添加事件到事件池
 */
Dep.prototype.addSub = function (fn) {
    this.subs.push(fn)
}
/**
 * @desc 数据更新时刷新页面
 * @return {[type]} [description]
 */
Dep.prototype.notify = function () {
    this.subs.forEach(fn => {
        fn.update()
    })
}

/**
 * @desc 监听小函数 每个需要监听的对象的实例对象
 * @param {object}   vm  当前this的实例
 * @param {string}   exp 监听的属性 a|a.a 
 * @param {Function} fn  数据更改以后执行的回调函数
 *
 * 1.我们在创建一个wecher监听对象的实例,传入上述的参数
 * 2.为了方便加入事件池,在初始化函数的过程中给Dep一个target属性,
 * 然后在触发属性的get方法,在哪里判断是否存在target属性,存在就加入,不存在则不操作
 * 3.在操作完成以后Dep.target属性制空
 */
function Wetcher (vm, exp, fn){
    this.fn = fn;
    this.vm = vm;
    this.exp = exp;
    Dep.target = this;
    let val = this.vm;
    let arr = this.exp;
    //获取值 会调用get方法
    arr.forEach(k => {
        val = val[k]
    })

    Dep.target = null;
}
/**
 * @description 监听小函数在数据变更以后调用
 * @return {[type]} [description]
 * 1.在这里我们拿到更新后的数据
 * 2.调用调用回调函数fn,并将新值传入
 */
Wetcher.prototype.update = function () {
    let val = this.vm
    let arr = this.exp
    //获取值 会调用get方法
    arr.forEach(k => {
        val = val[k]
    })
    //执行创建实例时的回调函数,并且传入新值替换
    this.fn(val)
}

/**
 * @description 计算属性函数
 * @return {} [description]
 */
function initComputed () {
    let vm = this
    let computed = vm.$options.computed

    //拿到所有的computed属性,进行遍历操作
    //Object.keys(computed) => 返回computed对象的所有属性的name的数组
    //判断如果val是一个函数,则直接返回这个函数,如果是一个变量,则调用变量的get方法
    Object.keys(computed).forEach(key => {

        //建立数据劫持
        Object.defineProperty(vm, key, {
            get: typeof computed[key] === 'function' ? computed[key] 
                : computed[key].get,
            set: function() {}
        })
    })
}
  • 3
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值