如何自己实现一个Vue2中的数据双向绑定

Vue中的数据双向绑定(Vue2)

前置条件

  1. 数组的reduce()方法
  2. 发布订阅模式
  3. 使用Object.defineProperty()进行数据劫持

数组的reduce()方法

reduce方法基本定义

MDN:Array.prototype.reduce()

Array.prototype.reduce()
reduce() 方法对数组中的每个元素按序执行一个由您提供的 reducer 函数,每一次运行 reducer 会将先前元素的计算结果作为参数传入,最后将其结果汇总为单个返回值。
第一次执行回调函数时,不存在“上一次的计算结果”。如果需要回调函数从数组索引为 0 的元素开始执行,则需要传递初始值。否则,数组索引为 0 的元素将被作为初始值 initialValue,迭代器将从第二个元素开始执行(索引为 1 而不是 0)。

概括说:reduce()就是一个滚雪球的方法,方便用于实现累加之类的操作

下面是一个例子:

let arr = [1, 2, 3, 4, 5]
let sum = arr.reduce((pre, item) => { return pre + item }, 0)
console.log(sum) //15

reduce函数有两个参数,第一个是每轮的回调函数,每次遍历都会执行这个函数,第二个参数为初始值,第一次遍历的时候会把初始值作为参数传入回调函数中,回调函数有多个参数,一般用前两个分别为上一次的值和当前遍历的值,之后回调函数会返回一个值,作为下一次遍历的pre值

reduce((previousValue, currentValue, currentIndex, array) => { /* ... */ }, initialValue)
reduce方法获取对象属性

通过一个数组,可以访问到对象最底层的属性

let arr = ['info', 'address', 'location']
let obj = {
    name: 'zs',
    info: {
        address: {
            location: 'china'
        }
    }
}
let addr = arr.reduce((pre, k) => { return pre[k] }, obj)
console.log(addr) //china

通过字符串获得所需要的数组,得到所需要的属性

let obj = {
    name: 'zs',
    info: {
        address: {
            location: 'china'
        }
    }
}

let addr = 'info.address.location'
let arr = addr.split('.').reduce((pre, k) => pre[k], obj)
console.log(arr) //china

用这种方法就可以处理Vue中的插值语句,例如{{info.address.location}}

发布订阅模式

发布订阅模式可以简单理解为,有一个对象负责收集所有的订阅信息,一旦收到更新消息后,就对订阅者进行通知,订阅者就进行更新

class Dep { //订阅信息收集类
    constructor() {
        this.sub = [] //订阅者数组
    }
    addSub(watcher) { //添加订阅者的方法
        this.sub.push(watcher)
    }
    notify() { //通知订阅者更新的方法
        this.sub.forEach(watcher => {
            watcher.update()
        })
    }
}
class Watcher {
    constructor(cb) { //传一个回调函数
        this.cb = cb
    }
    update() { //触发回调的方法
        this.cb()
    }
}

let w1 = new Watcher(() => {
    console.log('我是第一个订阅者')
})

let w2 = new Watcher(() => {
    console.log('我是第二个订阅者')
})

let dep = new Dep()
dep.addSub(w1)
dep.addSub(w2)
dep.notify()
    //输出
    //我是第一个订阅者
    //我是第二个订阅者

这种方法就实现了,当给一个data更新的时候,vue监听到了,就调用Dep.notify()通知每个订阅者,每一个订阅者就更新自己的dom元素,每一个Watcher在创建的时候都有一个回调函数作为参数,这个回调的作用就是更新自己的dom元素

使用Object.defineProperty()进行数据劫持

MDN:Object.defineProperty()

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

const object1 = {};
Object.defineProperty(object1, 'property1', {
    value: 42,
    writable: false
});
object1.property1 = 77;
// throws an error in strict mode

console.log(object1.property1);
// expected output: 42

