关于vue.js:手写-Vue-系列-之-Vue1x

40次阅读

共计 15301 个字符,预计需要花费 39 分钟才能阅读完成。

当学习成为了习惯,常识也就变成了常识。 感激各位的 关注 点赞 珍藏 评论

新视频和文章会第一工夫在微信公众号发送,欢送关注:李永宁 lyn

文章已收录到 github 仓库 liyongning/blog,欢送 Watch 和 Star。

前言

后面咱们用 12 篇文章具体解说了 Vue2 的框架源码。接下来咱们就开始手写 Vue 系列,写一个本人的 Vue 框架,用最简略的代码实现 Vue 的外围性能,进一步了解 Vue 外围原理。

为什么要手写框架

有人会有疑难:我曾经具体浏览过框架源码了,甚至不止两三遍,这难道还不够吗?我自认为对框架的源码曾经很相熟了,我感觉没必要再手写。

有没有必要手写框架 这个事件,和 有没有必要浏览框架源码 的答案一样。看你的出发点是什么。

读源码

如果你是抱以学习的态度,那不用说,浏览框架源码必定是有必要的。

大家都明确,平时的业务开发中,你身边人的程度可能都跟你差不多,所以你在业务中根本是看不到太多的优良编码和思维。

而一个框架所蕴含的优良设计和最佳实际就很多了,在浏览的时候有太多让你豁然开朗和惊艳的中央。即便你感觉本人当初段位不够,可能看不到那么多,然而源码对你的影响是耳濡目染的。看多了优良的代码,在你本人平时的编码中会不盲目的利用你学到的这些优良编码方式。更何况 Vue 的大部分代码都是尤大本人写的,代码品质那是毋庸置疑的。

手写框架

至于 手写框架是否有必要?只有你读了框架源码,就必须本人手写一个。理由很简略,你浏览框架源码的目标是学习,你说你对源码曾经十分熟了,你说你都学会了,那怎么测验?测验的形式也很简略,把你学到的东西向外输入,分三个阶段:

  1. 写技术博客、画思维导图(把握 30%)
  2. 给别人分享,比方组内分享、录视频都行(把握 60%)
  3. 手写框架,造轮子是测验你学习成绩最好的形式(把握 90%)

有没有发现前两阶段都是在讲别人的货色,你说你学到了,的确,你能向外输入,学你必定是学到了,然而学到了多少呢?我感觉差不多是 60%,举个例子:

他人问你 Vue 的响应式原理是什么?通过前两个阶段的输入,你可能说的有条有理,比方 Object.defineProperty、getter、setter、依赖收集、依赖告诉 watcher 更新等等。然而这整个过程你是否写进去呢?如果你第一次写,大概率是不行的,实现的时候会发现,基本不像你说的那么简略,要思考货色远不止你说的那些。如果不信大家能够试试,测验一下。

要晓得,造轮子的过程其实就是你利用的过程,只有你真的写进去了,你才算是真的学到了。如果只看不写,基本上能够算是进阶版的 只看不练

所以,测验你是否真的学会并深刻了解某个框架的实现原理,模拟 造轮子 是最好的测验形式。

手写 Vue1.x

在开始之前,咱们先做好筹备工作,在本人的工作目录下,新建咱们的源码目录,比方:

mkdir lyn-vue && cd lyn-vue

这里我不想额定装置和配置打包工具,太麻烦了,采纳古代浏览器原生反对的 ESM 的形式,所以大家须要在本地装一个 serve,起一个服务器。vite 就是这个原理,只不过它的服务端是本人实现的,因为它须要针对 import 的不同资源做相应的解决,比方解析 import 申请的是 node_modules 还是 用户本人的模块,亦或者是 TS 模块的转译等等。

npm i serve -g

装置好之后,在 lyn-vue 目录下执行 serve 命令,会在本地起一个服务器,接下来就进入编码阶段。

指标

上面的示例代码就是明天的指标,用咱们本人手写的 Vue 框架把这个示例跑起来。

咱们须要实现以下能力:

  • 数据响应式拦挡

    • 原始值
    • 一般对象
    • 数组
  • 数据响应式更新

    • 依赖收集,Dep
    • 依赖告诉 Watcher 更新
    • 编译器,compiler
  • methods + 事件 + 数据响应式更新
  • v-bind 指令
  • v-model 双向绑定

    • input 输入框
    • checkbox
    • select

