Vue响应式原理浅析

最近在学习前端框架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官方原理解析
  • B站Vue原理视频解析

你可能感兴趣的:(Vue响应式原理浅析)