源码实现

ViewModel

vue2.xOptions API 中的data函数返回一个对象实现了数据的响应式处理

  1. //vue2.x写法在源码里是一个Vue构造函数实例化传入一个options参数
  2. let vm = new Vue({
  3. el: '#app',
  4. data(){
  5. return {}
  6. }
  7. });

数据劫持其实在初始化的时候已经完成了

  1. Vue.prototype._init = function(){ initState(vm); }

问题:什么叫初始化状态initState

state是 保存的数据被定义为状态,状态的更改使视图发生更改,在 vue2 里,如computeddata, watch等配置项也归纳为state状态的一部分

问题:为什么要缓存另外的一个_data?

不希望操作它将用户编写的vm.$options.data,所以要区分开来

  1. var data = vm.$options.data;
  2. data = vm._data = typeof data === 'function'
  3. //如果用户编写的data是函数就执行该函数
  4. ? data.call(vm)
  5. //如果不是就放入用户些的data对象或者是空对象
  6. : data || {};

问题:为什么要用代理?

如果用户想要访问data下的属性,需要vm.$options.data.title/vm.$options.data().title而不是vm.title访问,不便于开发编写,所以需要通过defineProperty代理

  1. for (var key in data) {
  2. // title/classNum/total/teacher/students
  3. defineProperty(data, key, {
  4. //访问该属性的时候进行拦截并返回想要的数据
  5. get(){
  6. return vm['_data'][key];
  7. },
  8. set(newValue){
  9. vm[_data][key] = newValue;
  10. }
  11. });
  12. }

问题:为什么要观察data?

通过观察者模式不仅对data进行观察,对内部的属性也要观察,如果内部的属性是对象就要做相关的拦截,如果是数组要对数组方法的拦截

问题:为什么观察者对象/数组是一个类或构造函数Observer来处理?

因为修改的对象或数组里的属性是不确定的,有时候也会有新增内容,所以需要实例化一个Observe构造函数来写较为合适

问题:Observer构造函数内部如何实现数据观察?

通过Object.keys()遍历拿到data里所有的keyvalue,并将其通过defineProperty做下一步的响应式数据处理

问题:为什么不能对数组进行defineProperty?

因为数组本身defineProperty是不处理数组的,当然可以用一些方法去处理,但是比较麻烦,目的仅仅是为了拦截数组,那么就对数组中的方法进行重写

问题:为什么在 vue2里需要重写数组里的原生方法?

  • 需要保留原有的数组方法操作数据
  • 希望在push/unshift/splice新增数组元素时新增更多业务逻辑

因为有些数组的数据变更并不能被 vue 检测到,操作数组的一些动作,如通过索引值修改值,或者修改长度,或者是调用一些Array.prototype上的方法并不能触发这个属性的setter, splice方法也可以触发状态更新,在 vue2.x 版本将数组的 7 个方法push,pop,shift,unshift,splice,sort,reverse重写,调用包装后的数组方法就可以被 vue检测到

问题:数据劫持的目的是什么?

不希望原生对对象和数组的操作是一个纯操作,而是赋值或操作数组的过程时仍可以新增新的业务逻辑进去,像绑定视图数据,希望数据变化的过程中,视图也跟随着变化,那么就必须拦截 getter/setter 行为,在拦截的过程中,保留原来数据的操作的同时可以更改视图

问题:为什么observe函数内部要new Observer构造函数?

不能直接让程序走到观察者构造函数而是需要提前判断data是否符合是一个对象的条件,观察者构造函数的任务仅仅是观察一个对象

问题:如何将data进行数据劫持?

通过Object.definePropery劫持子属性里的对象/数组包括深度劫持

实现流程:

  1. 初始化状态initState

  2. 初始化数据initData

    1. 缓存_data并拿到里面的数据对象
    2. 代理属性访问方式
    3. 观察data内部
  3. observe:

    1. 观察data是不是对象,如果不是对象不进行观察
    2. data对象进行观察new Observer
  4. Observer:

    1. 对数组的处理:

      1. 定义observeArr(data)

        1. data的原型__proto__新增重写好的 7 个数组方法
        2. 遍历数组里的每一项
        3. 对每一项数组元素进行观察observe
      2. 定义 7 个数组方法名称的数组

      3. 定义array.js:

        1. 创建保留原有数组操作方法的一个新的对象

        2. 遍历 7 个数组方法名称,对新的对象里的属性方法进行修改(重写数组方法)

          1. 保存形参的参数类数组列表
          2. 通过slice将类数组转为新的数组
          3. 执行原数组的所有方法并传入新的形参参数列表数组
          4. push/unshift/splice方法的参数改为保存的形参列表
          5. 给新增的数组参数数据劫持拦截observeArr
          6. 新增数组元素时更多业务逻辑
    2. 对对象的处理:

      1. Observer.prototype.walk:对data对象进行definePropery
      2. 遍历拿到所有的属性key和属性值value
      3. defineReactiveData:

        1. defineProperty对属性进行 getter/setter拦截
        2. setter内部设置值的时候,newValue不知道是否对象,如果是也要进行数据劫持拦截
        3. observe:递归深度拦截
        4. 此时 getter/setter 内部可以新增新的业务代码如视图更新等

源码地址:

https://gitee.com/kevinleeeee/data-hijacked-vue2.x-demo


案例:源码实现 vue2.x

  • 功能 1:observe监听器/数据劫持/数据响应式/代理
  • 功能 2:页面渲染
  • 功能 3:编译文本/元素
  • 功能 4:依赖收集实现读写数据时重新更新组件和渲染页面
  • 功能 5:批量异步更新策略
  • 功能 6:数组依赖收集
  • 功能 7:watch实现
  • 功能 8:计算属性 computed实现
  1. //项目目录
  2. ├─package.json
  3. ├─Readme.md
  4. ├─webpack.config.js
  5. ├─src
  6. | index.js
  7. ├─source
  8. | ├─vue
  9. | | ├─index.js - Vue构造函数/初始化状态
  10. | | ├─utils.js - 编译文本/元素/去空格/拿到data对象key属性的值
  11. | | ├─observe
  12. | | | ├─array.js - 观察数组函数/原数组方法保留
  13. | | | ├─dep.js - 收集watcher/订阅/发布/定义管理stackwatcher静态方法
  14. | | | ├─index.js - 观察data/访问属性代理
  15. | | | ├─observer.js - 观察类/观察数组和对象/定义响应式
  16. | | | watcher.js - 收集deps/depsId/管理stack里的watcher/更新组件和渲染/添加依赖方法
  17. ├─public
  18. | index.html

