双向绑定原理:简单实现一个自己的VUE

291 阅读4分钟

在说vue双向绑定原理前,先了解下要用到那些知识点:文档碎片DocumentFragmentObject.defineProperty。首先简单介绍下这两个知识点。

DocumentFragment

DocumentFragment,文档片段接口,一个没有父对象的最小文档对象。它被作为一个轻量版的 Document 使用,就像标准的document一样,存储由节点(nodes)组成的文档结构。与document相比,最大的区别是DocumentFragment 不是真实 DOM 树的一部分,它的变化不会触发 DOM 树的重新渲染,且不会导致性能等问题。

<body><ul></ul>
<script>
    let a = document.querySelector('ul')
    let fragMent = document.createDocumentFragment()
    let arr = [1,2,3]    arr.forEach((item)=>{
        let li = document.createElement('li')
        li.innerText = item
        fragMent.appendChild(li)
    })
    a.appendChild(fragMent)</script></body>

页面效果

Object.defineProperty

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

示例1:给obj添加属性

let obj = {}
// 给obj添加一个叫‘xyz’的name属性
Object.defineProperty(obj,'name',{
    value:'xyz'
})
console.log(obj);

示例2:

let obj = {}
Object.defineProperty(obj,'name',{
    get:()=>{
        console.log('获取到了');
        return obj[name]
    },
    set:(newValue)=>{
        console.log('修改了');
        obj[name] = newValue
    }})
obj.name = 'zxy'
console.log(obj.name);

先了解这两个我们在继续接下来的

Observe

定义一个类给对象的每个属性都添加上definProperty

class Observe{
    // 把要观察的对象传进来
    constructor(obj) {
        this.query(obj)
    }
    query(data){
        // 看有没有值,并且是否是个对象
        if (data && typeof data === 'object'){
            //遍历这个对象
            for (let key in data){
                this.addQuery(data,key,data[key])
            }
        }
    }
    // 定义这个方法专门为对象的每个属性绑定defineProperty
    addQuery(data,key,value){
        // 是否属性的值为对象
        this.query(value)
        // 给对象的属性绑定defineProperty
        Object.defineProperty(data,key,{
            // get是获取属性值
            get() {
                console.log('获取到了');
                return value
            },
            // set是修改属性
            set:(v) => {
                // 如果新的属性值不等于旧值
                if (value !== v){
                    // 看修改的值是否为对象
                    this.query(v)
                    value = v
                }
            }
        })
    }
}
let obj = {
    name: 'xyz'
}
new Observe(obj)
console.log(obj.name);

接下来我们就写出一个自己的vue

定义自己的ZUE

HTML结构

<body>
<div id="app">
    <input type="text" v-model = 'name'>
    <p>{{name}}</p>
</div>
<script>
    let zue = new Zue({
        el:"#app",
        data:{
            name: 'xyz'
        }
    })
</script>
</body>

通过new来创建vue实例,说明vue也是一个类

class Zue{
    constructor(vm) {
        // 因为vue创建的时候指定的区域可以是一个ID名称,也可以是一个DOM元素
        if (this.isElement(vm.el)){
            this.$el = vm.el
        }else {
            this.$el = document.querySelector(vm.el)
        }
        this.$data = vm.data
        //在vue中我们可以通过this.的方法获取data中的数据。
        this.proxyData()
        // 先判断渲染区域有没有拿到
        if (this.$el){
            // 给所有的数据添加监听,用到了上面的Observe
            new Observe(this.$data)
        }
    }
    //我们将data中的数据绑定到this上
    proxyData(){
        for (let key in this.$data){
            Object.defineProperty(this,key,{
                get: () =>{
                    return this.$data[key]
                }
            })
        }
    }
    //判断是否为一个元素节点
    isElement(node){
        return node.nodeType === 1
    }
}

到这一步我们就可以通过elel和data来拿到元素和data中的数据了

Render

