Vue2双向绑定的原理

77 阅读6分钟

整体架构与核心思路

Vue 的双向绑定原理基于数据劫持、观察者模式等,通过多个核心模块协同工作,实现数据变化驱动视图更新,以及视图交互(如表单输入等)反向更新数据的效果,整体遵循 MVVM(Model-View-ViewModel)的设计模式。核心思路是对数据进行监听,解析模板指令,在数据和视图之间建立关联,当数据变动时能及时通知相关部分更新视图,反之亦然。 Untitled.png

实现过程

  1. Observe(监听器)

    • 功能:负责监听数据对象的属性变化。通过递归遍历数据对象的属性,使用 Object.defineProperty 对每个属性进行劫持,创建对应的 Dep 实例来管理依赖关系。

    • 实现细节

      • 在构造函数中接收要监听的数据对象,调用 observe 方法开启监听。
      • observe 方法判断传入的数据是否为对象,若是则循环其属性,针对每个属性调用 defineReactive 方法进行处理。
      • defineReactive 方法为每个属性创建 Dep 实例,先递归监听属性值(如果属性值也是对象),然后通过 Object.defineProperty 定义属性的 get 和 set 方法。在 get 方法中,若存在全局的 Dep.target(即当前正在计算的 Watcher 实例),则将其添加到对应 Dep 实例的依赖列表 subs 中;在 set 方法中,当属性值更新时,调用 dep.notify 通知所有依赖该属性的 Watcher 实例进行更新。
    class Observe {
        constructor(data) {
            this.data = data;
            this.observe(data);
        }
    
        observe(data) {
            //如果不是对象就退出监听 递归退出条件
            if (data && typeof data === "object") {
                //循环data取值给每一个key 添加 监听
                Object.keys(data).forEach((key) => {
                    this.defineReactive(data, key, data[key]);
                });
            }
        }
    
        defineReactive(data, key, val) {
            const dep = new Dep()
            //递归监听子对象
            this.observe(val);
            // 给data每一个key值设置监听
            Object.defineProperty(data, key, {
                get() {
                    // 在watcher里面get方法里面会把,Dep.target静态方法添加Watcher实例
                    // 此时 全局只有一个Dep.target 如果有就添加到当前的dep的subs队列里面
                    // dep实例存储watcher
                    Dep.target && dep.addSub(Dep.target);
                    return val;
                },
                set(newVal) {
                    //更新赋值
                    val = newVal;
                    // 有更新就通知
                    dep.notify()
                },
            });
        }
    }
    
  2. Compile(解析器)

    • 功能:扫描和解析 DOM 元素节点上的指令,将模板中的数据占位符(如 {{}} 插值表达式、v- 指令等)与数据进行绑定,并关联相应的更新函数,使数据能正确渲染到视图以及视图变化能反馈到数据上。

    • 实现细节

      • 构造函数接收 vm(Vue 实例),获取对应的 DOM 元素(支持传入选择器字符串或直接的 DOM 节点),将其转换为文档碎片 fragment 进行后续解析,避免频繁操作 DOM。
      • node2Fragment 方法通过循环将传入 DOM 元素的子节点剪切到新创建的文档碎片中,方便后续统一处理后再添加回 DOM。
      • compileElement 方法遍历文档碎片中的子节点,根据节点类型(文本节点或元素节点)分别调用 compileText 或 compileAttrs 方法进行处理,若子节点还有子节点则递归调用自身继续解析。
      • compileAttrs 方法遍历元素节点的属性,判断是否为指令(以 v- 或 @ 开头),针对不同指令(如 v-textv-on:clickv-model 等)进行不同的处理,例如创建 Watcher 实例关联数据和视图更新逻辑,或者绑定事件监听等。
      • compileText 方法解析文本节点中的插值表达式,提取表达式内容并创建 Watcher 实例,当数据变化时更新文本节点的内容。同时,parseText 方法用于格式化插值表达式,将其转换为可计算求值的形式。
     class Compile {
         constructor(vm) {
             this.$vm = vm;
             const el = vm.$el
             this.$el = isElementNode(el) ? el : document.querySelector(el);
             if (this.$el) {
                 const fragment = this.node2Fragment(this.$el)
                 // 解析文档碎片
                 this.compileElement(fragment)
                 this.$el.appendChild(fragment)
             }
         }
    
         node2Fragment(el) {
             // 创建新的文档碎片 只需要更新一次dom
             let fragment = document.createDocumentFragment(),
                 child
             // 剪切 firstChild 到文档碎片里面
             while ((child = el.firstChild)) {
                 fragment.appendChild(child)
             }
             return fragment
         }
    
         compileElement(node) {
             const childNodes = node.childNodes,
                 that = this;
             // 类数组转换
             [].slice.call(childNodes).forEach(node => {        //<div v-text="msg">{{msg}}</div>
                 // 如果是文字节点 {{msg}}
                 if (isTextNode(node)) {
                     const reg = /\{\{(.+?)\}\}/;
                     const text = node.textContent
                     if (reg.test(text)) {
                         this.compileText(node)
                     }
                 } else if (isElementNode(node)) {
                     // 如果是元素节点 <div></div>
                     this.compileAttrs(node)
                 }
                 // 如果还有子节点 就递归执行
                 if (node.childNodes && node.childNodes.length) {
                     that.compileElement(node)
                 }
    
             })
         }
    
         compileAttrs(node) {
             const nodeAttrs = node.attributes;
             [].slice.call(nodeAttrs).forEach(attr => {        //<div v-text="msg">{{msg}}</div>
                 const attrName = attr.name //'v-text'
                 // 如果是指令
                 if (isDirective(attrName)) {
                     const exp = attr.value
                     let dir = attrName.substring(2)
                    // 分析指令 并执行内容
                     if (dir === 'text') {
                         new Watcher(exp, this.$vm, (newVal) => {
                             node.textContent = newVal
                         })
                     } else if (dir === 'on:click') {
                         const fn = this.$vm[exp]
                         document.addEventListener('click', fn.bind(this.$vm), false)
                     } else if (dir === 'model') {
                         node.addEventListener('input', (event) => {
                             this.$vm[exp] = event.target.value
                         })
                         new Watcher(exp, this.$vm, (newVal) => {
                             node.value = newVal
                         })
    
                     }
                 }
    
             })
         }
    
         compileText(node) {
             // 解析文本节点
             let text = node.textContent.trim()
             const exp = this.parseText(text)
             new Watcher(exp, this.$vm, (newValue) => {
                 node.textContent = newValue
             })
         }
         // 格式化插值表达式
         parseText(text) {
             const reg = /\{\{(.+?)\}\}/g;
             const price = text.split(reg)
             const match = text.match(reg)
             return price.map(item => {
                 if (match && match.indexOf('{{' + item + "}}") > -1) {
                     return "(" + item + ")"
                 } else {
                     return "'" + item + "'"
                 }
             }).join('+')
    
         }
     }
     // 判断节点
     const isDirective = attr => attr.indexOf("v-") === 0 || attr.indexOf("@") === 0;
    
     const isElementNode = node => node.nodeType === 1;
    
     const isTextNode = node => node.nodeType === 3;
     ```
    
  3. Watcher(观察者)

    • 功能:作为连接 Observer 和 Compile 的桥梁,订阅数据属性的变化,当属性变动时接收通知并执行对应的回调函数,从而更新视图。

    • 实现细节

      • 构造函数接收表达式 exp(对应数据属性或插值表达式等)、vm(Vue 实例)以及回调函数 cb,在实例化时调用 update 方法进行初始化更新。
      • get 方法先将自身设置为全局的 Dep.target,然后通过 computedExp 方法计算表达式的值,计算过程中会触发对应数据属性的 get 方法,从而建立依赖关系(将自身添加到对应 Dep 的 subs 列表中),最后再将 Dep.target 置空并返回计算出的值。
      • update 方法获取表达式最新的值,并调用传入的回调函数 cb,将新值传递进去,以实现视图更新。
      • computedExp 方法利用 new Function 创建一个函数,在指定的 vm 作用域下计算表达式的值。
    let _uid = 0
    class Watcher {
        constructor(exp,vm,cb) {
            this.exp =exp
    this.vm =vm
    this.cb =cb
    this.uid = _uid++
            this.update()
        }
    
        get() {
            // 把自己添加到dep里面 在Observe.defineReactive 里面初始化监听对象的时候get() 方法添加到dep实例的subs队列里面
            Dep.target = this
            // 计算插值表达式
            const newVal = Watcher.computedExp(this.exp, this.vm)
            Dep.target = null
            // 删除节点
            return newVal
    
        }
    
        update() {
            // 获取插值表达式的值 callback给node节点
            const exp = this.get()
            this.cb && this.cb(exp)
        }
    
        staticcomputedExp(exp,vm) {
            // 实现插值表达式里面的内容
            const fn = new Function('vm', 'with(vm){return ' +exp+ '}')
            return fn(vm)
        }
    }
    
  4. Dep(依赖收集器)

    • 功能:用于收集依赖(即 Watcher 实例),管理数据属性与观察者之间的关系,在数据变化时负责通知所有依赖该属性的 Watcher 实例进行更新。

    • 实现细节

      • 构造函数初始化一个空对象 subs 用于存放 Watcher 实例,通过 addSub 方法将 Watcher 实例添加到 subs 中,以 Watcher 的唯一标识 uid 作为属性名进行存储。
      • notify 方法遍历 subs 中的所有 Watcher 实例,调用每个实例的 update 方法,实现批量通知更新的操作。
    class Dep {
        constructor() {
            // 存放所有的watcher
            this.subs = {}
        }
    
        addSub(watcher) {
            // 把watcher 添加到subs集合
            this.subs[watcher.uid] =watcher
    }
    
        notify() {
            // 循环通知更新
            for (const uid in this.subs) {
                this.subs[uid].update()
            }
        }
    }