github: https://github.com/OUDUIDUI/v...

Vue的设计思维

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

MVVMMVC 最大的不同就是MVVM实现了 ViewModel 的主动同步,也就是当Model 的属性扭转时,咱们不必再本人手动操作 Dom 元素,来扭转 View 的显示,而是扭转属性后该属性对应 View 层显示会主动扭转。

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

  • 数据响应式

    • 监听数据变动并在视图中更新
    • Vue2.x中,是依据Object.defineProperty()来实现数据响应式的
  • 模板引擎

    • 提供形容视图的模板语法
    • Vue的插槽{{}}和指令v-bindv-onv-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)

最初咱们来实现后面楼下的一个问题,就是数组的响应式解决。

之所以数组须要非凡解决,因为数组有七个自带办法能够去解决数组的内容,别离是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)

简略手写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执行更新函数。

实现

数据响应式

首先咱们新建一个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);       // 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-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>

接下来来解决指令。

对不同指令的解决是不一样,因而得对每一种指令都须要新建一个更新函数。这里只实现以下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>

数据更新

数据的更新就会用到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之后,须要将其搁置对应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>