作为当下最为流行的前端框架之一的Vue,在学习过程中,我们需要对其实现原理进行掌握,需要知道vue框架做了什么样的操作才能实现数据驱动页面的渲染,在此次学习过程中做出总结如下:
根据上图所示,在我们进行new Vue()创建构造函数时,我们需要进行初始化操作,对我们的数据进行响应式处理,也就是通过Observer进行劫持监听所有的属性,同时我们需要对模板进行编译,也就是通过Compile进行解析指令,它会进行初始化视图,这个时候如果数据发生变化时,我们的页面还不会发生改变,我们引入了一个概念(Watcher),我们在页面每进行一个数据的绑定时,我们会创建一个Watcher,它用来观察绑定的Dom的监听操作,每当数据变更时,我们的Watcher会进行更新Dom的操作,如果页面绑定了多个数据时,那么我们就引入另外一个概念(Dep),它用来集中管理多个Watcher,这样我们就形成了一个闭环。代码如下:
class Kvue {
constructor(options){
//保存选项
this.$options = options;
this.$data = options.data;
//响应化处理
observe(this.$data);
//代理,让我们可以直接访问$data上的属性
proxy(this,'$data');
}
}
//劫持监听所有的属性
class Observer {
constructor(value){
//保存value
this.value = value;
//将参数变成响应化
if(typeof value === 'object'){
this.walk(value);
}else if(Array.isArray(value)){
//...数组的响应化处理
}
}
//对象数据响应化处理
walk(value){
Object.keys(value).forEach(key=>{
defineReactive(obj, key, obj[ky]);
})
}
}
//代理函数,方便用户直接访问$data中的数据
function proxy(vm, sourceKey) {
//这里的ym指vue实例,sourceKey指$data
将data中的变量全部遍历并使其能够直接使用而不需要依赖与data上
Object.keys(vm[sourceKey]).forEach(key=>{
//key指data上的属性
Object.defineProperty(vm, key, {
get(){
return vm[sourceKey][key]
},
set(newVal){
vm[sourceKey][key] = newVal
}
})
})
}
function observe(obj) {
if(typeof obj !== 'object' || obj == null){
return;
}
//创建Observer
new Observer(obj);
}
function defineReactive(obj, key, val){
//递归,考虑到object的属性也有可能是object
observe(val)
Object.defineProperty(obj, key, {
get(){
return val
},
set(newVal){
return val = newVal
}
})
}
完成以上代码,我们就成功实现了Observer的部分,接着我们需要完成Compile的实现,来完成解析指令并初始化视图渲染Dom,代码如下:
//编译器
//递归遍历Dom
//判断节点类型,如果是文本,则判断是否是插值绑定
//如果是元素,则遍历其属性判断的是否是指令或事件,然后递归子元素
class Compiler {
constructor(el, vm){
//保存Vue实例
this.$vm = vm;
this.$el = document.querySelector(el);
if(this.$el) {
//执行编译
this.compile(this.$el)
}
}
compile(el) {
//遍历el树
const childNodes = el.childNodes;
Array.form(childrenNodes).forEach(node => {
//判断是否是元素
if(this.isElement(node)){
this.complieElement(node)
}else if(this.isInter(node)) {
//编译插值绑定
this.compileText(node)
}
if(node.childNodes && node.childNodes.length > 0) {
this.compile(node)
}
})
}
isElement(node) {
return node.nodeType === 1
}
isInter(node) {
//首先判断是否是文本标签,然后解析{{XXX}}中的内容
return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)
}
compileText(node) {
this.update(node, RegExp.$1, 'text')
}
//元素编译
compileElement(node) {
const nodeAttrs = node.attributes
Array.from(nodeAttrs).forEach(attr => {
const attrName = attr.name // k-xx
const exp = attr.value //{{xx}}
if(this.isDirective(attrName)) {
const dir = attrName.substring(2) //获取k-后面的指令内容
this[dir] && this[dir](node, exp) //执行指令
}
})
}
isDirective(attr) {
return attr.indexOf(k-) === 0
}
text(node, exp) {
this.update(node, exp, 'text')
}
textUpdater(node, value) {
node.textContent = value
}
html(node, exp) {
this.update(node, exp, 'html')
}
htmlUpdater(node, value) {
node.innerHTML = value
}
//更新函数作用:
// 1.初始化Dom
// 2.根据相对应的指令做出更新(...Updater())
update(node, exp, dir) {
const fn = this[dir + 'Updater']
fn && fn(node, this.$vm[exp])
}
}
完成这一步后,我们的compile进行初始化Dom就完成了,不过我们还缺少关键的一部操作,就是创建Watcher这一操作,当我们的Observer(发布者)来监听数据变化后,需要通知我们的观察者(Watcher),这个时候需要去收集依赖的工作,,这个时候我们引入Dep这一概念,我们通过Dep来通知观察者(Watcher),Wacther订阅Dep,数据变化时,由Dep通知Wacher变化,当Watcher接收到消息后,最终通知更新我们的视图,从而形成一个闭环,这就是我们整个从监听到渲染的过程,Wacther和Dep功能实现的代码如下:
const watchers = []; //临时用于保存watcher
class Watcher {
constructor(vm, ky, updateFn) {
this.vm = vm;
this.key = key;
this.updateFDn = updateFn;
//读一下当前的key,触发依赖收集
Dep.target = this
vm[key]
Dep.target = null
}
//未来会被Dep调用
update() {
this.updateFn.call(this.vm, this.vm[this.key])
}
}
声明Dep
//Dep:保存所有watcher,当某个key发生变化时,通知更新
class Dep {
constructor() {
this.deps = []
}
addDep(watcher) {
this.deps.push(watcher)
}
notify() {
this.deps.forEach(dep => dep.update())
}
}
创建Watcher时触发getter
class Wacther {
constructor(vm, ky, updateFn) {
//读一下当前的key,触发依赖收集
Dep.target = this
vm[key]
Dep.target = null
}
}
收集依赖,创建Dep实例
//在初始进行数据响应式的时候
function defineReactive(obj, key, val) {
//递归处理
observe()
//创建一个Dep实例
const dep = new Dep()
Object.defineProperty(obj, key, {
get() {
//收集依赖:把watcher和dep关联
//希望watcher实例化时,访问一下对应的key,同时把这个实例设置在Dep.target上
Dep.tart && dep.addDep(Dep.target)
}
set() {
if(newVal !== val){
observe(newVal)
val = newVal
//通知更新
dep.notify()
}
}
})
}
在进行compile解析模板时,我们在监听到值发生变化时,同时需要执行watcher的更新视图
class Compiler {
constructor(el, vm) {
this.$vm = vm;
this.$el = document.querySelector(el)
//执行编译
this.compile(this.$el)
}
//...
update(node, exp, dir) {
const fn = this[dir + 'Updater']
fn && fn(node, val)
//更新,创建一个Watcher实例
new Watcher(this.$vm, exp, val => {
fn && fn(node, val)
})
}
}