//专门负责渲染
class Render{
    constructor(vm) {
        this.vm = vm
        //1、将网页中的元素放到内存中渲染
        let fragment = this.nodeFragment(this.vm.$el)
        //2、利用指定的数据替换元素中的
        // console.log(fragment);
        this.forFragment(fragment)
        //3、将编译好的元素重新渲染到页面上
        this.vm.$el.appendChild(fragment)
    }
    nodeFragment(app){
        //创建一个文档碎片把元素装进去
        let newNode = document.createDocumentFragment()
        let node  = app.firstChild
        while (node){
            // 只要把界面元素放到文档碎片中,这个元素就会从界面上消失,放到内存中
            newNode.appendChild(node)
            node  = app.firstChild
        }
        // 将元素全部放完后返回文档碎片对象
        return newNode
    }
    forFragment(nodes){
        // 利用文档碎片的方法childNodes拿出所有的元素,但是这是个伪数组。
        // 利用扩展运算符转换一下
        let nodeList = [...nodes.childNodes]
        nodeList.forEach(node => {
            // 判断是否是一个元素
            if (this.vm.isElement(node)){
                // 处理元素的方法
                this.handleElement(node)
                // 可能包含子元素,要递归调用遍历子元素
                this.forFragment(node)
            }else {
                // 处理文本的方法
                this.handleText(node)
            }
        })
    }
    // 处理元素的方法
    handleElement(node){
        let allAttr = [...node.attributes]
        allAttr.forEach(attr => {
            //结构赋值
            let {name,value} = attr
            // console.log(name, value,'----');
            // 判断是否为v-开头
            if (name.startsWith('v-')){
                // 这里进一步分割是看如果有v-on:click的情况
                let [a,b] = name.split(':')
                // console.log(a, b);
                let [,command] = a.split('-')
                ZueMethod[command](node,value,this.vm,b)
            }
        })
    }
    // 处理文本的方法
    handleText(node){
        let nodeText = node.textContent
        // console.log(nodeText);
        // 利用正则匹配到{{}}
        let res = /\{\{.+?\}\}/gi
        if (res.test(nodeText)){
            //ZueMethod专门处理这些指令的。等会下面实现
            ZueMethod['textMethod'](node,nodeText,this.vm)
        }
    }
}

在实现ZueMethod对象之前,先了解下数据驱动视图。怎么实现对象(model)和视图(view)的自动更新。

简单说下流程:

  1. 我们定义的Observe进行数据劫持

  2. 然后在需要订阅的地方(如:模版编译),添加观察者(Watcher)

  3. 在Observe的get方法中添加订阅

  4. 在Observe的set方法中发布订阅

Watcher观察者

//观察者class Watcher{
    constructor(vm,value,fn) {
        // vm就是当前实例,value就是指令赋的值,fn回调函数
        this.vm = vm
        this.value = value
        this.fn = fn
        this.oldValue = this.getOldValue()
    }
    // 获取旧的值保存起来
    getOldValue(){
        // 实例化后当前this指向当前实例
        Dep.target = this
        let oldValue = ZueMethod.getVlaue(this.value,this.vm)
        Dep.target = null
        return oldValue
    }
    //获取新的值进行比较
    update(){
        let newValue = ZueMethod.getVlaue(this.value,this.vm)
        if (newValue !== this.oldValue){
            this.fn(newValue,this.oldValue)
        }
    }
}

Dep订阅

//订阅
class Dep{
    constructor() {
        this.sub = []
    }
    addSub(watcher){
        this.sub.push(watcher)
    }
    notify(){
        this.sub.forEach(watch => watch.update())
    }
}

之前说的ZueMethod就是编译模板的地方,也就是添加观察者的地方

添加观察者

