Vue响应式原理

Vue 使用 Object.defineProperty 定义 setter/getter 函数对数据进行劫持,实现数据的响应式。在 getter 函数中进行依赖收集,在 setter 函数中触发更新操作

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8" />
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  6. <meta http-equiv="X-UA-Compatible" content="ie=edge" />
  7. <title>Document</title>
  8. </head>
  9. <body>
  10. <div id="app">
  11. <p>你好,<span id="name"></span></p>
  12. </div>
  13. <script>
  14. var obj = {};
  15. Object.defineProperty(obj, "name", {
  16. get() {
  17. console.log("获取name");
  18. return document.querySelector("#name").innerHTML;
  19. },
  20. set(nick) {
  21. console.log("设置name");
  22. document.querySelector("#name").innerHTML = nick;
  23. }
  24. });
  25. obj.name = "Jerry";
  26. console.log(obj.name);
  27. </script>
  28. </body>
  29. </html>

Vue工作机制(简化版)

手写Vue源码 - 图1

Vue源码实现(简化版)

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="utf-8">
  5. <meta http-equiv="X-UA-Compatible" content="IE=edge">
  6. <title>Page Title</title>
  7. <meta name="viewport" content="width=device-width, initial-scale=1">
  8. </head>
  9. <body>
  10. <div>
  11. {{name}}
  12. </div>
  13. <script>
  14. // 递归遍历,使传递进来的对象响应化
  15. function observe(obj) {
  16. // 判断类型
  17. if (!obj || typeof obj !== 'object') {
  18. return
  19. }
  20. Object.keys(obj).forEach(key => {
  21. defineReactive(obj, key, obj[key])
  22. })
  23. }
  24. function defineReactive(obj, key, val) {
  25. // 递归子属性
  26. observe(val)
  27. let dp = new Dep()
  28. Object.defineProperty(obj, key, {
  29. enumerable: true,
  30. configurable: true,
  31. get: function reactiveGetter() {
  32. console.log('get value')
  33. // 将Dep.target指向的Watcher实例加入到Dep中
  34. if (Dep.target) {
  35. dp.addSub(Dep.target)
  36. }
  37. return val
  38. },
  39. set: function reactiveSetter(newVal) {
  40. console.log('change value')
  41. val = newVal
  42. // 执行 watcher 的 update 方法
  43. dp.notify()
  44. }
  45. })
  46. }
  47. // 通过 Dep类 解耦属性的依赖收集和更新操作(Dep 类是一个简单的观察者模式的实现)
  48. class Dep {
  49. constructor() {
  50. this.subs = []
  51. }
  52. // 添加依赖
  53. addSub(sub) {
  54. this.subs.push(sub)
  55. }
  56. // 更新
  57. notify() {
  58. this.subs.forEach(sub => {
  59. sub.update()
  60. })
  61. }
  62. }
  63. // 全局属性,通过该属性配置 Watcher
  64. Dep.target = null
  65. class Watcher {
  66. constructor(obj, key, cb) {
  67. this.obj = obj
  68. this.key = key
  69. this.cb = cb
  70. Dep.target = this // 将 Dep.target 指向自己
  71. obj[key] // 读一次key触发getter
  72. Dep.target = null // 最后将 Dep.target 置空
  73. }
  74. update() {
  75. // 获得新值
  76. this.value = this.obj[this.key]
  77. // 调用 update 方法更新 DOM
  78. this.cb(this.value)
  79. }
  80. }
  81. // 组件初始化的时候执行 observe 方法
  82. var data = { name: 'yck' }
  83. observe(data)
  84. // 编译器工作:解析模板,收集依赖,创建 Watcher 和 update 函数(这里的 update 函数会在 Watcher 执 行自身的 update 函数时被调用,从而更新 DOM)
  85. // 完整的编译器再执行 更新DOM 操作之前,会有一个 patch 过程,该过程会执行 diff算法, 进行虚拟DOM的比对,然后再更新 DOM
  86. function update(value) {
  87. document.querySelector('div').innerText = value
  88. }
  89. // 模拟解析到 `{{ name }}` 触发的操作
  90. let watch = new Watcher(data, 'name', update)
  91. // update Dom innerText
  92. data.name = 'yyy'
  93. </script>
  94. </body>
  95. </html>

