前言

之前项目中一直在用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实现的更多相关文章

  1. 使用 js 实现一个简易版的 vue 框架

    使用 js 实现一个简易版的 vue 框架 具有挑战性的前端面试题 refs https://www.infoq.cn/article/0NUjpxGrqRX6Ss01BLLE xgqfrms 201 ...

  2. Vue源码分析之实现一个简易版的Vue

    目标 参考 https://cn.vuejs.org/v2/guide/reactivity.html 使用 Typescript 编写简易版的 vue 实现数据的响应式和基本的视图渲染,以及双向绑定 ...

  3. 来,我们手写一个简易版的mock.js吧(模拟fetch && Ajax请求)

    预期的mock的使用方式 首先我们从使用的角度出发,思考编码过程 M1. 通过配置文件配置url和response M2. 自动检测环境为开发环境时启动Mock.js M3. mock代码能直接覆盖g ...

  4. C#基于Mongo的官方驱动手撸一个Super简易版MongoDB-ORM框架

    C#基于Mongo的官方驱动手撸一个简易版MongoDB-ORM框架 如题,在GitHub上找了一圈想找一个MongoDB的的ORM框架,未偿所愿,就去翻了翻官网(https://docs.mongo ...

  5. 一个简易版的Angular js 三层 示例

    var myApp = angular.module('produceline', []); myApp.factory('ajax', ["$http", "$q&qu ...

  6. 你是否有一个梦想?用JavaScript[vue.js、react.js......]开发一款自定义配置视频播放器

    前言沉寂了一周了,打算把这几天的结果呈现给大家.这几天抽空就一直在搞一个自定义视频播放器,为什么会有如此想法?是因为之前看一些学习视频网站时,看到它们做的视频播放器非常Nice!于是,就打算抽空开发一 ...

  7. 实现简易版的moment.js

    github源码地址: www.baidu.com 作者: 易怜白 项目中使用了时间日期的处理方法,只使用了部分方法,为了不在引入第三方的库(moment.js),这里自己封装了项目中使用到的方法. ...

  8. Vue.js:轻量高效的前端组件化方案(转载)

    摘要:Vue.js通过简洁的API提供高效的数据绑定和灵活的组件系统.在前端纷繁复杂的生态中,Vue.js有幸受到一定程度的关注,目前在GitHub上已经有5000+的star.本文将从各方面对Vue ...

  9. 【转】Vue.js:轻量高效的前端组件化方案

    摘要:Vue.js通过简洁的API提供高效的数据绑定和灵活的组件系统.在前端纷繁复杂的生态中,Vue.js有幸受到一定程度的关注,目前在GitHub上已经有5000+的star.本文将从各方面对Vue ...

随机推荐

  1. python "import this"

    The Zen of Python, by Tim Peters Beautiful is better than ugly.Explicit is better than implicit.Simp ...

  2. JS中的一元操作符

    表达式 一元操作符 优先级 结合性 运算顺序 表达式是什么? 就是JS 中的一个短语,解释器遇到这个短语以后会把对它进行计算,得到一个结果参与运算,我们把这种要参与到运算中的各种各样的短语称为表达式. ...

  3. Shell 字符串处理

    字符串处理方式 计算字符串长度 获取子串在字符串中的索引位置 计算子串长度 抽取(截取)字串 1.计算字符串长度,有两种方式 $ ${#string} $ expr length "$str ...

  4. Openresty 进行路由系统设计

    1.系统基础设计图为: 用户通过Http访问Openresty(Nginx + Lua), 其中Nginx虚拟主机中配置文件进行Lua脚本加载. LUA通过nginx内置变量或者http请求中变量来区 ...

  5. .net下的缓存技术

    1.为什么要缓存?缓存能解决的问题 1.1稳定性 同一个应用中,对同一数据.逻辑功能和用户界面的多次请求时经常发生的.当用户基数很大时,如果每次请求都进行处理,消耗的资源是很大的浪费,也同时造成系统的 ...

  6. 基于ZYNQ的双核启动与通信问题解决

    1    处理器间的通信 为AMP 设计创建应用之前,您需要考虑应用如何进行通信(如有需要).最简单的方法是使用片上存储器.Zynq SoC 配备256KB 的片上SRAM,可从以下四个源地址进行访问 ...

  7. Javascript学习--BOM操作

    1 获取UA(user Agent)用户代理 <!DOCtype html> <html> <head> <title></title> & ...

  8. web 自定义标签

    Web Components 标准非常重要的一个特性是,它使开发者能够将HTML页面的功能封装为 custom elements(自定义标签).而自定义标签的好处,就是在大型web开发的时候,可以封装 ...

  9. MSP430G2553 TimerA中断说明

    一.TimerA中断向量. G2553一共有2个TimerA,分别是TimerA0和TimerA1,中断入口地址分别是: TimerA0中断向量名称     <--> 中断源 ------ ...

  10. matlab 曲线拟合小记

    在matlab中经常需要对数据进行曲线拟合,如最常见的多项式拟合,一般可以通过cftool调用曲线拟合工具(curve fit tool),通过图形界面可以很方便的进行曲线拟合,但是有些时候也会遇到不 ...