/vue1.0.html

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <title>Lyn Vue1.0</title>
</head>

<body>
  <div id="app">
    <h3> 数据响应式更新 原理 </h3>
    <div>{{t}}</div>
    <div>{{t1}}</div>
    <div>{{arr}}</div>
    <h3>methods + 事件 + 数据响应式更新 原理 </h3>
    <div>
      <p>{{counter}}</p>
      <button v-on:click="handleAdd"> Add </button>
      <button v-on:click="handleMinus"> Minus </button>
    </div>
    <h3>v-bind</h3>
    <span v-bind:title="title"> 右键审查元素查看我的 title 属性 </span>
    <h3>v-model 原理 </h3>
    <div>
      <input type="text" v-model="inputVal" />
      <div>{{inputVal}}</div>
    </div>
    <div>
      <input type="checkbox" v-model="isChecked">
      <div>{{isChecked}}</div>
    </div>
    <div>
      <select v-model="selectValue">
        <option value="1">1</option>
        <option value="2">2</option>
        <option value="3">3</option>
      </select>
      <div>{{selectValue}}</div>
    </div>
  </div>
  <script type="module">
    import Vue from './src/index.js'
    const ins = new Vue({
      el: '#app',
      data() {
        return {
          // 原始值和对象的响应式原理
          t: 't value',
          t1: {tt1: 'tt1 value'},
          // 数组的响应式原理
          arr: [1, 2, 3],
          // 响应式更新
          counter: 0,
          // v-bind
          title: '看我',
          // v-model
          inputVal: 'test',
          isChecked: true,
          selectValue: 2
        }
      },
      // methods + 事件 + 数据响应式更新 原理
      methods: {handleAdd() {this.counter++},
        handleMinus() {this.counter--}
      },
    })
    // 数据响应式拦挡
    setTimeout(() => {console.log('********** 属性值为原始值时的 getter、setter ************')
      console.log(ins.t)
      ins.t = 'change t value'
      console.log(ins.t)
    }, 1000)

    setTimeout(() => {console.log('********** 属性的新值为对象的状况 ************')
      ins.t = {tt: 'tt value'}
      console.log(ins.t.tt)
    }, 2000)

    setTimeout(() => {console.log('********** 验证对深层属性的 getter、setter 拦挡 ************')
      ins.t1.tt1 = 'change tt1 value'
      console.log(ins.t1.tt1)
    }, 3000)

    setTimeout(() => {console.log('********** 将值为对象的属性更新为原始值 ************')
      console.log(ins.t1)
      ins.t1 = 't1 value'
      console.log(ins.t1)
    }, 4000)

    setTimeout(() => {console.log('********** 数组操作方法的拦挡 ************')
      console.log(ins.arr)
      ins.arr.push(4)
      console.log(ins.arr)
    }, 5000)
  </script>
</body>

</html>

数据响应式拦挡

Vue 构造函数

/src/index.js

/**
 * Vue 构造函数
 * @param {*} options new Vue(options) 时传递的配置对象
 */
export default function Vue(options) {this._init(options)
}

this._init

/src/index.js

/**
 * 初始化配置对象
 * @param {*} options 
 */
Vue.prototype._init = function (options) {
  // 将 options 配置挂载到 Vue 实例上
  this.$options = options
  // 初始化 options.data
  // 代理 data 对象上的各个属性到 Vue 实例
  // 给 data 对象上的各个属性设置响应式能力
  initData(this)
}

initData

/src/initData.js

/**
 * 1、初始化 options.data
 * 2、代理 data 对象上的各个属性到 Vue 实例
 * 3、给 data 对象上的各个属性设置响应式能力
 * @param {*} vm 
 */
export default function initData(vm) {
  // 获取 data 选项
  let {data} = vm.$options
  // 设置 vm._data 选项,保障它的值必定是一个对象
  if (!data) {vm._data = {}
  } else {vm._data = typeof data === 'function' ? data() : data
  }
  // 代理,将 data 对象上的的各个属性代理到 Vue 实例上,反对 通过 this.xx 的形式拜访
  for (let key in vm._data) {proxy(vm, '_data', key)
  }
  // 设置响应式
  observe(vm._data)
}

proxy

/src/utils.js

/**
 * 将 key 代理到 target 上,* 比方 代理 this._data.xx 为 this.xx
 * @param {*} target 指标对象,比方 vm
 * @param {*} sourceKey 原始 key,比方 _data
 * @param {*} key 代理的原始对象上的指定属性,比方 _data.xx
 */
