数据响应式是啥?
先来说说vue框架,它本质上是一个MVVM框架,而MVVM框架的三要素:数据响应式、模板引擎、渲染
数据响应式:监听数据变化并在视图中更新
Object.defifineProperty()
Proxy
模版引擎:提供描述视图的模版语法
插值:{{}}
指令:v-bind,v-on,v-model,v-for,v-if
渲染:如何将模板转换为html
模板 => vdom => dom
所以简单来说,数据变更能够响应在视图中,就是数据响应式。本文暂不涉及虚拟dom,先就vue中的数据响应式和模板引擎做一个简单的解析和实践。
原理分析
首先,我们先看一段vue实际使用的代码,从中分析双向绑定的实现方式和原理。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<p l-text="count"></p>
<p l-html="desc"></p>
</div>
<script src="lCompile.js"></script>
<script src="lvue.js"></script>
<script>
const app = new LVue({
el:'#app',
data:{
count:1,
desc:'<span style="color:red">这是lvue?</span>'
}
}
})
</script>
</body>
</html>
1. new LVue() 首先执行初始化,对data执行响应化处理,这个过程发生在Observer中
2. 同时对模板执行编译,找到其中动态绑定的数据,从data中获取并初始化视图,这个过程发生在 Compile中
3. 同时定义一个更新函数和Watcher,将来对应数据变化时Watcher会调用更新函数
4. 由于data的某个key在一个视图中可能出现多次,所以每个key都需要一个管家Dep来管理多个 Watcher
5. 将来data中数据一旦发生变化,会首先找到对应的Dep,通知所有Watcher执行更新函数
好了我知道你们不想看文字,然后我花了大力气画的图,好好看!肯定能看懂!看不懂找我!
好了,下面开始代码部分,代码部分这里先直接贴一个链接,大家可以直接去看。代码
具体实现
LVue
相当于入口文件吧,处理整体逻辑,把数据存起来,模板拿过来处理。
class LVue{
constructor(options){
this.$options = options
this.$data = options.data
// 代理方法
proxy(this,'$data')
// 创建observe观察者
observe(this.$data)
// 编译模板,下面写
new Compile(options.el, this)
}
}
// 代理方法,目的是可以直接用this访问到$data中的内容
function proxy(vm,str) {
Object.keys(vm[str]).forEach(val=>{
Object.defineProperty(vm,val,{
get(){
return vm[str][val]
},
set(newVal){
vm[str][val] = newVal
}
})
})
}
// 就简单的看一下是不是对象,因为defineReactive是对象的方法,
// 至于数组则是通过重写数组操作方法实现数据劫持的,不过vue3中使用了ES6的Proxy
function observe(obj) {
if (typeof obj !== 'object' || obj == null) {
// 希望传入的是obj
return
}
// 创建Observer实例,进行数据劫持,下面写
new Observer(obj)
}
Observer(数据劫持)
我们知道可以利用**Obeject.defineProperty()**
来监听属性变动,但是你不能简单的对那个对象监听一下,万一对象内部属性还是个对象呢??所以需要将observe的数据对象进行递归遍历,包括子属性对象的属性,都加上setter和getter,这样的话,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化。
class Observe {
constructor(value){
this.value = value
this.walk(value)
}
// 对传入的参数进行劫持
walk(obj){
// 因为前面已经判断过是对象了,直接循环执行数据劫持方法就行了
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
}
// 对象的响应式
function defineReactive(obj,key,val){
observe(val)
const dep = new Dep() // 这里是消息订阅器,用来建立数据更新与页面更新的对应关系。
// 所有dep都先不看,下面会具体分析
Object.defineProperty(obj,key,{
get:function(){
Dep.target&&dep.addDep(Dep.target)
return val
},
set:function (newVal) {
//当给data属性中设置值的时候, 更改获取的属性的值
if (newVal !== val) {
observe(newVal)
val = newVal
dep.notify() // 改变值时触发dep内部的循环更新
}
}
})
}
监听到变化之后就是怎么通知订阅者Watcher了,有人说,那就直接通知Watcher不就好了?!那下面,说一下依赖收集。
视图中会用到vue的data中的某个值,这称为依赖。同一个值,可能会出现很多次,每次出现都需要将它收集出来,用一个Watcher进行维护,这就是依赖收集。 而某个值出现多次,则需要多个Watcher,这时候我们就需要一个Dep来管理,我们在修改数据时由Dep通知Watcher批量更新。
来个简单版解释: 代码中某个值,在很多地方使用,每个使用的地方对应一个更新操作。需要把这些操作放到一个盒子里,值改变的时候把盒子里所有更新操作触发一下。
watcher
好了,大概知道Watcher和Dep是什么了,那下面给大家先来个实现思路:
- 劫持时defifineReactive为每一个key创建一个Dep(就是上面说的那个管理Watcher的东西)实例。
- 初始化视图时每一次读取某个key,例如name1,创建一个watcherName1。
- 此时就会触发key(name1)的getter方法,所以就可以在getter方法中将watcherName1添加到name1对应的Dep中。
- 当key(name1)更新时,setter触发,此时便可通过对应Dep通知其管理所有Watcher更新,这样Dep中所有的watcher都触发一次更新,就实现了数据的响应。
这时候看完这些再回去理解前面劫持部分的代码有关dep的部分是不是就理解了呢。那下面我们来看一下实现:
class Watcher {
constructor(vm,key,updateFn){
// vue实例
this.vm = vm
// 可触发/依赖 的key
this.key = key
// 更新函数
this.updateFn = updateFn
// 下面两行要回去对照数据劫持getter部分看一下
Dep.target = this // 把Watcher存一下,get中直接dep.addDep(Dep.target)存进去
this.vm[this.key]; // 这里的意思就行调用了一下对应的key,这样就能出发getter方法了
Dep.target = null
}
update(){
this.updateFn.call(this.vm,this.vm[this.key])
}
}
Dep
Dep就相对简单多了,本质上就是维护了一组Watcher,有一个更新事假,执行的是循环出发Watcher中的更新方法。代码如下:
class Dep{
constructor(){
this.deps = []
}
addDep(dep){
this.deps.push(dep)
}
notify(){
// deps里面是一个一个的 watch , 改变值后循环触发update方法
this.deps.forEach(dep => dep.update());
}
}
Compile
接下来是编译部分,说实话这一部分理解起来特别简单,就是以传进来的根节点为基础遍历整个dom,然后找出节点上属性中“v-”,”{{}}”等等这一类的标识属性,挨个创建Watcher来监听他们就好了。虽然理解简单,但是代码却很多,所以代码细节就不一行一行的解释了,大家可以仔细看看代码,我还是写了不少注释的~ 有疑问也欢迎骚扰~
class Compile {
constructor(el, vm){
this.$el = document.querySelector(el)
this.$vm = vm
if (this.$el){
this.compile(this.$el)
}
}
// 编译,vue的语法
compile(el){
const childNodes = el.childNodes
Array.from(childNodes).forEach(node=>{
if (this.isElement(node)){
// console.log("编译元素" + node.nodeName)
this.compileElement(node)
} else if(this.isInterpolation(node)){
// console.log("编译差值文本" + node.textContent)
this.compileText(node)
}
if (node.childNodes&&node.childNodes.length>0) {
this.compile(node)
}
})
}
isElement(node){
return node.nodeType === 1
}
isInterpolation(node){
return node.nodeType === 3 &&/\{\{(.*)\}\}/.test(node.textContent)
}
// node为元素时编译方法
compileElement(node){
let nodeAttrs = node.attributes
Array.from(nodeAttrs).forEach(attr=>{
let attrName = attr.name
let exp = attr.value
console.log(exp)
// 属性名以 l- 开头时处理
if (attrName.indexOf("l-")===0){
let dir = attrName.substring(2)
// 拿出后面的html、text 等,html、text会被在内部定义方法
this[dir]&&this[dir](node,exp)
}
// 时间处理
if(this.isEvent(attrName)){
// @click = onClick
const dir = attrName.substring(1) // click
// 事件监听
this.eventHandler(node,exp,dir)
}
})
}
isEvent(dir){
return dir.indexOf('@') == 0
}
eventHandler(node,exp,dir){
const fn = this.$vm.$options.methods &&
this.$vm.$options.methods[exp]
node.addEventListener(dir,fn.bind(this.$vm))
}
// node为文本时处理
compileText(node){
this.update(node,RegExp.$1,'text')
}
// 初始化时执行 更新方法,并传入text
text(node,exp){
this.update(node,exp,'text')
}
// 初始化时执行 更新方法 目的是 update中创建了Watcher,可以传入改变方法,在数据监听时就可执行了
html(node,exp){
this.update(node,exp,'html')
}
model(node,exp){
// update方法只完成赋值和更新
this.update(node,exp,'model')
// 所以还需要事件监听
node.addEventListener('input',e=>{
// 将新的值赋值给数据即可
this.$vm[exp]=e.target.value
})
}
// 创建更新函数,和watcher绑定
update(node,exp,dir){
const fn = this[dir+'Updater']
fn && fn(node,this.$vm[exp])
new Watcher(this.$vm,exp,function (val) {
fn && fn(node,val)
})
}
// v-text 绑定text方法
textUpdater(node,val){
node.textContent = val
}
// v-html 绑定html方法
htmlUpdater(node,val){
node.innerHTML = val
}
modelUpdater(node,val){
// 多用在表单元素,暂时只考虑表单元素赋值
node.value = val
}
}
结语
这篇文章主要是介绍了一下Vue的数据响应式和他的原理以及实现。有疑问欢迎提问,当然发现问题也欢迎随之指正。