我竟然动手做了一个Vue?(标题党)

200 阅读5分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

一、引言


昨天晚上做了个噩梦,场景如下:

面试官:你来讲讲vue的响应式原理吧

我:vue2是通过Object.defineProperty来实现响应式的

面试官:没了吗?

我:没了

面试官:......

我:......

吓得的我突然从梦中惊起,我还在想我的vue知识去哪了,还好只是个梦。

三、正文


今天我们来做一个简易Vue来耍耍,在上手之前呢要了解Vue响应式的原理,那我们就要先弄清发布订阅模式以及三个家伙 ObserverDepWatcher

发布订阅模式

概念:订阅者们把自己想要订阅的事件在调度中心进行注册,当发布者在调度中心进行发布的时候,会通知所有的订阅者。

例子:你在掘金中关注某了位up,当up发布文章的时候,掘金就会把此文章推送给你。这样你就是订阅者,up就是发布者,而掘金平台就是调度中心。

这样我们就可以理解Vue响应式中的三巨头了。

Observer

发布订阅中的发布者,负责对数据进行getset重写,通知调度中心进行更新。

Dep

发布订阅中的调度中心,负责收集所有的订阅者。

Watcher

发布订阅中的订阅者,在vue初始化的时候会实例各种watcher

工作原理

  • Observer:通过递归重写data中每一个属性的gettersetter,每一个属性都会有属于自己的Dep,当get触发会将Dep上面的静态属性(watcher)加到实例dep中,当set触发时,会调用实例dep进行notify通知所有的watcher更新。
  • Dep:收集watcher,在notify方法中调用所有的watcherupdate方法。
  • Watcher:在进行实例化的时候会将自身挂载到Dep上面的静态属性target中,然后通过触发get将自身放到该属性专属的dep实例中

这里的Dep静态属性target以及属性中的getter是怎么将watcher放入dep实例中可能会比较绕,我们来用代码来实现这些操作。

四、代码

Observer
import Dep from './dep.js'
export default class Observer {
    constructor(value) {
        this.value = value
        this.walk(value)
    }
    walk(obj) {
        //循环data对象重写setter和getter
        for (const key in obj) {
            defineReactive(obj,key,obj[key])
        }
    }
}
//将传进去的对象进行改造
 function observe(value) {
    if (typeof value === 'object') { 
        return new Observer(value)
    }
}

export function defineReactive(obj, key, val) {
    const dep = new Dep()
    //递归
    observe(val)
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: () => {
            //如果Dep上面的target是有值的就将target加入dep中,这里的触发时机是在Watcher.js中
            if (Dep.subTarget) {
                dep.addSub(Dep.subTarget)
            }
            return val
        },
        set: (value) => {
            val = value
            dep.notify()
        }
    })
}

Dep

let uid = 0
export default class Dep { 
    static subTarget = null
    constructor() { 
        this.id = ++uid
        //存放watcher的数组
        this.subs = []
    }
    //新增watcher
    addSub(watcher) { 
        if (Dep.subTarget !==null) { 
            this.subs.push(watcher)
        }
    }
    //响应更新
    notify() { 
        this.subs.forEach(watcher => { 
            watcher.update()
        })
    }
}
export function addTarget(watcher) { 
    Dep.subTarget = watcher
}
export function removeTarget() {
    Dep.subTarget = null
}
Watcher
import  { addTarget, removeTarget } from './dep.js'
import { parsePath } from '../utils/lang.js'
let uid = 0
export default class Watcher { 
    //cb是传入方法,在执行update的时候会执行cb
    constructor(vm, exp, cb) {
        this.vm = vm
        this.exp = exp
        this.cb = cb    
        this.id = ++uid
        //处理传进来的字符串,有可能是xxx.aabb
        this.getter = parsePath(this.exp)
        this.get()
    }
    //触发get
    get() {
        //将自身挂载到Dep的target上
        addTarget(this)
        //重点,为了触发getter 将Dep的target加进去
        const value = this.getter(this.vm)
        //移除target
        removeTarget(this)
        return value
     }
    //更新
    update() {
        this.cb(this.getter(this.vm))
    }
}

//str可能会是xxx.aabb 所以进行处理
function parsePath(str) {
    const arr = str.split('.')
    return function (obj) {
      for (let i = 0; i < arr.length; i++) {
        if (!obj) return
          obj = obj[arr[i]]
      }
      return obj
    }
  }

四、后续工作


我们完成了响应式部分,但是我们发现并没有使用啊,接下来我们来做Vuehtml模板编译的部分。