5.Vue2源码 - 图1

功能 1 实现步骤:

  1. 编写 Vue构造函数,挂载 option到实例,且初始化数据状态
  2. 挂载 data到实例并改写 data副本,代理并修改访问属性,对 data数据进行观察
  3. 定义观察类函数,处理对象劫持和数组劫持
  4. 定义响应式方法并对对象和数组数据进行新增 getter/setter
  5. 定义观察数组函数在数组原型上保留原数组的操作方法并新增业务逻辑接口
  6. 对嵌套的对象和数组进行观察和响应式处理

问题:如何渲染页面?

通过 watcher实例化后执行更新组件函数,组件函数内部执行实例原型上的更新函数,根据 dom节点渲染组件实现页面渲染

功能 2 实现步骤:

  1. 定义 Watcher类实现页面渲染,初始化 watcher
  2. 页面挂载时实例化 watcher,传入实例和更新组件函数
  3. 定义更新函数,根据用户数据渲染组件
  4. 将用户定义模板 dom节点保存到文档碎片里
  5. 将文档碎片插入到 el里实现页面渲染

功能 3 实现步骤:

  1. 在文档碎片插入 el之前定义一个编译函数
  2. 处理文本节点,拿到 key值替换 html文本节点内容
  3. 处理元素节点

问题:如何将 data定义的属性数据替换 html里定义的标签属性?

通过正则替换文本节点内容(node.textContext), 拿到 key以后再去获取 data数据里 key的值并作为被替换的文本的内容

问题:如何将vm['person.name']写法改为vm['person']['name']写法实现嵌套属性访问?

通过 reduce整理

  1. //获取data数据里对应key属性的值
  2. export function getValue(exp, vm) {
  3. //console.log(exp);
  4. //person.name
  5. //问题:明显vm['person.name']这样的写法是无法访问属性值
  6. //实际访问写法应该是vm['person']['name']
  7. //解决:通过reduce方法完成
  8. //1.以 . 作为分隔符分割字符串
  9. let keys = exp.split('.');
  10. //console.log(keys);
  11. //['person', 'name']
  12. //2.整理写法
  13. return keys.reduce((prev, next) => {
  14. prev = prev[next];
  15. return prev;
  16. }, vm);
  17. }

功能 4 实现步骤:

  1. 创建 Dep类(data里每个属性对应一个实例化的 dep,每个 dep对应一个唯一的 id)
  2. 利用发布订阅模式收集订阅者,定义订阅方法和发布方法
  3. 定义一个保存当前 watcher的函数
  4. 定义一个删除栈里当前的 watcher的函数
  5. watcher类的 get()里使用入栈和出栈的 dep函数
  6. 在定义响应式函数里的 getter新增执行订阅动作的逻辑(读取时会更新组件和渲染页面)
  7. 在定义响应式函数里的 setter新增执行发布动作的逻辑(改写数据时会再次更新组件和渲染页面)
  8. dep类定义 depend方法
  9. watcher类里定义 addDep方法,定义一个 deps容器,depsId容器

关于 Dep类(依赖):

  • id属性
  • 有收集订阅者的数组容器(存放 watcher)
  • 有发布 notify方法(遍历每一个 watcher并执行底下的 update方法)
  • 有订阅 addSub方法(将每一个 watcher加入到容器里)
  • depend方法(将 watcher存入 dep中,然后把 dep也存入 watcher中 多对多)

关于 Dep静态属性和方法:

  • stack栈数组容器(存放 watcher)
  • target属性
  • pushTarget方法(将 watcher赋值给 target,并加入 stack栈里)
  • popTarget方法(删除 stack里的 watchertarget指向 stack里的前一位 watcher)

关于 Wachcer类:

  • id属性
  • 有收集依赖的数组容器(存放 dep)
  • depsIdset容器(存放 depId)
  • addDep方法(存放 depId, 将 dep加入到依赖容器, 执行 dep.addSub 方法)
  • update方法(执行 get方法)
  • get方法(执行 pushTarget方法,执行更新组件函数, 执行 popTarget方法)

5.Vue2源码 - 图2

执行顺序:

  1. defineReactive方法执行(from observer)
  2. 实例化 dep依赖
  3. defineProperty拦截
  4. 当用户读取 data对象属性时执行 get方法
  5. watcher存在时执行 dep.depend 方法
  6. 执行该项 watcher里的 addDep方法
  7. 存放 depId, 将 dep加入到依赖容器, 执行 dep.addSub 方法
  8. 将每一个 watcher加入到容器里
  9. 当用户修改 data对象属性时执行 set方法
  10. 执行 dep.notify 方法
  11. 遍历每一个 watcher 并执行底下的 update方法
  12. 执行 pushTarget方法,
  13. watcher赋值给 target,并加入 stack栈里
  14. 执行更新组件函数
  15. 执行 popTarget方法
  16. 删除 stack里的 watchertarget指向 stack里的前一位 watcher

功能 5 实现步骤:

  1. watcher里定义队列管理函数,将 watcher添加到队列里,执行 nextTick函数延迟清空队列
  2. 执行 nextTick函数时传入 flushQueue清空队列函数(遍历队列所有 watcher并依次执行组件更新)
  3. nextTick函数里将用户写的回调函数存入回调函数队列,将包裹着 flushCallBacks的回调函数 作为参数 传入 4 种异步的方法里
  1. setTimeout(()=>{
  2. vm.message = 'Hi!';
  3. vm.message = 'Hey!';
  4. vm.message = 'Bye!';
  5. },3000);

问题:如何只渲染一次拿到最后赋值的结果?