Vue工作机制

手写Vue源码 - 图2

  • 在 new Vue() 之后。 Vue 会调用 init 函数进行初始化,其中最重要的是通过 Object.defineProperty 设置 setter 与 getter 函数,实现数据的响应式。
  • 初始化之后调用 $mount 挂载组件,进行编译操作,对 template 进行解析,编译可以分成parse、optimize 与 generate 三个阶段,最终得到 render function string:
    • parse:生成 AST(抽象语法树)
    • optimize:优化,判断标签中是否绑定了数据等
    • generate:AST —> renderStr(渲染函数字符串),通过 new Function(renderStr) 最终得到 render function
  • 执行 render 方法生成虚拟DOM,在虚拟 DOM 映射到真实 DOM 之前有一个 patch 过程,该过程会执行 diff 算法进行优化,减少重复的操作。最后生成真实 DOM。

diff 算法

diff 算法可以比对出两颗树的「差异」,我们来看一下,假设我们现在有如下两颗树,它们分别是新老 VNode 节点,这时候到了 patch 的过程,我们需要将他们进行比对,diff 算法是通过同层的树节点进行比较而非对树进行逐层搜索遍历的方式,所以时间复杂度只有 O(n),是一种相当高效的算法,如下图。

手写Vue源码 - 图3

这张图中的相同颜色的方块中的节点会进行比对,比对得到「差异」后将这些「差异」更新到视图上。因为只进行同层级的比对,所以十分高效。

  1. function patch (oldVnode, vnode, parentElm) {
  2. if (!oldVnode) {
  3. addVnodes(parentElm, null, vnode, 0, vnode.length - 1);
  4. } else if (!vnode) {
  5. removeVnodes(parentElm, oldVnode, 0, oldVnode.length - 1);
  6. } else {
  7. if (sameVnode(oldVNode, vnode)) {
  8. patchVnode(oldVNode, vnode);
  9. } else {
  10. removeVnodes(parentElm, oldVnode, 0, oldVnode.length - 1);
  11. addVnodes(parentElm, null, vnode, 0, vnode.length - 1);
  12. }
  13. }
  14. }

首先在 oldVnode(老 VNode 节点)不存在的时候,相当于新的 VNode 替代原本没有的节点,所以直接用 addVnodes 将这些节点批量添加到 parentElm 上。
然后同理,在 vnode(新 VNode 节点)不存在的时候,相当于要把老的节点删除,所以直接使用 removeVnodes 进行批量的节点删除即可。
最后一种情况,当 oldVNode 与 vnode 都存在的时候,需要判断它们是否属于 sameVnode(相同的节点)。如果是则进行patchVnode(比对 VNode )操作,否则删除老节点,增加新节点。

Vue 源码实现

index.html

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8" />
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  6. <meta http-equiv="X-UA-Compatible" content="ie=edge" />
  7. <title>Document</title>
  8. </head>
  9. <body>
  10. <div id="app">
  11. <p>{{ name }}</p>
  12. <p k-text="name"></p>
  13. <p>{{ age }}</p>
  14. <p>
  15. {{ doubleAge }}
  16. </p>
  17. <input type="text" k-model="name" />
  18. <button @click="changeName">测试</button>
  19. <div k-html="html"></div>
  20. </div>
  21. <script src="./compile.js"></script>
  22. <script src="./kvue.js"></script>
  23. <script>
  24. const vm = new KVue({
  25. el: "#app",
  26. data: {
  27. name: "I am test.",
  28. age: 12,
  29. html: "<button>这是一个按钮</button>"
  30. },
  31. created () {
  32. console.log("开始啦");
  33. setTimeout(() => {
  34. this.name = "我是测试";
  35. }, 1500);
  36. },
  37. methods: {
  38. changeName () {
  39. this.name = "哈喽,开课吧";
  40. this.age = 1;
  41. }
  42. }
  43. });
  44. </script>
  45. </body>
  46. </html>