Vue

import Observer from './core/observer/Observer.js'
import Compiler from './compiler/index.js'
export default class Vue { 
    constructor(options) { 
        this._data = options.data()
        this._methods = options.methods
        for (const key in this._data) {
            Object.defineProperty(this, key, {
                get: function () {
                    return this._data[key]
                },
                set: function (value) {
                    this._data[key] = value
                }
            })
        }
        for (const key in this._methods) {
            Object.defineProperty(this, key, {
                get: function () {
                    return this._methods[key].bind(this)
                },
                set: function (value) {
                    this._methods[key] = value.bind(this)
                }
            })
        }
        //处理数据
        new Observer(this._data)
        this.el = document.querySelector(options.el)
        //模板解析(解析html标签)
        new Compiler(this.el, this)
        //执行created方法
        options.created?.call(this)
    }
}

Compiler

import Watcher from '../core/observer/watcher.js'
import {parsePath} from '../core/utils/lang.js'
export default class Compiler { 
    constructor(dom, vm) { 
        this.vm = vm; 
        this._compileElement(dom)
    }
    _compileElement(el) { 
        let childs = Array.from(el.childNodes)
        childs.forEach(node => { 
            if (node.childNodes && node.childNodes.length > 0) {
                if (node.tagName === 'BUTTON') { 
                    this._compile(node)
                }
                this._compileElement(node)
            } else { 
                this._compile(node)
            }
        })
    }
    //根据节点的类型解析
    _compile(node) { 
        if (node.nodeType === 1) {
            this._compileAttr(node)
        } else if (node.nodeType === 3) { 
            this._compileText(node)
        }
    }
    //文本解析 获取{{}}中的key,通过key拿到值 重新赋值给textContent
    // 新建Watcher,当数据更新时更改textContent的值
    _compileText(node) { 
        let reg = /\{\{(.*)\}\}/
        let content = node.textContent
        if (reg.test(content)) { 
            let key = RegExp.$1
            node.textContent = parsePath(key)(this.vm)
                new Watcher(this.vm, key, val => {
                    node.textContent = val
            })
        }
    }
    //获取标签中v-model的key值,新建input监听,当有更改时直接更改vm中的数据
    //新增Watcher ,当数据有变动的时候更改value值 实现双向绑定
    // 获取标签中的@click,新增点击事件,点击的时候触发vm中对应的methods方法
    _compileAttr(node) { 
        let nodeAttr = Array.from(node.attributes)
        nodeAttr.forEach(attr => { 
            if (attr.name === 'v-model') {
                node.value = parsePath(attr.nodeValue)(this.vm)
                node.addEventListener('input', () => {
                    this._modifyValue(attr.nodeValue, node.value)
                })
                new Watcher(this.vm, attr.nodeValue, val => {
                    node.value = val
                })
            } else if (attr.name === '@click') { 
                node.addEventListener('click', (e) => {
                    this.vm[attr.nodeValue].call(this.vm)
                })
                
            }
        })
    }
    _modifyValue(exp,value) { 
        let arr = exp.split('.')
        let obj = this.vm
        for (let i = 0; i < arr.length; i++) {
            let current = obj[arr[i]]
            if (Object.prototype.toString.call(current) === '[object Object]') {
                obj = current
            } else { 
                obj[arr[i]] = value
            }
        }
    }
    
}

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="app">
      <div style="height:20px">{{obj.value}}</div>
      <input type="input" v-model="obj.value" />
      <button @click='setValue'>{{button}}</button>
    </div>
  </body>
</html>
<script type="module">
  import Vue from "../src/vue.js";
  const vue = new Vue({
    data() {
      return {
        button:'重置',
        obj:{
          value:'demo'
        }
      };
    },
    methods: {
      setValue() {
       this.obj.value = '666666'
      },
    },
    el: "#app",
  });
</script>

五、效果展示

QQ录屏20220107164531.gif

六、总结

        这期的内容可能代码偏多,主要需要理解发布订阅的核心思想,Object.defineProperty的用法以及一些dom的操作,在众多源码中,Vue的源码还是比较清晰易懂的,在模板解析那部分只是简单的解析标签内容,像VNode什么的都没有涉及到,以后我也会研究研究虚拟节点以及Diff的实现并且分享给大家,能够学习源码自己做出来点东西也是很快乐的,大家也要多动手多实践呀~

今天的分享就是这些啦~~

听说喜欢点赞的你,今年年终奖拿到手软😍