美文网首页程序员
7天深入Vue-响应式原理(三)

7天深入Vue-响应式原理(三)

作者: 申_9a33 | 来源:发表于2021-02-13 15:17 被阅读0次

    流程图

    捕获.PNG
    • Observer实现对所有数据劫持
    • Dep 负责收集每个key 的所有Watcher
    • Watcher 作为观察者,数据发生变化,执行更新
    • Compile 解析模板

    第一步实现Observer ,劫持所有数据

    //vue.js
    
    // 劫持obj属性key的get和set,响应试的具体实现
    function defineReactive(obj,key,val){
        observe(val) // 将内部数据继续响应式处理
    
        const dep = new Dep() // 负责收集当前key的所有watcher
      
        Object.defineProperty(obj, key, {
            get() {
                Dep.target && dep.addWatcher(Dep.target) // 收集target上的watcher
                return val;
            },
            set(newVal) {
                if (newVal !== val) {
                    // 如果传入newVal依然是obj,需要做响应式
                    observe(newVal);
                    val = newVal;
    
                    dep.notify() // 一旦值发生变化,通知所有watcher 更新
                }
            }
        })
    }
    
    // 将对应的值构造成一个响应式数据
    class Observe{
      constructor(value){
        if(typeof value === 'object'){
          this.walk(value)
        }
      }
    
      walk(obj){
        Object.keys(obj).forEach(key=>{
          defineReactive(obj,key,obj[key])
        })    
      }
    }
    
    // 递归将数据变成响应式
    function observe(obj){
        if (typeof obj !== 'object' || obj === null) {
            return
        }
    
        new Observe(obj)
    }
    

    第二步实现Dep ,收集对象中指定key的watcher

    // vue.js
    class Dep{
      constructor(){
         this.watchers = [];
      }
    
      addWatcher(watcher){
        this.watchers.push(watcher)
      }
    
      notify(){
        this.watchers.forEach(watcher=>watcher.update());
      }
    }
    

    第三步实现Watcher 保存对象上指定key 对象的更新函数

    // vue.js
    class Watcher{
    
      // 作为观察者,传入需要观察对象的那个属性,并且属性发生变化时需要执行的更新函数
      constructor(vm,key,updateFn){
        this.vm = vm
        this.key = key
        this.updateFn = updateFn
    
        // 创建watcher 时,触发get,让Dep收集当前Watcher
        Dep.target = this 
        this.vm[this.key]
        Dep.target = null
      }
    
      // 值发生变化,更新函数
      update(){
        this.updateFn .call(this.vm,this.vm[this.key]);
      }
    }
    

    第四步Compile 实现编译器解析vue模板

    Compile.js
    class Compile{
      constructor(el,vm){
        this.$vm = vm
        this.$el = document.querySelector(el)
    
        if(this.$el){
          // 执行编译
          this.comile(el)
        }
      }
    
      // 编译
      compile(el){
        // 遍历el树
        const childNodes = el.childeNodes;
    
        Array.from(childNodes).forEach(node=>{
          
          if(this.isElement(node)){
            // 是元素则编译元素
            this.compileElement(node)
          }else if(this.isInter(node)){
            // 是文本则编译文本
            this.compileText(node)
          }
    
          // 存在子节点,继续递归编译
          if(node.childeNodes&&node.childeNodes.length > 0){
            this.compile(node)
          }
        })
      }
    
      // 是元素
      isElement(node){
        return node.nodeType === 1  
      }
    
      // 是文本
      isInter(node){
        return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)
      }
    
      // 是指令
      isDirective(attr){
        return attr.indexOf('v-') === 0
      }
    
       // 是事件
        isEvent(dir) {
            return dir.indexOf('@') === 0
        }
    
      // 编译元素,需要解析指令和事件
      compileElement(node){
          const nodeAttrs = node.attributes // 拿到节点上的所有属性
          
          Array.from(nodeAttrs).forEach(attr=>{
            // 拿到属性名和值 v-bind="aaa"
            const attrName = attr.name // v-bind
            const exp = attr.value // aaa
    
            if(this.isDirective(attrName )){
               // 是指令
               const dir = attrName.substring(2); // bind
                // 执行指令
                this[dir] && this[dir](node, exp)
            }
    
                // 事件处理 @click
             if (this.isEvent(attrName)) {
                  const dir = attrName.substring(1) // click
                  // 事件监听
                  this.eventHandler(node, exp, dir)
              }
          })
      }
    
      // 编译文本,需要解析数据
      compileText(node){
        // 更新到dom ,并且创建一个wathcer
        this.update(node,RegExp.$1,'text')
      }
    
      // 更新数据,并创建watcher
      update(node,exp,dir){
        // 拿到指定的更新函数
        const fn = this[dir+"Updater"]    
    
        // 执行一次
        fn && fn(node,this.$vm[exp])
        
        // 新建一个watcher ,后续数据变化自动响应式处理
        new Watcher(this.$vm,exp,function(val){
          fn && fn(node,val)
        })
      }
    
    
      // v-text 指令
        text(node, exp) {
            this.update(node, exp, "text")
        }
    
      // v-html 指令
        html(node, exp) {
            this.update(node, exp, "html")
        }
    
        // v-model 指令
        model(node, exp) {
            // update 完成赋值与更新
            this.update(node, exp, "model")
    
            // 事件监听
            node.addEventListener('input', e => {
                this.$vm[exp] = e.target.value
            })
        }
    
      // 文本更新
      textUpdater(node,value){
        node.textContent = value;
      }
    
        // model 更新
        modelUpdater(node, value) {
            node.value = value
        }
    
        // html更新
        htmlUpdater(node, value) {
            node.innerHTML = value
        }
    
        // 事件处理
        eventHandler(node, exp, dir) {
            const fn = this.$vm.$options.methods && this.$vm.$options.methods[exp]
            node.addEventListener(dir, fn.bind(this.$vm))
        }
    }
    

    第五步 创建Vue类

    // vue.js
    
    // 将vm 上的属性的值代理到vm上,方便直接访问
    proxy(vm,attr){
        Object.keys(vm[attr]).forEach(key => {
            Object.defineProperty(vm, key, {
                get() {
                    return vm[attr][key]
                },
                set(newVal) {
                    vm[attr][key] = newVal;
                }
            })
        })
    }
    
    class Vue {
      constructor(options){
        this.$options = options
        this.$data = options.data;
    
        // 将数据响应化处理
        observe(this.$data)
    
        // 代理
        proxy(this, '$data')
    
        // 创建编译器
        new Compile(options.el, this)
      }
    }
    

    第六步使用自己的vue

    //index.html
    
    <!DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
    </head>
    
    <body>
    
    
        <div id="app">
            <p>{{counter}}</p>
            <p v-text="counter"></p>
            <p v-html="desc"></p>
    
            <input type="text" v-model="counter">
        </div>
    
        <script src="./compile.js"></script>
        <script src="./vue.js"></script>
    
        <script>
            const app = new Vue({
                el: '#app',
                data: {
                    counter: 1,
                    desc: "<span style='color:red'>哈哈</spam>"
                },
                methods: {
                    onClick() {
                        this.counter += 10
                    }
                },
            })
    
            setInterval(() => {
                app.counter++
            }, 1000)
        </script>
    </body>
    
    </html>
    

    源码

    相关文章

      网友评论

        本文标题:7天深入Vue-响应式原理(三)

        本文链接:https://www.haomeiwen.com/subject/ukpsxltx.html