kvue.js

  1. class KVue {
  2. constructor(options) {
  3. this.$options = options;
  4. this.$data = options.data;
  5. // 响应化
  6. this.observe(this.$data);
  7. // 创建编译器
  8. new Compile(options.el, this);
  9. // 执行 created 函数
  10. if (options.created) {
  11. options.created.call(this);
  12. }
  13. }
  14. // 递归遍历,使传递进来的对象响应化
  15. observe(value) {
  16. if (!value || typeof value !== "object") {
  17. return;
  18. }
  19. // 遍历
  20. Object.keys(value).forEach(key => {
  21. // 对 key 做响应式处理
  22. this.defineReactive(value, key, value[key]);
  23. this.proxyData(key);
  24. });
  25. }
  26. defineReactive(obj, key, val) {
  27. // 递归
  28. this.observe(val);
  29. // 创建Dep实例:Dep 和 key 一对一对应
  30. const dep = new Dep();
  31. // 给obj定义属性
  32. Object.defineProperty(obj, key, {
  33. get() {
  34. // 将 Dep.target 指向的 Watcher 实例加入到 Dep 中
  35. Dep.target && dep.addDep(Dep.target);
  36. return val;
  37. },
  38. set(newVal) {
  39. if (newVal !== val) {
  40. val = newVal;
  41. dep.notify();
  42. }
  43. }
  44. });
  45. }
  46. // 在 Vue 实例上定义属性,代理data中的数据
  47. proxyData(key) {
  48. Object.defineProperty(this, key, {
  49. get() {
  50. return this.$data[key];
  51. },
  52. set(newVal) {
  53. this.$data[key] = newVal;
  54. }
  55. });
  56. }
  57. }
  58. // Dep:管理若干 Watcher 实例,它和key一对一关系
  59. class Dep {
  60. constructor() {
  61. this.deps = [];
  62. }
  63. addDep(watcher) {
  64. this.deps.push(watcher);
  65. }
  66. notify() {
  67. this.deps.forEach(watcher => watcher.update());
  68. }
  69. }
  70. // 保存 ui 中依赖,通过 update 函数可以更新
  71. class Watcher {
  72. constructor(vm, key, cb) {
  73. this.vm = vm;
  74. this.key = key;
  75. this.cb = cb;
  76. // 将当前实例指向Dep.target
  77. Dep.target = this;
  78. this.vm[this.key]; // 读一次key触发getter
  79. Dep.target = null;
  80. }
  81. update() {
  82. this.cb.call(this.vm, this.vm[this.key]);
  83. // console.log(`${this.key}属性更新了`);
  84. }
  85. }