export function proxy(target, sourceKey, key) {
  Object.defineProperty(target, key, {
    // target.key 的读取操作实际上返回的是 target.sourceKey.key
    get() {return target[sourceKey][key]
    },
    // target.key 的赋值操作实际上是 target.sourceKey.key = newV
    set(newV) {target[sourceKey][key] = newV
    }
  })
}

observe

/src/observe.js

/**
 * 通过 Observer 类为对象设置响应式能力
 * @returns Observer 实例
 */
export default function observe(value) {
  // 防止有限递归
  // 当 value 不是对象间接完结递归
  if (typeof value !== 'object') return

  // value.__ob__ 是 Observer 实例
  // 如果 value.__ob__ 属性曾经存在,阐明 value 对象曾经具备响应式能力,间接返回已有的响应式对象
  if (value.__ob__) return value.__ob__

  // 返回 Observer 实例
  return new Observer(value)
}

Observer

/src/observer.js

/**
 * 为一般对象或者数组设置响应式的入口 
 */
export default function Observer(value) {
  // 为对象设置 __ob__ 属性,值为 this,标识以后对象曾经是一个响应式对象了
  Object.defineProperty(value, '__ob__', {
    value: this,
    // 设置为 false,禁止被枚举,// 1、能够在递归设置数据响应式的时候跳过 __ob__ 
    // 2、将响应式对象字符串化时也不限显示 __ob__ 对象
    enumerable: false,
    writable: true,
    configurable: true
  })

  if (Array.isArray(value)) {
    // 数组响应式
    protoArgument(value)
    this.observeArray(value)
  } else {
    // 对象响应式
    this.walk(value)
  }
}

/**
 * 遍历对象的每个属性,为这些属性设置 getter、setter 拦挡
 */
Observer.prototype.walk = function (obj) {for (let key in obj) {defineReactive(obj, key, obj[key])
  }
}

// 遍历数组的每个元素,为每个元素设置响应式
// 其实这里是为了解决元素为对象的状况,以达到 this.arr[idx].xx 是响应式的目标
Observer.prototype.observeArray = function (arr) {for (let item of arr) {observe(item)
  }
}

defineReactive

/src/defineReactive.js

/**
 * 通过 Object.defineProperty 为 obj.key 设置 getter、setter 拦挡
 */
export default function defineReactive(obj, key, val) {
  // 递归调用 observe,解决 val 依然为对象的状况
  observe(val)

  Object.defineProperty(obj, key, {
    // 当发现 obj.key 的读取行为时,会被 get 拦挡
    get() {console.log(`getter: key = ${key}`)
      return val
    },
    // 当产生 obj.key = xx 的赋值行为时,会被 set 拦挡
    set(newV) {console.log(`setter: ${key} = ${newV}`)
      if (newV === val) return
      val = newV
      // 对新值进行响应式解决,这里针对的是新值为非原始值的状况,比方 val 为对象、数组
      observe(val)
    }
  })
}

protoArgument

/src/protoArgument.js

/**
 * 通过拦挡数组的七个办法来实现
 */

// 数组默认原型对象
const arrayProto = Array.prototype
// 以数组默认原型对象为原型创立一个新的对象
const arrayMethods = Object.create(arrayProto)
// 被 patch 的七个办法,通过拦挡这七个办法来实现数组响应式
// 为什么是这七个办法?因为只有这七个办法是能更改数组自身的,像 cancat 这些办法都是会返回一个新的数组,不会改变数组自身
const methodsToPatch = ['push', 'pop', 'unshift', 'shift', 'splice', 'sort', 'reverse']

// 遍历 methodsToPatch
methodsToPatch.forEach(method => {
  // 拦挡数组的七个办法,先实现本职工作,再额定实现响应式的工作
  Object.defineProperty(arrayMethods, method, {value: function(...args) {// 实现办法的本职工作,比方 this.arr.push(xx)
      const ret = arrayProto[method].apply(this, args)
      // 未来接着实现响应式相干的能力
      console.log('array reactive')
      return ret
    },
    configurable: true,
    writable: true,
    enumerable: true
  })
})

/**
 * 笼罩数组(arr)的原型对象
 * @param {*} arr 
 */
export default function protoArgument(arr) {arr.__proto__ = arrayMethods}

成果

