Vue是目前炙手可热的JS框架,作为一个视图库,最重要的功能当然是数据绑定了,数据变化,模板变化。
接下来让我看看Vue实现的大致原理是怎样的。
这是我们的模板。
<div id="app">
<input type="text" v-model="name">
<p>
{{ name }}
</p>
<strong>{{ name }}</strong>
</div>
像下面这样实例化
var vm = new Vue({
el: "#app",
data: {
name: 'ok'
}
})
我们先不讨论v-model
指令。
我们初始化实例时传入一个data。data的name属性对应模板中的name。
我们要实现的功能点有
- vm实例代理data上的属性。
- 在vm对象内部,我们用this指代vm。当在vm内部this.name 被赋了一个新的值时,模板中的name也会同步变化。
这是单向数据绑定。
然后再考虑v-model
,v-model
相同于一个语法糖,监听了表单控件,用户输入。this.name也会变化。有了上面的条件,模板也会变化了。
这就是双向数据绑定。
那应该如何实现呢?
构造函数
function Vue(option) {
let { el, data } = option;
let node = document.querySelector(el);
this.data = data;
observe(data,this) // 代理绑定属性
let dom = kidnap(node,this) // 遍历dom树,编译,返回一个新的dom树
node.appendChild(dom)
}
代理绑定属性
Vue对属性变化检测的核心实现就是Object.defineProperty方法。这个方法可以为对象定义新的属性。可以设置getter,setter回调。
在这里的实践就是遍历data对象,data对象上面的每个属性被vm代理。当属性变化,setter回调,广播通知订阅者;getter被回调时,检测是否可以添加订阅者。
function defineReactive(obj,key,val) {
var dep = new Dep() // 为每一个属性实例一个发布者
Object.defineProperty(obj,key,{
get: function() {
if(Dep.target) {
dep.addSub(Dep.target); // 添加订阅者
}
return val
},
set: function (newVal) {
if(newVal === obj[key]) return;
val = newVal;
dep.notify() // 当属性变化,广播通知订阅者
}
})
}
function observe(obj,vm) {
Object.keys(obj).forEach(function (key) {
defineReactive(vm,key,obj[key]);
})
}
发布
class Dep {
constructor () {
this.subs = []
}
addSub (sub) {
this.subs.push(sub)
}
notify () {
this.subs.forEach(item => {
item.update()
})
}
}
每一个发布者都维护了一个订阅者数组,发布者的notify方法会遍历所有订阅者,调用订阅者的update方法。所以每一个订阅者必须实现一个update方法。
编译
我们知道当vm上的属性变化时,所有的订阅者都会收到通知。那么这些订阅者是谁呢?
订阅者就是模板中“Mustache” 语法(双大括号)的文本插值。
首先我们要将DOM中劫持过来。
function kidnap(node,vm) {
if(!node) return;
let frag = document.createDocumentFragment();
while (child = node.firstChild) {
frag.appendChild(child)
}
DFS(frag,function(node) {
compile(node,vm)
})
return frag;
}
值得一提的是上面代码中的appendChild方法。
DOM规定,一个DOM节点不能同属于两个父节点,所以对一个拥有父节点的节点执行appendChild其实是将它搬移到另一个节点。
同时进行while循环,可以巧妙的搬运一个节点下的所有子节点。
拿到模板之后对模板进行一些处理。
function compile(node,vm) {
if(!node) return;
var reg = /\{\{(.*)\}\}/;
if(node.nodeType == 1) {
let attr = node.attributes;
for (var i = 0; i < attr.length; i++) {
if(node.tagName.toLowerCase() == "input" && attr[i].name == "v-model") {
var name = attr[i].nodeValue;
node.addEventListener("keyup",function (e) {
vm[name] = e.target.value;
})
node.value = vm[name]
node.removeAttribute("v-model")
new Watcher(node,vm,name) // 订阅者
}
}
}
if(node.nodeType == 3) {
if(reg.test(node.nodeValue)) {
var name = RegExp.$1;
name = name.trim();
node.nodeValue = vm[name]
new Watcher(node,vm,name) // 订阅者
}
}
}
遍历每一个DOM节点。这里用到了DFS,深度优先搜索。可以参见我的上一篇文章。代码如下。
function DFS(node, cb) {
let deep = 1;
DFSdom(node,deep,cb)
}
function DFSdom(node, deep, cb) {
if(!node)
return;
cb(node,deep)
if(!node.childNodes.length) {
return;
}
deep++;
Array.from(node.childNodes).forEach(item => DFSdom(item,deep,cb))
}
订阅
我们可以看到在遍历DOM树的时候,对符合我们语法条件的节点进行了watch。watcher相当于订阅者。
class Watcher {
constructor (node, vm, name) {
Dep.target = this;
this.name = name;
this.node = node;
this.vm = vm;
this.update()
Dep.target = null;
}
update () {
this.value = this.vm[this.name];
this.node.nodeValue = this.value;
if(this.node.nodeType == 1) {
this.node.value = this.value
}
}
}