Object.defineProperty()有三个参数,分别为,对象、属性名和属性描述信息
在其中可以进行多种配置,如上面的value和writable,其中有两个方法是实现数据劫持的关键方法,即get()和set()

get

属性的 getter 函数,如果没有 getter,则为 undefined。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。
默认为 undefined。

set

属性的 setter 函数,如果没有 setter,则为 undefined。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。
默认为 undefined。

const obj = {}
Object.defineProperty(obj, 'name', {
    enumerable: true, //该属性可以被循环,比如forIn
    configurable: true, //该属性可以被配置
    get() {
        console.log('有人正在取obj.name')
        return '不给你取值' //取值操作会被拦截,取到的值是返回的一个结果
    },
    set(newValue) {
        console.log('有人正在设置obj.name', newValue)
            //赋值操作会被拦截,可以获取到新赋的值,但是不会赋值给属性
    }
})

obj.name = 'zs' //赋值
console.log(obj.name) //取值

实现一个数据劫持功能的Vue

首先写一个基本的html页面,类似Vue

index.html

<body>
    <div id="app">
        <h1>姓名是{{name}}</h1>
        <h1>年龄是{{age}}</h1>
    </div>
    <script src="./vue.js"></script>
    <script>
        let vue = new Vue({
            el: '#app',
            data: {
                name: 'zs',
                age: 18,
                attr: {
                    a1: 'a1',
                    a2: 'a2'
                }
            }
        })
    </script>
</body>

vue.js

class Vue {
    constructor(option) {
        this.$data = option.data

        //调用数据劫持的方法,对$data上的属性都进行数据劫持
        observe(this.$data)
    }
}

function observe(obj) {
    //如果传过来的不是一个对象,而是一些具体的值比如name,age,则不需要递归
    //这里没有考虑到数组的情况
    if (!obj || typeof obj !== 'object') {
        return
    } else { //如果是对象,就对对象的每个属性添加getter和setter
        //拿到当前对象上的每一个属性,形成一个数组
        console.log(Object.keys(obj)) //['name','age','attr']
        Object.keys(obj).forEach(key => {
            let value = obj[key] //将该属性的值存起来
            observe(value) //对子属性递归,添加getter和setter
            Object.defineProperty(obj, key, {
                enumerable: true,
                configurable: true,
                get() {
                    return value //将改属性的值返回出去
                },
                set(newValue) {
                    value = newValue
                    observe(value) //重新赋值属性,也需要递归添加gettet和setter
                }
            })
        })
    }
}

通过上面的代码就可以实现基本的数据劫持
现在访问data需要用到vue.$data.来访问,而不能通过vue.来访问,所以可以进行一个数据代理,更新后的代码如下

class Vue {
    constructor(option) {
        this.$data = option.data

        //调用数据劫持的方法,对$data上的属性都进行数据劫持
        observe(this.$data)

        //数据代理,可以通过vue.直接访问到vue.$data.的属性
        Object.keys(this.$data).forEach(key => {
            Object.defineProperty(this, key, {
                enumerable: true,
                configurable: true,
                get() {
                    return this.$data[key]
                },
                set(newValue) {
                    this.$data[key] = newValue
                }
            })
        })
    }
}

实现一个单向绑定的Vue

new一个Vue实例的时候会有一个el属性,拿到el属性后对其中的区域进行插值替换需要进行模板编译,因此写一个编译函数
vue.js

class Vue {
    constructor(option) {
        this.$data = option.data

        //调用数据劫持的方法,对$data上的属性都进行数据劫持
        observe(this.$data)

        //数据代理,可以通过vue.直接访问到vue.$data.的属性
        Object.keys(this.$data).forEach(key => {
            Object.defineProperty(this, key, {
                enumerable: true,
                configurable: true,
                get() {
                    return this.$data[key]
                },
                set(newValue) {
                    this.$data[key] = newValue
                }
            })
        })

        //进行模板编译
        compile(option.el, this)
    }
}

compile()函数