能达到如下成果,则示意数据响应式拦挡性能实现。即能跑通指标中示例代码的“数据响应式拦挡”局部的代码(最初的那堆 setTimeout)。

动图地址: https://gitee.com/liyongning/…

数据响应式更新

当初曾经能拦挡到对数据的获取和更新,接下来就能够在拦挡数据的中央减少一些“能力”,以实现 数据响应式更新 的性能。

减少的这些能力其实就是大家耳熟能详的货色:在 getter 中进行依赖收集,setter 中依赖告诉 watcher 更新。

Vue1.x 中响应式数据对象的所有属性(key)和 dep 是一一对应对应关系,一个 key 对应一个 dep;响应式数据在页面中每援用一次就会产生一个 watcher,所以在 Vue1.0 中 dep 和 watcher 是一对多的关系。

依赖收集

Dep

/src/dep.js

/**
 * Dep
 * Vue1.0 中 key 和 Dep 是一一对应关系,举例来说:* new Vue({*   data() {
 *     return {
 *       t1: xx,
 *       t2: {
 *         tt2: xx
 *       },
 *       arr: [1, 2, 3, { t3: xx}]
 *     }
 *   }
 * })
 * data 函数 return 回来的对象是一个 dep
 * 对象中的 key => t1、t2、tt2、arr、t3 都别离对应一个 dep
 */
export default function Dep() {
  // 存储以后 dep 实例收集的所有 watcher
  this.watchers = []}

// Dep.target 是一个动态属性,值为 null 或者 watcher 实例
// 在实例化 Watcher 时进行赋值,待依赖收集实现后在 Watcher 中又从新赋值为 null
Dep.target = null

/**
 * 收集 watcher
 * 在产生读取操作时(vm.xx) && 并且 Dep.target 不为 null 时进行依赖收集
 */
Dep.prototype.depend = function () {
  // 避免 Watcher 实例被反复收集
  if (this.watchers.includes(Dep.target)) return
  // 收集 Watcher 实例
  this.watchers.push(Dep.target)
}

/**
 * dep 告诉本人收集的所有 watcher 执行更新函数
 */
Dep.prototype.notify = function () {for (let watcher of this.watchers) {watcher.update()
  }
}
Watcher

/src/watcher.js

import Dep from "./dep.js"

/**
 * @param {*} cb 回调函数,负责更新 DOM 的回调函数
 */
export default function Watcher(cb) {
  // 备份 cb 函数
  this._cb = cb
  // 赋值 Dep.target
  Dep.target = this
  // 执行 cb 函数,cb 函数中会产生 vm.xx 的属性读取,进行依赖收集
  cb()
  // 依赖收集实现,Dep.target 从新赋值为 null,避免反复收集
  Dep.target = null
}

/**
 * 响应式数据更新时,dep 告诉 watcher 执行 update 办法,* 让 update 办法执行 this._cb 函数更新 DOM
 */
Watcher.prototype.update = function () {this._cb()
}
Observer

革新 Observer 构造函数,在 value.__ob__ 对象上设置一个 dep 实例。这个 dep 是对象自身的 dep,不便在更新对象自身时应用,比方:数组依赖告诉更新时就会用到。

/src/observer.js

/**
 * 为一般对象或者数组设置响应式的入口
 */
export default function Observer(value) {
  // 为对象自身设置一个 dep,不便在更新对象自身时应用,比方 数组告诉依赖更新时就会用到
  this.dep = new Dep()  
  // ... 省略已有内容
}
defineReactive

革新 defineReactive 办法,减少依赖收集和依赖告诉更新的代码

/src/defineReactive.js

/**
 * 通过 Object.defineProperty 为 obj.key 设置 getter、setter 拦挡
 * getter 时收集依赖
 * setter 时依赖通过 watcher 更新
 */
export default function defineReactive(obj, key, val) {
  // 递归调用 observe,解决 val 依然为对象的状况
  const childOb = observe(val)

  const dep = new Dep()

  Object.defineProperty(obj, key, {
    // 当发现 obj.key 的读取行为时,会被 get 拦挡
    get() {
      // 读取数据时 && Dep.target 不为 null,则进行依赖收集
      if (Dep.target) {dep.depend()
        // 如果存在子 ob,则顺道一块儿实现依赖收集
        if (childOb) {childOb.dep.depend()
        }
      }
      console.log(`getter: key = ${key}`)
      return val
    },
    // 当产生 obj.key = xx 的赋值行为时,会被 set 拦挡
    set(newV) {console.log(`setter: ${key} = ${newV}`)
      if (newV === val) return
      val = newV
      // 对新值进行响应式解决,这里针对的是新值为非原始值的状况,比方 val 为对象、数组
      observe(val)
      // 数据更新,让 dep 告诉本人收集的所有 watcher 执行 update 办法
      dep.notify()}
  })
}
protoArgument

