【Vue原理】从实例到源码分析数据双向绑定直冲高级

393 阅读4分钟

写此篇的理由:

  1. 一直困扰我很旧,双向绑定到底怎么实现的

  2. watch和computed的功能不一样,底层逻辑是用了啥

此文的收货:

  1. 深度理解双向绑定原理

  2. 写MVVM/双向绑定实例

  3. 分析源码的能力

  4. Object.defineProperty不能检测到对象属性的添加或删除,不能对数组新增属性监测更新,源码怎么处理的

  5. watch监听怎么实现的

后期补上:

  1. Proxy和Object.defineProperty的区别

  2. 用Proxy实现双向绑定实例

**从学习到成文花了近半个月时间,希望大家喜欢,也希望大家多多支持**

Vue中双向绑定方式:v-model 和 sync

<input v-model="title">

<sub :name.sync="age" />
将子组件的 name 属性与父组件的 age 数据绑定起来,其实原理是子组件中需要通过 $emit("update:name", value) 来触发变更。

原理图

先上我理解的原理图(使用VSC中draw.io插件绘制),有个直观认识,并且分别解释,后面详细实现一简单基于MVVM的Vue实例。

双向绑定主要由四大功能模块组成:解析器Compile、观察者Observer、订阅器Dep、订阅者Watcher

为便于理解后面的代码,写列出几点很重要的点:

  • 观察者Observer:用来劫持并监听所有属性(转变成setter/getter形式),如果属性发生变化,就通知订阅者

1) 用来对 data 所有属性数据进行劫持的构造函数
2) 给 data 中所有属性重新定义属性描述(get/set)
3) 为 data 中的每个属性创建对应的 dep 对象

  • 订阅器Dep:收集订阅者,统一通知订阅者更新

1) data 中的每个属性(所有层次)都对应一个 dep 对象
2) 创建的时机:

  • 在初始化 define data 中各个属性时创建对应的 dep 对象
  • 在 data 中的某个属性值被设置为新的对象时

3) Dep对象的结构

{
  id, // 每个 dep 都有一个唯一的 id
  subs //包含 n 个对应 watcher 的数组(subscribes 的简写)
}

4)subs 属性说明:

  • 当 watcher 被创建时, 内部将当前 watcher 对象添加到对应的 dep 对象的 subs 中

  • 当此 data 属性的值发生改变时, subs 中所有的 watcher 都会收到更新的通知,从而最终更新对应的界面

  • 订阅者Watcher:收到来自订阅者属性变化通知并执行相应的方法,从而更新视图

1)模板中每个非事件指令或表达式都对应一个 watcher 对象
2)监视当前表达式数据的变化
3)创建的时机:在初始化编译模板时
4)对象的组成

{
vm,   //vm 对象
exp//对应指令的表达式
cb,    //当表达式所对应的数据发生改变的回调函数
value, //表达式当前的值
depIds //表达式中各级属性所对应的 dep 对象的集合对象
      //属性名为 dep 的 id, 属性值为 dep
}
  • 解析器Compile:解析每个节点的相关指令,初始化模板数据和订阅者Watcher(部分指令才需要初始化Watcher,后面会讲到),并渲染页面

1)用来解析模板页面的对象的构造函数(一个实例)
2)利用 compile 对象解析模板页面
3)每解析一个表达式(非事件指令)都会创建一个对应的 watcher 对象, 并建立 watcher 与 dep 的关系
4)complie 与 watcher 关系: 一对多的关系

下面先手写一简单实现MVVM的实例,后面再来看Vue源码,更好理解

简单实现MVVM实例

<body>
    <div id="app">
        <h1>Thank you for your coming</h1>
        <input v-model="title">
        <h2>{{title}}</h2>
    </div>
</body>
<script src="../dist/bundle.js"></script>
<script type="text/javascript">
    var vm = new MVVM({
        el: '#app',
        data: {
            title: 'welcome',
            count: 1
        },
        // 发现只触发一次
        methods: {
            clickBtn: function (e) {
                this.title = 'welcome';
            }
        },
    });
 </script>

