美文网首页
Vue响应式原理浅析

Vue响应式原理浅析

作者: 哆啦_ | 来源:发表于2020-01-01 17:18 被阅读0次

    最近在学习前端框架Vue ,对其响应式原理做一些简单的分析

    大致原理:

    当把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的属性,将这些属性转为getter/setter,并针对每个key创建一个对应的Dep对象(用来管理watcher)。 然后会解析el的子元素,创建对应的watcher,这样每个节点元素都有其对应的watcher,随后的视图更新就是通过watcher来做的。
    当我们使用数据时,触发的是vmgetter方法,这时会将创建的watcher添加到dep中;在我们更改数据时,就会触发Vue中的setter,会调用depnotify方法,通知所有的watcher进行update

    大致效果:

    效果图.gif

    1. Vue的初始化

    这里做的主要事情是

    1. data中的数据代理到自身
    2. 创建一个Observer实例来将data属性转为getter/setter
    3. 然后解析el的子元素
    constructor(options) {
          this.$options = options
          this.$data = options.data
          this.$el = options.el
          // 监听setter/getter
          new Observer(this.$data)
          // 代理
          Object.keys(this.$data).forEach(key => {
              this._proxy(key)
          })
          // 解析el
          new Complier(this.$el, this)
      }
    
      // 代理data的属性到Vue上
      _proxy(key) {
          Object.defineProperty(this, key, {
              configurable:true,
              enumerable:true,
              get(){
                  return this.$data[key]
              },
              set(newValue) {
                  this.$data[key] = newValue
              },
          })
      }
    
    

    2. Observer

    Observer将Vue中data的属性转为getter/setter,依此来监测数据的变化。 data中每一个 key都有一个对应的Dep实例,Dep是依赖项,用来保存一个个watcher

    class Observer {
        constructor(data){
            this.data = data
            Object.keys(this.data).forEach(key => {
                this.defineReactive(this.data, key, this.data[key])
            })
        }
    
        // 设置响应式逻辑
        defineReactive(data, key, val){
            // 每个key都对应一个dep
            const dep = new Dep()
            // 设置setter/getter
            Object.defineProperty(data, key, {
                enumerable:true,
                configurable:true,
                get(){
                    // 添加watcher到对应的dep
                    if (Dep.target) {
                        dep.addSub(Dep.target)
                    }
                    return val
                },
                set(newValue){
                    if (newValue === val) {
                        return
                    }
                    val = newValue
                    // value发生变化 通知所有的watcher进行更新
                    dep.notify()
                }
            })
        }
    }
    
    

    3. Dep

    Dep是和data中的key一一对应的,也就是每一个key都有对应的dep,dep对外暴露了一些方法,比如添加wacther的方法,以及很重要的notify

    class Dep {
        constructor(){
            this.subs = []
        }
        // 添加watcher
        addSub(watcher){
            this.subs.push(watcher)
        }
        // 通知所有的watcher进行update
        notify(){
            this.subs.forEach(item => item.update())
        }
    }
    

    之所以每个key对应一个Dep实例,是因为data中可能有多个键值对,比如name,age等,当我们修改name的时候,肯定是只希望使用了name数据的元素进行改变,而使用age属性的不发生改变,使用dep就是为了管理每个属性对应的多个watcher

    4. Compiler

    这里是将对el的处理都放到了Compiler中,对el的子元素进行遍历,然后对子元素中使用的指令进行解析,创建对应的watcher

    // 匹配{{}}语法的正则
    const reg = /\{\{(.+)\}\}/
    
    class Compiler {
        constructor(el, vm){
            this.el = document.querySelector(el)
            this.vm = vm
    
            this.frag = this._createFragment()
            this.el.appendChild(this.frag)
        }
        _createFragment(){
            const frag = document.createDocumentFragment()
            let child
            while (child = this.el.firstChild) {
                this._compile(child)
                frag.appendChild(child)
            }
            return frag
        }
        _compile(node) {
            if (node.nodeType === Node.ELEMENT_NODE) {// 元素节点
                const attrs = node.attributes
                if (attrs.hasOwnProperty('v-model')) {
                    const attr = attrs['v-model']
                    // 取出v-model对应的变量名
                    const name = attr.nodeValue
                    // 监听input事件
                    node.addEventListener('input', e => {
                        // 将输入的内容保存 
                        // 同时会触发vm的setter 以此来通知所有的watcher进行update
                        this.vm[name] = e.target.value
                    })
                    // 创建该节点对应的watcher
                    new Watcher(node, name, this.vm)
                }
            } else if (node.nodeType === Node.TEXT_NODE) {// 文本节点
                const nodeValue = node.nodeValue // {{message}}
                if (reg.test(nodeValue)) {
                    // 取出{{}}中的名称
                    const name = RegExp.$1.trim()
                    // 创建该节点对应的watcher
                    new Watcher(node, name, this.vm)
                }
            }
        }
    }
    

    5. Watcher

    watcher的作用是用来通知元素进行更新,跟页面中的元素是一一对应的,这样当data中的数据发生改变时,会触发setter方法,这时dep就会通知其所有的watcher进行update

    class Watcher {
        constructor(node, name, vm){
            this.node = node
            this.name = name
            this.vm = vm
    
            // 将自身保存 以便在调用data的getter时能添加到dep中
            Dep.target = this
            this.update()
            // 清空target 防止重复添加
            Dep.target = null
        }
    
        // 更新视图
        update(){
            if (this.node.nodeType === Node.ELEMENT_NODE) {// 元素节点
                this.node.value = this.vm[this.name]
            } else if (this.node.nodeType === Node.TEXT_NODE) {// 文本节点
                this.node.nodeValue = this.vm[this.name]
            }
        }
    }
    

    6. 使用的方法

    1. Object.defineProperty
      该方法就是将data的属性转为getter/setter的,也是整个响应式的关键所在。 Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。查看具体用法

    2. document.createDocumentFragment()
      在遍历el的时候我们使用了文档片段,使用文档片段的好处:

      1. 文档片段存在于内存中,并不在DOM树中,所以将子元素插入到文档片段中不会引起页面回流,因此使用文档片段通常会有更好的性能

      2. 将文档片段插入到一个父节点时,被插入的是片段的所有子节点,而不是片段本身,因为所有的节点会被一次插入到文档中,而这个操作仅发生一个重渲染的操作,而不是每个节点分别被插入到文档中,因为后者会发生多次重渲染的操作

        查看具体用法以及DocumentFragment的说明

    1. appendChild
      将子节点添加到指定父节点的子节点列表的末尾。其实没什么好说的,不过其有一个特点,就是当插入的子节点存在于当前文档的文档树中的话,会从原来位置移除,然后再插入到新的位置。所以当在我们遍历el的子元素之后,要再将文档片段添加到el中,不然el中将不会有子元素。查看具体用法

    P.S. 当然在Vue中有着更复杂的操作,比如异步处理,安全校验等等,这里只是对其响应式做了一些简单的分析,难免有些错漏,还请各位大佬们指教。

    项目地址:https://github.com/lwy121810/VueReactiveImpl

    写在最后:

    2020年了,希望在这一年有所成长。元旦快乐!


    相关阅读:

    相关文章

      网友评论

          本文标题:Vue响应式原理浅析

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