compile.js

  1. // 遍历模板,处理插值表达式
  2. // 另外如果发现k-xx, @xx做特别处理
  3. class Compile {
  4. constructor(el, vm) {
  5. this.$vm = vm;
  6. this.$el = document.querySelector(el);
  7. if (this.$el) {
  8. // 1.$el中的内容 暂时存放 到一个fragment,提高操作效率
  9. this.$fragment = this.node2Fragment(this.$el);
  10. // 2.编译 fragment
  11. this.compile(this.$fragment);
  12. // 3.将编译结果插入到 挂载元素 下面
  13. this.$el.appendChild(this.$fragment);
  14. }
  15. }
  16. // 遍历el,把里面的内容 暂时存放 到新创建 fragment 中,这个操作会删除页面的 DOM 节点
  17. node2Fragment(el) {
  18. const fragment = document.createDocumentFragment();
  19. let child;
  20. while ((child = el.firstChild)) {
  21. // appenChild 是移动操作
  22. fragment.appendChild(child);
  23. }
  24. return fragment;
  25. }
  26. // 编译模板,替换动态值,处理指令和事件
  27. compile(el) {
  28. // 遍历el
  29. const childNodes = el.childNodes;
  30. Array.from(childNodes).forEach(node => {
  31. if (this.isElement(node)) {
  32. // console.log("编译元素:" + node.nodeName);
  33. // 如果是元素节点,我们要处理指令k-xx,事件@xx
  34. this.compileElement(node);
  35. } else if (this.isInterpolation(node)) {
  36. // console.log("编译文本:" + node.textContent);
  37. this.compileText(node);
  38. }
  39. // 递归子元素
  40. if (node.childNodes && node.childNodes.length > 0) {
  41. this.compile(node);
  42. }
  43. });
  44. }
  45. // 判断是否是元素
  46. isElement(node) {
  47. return node.nodeType === 1;
  48. }
  49. // 判断是否是插值表达式判断
  50. isInterpolation(node) {
  51. // 需要满足{{ xx }}
  52. return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent);
  53. }
  54. compileElement(node) {
  55. // 查看node的特性中是否有k-xx,@xx
  56. const nodeAttrs = node.attributes;
  57. Array.from(nodeAttrs).forEach(attr => {
  58. // 获取属性名称和值 k-text="abc"
  59. const attrName = attr.name; // k-text
  60. const exp = attr.value; // abc
  61. // 指令:k-xx
  62. if (attrName.indexOf("k-") === 0) {
  63. const dir = attrName.substring(2); // text
  64. // 执行指令
  65. this[dir] && this[dir](node, this.$vm, exp);
  66. } else if(attrName.indexOf('@') === 0) {
  67. // 事件 @click="handlClick"
  68. const eventName = attrName.substring(1); // click
  69. this.eventHandler(node, this.$vm, exp, eventName);
  70. }
  71. });
  72. }
  73. // 处理 k-model 双向数据绑定指令
  74. model(node, vm, exp) {
  75. // update是数据变了改界面
  76. this.update(node, vm, exp, "model");
  77. // 界面变了改数值
  78. node.addEventListener("input", e => {
  79. vm[exp] = e.target.value;
  80. });
  81. }
  82. modelUpdator(node, value) {
  83. node.value = value;
  84. }
  85. // 处理 k-html 指令
  86. html(node, vm, exp) {
  87. this.update(node, vm, exp, "html");
  88. }
  89. htmlUpdator(node, value) {
  90. node.innerHTML = value;
  91. }
  92. // 处理 @ 指令
  93. eventHandler(node, vm, exp, eventName){
  94. // 获取回调函数
  95. const fn = vm.$options.methods && vm.$options.methods[exp];
  96. if(eventName && fn) {
  97. node.addEventListener(eventName, fn.bind(vm))
  98. }
  99. }
  100. // 把插值表达式替换为实际内容
  101. compileText(node) {
  102. console.log(node)
  103. // {{xxx}}
  104. // RegExp.$1是匹配分组部分
  105. // console.log(RegExp.$1);
  106. const exp = RegExp.$1;
  107. this.update(node, this.$vm, exp, "text");
  108. }
  109. // 处理 k-text 指令
  110. text(node, vm, exp) {
  111. this.update(node, vm, exp, "text");
  112. }
  113. textUpdator(node, value) {
  114. node.textContent = value;
  115. }
  116. // 编写update函数,它可复用
  117. // exp是表达式, dir是具体操作:text,html,model
  118. update(node, vm, exp, dir) {
  119. const fn = this[dir + "Updator"];
  120. fn && fn(node, vm[exp]);
  121. // 创建Watcher
  122. // new Vue({
  123. // data: {
  124. // xxx: 'bla'
  125. // }
  126. // })
  127. // exp 就是 xxx
  128. new Watcher(vm, exp, function() {
  129. fn && fn(node, vm[exp]);
  130. });
  131. }
  132. }