1. 理解Vue的设计思想
MVVM模式
MVVM框架的三要素:数据响应式、模板引擎及其渲染
数据响应式: 监听数据变化并在视图中更新
- Object.defineProperty()
- Proxy
模版引擎:提供描述视图的模版语法
- 插值:{{}}
- 指令:v-bind,v-on,v-model,v-for,v-if
渲染:如何将模板转换为html
- 模板 => vdom => dom
2. 数据响应式原理
数据变更能够响应在视图中,就是数据响应式。vue2中利用 Object.defineProperty() 实现变更检测。
简单实现
// cvue.js
const obj = {};
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
console.log('get', key);
return val
},
set(newVal) {
if(newVal !== val) {
console.log('set ' + key + ':' + newVal);
val = newVal
}
},
});
}
defineReactive(obj, 'foo', 'foo')
obj.foo
obj.foo = 'fooooooooooooo'
这里值得注意的是,我们在defineReactive这个方法里面定义了key和val。但是我们在设置对象的get时将函数内部的val进行了返回,这里就形成了一个闭包。我们的val是不会被释放的。此时我们在外部引用了obj的值,那么obj中多个key和val的状态就会一直被保存。
那我们来结合视图看一下
<body>
<div id="app"></div>
<script>
const obj = {};
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
console.log("get", key);
return val
},
set(newVal) {
if (newVal !== val) {
console.log("set " + key + ":" + newVal);
val = newVal;
// 更新函数
update();
}
},
});
}
function update() {
app.innerText = obj.foo;
}
defineReactive(obj, "foo", "");
obj.foo = new Date().toLocaleTimeString();
setInterval(() => {
obj.foo = new Date().toLocaleTimeString();
}, 1000);
</script>
</body>
因为obj的状态被保留了,当obj的数据发生变化时就会触发我们的update()方法重新渲染视图。
但是这里有个问题,有没有想过如果我们这样定义obj:const obj = { foo: "foo", bar: "bar", baz: { a: 1 } };那这个baz中的a就不会是响应式的。
我们现在只做了对象的一层响应式,我们应该去递归对象中的每一个元素都变成响应式的。
function defineReactive(obj, key, val) {
// 递归
observe(val)
Object.defineProperty(obj, key, {
...
});
}
function observe(obj) {
if (typeof obj !== "object" || obj == null) {
// 希望传入的时obj
return;
}
Object.keys(obj).forEach((key) => {
defineReactive(obj, key, obj[key]);
});
}
const obj = { foo: "foo", bar: "bar", baz: { a: 1 } };
observe(obj)
这样我们就能将obj变成一个完全的响应式对象了。
但是这里仍存在问题,当用户直接obj.baz = {a:100}赋值一个新的对象,这个对象就丢失了响应式。所以我们在赋值的监听器中应该也加上响应式的转化。
set(newVal) {
if (newVal !== val) {
// 如果传入的newVal依旧是Obj,需要做响应化处理
observe(newVal)
val = newVal;
}
}
这时有没有想到在vue中如果我们想给对象设置一个新的属性,我们需要用到$set(),我们这里也可以这么做一下。也很简单,我们也封装一个set方法,然后将对象进行一个响应式的转化就行了。
function set(obj, key, val) {
defineReactive(obj, key, val)
}
3. 整个过程原理分析
- new Vue() 首先执行初始化,对data执行响应化处理,这个过程发生在Observer中
- 同时对模板执行编译,找到其中动态绑定的数据,从data中获取并初始化视图,这个过程发生在Compile中
- 同时定义一个更新函数和Watcher,将来对应数据变化时Watcher会调用更新函数
- 由于data的某个key在一个视图中可能出现多次,所以每个key都需要一个管家Dep来管理多个Watcher
- 将来data中数据一旦发生变化,会首先找到对应的Dep,通知所有Watcher执行更新函数
涉及类型介绍
- CVue:框架构造函数
- Observer:执行数据响应化(分辨数据是对象还是数组)
- Compile:编译模板,初始化视图,收集依赖(更新函数、watcher创建)
- Watcher:执行更新函数(更新dom)
- Dep:管理多个Watcher,批量更新
4. CVue构造函数以及数据响应化
根据上面的分析,我们先来实现一下CVue的构造函数,并且实现一下数据响应化。
function defineReactive(obj, key, val) {
observe(val);
Object.defineProperty(obj, key, {
get() {
console.log("get", key);
return val;
},
set(newVal) {
if (newVal !== val) {
console.log("set " + key + ":" + newVal);
observe(newVal);
val = newVal;
}
},
});
}
function observe(obj) {
if (typeof obj !== "object" || obj == null) {
return;
}
new Observer(obj)
}
// 创建CVue构造函数
class CVue {
constructor(options) {
this.$options = options
this.$data = options.data
// 数据响应化
observe(this.$data)
}
}
// 根据对象类型决定如何做响应化
class Observer {
constructor(value) {
this.value = value;
// 判断数据类型
if (typeof value === "object") {
this.walk(value);
}
}
// 对象数据响应化
walk(obj) {
Object.keys(obj).forEach((key) => {
defineReactive(obj, key, obj[key]);
});
}
}
这里新加了一个Observer类专门用来执行数据响应化。
但是我们这里想访问$data还得专门去实例.data访问,这里我们可以做个代理,省去这一步。
// 代理函数,方便用户直接访问$data中的数据
function proxy(vm, sourceKey) {
Object.keys(vm[sourceKey]).forEach(key => {
Object.defineProperty(vm, key, {
get() {
return vm[sourceKey][key]
},
set(newVal) {
vm[sourceKey][key] = newVal
}
})
})
}
class CVue {
constructor(options) {
this.$options = options
this.$data = options.data
// 数据响应化
observe(this.$data)
// 代理
proxy(this, '$data')
}
}
这样就能直接通过实例.XXX来访问数据了。
5. Compile编译器原理以及实现
响应式数据处理完成之后我们就来实现编译模板语法
上面就是编译的大致流程,我们就来一个个实现
// c-compile.js
class Compiler {
constructor(el, vm) {
this.$vm = vm;
this.$el = document.querySelector(el);
}
}
在创建vue实例的时候创建编译器,传入组件实例与根元素
// cvue.js
class CVue {
constructor(options) {
// 创建编译器
new Compiler(this.$options.el, this)
}
}
于是我们就可以根据根元素来遍历el树
// c-compile.js
constructor(el, vm) {
if (this.$el) {
// 执行编译
this.compile(this.$el);
}
}
// 编译函数
compile(el) {
// 遍历el树
const childNodes = el.childNodes;
// childNodes只是具有可迭代性并不是数组,用from转成真的数组
Array.from(childNodes).forEach((node) => {
// 判断是否是元素
if (this.isElement(node)) {
console.log("编译元素" + node.nodeName);
// 渲染元素
this.compileElement(node);
} else if (this.isInter(node)) {
console.log("编译插值绑定" + node.textContent);
// 添加内容
this.compileText(node);
}
// 递归子节点
if (node.childNodes && node.childNodes.length > 0) {
this.compile(node);
}
});
}
上面我们使用到了isElement和isInter来判断是否是元素(html标签)与插值(模板语法的内容),然后又用到两个方法来渲染和添加内容。我们就来依次实现一下。
isElement(node) {
return node.nodeType === 1;
}
isInter(node) {
// 首先是文本标签,其次内容是{{XXX}}
return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent);
}
compileText(node) {
// 因为我们在判断是否为插值之后直接调用当前方法,
// RegExp.$1一定会拿到匹配的内容不会被后面匹配的覆盖
node.textContent = this.$vm[RegExp.$1]; // {{ abc }} -> abc
}
compileElement(node) {
// 节点是元素
// 遍历其属性列表
const nodeAttrs = node.attributes;
Array.from(nodeAttrs).forEach((attr) => {
// 规定:指定以c-xx="oo"定义 c-text="counter"
const attrName = attr.name; // c-xx c-text
const exp = attr.value; // oo counter
// 判断是否为指令
if (this.isDirective(attrName)) {
const dir = attrName.substring(2); // xx text
// 如果存在指令,执行指定
this[dir] && this[dir](node, exp);
}
// 判断是否为事件
if (this.isEvent(attrName)) {
// 获取绑定的事件名
const eventName = attrName.substring(1)
// 获取绑定的回调函数
const Fn = this.getFn(exp)
// 获取参数的具体值
const args = []
Fn.args.forEach(arg => {
if(this.$vm[arg]) {
args.push(this.$vm[arg])
} else {
args.push(arg)
}
})
// 添加事件监听
node.addEventListener(eventName, () => {
this.$vm.$options.methods[Fn.fnName].call(this.$vm, ...args)
})
}
});
}
isDirective(attr) {
return attr.indexOf("c-") === 0;
}
isEvent(attr) {
return attr.indexOf("@") === 0;
}
getFn(func) {
// 先用正则匹配,取得符合参数模式的字符串.
// 第一个分组是这个: ([^)]*) 非右括号的任意字符
// console.log(func.toString().match(/(.*)\((.*)\)/));
var fnName = func.toString().match(/(.*)\((.*)\)/)[1];
var Args = func.toString().match(/(.*)\((.*)\)/)[2];
// 用逗号来分隔参数(arguments string).
return {
fnName,
args: Args.split(",").map(function(arg) {
// 去除注释(inline comments)以及空格
return arg.replace(/\/\*.*\*\//, "").trim();
}).filter(function(arg) {
// 确保没有 undefined.
return arg;
})
}
}
// c-text 渲染内容
text(node, exp) {
node.textContent = this.$vm[exp];
}
// c-html 渲染html
html(node, exp) {
node.innerHTML = this.$vm[exp];
}
// c-model
model(node, exp) {
// 完成赋值和更新
node.value = this.$vm[exp]
// 事件监听
node.addEventListener("input", e => {
// 新值赋值给数据即可
this.$vm[exp] = e.target.value
});
}
<body>
<div id="app">
<p @click="add(num)">{{counter}}</p>
<p c-text="counter"></p>
<p c-html="desc"></p>
<input type="text" c-model="desc">
</div>
<script src="cvue.js"></script>
<script src="c-compile.js"></script>
<script>
const app = new CVue({
el:'#app',
data: {
counter: 1,
desc: '<span style="color: red;">CVue</span>'
},
methods: {
add(val) {
this.counter += val
},
},
})
setInterval(() => {
app.counter++
}, 1000);
</script>
</body>
正常显示
6. 视图更新原理以及实现
依赖收集
视图中会用到data中某key,这称为依赖。同一个key可能出现多次,每次都需要收集出来用一个Watcher来维护它们,此过程称为依赖收集。 多个Watcher需要一个Dep来管理,需要更新时由Dep统一通知。
实现思路
- defineReactive为每一个key创建一个Dep实例
- 初始化视图时读取某个key,创建一个watcher
- 触发对应key的getter方法时,就将对于的watcher添加到这个key的Dep上
- 当这个值发生变化时,setter触发,就从它的Dep中通知所有的watcher更新
所以又回到了这个图的来。
我们先来实现watcher,监听数据的变化并通知更新。
我们现在还没有Dep来进行管理,我们就简单一点,创建一个全局的watcers来监听所有的参数。
// cvue.js
const watchers = [] // 临时保存watcher
class Watcher {
constructor(vm, key, updateFn) {
this.vm = vm
this.key = key
// 当前key对于的更新方法
this.updateFn = updateFn
watchers.push(this)
}
update() {
this.updateFn.call(this.vm, this.vm[this.key])
}
}
// 在setter中统一将所有watcher进行更新
set(newVal) {
if (newVal !== val) {
...
// 通知更新
watchers.forEach(w => w.update())
}
},
这里我们在创建watcher时传入了一个更新方法,用来更新页面中的数据。
// compile.js
compileText(node) {
node.textContent = this.$vm[RegExp.$1]; // {{ abc }} -> abc
}
isDirective(attr) {
return attr.indexOf("c-") === 0;
}
// c-text 渲染内容
text(node, exp) {
node.textContent = this.$vm[exp];
}
// c-html 渲染html
html(node, exp) {
node.innerHTML = this.$vm[exp];
}
// c-model
model(node, exp) {
node.value = this.$vm[exp]
node.addEventListener("input", e => {
this.$vm[exp] = e.target.value
});
}
这些是我们之前的渲染方法,我们要写更新方法的话需要在每个里面都写上它对应的更新方法。这样代码就会变的臃肿且可读性差。于是乎,我们就创建一个update方法,为每个指令再创建一个对应的更新方法,这样我们就只需要统一调用update方法,传入对应的更新名,再调用对应的方法就可以了。
// c-compile.js
update(node, exp, dir) {
// 初始化
// 指令对应的更新函数xxUpdater
const fn = this[dir + 'Updater']
fn && fn(node, this.$vm[exp])
// 更新处理,封装一个更新函数,可以更新对应的dom元素
new Watcher(this.$vm, exp, function(val) {
fn && fn(node, val)
})
}
modelUpdater(node, value) {
node.value = value;
}
textUpdater(node, value) {
node.textContent = value;
}
htmlUpdater(node, value) {
node.innerHTML = value
}
接着改造之前的渲染方法。
compileText(node) {
this.update(node, RegExp.$1, 'text')
}
text(node, exp) {
this.update(node, exp, 'text')
}
html(node, exp) {
this.update(node, exp, 'html')
}
model(node, exp) {
this.update(node, exp, "model");
node.addEventListener("input", e => {
this.$vm[exp] = e.target.value
});
}
这样我们的属性监听就完成了。但是我们现在只要有一个key发生改变,所有的watcher都要重新执行更新方法。于是就需要用Dep来搜集管理watcher,让key与watcher一一对应,每次只需要更新发生变化的key的所有watcher。
现在我们梳理一下我们要做的步骤:
- 为每一个key创建一个Dep
- 一个key的Dep收集所有的watcher
- key的值发生变化时通知对应Dep中的所有wathcer更新。
我们先声明Dep:
// cvue.js
// Dep:依赖,管理某个key相关Watcher势力
class Dep {
constructor() {
// 存放watcher
this.deps = []
}
addDep(dep) {
this.deps.push(dep)
}
// 通知更新
notify() {
this.deps.forEach(dep => dep.update())
}
}
创建watcher触发getter
class Watcher {
constructor(vm, key, updateFn) {
// Dep.target静态属性上设置为当前watcher实例
Dep.target = this
this.vm[this.key] // 读取触发了getter
Dep.target = null // 收集完就置空
}
}
收集依赖,创建Dep实例
function defineReactive(obj, key, val) {
// 创建一个Dep和当前key一一对应
const dep = new Dep()
Object.defineProperty(obj, key, {
get() {
// 依赖收集
Dep.target && dep.addDep(Dep.target)
return val;
},
set(newVal) {
if (newVal !== val) {
// 通知更新
dep.notify()
}
},
});
}
最后我们再来梳理一下我们整个CVue的执行过程
7. 优化——数组的响应化
上面我们的CVue通过Object.defineProperty``的方法实现了对象的数据响应式,但是我们在修改数组的时候通常是用push、shift、pop...`来修改数组。这些方法对数组的修改是无法被监听到的。
这些方法就是我们来进行响应化的切入点。而有人说直接赋值呢,直接赋值修改整个数组,会被响应化后的data对象监听,所以不需要考虑。
Vue官方对此的做法是重写Array的这七种方法,在重写的方法里我们就可以做我们想做的事。
// 获取array的原型
const orginalProto = Array.prototype;
// 我们不能直接改array的原型,我们创建一个array原型的备份
const arrayProto = Object.create(orginalProto);
// 重写的array的7大方法
const methodsToPatch = [
"push",
"pop",
"shift",
"unshift",
"splice",
"sort",
"reverse",
];
// 重写原型的方法
methodsToPatch.forEach((method) => {
// 获取原生方法
const original = orginalProto[method];
// 执行原方法
original()
// 我们可以进行的后续操作
});
这里我们需要做的后续方法就是对数组执行收集到的依赖,执行更新方法。
对于对象来说,对象不会有很多的属性,可以为每个属性设置getter和setter,但是数组可以有很多的元素,给每个元素都添加会极大消耗性能。
这里官方的做法又给我秀到了。我们在对象进行defineReactive来创建Dep存储依赖,数组用不了。那干脆直接在Observer里创建一个Dep来存储数组的依赖,直接通过def函数来为数据创建一个__ob__属性存放当前的Observer实例。这样数组就能查看自己的依赖,然后创建了一个dependArray方法,从__ob__拿到Dep实例,在编译器中进行触发依赖收集。在getter中判别是否为数组,是数组就调用dependArray。
// 添加属性值
function def(obj, key, val, enumerable) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true,
});
}
// 根据类型决定如何做响应化
class Observer {
constructor(value) {
this.value = value;
this.dep = new Dep();
def(value, "__ob__", this);
// 判断数据类型
if (typeof value === "object") {
...
}
if (Array.isArray(value)) {
this.changeArray(value);
}
}
// 数组数据响应化。。。
changeArray(arr) {
arr.__proto__ = arrayProto;
const keys = Object.keys(arr);
for (let i = 0; i < keys.length; i++) {
observe(arr[i]);
}
}
}
// 递归转化所有数组元素
function dependArray (value) {
value.__ob__.dep.addDep(e.__ob__.dep.target)
for (let e, i = 0, l = value.length; i < l; i++) {
e = value[i]
// 对象元素也有__ob__也要搜集依赖
e.__ob__.dep.addDep(e.__ob__.dep.target)
if (Array.isArray(e)) {
dependArray(e)
}
}
}
这里我们的e.__ob__.dep.addDep(e.__ob__.dep.target)通过__ob__实例添加依赖的写法太臃肿了。我们来改写一下依赖收集的方法。
class Watcher {
addDep(dep) {
dep.addDeps(this);
}
}
class Dep {
depend() {
if (Dep.target) {
Dep.target.addDep(this);
}
}
addDeps(dep) {
this.deps.push(dep);
}
}
dep为Dep实例,Dep.target又为Watcher实例,这样互相引用也是一一对应的一种体现我们data中的每个属性key与dep是一一对应的,这样我们就能串起来。
这样 e.__ob__.dep.addDep(e.__ob__.dep.target)就可以改写成e.__ob__.dep.depend()。
最后在调用7大方法的时候我们可以通知依赖进行更新。
methodsToPatch.forEach((method) => {
// 获取原生方法
const original = orginalProto[method];
// 重写原型上的方法
def(arrayProto, method, function mutator(...args) {
const result = original.apply(this, args);
const ob = this.__ob__;
// 通知更新
ob.dep.notify();
return result;
});
});
这样就实现了一个简易的CVue了。
8. 最后附上完整代码
cvue.js
// 获取array的原型
const orginalProto = Array.prototype;
// 我们不能直接改array的原型,我们创建一个array原型的备份
const arrayProto = Object.create(orginalProto);
// 重写的array的7大方法
const methodsToPatch = [
"push",
"pop",
"shift",
"unshift",
"splice",
"sort",
"reverse",
];
methodsToPatch.forEach((method) => {
// 获取原生方法
const original = orginalProto[method];
// 重写原型上的方法
def(arrayProto, method, function mutator(...args) {
const result = original.apply(this, args);
const ob = this.__ob__;
ob.dep.notify();
return result;
});
});
// 添加属性值
function def(obj, key, val, enumerable) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true,
});
}
// 递归转化所有数组元素
function dependArray (value) {
value.__ob__.dep.depend();
for (let e, i = 0, l = value.length; i < l; i++) {
e = value[i]
e && e.__ob__ && e.__ob__.dep.depend()
if (Array.isArray(e)) {
dependArray(e)
}
}
}
function defineReactive(obj, key, val) {
// 递归
observe(val);
// 创建一个Dep和当前key一一对应
const dep = new Dep();
// let childOb = observe(val)
Object.defineProperty(obj, key, {
get() {
// 依赖收集
if (Dep.target) {
dep.depend();
if (Array.isArray(val)) {
dependArray(val)
}
}
return val;
},
set(newVal) {
if (newVal !== val) {
// console.log("set " + key + ":" + newVal);
// 如果传入的newVal依旧是Obj,需要做响应化处理
observe(newVal);
val = newVal;
// 通知更新
// watchers.forEach(w => w.update())
dep.notify();
}
},
});
}
function observe(obj) {
if (typeof obj !== "object" || obj == null) {
// 希望传入的是obj
return;
}
// 创建Observer实例
new Observer(obj);
}
// 代理函数,方便用户直接访问$data中的数据
function proxy(vm, sourceKey) {
Object.keys(vm[sourceKey]).forEach((key) => {
Object.defineProperty(vm, key, {
get() {
return vm[sourceKey][key];
},
set(newVal) {
vm[sourceKey][key] = newVal;
},
});
});
}
// 创建CVue构造函数
class CVue {
constructor(options) {
this.$options = options;
this.$data = options.data;
// 数据响应化
observe(this.$data);
// 代理
proxy(this, "$data");
// 创建编译器
new Compiler(this.$options.el, this);
}
}
// 根据对象类型决定如何做响应化
class Observer {
constructor(value) {
this.value = value;
this.dep = new Dep();
def(value, "__ob__", this);
// 判断数据类型
if (typeof value === "object") {
this.walk(value);
}
if (Array.isArray(value)) {
this.changeArray(value);
}
}
// 对象数据响应化
walk(obj) {
Object.keys(obj).forEach((key) => {
defineReactive(obj, key, obj[key]);
});
}
// 数组数据响应化。。。
changeArray(arr) {
arr.__proto__ = arrayProto;
const keys = Object.keys(arr);
for (let i = 0; i < keys.length; i++) {
observe(arr[i]);
}
}
}
// 观察者:保存更新函数,值发生变化调用更新函数
// const watchers = []
class Watcher {
constructor(vm, key, updateFn) {
this.vm = vm;
this.key = key;
this.updateFn = updateFn;
// watchers.push(this)
// Dep.target静态属性上设置为当前watcher实例
Dep.target = this;
this.vm[this.key]; // 读取触发了getter
Dep.target = null; // 收集完就置空
}
update() {
this.updateFn.call(this.vm, this.vm[this.key]);
}
addDep(dep) {
dep.addDeps(this);
}
}
// Dep:依赖,管理某个key相关Watcher势力
class Dep {
constructor() {
this.deps = [];
}
depend() {
if (Dep.target) {
Dep.target.addDep(this);
}
}
addDeps(dep) {
this.deps.push(dep);
}
notify() {
this.deps.forEach((dep) => dep.update());
}
}
c-compile.js
// 编译器
// 递归遍历dom树
// 判断节点类型,如果是文本,则判断是否插值绑定
// 如果是元素,则遍历属性判断是否是指令或事件,然后递归子元素
class Compiler {
constructor(el, vm) {
this.$vm = vm;
this.$el = document.querySelector(el);
if (this.$el) {
// 执行编译
this.compile(this.$el);
}
}
compile(el) {
// 遍历el树
const childNodes = el.childNodes;
Array.from(childNodes).forEach((node) => {
// 判断是否是元素
if (this.isElement(node)) {
// console.log("编译元素" + node.nodeName);
this.compileElement(node);
} else if (this.isInter(node)) {
// console.log("编译插值绑定" + node.textContent);
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) => {
// 规定:指定以c-xx="oo"定义 c-text="counter"
const attrName = attr.name; // c-xx c-text
const exp = attr.value; // oo counter
if (this.isDirective(attrName)) {
const dir = attrName.substring(2); // xx text
// 如果存在指令,执行指定
this[dir] && this[dir](node, exp);
}
// 判断是否为事件
if (this.isEvent(attrName)) {
const eventName = attrName.substring(1);
const Fn = this.getFn(exp);
// const method = this.$vm.$options.methods[Fn.fnName]
const args = [];
Fn.args.forEach((arg) => {
if (this.$vm[arg]) {
args.push(this.$vm[arg]);
} else {
args.push(arg);
}
});
node.addEventListener(eventName, () => {
this.$vm.$options.methods[Fn.fnName].call(this.$vm, ...args);
});
}
});
}
isDirective(attr) {
return attr.indexOf("c-") === 0;
}
isEvent(attr) {
return attr.indexOf("@") === 0;
}
getFn(func) {
// 先用正则匹配,取得符合参数模式的字符串.
// 第一个分组是这个: ([^)]*) 非右括号的任意字符
// console.log(func.toString().match(/(.*)\((.*)\)/));
var fnName = func.toString().match(/(.*)\((.*)\)/)[1];
var Args = func.toString().match(/(.*)\((.*)\)/)[2];
// 用逗号来分隔参数(arguments string).
return {
fnName,
args: Args.split(",")
.map(function (arg) {
// 去除注释(inline comments)以及空格
return arg.replace(/\/\*.*\*\//, "").trim();
})
.filter(function (arg) {
// 确保没有 undefined.
return arg;
}),
};
}
update(node, exp, dir) {
// 初始化
// 指令对应的更新函数xxUpdater
const fn = this[dir + "Updater"];
fn && fn(node, this.$vm[exp]);
// 更新处理,封装一个更新函数,可以更新对应的dom元素
new Watcher(this.$vm, exp, function (val) {
fn && fn(node, val);
});
}
// c-text
text(node, exp) {
this.update(node, exp, "text");
}
// c-html
html(node, exp) {
this.update(node, exp, "html");
}
// c-model
model(node, exp) {
// update方法只完成赋值和更新
this.update(node, exp, "model");
// node.value = this.$vm[exp]
// 事件监听
node.addEventListener("input", e => {
// 新值赋值给数据即可
this.$vm[exp] = e.target.value
});
}
modelUpdater(node, value) {
// 表单元素赋值
node.value = value;
}
textUpdater(node, value) {
node.textContent = value;
}
htmlUpdater(node, value) {
node.innerHTML = value;
}
}