也许很多人写了很久的vue但是实际对于其双向绑定和其渲染还是存在一个模糊的概念,仅知道是通过defineProperty来达到,但是具体详细怎么实现,及其分多少个模块,并不是非常清除。
模块划分
大体分三个模块:
observer观察者(dep容器)
compile解释器
watcher订阅者
下面用简单实现一个v-model及{{name}}的简单示例来描述此三个模块。
1.先来看一下初始化
{{name}}
{{xiaxia}}
入口文件(index.js)
class Vue {
constructor(options) {
this.$el = document.querySelector(options.el);
this.$data = options.data;
Object.keys(this.$data).forEach(key => {
this.proxyData(key);
});
this.init(this.$data);
}
init() {
// 加入观察者
observer(this.$data);
// 编译器
new Compile(this);
}
// 数据劫持 重写get set 让数据直接读取写入
proxyData(key) {
Object.defineProperty(this, key, {
get(){
return this.$data[key]
},
set(value){
this.$data[key] = value;
}
});
}
}
定义一个构造函数
在constructor传入了所挂载的节点以及其data对象(此处为对象,实则vue中data为一个方法,暂不做深入分析,往后会提到)
遍历data对象中的元素修改其get和set以此来达到this.name可直接访问及修改其属性的效果。
并在初始化中引入observer观察者及compile编译器
observer观察者
function observer(data) {
if (!data || typeof data !== "object") {
return;
}
for (let key in data) {
defineReactive(data, key, data[key]);
}
}
function defineReactive(data, key, value) {
//递归调用,监听所有属性
observer(value);
const dep = new Dep();
Object.defineProperty(data, key, {
get(){
if (Dep.target) {
dep.addSub(Dep.target);
}
return value;
},
set(newVal){
if (value !== newVal) {
value = newVal;
dep.notify(); //通知订阅器
}
}
});
}
监听每一个属性(此处存在一定的性能问题在vue3.0已优化,目前哪怕没有用到的属性也会去监听浪费性能,暂不展开)
get中addSub为把watcher加入到容器中管理
set则是通过对不比其值来通知watcher订阅器
// 容器
class Dep {
constructor() {
this.subs = [];
}
addSub(sub) {
this.subs.push(sub);
}
notify() {
this.subs.forEach(sub => {
sub.update();
})
}
}
// es6 class只能以此添加公共方法属性
Dep.prototype.target=null
其中update为watcher中方法
watcher订阅者
class Watcher{
constructor(vm, prop, callback){
this.vm = vm;
this.prop = prop;
this.callback = callback;
this.value = this.get();
}
update(){
const value = this.vm.$data[this.prop];
const oldVal = this.value;
if (value !== oldVal) {
this.value = value;
this.callback(value);
}
}
get(){
Dep.target = this; //储存订阅器
const value = this.vm.$data[this.prop]; //因为属性被监听,这一步会执行监听器里的 get方法
Dep.target = null;
return value;
}
}
watcher中get方法之所以设置Dep.target = this后对this.vm也就是data赋值是为了触发observer中的get属性,故赋值成功后则应当置空。
update方法则含一个回掉函数,watcher在compile中创建
compile解释器
class Compile {
constructor(vm) {
this.vm = vm;
this.el = vm.$el;
this.init()
}
init() {
this.fragment = this.nodeFragment(this.el);
this.compileNode(this.fragment);
this.el.appendChild(this.fragment);
}
nodeFragment(el) {
// 创建内存中的DOM
const fragment = document.createDocumentFragment();
let child = el.firstChild;
//将子节点,全部移动文档片段里
while (child) {
fragment.appendChild(child);
child = el.firstChild;
}
return fragment;
}
// 编译节点
compileNode(fragment) {
const childNodes = fragment.childNodes;
[...childNodes].forEach(node => {
// 如果节点是元素节点,则 nodeType 属性将返回 1。
// 如果节点是属性节点,则 nodeType 属性将返回 2。
// ......
// 参照https://www.w3school.com.cn/jsref/prop_node_nodetype.asp
if (this.isElementNode(node)) {
this.compile(node);
}
const reg = /\{\{(.*)\}\}/;
const text = node.textContent;
if (reg.test(text)) {
const prop = reg.exec(text)[1];
this.compileText(node, prop); //替换模板
}
if (node.childNodes && node.childNodes.length) {
this.compileNode(node);
}
})
}
compile(node) {
// 编译vue的指令
let nodeAttrs = node.attributes;
[...nodeAttrs].forEach(attr => {
let { name, value } = attr;
if (name === "v-model") {
this.compileModel(node, value);
}
});
}
compileModel(node, prop) {
let val = this.vm.$data[prop];
// 初始化值
this.updateModel(node, val);
// 添加观察者
new Watcher(this.vm, prop, (value) => {
// 回调函数
this.updateModel(node, value);
});
node.addEventListener('input', e => {
let newValue = e.target.value;
if (val === newValue) {
return;
}
this.vm.$data[prop] = newValue;
});
}
compileText(node,prop){
let text = this.vm.$data[prop];
this.updateView(node, text);
new Watcher(this.vm, prop, (value) => {
this.updateView(node, value);
});
}
updateModel(node,value){
node.value = typeof value === 'undefined' ? '' : value;
}
updateView(node,value){
node.textContent = typeof value === 'undefined' ? '' : value;
}
isElementNode(node) {
return node.nodeType === 1;
}
}
init为初始化方法,也就是vue第一次默认渲染的数据。
从onst fragment = document.createDocumentFragment();中可以看到其创建了一个文档片段,其实也就是所谓的VirtualDOM虚拟DOM
compileNode 遍历其子节点通过正则找到{{name}}来替换其内容以及通过nodeType来定位到元素找到v-model来添加watcher观察者和给input添加监听来修改其值(之前定义好的get,set)
通过Watcher的get来把watcher添加到dep及通过dep的notify来调用watcher的更新方法产生的值来更新数据(有点绕。。。。。。)
Finally
主要还是得读代码,三者存在循环调用密不可分。
附上代码https://github.com/Xyifeng/vue-core-demo