function compile(el, vm) {
    //获取el对应的dom元素
    vm.$el = document.querySelector(el)

    //创建文档碎片,在内存中操作dom,绘制好之后再返回给页面
    //避免页面检测到变化不断重绘重排浪费性能
    const fragment = document.createDocumentFragment()

    //当存在子节点时,每循环一次将子节点移入文档碎片中(此时元素会从页面中消失)
    while ((childNode = vm.$el.firstChild)) {
        fragment.appendChild(childNode)
    }

    //进行模板编译
    replace(fragment)

    //处理完所有的区域后再把子节点还回去
    vm.$el.appendChild(fragment)


    //定义模板编译函数
    function replace(node) {
        //定义插值表达式的正则,匹配{{name}}这种
        const regMustache = /\{\{\s*(\S+)\s*\}\}/

        //只替换文本子节点,否则递归,找到文本子节点,对其替换

        //根据nodeType判断是文本子节点
        if (node.nodeType === 3) {
            const text = node.textContent //拿到文本内容
            const execResult = regMustache.exec(text) //匹配正则文本,1号元素则是需要的属性文本


            if (execResult) {
                //首先分割,然后再用reduce拿到其属性的值,将其返回,值是vm中的$data
                const value = execResult[1].split('.').reduce((preObj, k) => { return preObj[k] }, vm)

                //对当前节点的文本内容进行替换,替换为value
                node.textContent = text.replace(regMustache, value)
            }
            return
        }

        //不是纯文本子节点,则需要去递归其子节点
        node.childNodes.forEach((node) => {
            replace(node)
        })
    }
}

添加发布订阅模式

添加发布订阅模式实现数据更新,否则只会在页面第一次加载时填充

实现

数据更新 => 页面更新

这一句记住了如何更新自己,即watcher的更新方法

node.textContent = text.replace(regMustache, value)

首先需要定义Dep类和Watcher类,和上面代码有些许变化

class Dep {
    constructor() {
        this.subs = []
    }
    addSub(watcher) {
        this.subs.push(watcher)
    }
    notify() {
        this.subs.forEach(watcher => {
            watcher.update()
        })
    }
}

class Watcher {
    //cb中记录着如何更新自己
    //vm中保存这最新数据
    //vm身上的众多数据中哪一个是这个watcher的属性,用key
    constructor(vm, key, cb) {
        this.vm = vm
        this.key = key
        this.cb = cb
    }
    update() {
        this.cb()
    }
}

之后需要创建Watcher,在第一次更新插值替换后,watcher已经知道了如何更新自己,所以在第一次更新后创建watcher实例,传入回调,在compile函数中
更改之后的replace:

function replace(node) {
    //定义插值表达式的正则,匹配{{name}}这种
    const regMustache = /\{\{\s*(\S+)\s*\}\}/

    //只替换文本子节点,否则递归,找到文本子节点,对其替换
    //根据nodeType判断是文本子节点
    if (node.nodeType === 3) {
        const text = node.textContent //拿到文本内容
        const execResult = regMustache.exec(text) //匹配正则文本,1号元素则是需要的属性文本
        if (execResult) {
        //首先分割,然后再用reduce拿到其属性的值,将其返回,值是vm中的$data
        const value = execResult[1].split('.').reduce((preObj, k) => { return preObj[k] }, vm)

        //对当前节点的文本内容进行替换,替换为value
        node.textContent = text.replace(regMustache, value)

        //创建Watcher实例,并且回调中添加一个newVaule为新的值
        new Watcher(vm, execResult[1], (newValue) => {
            node.textContent = text.replace(regMustache, newValue)
            })
        }
        return
    }

    //不是纯文本子节点,则需要去递归其子节点
    node.childNodes.forEach((node) => {
        replace(node)
    })
}
巧妙点

然后是把Watcher实例存到Dep中,这一步的操作时通过Watcher的构造函数来实现的,更新后的Watcher:

