程序员

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

2021-02-13  本文已影响0人  申_9a33

流程图

捕获.PNG

第一步实现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>

源码

上一篇下一篇

猜你喜欢

热点阅读