解析器Compile实现

解析每个节点的相关指令,初始化模板数据和订阅者Watcher,并渲染页面

import Watcher from './watcher'
class Compile {
  constructor(el, vm){
    this.vm = vm;
    this.el = document.querySelector(el);
    this.fragment = null;
    this.init();
  }
  init(){
    if (this.el) {
        this.fragment = this.nodeToFragment(this.el);
        this.compileElement(this.fragment);
        this.el.appendChild(this.fragment);
    } else {
        console.log('Dom元素不存在');
    }
  }
  // DocumentFragment:  文档碎片(高效批量更新多个节点)
  nodeToFragment(el) {
    let fragment = document.createDocumentFragment();
    let child = el.firstChild;
    while (child) {
        // 将Dom元素移入fragment中
        fragment.appendChild(child);
        child = el.firstChild  
    }
    return fragment;
  }
  compileElement (el) {
    let childNodes = el.childNodes;
    let self = this;
    [].slice.call(childNodes).forEach((node) => {
      let reg = /\{\{(.*)\}\}/;
      let text = node.textContent;
      if (self.isElementNode(node)) {  
        self.compile(node);
      } else if (self.isTextNode(node) && reg.test(text)) {
        self.compileText(node, reg.exec(text)[1]);
      }

      if (node.childNodes && node.childNodes.length) {
        self.compileElement(node);
      }
    });
  }
  compile(node) {
    let nodeAttrs = node.attributes;
    let self = this;
    [].forEach.call(nodeAttrs, function(attr) {
      let attrName = attr.name;
      if (self.isDirective(attrName)) {
        let exp = attr.value;
        let dir = attrName.substring(2);
        if (self.isEventDirective(dir)) {  // 事件指令
            self.compileEvent(node, self.vm, exp, dir);
        } else {  // v-model 指令
            self.compileModel(node, self.vm, exp, dir);
        }
        node.removeAttribute(attrName);
      }
    });
  }
  // 把值更新到页面中
  // text跟新需要new Watcher
  compileText(node, exp) {
    let self = this;
    let initText = this.vm[exp];
    this.updateText(node, initText);
    console.log('compileText1')
    new Watcher(this.vm, exp, function (value) {
      console.log('compileText2',value)  //只有value变化了才会执行此处
      self.updateText(node, value);
    });
  }
  // 函数最终还是要落地到text,点击函数,触发data更新,执行compileText
  compileEvent(node, vm, exp, dir) {
    let eventType = dir.split(':')[1];
    let cb = vm.methods && vm.methods[exp];
    console.log('compileEvent')
    if (eventType && cb) {
      node.addEventListener(eventType, cb.bind(vm), false);
    }
  }
  // model跟新需要new Watcher
  compileModel(node, vm, exp, dir) {
    let self = this;
    let val = this.vm[exp];
    this.modelUpdater(node, val);
    new Watcher(this.vm, exp, function (value) {
      self.modelUpdater(node, value);
    });

    node.addEventListener('input', function(e) {
      let newValue = e.target.value;
      if (val === newValue) {
          return;
      }
      self.vm[exp] = newValue;
      val = newValue;
  });
  }
  updateText(node, value) {
    console.log('updateText')
    node.textContent = typeof value == 'undefined' ? '' : value;
  }
  modelUpdater(node, value, oldValue) {
    node.value = typeof value == 'undefined' ? '' : value;
  }
  isDirective(attr) {
    return attr.indexOf('v-') == 0;
  }
  isEventDirective(dir) {
    return dir.indexOf('on:') === 0;
  }
  isElementNode(node) {
    return node.nodeType == 1;
  }
  isTextNode(node) {
    return node.nodeType == 3;
  }

}

export default Compile;

监听者Observer实现

用来劫持并监听所有属性(转变成setter/getter形式),如果属性发生变化,就通知订阅者