class Watcher {
    //cb中记录着如何更新自己
    //vm中保存这最新数据
    //vm身上的众多数据中哪一个是这个watcher的属性,用key
    constructor(vm, key, cb) {
        this.vm = vm
        this.key = key
        this.cb = cb

        //关键点
        // 首先给Dep添加了Target这个属性,其指向了当前的watcher实例,临时挂载属性
        Dep.target = this

        //这时候通过reduce取vm中的值,就会被数据劫持,到get()中执行
        //目的也不是取值,所以不需要拿到返回值
        key.split('.').reduce((preObj, k) => { return preObj[k] }, vm)

        //执行完毕将target置空,避免内存占用
        Dep.target = null
    }
    update() {
        this.cb()
    }
}

function observe(obj) {
    //如果传过来的不是一个对象,而是一些具体的值比如name,age,则不需要递归
    //这里没有考虑到数组的情况
    if (!obj || typeof obj !== 'object') return
    const dep = new Dep()

    //如果是对象,就对对象的每个属性添加getter和setter
    //拿到当前对象上的每一个属性,形成一个数组
    // console.log(Object.keys(obj)) //['name','age','attr']
    Object.keys(obj).forEach(key => {
        let value = obj[key] //将该属性的值存起来
        observe(value) //对子属性递归,添加getter和setter
        Object.defineProperty(obj, key, {
            enumerable: true,
            configurable: true,
            get() {
                //只要执行到了这一行,那么new的Watcher实例
                //就被放到了dep.subs这个数组中了
                Dep.target && dep.addSub(Dep.target)

                return value //将改属性的值返回出去
            },
            set(newValue) {
                value = newValue
                observe(value) //重新赋值属性,也需要递归添加gettet和setter
            }
        })
    })
}

在更新数据的时候也需要通知订阅者,即在set中notify()

function observe(obj) {
    //如果传过来的不是一个对象,而是一些具体的值比如name,age,则不需要递归
    //这里没有考虑到数组的情况
    if (!obj || typeof obj !== 'object') return
    const dep = new Dep()

    //如果是对象,就对对象的每个属性添加getter和setter
    //拿到当前对象上的每一个属性,形成一个数组
    // console.log(Object.keys(obj)) //['name','age','attr']
    Object.keys(obj).forEach(key => {
        let value = obj[key] //将该属性的值存起来
        observe(value) //对子属性递归,添加getter和setter
        Object.defineProperty(obj, key, {
            enumerable: true,
            configurable: true,
            get() {
                //只要执行到了这一行,那么new的Watcher实例
                //就被放到了dep.subs这个数组中了
                Dep.target && dep.addSub(Dep.target)

                return value //将改属性的值返回出去
            },
            set(newValue) {
                value = newValue
                observe(value) //重新赋值属性,也需要递归添加gettet和setter
                //通知更新
                dep.notify()
            }
        })
    })
}

之后写新值的更新update函数

class Watcher {
    //cb中记录着如何更新自己
    //vm中保存这最新数据
    //vm身上的众多数据中哪一个是这个watcher的属性,用key
    constructor(vm, key, cb) {
        this.vm = vm
        this.key = key
        this.cb = cb

        //关键点
        // 首先给Dep添加了Target这个属性,其指向了当前的watcher实例,临时挂载属性
        Dep.target = this

        //这时候通过reduce取vm中的值,就会被数据劫持,到get()中执行
        //目的也不是取值,所以不需要拿到返回值
        key.split('.').reduce((preObj, k) => { return preObj[k] }, vm)

        //执行完毕将target置空,避免内存占用
        Dep.target = null
    }
    update() {
        //取到最新的值,拿给value
        const value = this.key.split('.').reduce((preObj, k) => preObj[k], this.vm)

        //将value传给cb,更新自己
        this.cb(value)
    }
}

文本框的单向数据绑定

html结构:

