Vue 响应式原理简易实现

在Vue框架中vue能够做到数据变化时视图自动更新,主要依靠其响应式方式,今天我分享便是它的简易实现。

一、Vue 实例的构造与初始化

基础结构:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script src="./index.js"></script>
  <div id="app">
    <h1>标题是:{{ myTitle }} -- {{ myTitle }}</h1>
    <p>内容是: {{ myContent }} </p>
  </div>
  <script>
    const vm = new Vue({
      el: '#app',
      data: {
        myTitle:'这是一个标题',
        myContent:'这是一段文本'
      }
    })
  </script>
</body>
</html>

预览:在这里插入图片描述

(一)Vue 类的构造函数

首先看 Vue 类的构造函数:

class Vue {
  constructor(options) {
    this.$options = options || {};
    this.$data = options.data || {};
    const el = options.el;
    this.$el = typeof el === 'string' ? document.querySelector(el) : el;

    // 1. 将 data 中的属性代理到 Vue 实例上
    proxy(this, this.$data);
    // 2. 对 data 进行响应式观测
    new Observer(this.$data);
    // 3. 解析模板,建立数据与视图的关联
    new Compile(this);
  }
}

当我们 new 一个 Vue 实例时,会依次做三件关键事:

  1. 属性代理:通过 proxy 函数,把 data 对象里的属性“代理”到 Vue 实例上,这样我们可以直接用 this.xxx 访问 data.xxx
  2. 响应式观测:创建 Observer 实例,对 data 进行递归的响应式处理,让 data 里的每个属性都具备“被监听”的能力。
  3. 模板解析:创建 Compile 实例,解析页面中的模板(比如包含 {{}} 插值的节点),建立数据和 DOM 之间的关联。

(二)proxy:让属性访问更便捷

proxy 函数的作用是属性代理:

function proxy(target, data) {
  Object.keys(data).forEach(key => {
    Object.defineProperty(target, key, {
      enumerable: true,
      configurable: true,
      get() {
        return data[key];
      },
      set(newValue) {
        data[key] = newValue;
      }
    })
  })
}

举个例子,如果 data 里有 name: 'Vue',原本要通过 this.$data.name 访问,经过代理后,直接 this.name 就能拿到值,赋值时 this.name = 'React' 也会同步修改 data.name。这一步是为了让开发者用起来更顺手,隐藏了 $data 这个中间层。

二、Observer:让数据变得“可观测”

(一)Observer 类的职责

Observer 类负责把普通的 data 对象变成“响应式”对象:

class Observer {
  constructor(data) {
    this.data = data;
    this.dep = new Dep();
    this.walk(data)
  }
  walk(data) {
    const dep = this.dep;
    Object.keys(data).forEach(key => defineReactive(data, key, data[key], dep))
  }
}

它的核心是 walk 方法,遍历 data 中的每个属性,然后调用 defineReactive 函数,对每个属性进行“响应式化”处理。

(二)defineReactive:给属性加“监听器”

defineReactive 是响应式的核心实现:

function defineReactive(data, key, value, dep) {
  if (typeof value === 'object' && value !== null) {
    return new Observer(value);
  }
  
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get() {
      Dep.target && dep.addSub(Dep.target);
      return value;
    },
    set(newValue) {
      if (value === newValue) return;
      value = newValue;
      if (typeof value === 'object' && value !== null) {
        return new Observer(value);
      }
      dep.notify(); // 通知更新
    }
  })
}
  • get 方法:当属性被访问时(比如模板里用到 {{name}},读取 name 时),如果存在 Dep.target(后面会讲,这是 Watcher 实例),就把这个 Watcher 加入到当前属性的依赖集合 dep 中,这一步叫“依赖收集”。
  • set 方法:当属性被赋值时,先判断新值和旧值是否一样,不一样的话更新值。如果新值是对象,还需要递归地把新对象也变成响应式。最后,通过 dep.notify() 通知所有依赖这个属性的 Watcher,让它们去更新视图。
  • Dep 类Dep 是“依赖收集器”,每个响应式属性都有一个对应的 Dep 实例,用来存储依赖它的 Watcher
    class Dep {
      constructor() {
        this.subs = []
      }
    
      addSub(sub) {
        this.subs.push(sub);
      }
    
      notify() {
        this.subs.forEach(sub => sub.update());
      }
    }
    