革新七个数组办法的 patch 补丁,当数组新增元素时,对新元素进行响应式解决和依赖告诉更新。

/src/protoArgument.js

/**
 * 通过拦挡数组的七个办法来实现
 */

// 数组默认原型对象
const arrayProto = Array.prototype
// 以数组默认原型对象为原型创立一个新的对象
const arrayMethods = Object.create(arrayProto)
// 被 patch 的七个办法,通过拦挡这七个办法来实现数组响应式
// 为什么是这七个办法?因为只有这七个办法是能更改数组自身的,像 cancat 这些办法都是会返回一个新的数组,不会改变数组自身
const methodsToPatch = ['push', 'pop', 'unshift', 'shift', 'splice', 'sort', 'reverse']

// 遍历 methodsToPatch
methodsToPatch.forEach(method => {
  // 拦挡数组的七个办法,先实现本职工作,再额定实现响应式的工作
  Object.defineProperty(arrayMethods, method, {value: function(...args) {// 实现办法的本职工作,比方 this.arr.push(xx)
      const ret = arrayProto[method].apply(this, args)
      // 未来接着实现响应式相干的能力
      console.log('array reactive')
      // 新增的元素列表
      let inserted = []
      switch(method) {
        case 'push':
        case 'unshift':
          inserted = args
          break;
        case 'splice':
          // this.arr.splice(idx, num, x, x, x)
          inserted = args.slice(2)
          break;
      }
      // 如果数组有新增的元素,则对新增的元素进行响应式解决
      inserted.length && this.__ob__.observeArray(inserted)
      // 依赖告诉更新
      this.__ob__.dep.notify()
      return ret
    },
    configurable: true,
    writable: true,
    enumerable: true
  })
})

/**
 * 笼罩数组(arr)的原型对象
 * @param {*} arr 
 */
export default function protoArgument(arr) {arr.__proto__ = arrayMethods}

到这里依赖收集就全副实现了。然而你会发现页面还是没有产生任何变动,响应式数据在页面没有失去渲染,数据更新时页面更是没有任何变动。这是为什么?还须要做什么事件吗?

其实回顾依赖收集的代码会发现有一个被咱们脱漏的中央,大家有没有发现 Watcher 构造函数仿佛素来没有被实例化过,那也就是说依赖收集其实素来没有被触发过,因为只有实例化 Watcher 时 Dep.target 才会被赋值。

那么问题就来了,Watcher 应该在什么什么时候被实例化呢?大家可能没有看过 Vue1 的源码,然而 Vue2 的源码后面带大家看过了,认真回忆一下,什么时候会去实例化 Watcher。

答案是 mountComponent,也就是挂载阶段,初始化实现后执行 &dollar;mount,$mount 调用 mountComponent,mountComponent 办法中有一步就是在实例化 Watcher。如果这块儿有忘记,大家能够再去翻看一下这部分的源码。

所以接下来咱们要实现的就是编译器了,也就是 &dollar;mount 办法。

编译器

这部分利用 DOM 操作实现了一个简版的编译器。从中你能够看到节点树的编译过程,明确文本节点、v-on:click、v-bind、v-model 指令的实现原理。

$mount

/src/index.js

Vue.prototype._init = function (options) {
  ... 省略
  
  // 如果存在 el 配置项,则调用 $mount 办法编译模版
  if (this.$options.el) {this.$mount()
  }
}

Vue.prototype.$mount = function () {mount(this)
}
mount

/src/compiler/index.js

/**
 * 编译器
 */
export default function mount(vm) {
  // 获取 el 选择器所示意的元素
  let el = document.querySelector(vm.$options.el)

  // 编译节点
  compileNode(Array.from(el.childNodes), vm)
}
compileNode

/src/compiler/compileNode.js

/**
 * 递归编译整棵节点树
 * @param {*} nodes 节点
 * @param {*} vm Vue 实例
 */
