Vue底层原理-基本功能实现-双向数据绑定原理-手写Vue基础版

本篇讲述手写一个简单版的Vue,后面会有双向数据绑定的原理

结 尾 部 分 放 上 源 码 \color{skyblue}{结尾部分放上源码}
(本篇用到较多ES6语法,对ES6不熟悉可以参考阮一峰ES6讲解
准备工作

创建一个空白页面index.html,做基本初始化工作

<body>
	<div id="app">
		
	</div>
	<script src="./src/compile.js"></script>
	<script src="./src/observer.js"></script>
	<script src="./src/watcher.js"></script>
	<script src="./src/myVue.js"></script>
	<script>
	    const vm = new myVue({
     
     
	        el: "#app",
	        data: {
     
     
	
	        },
	        methods: {
     
     
	            
	        }
	    })
	</script>
</body>

index.html同级目录创建一个src文件夹存放js文件,在src下面创建四个js文件
compile.jsobserver.jswatcher.jsmyVue.js(每个文件用来做什么后面会有说明)

m y V u e . j s 用 于 定 义 响 应 式 框 架 的 构 造 函 数 ( 创 建 m y V u e 的 实 例 ) \color{#ff0000}{myVue.js用于定义响应式框架的构造函数(创建myVue的实例)} myVue.js(myVue)

// 定义一个类 用于创建vue实例
class myVue {
    
    
    constructor(options = {
    
    }) {
    
    
    	//options是在页面new myVue({})时传入的这个对象,options = {}表示不传默认为空对象
        //给vue实例增加属性,将传过来的值挂载到myVue上,(在vue中习惯给属性名前面加上$符号)
        this.$el = options.el
        this.$data = options.data
        this.$methods = options.methods
        //如果指定了el参数,对el进行解析
        if (this.$el) {
    
    
            //compile负责解析模板的内容
            //需要:模板和数据,把整个vue实例传过去
            new Compile(this.$el, this)
        }
    }
}

在页面中new myVue()的时候调用这个类

c o m p i l e . j s 负 责 解 析 模 板 里 的 内 容 ( 解 析 e l 里 面 的 指 令 和 事 件 绑 定 ) \color{#ff0000}{compile.js负责解析模板里的内容(解析el里面的指令和事件绑定)} compile.js(el)

编译解析过程在内存中进行 用到了 文档片段 fragment
// 方便后面打印数据在控制台 直接log()
let {
    
    
    log,
    dir
} = console
//负责解析模板内容
class Compile {
    
    
    //参数1:模板容器 myVue中传过来的this.$el 
    //参数2:vue实例 myVue中传过来的this
    constructor(el, vm) {
    
    
        //el:new vue传递的选择器 #app
        //下面写法表示 el 的参数是一个选择器的字符串,或者也可以是一个DOM对象
        this.el = typeof el === 'string' ? document.querySelector(el) : el
        //vm:new的myVue实例
        this.vm = vm
        //编译模板
        if (this.el) {
    
    
            //1.把el中所有子节点都放入到内存中,fragment
            let fragment = this.node2fragment(this.el)
            // log(fragment)
            //2.在内存中编译fragment
            this.compile(fragment)
            //3.把fragment一次性的添加到页面
            this.el.appendChild(fragment)
        }
    }
    //核心方法
    node2fragment(node) {
    
    
        let fragment = document.createDocumentFragment()
        //把el中所有的子节点添加到文档碎片中
        let childNodes = node.childNodes
        this.toArray(childNodes).forEach(node => {
    
    
            //把所有的子节点添加到fragment中
            fragment.appendChild(node)
        })
        return fragment
    }
    /**
     * 编译文档碎片(内存中进行)
     * 
     * @param {*} fragment
     */
    compile(fragment) {
    
    
        let childNodes = fragment.childNodes
        this.toArray(childNodes).forEach(node => {
    
    
            //编译子节点
            // log(node)
            //如果是元素,需要解析指令
            if (this.isElementNode(node)) {
    
    
                //如果是元素,需要解析指令
                this.compileElement(node)
            }
            //如果是文本节点,需要解析插值表达式
            if (this.isTextNode(node)) {
    
    
                //如果是文本节点,需要解析插值表达式
                this.compileText(node)
            }
            //如果当前节点还有子节点,需要递归的解析
            if (node.childNodes && node.childNodes.length > 0) {
    
    
                this.compile(node)
            }
        })
    }
    //解析html标签
    compileElement(node) {
    
    
        // log('需要解析html')
        //1.获取当前节点下所有的属性
        let attributes = node.attributes
        // log(attributes)
        this.toArray(attributes).forEach(attr => {
    
    
            //2.解析vue的指令(所有以v-开头的属性)
            // log(attr)
            let attrName = attr.name
            if (this.isDirective(attrName)) {
    
    
                let type = attrName.slice(2)
                let expr = attr.value
                // log(type)
                if (this.isEventDirective(type)) {
    
    
                    compileUtil['eventHandler'](node, this.vm, type, expr)
                } else {
    
    
                    compileUtil[type] && compileUtil[type](node, this.vm, expr)
                }
            }
        })
    }
    //解析文本节点
    compileText(node) {
    
    
        compileUtil.mustache(node, this.vm)
    }
    //工具方法
    //转数组
    toArray(likeArray) {
    
    
        return [].slice.call(likeArray)
    }
    //是元素节点
    isElementNode(node) {
    
    
        //nodeType:节点的类型  如果是1:元素节点   3:文本节点
        return node.nodeType === 1
    }
   	//是文本节点
    isTextNode(node) {
    
    
        return node.nodeType === 3
    }
    //解析v-开头的指令
    isDirective(attrName) {
    
    
        return attrName.startsWith('v-')
    }
    //是否为v-on: 判断是否为事件绑定的指令
    isEventDirective(attrName) {
    
    
        return attrName.split(':')[0] === 'on'
    }
}
let compileUtil = {
    
    
    //解析插值表达式复杂数据类型
    mustache(node, vm) {
    
    
        // log('需要解析文本')
        let txt = node.textContent
        //用()给正则表达式做分组,方便下面获取到插值表达式里面的变量
        let reg = /\{\{(.+)\}\}/
        if (reg.test(txt)) {
    
    
            // log(txt) //需要解析的文本
            let expr = RegExp.$1.trim()
            // log(expr) //获取到插值表达式里面的变量
            node.textContent = txt.replace(reg, compileUtil.getVMValue(vm, expr))
        }
    },
    //解析v-text指令
    text(node, vm, expr) {
    
    
        node.textContent = this.getVMValue(vm, expr)
        // log(node)
    },
    // 解析v-html指令
    html(node, vm, expr) {
    
    
        node.innerHTML = this.getVMValue(vm, expr)
    },
    //解析v-on指令
    eventHandler(node, vm, type, expr) {
    
    
        let eventType = type.split(':')[1]
        // log(eventType)
        let fn = vm.$methods && vm.$methods[expr]
        if (eventType && fn) {
    
    
            //使用bind改变methods里的this指向vm实例
            node.addEventListener(eventType, fn.bind(vm))
        }
    },
    //这个方法用于获取vm中的数据
    getVMValue(vm, expr) {
    
    
        let data = vm.$data
        expr.split('.').forEach(item => {
    
    
            // log(item)
            data = data[item]
        })
        return data
    }
}
至此已经实现了指令 v-text、v-html、事件绑定 v-on以及插值表达式{ {}}的解析,其他指令可举一反三

o b s e r v e r . j s 用 于 给 d a t a 中 所 有 的 数 据 添 加 g e t t e r 和 s e t t e r ( 监 听 d a t a 中 的 所 有 数 据 ) \color{#ff0000}{observer.js用于给data中所有的数据添加getter和setter(监听data中的所有数据)} observer.jsdatagettersetter(data)

/**
 1. observer用于给data中所有的数据添加getter和setter
 */
class Observer {
    
    
    constructor(data) {
    
    
        this.data = data
        this.walk(data)
    }
    /**核心方法
     * 遍历data中所有的数据,都添加上getter和setter
     */
    walk(data) {
    
    
        if (!data || typeof data != "object") {
    
    
            return
        }
        Object.keys(data).forEach(key => {
    
    
            // log(key) //给data对象的key设置getter和setter
            this.defineReactive(data, key, data[key])
            //如果data[key]是复杂数据类型,递归walk,直到简单数据类型后return
            this.walk(data[key])
        })
    }
    defineReactive(obj, key, value) {
    
    
    	//Vue2.x版本中使用Object.defineProperty()进行数据劫持,官方公布3.x版本改写为proxy()
        Object.defineProperty(obj, key, {
    
    
            enumerable: true,
            configurable: true,
            get() {
    
    
                // log('你获取了值', value)
                return value
            },
            set(newValue) {
    
    
                if (value === newValue) {
    
    
                    return
                }
                // log('你设置了newValue', newValue)
                value = newValue
            }
        })
    }
}

在 m y V u e . j s 中 初 始 化 O b s e r v e r \color{skyblue}{在myVue.js中初始化Observer} myVue.jsObserver

constructor(options = {
    
    }) {
    
    
    //给vue实例增加属性
    this.$el = options.el
    this.$data = options.data
    this.$methods = options.methods
    //------在此处初始化Observer------
    new Observer(this.$data)
    //如果指定了el参数,对el进行解析
    if (this.$el) {
    
    
        //compile负责解析模板的内容
        //需要:模板和数据,把整个vue实例传过去
        new Compile(this.$el, this)
    }
}

最后实现双向数据绑定,即响应式框架

讲双向数据绑定要先讲一种设计模式,发布订阅模式(也叫观察者模式)的设计思想

观察者模式:定义了对象间一种一对多的依赖关系,当目标对象 Subject 的状态发生改变时,所有依赖它的对象 Observer 都会得到通知。

模式特征:

一个目标者对象 Subject,拥有方法:添加 / 删除 / 通知 Observer;

多个观察者对象 Observer,拥有方法:接收 Subject 状态变更通知并处理;

目标对象 Subject 状态变更时,通知所有 Observer。

Subject 添加一系列 Observer, Subject 负责维护与这些 Observer 之间的联系,“你对我有兴趣,我更新就会通知你”。

简 单 理 解 发 布 订 阅 模 式 : 发 布 者 可 以 理 解 为 微 信 公 众 号 作 者 , 订 阅 者 可 以 理 解 为 关 注 公 众 号 的 人 。 \color{skyblue}{简单理解发布订阅模式:发布者可以理解为微信公众号作者,订阅者可以理解为关注公众号的人。}
当 作 者 发 布 新 的 文 章 时 所 有 关 注 的 人 都 会 收 到 新 的 消 息 , 即 发 布 者 发 布 消 息 , 所 有 订 阅 者 收 到 通 知 来 及 时 更 新 内 容 \color{skyblue}{当作者发布新的文章时所有关注的人都会收到新的消息,即发布者发布消息,所有订阅者收到通知来及时更新内容}

w a t c h e r . j s 模 块 负 责 把 c o m p i l e 模 块 和 o b s e r v e r 模 块 关 联 起 来 ( 用 于 管 理 订 阅 者 ) \color{#ff0000}{watcher.js模块负责把compile模块和observer模块关联起来(用于管理订阅者)} watcher.jscompileobserver()

双 向 数 据 绑 定 核 心 功 能 在 这 里 实 现 \color{#ff0000}{双向数据绑定核心功能在这里实现}

/**
 * watcher模块负责把compile模块和observer模块关联起来
 */
class Watcher {
    
    
    /**
     * vm:当前的vue实例
     * expr:data中数据的名字
     * cb:一旦数据发生了改变,则需要调用cb
     */
    constructor(vm, expr, cb) {
    
    
        this.vm = vm
        this.expr = expr
        this.cb = cb
        /** 
         * this表示的就是新创建的watcher对象
         * 存储到Dep.target属性上
         * */
        Dep.target = this
        //把expr的旧值给存起来
        this.oldValue = this.getVMValue(vm, expr)
        //清空Dep.target
        Dep.target = null
    }
    //对外暴露的一个方法,这个方法用于更新页面
    update() {
    
    
        let oldValue = this.oldValue
        let newValue = this.getVMValue(this.vm, this.expr)
        if (oldValue != newValue) {
    
    
            this.cb(newValue)
        }
    }
    //这个方法用于获取vm中的数据
    getVMValue(vm, expr) {
    
    
        let data = vm.$data
        expr.split('.').forEach(item => {
    
    
            // log(item)
            data = data[item]
        })
        return data
    }
}
/**
 * dep对象管理订阅者和通知所有订阅者
 * */
class Dep {
    
    
    constructor() {
    
    
        //用于管理订阅者
        this.subs = []
    }
    //添加订阅者
    addSub(watcher) {
    
    
        this.subs.push(watcher)
    }
    //通知,发布
    notify() {
    
    
        //遍历所有的订阅者,调用watcher的update()方法
        this.subs.forEach(sub => {
    
    
            sub.update()
        })
    }
}

在compile.js的指令解析方法中加入Watcher,通过watcher对象,监听expr(data中数据的名字)数据的变化,一旦变化了,执行下面的回调函数

m u s t a c h e 方 法 增 加 W a t c h e r 监 听 \color{skyblue}{mustache方法增加Watcher监听} mustacheWatcher

mustache(node, vm) {
    
    
    // log('需要解析文本')
    let txt = node.textContent
    //用()给正则表达式做分组,方便下面获取到插值表达式里面的变量
    let reg = /\{\{(.+)\}\}/
    if (reg.test(txt)) {
    
    
        // log(txt) //需要解析的文本
        let expr = RegExp.$1.trim()
        // log(expr) //获取到插值表达式里面的变量
        node.textContent = txt.replace(reg, compileUtil.getVMValue(vm, expr))
        //-------------此处为新增Watcher监听------------
        new Watcher(vm, expr, newValue => {
    
    
            node.textContent = txt.replace(reg, newValue)
        })
    }
}

t e x t 方 法 增 加 W a t c h e r 监 听 \color{skyblue}{text方法增加Watcher监听} textWatcher

text(node, vm, expr) {
    
    
    node.textContent = this.getVMValue(vm, expr)
    // log(node)
    //通过watcher对象,监听expr的数据的变化,一旦变化了,执行下面的回调函数
    //-------------此处为新增Watcher监听------------
    new Watcher(vm, expr, newValue => {
    
    
        node.textContent = newValue
    })
}

h t m l 方 法 增 加 W a t c h e r 监 听 \color{skyblue}{html方法增加Watcher监听} htmlWatcher

html(node, vm, expr) {
    
    
	node.innerHTML = this.getVMValue(vm, expr)
	//-------------此处为新增Watcher监听------------
    new Watcher(vm, expr, newValue => {
    
    
        node.innerHTML = newValue
    })
}

增 加 m o d e l 方 法 和 W a t c h e r 监 听 \color{skyblue}{增加model方法和Watcher监听} modelWatcher

//解析v-model指令
model(node, vm, expr) {
    
    
    let self = this
    node.value = this.getVMValue(vm, expr)
    //实现双向数据绑定,给node注册input事件,当当前value值发生改变时,修改对应的数据
    node.addEventListener('input', function () {
    
    
        self.setVMValue(vm, expr, this.value)
    })
    //-------------此处为新增Watcher监听------------
    new Watcher(vm, expr, newValue => {
    
    
        node.value = newValue
    })
}

增 加 s e t V M V a l u e 方 法 处 理 数 据 双 向 绑 定 的 复 杂 数 据 类 型 \color{skyblue}{增加setVMValue方法处理数据双向绑定的复杂数据类型} setVMValue

//处理数据双向绑定的复杂数据类型
setVMValue(vm, expr, value) {
    
    
    let data = vm.$data
    let arr = expr.split('.')
    arr.forEach((key, index) => {
    
    
        // log(key)
        if (index < arr.length - 1) {
    
    
            data = data[key]
        } else {
    
    
            data[key] = value
        }
    })
}

在observer.js的defineReactive方法中给data中的每一个数据添加订阅

//data中的每一个数据都应该维护一个Dep对象
//Dep保存了所有的订阅了该数据的订阅者
defineReactive(obj, key, value) {
    
    
    let that = this
    let dep = new Dep()
    Object.defineProperty(obj, key, {
    
    
        enumerable: true,
        configurable: true,
        get() {
    
    
            //如果Dep.target中有watcher对象,存储到订阅者数组中
            Dep.target && dep.addSub(Dep.target)
            // log('你获取了值', value)
            return value
        },
        set(newValue) {
    
    
            if (value === newValue) {
    
    
                return
            }
            // log('你设置了newValue', newValue)
            value = newValue
            //如果newValue也是一个新对象,也要对其进行数据劫持
            that.walk(newValue)
            //发布通知,让所有订阅者更新内容
            dep.notify()
        }
    })
}

到此已经实现了双向数据绑定

最后再将data、methods中所有的数据都代理到vm上,实现在内部可以直接用this访问数据和方法

在 m y V u e . j s 中 增 加 p r o x y 方 法 把 d a t a 和 m e t h o d s 中 所 有 的 数 据 代 理 到 v m 实 例 上 \color{skyblue}{在myVue.js中增加proxy方法把data和methods中所有的数据代理到vm实例上} myVue.jsproxydatamethodsvm

//把data和methods中所有的数据代理到vm上
proxy(data) {
    
    
    Object.keys(data).forEach(key => {
    
    
        Object.defineProperty(this, key, {
    
    
            enumerable: true,
            configurable: true,
            get() {
    
    
                return data[key]
            },
            set(newValue) {
    
    
                if (data[key] == newValue) {
    
    
                    return
                }
                data[key] = newValue
            }
        })
    })
}

在 m y V u e . j s 中 的 c o n s t r u c t o r 函 数 里 面 执 行 p r o x y 方 法 \color{skyblue}{在myVue.js中的constructor函数里面执行proxy方法} myVue.jsconstructorproxy

constructor(options = {
    
    }) {
    
    
    //给vue实例增加属性
    this.$el = options.el
    this.$data = options.data
    this.$methods = options.methods
    new Observer(this.$data)
    //--------------在此处执行proxy----------------
    //把data中所有的数据都代理到vm上
    this.proxy(this.$data)
    //把methods中所有的数据都代理到vm上
    this.proxy(this.$methods)
    //如果指定了el参数,对el进行解析
    if (this.$el) {
    
    
        //compile负责解析模板的内容
        //需要:模板和数据,把整个vue实例传过去
        new Compile(this.$el, this)
    }
}

至此已经实现了指令、插值表达式、事件绑定、双向数据绑定的功能

分享一个vsCode批量添加带src属性img标签的方法

<!-- img[src='/images/pic_$$.png']*n -->

源代码没有打包,只能单个引入文件

如 有 问 题 或 错 误 , 欢 迎 指 正 \color{skyblue}{如有问题或错误,欢迎指正}

点击跳转到源码地址

猜你喜欢

转载自blog.csdn.net/zty867097449/article/details/106573754