通过批量更新页面,避免重复渲染

  1. //让flushCallBacks异步执行的几种方法
  2. //看浏览器是否兼容异步方法
  3. //方法一:Promise
  4. if (Promise) {
  5. return Promise.resolve().then(timerFunction);
  6. }
  7. //方法二:html5 API
  8. if (MutationObserver) {
  9. let observe = new MutationObserver(timerFunction);
  10. //假如有文本节点
  11. let textNode = document.createTextNode(10);
  12. //监听textNode变化
  13. //characterData变化证明文本节点发生变化
  14. observe.observe(textNode, {
  15. characterData: true
  16. });
  17. textNode.textContent = 20;
  18. return;
  19. }
  20. //方法三:类似setTimeout 性能优于setTimeout 老版本浏览器不兼容
  21. if (setImmediate) {
  22. return setImmediate(timerFunction);
  23. }
  24. //方法四:
  25. setTimeout(timerFunction, 0);

功能 6 实现步骤:

  1. Observernew了一个 dep实例
  2. Observer类里的 data数据多定义一个属性__ob__,get时返回实例,实现数组访问时可以拿到 Observer实例
  3. defineReactive函数里保存一个执行observe(value)时返回的实例,在 get访问时通过访问实例下 dep底下的依赖收集方法进行数组的依赖收集
  4. 这样数组相关的 watcher放入 dep里面,一旦数组发生变化,通知 watcher进行重新的渲染
  5. 定义 dependArray函数实现嵌套数组的依赖收集

问题:没有做数组依赖的会发生什么情况?

当 data 对象里面的数组被修改时,虽然修改成功,但是组件没有更新,页面也没有重新渲染

功能 7 实现步骤:

  1. initState函数里有定义 options.watch 配置项,且里面有 initWatch函数
  2. 定义 initWatch函数: 打印vm.$options.watch可以拿到包含 message的对象{message: ƒ}
  3. 循环对象里的每一项键值对,将该项属性的值(事件处理函数)保存为变量 handler
  4. 定义 createWatcher方法,接收参数 vm,key,handler
  5. createWatcher方法返回挂载在 vm实例 $watch方法的结果
  6. 在原型上的$watch方法内部实例化new Watcher(vm,expr,handler,{配置项});
  7. 定义this.getter = function(){return getValue(watch属性名,实例)},该函数返回的结果是 watch里的属性的属性值是一个事件处理函数执行后的结果
  8. Watcher类里 get方法内部 getter方法执行的 value返回出去赋值给实例的this.value,此操作在每次实例化 Watcher时拿到旧的值
  9. 当数据被修改时触发的发布的 watcher执行
  10. watcher.update 方法执行,触发 get方法拿到新的值
  11. 然后当新老值不一样的时候执行用户传入的回调函数
  12. 返回用户想要的新老数据

问题:watch定义方式是怎么样的?

  1. watch: {
  2. //如何监听message的变化?
  3. message: function(newValue, oldValue){
  4. console.log(newValue, oldValue);
  5. }
  6. }

功能 7 实现步骤:

  1. 定义 initComputed方法时创建 watcher实例
  2. 定义 watchers,let watchers = vm._watcherComputed = Object.create(null);
  3. 实例传入参数vm, userDef, () => {}, {lazy: true}
  4. lazy:true配置为了首次实例化 watcher时不去取值
  5. 将实例的结果赋值给带有用户定义属性名的对象里watchers[key]
  6. defineProperty劫持属性,get时定义函数 createComputedGetter执行
  7. createComputedGetter执行时返回watcher.value
  8. 此时 computed实现了

问题:当更新属性发生变化时如何处理?

源码地址:

https://gitee.com/kevinleeeee/vue2.x-source-demo

data属性

底层实现对 data里变量的读取/修改

  1. //实现通过实例对象vm访问data里面的变量,可以访问和修改
  2. var vm = new Vue({
  3. data() {
  4. return {
  5. a: 1,
  6. b: 2
  7. }
  8. }
  9. });
  10. function Vue(options) {
  11. //vue在创建实例的过程中调用data函数,返回数据对象
  12. this.$data = options.data();
  13. var _this = this;
  14. //希望访问的方式:this.a => this.$data.a
  15. for (var key in this.$data) {
  16. //独立作用域
  17. //k是当前作用域的临时局部变量
  18. (function (k) {
  19. //写法一:到IE8存在不兼容
  20. //代理方式修改访问/修改的方式
  21. //_this访问当前k
  22. Object.defineProperty(_this, k, {
  23. get: function () {
  24. return _this.$data[k];
  25. },
  26. set: function (newValue) {
  27. _this.$data[k] = newValue;
  28. }
  29. })
  30. //写法二:兼容性好,Mozilla的API
  31. //实例继承过来的方法
  32. //__defineGetter__(访问属性,回调函数)
  33. _this.__defineGetter__(k, function () {
  34. return _this.$data[k];
  35. });
  36. //__defineSetter__(访问属性,回调函数)
  37. _this.__defineSetter__(k, function (newValue) {
  38. _this.$data[k] = newValue;
  39. });
  40. })(key);
  41. }
  42. }
  43. console.log(vm);
  44. /**
  45. * Vue {...}
  46. * $data: {a: 1, b: 2}
  47. * a: 1
  48. * b: 2
  49. * get a: ƒ ()
  50. * set a: ƒ (newValue)
  51. */

methods属性

