Vue双向绑定原理实现

compile.js

// 编译
class Compile{
     
    constructor(el,vm){
     
        this.$el = el
        this.$vm = vm

        if(this.$el){
     
            // 1.获取到app下面所有的节点
            this.$Fragment = this.getNodeFragment(this.$el)
            // 2.进行编译
            this.compile(this.$Fragment)
            // 3.将编译好的节点插入app
            this.$el.appendChild(this.$Fragment)
        }
    }
    getNodeFragment(root){
     
        // 创建文档碎片
        var frag = document.createDocumentFragment()
        var child
        // firstChild返回所有子节点
        while(child = root.firstChild){
     
            // 将节点保存在了js的内存当中 页面上就不会有这个节点了
            frag.appendChild(child)
        }

        return frag
    }
    compile(fragment){
     
        var childNodes = fragment.childNodes
        // 遍历所有的子节点
        Array.from(childNodes).forEach((node)=>{
     
            // 判断文本节点
            if(this.isText(node)){
     
                // 文本节点的编译
                this.compileText(node)            
            }
            // 判断元素节点
            if(this.isElement(node)){
     
                var attrs = node.attributes

                Array.from(attrs).forEach((attr)=>{
     
                    // v-text sex
                    // @click handle
                    var key = attr.name
                    var value = attr.value

                    // 判断是否是指令
                    if(this.isDirective(key)){
     
                        // text
                        // 获取指令名称
                        var dir = key.substr(2)
                        // 调用指令对应的函数
                        this[dir+"update"] && this[dir+"update"](node,this.$vm[value])
                    }
                    // 判断是否是事件
                    if(this.isEvent(key)){
     
                        var dir = key.substr(1)
                        this.handleEvent(node,this.$vm,value,dir)
                    }
                })
            }

            // 如果子节点下面还有子节点那么就进行递归
            if(node.childNodes && node.childNodes.length>0){
     
                this.compile(node)
            }
        })
    }
    isText(node){
     
        // 判断文本节点 并且文本节点中必须要有{
     {内容}}
        return node.nodeType === 3 && /\{\{(.+)\}\}/.test(node.textContent)
    }
    isElement(node){
     
        return node.nodeType === 1
    }
    compileText(node){
     
        // 分别代表元素 vue的实例 {
     {属性}} 标识
        this.update(node,this.$vm,RegExp.$1,'text')
    }
    // 更新
    update(node,vm,exp,dir){
     
        var updateFn = this[dir+"update"]
        updateFn && updateFn(node,vm[exp])

        new Watcher(node,vm,exp,(value)=>{
       
            updateFn && updateFn(node,vm[exp])
        })
    }
    textupdate(node,value){
     
        node.textContent = value
    }
    // 判断指令
    isDirective(attr){
     
        return attr.indexOf("v-") === 0
    }
    // 判断事件
    isEvent(attr){
     
        return attr.indexOf("@") === 0
    }
    // 事件处理
    handleEvent(node,vm,callback,type){
     
        // 判断methods是否存在以及callback函数是否在methods中 如果存在则进行绑定
        var fn = vm.$options.methods && vm.$options.methods[callback]
        node.addEventListener(type,fn.bind(vm))
    }
}

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <div id="app">
        <div>{
     {
     username}}</div>
        <div>{
     {
     age}}</div>
        <div v-text="sex"></div>
        <button @click="handle">点击</button>
    </div>
</body>
</html>
<script src="./index.js"></script>
<script src="./compile.js"></script>
<script>
    var vm = new LiuyangVue({
     
        el:"#app",//挂载点
        data:{
     
            username:"liuyang",
            age:18,
            sex:"女"
        },
        methods:{
     
            handle(){
     
                this.username = "哈哈"
            }
        }
    })

    console.log(vm);
    
</script>

index.js

class LiuyangVue{
     
    // 配置项
    constructor(options){
     
        this.$options = options
        // 数据 
        // 给data上所有的属性进行遍历 给每个属性加上getter和setter方法
        this.$data = options.data
        // 挂载点
        this.$el = document.querySelector(options.el)
        // 数据劫持 作用:给data上的每个属性添加getter和setter方法
        this.observer(this.$data)

        // 进行编译 vue初始化的时候
        new Compile(this.$el,this)
    }

    observer(data){
     
        // 判断传入的data是不是一个对象
        if(!data || typeof data !== "object")
            return
        // 获取到data身上所有的key值进行遍历
        Object.keys(data).forEach(key=>{
     
            // 给data身上所有的属性添上getter和setter方法
            this.defineReactive(data,key,data[key])
            // 对象 key 配置项
            // Object.defineProperty("对象","key",{})
            // 将data身上所有的属性复制一份到vm的实例身上
            this.proxyData(key)
        })
    }
    proxyData(key){
     
        Object.defineProperty(this,key,{
     
            get(){
     
                return this.$data[key]
            },
            set(newVal){
     
                this.$data[key] = newVal
            }
        })
    }
    defineReactive(data,key,val){
     
        // 递归(对子属性对象的属性进行递归) 检测data属性的值是否还是一个对象 如果是则再进行遍历
        this.observer(val)

        var dep = new Dep()
        // 添加getter和setter方法
        Object.defineProperty(data,key,{
     
            get(){
     
                // 依赖收集
                Dep.target && dep.addDep(Dep.target)

                // 访问
                return val
            },
            set(newVal){
     
                // 设置
                if(newVal == val)
                    return

                val = newVal

                // 当设置的时候我们只要做一次通知更新即可
                dep.notify()
            }
        })
    }
}

class Dep{
     
    constructor(){
     
        // 存储所有的依赖
        this.deps=[]
    }
    addDep(dep){
     
        this.deps.push(dep)
    }
    notify(){
     
        this.deps.forEach((dep)=>{
     
            dep.update()
        })
    }
}

class Watcher{
     
    constructor(node,vm,exp,cb){
     
        this.$vm = vm
        this.$exp = exp
        this.cb = cb

        Dep.target = this
        this.$vm[this.$exp]//这一步是在做getter的触发 vm.username 做访问
        Dep.target = null
    }
    update(){
     
        this.cb.call(this.$vm,this.$vm[this.$exp])
    }
}

你可能感兴趣的:(Vue,前端,vue.js)