import Dep from './dep'
class Observer{
  constructor(data){
    this.data = data;
    this.traverse(data);
  }
  traverse(data){
    Object.keys(data).forEach((key)=>{
      this.defineReactive(data, key, data[key])
    })
  }
  defineReactive(data, key, val){
    var dep = new Dep();
    Object.defineProperty(data, key, {
      enumerable: true,
      configurable: true,
      get: () => {
        if (Dep.target) {
            dep.addSub(Dep.target); //加入依赖
            console.log(dep)
        }
        return val;
      },
      set: (newVal) => {
        if (newVal === val) {
            return;
        }
        val = newVal;
        dep.notify();
      }
    });
  }
}

function observe(value, vm) {
  if (!value || typeof value !== 'object') {
    return;
  }
  return new Observer(value);
};
export default observe;

订阅器Dep实现

收集订阅者,统一通知订阅者更新

class Dep{
  constructor(){
    this.subs = []
  }
  //收集订阅者
  addSub(sub){
    this.subs.push(sub)
  }
  //统一通知订阅者更新
  notify(){
    this.subs.forEach((sub) =>{
        sub.update();
    });
  }
}
Dep.target = null;
export default Dep

订阅者Watcher实现

收到来自订阅者属性变化通知并执行相应的方法,从而更新视图

import Dep from './dep'
class Watcher{
  /* 思考需要传什么参数呢? 
    肯定要知道更新函数cb;
    肯定要知道哪个更新的东西,也就是data中某一个键值对
    肯定要知道哪个对象上的data,所以要传vm
  */
  constructor(vm, exp, cb){
    this.vm = vm;
    this.exp = exp;
    this.cb = cb;
    this.value = this.get();  // 编译就得将自己添加到订阅器
  }
  // 先要获取旧值,再判断是否跟新值一样,不一样的话,调用更新函数
  update(){
    let value = this.vm.data[this.exp];
    let oldVal = this.value;
    if (value !== oldVal) {
        this.value = value;  //更新初始值
        this.cb.call(this.vm, value, oldVal); //监听函数为什么能获取新旧值就是从这里传出去的
    }
  }
  get(){
    Dep.target = this;  // 缓存自己
    let value = this.vm.data[this.exp]  // 强制执行Observer里的get函数
    Dep.target = null;  // 释放自己
    return value;
  }
}
export default Watcher;

Vue双向绑定源码

开始从 Vue 源码层面分析解析器Compile、观察者Observer、订阅器Dep、订阅者Watcher的实现,帮助大家加强了解 Vue 源码如何实现数据双向绑定。

监听器 Observer 源码及分析

其实监听器核心就是利用 Object.defineProperty 给数据添加了 getter 和 setter,目的就是为了在我们访问数据以及写数据的时候能自动执行一些逻辑 。

Vue初始化阶段,_init 方法中,会执行 initState(vm) 方法

  1. initState
export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}
  • 对 props、methods、data、computed 和 wathcer 等属性做了初始化操作,这里我们重点分析 data。
  1. initData
function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  if (!isPlainObject(data)) {
    data = {}
  }
  // proxy data on instance
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    }
    if (props && hasOwn(props, key)) {

    } else if (!isReserved(key)) {
      proxy(vm, `_data`, key)
    }
  }
  // observe data
  observe(data, true /* asRootData */)
}
  • 对定义 data 函数返回对象的遍历,通过 proxy 把每一个值 vm._data.xxx 都代理到 vm.xxx 上;
  • 调用 observe 方法观测整个 data 的变化,把 data 也变成响应式
  1. observe
export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  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
}
  • 给非 VNode 的对象类型数据添加一个 Observer
  • 通过__ob__判断是否已经添加过Observer
  • 如果已经添加过则直接返回,否则在满足一定条件下去实例化一个 Observer 对象实例
  1. Observer