实例方法挂载的实现

  1. //实例方法挂载的实现
  2. var Vue = (function () {
  3. function Vue(options) {
  4. //每次实例化Vue执行data返回一个唯一的data对象防止指向同一个引用值
  5. this.$data = options.data();
  6. //挂载到实例上
  7. this._methods = options.methods;
  8. //传入实例本身
  9. this._init(this);
  10. }
  11. /**
  12. * 初始化实例对象里面的属性和方法
  13. * @param {*} vm 该组件实例
  14. */
  15. Vue.prototype._init = function (vm) {
  16. initData(vm);
  17. initMethods(vm);
  18. }
  19. //直接越过$data访问属性
  20. function initData(vm) {
  21. for (var key in vm.$data) {
  22. //代理每一个属性
  23. (function (k) {
  24. Object.defineProperty(vm, k, {
  25. get: function () {
  26. return vm.$data[key];
  27. },
  28. set: function (newValue) {
  29. vm.$data[key] = newValue;
  30. }
  31. });
  32. })(key);
  33. }
  34. }
  35. function initMethods(vm) {
  36. //把自定义的方法挂载到vm实例对象里
  37. for (var key in vm._methods) {
  38. vm[key] = vm._methods[key];
  39. }
  40. }
  41. return Vue;
  42. })();
  43. var vm = new Vue({
  44. data() {
  45. return {
  46. a: 1,
  47. b: 2
  48. }
  49. },
  50. methods: {
  51. increaseA(num) {
  52. this.a += num;
  53. },
  54. increaseB(num) {
  55. this.b += num;
  56. },
  57. getTotal() {
  58. console.log(this.a + this.b);
  59. }
  60. }
  61. });
  62. vm.increaseA(1);
  63. vm.increaseA(1);
  64. vm.increaseA(1);
  65. vm.increaseA(1);
  66. vm.increaseB(2);
  67. vm.increaseB(2);
  68. vm.increaseB(2);
  69. vm.increaseB(2);
  70. vm.getTotal();
  71. console.log(vm);

computed属性

实现一个 computed

  1. var Vue = (function () {
  2. //匹配{{}}
  3. var reg_var = /\{\{(.+?)\}\}/g;
  4. /**
  5. * 私有数据computedData
  6. * 容器保存computed对象里方法集合的函数本体和依赖
  7. * 该对象结构为:
  8. * dep: 依赖(就是实例里data里的属性) 数组存放的是data数据里的key
  9. *
  10. * computedData:
  11. * {total: {value: 3, dep: ["a", "b"], get: ƒ}}
  12. *
  13. * total = {
  14. * value: computed里get函数执行返回的结果
  15. * get: get函数本体
  16. * dep: ['a', 'b']
  17. * }
  18. */
  19. var computedData = {};
  20. /**
  21. *
  22. * 每一个属性都有对应的dom节点,属性改变时节点也会更新
  23. */
  24. var dataPool = {};
  25. var Vue = function (options) {
  26. this.$el = document.querySelector(options.el);
  27. this.$data = options.data();
  28. this._init(this, options.computed, options.template);
  29. }
  30. /**
  31. * 初始化实例
  32. * @param {object} vm 实例对象
  33. * @param {object} computed 计算方法集合对象
  34. * @param {string} template 字符串模板
  35. */
  36. Vue.prototype._init = function (vm, computed, template) {
  37. dataReactive(vm);
  38. computedReactive(vm, computed);
  39. render(vm, template);
  40. }
  41. /**
  42. * 将data数据应式处理
  43. * @param {object} vm 实例对象
  44. */
  45. function dataReactive(vm) {
  46. var _data = vm.$data;
  47. //枚举data数据属性
  48. for (var key in _data) {
  49. (function (k) {
  50. //劫持数据达到直接访问/修改作用
  51. Object.defineProperty(vm, k, {
  52. //vm访问k时得到
  53. get: function () {
  54. return _data[k];
  55. },
  56. //vm访问k时设置
  57. set: function (newValue) {
  58. _data[k] = newValue;
  59. //更新数据
  60. updata(vm, k);
  61. //更新计算数据
  62. _updateComputedData(vm, k, function (k) {
  63. updata(vm, k);
  64. });
  65. }
  66. });
  67. })(key);
  68. }
  69. }
  70. /**
  71. * 将computedData数据响应式处理
  72. * 数据劫持访问到value属性里的数据
  73. * @param {object} vm 实例对象
  74. * @param {object} computed 计算方法集合对象
  75. */
  76. function computedReactive(vm, computed) {
  77. _initComputedData(vm, computed);
  78. //使用computedData
  79. //computedData: {total: {value: 3, dep: ["a", "b"], get: ƒ}}
  80. for (var key in computedData) {
  81. (function (k) {
  82. Object.defineProperty(vm, k, {
  83. //vm访问k时得到
  84. get: function () {
  85. //value保存的是该方法执行后的结果
  86. return computedData[k].value;
  87. },
  88. //vm访问k时设置
  89. set: function (newValue) {
  90. //将开发者用户修改后的新的数据重新赋值更新
  91. computedData[k].value = newValue;
  92. }
  93. });
  94. })(key);
  95. }
  96. }
  97. /**
  98. * 渲染页面
  99. * @param {object} vm 实例对象
  100. * @param {string} template 字符串模板
  101. */
  102. function render(vm, template) {
  103. var container = document.createElement('div');
  104. var _el = vm.$el;
  105. container.innerHTML = template;
  106. var domTree = _compileTemplate(vm, container);
  107. _el.appendChild(domTree);
  108. }
  109. /**
  110. * 编译模板
  111. * @param {object} vm 实例对象
  112. * @param {HTMLDivElement} container 带有模板的div元素包装器
  113. * @return {HTMLDivElement} container 替换好模板内容的div元素
  114. */
  115. function _compileTemplate(vm, container) {
  116. //找到所有节点
  117. var allNodes = container.getElementsByTagName('*');
  118. var nodeItem = null;
  119. // console.log(allNodes);
  120. //HTMLCollection(5) [span, span, span, span, span]
  121. //枚举每个节点
  122. for (var i = 0; i < allNodes.length; i++) {
  123. nodeItem = allNodes[i];
  124. //匹配{{}}
  125. var matched = nodeItem.textContent.match(reg_var);
  126. // console.log(matched);
  127. //["{{a}}"]/null/["{{b}}"]/null/["{{total}}"]
  128. if (matched) {
  129. nodeItem.textContent = nodeItem.textContent.replace(reg_var, function (node, key) {
  130. //console.log(node); {{a}}/{{b}}/{{total}}
  131. //console.log(key); a/b/total
  132. //每一个属性都有对应的dom节点,属性改变时节点也会更新
  133. dataPool[key.trim()] = nodeItem;
  134. // console.log(dataPool);
  135. //{a: span, b: span, total: span}
  136. // console.log(vm[key.trim()]); 1/2/3
  137. //返回替换实例键名对应的值
  138. return vm[key.trim()];
  139. });
  140. }
  141. }
  142. // console.log(container);
  143. //被data数据替换{{变量}}好的模板
  144. return container;
  145. }
  146. /**
  147. * 初始化ComputedData容器的内部函数
  148. * @param {object} vm 实例对象
  149. * @param {object} computed 计算方法集合
  150. */
  151. function _initComputedData(vm, computed) {
  152. //枚举computed计算方法集合里的所有方法名
  153. for (var key in computed) {
  154. //试着打印描述符
  155. // console.log(Object.getOwnPropertyDescriptor(computed, key));
  156. //注意:value保存的是当前方法函数本身,可以拿到执行
  157. //{writable: true, enumerable: true, configurable: true, value: ƒ}
  158. var descriptor = Object.getOwnPropertyDescriptor(computed, key);
  159. //如果有get 拿get 没有则拿value
  160. var descriptorFn = descriptor.value.get ? descriptor.value.get : descriptor.value;
  161. // console.log(key); total
  162. //初始化totol = {}
  163. computedData[key] = {};
  164. //descriptorFn()执行后的结果存入computedData对象里的computedData.value属性里
  165. //改变指向是因为total函数内部有用this
  166. computedData[key].value = descriptorFn.call(vm);
  167. // console.log(computedData); 函数执行后的结果 {total: 3}
  168. //将第二个属性get保存到total对象里
  169. computedData[key].get = descriptorFn.bind(vm);
  170. //将第三个属性dep依赖保存到total对象里
  171. computedData[key].dep = _collectDep(descriptorFn);
  172. // console.log(computedData);
  173. //computedData: {total: {value: 3, dep: ["a", "b"], get: ƒ}}
  174. }
  175. }
  176. /**
  177. * 专门收集依赖函数
  178. * 匹配函数内部有哪些依赖
  179. * 匹配规则:this.字段任意字符出现1次或多次非贪婪
  180. * @param {function} fn computed方法集合里方法的函数本身
  181. * @return {array} 返回一个存放函数本身里实例对象data的变量集合的数组
  182. */
  183. function _collectDep(fn) {
  184. //转为字符串再正则匹配
  185. var _collection = fn.toString().match(/this.(.+?)/g);
  186. // console.log(_collection);
  187. //["this.a", "this.b"]
  188. if (_collection.length > 0) {
  189. for (var i = 0; i < _collection.length; i++) {
  190. //去掉前面的this
  191. _collection[i] = _collection[i].split('.')[1];
  192. }
  193. // console.log(_collection);
  194. //["a", "b"]
  195. return _collection;
  196. }
  197. }
  198. /**
  199. * 更新修改数据信息
  200. * @param {object} vm 实例对象
  201. * @param {*} key 枚举data数据属性的key
  202. */
  203. function updata(vm, key) {
  204. dataPool[key].textContent = vm[key];
  205. }
  206. /**
  207. * 更新计算数据
  208. * @param {object} vm 实例对象
  209. * @param {*} key 枚举data数据属性的key
  210. * @param {function} updata 回调函数
  211. */
  212. function _updateComputedData(vm, key, updata) {
  213. //初始化第一批的依赖数据
  214. var _dep = null;
  215. //computedData: {total: {value: 3, dep: ["a", "b"], get: ƒ}}
  216. for (var _key in computedData) {
  217. // console.log(_key); total
  218. _dep = computedData[_key].dep;
  219. // console.log(_dep); ["a", "b"]
  220. for (var i = 0; i < _dep.length; i++) {
  221. //如果键名一致证明是修改该数据
  222. if (_dep[i] === key) {
  223. //重新执行第一批依赖的get方法
  224. //vm[_key] => vm.total
  225. vm[_key] = computedData[_key].get();
  226. //更新变量
  227. updata(_key);
  228. }
  229. }
  230. }
  231. }
  232. return Vue;
  233. })();
  234. //使用
  235. var vm = new Vue({
  236. el: '#app',
  237. template: `
  238. <span>{{a}}</span>
  239. <span>+</span>
  240. <span>{{b}}</span>
  241. <span> = </span>
  242. <span>{{total}}</span>
  243. `,
  244. data() {
  245. return {
  246. a: 1,
  247. b: 2
  248. }
  249. },
  250. computed: {
  251. total() {
  252. console.log('computed total');
  253. return this.a + this.b;
  254. }
  255. }
  256. });
  257. console.log(vm);
  258. console.log(vm.total);
  259. console.log(vm.total);
  260. console.log(vm.total);
  261. vm.a = 100;
  262. vm.b = 200;
  263. console.log(vm.total);
  264. console.log(vm.total);
  265. console.log(vm.total);

