Skip to content

Vue 响应式

What?

  • 数据与函数关联
  • 数据发生改变,自动调用依赖该数据的函数

How?

  • 依赖 defineProperty 做数据劫持
  • 靠 get 收集依赖,保存有哪个函数在用
  • 修改属性的值时,在 set 中调用与依赖该属性的每个函数

Example

修改单一属性值,并执行更新方法

js
let user = {
  name: "王花花",
};

function showName() {
  const doc = document.querySelector(".name");
  doc.textContent = `姓名: ${user.name}`;
}

// 定义一个内部属性,用于保存劫持的值
let internalValue = user.name;
let func = null;
Object.defineProperty(user, "name", {
  get() {
    console.log("获取 name");
    func = showName; // 收集: 相关函数
    return internalValue;
  },
  set(value) {
    console.log("设置 name 为", value);
    internalValue = value; // 派发: 执行函数
    func();
  },
});

user.name; // 获取 name
user.name = "李明花"; // 设置 name 为 李明花
let user = {
  name: "王花花",
};

function showName() {
  const doc = document.querySelector(".name");
  doc.textContent = `姓名: ${user.name}`;
}

// 定义一个内部属性,用于保存劫持的值
let internalValue = user.name;
let func = null;
Object.defineProperty(user, "name", {
  get() {
    console.log("获取 name");
    func = showName; // 收集: 相关函数
    return internalValue;
  },
  set(value) {
    console.log("设置 name 为", value);
    internalValue = value; // 派发: 执行函数
    func();
  },
});

user.name; // 获取 name
user.name = "李明花"; // 设置 name 为 李明花

以上代码实现单一指定的数据发生变化时调用执行相关函数,如果多个呢? 例如:

js
let user = {
  name: "王花花",
  gender: "男", 
  age: "20", 
};

function showName() {
  const doc = document.querySelector(".name");
  doc.textContent = `姓名: ${user.name}`;
}

function showGender() {
  
  const doc = document.querySelector(".gender"); 
  doc.textContent = `性别: ${user.gender}`; 
} 

function showAge() {
  
  const doc = document.querySelector(".age"); 
  doc.textContent = `年龄: ${user.age}`; 
} 

showName();
let user = {
  name: "王花花",
  gender: "男", 
  age: "20", 
};

function showName() {
  const doc = document.querySelector(".name");
  doc.textContent = `姓名: ${user.name}`;
}

function showGender() {
  
  const doc = document.querySelector(".gender"); 
  doc.textContent = `性别: ${user.gender}`; 
} 

function showAge() {
  
  const doc = document.querySelector(".age"); 
  doc.textContent = `年龄: ${user.age}`; 
} 

showName();

把它定义为一个名为 observe 函数,接收一个对象,遍历对象对所有属性进行数据劫持,在 get 中收集对应依赖函数,在 set 中派发执行所有依赖函数。

js
/**
 * @description: 观察对象属性值变化
 * @param {*} data 观察对象
 */
function observe(data) {
  for (const key in data) {
    let internalValue = data[key];
    let funcs = [];
    Object.defineProperty(data, key, {
      get() {
        // 收集依赖: 哪个函数在用我
        funcs.push(fn); // fn未知!
        return internalValue;
      },
      set(value) {
        internalValue = value;
        // 派发更新,执行用我的函数
        for (let i = 0; i < funcs.length; i++) {
          
          const fn = funcs[i]; 
          fn(); 
        } 
      },
    });
  }
}
/**
 * @description: 观察对象属性值变化
 * @param {*} data 观察对象
 */
function observe(data) {
  for (const key in data) {
    let internalValue = data[key];
    let funcs = [];
    Object.defineProperty(data, key, {
      get() {
        // 收集依赖: 哪个函数在用我
        funcs.push(fn); // fn未知!
        return internalValue;
      },
      set(value) {
        internalValue = value;
        // 派发更新,执行用我的函数
        for (let i = 0; i < funcs.length; i++) {
          
          const fn = funcs[i]; 
          fn(); 
        } 
      },
    });
  }
}
js
// ...
// 这部分代码抽离为公共方法
let internalValue = user.name;
Object.defineProperty(user, "name", {
  get() {
    console.log("获取 name");
    func = showName; // 收集: 相关函数
    return internalValue;
  },
  set(value) {
    console.log("设置 name 为", value);
    internalValue = value;
    func(); // 派发:执行函数
  },
});
// ...
// ...
// 这部分代码抽离为公共方法
let internalValue = user.name;
Object.defineProperty(user, "name", {
  get() {
    console.log("获取 name");
    func = showName; // 收集: 相关函数
    return internalValue;
  },
  set(value) {
    console.log("设置 name 为", value);
    internalValue = value;
    func(); // 派发:执行函数
  },
});
// ...

fn 未知 - 谁在用我?

  • 不要直接调用 showName
  • 找个地方保存,比如 window.__func = showName
js
function observe(data) {
  for (const key in data) {
    let internalValue = data[key];
    let funcs = [];
    Object.defineProperty(data, key, {
      get() {
        // 收集依赖: 哪个函数在用我
        // funcs.push(fn); // fn未知!
        const fn = window.__func; 
        if (fn && !funcs.includes(fn)) { 
          funcs.push(fn);
        }
        return internalValue;
      },
      set(value) {...},
    });
  }
}

