vue 响应式原理实现

1. 整体分析

深入响应式原理

https://github.com/DMQ/mvvm

整体结构

1.1 Vue

data 中的成员注入到 Vue 实例,并且把 data 中的成员转成 getter/setter

1.2 Observer

能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知 Dep

1.3 Compiler

解析每个元素中的指令/插值表达式,并替换成相应的数据

1.4 Dep

添加观察者(watcher),当数据变化通知所有观察者

1.5 Watcher

数据变化更新视图


2. 具体实现

2.1 vue

功能:

  • 负责接收初始化的参数(选项)
  • 负责把 data 中的属性注入到 Vue 实例,转换成 getter/setter
  • 负责调用 observer 监听 data 中所有属性的变化
  • 负责调用 compiler 解析指令/插值表达式
class Vue { 
  constructor (options) { 
    // 1. 保存选项的数据 
    this.$options = options || {} 
    this.$data = options.data || {} 
    const el = options.el
    this.$el = typeof el === 'string' ? document.querySelector(el) : el
    // 2. 负责把 data 注入到 Vue 实例 
    this._proxyData(this.$data) 
    // 3. 负责调用 Observer 实现数据劫持
    new Observer(this.$data)
    // 4. 负责调用 Compiler 解析指令/插值表达式等 
    new Compiler(this);
  }
  _proxyData (data) { 
    // 遍历 data 的所有属性 
    Object.keys(data).forEach(key => { 
      Object.defineProperty(this, key, {
        enumerable: true,
        configurable: true,
        get () { 
          return data[key] 
        },
        set (newValue) { 
          if (data[key] === newValue) { return }
          data[key] = newValue 
        } 
      });
    });
  } 
}

2.2 Observer

功能:

  • 负责把 data 选项中的属性转换成响应式数据

  • data 中的某个属性也是对象,把该属性转换成响应式数据

  • 数据变化发送通知

// 负责数据劫持 
// 把 $data 中的成员转换成 getter/setter
class Observer { 
  constructor (data) { 
    this.walk(data)
  }
  walk(data) {
    // 1. 判断数据是否是对象,如果不是对象返回 
    // 2. 如果是对象,遍历对象的所有属性,设置为 getter/setter
    if (!data || typeof data !== 'object') { return }
    // 遍历 data 的所有成员 
    Object.keys(data).forEach(key => { 
      this.defineReactive(data, key, data[key]);
    })
  }
  // 定义响应式成员
  defineReactive (data, key, val) {
    const that = this 
    // 负责收集依赖,并发送通知
    let dep = new Dep();
    // 如果 val 是对象,继续设置它下面的成员为响应式数据
    this.walk(val)
    // 遍历 data 的所有属性 
    Object.defineProperty(data, key, {
      enumerable: true,
      configurable: true,
      get () { 
        // 收集依赖
        Dep.target && dep.addSub(Dep.target)
        return val
      },
      set (newValue) { 
        if (data[key] === newValue) { return }
        // 如果 newValue 是对象,设置 newValue 的成员为响应式 
        that.walk(newValue)
        val = newValue 
        // 发送通知
        dep.notify();
      } 
    });
  } 
}

2.3 Compiler

功能

  • 负责编译模板,解析指令/插值表达式

  • 负责页面的首次渲染

  • 当数据变化后重新渲染视图

// 负责解析指令/插值表达式 
class Compiler { 
  constructor (vm) { 
    this.vm = vm 
    this.el = vm.$el
    // 编译模板
    this.compile(this.el) 
  }
  // 编译模板 
  // 处理文本节点和元素节点 
  compile (el) { 
    const nodes = el.childNodes 
    Array.from(nodes).forEach(node => { 
      // 判断是文本节点还是元素节点 
      if (this.isTextNode(node)) { 
        this.compileText(node) 
      } else if (this.isElementNode(node)) { 
        this.compileElement(node) 
      }
      
      if (node.childNodes && node.childNodes.length) { 
        // 如果当前节点中还有子节点,递归编译 
        this.compile(node) 
      } 
    }) 
  }
  // 判断是否是文本节点 
  isTextNode (node) { 
    return node.nodeType === 3 
  }
  // 判断是否是属性节点 
  isElementNode (node) { 
    return node.nodeType === 1 
  }
  // 判断是否是以 v- 开头的指令 
  isDirective (attrName) { 
    return attrName.startsWith('v-') 
  }
  // 编译文本节点 
  compileText (node) { }
  // 编译属性节点 
  compileElement (node) { } 
}
2.3.1 compileText()
  • 负责编译插值表达式