let ZueMethod = {
    //这里是如果对象中的属性值是对象进行处理,比如a.b.c
    getVlaue(value,vm){
        return value.split('.').reduce((a,b) => {
            return a[b.trim()]
        },vm.$data)
    },
    // 对模板中的数据进行处理
    getText(value,vm){
        let res = /\{\{(.+?)\}\}/gi
        // 这里是如果插值语法进行处理
        let val = value.replace(res,(...args) =>{
            console.log(args);
            return this.getVlaue(args[1],vm)
        })
        console.log(val);
        return val
    },
    // 获取新的值
    setValue(vm,value,newValue){
        value.split('.').reduce((a,b,index,arr) => {
            if (index === arr.length-1){
                a[b.trim()] = newValue
            }
            return a[b.trim()]
        },vm.$data)
    },
    // v-model
    model:function (node,value,vm){
        // 添加监听
        new Watcher(vm,value,(newValue,oldValue) => {
            // console.log('执行了',newValue);
            node.value = newValue
        })
        let newValue = this.getVlaue(value,vm)
        node.value = newValue
        // 获取最新的数据
        node.addEventListener('input', (e) =>{
             let newValue = e.target.value
            this.setValue(vm,value,newValue)
        })
    },
    //v-html
    html:function (node,value,vm){
        new Watcher(vm,value,(newValue,oldValue) => {
            node.innerHTML = newValue
        })
        let val = this.getVlaue(value,vm)
        node.innerHTML = val
    },
    // v-text
    text:function (node,value,vm){
        new Watcher(vm,value,(newValue,oldValue) => {
            node.innerText = newValue
        })
        let val = this.getVlaue(value,vm)
        node.innerText = val
    },
    //v-on
    on:function (node,value,vm,b){
        node.addEventListener(b, (e) =>{
            vm.$methods[value].call(vm,e)
        })
    },
    // 处理模板中数据的方法
    textMethod:function (node,value,vm){
        // console.log(value);
        let res = /\{\{(.+?)\}\}/gi
        let val = value.replace(res,(...args) => {
            // console.log(args);
            new Watcher(vm,args[1],(newValue,oldValue)=>{
                node.textContent = this.getText(value,vm)
            })
            return this.getVlaue(args[1],vm)
        })
        node.textContent = val
    }
}

哈哈,代码比较多,大家看一个指令就行了。其他的大差不差

这时候我们的vue就基本实现完成了,但是还有个最重要的!就是我们添加订阅和发布订阅了!

在definEProperty中,我们获取数据会触发get方法,修改数据会触发set方法。所以我们可以在get中添加订阅,set中发布订阅

小小的改动下Observe中defineProperty的get和set方法

get() {
    // 添加订阅。我们在Watcher给Dep绑定了一个target属性
    // 在编译模板时,Watcher进行实例化,添加了观察者。那么this就指向当前实例
    Dep.target && dep.addSub(Dep.target)
    return value},
set:(v) => {
    if (value !== v){
        // console.log(v);
        // 修改的值是否为对象
        this.query(v)
        value = v
        // 数据改变时发布订阅
        dep.notify()
    }
}

完整版

将以上组合在一起。差不多就是个简化版的vue啦

具体代码如下,有点多==

let ZueMethod = {
    getVlaue(value,vm){
        return value.split('.').reduce((a,b) => {
            return a[b.trim()]
        },vm.$data)
    },
    getText(value,vm){
        let res = /\{\{(.+?)\}\}/gi
        let val = value.replace(res,(...args) =>{
            return this.getVlaue(args[1],vm)
        })
        return val
    },
    setValue(vm,value,newValue){
        value.split('.').reduce((a,b,index,arr) => {
            if (index === arr.length-1){
                a[b.trim()] = newValue
            }
            return a[b.trim()]
        },vm.$data)
    },
    model:function (node,value,vm){
        new Watcher(vm,value,(newValue,oldValue) => {
            node.value = newValue
        })
        let newValue = this.getVlaue(value,vm)
        node.value = newValue
        node.addEventListener('input', (e) =>{
            let newValue = e.target.value
            this.setValue(vm,value,newValue)
        })
    },
    html:function (node,value,vm){
        new Watcher(vm,value,(newValue,oldValue) => {
            node.innerHTML = newValue
        })
        let val = this.getVlaue(value,vm)
        node.innerHTML = val
    },
    text:function (node,value,vm){
        new Watcher(vm,value,(newValue,oldValue) => {
            node.innerText = newValue
        })
        let val = this.getVlaue(value,vm)
        node.innerText = val
    },
    on:function (node,value,vm,b){
        node.addEventListener(b, (e) =>{
            vm.$methods[value].call(vm,e)
        })
    },
    textMethod:function (node,value,vm){
        let res = /\{\{(.+?)\}\}/gi
        let val = value.replace(res,(...args) => {
            new Watcher(vm,args[1],(newValue,oldValue)=>{
                node.textContent = this.getText(value,vm)
            })
            return this.getVlaue(args[1],vm)
        })
        node.textContent = val
    }
}

