github: https://github.com/OUDUIDUI/v...
Vue的设计思维
Vue
设计思维参考了MVVM
模型,行将视图View
和行为Model
抽象化,行将视图UI和业务逻辑离开来,而后通过ViewModel
层来实现双向数据绑定。
MVVM
与 MVC
最大的不同就是MVVM
实现了 View
和 Model
的主动同步,也就是当Model
的属性扭转时,咱们不必再本人手动操作 Dom
元素,来扭转 View
的显示,而是扭转属性后该属性对应 View 层显示会主动扭转。
MVVM
框架的三个因素:数据响应式、模板引擎及其渲染。
数据响应式
- 监听数据变动并在视图中更新
- 在
Vue2.x
中,是依据Object.defineProperty()
来实现数据响应式的
模板引擎
- 提供形容视图的模板语法
Vue
的插槽{{}}
和指令v-bind
、v-on
、v-model
等
渲染
- 将模板渲染成
HTML
进行显示
- 将模板渲染成
数据响应式原理
在JavaScript
的对象Object
中有一个属性叫拜访器属性,其中有[[Get]]
和[[Set]]
个性,它们别离是获取函数或设置函数,即在获取对象特定属性的时候回调用到。
而拜访器属性是不能间接定义的,必须应用Object.defineProperty()
进行定义。
const obj = { _name: 'Matt'};Object.defineProperty(obj, 'name', { get() { return this._name; }, set(newVal) { console.log('set name') this._name = newVal; }})console.log(obj.name); // 'Matt'obj.name = 'OUDUIDUI'; // 'set name'console.log(obj.name); // 'Henry'
而Vue2.x
就是在set
函数中进行监听,当数据发生变化了,就会进行响应操作。
因而,咱们能够简略实现一个Vue
中的defineReactive
函数。
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>reactive app</title></head><body><div id="app"></div><script> /** * defineReactive : 将对象中某一个属性设置为响应式数据 * @param obj<Object>: 对象 * @param key<any>: key名 * @param val<any>: 初始值 */ function defineReactive(obj, key, val) { Object.defineProperty(obj, key, { get() { console.log(`get ${key}`) return val; // 此时val存在obj的闭包外面 }, set(newVal) { console.log(`set ${key}`) if (newVal !== val) { val = newVal; update(); // 更新函数 } } }) } /** * update : 更新函数,从新渲染app DOM */ function update() { const app = document.getElementById('app'); app.innerHTML = `obj.time = ${obj.time}` } const obj = {}; defineReactive(obj, 'time', new Date().toLacaleTimeString()); // 将obj进行响应式解决 setInterval(() => obj.time = new Date().toLacaleTimeString(), 1000); // 定时更新obj.time的值</script>
在代码中,咱们在set
中,调用了update
更新函数,因而咱们定时器每更新obj.time
一次,update
函数就会被调用一次,因而页面数据也会更新一次。这时候,咱们就简略的实现了数据响应式。
但defineReactive
函数有个问题,就是一次只能对一个属性值进行响应式解决,而且如果这个属性是个对象的话,咱们更改对象外面的值的时候,是实现不了响应式的。
const obj = {};defineReactive(obj, 'info', {name: 'OUDUIDUI', age: 18}); // 将obj进行响应式解决setTimeout(() => obj.info.age++, 1000); // 这时候不会触发set函数
因而,咱们须要一个新的办法去实现对整个对象进行响应式解决,在Vue
中这个办法叫observe
。
在这个函数中,咱们先须要对传入的obj
进行类型判断,而后对对象进行遍历,对每一个属性进行响应式解决。这个中央须要对数组做解决,这个放到前面再说。
/** * observe: 将整个对象设置为响应式数据 * @param obj<Object>: 对象 */function observe(obj) { // 如果obj不是对象的话,跳出函数 if (typeof obj !== "object" || obj === null) { return; } // 判断传入obj的类型 if(Array.isArray(obj)){ // TODO }else { // 遍历obj所有所有key,做响应式解决 Object.keys(obj).forEach(key => { defineReactive(obj, key, obj[key]); }) }}
同时,咱们须要实现对这个对象一个递归解决,因而咱们须要批改一下defineReactive
函数。咱们只须要在最开始的中央,调用一次observe
函数,如果传入的val
是对象,就会进行递归响应式解决,如果不是就返回。
function defineReactive(obj, key, val) { observe(val); // 递归解决:如果val是对象,持续做响应式解决 Object.defineProperty(obj, key, { ... })}
咱们来测试一下:
const obj = { time: new Date().toLocaleTimeString(), info: { name: 'OUDUIDUI', age: 18 }};observe(obj);setInterval(() => { obj.time = new Date().toLocaleTimeString();}, 1000)setTimeout(() => { obj.info.age++;}, 2000)
这里还有一个小问题,就是如果obj
本来有一个属性是惯例类型,即字符串、数值等等,而后再将其改为援用类型时,如对象、数值等,该援用类型外部的属性,是没有响应式的。比方下来这种状况:
const obj = { text: 'Hello World',};observe(obj); // 响应式解决obj.text = { en: 'Hello World' }; // 将obj.text由字符串改成一个对象setTimeout(() => { obj.text.en = 'Hi World'; // 此时批改text对象属性页面是不会更新的,因为obj.text.en不是响应式数据}, 2000)
对于这种状况,咱们只须要在defineReactive
函数中,set
的时候调用一下observe
函数,将newVal
传入,如果是对象就进行响应式解决,否则就间接返回。
function defineReactive(obj, key, val) { observe(val); Object.defineProperty(obj, key, { get() { console.log(`get ${key}`) return val; }, set(newVal) { console.log(`set ${key}`) if (newVal !== val) { observe(newVal); // 如果newVal是对象,再次做响应式解决 val = newVal; update(); } } })}
咱们测试一下。
function update() { const app = document.getElementById('app'); app.innerHTML = `obj.text = ${JSON.stringify(obj.text)}`}const obj = { text: 'Hello World'};// 响应式解决observe(obj);setTimeout(() => { obj.text = { // 将obj.text由字符串改成一个对象 en: 'Hello World' }}, 2000)setTimeout(() => { obj.text.en = 'Hi World';}, 4000)
最初咱们来实现后面楼下的一个问题,就是数组的响应式解决。
之所以数组须要非凡解决,因为数组有七个自带办法能够去解决数组的内容,别离是push
、pop
、shift
、unshift
、reverse
、sort
、splice
,它们都是能够批改数组自身的。
所以,咱们须要对七个办法进行监听。咱们能够先克隆一个新的数组原型,而后在新的原型中,新建这七个办法,先执行对应的办法操作后,进行数据响应式更新解决。
// 数组响应式const originalProto = Array.prototype;const arrayProto = Object.create(originalProto); // 以Array.prototype为原型翻新一个新对象['push', 'pop', 'shift', 'unshift','reverse', 'sort', 'splice'].forEach(method => { arrayProto[method] = function () { // 原始操作 originalProto[method].apply(this, arguments); // 笼罩操作:告诉更新 update(); }})
而后持续实现observe
函数操作。
如果类型是数组的话,将其的原型进行笼罩,而后再数组每一个元素进行响应式解决。
function observe(obj) { if (typeof obj !== "object" || obj === null) { return; } // 判断传入obj的类型 if (Array.isArray(obj)) { // 笼罩原型 obj.__proto__ = arrayProto; // 对数组外部原型执行响应式 for (let i = 0; i < obj.length; i++) { observe(obj[i]); } } else { Object.keys(obj).forEach(key => { defineReactive(obj, key, obj[key]); }) }}
测试一下:
function update() { const app = document.getElementById('app'); app.innerHTML = `obj.nums = ${JSON.stringify(obj.nums)}`}const obj = { nums: [4, 2, 3]};// 响应式解决observe(obj);setTimeout(() => { obj.nums.push(1);}, 2000)setTimeout(() => { obj.nums.sort((a,b) => a - b);}, 4000)
简略手写Vue
原理剖析
当咱们应用vue
的时候,首先都会创立一个Vue
实例,而后在外面初始化element
、data
、methods
等等。
const app = new Vue({ el: '#app', data: { count: 1 }, methods:{}});
而后咱们能够在data
外面设置一些变量,而这些变量会被解决为响应式数据,而后咱们就能够应用模板语句去渲染data
数据。
<div id="app"> <p>{{counter}}</p></div>
所以咱们须要实现的性能就是对data
进行响应式解决、编译和渲染模板、以及数据变动时更新模板。
因而咱们创立Vue
实例须要实现以下内容:
- 对
data
执行响应式解决,这个过程产生在Observer
中; - 对模板执行编译,找到其中动静绑定的数据,从
data
中获取并初始化视图,这个过程产生在Compile
中; - 每创立一个响应式数据,同时定义一个更新函数和
Watcher
,未来对应数据变动时Watcher
会调用更新函数; - 因为
data
的某个key
在一个视图中可能呈现屡次,所以每个key
都须要一个依赖Dependence
来治理多个Watcher
;未来data
中数据一旦发生变化,会首先找到对应的Dependence
,而后Dependence
告诉对应所有的Watcher
执行更新函数。
实现
数据响应式
首先咱们新建一个vue.js
,创立一个Vue
的类,在constructor
对参数数据进行保留。
/** * Vue: * 1. 对data选项做响应式解决 * 2. 编译模板 * @param options<Object>: 蕴含el、data、methods等等 */class Vue { constructor(options) { this.$options = options; this.$data = options.data; // data选项 // 对data进行响应式解决 observe(this.$data); }}
observe()
办法跟后面所说的相似,只不过咱们把大部分内容放入Observer
类中,因为咱们须要对每一个响应式数据进行监听并告诉Dep
。
/** * observe: 将整个对象设置为响应式数据 * @param obj<Object>: 对象 */function observe(obj) { // 如果obj不是对象的话,跳出函数 if (typeof obj !== "object" || obj === null) { return; } // 响应式解决 new Observer(obj);}
Observer
中constructor
构造函数的内容,根本就是之前observe
办法中的内容,以及类中的defineReactive
办法也跟后面讲的统一,这里就不说了。
惟一不同的是,这里不再是调用update
函数,而在前面咱们须要创立一个依赖Dependence
实例并调用,当初咱们先留空着。
/** * Observer: * 1. 依据传入value的类型做响应的响应式解决 * @param value<Object || Array> */class Observer { constructor(value) { this.value = value; // 数据类型判断 if(Array.isArray(value)){ // 笼罩原型 value.__proto__ = this.getArrayProto(); // 对数组外部原型执行响应式 for (let i = 0; i < value.length; i++) { observe(value[i]); } }else { // 遍历obj所有所有key,做响应式解决 Object.keys(value).forEach(key => { this.defineReactive(value, key, value[key]); }) } } getArrayProto() { const self = this; const originalProto = Array.prototype; const arrayProto = Object.create(originalProto); ['push', 'pop', 'shift', 'unshift','reverse', 'sort', 'splice'].forEach(method => { arrayProto[method] = function () { originalProto[method].apply(self, arguments); // TODO 告诉变动 } }) return arrayProto; } /** * defineReactive : 将对象中某一个属性设置为响应式数据 * @param obj<Object>: 对象 * @param key<any>: key名 * @param val<any>: 初始值 */ defineReactive(obj, key, val) { observe(val); Object.defineProperty(obj, key, { get() { Dependence.target && dep.addDep(Dependence.target); return val; }, set(newVal) { if (newVal !== val) { observe(newVal); val = newVal; // TODO 告诉变动 } } }) }}
当初咱们根本实现了对data
数据进行响应式解决。
但当初咱们在JavaScript
中创立了Vue
实例后,咱们无奈间接在实例中获取到data
数据,而是须要通过实例中的$data
中获取到data
的内容。
const app = new Vue({ el: '#app', data: { desc: 'HelloWorld', }});console.log(app.desc); // undefinedconsole.log(app.$data.desc); // 'HelloWorld'
因为咱们得对data
中的数据实现一下代理,代理的实现也是通过对象的拜访器属性实现,这里也不多说。
class Vue { constructor(options) { this.$options = options; this.$data = options.data; observe(this.$data); // 代理 proxy(this); }}/** * proxy: 数据代理 * @param vm<Object> */function proxy(vm) { Object.keys(vm.$data).forEach(key => { Object.defineProperty(vm, key, { get() { return vm.$data[key] }, set(v) { vm.$data[key] = v; } }) })}
这时候咱们就能够用app.desc
拜访到data.desc
属性了。
模板编译和渲染
在咱们实现数据响应式后,咱们就能够对模板进行编译和渲染,这时候就须要来实现Compile
类。
class Vue { constructor(options) { this.$options = options; this.$data = options.data; // data选项 observe(this.$data); proxy(this); // 模板编译和渲染 new Compile(options.el, this); }}
Compile
类的构造函数接管两个参数,一个是element
,一个是Vue
实例中的this
,这个实际上就是View Model
的数据,也是咱们在Vue
中常见的vm
。
在构造函数中,先对传入数据进行保留,而后获取节点,如果节点存在的话,就开始进行编译解决。
/** * Compile: * 1. 解析模板 * a. 解决插值 * b. 解决指令和事件 * c. 以上两者初始化和更新 * @param el * @param vm */class Compile { constructor(el, vm) { this.$vm = vm; this.$el = document.querySelector(el); if(this.$el){ // 编译节点 this.compile(this.$el); } } /** * compile: 递归节点,对节点进行编译 * @param el */ compile(el){ }}
首先,咱们须要对节点进行递归遍历,而后通过nodeType
辨认出以后节点的信息,如果是元素节点的话,咱们须要对其进行指令和事件处理,如果是文本节点的话,同时含有{{}}
的话,咱们须要对齐进行文本替换解决。
class Compile { constructor(el, vm) { ... } /** * compile: 递归节点,对节点进行编译 * @param el */ compile(el){ // 遍历el子节点,判断他们类型做相应的解决 const childNodes = el.childNodes; childNodes.forEach(node => { if(node.nodeType === 1){ // 元素 console.log('元素', node.nodeName); // TODO 指令和事件处理 }else if(this.isInter(node)){ // 文本 console.log('文本', node.textContent); // TODO 文本替换解决 } // 递归 if(node.childNodes){ this.compile(node); } }) } // 判断是否为插值表达式 isInter(node) { return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent); }}
首先咱们来实现一下文本编译。
因为咱们后面判断的时候,应用过正则去判断node.textContent
,因而如果符合标准的话,咱们就能够通过RegExp.$1
获取到属性名,因而咱们就能够那属性名去data
中进行匹配。
class Compile { constructor(el, vm) { ... } compile(el){ const childNodes = el.childNodes; childNodes.forEach(node => { if(node.nodeType === 1){ // TODO 指令和事件处理 }else if(this.isInter(node)){ // 文本初始化 this.compileText(node); } if(node.childNodes){ this.compile(node); } }) } // 编译文本 compileText(node) { node.textContent = this.$vm[RegExp.$1]; }}
这时候,咱们能够测试一下。
<!doctype html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>vue app</title></head><body><div id="app"> <p>{{desc}}</p></div><script src="./src/vue.js"></script><script> const app = new Vue({ el: '#app', data: { desc: 'HelloWorld', }, });</script></body></html>
接下来,咱们简略实现一下指令和实现,这个demo
就实现一下v-text
、v-html
以及事件绑定@click
。
首先,当咱们递归节点的时候,当nodeType === 1
的时候,咱们得悉该节点是一个元素,就能够通过node.attributes
去获取该标签中的所有指令。而后通过遍历和辨认attrName
是否以v-
或者@
结尾的。
if(node.nodeType === 1) { // 元素 console.log('元素', node.nodeName); // 解决指令和事件 const attrs = node.attributes; Array.from(attrs).forEach(attr => { const attrName = attr.name; const exp = attr.value; if (attrName.startsWith('v-')) { // 解决指令 } if (attrName.indexOf('@') === 0) { // 处理事件 } })}
因为事件处理比较简单,所以咱们先来处理事件。
咱们只须要提取出事件的类型,而后将节点node
、办法名exp
和事件类型dir
进行事件监听。
这里须要次要的是,addEventListener
事件监听第二个参数的办法,须要绑定this.$vm
,因为在办法中有可能会用到data
数据。
class Compile { constructor(el, vm) { ... } compile(el){ const childNodes = el.childNodes; childNodes.forEach(node => { if(node.nodeType === 1){ const attrs = node.attributes; Array.from(attrs).forEach(attr => { const attrName = attr.name; const exp = attr.value; if(attrName.startsWith('v-')){ // 解决指令 } // 处理事件 if(attrName.indexOf('@') === 0){ const dir = attrName.substring(1); // 事件监听 this.eventHandler(node, exp, dir); } }) }else if(node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)){ console.log('文本', node.textContent); this.compileText(node); } if(node.childNodes){ this.compile(node); } }) } /** * eventHandler: 节点事件处理 * @param node: 节点 * @param exp: 函数名 * @param dir: 事件类型 */ eventHandler(node, exp, dir) { const fn = this.$vm.$options.methods && this.$vm.$options.methods[exp]; node.addEventListener(dir, fn.bind(this.$vm)); }}
当初来测试一下。
<!doctype html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>vue app</title></head><body><div id="app"> <button @click="add">测试</button></div><script src="./src/vue.js"></script><script> const app = new Vue({ el: '#app', data: { desc: 'HelloWorld' }, methods:{ test() { console.log(this.desc); } } });</script></body></html>
接下来来解决指令。
对不同指令的解决是不一样,因而得对每一种指令都须要新建一个更新函数。这里只实现以下v-text
、v-html
、v-model
。
每个办法名是与指令名统一,这有利于前面间接用指令名去查找。而后每个办法都承受两个参数——node
节点和exp
变量名。
class Compile { constructor(el, vm) { ... } compile(el){ ... } // v-text text(node, exp) { node.textContent = this.$vm[exp]; } // v-html html(node, exp) { node.innerHTML = this.$vm[exp]; } // v-model model(node, exp){ // 表单原生赋值 node.value = value; // 事件监听 node.addEventListener('input', e => { // 赋值实现双向绑定 this.$vm[exp] = e.target.value; }) }}
而后解决指令只须要间接查找一下this
有没有这个指令办法,有的话调用。
// 解决指令if(attrName.startsWith('v-')){ const dir = attrName.substring(2); this[dir] && this[dir](node, exp);}
最初试验一下。
<!doctype html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>vue app</title></head><body><div id="app"> <p v-text="desc"></p> <p v-html="desc2"></p> <input type="text" v-model="desc" /></div><script src="./src/vue.js"></script><script> const app = new Vue({ el: '#app', data: { counter: 1, desc: 'HelloWorld', desc2: `<span style="font-weight: bolder">Hello World</span>` } });</script></body></html>
数据更新
数据的更新就会用到Watcher
监听器和Dependence
观察者。
当咱们视图中用到了data
中某个属性key
,这称为依赖,比方<div>{{desc}}</div>
,desc
就是一个依赖。而同一个key
呈现屡次的时候,每一次都会创立一个Watcher
来保护它们,而这个过程称为依赖收集。然而但某个key
发生变化的时候,咱们须要通过该依赖下的所有Watcher
去更新,这时候就须要一个Dependence
来治理,须要更新的时候就由它来对立告诉。
在实现这个性能之前,咱们须要先来重构一个中央的代码。
就是咱们只需在模板中用到data
属性的中央须要创立一个Watcher
监听器,因而咱们须要在Compile
中创立。然而在其中咱们插值表达式用到了一个更新办法,每个指令各用到了一个更新办法。
因而咱们须要一个高级函数,将其都封装起来。也就是当用到每一种指令或插值表达式,咱们都会经验调用这个高级函数,因而咱们也能够在这个高级函数中创立Watcher
。
class Compile { constructor(el, vm) { ... } compile(el){ ... } /** * update: 高阶函数 —— 操作节点 * @param node: 节点 * @param exp: 绑定数据变量名 * @param dir: 指令名 */ update(node, exp, dir) { // 初始化 const fn = this[dir + 'Updater']; fn && fn(node, this.$vm[exp]); // TODO 创立监听器 } // 编译文本 compileText(node) { this.update(node, RegExp.$1, 'text'); } // v-text text(node, exp) { this.update(node, exp, 'text'); } textUpdater(node, value) { node.textContent = value; } // v-html html(node, exp) { this.update(node, exp, 'html'); } htmlUpdater(node, value) { node.innerHTML = value; } // v-model model(node, exp){ this.update(node,exp, 'model'); node.addEventListener('input', e => { this.$vm[exp] = e.target.value; }) } modelUpdater(node, value){ node.value = value; } eventHandler(node, exp, dir) { ... }}
紧接着,咱们就能够来创立Watcher
类。
这个类的性能其实很简略,就是保留这个更新函数,而后当数据更新的时候,咱们调用一下更新函数就能够了。
/** * Watcher: * 1. 监听器 —— 负责依赖更新 * @param vm * @param key: 绑定数据变量名 * @param updateFn: 更新函数 */class Watcher { constructor(vm, key, updateFn) { this.vm = vm; this.key = key; this.updateFn = updateFn; } update() { // 执行理论更新操作 this.updateFn.call(this.vm, this.vm[this.key]); }}
而后在高阶函数中调用。
update(node, exp, dir) { const fn = this[dir + 'Updater']; fn && fn(node, this.$vm[exp]); // 创立Watcher监听器 new Watcher(this.$vm, exp, function (val){ fn && fn(node, val); })}
而Dependence
这个类,次要就三个性能:
- 一个是在每一次将
data
响应式解决的时候,都要创立一个相应的空数组deps
,用于收集相应的监听器; - 第二个是再每一次创立新的
Watcher
,都要将其搁置对应的deps
数组中; - 第三个是每次数据更新的时候,咱们就要遍历对应的
deps
,告诉对应的所有监听器更新视图。
因而,咱们就能够来实现Dependence
类。
/** * Dependence: * 观察者 —— 负责告诉监听器更新 */class Dependence { constructor() { this.deps = []; } /** * addDep: 增加新的监听器 * @param dep */ addDep(dep) { this.deps.push(dep); } /** * notify: 告诉更新 */ notify() { this.deps.forEach(dep => dep.update()); }}
而后咱们在Observer
类中,实现数据响应式的时候,须要创立一个Dependence
实例,并且更新的时候告诉更新。
class Observer { constructor(value) { this.value = value; // 创立Dependence实例 this.dep = new Dependence(); ... } getArrayProto() { const self = this; const originalProto = Array.prototype; const arrayProto = Object.create(originalProto); ['push', 'pop', 'shift', 'unshift','reverse', 'sort', 'splice'].forEach(method => { arrayProto[method] = function () { originalProto[method].apply(self, arguments); // 笼罩操作:告诉更新 self.dep.notify(); } }) return arrayProto; } defineReactive(obj, key, val) { observe(val); const self = this; Object.defineProperty(obj, key, { get() { return val; }, set(newVal) { if (newVal !== val) { observe(newVal); val = newVal; // 告诉更新 self.dep.notify(); } } }) }}
最初一步,就是收集监听器。这一步的一个难点就在于咱们在创立Watcher
之后,须要将其搁置对应key
的deps
中,而对应的deps
,只能在对应的Observer
类中能力拜访到。
因而,咱们能够调用一次get
,来实现收集工作。
所以咱们能够间接在创立完Watcher
后,而后将这个this
赋值给Dependence
类的一个新建属性中,而后拜访一下对应key
,因而触发get
办法,就执行收集工作。
当然对于数组也是一样失去了,咱们能够调用一下push
办法且不传参,就能够将Watcher
实例增加到数组对应的deps
中。
class Watcher { constructor(vm, key, updateFn) { this.vm = vm; this.key = key; this.updateFn = updateFn; // 触发依赖收集 Dependence.target = this; // 将this赋值给Dependence的target属性 Array.isArray(this.vm[this.key]) ? this.vm[this.key].push() : ''; // 触发收集 Dependence.target = null; // 收集实现后,将target设置回null } update() { ... }}
get() { // 依赖收集 Dependence.target && self.dep.addDep(Dependence.target); return val;}
getArrayProto() { const self = this; const originalProto = Array.prototype; const arrayProto = Object.create(originalProto); ['push', 'pop', 'shift', 'unshift','reverse', 'sort', 'splice'].forEach(method => { arrayProto[method] = function () { originalProto[method].apply(self, arguments); // 收集监听器 Dependence.target && self.dep.addDep(Dependence.target); self.dep.notify(); } }) return arrayProto;}
最初测试一下。
<!doctype html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>vue app</title></head><body><div id="app"> <p @click="add" style="cursor: pointer">{{counter}}</p> <p v-text="desc"></p> <p v-html="desc2"></p> <input type="text" v-model="desc" /> <div @click="pushArr">{{arr}}</div></div><script src="./src/vue.js"></script><script> const app = new Vue({ el: '#app', data: { counter: 1, desc: 'HelloWorld', desc2: `<span style="font-weight: bolder">Hello World</span>`, arr: [0], }, methods:{ add() { this.counter++; }, pushArr() { this.arr.push(this.arr.length); } } });</script></body></html>