一个极其简易版的vue.js实现
前言
之前项目中一直在用vue,也边做边学摸滚打爬了近一年。对一些基础原理性的东西有过了解,但是不深入,例如面试经常问的vue的响应式原理,可能大多数人都能答出来Object.defineProperty进行数据劫持,但是深入其实现细节,还是有很多之前没考虑到的东西,例如依赖收集后如何通知订阅器,以及订阅发布模式如何实现等等。过程中读了部分源码,受益匪浅,除此之外,动手去实现它也是个很棒的学习方式,话不多说,看代码,仓库地址。
实现
vue的更新机制我们简单概括一下就是,先对template进行解析,若检测到template中使用了data中定义的属性,则生成一个对应的watcher,通过劫持getter进行依赖(即watcher)收集,收集的内容保存在订阅器Dep,通过劫持setter做到改变属性从而通知订阅器更新,那么我们首先要做的就是对属性进行劫持。
vue2.0中使用的是Object.defineProperty,有传言说vue 3.0将会使用Proxy来代替Object.defineProperty,其有诸多好处:
- defineProperty不能对数组进行劫持,因此vue的文档中才会提到只有push、pop等8种方法能够检测变化,而arr[index] = newValue并不能检测变化,push等方法能检测变化也是因为开发者对Array原生方法进行hack实现的。
- defineProperty只能改变对象的某一个属性,若需要劫持整个对象,必须遍历对象,对每个属性劫持,因此效率并不高。而Proxy更像是一个代理,它会产生一个新的对象,该对象内部的属性均以实现劫持。但要注意,某个属性若也是一个对象类型,需要对该属性也执行proxy操作才能实现劫持。
Proxy目前来看唯一的缺点就是兼容性可能存在问题,不过无伤大雅,我们也顺应潮流,使用Proxy来实现数据劫持,代码很简单:
/**
* 接受一个对象,对属性进行依赖追踪
*/
function observable(obj) {
const dep = new Dep()
const proxy = new Proxy(obj, {
get(target, property) {
const value = target[property]
if (value && typeof value === 'object') { // 若属性为object,递归处理
target[property] = observable(value)
}
if (Dep.target) { // Dep.target指向当前watcher
dep.addWatcher(Dep.target)
}
return target[property]
},
set(target, property, value) {
target[property] = value
dep.notify() // 通知订阅器
}
})
return proxy
}
注意该方法需要返回proxy实例,因为只有通过proxy实例访问属性才具有劫持效果。我们可以看到代码中有一个Dep,这个东西即是订阅器,可以理解为它维护了一个依赖(watcher)的数组,并实现了一些管理数据的方法诸如addWatcher添加依赖,以及需要提供一个notify方法来遍历所有的watcher执行其相应的更新函数,同样代码很简单:
/**
* 依赖收集器,存放所有的watcher,并提供发布功能(notify)
*/
class Dep {
constructor() {
this.watchers = []
}
addWatcher(watcher) { // 添加watcher
this.watchers.push(watcher)
}
notify() { // 通知方法,调用即依次遍历所有watcher执行更新
this.watchers.forEach((watcher) => {
watcher.update()
})
}
}
最后我们来看下watcher,我们知道watcher即我们所说的依赖,它是在编译template的时候,若找到data中声明的属性,即会生成一个对应的watcher实例,触发依赖收集,加入订阅器。同时还需要提供一个update函数,在触发notify的时候调用来更新视图,代码如下:
/**
* watcher即所谓的依赖,监听具体的某个属性
*/
class Watcher {
constructor(proxy, property, cb) {
this.proxy = proxy
this.property = property
this.cb = cb
this.value = this.get()
}
update() { // 执行更新
const newValue = this.proxy[this.property]
if (newValue !== this.value && this.cb) { // 对比property新旧值,决定是否更新
this.cb(newValue)
}
}
get() { // 只在初始化时调用,用于依赖收集
Dep.target = this // 将自身指向Dep.target,执行完依赖收集再去释放
const value = this.proxy[this.property]
Dep.target = null
return value
}
}
至此,响应式原理大致已经成形,接着我们只要写一个简易的模板解析,demo就能跑起来啦。我这边的实现比较挫,仅仅是通过正则匹配来实现了一个不带diff的virture dom,纯属娱乐,重点还是在实现响应式原理上,这边贴一下代码:
let init = false // 只在初始化时去生成watcher
const eventMap = new Map() // 存放事件
const root = document.getElementById('root') // 根节点
/**
* 用于将传入RayActive的vm对象进行代理,可通过this.xx访问this.data.xx
* @param {Object} vm
* @param {Proxy} proxydata 经过proxy代理的vm.data对象,使this.xx操作也能触发视图更新
*/
function vmProxy(vm, proxydata) {
return new Proxy(vm, {
get(target, property) {
return target.data[property] || target.methods[property]
},
set(target, property, value) {
proxydata[property] = value
}
})
}
/**
* 编译vm,分别对data和render做相应处理
* @param {Object} vm 需要被编译的vm对象
*/
function compile(vm) {
const proxydata = compileData(vm.data)
compileRender(proxydata, vm.render)
bindEvents(vm, vmProxy(vm, proxydata))
}
/**
*
* @param {Object} data 需要被编译的vm中的data对象
*/
function compileData(data) {
return observable(data)
}
/**
*
* @param {*} render 需要被编译的render字符串
* @param {*} proxydata 经proxy转换过的data
*/
function compileRender(proxydata, render) {
if (render) {
const variableRegexp = /\{\{(.*?)\}\}/g
const variableResult = render.replace(variableRegexp, (a, b) => { // 替换变量为相应的data值
if (!init) { // 只在初始化时去生成watcher
new Watcher(proxydata, b, function() {
compileRender(proxydata, render)
})
}
return proxydata[b]
})
const eventRegexp = /(?<=<.*)@(.*)="(.*?)"(?=.*>)/
const result = variableResult.replace(eventRegexp, (a, b, c) => { // 为绑定事件的标签添加唯一id标识
const id = Math.random().toString(36).slice(2)
eventMap.set(id, {
type: b,
method: c
})
return a + ` id=${id}`
})
init = true
root.innerHTML = result
}
}
/**
* 通过root节点做事件代理,绑定模板中声明的事件
* @param {*} vm
* @param {*} proxyvm 经过proxy代理的vm
*/
function bindEvents(vm, proxyvm) {
for (let [key, value] of eventMap) {
root.addEventListener(value.type, (e) => {
const method = vm.methods[value.method]
if (method && e.target.id === key) {
method.apply(proxyvm) // 将vm中methods方法的this指向经过proxy的vm对象
}
})
}
}
/**
* 可理解为Vue中的Vue类,使用方式为new RayActive(vm)
*/
class RayActive {
constructor(vm) {
compile(vm)
}
}
总结
这个简易实现仅仅是帮助大家学习vue的一些原理性的东西,跟vue比其他来只是冰山一角。这个代码还有很大的优化空间,比如执行notify时这里会通知所有的watcher等等,值得有空去研究一下。同时,我们能看到订阅发布模式带来的好处。如果不引入订阅器,那我们更新dom的代码得放到setter中去,那么就耦合了数据劫持与操作dom的逻辑。引入订阅器,能让我们在proxy中仅仅做依赖收集和通知的操作,剩下的各种复杂的或是个性化的逻辑可以放到watcher中去实现,完美做到了关注点分离。
一个极其简易版的vue.js实现的更多相关文章
- 使用 js 实现一个简易版的 vue 框架
使用 js 实现一个简易版的 vue 框架 具有挑战性的前端面试题 refs https://www.infoq.cn/article/0NUjpxGrqRX6Ss01BLLE xgqfrms 201 ...
- Vue源码分析之实现一个简易版的Vue
目标 参考 https://cn.vuejs.org/v2/guide/reactivity.html 使用 Typescript 编写简易版的 vue 实现数据的响应式和基本的视图渲染,以及双向绑定 ...
- 来,我们手写一个简易版的mock.js吧(模拟fetch && Ajax请求)
预期的mock的使用方式 首先我们从使用的角度出发,思考编码过程 M1. 通过配置文件配置url和response M2. 自动检测环境为开发环境时启动Mock.js M3. mock代码能直接覆盖g ...
- C#基于Mongo的官方驱动手撸一个Super简易版MongoDB-ORM框架
C#基于Mongo的官方驱动手撸一个简易版MongoDB-ORM框架 如题,在GitHub上找了一圈想找一个MongoDB的的ORM框架,未偿所愿,就去翻了翻官网(https://docs.mongo ...
- 一个简易版的Angular js 三层 示例
var myApp = angular.module('produceline', []); myApp.factory('ajax', ["$http", "$q&qu ...
- 你是否有一个梦想?用JavaScript[vue.js、react.js......]开发一款自定义配置视频播放器
前言沉寂了一周了,打算把这几天的结果呈现给大家.这几天抽空就一直在搞一个自定义视频播放器,为什么会有如此想法?是因为之前看一些学习视频网站时,看到它们做的视频播放器非常Nice!于是,就打算抽空开发一 ...
- 实现简易版的moment.js
github源码地址: www.baidu.com 作者: 易怜白 项目中使用了时间日期的处理方法,只使用了部分方法,为了不在引入第三方的库(moment.js),这里自己封装了项目中使用到的方法. ...
- Vue.js:轻量高效的前端组件化方案(转载)
摘要:Vue.js通过简洁的API提供高效的数据绑定和灵活的组件系统.在前端纷繁复杂的生态中,Vue.js有幸受到一定程度的关注,目前在GitHub上已经有5000+的star.本文将从各方面对Vue ...
- 【转】Vue.js:轻量高效的前端组件化方案
摘要:Vue.js通过简洁的API提供高效的数据绑定和灵活的组件系统.在前端纷繁复杂的生态中,Vue.js有幸受到一定程度的关注,目前在GitHub上已经有5000+的star.本文将从各方面对Vue ...
随机推荐
- 菜鸟python之路-第五章(记录读书点滴)
数字 1.数字类型 python支持多种数字类型:整型.长整型.布尔型.双精度浮点型.十进制浮点型和复数 . 创建数值对象并赋值 aint=1 along=-999999999999999L aflo ...
- CSS&JS小结
回顾:html: 作用:展示 文件标签: <html> <head> <title></title> </head> <body> ...
- python基础13_zip_import
继续内置函数,zip函数被比喻成拉链,将两边的齿对应起来. #!/usr/bin/env python # coding:utf-8 ## 比喻像个拉链,将两边对应起来. # 多出来的部分,不作处理. ...
- 【转】Android-Input 触摸设备
https://source.android.com/devices/input/touch-devices 触摸设备 Android 支持各种触摸屏和触摸板,包括基于触控笔的数字化板. 触摸屏是与显 ...
- React 组件框架
- Java Web安全之程序逻辑缺陷
Java Web程序逻辑缺陷本质是由于程序设计和开发者设计的程序执行逻辑存在某种缺陷而导致的安全隐患.企业的代码审查和渗透测试通常主要针对的大多是诸如xss攻击和sql注入和跨站点脚本这些头条式漏洞, ...
- MySql5.7多实例配置教程
最近朋友在搞在Linux上配置MySql5.7多实例教程,在网上查询了很多资料,一直报各种各样的错误,后来在网上搜了一篇博客,根据其配置,最近是配置成功了 参考配置连接:https://blog.cs ...
- C++中cin.getline与cin.get要注意的地方
cin.getline与cin.get的区别是,cin.getline不会将结束符或者换行符残留在输入缓冲区中.
- Linux-1-用户管理
目录: 用户账号的添加.删除与修改 用户口令的管理 用户组的管理 总结用户与用户组常用命令 ***用户账号的添加.删除与修改*** 添加用户:useradd 选项 用户名 选项: -c comme ...
- JAVA 对接腾讯地图,经纬度转换
package com.lvjing.util; import net.sf.json.JSONArray; import net.sf.json.JSONObject; import org.spr ...