function autoRun(fn) { 
  window.__func = fn; // 存储依赖 
  fn(); // 获取依赖并收集 
  window.__func = null; // 收集完置空 
}
function observe(data) {
  for (const key in data) {
    let internalValue = data[key];
    let funcs = [];
    Object.defineProperty(data, key, {
      get() {
        // 收集依赖: 哪个函数在用我
        // funcs.push(fn); // fn未知!
        const fn = window.__func; 
        if (fn && !funcs.includes(fn)) { 
          funcs.push(fn);
        }
        return internalValue;
      },
      set(value) {...},
    });
  }
}

function autoRun(fn) { 
  window.__func = fn; // 存储依赖 
  fn(); // 获取依赖并收集 
  window.__func = null; // 收集完置空 
}
js
// ...
windows.__func = showName;
showName();
windows.__func = null;

windows.__func = showGender;
showGender();
windows.__func = null;

windows.__func = showAge;
showAge();
windows.__func = null;

// 封装一下 autoRun(fn)
autoRun(showName);
autoRun(showGender);
autoRun(showAge);
// ...
windows.__func = showName;
showName();
windows.__func = null;

windows.__func = showGender;
showGender();
windows.__func = null;

windows.__func = showAge;
showAge();
windows.__func = null;

// 封装一下 autoRun(fn)
autoRun(showName);
autoRun(showGender);
autoRun(showAge);

demo

html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>vue-defineProperty</title>
  </head>
  <style>
    .container {
      display: flex;
      justify-content: center;
      flex-direction: column;
      margin: auto;
      align-items: center;
      height: 50vh;
    }
  </style>
  <body>
    <div class="container">
      <span class="name">姓名:</span>
      <span class="gender">性别:</span>
      <span class="age">年龄:</span>
    </div>


    <script src="./vue.js"></script>
    <script src="./index.js"></script>
  </body>
</html>
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>vue-defineProperty</title>
  </head>
  <style>
    .container {
      display: flex;
      justify-content: center;
      flex-direction: column;
      margin: auto;
      align-items: center;
      height: 50vh;
    }
  </style>
  <body>
    <div class="container">
      <span class="name">姓名:</span>
      <span class="gender">性别:</span>
      <span class="age">年龄:</span>
    </div>


    <script src="./vue.js"></script>
    <script src="./index.js"></script>
  </body>
</html>
js
let user = {
  name: "王花花",
  gender: "男",
  age: "20",
};

function showName() {
  const doc = document.querySelector(".name");
  doc.textContent = `姓名: ${user.name}`;
}

function showGender() {
  const doc = document.querySelector(".gender");
  doc.textContent = `性别: ${user.gender}`;
}

function showAge() {
  const doc = document.querySelector(".age");
  doc.textContent = `年龄: ${user.age}`;
}

observe(user);

autoRun(showName)
autoRun(showGender)
autoRun(showAge)
let user = {
  name: "王花花",
  gender: "男",
  age: "20",
};

function showName() {
  const doc = document.querySelector(".name");
  doc.textContent = `姓名: ${user.name}`;
}

function showGender() {
  const doc = document.querySelector(".gender");
  doc.textContent = `性别: ${user.gender}`;
}

function showAge() {
  const doc = document.querySelector(".age");
  doc.textContent = `年龄: ${user.age}`;
}

observe(user);

autoRun(showName)
autoRun(showGender)
autoRun(showAge)
js
/**
 * @description: 观察对象属性值变化
 * @param {*} data 观察对象
 * @return {*}
 */
function observe(data) {
  for (const key in data) {
    let internalValue = data[key];
    let funcs = [];
    Object.defineProperty(data, key, {
      get() {
        const fn = window.__func;
        if (fn && !funcs.includes(fn)) {
          funcs.push(fn);
        }
        return internalValue;
      },
      set(value) {
        internalValue = value;
        for (let i = 0; i < funcs.length; i++) {
          const fn = funcs[i];
          fn();
        }
      },
    });
  }
}
/**
 * @description: 存储依赖、首次触发收集依赖、收集完置空
 * @param {*} fn 用到数据的函数
 */
function autoRun(fn) {
  window.__func = fn; // 存储依赖
  fn(); // 获取依赖并收集
  window.__func = null; // 收集完置空
}
/**
 * @description: 观察对象属性值变化
 * @param {*} data 观察对象
 * @return {*}
 */
function observe(data) {
  for (const key in data) {
    let internalValue = data[key];
    let funcs = [];
    Object.defineProperty(data, key, {
      get() {
        const fn = window.__func;
        if (fn && !funcs.includes(fn)) {
          funcs.push(fn);
        }
        return internalValue;
      },
      set(value) {
        internalValue = value;
        for (let i = 0; i < funcs.length; i++) {
          const fn = funcs[i];
          fn();
        }
      },
    });
  }
}
/**
 * @description: 存储依赖、首次触发收集依赖、收集完置空
 * @param {*} fn 用到数据的函数
 */
function autoRun(fn) {
  window.__func = fn; // 存储依赖
  fn(); // 获取依赖并收集
  window.__func = null; // 收集完置空
}

学习视频:https://ke.qq.com/course/5892689/13883885517138513#term_id=106109971