简单手写实现Vue2.x

github: github.com/OUDUIDUI/vu…javascript

Vue的设计思想

Vue设计思想参考了MVVM模型,即将视图View和行为Model抽象化,即将视图UI和业务逻辑分开来,而后经过ViewModel层来实现双向数据绑定。html

MVVMMVC 最大的不一样就是MVVM实现了 ViewModel 的自动同步,也就是当Model 的属性改变时,咱们不用再本身手动操做 Dom 元素,来改变 View 的显示,而是改变属性后该属性对应 View 层显示会自动改变。vue

MVVM框架的三个要素:数据响应式、模板引擎及其渲染java

  • 数据响应式
    • 监听数据变化并在视图中更新
    • Vue2.x中,是根据Object.defineProperty()来实现数据响应式的
  • 模板引擎
    • 提供描述视图的模板语法
    • Vue的插槽{{}}和指令v-bindv-onv-model
  • 渲染
    • 将模板渲染成HTML进行显示

数据响应式原理

JavaScript的对象Object中有一个属性叫访问器属性,其中有[[Get]][[Set]]特性,它们分别是获取函数或设置函数,即在获取对象特定属性的时候回调用到。node

而访问器属性是不能直接定义的,必须使用Object.defineProperty()进行定义。react

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函数中进行监听,当数据发生变化了,就会进行响应操做。git

所以,咱们能够简单实现一个Vue中的defineReactive函数。github

<!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函数就会被调用一次,所以页面数据也会更新一次。这时候,咱们就简单的实现了数据响应式。web

defineReactive函数有个问题,就是一次只能对一个属性值进行响应式处理,并且若是这个属性是个对象的话,咱们更改对象里面的值的时候,是实现不了响应式的。数组

const obj = {};
defineReactive(obj, 'info', {name: 'OUDUIDUI', age: 18});  // 将obj进行响应式处理
setTimeout(() => obj.info.age++, 1000);  // 这时候不会触发set函数
复制代码

demo1.gif

所以,咱们须要一个新的方法去实现对整个对象进行响应式处理,在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)
复制代码

demo2.gif

这里还有一个小问题,就是若是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)
复制代码

demo3.gif

最后咱们来完成前面楼下的一个问题,就是数组的响应式处理。

之因此数组须要特殊处理,由于数组有七个自带方法能够去处理数组的内容,分别是pushpopshiftunshiftreversesortsplice,它们都是能够修改数组自己的。

因此,咱们须要对七个方法进行监听。咱们能够先克隆一个新的数组原型,而后在新的原型中,新建这七个方法,先执行对应的方法操做后,进行数据响应式更新处理。

// 数组响应式
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)
复制代码

demo4.gif

简单手写Vue

原理分析

当咱们使用vue的时候,首先都会建立一个Vue实例,而后在里面初始化elementdatamethods等等。

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执行更新函数。

Vue1.jpg

实现

数据响应式

首先咱们新建一个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);
}
复制代码

Observerconstructor构造函数的内容,基本就是以前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);   	// undefined
console.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>
复制代码

demo5.png

接下来,咱们简单实现一下指令和实现,这个demo就实现一下v-textv-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>
复制代码

demo6.gif

接下来来处理指令。

对不一样指令的处理是不同,所以得对每一种指令都须要新建一个更新函数。这里只实现如下v-textv-htmlv-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>
复制代码

demo7.png

数据更新

数据的更新就会用到Watcher监听器和Dependence观察者。

当咱们视图中用到了data中某个属性key,这称为依赖,好比<div>{{desc}}</div>desc就是一个依赖。而同一个key出现屡次的时候,每一次都会建立一个Watcher来维护它们,而这个过程称为依赖收集。然而但某个key发生变化的时候,咱们须要经过该依赖下的全部Watcher去更新,这时候就须要一个Dependence来管理,须要更新的时候就由它来统一通知。

Vue2.jpg

在实现这个功能以前,咱们须要先来重构一个地方的代码。

就是咱们只需在模板中用到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以后,须要将其放置对应keydeps中,而对应的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>
复制代码

demo8.gif

相关文章
相关标签/搜索