<body>
    <div id="app">
        <h1>姓名是{{name}}</h1>
        <h1>年龄是{{age}}</h1>
        <h1>attr的a1值是{{attr.a1}}</h1>
        <div>name的值:<input type="text" v-model="name" /></div>
        <div>age的值:<input type="text" v-model="age" /></div>
    </div>
    <script src="./vue.js"></script>
    <script>
        let vue = new Vue({
            el: '#app',
            data: {
                name: 'zs',
                age: 18,
                attr: {
                    a1: 'a1',
                    a2: 'a2'
                }
            }
        })
        console.log(vue)
    </script>
</body>

在replace中,需要对v-model的文本框进行插值,原理和上面一样,通过reduce等方法取到值,然后返回

//定义模板编译函数
function replace(node) {
    //定义插值表达式的正则,匹配{{name}}这种
    const regMustache = /\{\{\s*(\S+)\s*\}\}/

    //只替换文本子节点,否则递归,找到文本子节点,对其替换

    //根据nodeType判断是文本子节点
    if (node.nodeType === 3) {
        const text = node.textContent //拿到文本内容
        const execResult = regMustache.exec(text) //匹配正则文本,1号元素则是需要的属性文本


        if (execResult) {
            //首先分割,然后再用reduce拿到其属性的值,将其返回,值是vm中的$data
            const value = execResult[1].split('.').reduce((preObj, k) => { return preObj[k] }, vm)

            //对当前节点的文本内容进行替换,替换为value
            node.textContent = text.replace(regMustache, value)

            //创建Watcher实例,并且回调中添加一个newVaule为新的值
            new Watcher(vm, execResult[1], (newValue) => {
                node.textContent = text.replace(regMustache, newValue)
            })
        }
        return
    }
    //判断当前的node节点是否为input输入框
    if (node.nodeType === 1 && node.tagName.toUpperCase() === 'INPUT') {
        //获取标签的属性,是一个伪数组,转化成为真数组
        const attrs = Array.from(node.attributes)

        //寻找v-model属性
        const findResult = attrs.find((x) => x.name === 'v-model')
        if (findResult) {
            //取到找到结果的值
            const expStr = findResult.value
            const value = expStr.split('.').reduce((preObj, k) => preObj[k], vm)
            node.value = value
            new Watcher(vm, expStr, (newValue) => {
                node.value = newValue
            })
        }
    }

    //不是纯文本子节点,则需要去递归其子节点
    node.childNodes.forEach((node) => {
        replace(node)
    })
}

单向数据绑定完整代码

class Vue {
    constructor(option) {
        this.$data = option.data

        //调用数据劫持的方法,对$data上的属性都进行数据劫持
        observe(this.$data)

        //数据代理,可以通过vue.直接访问到vue.$data.的属性
        Object.keys(this.$data).forEach(key => {
            Object.defineProperty(this, key, {
                enumerable: true,
                configurable: true,
                get() {
                    return this.$data[key]
                },
                set(newValue) {
                    this.$data[key] = newValue
                }
            })
        })

        //进行模板编译
        compile(option.el, this)
    }
}

function observe(obj) {
    //如果传过来的不是一个对象,而是一些具体的值比如name,age,则不需要递归
    //这里没有考虑到数组的情况
    if (!obj || typeof obj !== 'object') return
    const dep = new Dep()

    //如果是对象,就对对象的每个属性添加getter和setter
    //拿到当前对象上的每一个属性,形成一个数组
    // console.log(Object.keys(obj)) //['name','age','attr']
    Object.keys(obj).forEach(key => {
        let value = obj[key] //将该属性的值存起来
        observe(value) //对子属性递归,添加getter和setter
        Object.defineProperty(obj, key, {
            enumerable: true,
            configurable: true,
            get() {
                //只要执行到了这一行,那么new的Watcher实例
                //就被放到了dep.subs这个数组中了
                Dep.target && dep.addSub(Dep.target)

                return value //将改属性的值返回出去
            },
            set(newValue) {
                value = newValue
                observe(value) //重新赋值属性,也需要递归添加gettet和setter
                dep.notify()
            }
        })
    })
}