export default function compileNode(nodes, vm) {
  // 循环遍历以后节点的所有子节点
  for (let i = 0, len = nodes.length; i < len; i++) {const node = nodes[i]
    if (node.nodeType === 1) { // 元素节点
      // 编译元素上的属性节点
      compileAttribute(node, vm)
      // 递归编译子节点
      compileNode(Array.from(node.childNodes), vm)
    } else if (node.nodeType === 3 && node.textContent.match(/{{(.*)}}/)) {
      // 编译文本节点
      compileTextNode(node, vm)
    }
  }
}
compileTextNode

文本节点响应式更新的原理

/src/compiler/compileTextNode.js

/**
 * 编译文本节点
 * @param {*} node 节点
 * @param {*} vm Vue 实例
 */
export default function compileTextNode(node, vm) {// <span>{{ key}}</span>
  const key = RegExp.$1.trim()
  // 当响应式数据 key 更新时,dep 告诉 watcher 执行 update 函数,cb 会被调用
  function cb() {node.textContent = JSON.stringify(vm[key])
  }
  // 实例化 Watcher,执行 cb,触发 getter,进行依赖收集
  new Watcher(cb)
}
compileAttribute

v-on:click、v-bind 和 v-model 指令的原理

/src/compiler/compileAttribute.js

/**
 * 编译属性节点
 * @param {*} node 节点
 * @param {*} vm Vue 实例
 */
export default function compileAttribute(node, vm) {
  // 将类数组格局的属性节点转换为数组
  const attrs = Array.from(node.attributes)
  // 遍历属性数组
  for (let attr of attrs) {
    // 属性名称、属性值
    const {name, value} = attr
    if (name.match(/v-on:click/)) {
      // 编译 v-on:click 指令
      compileVOnClick(node, value, vm)
    } else if (name.match(/v-bind:(.*)/)) {
      // v-bind
      compileVBind(node, value, vm)
    } else if (name.match(/v-model/)) {
      // v-model
      compileVModel(node, value, vm)
    }
  }
}
compileVOnClick

/src/compiler/compileAttribute.js

/**
 * 编译 v-on:click 指令
 * @param {*} node 节点
 * @param {*} method 办法名
 * @param {*} vm Vue 实例
 */
function compileVOnClick(node, method, vm) {
  // 给节点增加一个 click 事件,回调函数是对应的 method
  node.addEventListener('click', function (...args) {
    // 给 method 绑定 this 上下文
    vm.$options.methods[method].apply(vm, args)
  })
}
compileVBind

/src/compiler/compileAttribute.js

/**
 * 编译 v-bind 指令
 * @param {*} node 节点
 * @param {*} attrValue 属性值
 * @param {*} vm Vue 实例
 */
function compileVBind(node, attrValue, vm) {
  // 属性名称
  const attrName = RegExp.$1
  // 移除模版中的 v-bind 属性
  node.removeAttribute(`v-bind:${attrName}`)
  // 当属性值发生变化时,从新执行回调函数
  function cb() {node.setAttribute(attrName, vm[attrValue])
  }
  // 实例化 Watcher,当属性值发生变化时,dep 告诉 watcher 执行 update 办法,cb 被执行,从新更新属性
  new Watcher(cb)
}
compileVModel

/src/compiler/compileAttribute.js

/**
 * 编译 v-model 指令
 * @param {*} node 节点 
 * @param {*} key v-model 的属性值
 * @param {*} vm Vue 实例
 */
function compileVModel(node, key, vm) {
  // 节点标签名、类型
  let {tagName, type} = node
  // 标签名转换为小写
  tagName = tagName.toLowerCase()
  if (tagName === 'input' && type === 'text') {
    // <input type="text" v-model="inputVal" />

    // 设置 input 输入框的初始值
    node.value = vm[key]
    // 给节点增加 input 事件,当事件产生时更改响应式数据
    node.addEventListener('input', function () {vm[key] = node.value
    })
  } else if (tagName === 'input' && type === 'checkbox') {
    // <input type="checkbox" v-model="isChecked" />

    // 设置抉择框的初始状态
    node.checked = vm[key]
    // 给节点增加 change 事件,当事件产生时更改响应式数据
    node.addEventListener('change', function () {vm[key] = node.checked
    })
  } else if (tagName === 'select') {
    // <select v-model="selectedValue"></select>

    // 设置下拉框初始选中的选项
    node.value = vm[key]
    // 增加 change 事件,当事件产生时更改响应式数据
    node.addEventListener('change', function () {vm[key] = node.value
    })
  }
}

