美文网首页
vue双向绑定原理

vue双向绑定原理

作者: cesiuming | 来源:发表于2021-01-23 01:19 被阅读0次

    理解vue的设计思想

    MVVM模式

    image.jpeg

    MVVM框架的三要素:数据响应式、模版引擎及其渲染

    数据响应式:监听数据变化并在视图中更新

    • Object.defineProperty()
    • Proxy

    模版引擎:提供描述视图的模版语法

    • 插值:{{}}
    • 指令:v-bind,v-on,v-model,v-for,v-if

    渲染:如何将模版转换为html

    • 模版 =>vdom =>dom

    vue数据双向绑定原理

    原理分析

    1. new Vue() 首先执行初始化,对data执行响应化处理,这个过程发生在Observer中

    2. 同时对模板执行编译,找到其中动态绑定的数据,从data中获取并初始化视图,这个过程发生在Compile中

    3. 同时定义一个更新函数和Watcher,将来对应数据变化时Watcher会调用更新函数

    4. 由于data的某个key在一个视图中可能出现多次,所以每个key都需要一个管家Dep来管理多个Watcher

    5. 将来data中数据一旦发生变化,会首先找到对应的Dep,通知所有Watcher执行更新函数

    image.jpeg

    思路分析:视图到数据的改动要监听DOM的变化再同步赋值给变量,比如给input添加change或input监听事件并在事件处理函数中给变量赋值,实际也就是v-model指令在做的事,另一个方向,监听数据变化,再去更新对应的DOM,这里的监听及处理也就是vue的实现方式——发布订阅模式+数据劫持(Object.defineProperty)。

    • 监听数据变化(数据劫持/数据代理)
    • 收集视图依赖了哪些数据 (依赖收集)
    • 数据变化时,自动“通知”视图需要修改哪些部分,并进行更新 (发布订阅模式)

    简单版双向绑定的实现

    <input type="text" id="input" />
        <p id="data"></p>
        <script>
            const obj = {};
            const input = document.getElementById('input');
            // 数据劫持,实现数据->视图的绑定
            Object.defineProperty(obj, 'name', {
                configurable: true,
                enumerable: true,
                get() {
                    return input.value;
                },
                set(newVal) {
                    input.value = newVal;
                    document.getElementById('data').innerHTML = newVal;
                }
             });
            // 监听输入框,实现视图->数据的绑定
            input.addEventListener('keyup', () => {
                obj.name = input.value;
            })
        </script>
    
    image.jpeg

    遍历需要响应化的对象

    // 对象响应化:遍历每个key,定义getter、setter
    function observe(obj) {
        if (typeof obj !== 'object' || obj == null) {
            return
        }
        Object.keys(obj).forEach(key => {
            defineReactive(obj, key, obj[key])
        })
    }
    

    解决嵌套对象问题

    function defineReactive(obj, key, val) {
        observe(val)
        Object.defineProperty(obj, key, {
        //...
    

    解决赋值是对象的情况

    set(newVal) {
        if (newVal !== val) {
            observe(newVal) // 新值是对象的情况
            notifyUpdate()
    

    1、Observer—数据监听系统,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者,核心方法就是Object.defineProperty( )

    function defineReactive(data, key, val) {
        observe(val); // 递归遍历所有子属性
        Object.defineProperty(data, key, {
            enumerable: true,
            configurable: true,
            get: function() {
                return val;
            },
            set: function(newVal) {
                val = newVal;
                console.log('属性' + key + '已经被监听了,现在值为:“' + newVal.toString() + '”');
            }
        });
    }
    
    function observe(data) {
        if (!data || typeof data !== 'object') {
            return;
        }
        Object.keys(data).forEach(function(key) {
            defineReactive(data, key, data[key]);
        });
    };
    

    2、Dep —发布订阅模型,作为连接Observer和Compile的桥梁,一个Dep实例对应一个对象属性或一个被观察的对象,能够订阅并收集每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图。

    function defineReactive(data, key, val) {
        observe(val); // 递归遍历所有子属性
        var dep = new Dep(); // dep
        Object.defineProperty(data, key, {
            enumerable: true,
            configurable: true,
            get: function() {
                if (是否需要添加订阅者) {
                    dep.addSub(watcher); // 在这里添加一个订阅者
                }
                return val;
            },
            set: function(newVal) {
                if (val === newVal) {
                    return;
                }
                val = newVal;
                console.log('属性' + key + '已经被监听了,现在值为:“' + newVal.toString() + '”');
                dep.notify(); // 如果数据变化,通知所有订阅者
            }
        });
    }
    
    function Dep () {
        this.subs = [];
    }
    Dep.prototype = {
        addSub: function(sub) {
            this.subs.push(sub);
        },
        notify: function() {
            this.subs.forEach(function(sub) {
                sub.update();
            });
        }
    };
    

    3、Watcher—订阅者Watcher在初始化的时候需要将自己添加进订阅器Dep中

    function Watcher(vm, exp, cb) {
        this.cb = cb;
        this.vm = vm;
        this.exp = exp;
        this.value = this.get();  // 将自己添加到订阅器的操作
    }
    
    Watcher.prototype = {
        update: function() {
            this.run();
        },
        run: function() {
            var value = this.vm.data[this.exp];
            var oldVal = this.value;
            if (value !== oldVal) {
                this.value = value;
                this.cb.call(this.vm, value, oldVal);
            }
        },
        get: function() {
            Dep.target = this;  // 缓存自己
            var value = this.vm.data[this.exp]  // 强制执行监听器里的get函数
            Dep.target = null;  // 释放自己
            return value;
        }
    };
    

    然后需要对监听器Observer稍微调整,主要是对应watcher类原型上的get方法

    function defineReactive(data, key, val) {
        observe(val); // 递归遍历所有子属性
        var dep = new Dep(); 
        Object.defineProperty(data, key, {
            enumerable: true,
            configurable: true,
            get: function() {
                if (Dep.target) {.  // 判断是否需要添加订阅者
                    dep.addSub(Dep.target); // 在这里添加一个订阅者
                }
                return val;
            },
            set: function(newVal) {
                if (val === newVal) {
                    return;
                }
                val = newVal;
                console.log('属性' + key + '已经被监听了,现在值为:“' + newVal.toString() + '”');
                dep.notify(); // 如果数据变化,通知所有订阅者
            }
        });
    }
    Dep.target = null;
    

    4、Compile—指令解析系统,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数

    image.jpeg
    class Compile {
        constructor(el, vm) {
            this.$vm = vm;
            this.$el = document.querySelector(el);
            if (this.$el) {
                this.compile(this.$el);
            }
        }
        compile(el) {
            const childNodes = el.childNodes;
            Array.from(childNodes).forEach(node => {
                if (this.isElement(node)) {
                    console.log("编译元素" + node.nodeName);
                } else if (this.isInterpolation(node)) {
                    console.log("编译插值文本" + node.textContent);
                }
                if (node.childNodes && node.childNodes.length > 0) {
                    this.compile(node);
                }
            });
        }
        isElement(node) {
            return node.nodeType == 1;
        }
        isInterpolation(node) {
            return node.nodeType == 3 && /\{\{(.*)\}\}/.test(node.textContent);
        }
    }
    

    VUE 源码所对应位置

    • core/observer/index.js observe() 返回一个Observer实例
    export function observe (value: any, asRootData: ?boolean): Observer | void {
      // 观察者
      let ob: Observer | void
      if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    
    ob = value.__ob__ 
      } else if (
        shouldObserve &&
        !isServerRendering() &&
        (Array.isArray(value) || isPlainObject(value)) &&
        Object.isExtensible(value) &&
        !value._isVue
      ) {// 创建观察者
        ob = new Observer(value)
      }
      if (asRootData && ob) {
        ob.vmCount++
      }
      return ob
    }
    
    • core/observer/index.js Observer对象根据数据类型执行对应的响应化操作
    export class Observer {
      value: any;
      dep: Dep; // 保存数组类型数据的依赖
    
      constructor (value: any) {
        this.value = value
        this.dep = new Dep()
        def(value, '__ob__', this) // 在getter中可以通过__ob__可获取ob实例
        if (Array.isArray(value)) { // 数组响应化
          protoAugment(value, arrayMethods)  
          this.observeArray(value)
        } else { // 对象响应化
          this.walk(value)
        }
      }
    
      /**
       * 遍历对象所有属性定义其响应化
       */
      walk (obj: Object) {
        const keys = Object.keys(obj)
        for (let i = 0; i < keys.length; i++) {
          defineReactive(obj, keys[i])
        }
      }
    
      /**
       * 对数组每一项执行observe
       */
      observeArray (items: Array<any>) {
        for (let i = 0, l = items.length; i < l; i++) {
          observe(items[i])
        }
      }
    }
    
    • core/observer/dep.js Dep负责管理一组Watcher,包括watcher实例的增删及通知更新
    export default class Dep {
      static target: ?Watcher; // 依赖收集时的wacher引用
      subs: Array<Watcher>; // watcher数组
    
      constructor () {
        this.subs = [] 
      }
      //添加watcher实例
      addSub (sub: Watcher) {
        this.subs.push(sub)
      }
      //删除watcher实例
      removeSub (sub: Watcher) {
        remove(this.subs, sub)
      }
      //watcher和dep相互保存引用
      depend () {
        if (Dep.target) {
          Dep.target.addDep(this)
        }
      }
    
      notify () {
        // stabilize the subscriber list first
        const subs = this.subs.slice()
    
        for (let i = 0, l = subs.length; i < l; i++) {
          subs[i].update()
        }
      }
    }
    
    • src/core/observer/watcher.js watcher监控一个表达式或关联一个组件更新函数,数值更新则指定回调或更新函数被调用
    export default class Watcher {
      constructor (
        vm: Component,
        expOrFn: string | Function,
        cb: Function,
        options?: ?Object,
        isRenderWatcher?: boolean
      ) {
        this.vm = vm
        // 组件保存render watcher
        if (isRenderWatcher) {
          vm._watcher = this
        }
        // 组件保存非render watcher
        vm._watchers.push(this)
    
        // options...
    
        // 将表达式解析为getter函数
        // 如果是函数则直接指定为getter,那什么时候是函数?
        // 答案是那些和组件实例对应的Watcher创建时会传递组件更新函数updateComponent
        if (typeof expOrFn === 'function') {
          this.getter = expOrFn
        } else {
          // 这种是$watch传递进来的表达式,它们需要解析为函数
          this.getter = parsePath(expOrFn)
          if (!this.getter) {
            this.getter = noop
          }
        }
        // 若非延迟watcher,立即调用getter
        this.value = this.lazy ? undefined : this.get()
      }
    
      /**
       * 模拟getter, 重新收集依赖re-collect dependencies.
       */
      get () {
        // Dep.target = this
        pushTarget(this)
        let value
        const vm = this.vm
        try {
          // 从组件中获取到value同时触发依赖收集
          value = this.getter.call(vm, vm)
        } 
        catch (e) {} 
        finally {
          // deep watching,递归触发深层属性
          if (this.deep) {
            traverse(value)
          }
          popTarget()
          this.cleanupDeps()
        }
        return value
      }
    
        addDep (dep: Dep) {
        const id = dep.id
        if (!this.newDepIds.has(id)) {
          // watcher保存dep引用
          this.newDepIds.add(id)
          this.newDeps.push(dep)
          // dep添加watcher
          if (!this.depIds.has(id)) {
            dep.addSub(this)
          }
        }
      }
    
      update () {
        // 更新逻辑
        if (this.lazy) {
          this.dirty = true
        } else if (this.sync) {
          this.run()
        } else {
          //默认lazy和sync都是false,所以会走该逻辑
          queueWatcher(this)
        }
      }
    }
    

    数组响应化

    • src/core/observer/array.js 为数组原型中的7个可以改变内容的方法定义拦截器
    // 数组原型
    const arrayProto = Array.prototype
    // 修改后的原型
    export const arrayMethods = Object.create(arrayProto)
    // 七个待修改方法
    const methodsToPatch = [
      'push',
      'pop',
      'shift',
      'unshift',
      'splice',
      'sort',
      'reverse'
    ]
    
    /**
     * 拦截这些方法,额外发送变更通知
     */
    methodsToPatch.forEach(function (method) {
      // 原始数组方法
      const original = arrayProto[method]
      // 修改这些方法的descriptor
      def(arrayMethods, method, function mutator (...args) {
        // 原始操作
        const result = original.apply(this, args)
        // 获取ob实例用于发送通知
        const ob = this.__ob__
        // 三个能新增元素的方法特殊处理
        let inserted
        switch (method) {
          case 'push':
          case 'unshift':
            inserted = args
            break
          case 'splice':
            inserted = args.slice(2)
            break
        }
        // 若有新增则做响应处理
        if (inserted) ob.observeArray(inserted)
        // 通知更新
        ob.dep.notify()
        return result
      })
    })
    
    • core/observer/index.js 覆盖数组原型
    if (Array.isArray(value)) {
       // 替换数组原型
       protoAugment(value, arrayMethods) // value.__proto__ = arrayMethods
       this.observeArray(value)
    }
    
    • core/observer/index.js 数组响应式的特殊处理
    observeArray (items: Array<any>) {
        for (let i = 0, l = items.length; i < l; i++) {
          observe(items[i])
        }
      }
    
    • core/observer/index.js 依赖收集时的特殊处理
    //getter中
    if (Array.isArray(value)) {
      dependArray(value)
    }
    
    // 数组中每一项也需要收集依赖
    function dependArray (value: Array<any>) {
      for (let e, i = 0, l = value.length; i < l; i++) {
        e = value[i]
        e && e.__ob__ && e.__ob__.dep.depend()
        if (Array.isArray(e)) {
          dependArray(e)
        }
      }
    }
    

    整体感知virtual DOM

    虚拟DOM(Virtual DOM)是对DOM的JS抽象表示,它们是JS对象,能够描述DOM结构和关系。应用的各种状态变化会作用于虚拟DOM,最终映射到DOM上。

    virtual DOM分为三个步骤:

    1.createElement(): 用 JavaScript对象(虚拟树) 描述 真实DOM对象(真实树)

    2.diff(oldNode, newNode) : 对比新旧两个虚拟树的区别,收集差异

    3.patch() : 将差异应用到真实DOM树

    优点

    虚拟DOM轻量、快速:当它们发生变化时通过新旧虚拟DOM比对可以得到最小DOM操作量,从而提升性能

    跨平台:将虚拟dom更新转换为不同运行时特殊操作实现跨平台

    兼容性:还可以加入兼容性代码增强操作的兼容性

    必要性

    vue 1.0中有细粒度的数据变化侦测,它是不需要虚拟DOM的,但是细粒度造成了大量开销,这对于大型项目来说是不可接受的。因此,vue 2.0选择了中等粒度的解决方案,每一个组件一个watcher实例,这样状态变化时只能通知到组件,再通过引入虚拟DOM去进行比对和渲染。

    patchVnode

    比较两个VNode,包括三种类型操作:属性更新、文本更新、子节点更新

    具体规则如下:

    1. 新老节点均有children子节点,则对子节点进行diff操作,调用updateChildren

    2. 如果老节点没有子节点而新节点有子节点,先清空老节点的文本内容,然后为其新增子节点。

    3. 当新节点没有子节点而老节点有子节点的时候,则移除该节点的所有子节点。

    4. 当新老节点都无子节点的时候,只是文本的替换。

    相关文章

      网友评论

          本文标题:vue双向绑定原理

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