function compile(el, vm) {
    //获取el对应的dom元素
    vm.$el = document.querySelector(el)

    //创建文档碎片,在内存中操作dom,绘制好之后再返回给页面
    //避免页面检测到变化不断重绘重排浪费性能
    const fragment = document.createDocumentFragment()

    //当存在子节点时,每循环一次将子节点移入文档碎片中(此时元素会从页面中消失)
    while ((childNode = vm.$el.firstChild)) {
        fragment.appendChild(childNode)
    }

    //进行模板编译
    replace(fragment)

    //处理完所有的区域后再把子节点还回去
    vm.$el.appendChild(fragment)


    //定义模板编译函数
    function replace(node) {
        //定义插值表达式的正则,匹配{{name}}这种
        const regMustache = /\{\{\s*(\S+)\s*\}\}/

        //只替换文本子节点,否则递归,找到文本子节点,对其替换

        //根据nodeType判断是文本子节点
        if (node.nodeType === 3) {
            const text = node.textContent //拿到文本内容
            const execResult = regMustache.exec(text) //匹配正则文本,1号元素则是需要的属性文本


            if (execResult) {
                //首先分割,然后再用reduce拿到其属性的值,将其返回,值是vm中的$data
                const value = execResult[1].split('.').reduce((preObj, k) => { return preObj[k] }, vm)

                //对当前节点的文本内容进行替换,替换为value
                node.textContent = text.replace(regMustache, value)

                //创建Watcher实例,并且回调中添加一个newVaule为新的值
                new Watcher(vm, execResult[1], (newValue) => {
                    node.textContent = text.replace(regMustache, newValue)
                })
            }
            return
        }
        //判断当前的node节点是否为input输入框
        if (node.nodeType === 1 && node.tagName.toUpperCase() === 'INPUT') {
            //获取标签的属性,是一个伪数组,转化成为真数组
            const attrs = Array.from(node.attributes)

            //寻找v-model属性
            const findResult = attrs.find((x) => x.name === 'v-model')
            if (findResult) {
                //取到找到结果的值
                const expStr = findResult.value
                const value = expStr.split('.').reduce((preObj, k) => preObj[k], vm)
                node.value = value
                new Watcher(vm, expStr, (newValue) => {
                    node.value = newValue
                })
            }
        }

        //不是纯文本子节点,则需要去递归其子节点
        node.childNodes.forEach((node) => {
            replace(node)
        })
    }
}

class Dep {
    constructor() {
        this.subs = []
    }
    addSub(watcher) {
        this.subs.push(watcher)
    }
    notify() {
        this.subs.forEach(watcher => {
            watcher.update()
        })
    }
}

class Watcher {
    //cb中记录着如何更新自己
    //vm中保存这最新数据
    //vm身上的众多数据中哪一个是这个watcher的属性,用key
    constructor(vm, key, cb) {
        this.vm = vm
        this.key = key
        this.cb = cb

        //关键点
        // 首先给Dep添加了Target这个属性,其指向了当前的watcher实例
        Dep.target = this

        //这时候通过reduce取vm中的值,就会被数据劫持,到get()中执行
        key.split('.').reduce((preObj, k) => { return preObj[k] }, vm)

        //执行完毕将target置空,避免内存占用
        Dep.target = null
    }
    update() {
        //取到最新的值,拿给value
        const value = this.key.split('.').reduce((preObj, k) => preObj[k], this.vm)

        //将value传给cb,更新自己
        this.cb(value)
    }
}

实现数据的双向绑定

通过监听文本框的输入事件,来拿到文本框最新的值,然后把值传给Vue实例,更新$data值,即可以实现双向绑定
,在拿到文本框之后,添加一个监听事件,完整代码,这时候就很好理解了

replace函数