// 编译文本节点 
compileText (node) {
  const reg = /\{\{(.+?)\}\}/ 
  // 获取文本节点的内容 
  const value = node.textContent 
  if (reg.test(value)) { 
    // 插值表达式中的值就是我们要的属性名称 
    const key = RegExp.$1.trim() 
    // 把插值表达式替换成具体的值 
    node.textContent = value.replace(reg, this.vm[key])
    
    // 创建watcher对象,当数据改变时更新视图
    new Watcher(this.vm, key, value => { 
      node.textContent = value 
    });
  }
}
2.3.2 compileElement()
  • 负责编译元素的指令
  • 处理 v-text 的首次渲染
  • 处理 v-model 的首次渲染
// 编译属性节点 
compileElement (node) {
  // 遍历元素节点中的所有属性,找到指令 
  Array.from(node.attributes).forEach(attr => { 
    // 获取元素属性的名称 
    let attrName = attr.name 
    // 判断当前的属性名称是否是指令 
    if (this.isDirective(attrName)) { 
      // attrName 的形式 v-text v-model 
      // 截取属性的名称,获取 text model 
      attrName = attrName.substr(2)
      // 获取属性的名称,属性的名称就是我们数据对象的属性 v-text="name",获取的是 name 
      const key = attr.value 
      // 处理不同的指令 
      this.update(node, key, attrName) 
    } 
  });
}

// 负责更新 DOM 
// 创建 Watcher 
update (node, key, dir) { 
  // node 节点,key 数据的属性名称,dir 指令的后半部分 
  const updaterFn = this[dir + 'Updater'] 
  updaterFn && updaterFn.call(this, node, this.vm[key], key);
}

// v-text 指令的更新方法 
textUpdater (node, value, key) { 
  node.textContent = value;
  new Watcher(this.vm, key, value => { 
    node.textContent = value 
  });
}

// v-model 指令的更新方法 
modelUpdater (node, value, key) { 
  node.value = value;
  new Watcher(this.vm, key, value => { 
    node.value = value
  });
  // 监听视图的变化 
  node.addEventListener('input', () => { 
    this.vm[key] = node.value 
  });
}

2.4 Dep(Dependency)

  • 功能

    • 收集依赖,添加观察者(watcher)
    • 通知所有观察者
  • 代码

    class Dep { 
      constructor () { 
        // 存储所有的观察者 
        this.subs = [] 
      }
      
      // 添加观察者 
      addSub (sub) { 
        if (sub && sub.update) { 
          this.subs.push(sub) 
        } 
      }
      
      // 通知所有观察者 
      notify () { 
        this.subs.forEach(sub => { 
          sub.update() 
        });
      } 
    }
    

2.5 Watcher

  • 功能

    • 当数据变化触发依赖, dep 通知所有的 Watcher 实例更新视图
    • 自身实例化的时候往 dep 对象中添加自己
  • 代码

    class Watcher { 
      constructor (vm, key, cb) { 
        this.vm = vm 
        // data 中的属性名称 
        this.key = key 
        // 回调函数负责更新视图
        this.cb = cb 
        // 把watcher对象记录到Dep类的静态属性target上
        Dep.target = this 
        // 触发get方法,在get方法中会调用addSub
        this.oldValue = vm[key] 
        Dep.target = null 
      }
    
      // 当数据发生变化时更新视图
      update () { 
        const newValue = this.vm[this.key] 
        if (this.oldValue === newValue) { return }
        this.cb(newValue) 
      } 
    }
    

3. 总结

  • Vue

    • 记录传入的选项,设置 $data/$el

    • data 的成员注入到 Vue 实例

    • 负责调用 Observer 实现数据响应式处理(数据劫持)

    • 负责调用 Compiler 编译指令/插值表达式等

  • Observer

    • 数据劫持

    • 负责把 data 中的成员转换成 getter/setter

    • 负责把多层属性转换成 getter/setter

    • 如果给属性赋值为新对象,把新对象的成员设置为 getter/setter

    • 添加 DepWatcher 的依赖关系

    • 数据变化发送通知

  • Compiler

    • 负责编译模板,解析指令/插值表达式

    • 负责页面的首次渲染过程

    • 当数据变化后重新渲染

  • Dep

    • 收集依赖,添加订阅者(watcher)

    • 通知所有订阅者

  • Watcher

    • 自身实例化的时候往dep对象中添加自己

    • 当数据变化dep通知所有的 Watcher 实例更新视图

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,142评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,298评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,068评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,081评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,099评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,071评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,990评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,832评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,274评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,488评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,649评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,378评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,979评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,625评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,643评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,545评论 2 352

推荐阅读更多精彩内容