vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
具体步骤:
第一步:需要 observe 的数据对象进行递归遍历,包括子属性对象的属性,都加上 setter 和 getter 这样的话,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化
// 观察者
class Observer {
constructor(data) {
this.observer(data)
}
observer(data) {
// 判断:如果是对象才观察
if (data && typeof data == 'object') {
// 如果是对象
for (let key in data) { // 循环遍历 $data , 让每一个属性都具有 set 和 get 方法
this.definedReactive(data, key, data[key])
}
}
}
definedReactive(obj, key, value) {// Object.definedProperty 必须的参数:对象,属性名,操作属性
this.observer(value)// 递归操作,遍历对象中的对象
let dep = new Dep() // 给每一个属性,都加上具有发布订阅的功能
Object.defineProperty(obj, key, {
get() {
// 创建 watcher 时 , 会取到对应的内容 , 并且把watcher 放到了全局上
Dep.target && dep.addSub(Dep.target)
return value
},
set: (newVal) => {
if (newVal != value) {
this.observer(newVal)// 监控新值,保证对象更改属性也为对象时,新属性具有 get 和 set 方法
value = newVal
dep.notify()
}
}
})
}
}
第二步:compile解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
// 基类 调度
class Compile {
constructor(el, vm) {
// 判断el 属性,是不是一个元素,如果不是元素,那就获取它
this.vm = vm
this.el = this.isELementNode(el) ? el : document.querySelector(el)
// 把当前节点中的元素 获取到 然后放到内存中
let fragment = this.nodeToFragment(this.el)
// 把节点中的内容进行替换
// 编译模板 用数据编译
this.compileNode(fragment)
// 把内容再塞回到页面中
this.el.appendChild(fragment)
}
在这里添加判断和处理不同类型节点的函数... ...
}
第三步:Watcher订阅者是Observer和Compile之间通信的桥梁,主要做的事情是:
class Dep {
constructor() {
this.subs = []; // 存放所有的 watcher
}
// 订阅
addSub(watcher) { // 添加 watcher
this.subs.push(watcher)
}
// 发布
notify() {
this.subs.forEach(watcher => watcher.update())
}
}
// 这里的每一个watcher都是单独的 类Watcher 的实例对象,发布者即修改对象属性时都会触发watcher的update方法
1、在自身实例化时往属性订阅器(dep)里面添加自己
即上面代码块中的 this.subs.push(watcher)
2、自身必须有一个update()方法
update() { // 更新操作 , 数据变化后,会调用观察者的update方法
let newVal = CompileUtil.getVal(this.vm, this.expr)
if (newVal !== this.oldValue) {
this.callback(newVal)
}
}
3、待属性变动dep.notice()通知时,能调用自身的 update() 方法,并触发Compile中绑定的回调,则功成身退。
上文代码块中的
// 发布 notify() { this.subs.forEach(watcher => watcher.update()) }
第四步:MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果。
class Vue {
constructor(options) {
this.$el = options.el
this.$data = options.data
let computed = options.computed
let methods = options.methods
// 如果这个根元素 存在 那么就编译模板
if (this.$el) {
// 把数据,全部转换成用 Object.definedProperty 来定义的
new Observer(this.$data)
for (let key in computed) { // 处理computed依赖关系
Object.defineProperty(this.$data, key, {
get: () => {
return computed[key].call(this)
}
})
}
for (let key in methods) {
Object.defineProperty(this, key, {
get() {
return methods[key]
}
})
}
// 把数据获取操作 vm 上的取值操作,都代理到 vm.$data
this.proxyVm(this.$data)
new Compile(this.$el, this)
}
}
// 将 this.data 代理到 this
proxyVm(data) {
for (let key in data) {
Object.defineProperty(this, key, {
get() {
return data[key] // 进行了转化操作
},
set(newVal){ // 设置代理方法
data[key] = newVal
}
})
}
}
}
页面中构造 Vue 实例函数:
const vm = new Vue({
el: '#App',
// methods computed 等... ...
})
=======================分割线=================================== 这里是完整的代码,你可以直接copy下来体验一下,当然这个代码并不完整,没有对数组进行处理(Vue的源码一篇文章是放不下的...):
// 观察者 (发布-订阅) 观察者 被观察者
class Dep {
constructor() {
this.subs = []; // 存放所有的 watcher
}
// 订阅
addSub(watcher) { // 添加 watcher
this.subs.push(watcher)
}
// 发布
notify() {
this.subs.forEach(watcher => watcher.update())
}
}
class Watcher {
constructor(vm, expr, callback) {// 实例, 表达式,回调函数
this.vm = vm,
this.expr = expr,
this.callback = callback
// 默认先存放一个老值
this.oldValue = this.get()
}
get() {
Dep.target = this // 取值 ,把这个观察者和数据关联起来
let value = CompileUtil.getVal(this.vm, this.expr)// 调用函数取到实例中的 $data 定义的值
Dep.target = null // 如果不取消,则任何数据取值,都会添加 watcher
return value
}
update() { // 更新操作 , 数据变化后,会调用观察者的update方法
let newVal = CompileUtil.getVal(this.vm, this.expr)
if (newVal !== this.oldValue) {
this.callback(newVal)
}
}
}
// 观察者
class Observer {
constructor(data) {
this.observer(data)
}
observer(data) {
// 判断:如果是对象才观察
if (data && typeof data == 'object') {
// 如果是对象
for (let key in data) { // 循环遍历 $data , 让每一个属性都具有 set 和 get 方法
this.definedReactive(data, key, data[key])
}
}
}
definedReactive(obj, key, value) {// Object.definedProperty 必须的参数:对象,属性名,操作属性
this.observer(value)// 递归操作,遍历对象中的对象
let dep = new Dep() // 给每一个属性,都加上具有发布订阅的功能
Object.defineProperty(obj, key, {
get() {
// 创建 watcher 时 , 会取到对应的内容 , 并且把watcher 放到了全局上
Dep.target && dep.addSub(Dep.target)
return value
},
set: (newVal) => {
if (newVal != value) {
this.observer(newVal)// 监控新值,保证对象更改属性也为对象时,新属性具有 get 和 set 方法
value = newVal
dep.notify()
}
}
})
}
}
// 基类 调度
class Compile {
constructor(el, vm) {
// 判断el 属性,是不是一个元素,如果不是元素,那就获取它
this.vm = vm
this.el = this.isELementNode(el) ? el : document.querySelector(el)
// 把当前节点中的元素 获取到 然后放到内存中
let fragment = this.nodeToFragment(this.el)
// 把节点中的内容进行替换
// 编译模板 用数据编译
this.compileNode(fragment)
// 把内容再塞回到页面中
this.el.appendChild(fragment)
}
// 处理元素节点
compileElement(ele) {
let attributez = ele.attributes;// 获取元素节点的属性赋值给类数组
[...attributez].forEach(attr => {
// 对属性进行判断
let { name, value: expr } = attr
// 判断是不是 Vue 指令,即是不是使用 “v-” 开头的,使用 startsWith方法
if (name.startsWith('v-')) {// v-model v-html v-bind// 解构赋值后value 名为 expr
let [, directive] = name.split('-')// 使用 - 对name进行切割,然后使用不同的指令进行处理,自定义函数 CompileUtil[directive]
let [directiveName, eventName] = directive.split(':')
// 将元素节点:ele,attr的表达式:expr,以及实例:vm 传入函数进行处理
CompileUtil[directiveName](ele, expr, this.vm, eventName)
}
})
}
// 处理文本节点
compileText(text) {
let content = text.textContent
// 使用正则表达式匹配文本节点中含有 {{}} 插值表达式符号的文本
if (/\{\{(.+?)\}\}/.test(content)) {// 找到所有带有插值表达式的文本
CompileUtil['text'](text, content, this.vm)
}
}
compileNode(node) {// 编译内存中的dom节点
let childNodes = node.childNodes;// 将获取到的子节点赋值
[...childNodes].forEach(child => {// 展开类数组
if (this.isELementNode(child)) {
this.compileElement(child)// 对元素节点进行处理
// 递归处理,深层遍历
this.compileNode(child)
} else {
this.compileText(child)// 对文本节点进行处理
}
})
}
nodeToFragment(node) {
// 创建一个文档碎片 使用 createDocumentFragment()方法
let fragment = document.createDocumentFragment()
let firstChild
// while 当满足条件为 true 时则循环一直执行
while (firstChild = node.firstChild) {
// appendChild具有移动性,把真实DOM 树上的元素节点放入 自定义的 fragment中
fragment.appendChild(firstChild)
}
return fragment
}
isELementNode(node) {
// nodeType 判断节点为元素节点还是文本节点
return node.nodeType === 1
}
}
// 不同类型数据处理
CompileUtil = {
getVal(vm, expr) {
return expr.split('.').reduce((data, current) => {
return data[current]
}, vm.$data)// 使用 reduce 方法对$data进行遍历最终返回为expr的切割值的返回==> expr.split('.')==>$data.thame==>'泰晤士'
},
setValue(vm, expr, value) {
expr.split('.').reduce((data, current, index, arr) => {
if (index == arr.length - 1) {
return data[current] = value
}
return data[current]
}, vm.$data)
},
model(node, expr, vm) {// node 是节点 expr 是表达式 vm 是当前实例
// 给输入框赋予value 属性, node.value = 实例中的$data的值
let fn = this.updater['modelUpdater']
// 给元素节点添加一个观察者,如果内容更新了就会触发此方法,会拿新值,给输入框赋予值
new Watcher(vm, expr, (newVal) => {
fn(node, newVal)
})
node.addEventListener('input', (e) => {
let value = e.target.value; // 获取用户输入的内容
this.setValue(vm, expr, value)
})
let value = this.getVal(vm, expr)
fn(node, value)
},
html(node,expr,vm) { // v-html="message"
let fn = this.updater['htmlUpdater']
new Watcher(vm, expr, (newVal) => {
fn(node, newVal)
})
let value = this.getVal(vm, expr)
fn(node, value)
},
getContentValue(vm, expr) {
return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
return this.getVal(vm, args[1])
})
},
on(node, expr, vm, eventName) { // 对 v-on 进行处理 v-on:click="change" 被切割 expr === change
node.addEventListener(eventName, (e) => {
vm[expr].call(vm, e)// 改变this指向
})
},
text(node, expr, vm) {
let fn = this.updater['textUpdater']// 将获取到的值,重新插入到节点中
let content = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
// 给表达式,每个插值表达式都加上观察者
new Watcher(vm, args[1], () => {
fn(node, this.getContentValue(vm, expr)) // 返回一个全的字符串
})
return this.getVal(vm, args[1])// 通过正则条件,拿到匹配到的插值表达式里的字符串,将字符串传入getVal函数取到vm 中的 $data的值
})
fn(node, content)
},
updater: {
htmlUpdater(node,value){ // 简易赋值, innerHTML不安全,存在 xss攻击风险
node.innerHTML = value
},
// 把数据插入到节点中
modelUpdater(node, value) {
return node.value = value
},
textUpdater(node, value) {
return node.textContent = value
}
}
}
class Vue {
constructor(options) {
this.$el = options.el
this.$data = options.data
let computed = options.computed
let methods = options.methods
// 如果这个根元素 存在 那么就编译模板
if (this.$el) {
// 把数据,全部转换成用 Object.definedProperty 来定义的
new Observer(this.$data)
for (let key in computed) { // 处理computed依赖关系
Object.defineProperty(this.$data, key, {
get: () => {
return computed[key].call(this)
}
})
}
for (let key in methods) {
Object.defineProperty(this, key, {
get() {
return methods[key]
}
})
}
// 把数据获取操作 vm 上的取值操作,都代理到 vm.$data
this.proxyVm(this.$data)
new Compile(this.$el, this)
}
}
proxyVm(data) {
for (let key in data) {
Object.defineProperty(this, key, {
get() {
return data[key] // 进行了转化操作
},
set(newVal){ // 设置代理方法
data[key] = newVal
}
})
}
}
}