watch属性

案例:驱动实现

技术:

webpack + vue + es6

实现功能:

  • computed实现
  • watch实现
  • 实现响应式与暴露回调接口
  • data/computed/watch 驱动

源码地址:

https://gitee.com/kevinleeeee/vue-drivers-demo

v-if/v-show

简单实现一个vue2.x版本的v-if/v-show/@click

原理:

通过找到注释节点占位<!-- v-if -->,找到父节点appendChild()进去或替换

  1. /**
  2. * 原理:
  3. * 合理利用数据保存视图相关的信息
  4. * 通过数据与视图绑定在一起
  5. * update的时候可以很好的操作数据
  6. * 对事件处理函数循环绑定
  7. * 如何处理v-if删除节点/恢复节点(注释节点占位)
  8. *
  9. * 把template模板转变为dom节点,将dom里的数据和dom绑定在一起,当数据更新的时候,更新节点
  10. * 用Map{ dom: {}}来实现 dom键名为对象
  11. * showPool数据结构
  12. * showPool = [
  13. * [
  14. * dom,
  15. * {
  16. * type: if/show,
  17. * prop: data
  18. * }
  19. * ]
  20. * ]
  21. *
  22. * eventPool数据结构
  23. * eventPool = [
  24. * [
  25. * dom,
  26. * handler
  27. * ]
  28. * ]
  29. */

问题:如何实现v-if/v-show?

  1. 数据代理实现访问data数据
  2. 数据劫持
  3. 初始化 dom 数据使v-if/v-show/@click和 dom 绑定在一起
  4. 初始化视图,根据data数据先初始化时候显示视图组件
  5. 根据事件池去做时间处理函数的循环绑定
  6. 改变数据的同时改变 dom 视图

