zoukankan      html  css  js  c++  java
  • 一个极其简易版的vue.js实现

    前言

    之前项目中一直在用vue,也边做边学摸滚打爬了近一年。对一些基础原理性的东西有过了解,但是不深入,例如面试经常问的vue的响应式原理,可能大多数人都能答出来Object.defineProperty进行数据劫持,但是深入其实现细节,还是有很多之前没考虑到的东西,例如依赖收集后如何通知订阅器,以及订阅发布模式如何实现等等。过程中读了部分源码,受益匪浅,除此之外,动手去实现它也是个很棒的学习方式,话不多说,看代码,仓库地址

    实现

    vue的更新机制我们简单概括一下就是,先对template进行解析,若检测到template中使用了data中定义的属性,则生成一个对应的watcher,通过劫持getter进行依赖(即watcher)收集,收集的内容保存在订阅器Dep,通过劫持setter做到改变属性从而通知订阅器更新,那么我们首先要做的就是对属性进行劫持。
    vue2.0中使用的是Object.defineProperty,有传言说vue 3.0将会使用Proxy来代替Object.defineProperty,其有诸多好处:

    • defineProperty不能对数组进行劫持,因此vue的文档中才会提到只有push、pop等8种方法能够检测变化,而arr[index] = newValue并不能检测变化,push等方法能检测变化也是因为开发者对Array原生方法进行hack实现的。
    • defineProperty只能改变对象的某一个属性,若需要劫持整个对象,必须遍历对象,对每个属性劫持,因此效率并不高。而Proxy更像是一个代理,它会产生一个新的对象,该对象内部的属性均以实现劫持。但要注意,某个属性若也是一个对象类型,需要对该属性也执行proxy操作才能实现劫持。

    Proxy目前来看唯一的缺点就是兼容性可能存在问题,不过无伤大雅,我们也顺应潮流,使用Proxy来实现数据劫持,代码很简单:

    /**
     * 接受一个对象,对属性进行依赖追踪
     */
    function observable(obj) {
      const dep = new Dep()
      
      const proxy = new Proxy(obj, {
        get(target, property) {
          const value = target[property]
          if (value && typeof value === 'object') { // 若属性为object,递归处理
            target[property] = observable(value)
          }
          if (Dep.target) { // Dep.target指向当前watcher
            dep.addWatcher(Dep.target)
          }
          return target[property]
        },
        set(target, property, value) {
          target[property] = value
          dep.notify() // 通知订阅器
        }
      })
      return proxy
    }
    

    注意该方法需要返回proxy实例,因为只有通过proxy实例访问属性才具有劫持效果。我们可以看到代码中有一个Dep,这个东西即是订阅器,可以理解为它维护了一个依赖(watcher)的数组,并实现了一些管理数据的方法诸如addWatcher添加依赖,以及需要提供一个notify方法来遍历所有的watcher执行其相应的更新函数,同样代码很简单:

    /**
     * 依赖收集器,存放所有的watcher,并提供发布功能(notify)
     */
    class Dep {
      constructor() {
        this.watchers = []
      }
      addWatcher(watcher) { // 添加watcher
        this.watchers.push(watcher)
      }
      notify() { // 通知方法,调用即依次遍历所有watcher执行更新
        this.watchers.forEach((watcher) => {
          watcher.update()
        })
      }
    }
    

    最后我们来看下watcher,我们知道watcher即我们所说的依赖,它是在编译template的时候,若找到data中声明的属性,即会生成一个对应的watcher实例,触发依赖收集,加入订阅器。同时还需要提供一个update函数,在触发notify的时候调用来更新视图,代码如下:

    /**
     * watcher即所谓的依赖,监听具体的某个属性
     */
    class Watcher {
      constructor(proxy, property, cb) {
        this.proxy = proxy
        this.property = property
        this.cb = cb
        this.value = this.get()
      }
      update() { // 执行更新
        const newValue = this.proxy[this.property]
        if (newValue !== this.value && this.cb) { // 对比property新旧值,决定是否更新
          this.cb(newValue)
        }
      }
      get() { // 只在初始化时调用,用于依赖收集
        Dep.target = this // 将自身指向Dep.target,执行完依赖收集再去释放
        const value = this.proxy[this.property]
        Dep.target = null
        return value
      }
    }
    

    至此,响应式原理大致已经成形,接着我们只要写一个简易的模板解析,demo就能跑起来啦。我这边的实现比较挫,仅仅是通过正则匹配来实现了一个不带diff的virture dom,纯属娱乐,重点还是在实现响应式原理上,这边贴一下代码:

    let init = false // 只在初始化时去生成watcher
    const eventMap = new Map() // 存放事件
    const root = document.getElementById('root') // 根节点
    
    /**
     * 用于将传入RayActive的vm对象进行代理,可通过this.xx访问this.data.xx
     * @param {Object} vm 
     * @param {Proxy} proxydata 经过proxy代理的vm.data对象,使this.xx操作也能触发视图更新
     */
    function vmProxy(vm, proxydata) {
      return new Proxy(vm, {
        get(target, property) {
          return target.data[property] || target.methods[property]
        },
        set(target, property, value) {
          proxydata[property] = value
        }
      })
    }
    
    /**
     * 编译vm,分别对data和render做相应处理
     * @param {Object} vm 需要被编译的vm对象
     */
    function compile(vm) {
      const proxydata = compileData(vm.data)
      compileRender(proxydata, vm.render)
      bindEvents(vm, vmProxy(vm, proxydata))
    }
    
    /**
     * 
     * @param {Object} data 需要被编译的vm中的data对象
     */
    function compileData(data) {
      return observable(data)
    }
    
    /**
     * 
     * @param {*} render 需要被编译的render字符串
     * @param {*} proxydata 经proxy转换过的data
     */
    function compileRender(proxydata, render) {
      if (render) {
        const variableRegexp = /{{(.*?)}}/g
        const variableResult = render.replace(variableRegexp, (a, b) => { // 替换变量为相应的data值
          if (!init) { // 只在初始化时去生成watcher
            new Watcher(proxydata, b, function() {
              compileRender(proxydata, render)
            })
          }
          return proxydata[b]
        })
        const eventRegexp = /(?<=<.*)@(.*)="(.*?)"(?=.*>)/
        const result = variableResult.replace(eventRegexp, (a, b, c) => { // 为绑定事件的标签添加唯一id标识
          const id = Math.random().toString(36).slice(2)
          eventMap.set(id, {
            type: b,
            method: c
          })
          return a + ` id=${id}`
        })
        init = true
        root.innerHTML = result
      }
    }
    
    /**
     * 通过root节点做事件代理,绑定模板中声明的事件
     * @param {*} vm 
     * @param {*} proxyvm 经过proxy代理的vm
     */
    function bindEvents(vm, proxyvm) {
      for (let [key, value] of eventMap) {
        root.addEventListener(value.type, (e) => {
          const method = vm.methods[value.method]
          if (method && e.target.id === key) {
            method.apply(proxyvm) // 将vm中methods方法的this指向经过proxy的vm对象
          }
        })
      }
    }
    
    /**
     * 可理解为Vue中的Vue类,使用方式为new RayActive(vm)
     */
    class RayActive {
      constructor(vm) {
        compile(vm)
      }
    }
    

    总结

    这个简易实现仅仅是帮助大家学习vue的一些原理性的东西,跟vue比其他来只是冰山一角。这个代码还有很大的优化空间,比如执行notify时这里会通知所有的watcher等等,值得有空去研究一下。同时,我们能看到订阅发布模式带来的好处。如果不引入订阅器,那我们更新dom的代码得放到setter中去,那么就耦合了数据劫持与操作dom的逻辑。引入订阅器,能让我们在proxy中仅仅做依赖收集和通知的操作,剩下的各种复杂的或是个性化的逻辑可以放到watcher中去实现,完美做到了关注点分离。

  • 相关阅读:
    Java对【JSON数据的解析】--Gson解析法
    Java对【JSON数据的解析】--官方解析法
    Java之JSON数据
    网络编程应用:基于UDP协议【实现聊天程序】--练习
    {网络编程}和{多线程}应用:基于UDP协议【实现多发送方发送数据到同一个接收者】--练习
    PHP获取页面执行时间的方法(推荐)
    Linux下查看CPU型号,内存大小,硬盘空间的命令(详解)
    Elasticsearch 全字段搜索_all,query_string查询,不进行分词
    mysql 查询某字段值全是数字
    linux服务器中Apache隐藏index.php失败
  • 原文地址:https://www.cnblogs.com/danceonbeat/p/10656837.html
Copyright © 2011-2022 走看看