export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that have this object as root $data

  constructor (value: any) {
  //为观测的属性添加 __ob__ 属性,它的值等于 this,即当前 Observe 的实例,观测过的数据都会被添加上 __ob__ 属性,通过判断该属性是否存在,防止重复观测
    this.dep = new Dep()
    if (Array.isArray(value)) {
      // 为数组添加重写的数组方法,比如:push、unshift、splice 等方法,重写目的是在调用这些方法时,进行更新渲染
      if (hasProto) {
        //使用__proto__拦截原型链以增强目标对象和数组
        protoAugment(value, arrayMethods)
      } else {
        //通过定义隐藏的属性来加强数组和对象
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)  //只对数组
    } else {
      this.walk(value)  //对对象,那对单个属性呢在哪里呢?
    }
  }

  /**
   观测对象数据,defineReactive 为数据定义 get 和 set ,也就是数据拦截
   */
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

  /**
   观测数组内的数据,形成递归观测
   observe 内部返回an observer instance,如果不存在会调用 new Observe,如果存在直接返回
  意味着深层数组递归遍历
   */
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}
  • 实例化 Dep 对象
  • 对 value 做判断,对于数组会调用 observeArray 方法,对纯对象调用 walk 方法
  • observeArray 是遍历数组再次调用 observe 方法,walk 方法是遍历对象的 key 调用 defineReactive 方法
  1. defineReactive

源码路劲:github.com/vuejs/vue/b…

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
// 初始化Dep实例
  const dep = new Dep()

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }
  // 满足预定义的 getter/setters
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }
  //observe 函数返回值为 Observe 实例
  //对子对象递归调用observe,所以无论obj的子属性都能变成响应式对象
  let childOb = !shallow && observe(val)
  
   // 用Object.defineProperty对data数据进行拦截
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()  
        if (childOb) {
        /**
         * depend在Dep中的函数
         * depend() {
            Dep.target.addDep(this)
           }
       */
          childOb.dep.depend()  //为 childOb 的 dep 添加依赖
          //当数组被触及时,遍历收集数组元素的依赖关系;因为我们不能像属性getter一样拦截数组元素访问。
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      // 对新值重新监测
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}
  • 对子对象递归调用observe,所以无论obj的子属性都能变成响应式对象
  • 用Object.defineProperty对data数据进行拦截
  1. 代码的详解归总如下:
  • 为观测的属性添加 ob 属性,它的值等于 this,即当前 Observe 的实例
  • 为数组添加重写的数组方法,比如:push、unshift、splice 等方法,重写目的是在调用这些方法时,进行更新渲染
  • 观测数组内的数据,形成递归观测,observe 内部返回an observer instance,如果不存在会调用 new Observe,如果存在直接返回
  • 当数组被触及时,遍历收集数组元素的依赖关系
  • 观测对象数据,defineReactive 为数据定义 get 和 set ,也就是数据拦截

订阅器 Dep 源码及分析

源码路劲:github.com/vuejs/vue/b…

export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;
  constructor () {
    this.id = uid++
    this.subs = []
  }
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }
  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }
  notify () {
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}
// 在同一时间只能有一个全局的 Watcher 被计算
Dep.target = null

此处需要特别注意的是:它有一个静态属性 target,这是一个全局唯一 Watcher,这是一个非常巧妙的设计,因为在同一时间只能有一个全局的 Watcher 被计算,另外它的自身属性 subs 也是 Watcher 的数组。

Dep 实际上就是对 Watcher 的一种管理,Dep 脱离 Watcher 单独存在是没有意义的。

订阅者 Watcher 源码及分析

源码路劲:github.com/vuejs/vue/b…

export default class Watcher {
  vm: Component;
  expression: string;
  cb: Function;
  id: number;
  deep: boolean;
  user: boolean;
  lazy: boolean;
  sync: boolean;
  dirty: boolean;
  active: boolean;
  deps: Array<Dep>;
  newDeps: Array<Dep>;
  depIds: SimpleSet;  
  newDepIds: SimpleSet;
  before: ?Function;
  getter: Function;
  value: any;
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
      this.before = options.before
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.lazy // for lazy watchers
    this.deps = []
    this.newDeps = []
    //1. 
    this.depIds = new Set()  //this.deps的id集合
    this.newDepIds = new Set() //this.newDepIds的id集合
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = noop
      }
    }
    this.value = this.lazy
      ? undefined
      : this.get()
      
   ...
   
   /**
   * Depend on all deps collected by this watcher.
   模板中一个非事件表达式对应一个 watcher, 一个 watcher 中可能包含多个 dep
   */
    depend () {
      let i = this.deps.length
      while (i--) {
        this.deps[i].depend()
      }
    }
  } 