问题:为什么初始化 dom 时绑定v-if/v-show/@click?

在执行methods对象里的方法时才能找到视图相应的节点去更改它的视图

问题:vue在初始化 dom 时做了什么操作?

  1. 转化为 AST 树
  2. 转化为虚拟节点
  3. 转化为真实节点
  4. 将数据和真实节点保存在一起

问题:此轮子中,如何将v-if/v-show/@click和视图绑定在一起?

定义池子保存当前的节点和v-if/v-show/@click属性

  1. /**
  2. * showPool: [
  3. * [
  4. * dom,
  5. * {
  6. * //如果是if则需要增删节点
  7. * type: if / show,
  8. * //如果是show则需要显示或隐藏
  9. * show: true / false,
  10. * data: 绑定的数据
  11. * }
  12. * ]
  13. * ]
  14. */
  15. /**
  16. * console.log(this.showPool);
  17. * Map(4) {
  18. * div.box.box1 => {
  19. * key: div.box.box1,
  20. * value: {type: 'if', show: false, data: 'boxShow1'}
  21. * },
  22. * div.box.box2 => {…},
  23. * div.box.box3 => {…},
  24. * div.box.box4 => {…}
  25. * }
  26. */
  1. /**
  2. * eventPool: [
  3. * [
  4. * dom,
  5. * handler
  6. * ]
  7. * ]
  8. */
  9. /**
  10. * console.log(this.eventPool);
  11. * Map(4) {
  12. * {
  13. * key: button,
  14. * value: ƒ showBox1()
  15. * },
  16. * button => ƒ,
  17. * button => ƒ,
  18. * button => ƒ
  19. * }
  20. */

问题:当遇到v-if节点时,如何删除了之后恢复时保证位置不变?

通过新增一个注释节点替换被删除的节点从而实现占位

源码地址:https://gitee.com/kevinleeeee/vue2-vif-vshow-resource-demo

样式绑定

实现 style/class 样式绑定

解决:

标签属性的解析,并关联 data数据里的属性

技术:

es6

源码地址:

https://gitee.com/kevinleeeee/vue-class-style-demo

模板编译

案例:实现模板编译

技术:rollup/es5/AST 树/数据响应式

  1. //项目目录
  2. ├─index.html
  3. ├─package-lock.json
  4. ├─package.json
  5. ├─rollup.config.js - 配置rollup
  6. ├─src
  7. | ├─index.js
  8. | ├─init.js - 初始化响应式数据/挂载vm/挂载组件/挂载render函数
  9. | ├─lifecycle.js - 管理组件挂载/所有的生命周期函数/进行补丁替换
  10. | ├─state.js - 初始化响应式数据/获取所有数据并代理数据
  11. | ├─vdom
  12. | | ├─index.js - 管理Vue原型上的所有render函数和内部代码的Vue原型上的方法(_c/_v/_s)
  13. | | ├─patch.js - 负责创建元素和文本节点的虚拟节点
  14. | | vnode.js - 根据虚拟节点再创建真实的dom节点(包括属性更新)/新旧补丁的替换方法
  15. | ├─observer
  16. | | ├─array.js 对数组进行原型上的数组操作功能补全
  17. | | ├─index.js - 观察数据
  18. | | Observer.js - 对观察的数据进行处理/定义响应式数据
  19. | ├─compiler
  20. | | ├─astParser.js - 专门正则规则解析html到和组装AST结构树
  21. | | ├─generate.js - 生成新的AST树结构并进行render函数内部代码的格式组装
  22. | | index.js - html模板转化AST树/将AST树返回的code代码创建新的render函数
  23. ├─dist
  24. | ├─umd
  25. | | ├─vue.js
  26. | | vue.js.map

vue2.x 基于 options API 的写法

  1. let options = {...};
  2. let vm = new Vue(options);
  1. //如何拿到模板template?
  2. //优先级(没有找到模板的情况):render函数 > template > el(html)
  3. <body>
  4. <!--查找顺序: 3.html -->
  5. <div id="app" style="color: red; font-size: 20px;">
  6. hello {{name}}
  7. <h1>{{name}}</h1>
  8. <ul>
  9. <li style="color: green;">{{age}}</li>
  10. <li>{{info.job}}</li>
  11. </ul>
  12. </div>
  13. <script>
  14. //模拟用户填写的 optionsAPI
  15. let vm = new Vue({
  16. el: '#app',
  17. //查找顺序: 2.template模板
  18. template: ``,
  19. //查找顺序: 1.render函数
  20. //createElement 函数方法
  21. render(createElement) {...},
  22. data() {
  23. return {...}
  24. }
  25. });
  26. </script>
  27. </body>

编译过程:

  1. 拿到 template
  2. 编译

    1. 将 template 转换到 AST 树
    2. AST 形成了以后转化为 render 函数(一系列的字符串方法解析)
    3. render 函数写完后转换为虚拟 DOM 节点
    4. 设置 PATCH 补丁,对比新旧节点打补丁
  3. 形成真实 DOM

源码实现过程:

  1. 从 index.html 拿到 html 模板
  2. 执行初始化文件(初始化响应式数据),获取 el 元素<div id="app"</div>
  3. 将 el 作为模板传入编译和生产 render 函数(compileToRenderFunction(el))
  4. 编译函数先进行 html 转化为 AST 树结构的对象
  5. 编译函数然后将 AST 树结构的对象组装成生产 render 的函数的内部代码(code)
  6. 将拼装好的 render 函数挂载到vm.$options
  7. 在 Vue 原型上完善 render 函数内部的方法(_v()/_s()/_c())
  8. 执行 render 函数后生产出虚拟节点 vnode
  9. 通过打补丁的方式将虚拟节点创建成真实节点 dom
  10. 并对真实节点 DOM 的属性进行更新
  11. 最终新的虚拟节点替换老的真实节点
  12. 实现视图的内容更新

关于 AST(Abstract syntax tree):

AST 树 - 抽象语法树

是源代码的抽象语法结构的树状描述

