vue的使用:

  1. new Vue(options),options中配置需要的data,computed,methods,生命周期钩子等
  2. 通过胡子语法绑定变量,通过v-bind(@)绑定methods中事件等

    1. <!-- Vue的简单使用 -->
    2. <div id="app">
    3. <div class="user">姓名:{{user.name}}</div>
    4. <input type="text" v-model="value.a">
    5. <button @click="add">点我加1</button>
    6. </div>
    7. <script>
    8. var vm = new Vue({
    9. el: '#app',
    10. data: {
    11. user: {
    12. name: '张三'
    13. },
    14. value: { a: 23 }
    15. },
    16. methods:{
    17. add(){
    18. this.value.a++
    19. }
    20. }
    21. })
    22. </script>

    本次手写实现的功能:

    1. 模板编译与渲染

  3. 主要采用document.createDocumentFragment()方法创建文档碎片节点,生成虚拟dom,编译模板。

  4. 本次未使用抽象语法树方法编译模板,想了解这部分可以参考:vue源码—手写实现AST抽象语法树
  5. 未封装h函数生成虚拟dom,未封装patch进行diff与渲染dom,想了解可参考:虚拟dom与diff算法
  6. 只通过正则表达式解析胡子语法,想了解mustache模板引擎实现可参考:手写vue的胡子语法

    2. 数据响应

  7. 编译模板、watch监听时,涉及到使用data数据的地方都需要收集依赖

  8. 注意:本次手写vue复用了之前写过的数据响应式代码。详情请参考:vue数据响应式原理

    3. 指令编译

  9. 以v-model为例

  10. 编译时获取节点的所有属性,如果是v-model=”data”,则让当前节点node.value=data,并监听其input事件,触发input时,改变this.data的值

    4. 事件监听

  11. 以@click为例

  12. 编译时获取节点的属性,如果是@click,则让当前node监听这个方法,并执行其回调

    5. 生命周期

  13. 以created为例

  14. 在初始化数据完成后,调用created的回调函数
  15. 生命周期具体执行时间,可以参考Vue官网

    实现代码

    Vue.js

  • Vue构造函数,初始化vue实例,监听watch中的属性值,执行生命周期钩子等
  • 代码中用到的observe函数与Watcher构造函数,请参考:vue数据响应式原理 中的完整代码部分

    1. import Compile from "./Compile"
    2. // 引入数据响应式的代码,可参考之前数据响应式原理的文章
    3. import { observe, Watcher } from "./initData"
    4. // vue构造函数
    5. export default class Vue {
    6. constructor(options) {
    7. this.$options = options // 存储传入的options
    8. this._data = options.data // 存储data
    9. // 数据响应式
    10. observe(this._data)
    11. // 初始数据,把data的属性绑定到this,通过this.name可访问data的name属性
    12. this._initData(this._data)
    13. // 将methods的属性绑定到this,通过this.add()可执行add方法
    14. this._initData(options.methods)
    15. // 执行creatd的回调,其他生命周期可参考vue生命周期在对应的地方执行
    16. this.$options.created.call(this)
    17. // 处理watch中监听的数据
    18. this._initWatch(options.watch)
    19. // 实例Compile,编译模板,传入参数:1.挂载点,2.vue实例
    20. new Compile(options.el, this)
    21. }
    22. _initData(data) {
    23. let self = this
    24. // 将data中的属性绑定到vue实例上
    25. Object.keys(data).forEach(key => {
    26. Object.defineProperty(self, key, {
    27. get() {
    28. return data[key]
    29. },
    30. set(val) {
    31. data[key] = val
    32. }
    33. })
    34. })
    35. }
    36. _initWatch(watch) {
    37. let self = this
    38. Object.keys(watch).forEach(key => {
    39. // 实例Watcher,收集依赖,原理参考之前数据响应式原理的文章
    40. // 参数1.vue实例,2.监听的属性值,3.回调函数
    41. new Watcher(self, key, watch[key])
    42. })
    43. }
    44. }

    Compile.js

  • Compile构造函数,用于编译模板,包括解析:v-moel,@click,{{name}}等

  • 代码中用到的parsePath函数与Watcher构造函数,请参考:vue数据响应式原理 中的完整代码部分
    1. // 引入数据响应式的代码,可参考之前数据响应式原理的文章
    2. // parsePath是通过表达式获取对象的值,比如获取obj[a.c.g]的值
    3. import { parsePath, Watcher } from "./initData"
    4. // Compile构造函数
    5. export default class Compile {
    6. constructor(el, vue) {
    7. this.$el = document.querySelector(el) // 获取挂载点的真实DOM
    8. this.$vue = vue // 存储Vue实例
    9. if (this.$el) {
    10. // 将真实DOM转换为虚拟DOM
    11. let $fragment = this.node2Fragment(this.$el)
    12. // 编译解析模板,包括解析胡子语法,指令等
    13. this.compile($fragment)
    14. // 渲染DOM
    15. this.$el.appendChild($fragment)
    16. }
    17. }
    18. node2Fragment(el) {
    19. let fragment = document.createDocumentFragment(); // 创建文本碎片
    20. let ch
    21. // 循环遍历真实dom,并添加到文本碎片中
    22. // 这里每一次appendChild,真实dom中就会少一个节点
    23. while (ch = el.firstChild) {
    24. fragment.appendChild(ch)
    25. }
    26. return fragment
    27. }
    28. compile(el) {
    29. let txtReg = /\{\{(.*?)\}\}/ // 匹配胡子语法的正则
    30. el.childNodes.forEach(ch => {
    31. if (ch.nodeType == 1) {
    32. // 如果是element节点,调用编译element的方法
    33. this.compileElement(ch)
    34. } else if (ch.nodeType == 3 && txtReg.test(ch.textContent)) {
    35. // 如果是文本节点,且文本内容中使用了胡子语法
    36. let word = ch.textContent.match(txtReg)[1] // 获取胡子中的值
    37. // 编译文本节点,参数:1.当前node,2.胡子中的值,3.正则表达式
    38. this.compileText(ch, word, txtReg)
    39. }
    40. })
    41. }
    42. compileElement(node) {
    43. // 编译属性,Array.from将类数组对象转换为数组
    44. Array.from(node.attributes).forEach(attr => {
    45. if (attr.name.indexOf('v-') == 0) {
    46. // 编译指令属性
    47. let directive = attr.name.slice(2)
    48. let exp = attr.value
    49. // 编译v-model
    50. if (directive == 'model') {
    51. let data = parsePath(exp)(this.$vue) // 获取v-model绑定的数据值
    52. node.value = data // 给输入框赋值
    53. // 监听v-model绑定的值的变化,改变时让输入框值也改变
    54. new Watcher(this.$vue, exp, newVal => {
    55. node.value = newVal
    56. })
    57. // 监听的input事件
    58. node.addEventListener('input', e => {
    59. let newVal = e.target.value
    60. this.setValue(this.$vue, exp, newVal) // 改变vue实例中对应的属性值
    61. })
    62. }
    63. }
    64. // 事件监听
    65. if (attr.name.indexOf('@') == 0) {
    66. let event = attr.name.slice(1) // 获取事件名
    67. let exp = attr.value // 获取methods中的属性值
    68. // 给当前node添加事件监听,传入vue实例中的方法,绑定this为vue实例
    69. node.addEventListener(event, this.$vue[exp].bind(this.$vue))
    70. }
    71. })
    72. // 递归,继续编译子节点的子节点
    73. this.compile(node)
    74. }
    75. // 编译文本节点
    76. compileText(node, word, txtReg) {
    77. let oldText = node.textContent // 获取文本节点的完整字符串
    78. let value = parsePath(word)(this.$vue) // 获取胡子中变量的对应值
    79. node.textContent = oldText.replace(txtReg, value) // 替换文本节点中的变量
    80. // 监听变量的变化,重新改变文本内容
    81. new Watcher(this.$vue, word, val => {
    82. node.textContent = oldText.replace(txtReg, val)
    83. })
    84. }
    85. // 给obj对象的exp表达式的属性设置新值,给obj[a.b.c]设置值
    86. setValue(obj, exp, newVal) {
    87. let arr = exp.split('.')
    88. let res = obj
    89. arr.forEach((item, i) => {
    90. if (i == arr.length - 1) {
    91. res[item] = newVal
    92. } else {
    93. res = res[item]
    94. }
    95. })
    96. }
    97. }

    index.html 测试代码

    1. ...
    2. <div id="app">
    3. <div class="user">
    4. <ul>
    5. <li>姓名:{{user.name}}</li>
    6. <li>年龄:{{user.age}}</li>
    7. <li>性别:{{user.gender}}</li>
    8. </ul>
    9. </div>
    10. <input type="text" v-model="value.a">
    11. <div>内容:{{value.a}}</div>
    12. <button @click="add">点我加1</button>
    13. </div>
    14. <!-- index.js中把Vue构造函数挂到了window上 -->
    15. <script src="index.js"></script>
    16. <script>
    17. var vm = new Vue({
    18. el: '#app',
    19. data: {
    20. user: {
    21. name: '张三',
    22. age: 18,
    23. gender: '男',
    24. },
    25. value: {
    26. a: 11
    27. }
    28. },
    29. watch: {
    30. 'user.name'(newVal, oldVal) {
    31. console.log(`watch监听:username发生改变了,新值${newVal},旧值${oldVal}`)
    32. }
    33. },
    34. created() {
    35. console.log('created:this是',this)
    36. },
    37. methods:{
    38. add(){
    39. this.value.a++
    40. }
    41. }
    42. })
    43. </script>

    效果:

    手写vue.gif