...

实例化一个Watcher 的时候,会执行 this.get() 方法

  get () {
  // 1. 把 Dep.target 赋值为当前的渲染 watcher 并压栈(为了恢复用)
    pushTarget(this)
    let value
    const vm = this.vm
    try {
    //2. 触发了数据对象的getter函数,根据this.getter被赋值,也就是会调用expOrFn或者parsePath
      value = this.getter.call(vm, vm) 
    } catch (e) {
    	//如果this.user为true,执行,否则直接抛错,this.user代表啥呢?
        handleError(e, vm, `getter for watcher "${this.expression}"`)
    } finally {
      //finally函数肯定会执行,如果是深度监听执行深度遍历,由此可监听属性会执行这
      if (this.deep) {
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }
}
  1. 把 Dep.target 赋值为当前的渲染 watcher 并压栈(为了恢复用)
  2. 触发了数据对象的getter函数,根据this.getter被赋值,也就是会调用expOrFn或者parsePath

Vue实例中data对象值的每个getter 都持有一个 dep,在触发 getter 的时候会调用 dep.depend() 方法,也就会执行 Dep.target.addDep(this)。

注:一次渲染包含多个dep,因为可能有多个属性需要更新。

  addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep) // newDeps是收集所有dep的
      if (!this.depIds.has(id)) {
        dep.addSub(this)  // 把当前的watcher添加这个数据持有的 dep 的 subs 中
      }
    }
  }

把watcher添加依赖到Dep中,这里有根据newDepIds和每个 dep 都有一个唯一的 id逻辑判断,保证同一watcher(可以理解为指令)不会被添加多次。

dep.addSub(this)是把当前的 watcher 订阅到这个数据持有的 dep 的 subs 中,目的是为后续数据变化时候能通知到哪些 subs 做准备。

所以在 vm._render()(渲染而不是加载) 过程中,会触发所有数据的 getter,这样实际上已经完成了一个依赖收集的过程。

注意:指令中可能有多处表达式使用到了同一个属性,所以一个 dep 中可能包含多个 watcher

当我们在组件中对响应的数据做了修改,就会触发 setter 的逻辑,最后调用 watcher 中的 update 方法,不细讲其中执行的每个函数,不过最终会获取到新值,并且把新值和旧值传给回调函数this.cb.call(this.vm, value, oldValue)。

//Subscriber接口
update () {
  /* istanbul ignore else */
  if (this.lazy) {
    this.dirty = true
  } else if (this.sync) {
    this.run()
  } else {
    queueWatcher(this)
  }
}

执行get都会清除收集的依赖,但是this.depIds会保存下来

  cleanupDeps () {
    let i = this.deps.length
    while (i--) {
      const dep = this.deps[i]
      if (!this.newDepIds.has(dep.id)) {
        dep.removeSub(this)
      }
    }
    let tmp = this.depIds
    this.depIds = this.newDepIds
    this.newDepIds = tmp
    this.newDepIds.clear()
    tmp = this.deps
    this.deps = this.newDeps
    this.newDeps = tmp
    this.newDeps.length = 0
  }

重点总结

  1. Watcher 和 Dep 是多对多的关系
  • data 中的一个属性对应一个 dep, 一个 dep 中可能包含多个 watcher(模板中有几个表达式使用到了同一个属性)
  • 模板中一个非事件表达式对应一个 watcher, 一个 watcher 中可能包含多个 dep(表达式是多层: a.b)
  1. Complie 与 Watcher 是一对多的关系
  2. 数据绑定使用到2个核心技术
  • defineProperty()
  • 消息订阅与发布
  1. Object.defineProperty不能检测到对象属性的添加或删除,不能对数组新增属性监测更新