虚拟 DOM(描述 DOM 节点)和 AST 树的区别:

  • 当虚拟 DOM 变成真实 DOM 的时候,当把补丁打到真实 DOM 的时候,可以自定义一些内容
  • AST 树是对源代码层面上的一种树结构的数据结构化
  1. //希望的AST树写法
  2. //模拟html dom树型结构
  3. //注意:v-for v-model等不能存在于虚拟dom节点里,不便于浏览器识别
  4. //解决方法:将AST树形成以后,对AST进行优化,把多余的vue内置的语法糖属性(v-开头)全部解析成功能,从而让浏览器识别简化后的dom树
  5. {
  6. tag: 'div',
  7. //元素节点为1
  8. type: 1,
  9. attrs: [
  10. { name: 'id', value: 'app' },
  11. { name: 'style', value: { color: 'red', font-size: '20px' } }
  12. ],
  13. children: [
  14. { type: 3, text: 'hello' }
  15. ]
  16. }

如何通过正则匹配模板中的内容?

  1. /**
  2. * 正则规则
  3. * 来源于vue/src/compiler/parser/html-parser.js
  4. */
  5. //匹配格式:id="app"/id='app'/id=app
  6. const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
  7. //匹配格式:标签名 div/span.../ <my-header>
  8. const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`;
  9. //匹配格式:特殊的 标签格式 <my:header>
  10. const qnameCapture = `((?:${ncname}\\:)?${ncname})`;
  11. //匹配格式:<div
  12. const startTagOpen = new RegExp(`^<${qnameCapture}`);
  13. //匹配格式:> 或者是 />
  14. const startTagClose = /^\s*(\/?)>/;
  15. //匹配格式:</div>
  16. const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`);
  1. //模板
  2. <div id="app" style="color: red; font-size: 20px;">
  3. hello {{name}}
  4. <h1>{{name}}</h1>
  5. <ul>
  6. <li style="color: green;">{{age}}</li>
  7. <li>{{info.job}}</li>
  8. </ul>
  9. </div>
  10. 如何匹配?
  11. 1.先匹配 <div
  12. 2.删除 <div
  13. 3.匹配 id="app"/id='app'/id=app
  14. 4.删除 id="app"
  15. 5.匹配 style="color: red; font-size: 20px;"
  16. 6.将属性解析成对象存储
  17. -> attrs: [{name:'style', value: {color: 'red', font-size: '20px'}}]
  18. 7.删除 style="color: red; font-size: 20px;"
  19. 8.继续匹配直到匹配到 > 说明结束
  20. 9.删除 >
  1. //组装AST树
  2. function createASTElement(tagName, attrs) {
  3. return {
  4. tag: tagName,
  5. //元素节点
  6. type: 1,
  7. children: [],
  8. attrs,
  9. //根据父节点才能拿出结构
  10. parent
  11. }
  12. }

如何 AST 树转成 render 函数?

  1. //利用generate生成函数 根据AST树数据生成 字符串代码
  2. //通过以下3个函数进行字符串拼接成需要的字符串代码:
  3. //_c()是负责创建元素节点的函数
  4. //_v()是负责创建文本节点的函数
  5. //_s()是负责将{{name}}转化为真实数据_s(name)
  6. /**
  7. <div id="app" style="color: red; font-size: 20px;">
  8. hello {{name}}
  9. <span class="text" style="color:green">{{age}}</span>
  10. </div>
  11. */
  12. function vrender() {
  13. return `
  14. _c(
  15. "div",
  16. {
  17. id: "app",
  18. style: {
  19. "color": "red",
  20. "font-size": "20px"
  21. }
  22. },
  23. _v("hello" +_s(name)),
  24. _c(
  25. "span",
  26. {
  27. "class": "text",
  28. "style": {
  29. "color": "green"
  30. }
  31. },
  32. _v(_s(age))
  33. )
  34. )
  35. `
  36. }
  1. //根据AST树数据生成 字符串代码
  2. function generate(el) {
  3. /**
  4. * console.log(el);
  5. * {
  6. * tag: "div",
  7. * type: 1,
  8. * attrs: (2) [{…}, {…}],
  9. * children: (2) [{…}, {…}],
  10. * parent: Window
  11. * }
  12. */
  13. /**
  14. * 写法:
  15. * _c(元素, 属性对象{})
  16. */
  17. //处理children里面的属性对象
  18. let children = getChildren(el);
  19. //
  20. let code = `
  21. _c('${el.tag}',${el.attrs.length > 0 ?
  22. `${formatProps(el.attrs)}` :
  23. 'undefined'
  24. }${children ? `,${children}` : ''}
  25. )
  26. `;
  27. // console.log(code);
  28. return code;
  29. }

AST 形成了以后转化为 render 函数

  1. const ast = parseHtmlToAST(html),
  2. //根据AST树数据生成 字符串代码
  3. code = generate(ast),
  4. /**
  5. * console.log(code);
  6. * _c('div',{id:"app"style:{"color":" red"," font-size":" 20px"},_v("hello "+_s(name)+" 欢迎光临"),
  7. _c('span',{class:"text"style:{"color":"green"},_v(_s(age))
  8. )
  9. ,
  10. _c('p',undefined,_v("hello vue")
  11. )
  12. )
  13. */
  14. //生成render函数
  15. //with(obj)相当于将obj写法 省略this写法
  16. //var obj ={a: 1, b: 2}
  17. //with(obj){ console.log(a, b, a + b); } 1 2 3
  18. //将新实例的函数内部的作用域this抛出
  19. //这样,可以实现外部访问code里面的属性时不用写this.name/this.age..
  20. render = new Function(`with(this){ return ${code} }`);
  21. /**
  22. * console.log(render);
  23. * ƒ anonymous() {
  24. * with(this){ return
  25. * _c('div',{id:"app",style:{"color":" red"," font-size":" 20px"}},_v("hello "+_s(name)+" 欢迎光临"),
  26. * _c('span',{class:"text",style:{"color":"green"}},_v(_s(age))
  27. * …
  28. * }
  29. */
  30. }

