实现迷你版vue2的响应式核心原理代码

vue2 的响应式核心原理代码 其实就只有几个模块

1. 代理 Object.defineProperty

2.依赖收集dep,收集所有监听页面数据的watcher实例

3. 监听页面数据实例 watcher

4.观察者 Observer ,实现数据劫持 

5.编译模块 Compiler

下面来简单实现一下各个模块代码,新建index.js文件

1.首先简单实现Vue类,和基本代理功能

export class Vue{
    constructor(options = {}){
        this.$options = options
        this.$el = typeof options.el ==='string' ? document.querySelector(options.el):options.el
        this.$data = options.data
        this.$methods = options.methods

        this.proxy(this.$data)

        //observer 拦截 this.$data
        new Observer(this.$data)

        new Compiler(this)
    }
    //代理一下,this.$data.xx=>this.xx
    proxy(data){
        Object.keys(data).forEach(key=>{
            Object.defineProperty(this,key,{
                enumerable:true,
                configurable:true,
                get(){
                    return data[key]
                },
                set(newValue){
                    console.log(newValue)
                    if(data[key] === newValue || (Number.isNaN(data[key]) && Number.isNaN(newValue)) ) return
                    data[key] = newValue
                }
            })
        })
    }

}

 2.Dep依赖收集模块,收集所有watcher实例

class Dep{
    constructor(){
        this.deps = new Set()
    }
    //手机副作用代码,count
    add(dep){
        console.log("dep")
        console.log(dep)
        if(dep && dep.update) this.deps.add(dep)
    }
    notify(){
        console.log("this.deps")
        console.log(this.deps)
        this.deps.forEach(dep=>dep.update())
    }
}

3. watcher模块,监听数据被修改后引发回调

// html -> <h1>{{count}}</h1> -> compiler 发现有 {{count}}
// -> new Watcher(vm,'count',()=>renderToView(count)) -> count getter 被触发
//-> dep.add(watcher实例) -> this.count++ -> count setter -> dep.notify 
// -> ()=>renderToView(count) -> 页面就变了

class Watcher{
    constructor(vm,key,cb){
        this.vm = vm
        this.key = key
        this.cb = cb  
        window.vm = vm
        Dep.target = this
        this.__old = vm[key] //存下了初始值,触发 getter
        Dep.target = null
    }
    update(){
        let newValue = this.vm[this.key]
        if(this.__old === newValue || (Number.isNaN(newValue) && Number.isNaN(this.__old)) ) return
        this.cb(newValue)
    }

}

4.Observer模块,数据劫持的实现 (这里先不考虑数组的劫持实现,vue2的数组劫持是重写了数组的所有方法)

class Observer{
    constructor(data){
       this.walk(data)
    }

    walk(data){
        if(!data || typeof data !== 'object') return
        Object.keys(data).forEach(key=>{
            return this.defineReactive(data,key,data[key])
        })
    }

    defineReactive(data,key,value){
        let that = this;
        this.walk(value); //因为值也可能是对象 {a:{b:0}}
        let dep = new Dep();
        Object.defineProperty(data,key,{
            configurable:true,
            enumerable:true,
            get(){
                //watcher 实例
                
                Dep.target && dep.add(Dep.target)
                return value
            },
            set(newValue){
                if(value === newValue || (Number.isNaN(data[key]) && Number.isNaN(newValue) )) return
                value = newValue
                that.walk(newValue)
                dep.notify()
            }
        })
    }



}

5.Compiler模块,编译页面代码,实现数据绑定

class Compiler{
    constructor(vm){
        this.el = vm.$el
        this.vm = vm
        this.methods = vm.$methods
        this.compile(this.el)
    }

    compile(el){
        let childNodes = el.childNodes
        //类数组
        Array.from(childNodes).forEach(node=>{
            if(this.isTextNode(node)){
                this.compileText(node)
            }
            else if(this.isElementNode(node)){
                this.compileElement(node)
            }
            
            if(node.childNodes && node.childNodes.length >0){
                this.compile(node)
            }
        })
    }
    //<input v-model="msg" />
    compileElement(node){
        if(node.attributes.length){
            Array.from(node.attributes).forEach(attr=>{
                let attrName = attr.name
                if(this.isDirective(attrName)){
                    //v-on:click v-model
                    attrName = attrName.indexOf(':') > -1 ? attrName.substr(5) : attrName.substr(2)
                    let key = attr.value
                    this.update(node, key, attrName, this.vm[key])
                }
            })
        }

    }
    update(node,key,attrName,value){
        if(attrName ==='text'){
         
            node.textContent = value
            new Watcher(this.vm,key,val=>node.textContent = val)
        }else if(attrName === 'model'){
            node.value = value
            new Watcher(this.vm,key,val=>node.value = val)
            node.addEventListener('input',()=>{
                //这里可以加防抖
                this.vm[key] = node.value
            })
        }else if(attrName === 'click'){
            node.addEventListener('click',this.methods[key].bind(this.vm))
        }else if(attrName === 'html'){
            node.innerHtml = value
            new Watcher(this.vm,key,val=>node.innerHtml = val)
        }
        // ... 
    }
    //'this is {{count}} {{msg}}'
    compileText(node){
        let reg = /\{\{(.+?)\}\}/g
        let value = node.textContent;
        node.textContent = value.replace(reg,(val,key)=>{
            key = key.trim()
            new Watcher(this.vm,key,val=>{
                let newValue = value.replace(reg,(oneValue,oneKey)=>{
                    return this.vm[oneKey.trim()]
                })
                node.textContent = newValue
            })
            return this.vm[key]
        })
    }

    isDirective(str){
        return str.startsWith('v-')
    }
    isElementNode(node){
        return node.nodeType === 1
    }
    isTextNode(node){
        return node.nodeType === 3
    }
    


}

测试一下,新建index.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>
  <script type='module'>
    import { Vue } from './index.js'
    let vm = new Vue({
      el: '#app',
      data: {
        msg: 'Hello Vue2.x',
        count: 666
      },
      methods: {
        increase() {
          this.count++
        }
      }
    })

    setTimeout(()=>{vm.msg = 'hello world'},2000)
  </script>
</head>
<body>
  <div id="app">
    <h3>{{msg}} {{ count }}</h3>
    <h3>{{ count }}</h3>
    <h1>v-text</h1>
    <div v-text="msg"></div>
    <h1>v-model</h1>
    <input type="text" v-model="msg" >
    <input type="text" v-model="count">
    <button v-on:click="increase">按钮</button>
  </div>
</body>
</html>

效果


版权声明:本文为weixin_38407447原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。