三、Watcher:数据与视图的“纽带”

(一)Watcher 类的作用

Watcher 是连接数据和视图的桥梁:

class Watcher {
  constructor(vm, key, callback){
    this.vm = vm;
    this.key = key;
    this.callback = callback;

    Dep.target = this;
    this.oldValue = vm[key];
    Dep.target = null;
  }

  update(){
    const newValue = this.vm[this.key];
    if(this.oldValue === newValue) return;
    this.callback(newValue);
    this.oldValue = newValue;
  }
}

当创建 Watcher 实例时,会先把 Dep.target 指向自己,然后访问 vm[key](触发属性的 get 方法),这样 get 方法里的 dep.addSub(Dep.target) 就会把当前 Watcher 加入到属性的依赖集合中。之后,Dep.target 重置为 null,避免后续无关的依赖收集。

当数据变化时,Dep 会调用 Watcherupdate 方法,update 里会拿到新值,和旧值比较,如果不一样,就调用回调函数去更新视图。

四、Compile:解析模板,建立关联

(一)Compile 类的工作

Compile 类负责解析模板,找到数据和 DOM 的关联,并创建 Watcher 来监听数据变化:

class Compile {
  constructor(vm){
    this.vm = vm;
    this.el = vm.$el; 
    this.compile(this.el);
  }

  compile(el) {
    const childNodes = el.childNodes;
    Array.from(childNodes).forEach(node=>{
    		//1 表示元素节点,就是 HTML 里的各种标签,比如 div、p、span 这些。
			//2 表示属性节点,指的是元素的属性,比如 class、id
			//3 表示文本节点,就是元素里的文字内容,比如<p>里面的文字。
      if(node.nodeType === 3){
        this.compileText(node); // 文本节点
      }else if(node.nodeType === 1){
        // 元素节点(可扩展处理指令等,这里暂略)
      }

      if(node.childNodes && node.childNodes.length !== 0){
        this.compile(node);
      }
    })
  }
  compileText(node) {
    const reg = /\{\{(.+?)\}\}/g;
    const value = node.textContent.replace(/\s/g,'');

    const tokens = [];

    let result, index, lastIndex = 0;
    while(result = reg.exec(value)){
      index = result.index;
      if(index > lastIndex){
        tokens.push(value.slice(lastIndex, index));
      }
      const key = result[1].trim();
      tokens.push(this.vm[key]);      
      lastIndex = index + result[0].length;
      const pos = tokens.length - 1;
      // 创建 Watcher,数据变化时更新节点文本
      new Watcher(this.vm, key, newValue => {
        tokens[pos] = newValue;
        node.textContent = tokens.join('');
      });
    } 
    if(lastIndex < value.length){
      tokens.push(value.slice(lastIndex));
    }
    if(tokens.length){
      node.textContent = tokens.join('');
    }
  }
}

nodeType的几种常见情况:
在这里插入图片描述
result = reg.exec(value)的输出结果:
在这里插入图片描述

compile 方法递归遍历 DOM 节点,遇到文本节点时,调用 compileText方法。compileText 会用正则匹配 {{}} 格式的插值表达式,提取出数据的 key,然后创建 Watcher 监听这个 key 对应的数据变化。当数据变化时,Watcher 的回调函数会更新节点的文本内容。

五、整体过程

  1. 初始化阶段

    • 构造 Vue 实例,进行属性代理、响应式观测、模板解析。
    • Observer 遍历 data,通过 defineReactive 给每个属性加上 get/set 拦截,同时为每个属性创建 Dep
    • Compile 解析模板,遇到插值表达式,提取数据 key,并为每个 key 创建 WatcherWatcher 触发 get 方法,完成“依赖收集”(把自己加入 Dep)。
  2. 数据更新阶段

    • 当我们修改数据(如 this.name = 'New Name'),会触发属性的 set 方法。
    • set 方法里调用 dep.notify(),通知所有依赖该属性的 Watcher
    • Watcherupdate 方法被调用,执行回调函数,更新对应的 DOM 节点,视图随之更新。

预览:

在这里插入图片描述
出处详见:vue2响应式系统

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