问题:render 函数如何转化为虚拟节点?

  1. //管理所有的render函数
  2. //传入Vue 是所有render函数在该构造函数原型上进行扩展
  3. function renderMixin(Vue) {
  4. //针对vnode的render函数
  5. Vue.prototype._render = function () {
  6. const vm = this,
  7. //拿到AST形成后转出的render函数(字符串代码)
  8. render = vm.$options.render,
  9. //执行后变成vnode节点
  10. vnode = render.call(vm);
  11. //此时出来了虚拟节点
  12. /**
  13. * console.log(vnode);
  14. * {
  15. * tag: "div",
  16. * props: {id: 'app', style: {…}},
  17. * children: (3) [{…}, {…}, {…}],
  18. * text: undefined
  19. * }
  20. */
  21. return vnode;
  22. }
  23. //负责处理创建元素节点
  24. Vue.prototype._c = function () {
  25. return createElement(...arguments);
  26. }
  27. //负责处理{{}}里面的变量字符
  28. Vue.prototype._s = function (value) {
  29. if (value === null) return;
  30. return typeof value === 'object' ? JSON.stringify(value) : value;
  31. }
  32. //负责处理创建文本节点
  33. Vue.prototype._v = function (text) {
  34. return createTextVnode(text);
  35. }
  36. }

问题:如何设置补丁并打入真实的 DOM 里面?

  1. /**
  2. * 打补丁函数patch(oldNode, vNode)
  3. * @param {*} oldNode 指视图html已经写好的模板节点
  4. * @param {*} vNode AST生成的虚拟节点
  5. */
  6. function patch(oldNode, vNode) {
  7. /**
  8. * console.log(vNode);
  9. * {
  10. * el: div#app,
  11. * tag: "div",
  12. * text: undefined,
  13. * children: (3) [{…}, {…}, {…}],
  14. * props: {id: 'app', style: {…}}
  15. * }
  16. */
  17. let el = createElement(vNode),
  18. parentElement = oldNode.parentNode;
  19. //把el放到oldNode的后边
  20. //放到<script>的上方
  21. parentElement.insertBefore(el, oldNode.nextSibling);
  22. //移除旧的节点
  23. parentElement.removeChild(oldNode);
  24. }

补充以及配置:

工具:rollup 打包工具

专门打包 JS 代码

  1. //rollup脚本
  2. //-c -> config
  3. //-w -> watch
  4. "scripts": {
  5. "dev": "rollup -c -w"
  6. }
  1. //关于rollup-plugin-commonjs
  2. 实现引入文件省略.js后缀
  1. //配置文件rollup.config.js
  2. import babel from 'rollup-plugin-babel';
  3. import serve from 'rollup-plugin-serve';
  4. import commonjs from 'rollup-plugin-commonjs';
  5. export default {
  6. input: './src/index.js',
  7. output: {
  8. format: 'umd',
  9. name: 'Vue',
  10. file: 'dist/umd/vue.js',
  11. sourcemap: true
  12. },
  13. plugins: [
  14. babel({
  15. exclude: 'node_modules/**'
  16. }),
  17. serve({
  18. open: true,
  19. port: 8080,
  20. contentBase: '',
  21. openPage: '/index.html'
  22. }),
  23. commonjs
  24. ]
  25. }

源码地址:

https://gitee.com/kevinleeeee/compile-template-driver-vue2.x-demo

加载器

tpl-loader

案例:手写 tpl-loader 分离模板组件

xxx.vue 是单文件应用组件,把<template>的视图文件/<style>样式文件提取分离成单个文件管理,剩下逻辑<script>代码在 xxx.vue文件里单独管理,实现代码精简,易于阅读,方便调试

  1. //项目目录
  2. ├─package-lock.json
  3. ├─package.json
  4. ├─webpack.config.js
  5. ├─src
  6. | ├─main.js - app入口文件
  7. | ├─components
  8. | | ├─MyTitle
  9. | | | ├─index.js - 组件入口文件
  10. | | | ├─MyTitle.scss - 组件样式
  11. | | | MyTitle.tpl - 组件模板
  12. ├─public
  13. | index.html
  14. ├─loaders
  15. | ├─tpl-loader
  16. | | index.js - 自己手写的加载器代码
  1. //webpack.config.js
  2. module.exports = {
  3. ...,
  4. resolveLoader: {
  5. //通过这个配置找到tpl-loader依赖目录
  6. //合并node_modules和自己定义的tpl-loade目录
  7. modules: [
  8. 'node_modules',
  9. resolve(__dirname, './loaders')
  10. ]
  11. },
  12. module: {
  13. rules: [
  14. //定义tpl文件使用自己写的tpl-loader加载器
  15. {
  16. test: /\.tpl$/,
  17. loader:'tpl-loader'
  18. },
  19. ]
  20. }
  1. //tpl-loader其实是一个函数
  2. //手写的tpl-loader
  3. //commonJS规范
  4. function tplLoader(source) {
  5. // console.log(source);
  6. // 拿到的是组件入口文件里引入的tpl模板文件字符串代码
  7. //<h1 @click="handleTitleClick($event)">{{title}}</h1>
  8. //<h2 @click="handleTitleClick($event)">{{subTitle}}</h2>
  9. //其实组件里入口文件导出的是一个方法,且方法里传入一个组件
  10. //所以这里会返回的也是一个方法 (组件)=>{}
  11. //内部也会返回一个组件
  12. //这里会将template属性和内容新增至组件对象里
  13. return `
  14. export default (component) => {
  15. component.template = \`${source}\`;
  16. return component;
  17. };
  18. `;
  19. }
  20. module.exports = tplLoader;
  1. //在APP入口文件引入组件MyTitle
  2. //打印组件发现自己写的tpl-loade加载器把template属性和内容都添加进组件对象里
  3. import MyTitle from "./components/MyTitle";
  4. /**
  5. * console.log(MyTitle);
  6. * {
  7. * data: ƒ data(),
  8. * methods: {handleTitleClick: ƒ},
  9. * template: "<h1 @click=\"handleTitleClick($event)\">{{title}}</h1>\n<h2 @click=..."
  10. * }
  11. */

源码地址:

https://gitee.com/kevinleeeee/vue-tpl-loader-demo