总结

到这里,一个简版的 Vue1.x 就实现完了。回顾一下,咱们实现了如下性能:

  • 数据响应式拦挡

    • 一般对象
    • 数组
  • 数据响应式更新

    • 依赖收集

      • Dep
      • Watcher
    • 编译器

      • 文本节点
      • v-on:click
      • v-bind
      • v-model

指标 中示例代码的执行后果如下:

动图地址:https://p1-juejin.byteimg.com…

面试官 问:Vue1.x 数据响应式是如何实现的?

Vue 数据响应式的外围原理是 Object.defineProperty

通过递归遍历整个 data 对象,为对象中的每个 key 设置一个 getter、setter。如果 key 为数组,则走数组响应式的流程。

数组响应式是通过 Object.defineProperty 去拦挡数组的七个办法实现的。首先加强了那个七个办法,在实现办法本职工作的根底上减少了依赖告诉更新的能力,而且如果有新增数据,则新数据也会被进行响应式解决。

数据响应式更新的能力是通过数据响应式拦挡联合 Dep、Watcher、编译器来实现的。

当做完数据初始化工作当前(即响应式拦挡),就进入挂载阶段,开始编译整棵 DOM 树,编译过程中 碰到响应式数据,实例化 Watcher,这时会产生数据读取操作,触发 getter,进行依赖收集,将 Watcher 实例放到以后响应式属性对应的 dep 中。

待未来响应式数据更新时,触发 setter,而后登程 dep 告诉本人收集的所有 Watcher 实例去执行 update 办法,触发回调函数的执行,从而更新 DOM。

以上 Vue1.x 的整个响应式原理的实现。

面试官 问:你如何评估 Vue1.x 响应式原理的设计?

Vue1.x 其实是尤大为了解决本人工作上的痛点而实现的,过后他感觉各种 DOM 操作太繁琐了,初始化时须要通过 DOM 操作将数据设置到节点上,还要监听 DOM 操作,当 DOM 更新时,更新相应的数据。于是他就想着能不能把这个过程自动化,这就产生了 Vue1.x。

这么一想,Vue1.x 的实现其实就很正当了,的确达到了预期的指标。通过 Object.defineProperty 拦挡数据的读取和设置,页面首次渲染时,通过编译器编译整棵 DOM 树,给 DOM 节点设置初始值,当 DOM 节点更新时又自动更新了响应式数据,或者响应式数据更新时,通过 Watcher 自动更新对应的 DOM 节点。

这个时候的 Vue 在实现中小型 Web 零碎是没有任何问题的。而且相比于 Vue 2.x 性能会更好,因为响应式数据更新时,Watcher 能够间接更新对应的 DOM 节点,没有 2.x 的 VNode 开销和 Diff 过程。

然而大型 Web 零碎就搞不定了,理由也很简略,也是因为它的设计。因为 Vue1.x 中 Watcher 和 模版中响应式数据是 一一对应 关系,也就是说页面中每援用一次响应式数据,就会产生一个 Watcher。在大型零碎中,一个页面的数据量可能是十分大的,那就会产生大量的 Watcher,占用大量资源,导致性能降落。

所以一句话总结就是,Vue1.x 在中小型零碎中性能会很好,定向更新 DOM 节点,然而大型零碎因为 Watcher 太多,导致资源占用过多,性能降落。

于是 Vue2.x 中通过引入 VNode 和 Diff 的来解决这个问题,具体的实现原理将在下一篇文章 手写 Vue 系列之 Vue2.x 中去介绍。

预报

接下来的文章,会将本篇文章中实现的 Vue1.x 降级为 Vue2.x,引入 Vnode、diff 算法来解决 Vue1.x 的性能瓶颈。

另外会额定实现一些其它的外围原理,比方 computed、异步更新队列、child component、插槽 等。

链接

  • 配套视频,微信公众号回复:” 精通 Vue 技术栈源码原理视频版 ” 获取
  • 精通 Vue 技术栈源码原理 专栏
  • github 仓库 liyongning/Vue 欢送 Star

感激各位的:关注 点赞 珍藏 评论,咱们下期见。


当学习成为了习惯,常识也就变成了常识。 感激各位的 关注 点赞 珍藏 评论

新视频和文章会第一工夫在微信公众号发送,欢送关注:李永宁 lyn

文章已收录到 github 仓库 liyongning/blog,欢送 Watch 和 Star。

正文完
 0