vue双向绑定原理

理解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)。

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

简单版双向绑定的实现


    

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) {
    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数组

  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) {
    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) {
  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双向绑定原理)