class Zue{
    constructor(vm) {
        if (this.isElement(vm.el)){
            this.$el = vm.el
        }else {
            this.$el = document.querySelector(vm.el)
        }
        this.$data = vm.data
        this.$methods = vm.methods
        this.proxyData()
        if (this.$el){
            new Observe(this.$data)
            new Render(this)
        }
    }
    proxyData(){
        for (let key in this.$data){
            Object.defineProperty(this,key,{
                get: () =>{
                    return this.$data[key]
                }
            })
        }
   }
    isElement(node){
        return node.nodeType === 1
    }
}

//专门负责渲染
class Render{
    constructor(vm) {
        this.vm = vm
        let fragment = this.nodeFragment(this.vm.$el)
        this.forFragment(fragment)
        this.vm.$el.appendChild(fragment)
    }
    nodeFragment(app){
        let newNode = document.createDocumentFragment()
        let node  = app.firstChild
        while (node){
            newNode.appendChild(node)
            node  = app.firstChild
        }
        return newNode
    }
    forFragment(nodes){
        let nodeList = [...nodes.childNodes]
        nodeList.forEach(node => {
            if (this.vm.isElement(node)){
                this.handleElement(node)
                this.forFragment(node)
            }else {
                this.handleText(node)
            }
        })
    }
    handleElement(node){
        let allAttr = [...node.attributes]
        allAttr.forEach(attr => {
            let {name,value} = attr
            if (name.startsWith('v-')){
                let [a,b] = name.split(':')
                let [,command] = a.split('-')
                ZueMethod[command](node,value,this.vm,b)
            }
        })
    }
    handleText(node){
        let nodeText = node.textContent
        let res = /\{\{.+?\}\}/gi
        if (res.test(nodeText)){
            ZueMethod['textMethod'](node,nodeText,this.vm)
        }
    }
}

class Observe{
    constructor(obj) {
        this.query(obj)
    }
    query(data){
        if (data && typeof data === 'object'){
            for (let key in data){
                this.addQuery(data,key,data[key])
            }
        }
    }
    addQuery(data,key,value){
        this.query(value)
        let dep = new Dep()
        Object.defineProperty(data,key,{
            get() {
                Dep.target && dep.addSub(Dep.target)
                return value
            },
            set:(v) => {
                if (value !== v){
                    this.query(v)
                    value = v
                    dep.notify()
                }
            }
        })
    }
}

//发布订阅
class Dep{
    constructor() {
        this.sub = []
    }
    addSub(watcher){
        this.sub.push(watcher)
    }
    notify(){
        this.sub.forEach(watch => watch.update())
    }
}

//观察者
class Watcher{
    constructor(vm,value,fn) {
        this.vm = vm
        this.value = value
        this.fn = fn
        this.oldValue = this.getOldValue()
    }
    getOldValue(){
        Dep.target = this
        let oldValue = ZueMethod.getVlaue(this.value,this.vm)
        Dep.target = null
        return oldValue
    }
    update(){
        let newValue = ZueMethod.getVlaue(this.value,this.vm)
        if (newValue !== this.oldValue){
            this.fn(newValue,this.oldValue)
        }
    }
}

我表达的可能不是太好,不太容易理解,也可能有错误的地方我没检查出来。希望大家多多包含😊