function replace(node) {
    //定义插值表达式的正则,匹配{{name}}这种
    const regMustache = /\{\{\s*(\S+)\s*\}\}/

    //只替换文本子节点,否则递归,找到文本子节点,对其替换

    //根据nodeType判断是文本子节点
    if (node.nodeType === 3) {
        const text = node.textContent //拿到文本内容
        const execResult = regMustache.exec(text) //匹配正则文本,1号元素则是需要的属性文本


        if (execResult) {
            //首先分割,然后再用reduce拿到其属性的值,将其返回,值是vm中的$data
            const value = execResult[1].split('.').reduce((preObj, k) => { return preObj[k] }, vm)

            //对当前节点的文本内容进行替换,替换为value
            node.textContent = text.replace(regMustache, value)

            //创建Watcher实例,并且回调中添加一个newVaule为新的值
            new Watcher(vm, execResult[1], (newValue) => {
                node.textContent = text.replace(regMustache, newValue)
            })
        }
        return
    }
    //判断当前的node节点是否为input输入框
    if (node.nodeType === 1 && node.tagName.toUpperCase() === 'INPUT') {
        //获取标签的属性,是一个伪数组,转化成为真数组
        const attrs = Array.from(node.attributes)

        //寻找v-model属性
        const findResult = attrs.find((x) => x.name === 'v-model')
        if (findResult) {
            //取到找到结果的值
            const expStr = findResult.value
            const value = expStr.split('.').reduce((preObj, k) => preObj[k], vm)
            node.value = value
            new Watcher(vm, expStr, (newValue) => {
                node.value = newValue
            })

            node.addEventListener('input', (e) => {
                const keyArr = expStr.split('.')

                //拿到最后一项的值,对其更新
                const obj = keyArr.slice(0, keyArr.length - 1).reduce((preObj, k) => preObj[k], vm)
                obj[keyArr[keyArr.length - 1]] = e.target.value
            })
        }
    }

完整代码

html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <div id="app">
        <h1>姓名是{{name}}</h1>
        <h1>年龄是{{age}}</h1>
        <h1>attr的a1值是{{attr.a1}}</h1>
        <div>name的值:<input type="text" v-model="name" /></div>
        <div>age的值:<input type="text" v-model="age" /></div>
    </div>
    <script src="./vue.js"></script>
    <script>
        let vue = new Vue({
            el: '#app',
            data: {
                name: 'zs',
                age: 18,
                attr: {
                    a1: 'a1',
                    a2: 'a2'
                }
            }
        })
        console.log(vue)
    </script>
</body>

</html>

vue.js

class Vue {
    constructor(option) {
        this.$data = option.data

        //调用数据劫持的方法,对$data上的属性都进行数据劫持
        observe(this.$data)

        //数据代理,可以通过vue.直接访问到vue.$data.的属性
        Object.keys(this.$data).forEach(key => {
            Object.defineProperty(this, key, {
                enumerable: true,
                configurable: true,
                get() {
                    return this.$data[key]
                },
                set(newValue) {
                    this.$data[key] = newValue
                }
            })
        })

        //进行模板编译
        compile(option.el, this)
    }
}

function observe(obj) {
    //如果传过来的不是一个对象,而是一些具体的值比如name,age,则不需要递归
    //这里没有考虑到数组的情况
    if (!obj || typeof obj !== 'object') return
    const dep = new Dep()

    //如果是对象,就对对象的每个属性添加getter和setter
    //拿到当前对象上的每一个属性,形成一个数组
    // console.log(Object.keys(obj)) //['name','age','attr']
    Object.keys(obj).forEach(key => {
        let value = obj[key] //将该属性的值存起来
        observe(value) //对子属性递归,添加getter和setter
        Object.defineProperty(obj, key, {
            enumerable: true,
            configurable: true,
            get() {
                //只要执行到了这一行,那么new的Watcher实例
                //就被放到了dep.subs这个数组中了
                Dep.target && dep.addSub(Dep.target)

                return value //将改属性的值返回出去
            },
            set(newValue) {
                value = newValue
                observe(value) //重新赋值属性,也需要递归添加gettet和setter
                dep.notify()
            }
        })
    })
}

function compile(el, vm) {
    //获取el对应的dom元素
    vm.$el = document.querySelector(el)

    //创建文档碎片,在内存中操作dom,绘制好之后再返回给页面
    //避免页面检测到变化不断重绘重排浪费性能
    const fragment = document.createDocumentFragment()

    //当存在子节点时,每循环一次将子节点移入文档碎片中(此时元素会从页面中消失)
    while ((childNode = vm.$el.firstChild)) {
        fragment.appendChild(childNode)
    }

    //进行模板编译
    replace(fragment)

    //处理完所有的区域后再把子节点还回去
    vm.$el.appendChild(fragment)


    //定义模板编译函数
    function replace(node) {
        //定义插值表达式的正则,匹配{{name}}这种
        const regMustache = /\{\{\s*(\S+)\s*\}\}/

        //只替换文本子节点,否则递归,找到文本子节点,对其替换

        //根据nodeType判断是文本子节点
        if (node.nodeType === 3) {
            const text = node.textContent //拿到文本内容
            const execResult = regMustache.exec(text) //匹配正则文本,1号元素则是需要的属性文本


            if (execResult) {
                //首先分割,然后再用reduce拿到其属性的值,将其返回,值是vm中的$data
                const value = execResult[1].split('.').reduce((preObj, k) => { return preObj[k] }, vm)

                //对当前节点的文本内容进行替换,替换为value
                node.textContent = text.replace(regMustache, value)

                //创建Watcher实例,并且回调中添加一个newVaule为新的值
                new Watcher(vm, execResult[1], (newValue) => {
                    node.textContent = text.replace(regMustache, newValue)
                })
            }
            return
        }
        //判断当前的node节点是否为input输入框
        if (node.nodeType === 1 && node.tagName.toUpperCase() === 'INPUT') {
            //获取标签的属性,是一个伪数组,转化成为真数组
            const attrs = Array.from(node.attributes)

            //寻找v-model属性
            const findResult = attrs.find((x) => x.name === 'v-model')
            if (findResult) {
                //取到找到结果的值
                const expStr = findResult.value
                const value = expStr.split('.').reduce((preObj, k) => preObj[k], vm)
                node.value = value
                new Watcher(vm, expStr, (newValue) => {
                    node.value = newValue
                })

                node.addEventListener('input', (e) => {
                    const keyArr = expStr.split('.')

                    //拿到最后一项的值,对其更新
                    const obj = keyArr.slice(0, keyArr.length - 1).reduce((preObj, k) => preObj[k], vm)
                    obj[keyArr[keyArr.length - 1]] = e.target.value
                })
            }

        }

        //不是纯文本子节点,则需要去递归其子节点
        node.childNodes.forEach((node) => {
            replace(node)
        })
    }
}

class Dep {
    constructor() {
        this.subs = []
    }
    addSub(watcher) {
        this.subs.push(watcher)
    }
    notify() {
        this.subs.forEach(watcher => {
            watcher.update()
        })
    }
}

class Watcher {
    //cb中记录着如何更新自己
    //vm中保存这最新数据
    //vm身上的众多数据中哪一个是这个watcher的属性,用key
    constructor(vm, key, cb) {
        this.vm = vm
        this.key = key
        this.cb = cb

        //关键点
        // 首先给Dep添加了Target这个属性,其指向了当前的watcher实例
        Dep.target = this

        //这时候通过reduce取vm中的值,就会被数据劫持,到get()中执行
        key.split('.').reduce((preObj, k) => { return preObj[k] }, vm)

        //执行完毕将target置空,避免内存占用
        Dep.target = null
    }
    update() {
        //取到最新的值,拿给value
        const value = this.key.split('.').reduce((preObj, k) => preObj[k], this.vm)

        //将value传给cb,更新自己
        this.cb(value)
    }
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值