手撕简易Vue-非Vue3

hello,大家好,我是梅利奥猪猪!就之前带大家手撸简易VueRouter3Vuex3的那个!这次继续给大家带来干货,干货的主题就是手写个简易Vue(非vue3),麻雀虽小五脏俱全,学完这个你将收获,面试时强大的气场!主要能学会以下知识点

  • 手撕响应式
  • 手撕简易编译器
  • 手撕WatcherDep,知道他们如何建立关系

前置知识补充

众所周知,vue是个mvvm框架,这里就不做具体展开了,具体请看下图

01-mvvm.jpg

其中,图中的ViewModel对于我们来说,就像黑盒般的存在,他到底做了什么,让我们写vue,纵享丝滑,写的如此快乐轻松呢!这也就是此篇文章的目的,带大家来写个简易Vue,具体要实现的东西,也请看下图

02-目标.jpg

可能现在的你还看不懂,看上去好复杂啊,随着文章的深入,这幅图的所有实现都会带着大家写完!那我们开始吧

开始开发

Observer – 数据劫持

defineReactive架子 – 测试拦截

大家都知道,Vue2的响应式原理是Object.defineProperty, 如果对Object.defineProperty的使用还不清楚想更深入的小伙伴,可以去看下MDN,这里直接跟着我写也行!

新建my-vue.js

/* eslint-disable */ 
function defineReactive (obj, key, val) {

  Object.defineProperty(obj, key, {
    get () {
      console.log('get', { key, val })
      return val
    },
    set (newVal) {
      if (newVal !== val) {
        console.log('set', { key, newVal })
        val = newVal
      }
    }

  })
}


const obj = {}
defineReactive(obj, 'foo', 'foo')
obj.foo
obj.foo = 'new foo'

写完这个,我们可以测试下,用node环境跑下代码,会发现,的确get和set的时候都触发了打印!

03-初次劫持数据.jpg

放在一个测试页面并提供update方法测试

有了这个初步的劫持,那我们还可以玩什么呢,既然修改数据能触发set,那如果我在set里提供方法,更改视图会发生什么事呢!我们赶紧来试试!

cv前面的js代码到测试页面,修改下,添加update函数,之后在控制台修改对象的属性玩耍下!

<!DOCTYPE html>

<html lang="en">



<head>

    <meta charset="UTF-8">

    <meta http-equiv="X-UA-Compatible" content="IE=edge">

    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <title>测试数据劫持</title>
</head>



<body>

    <div id="app">

        <h1 id="a"></h1>
        <h1 id="b"></h1>
        <h1 id="c"></h1>
    </div>

    <script>
        function defineReactive(obj, key, val) {
            Object.defineProperty(obj, key, {
                get() {
                    console.log('get', { key, val });
                    return val
                },
                set(newVal) {
                    if (newVal != val) {
                        console.log('set', { key, newVal });
                        val = newVal
                        // 注意以下代码新增, 要写在赋值后面哈!
                        update()
                    }
                }
            })
        }
        const obj = {}
        // 添加3个属性
        defineReactive(obj, 'a', 'a')
        defineReactive(obj, 'b', 'b')
        defineReactive(obj, 'c', 'c')
        // 定义update函数,就做简单的页面视图更新
        function update () {
            a.innerHTML = obj.a
            b.innerHTML = obj.b
            c.innerHTML = obj.c
        }
        // 一进页面先调用下
        update()

    </script>
</body>

</html>

04-控制台数据劫持玩耍.gif

我们可以发现,只要控制台修改了数据,视图就更新了,接着我们分析下问题,有这么几个疑问

  1. 在使用vue的时候,更新函数我们有写过吗
  • 其实都是template自动编译成更新函数的
  1. 劫持字段要用户一个一个自己去劫持,不合理
  • 需要遍历,后面还需要递归(等等就讲)
  1. 全量更新,只要数据一发生改变(改变其中一个属性),视图就直接全部更新了,显然不合理
  • 精确定位具体的dom元素 – 本次简易版实现使用这种
  • 利用虚拟dom差异化更新 – 以后带大家看源码,我会产出博客的!

observe的初步实现

用户不应该手动设置属性,通过遍历处理劫持每个key

function observe (obj) {
  Object.keys(obj).forEach(key => defineReactive(obj, key, obj[key]))
}




const obj = {
  foo: 'foo',
  bar: 'bar'
}
// defineReactive(obj, 'foo', 'foo')
observe(obj)

obj.foo
obj.foo = 'new foo'
obj.bar
obj.bar = 'new bar'

05-遍历劫持所有key.jpg

数据劫持需要递归处理

以上代码还没有递归,为什么要递归,因为用户设置的属性,还可以是对象,不递归会带来问题

const obj = {



    a: 1,

    b: 2,

    c: {
        haha: 3
    }

}
obj.c.haha
obj.c.haha = 4

06-不递归劫持不到里面的属性.jpg

上面的截图,能发现,并没有劫持到haha这个属性,所以observe方法中defineReactiveobj[key]如果是对象就需要递归

function defineReactive (obj, key, val) {

  // 直接数据劫持,不用担心observe方法写了递归结束条件
  observe(val)
  ...
}

function observe (obj) {
  // 递归结束条件
  if (typeof obj !== 'object' || obj === null) {
    return
  }
  Object.keys(obj).forEach(key => defineReactive(obj, key, obj[key]))
}

此时在跑前面的代码,就能劫持到我们想要的属性了

07-递归后解决劫持问题.jpg

劫持set函数里还要在observe,因为用户可能直接赋值新的对象

obj.c = {
    heihei: 444
}




obj.c.heihei
obj.c.heihei = 555

如果给属性赋值了新对象,数据劫持还是会出现问题

08-重新设置新的对象劫持出现问题.jpg

        set(newVal) {
            if (newVal !== val) {
                // 需要劫持
                observe(newVal)
                val = newVal
            }
        }

在set添加observe后,此时效果就出来了

09-在set方法里observe.jpg

set方法

给obj设置个新属性,会劫持吗

const obj = {



    a: 1,

    b: 2,

}


observe(obj)
obj.c = 3
obj.c
obj.c = 4

10-新加个属性不会劫持.jpg

所以Vue提供了set方法,新的属性就要新的劫持!

// 手写,新增set方法
function set(obj, key, val) {
    defineReactive(obj, key, val)
}



const obj = {
    a: 1,
    b: 2,
}

observe(obj)

// 调用set方法
set(obj, 'c', 3)
// 这行就不需要了
// obj.c = 3
obj.c
obj.c = 4

11-set方法实现及使用.jpg

数组问题

可以拦截的情况

const arr = [1, 2, 3]

observe(arr)

arr[0]
arr[1] = 222

12-数组可以劫持的情况.jpg

用户可能访问更大的索引,或者给数组添加删除等方法(会改变数组自身的方法),此时就拦截不了, 数组7个变更方法处理会变更数组自身的方法,在做数组操作的同时,进行变更通知 此次不实现

13-数组不能劫持的情况.jpg

写静态页面使用官方的vuejs

<!DOCTYPE html>

<html lang="en">



<head>

    <meta charset="UTF-8">

    <meta http-equiv="X-UA-Compatible" content="IE=edge">

    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <title>Document</title>
</head>



<body>

    <div id="app">

        {{count}}
        <div>{{count}}</div>
        <p v-text="count"></p>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
    <script>
        const vm = new Vue({
            el: '#app',
            data: {
                count: 0
            }
        })
        setInterval(() => {
            vm.count++
        }, 1000)
    </script>
</body>

</html>

能发现页面中的count在不断变化,接下去我们要替换我们自己的my-vue.js

my-vue.js

首先报错了,我们可以这么处理

class Vue {

  constructor (options) {
    // 0 - 保存选项
    this.$options = options
    this.$data = options.data
    // 1 - 响应式 - 写好点可以做判断因为可能传的是函数
    observe(this.$data)
    // 2 - 编译模板
  }

}

然后发现没有打印get和set,为什么没有劫持,因为vm上没有count,count在$data上

setInterval(() => {
    vm.$data.count++
}, 1000)

将代码从vm.count++改成vm.$data.count++就有对应的打印了

14-数据在$data上.jpg

代理属性

前面的问题是因为app上是没有counter属性的,在$data上才有counter属性,所以要做一层代理

在实现前,先闲扯一句,相当于现在有这么一个面试题

const obj = {



  data: {

    a: 1,

    b: 2

  }

}



// 希望你可以通过以下方式访问到data中的a和b属性
obj.a
obj.b
// 希望你可以通过以下方式,可以修改data里a和b的值
obj.a = 11
obj.b = 22

实现的代码如下

const obj = {



  data: {

    a: 1,

    b: 2

  }

}



Object.keys(obj.data).forEach(key => Object.defineProperty(obj, key, {
    get() {
        return obj.data[key]
    },

    set(val) {
        obj.data[key] = val
    }

}))

15-控制台测试代理.jpg

通过以上练习,接下去对Vue实例代理一层是不是就简单多了!实现后就能看到vm.count++也能触发劫持效果

// 简单代理一层
function proxy (vm) {
  Object.keys(vm.$data).forEach(key => {
    Object.defineProperty(vm, key, {
      get () {
        return vm.$data[key]
      },
      set (v) {
        vm.$data[key] = v
      }
    })
  })
}


class Vue {
  constructor (options) {
    // 0 - 保存选项
    this.$options = options
    this.$data = options.data
    // 1 - 响应式
    observe(this.$data)
    // 2- 做代理
    proxy(this)
    // 3 - 编译模板
  }
}

至此,我们数据劫持相关的知识就先到这里,接下去就要开始写个简简单单的编译器

Compile – 编译器

接下去做编译的部分

原理简单说明

  • 获取dom
  • 遍历childNodes
    • 编译节点
      • 遍历属性
        • v-开头
        • @开头
    • 编译文本

Compile类

我们先简单搭个架子,Compile类,构造函数需要传入一开始设置的el选项以及vm实例(后面会用到),通过el选择器,获取元素,在调用编译方法!(compile方法用于处理编译的逻辑)具体代码如下

class Vue {

    constructor(options) {
        // console.log(options);
        this.$options = options
        this.$data = options.data
        observe(this.$data)
        proxy(this)
        // 将选择器和实例都传入
        new Compile(options.el, this)
    }
}

class Compile {
    constructor(elSelector, vm) {
        this.$vm = vm
        const element = document.querySelector(elSelector)
        // console.log(element, this.$vm);
        this.compile(element)
    }
    compile(element) {
        // 这里不能用children要用childNodes
        // console.log(element.children);
        // console.log(element.childNodes);
        const childNodes = element.childNodes
        console.log(childNodes);
    }
}

可以给各位小伙伴一个思考,为什么编译方法里获取的是childNodes而不是children,基础好的小伙伴应该猜到了!因为children只能获取元素,但childNodes可以获取节点

16-children和childNodes区别.jpg

判断元素还是文本

这里的知识相对容易,首先要知道的nodeType的值,可以查看MDN,其中文本节点,我们还要判断是否有双大扩号语法,所以要用到正则,具体代码如下

  // 是否元素
  isElement (node) {
    return node.nodeType === 1
  }


  // 文本
  isInter (node) {
    // nodeType在mdn查询 正则可以在浏览器里调试 RegExp.$1
    return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)
  }

打印节点相关内容

接着遍历childNodes,使用前面的判断方法,做些打印

  compile(element) {
    
    const childNodes = element.childNodes;
    // console.log(childNodes);
    childNodes.forEach(node => {
        // console.log(node.nodeName, node.nodeType);
        if (this.isElement(node)) {
            console.log('element',node.nodeName);
            // 如果是元素 还需要递归
        } else if (this.isInter(node)) {
            // 文本
            console.log('inter', node.textContent);
        }
    })
  }

17-打印元素和节点.jpg

元素节点需要递归处理

元素节点里还有文本节点,所以要做递归处理

if (this.isElement(node)) {

    console.log('element', node.nodeName)
    if (node.childNodes.length > 0) {
        // 元素节点里还有节点,那递归遍历
        this.compile(node)
    }

} 

18-递归获取双大括号文本节点.jpg

编译text节点

实现compileText(不要忘记加trim,还有这里简易实现没有考虑文本节点里复杂的逻辑),并在判断文本节点的地方调用这个方法

  compileText (node) {
    // 不要忘记trim 可能双大扩前后加了空格
    node.textContent = this.$vm[RegExp.$1.trim()]
  }


else if(this.isInter(node)) {
    console.log('inter', node.textContent);
    this.compileText(node)
}


19-简易编译文本节点.jpg

元素节点解析指令

元素先要解析他的属性

if (this.isElement(node)) {

    // console.log('element', node.nodeName)
    const attrs = node.attributes; // 新增代码
    console.log(attrs); // 新增代码
    if (node.childNodes.length > 0) {
        // 递归处理
        this.compile(node)
    }

}

20-打印元素上的属性.jpg

接着遍历attrs,注意了他是个伪数组,所以先要转成真数组,在遍历!遍历后console.dir打印下attr

[...attrs].forEach(attr => {
    console.dir(attr)
})

21-dir打印attr.jpg

我们知道了attr上有namevalue属性,那接下去,我们就解构加起别名处理下!

const attrs = node.attributes;
  // console.log(attrs);
  [...attrs].forEach(({name: attrName, value: exp}) => {
      console.log(attrName, exp)
  })

判断是否是指令

判断是否是指令,只要判断字符串开始是否有v-就可以

isDir (str) {
  return str.startsWith('v-')
}


每个指令提供一个方法

text方法实现

  text (node, exp) {
    console.log('text方法', node, exp);
    node.textContent = this.$vm[exp]
  }


v-text就调用text方法(v-html就调用html方法等)

if (this.isDir(attrName)) {
    // console.log('是指令', attrName, '表达式为', exp);
    const dirName = attrName.slice(2)
    // console.log(dirName);
    this[dirName]?.(node, exp)
}



22-text指令解析成功.jpg

思考:小伙伴们可以自行完成v-html指令试试哈,后面还会带大家实现v-model,还有事件@click等等

至此,解析指令,初始化视图,一个简易的编译器就写完了

Watcher – 观察者

  • vue的实现:模板 => vdom => dom

  • 本次实现跳过虚拟dom,简易的实现(其实是Vue1的实现)

  • 观察者模式不理解的,可以自行补充该知识哈,或者看下以下这个扎心的例子

class ProductManager {
    constructor () {
        this.workers = []
    }


    addWorker (worker) {
        this.workers.push(worker)
    }
    notify (prd) {
        this.workers.forEach(item => {
            item.update(prd)
        })
    }
    setPrd (prd) {
        this.notify(prd)
    }


}


class Worker {
    constructor(name) {
        this.name = name
    }

    update(prd) {
        console.log(prd + `需求来了,${this.name}准备996`);
    }
}



const pm = new ProductManager()
const frontWorker = new Worker('前端')
const endWorker = new Worker('后端')
const testWorker = new Worker('测试')
pm.addWorker(frontWorker)
pm.addWorker(endWorker)
pm.addWorker(testWorker)
pm.setPrd('一个复杂的功能')

此例子模拟了产品发布一个复杂的功能,只要setPrd就通知了所有人员一起干活!

依赖收集

视图中会用到data中的某key,这称为依赖,同一个key可能出现多次,每次都需要收集出来用一个Watcher来维护他们,此过程称为依赖收集

多个Watcher需要一个Dep来管理(vue的最终实现是多对多的关系),需要更新时由Dep统一通知,接下去说明下关系

<div>
    <p>{{name1}}</p>
    <p>{{name2}}</p>
    <p>{{name1}}</p>
</div>

收集依赖 有几个大括号就有几个watcher 所以这里有3个watcher

有几个key就有几个管家dep 所以dep只有2个,

Dep1 deps = [watcher1, watcher3]

Dep2 deps = [watcher2]

实现思路

  1. defineReactive时为每一个key创建一个Dep实例
  2. 初始化视图时读取个key 创建一个watcher 比如name1 就创建个watcher1
  3. 由于触发name1的getter方法,将watcher1添加到name1对应的Dep中
  4. 当name1更新,setter触发时,通过对应Dep通知管理所有Watcher更新

源码是N对N 这里我们简化下Dep和watcher 1对N

那接下来搭架子, 我们要了解watcher具体要干什么,请看以下代码及注释

/**
 * 负责具体节点更新
 * Watcher的用法是是用来更新数据的 this.$vm[exp]可以拿到对应的数据,在通过val修改,这里第三个参数传递方法
 * new Watcher(this.$vm, exp, (val) => {})
 */
class Watcher {
    constructor(vm, key, updater) {
        this.vm = vm;
        this.key = key
        this.updater = updater
    }


    // 给管家调用
    update () {
        this.updater(this.vm[this.key])
    }


}



/**
 * Dep和响应式的key属性有一一对应关系
 * 负责通知watchers更新
 */
class Dep {


}



watcher实例化前准备工作

只要涉及到编译的地方都要new Watcher

watcher在什么时候实例化,编译的时候实例化,只要有动态绑定的就实例化!比如text方法,compileText方法等,因为他们都要解析动态绑定的值,所以我们需要重构下Compile类,提供更高级的方法

为了复用,可以提供textUpdater,他做的事情就是给节点赋值内容

除了textUpdater,以后可能有其他的xxxUpdater,所以可以提供一个统一的update方法,
update函数的形参为node, exp, dir,分别指的是节点,表达式,哪一种指令(之后调用哪种Updater)

  // 处理所有动态绑定
  update(node, exp, dir) {
    // 1. 初始化
    this[dir + 'Updater']?.(node, this.$vm[exp])
    // 2. 创建Watcher实例,负责后续管理
  }
  textUpdater(node, val) {
    // console.log(node, val);
    node.textContent = val
  }
  text (node, exp) {
    // console.log('text方法', node, value);
    // node.textContent = this.$vm[value]
    this.update(node, exp, 'text')
  }

  compileText (node) {
    // node.textContent = this.$vm[RegExp.$1]
    this.update(node, RegExp.$1, 'text')
  }

实例化watcher

实例化Watcher

  update(node, exp, dir) {
    // 1. 初始化
    this[dir + 'Updater']?.(node, this.$vm[exp])
    // 2. 创建Watcher实例,负责后续管理, 一定要用箭头函数否则this指向有问题
    new Watcher(this.$vm, exp, (val) => {
        this[dir + 'Updater']?.(node, val) 
    })
  }

简单粗暴全量更新

简单粗暴全量更新,声明一个全局的watchers,在实例化Watcher的时候,就添加watcher,最后在劫持的set里调用update方法

const watchers = []
function defineReactive (obj, key, val) {

  // 直接数据劫持,不用担心observe方法写了递归结束条件
  observe(val)
  Object.defineProperty(obj, key, {
    get () {
      // 形成闭包
    //   console.log('get', { key })
      return val
    },

    set (newVal) {
      if (newVal !== val) {
        // 形成闭包
        // console.log('set', { key })
        // console.log(watchers)
        observe(newVal)
        val = newVal
        watchers.forEach(w => w.update())
      }
    }

  })
}



class Watcher {
    constructor(vm, key, updater) {
        this.vm = vm;
        this.key = key
        this.updater = updater
        watchers.push(this)
    }
    // 给管家调用
    update () {

        const val = this.vm[this.key]
        this.updater.call(this.vm, val)
    }
}

此时效果已经出来了,已经能看到页面能不断更新了,(前面定时器的原因不断累加),但我们实现还不够完美,还要处理下管家Dep

处理Dep

把前面粗暴的代码删了

class Dep {
    constructor() {
        this.deps = []
    }


    addDep (dep) {
        // dep就是watcher
        this.deps.push(dep)
    }

    notify () {
        this.deps.forEach(dep => dep.update())
    }


}

难点-如何建立Watcher和Dep的关系

  • 之前说了,一个key对应一个管家,所以在defineReactive中,实例化Dep
  • 实例化后的Watcher什么时候添加进管家的deps,这里有个巧妙的方式
    • 实例化后Watcher挂在Dep.target上
    • 手动取值触发下劫持的get函数,在get函数中,给dep添加watcher
    • 最后Dep.target清空
  • 何时通知所有的watcher
    • 在set的时候,当前dep直接notify

核心代码如下

function defineReactive (obj, key, val) {

    // 新增实例化Dep
    const dep = new Dep()
    observe(val)
    Object.defineProperty(obj, key, {
        get () {
            // 新增 - 添加watcher
            Dep.target && dep.addDep(Dep.target)
            return val
        },
        set(newVal) {
            if (newVal != val) {
                // console.log('set', {key, newVal});
                observe(newVal)
                val = newVal
                // 新增 - 通知所有watcher更新
                dep.notify()
            }
        }
    })
}

class Watcher {
    constructor(vm, key, updater) {
        this.vm = vm;
        this.key = key;
        this.updater = updater
        // 以下三行代码新增
        Dep.target = this // 实例化watcher挂在Dep.target上
        this.vm[this.key] // 手动触发劫持的get
        Dep.target = null // 触发完将Dep.target清空
    }
    update () {

        this.updater(this.vm[this.key])
    }

}

至此整个简易的vue就实现好了,在来回看之前的那个图,我们在分析下

23-分析开局的图.jpg

完结撒花!

补充

数组变异方法 – 了解

  • 目前数组是无法感知有变化的
const arr = [1, 2, 3]

observe(arr)

arr.push(4, 5)
arr[3] = 444
arr[4] = 555
  • 实现数组响应式

    • 找到数组原型
    • 覆盖修改数组的更新方法
    // 原先的
    const originalProto = Array.prototype
    
    // 备份一份
    const arrayProto = Object.create(originalProto)
    const methodsToPatch = [
        'push',
        'pop',
        'shift',
        'unshift',
        'splice',
        'sort',
        'reverse'
    ]
    methodsToPatch.forEach(method => {
        // console.log(method);
        arrayProto[method] = function () {
            // 执行原先的
            originalProto[method].apply(this, arguments)
            // 处理覆盖的逻辑
            console.log('变异方法开始', method);
        }
    })
    
    • 在observe方法中做判断,是数组的话将得到的新的原型设置到数组实例原型上
    function observe(obj) {
        if (typeof obj !== 'object' || obj === null) {
            return;
        }
    
    
        if (Array.isArray(obj)) {
            obj.__proto__ = arrayProto
            for (let i = 0; i < obj.length; i++) {
                const item = obj[i];
                observe(item)
            }
        } else {
            Object.keys(obj).forEach(key => {
                defineReactive(obj, key, obj[key])
            })
        }
    
    
    }
    
    
    

编译器@事件实现

if (this.isEvent(attrName)) {
    // console.log(attrName, exp);
    const eventName = attrName.slice(1)
    // console.log(eventName, exp);
    this.eventHandler(node, eventName, exp)
}





eventHandler(node, eventName, exp) {
    const fn = this.$vm.$options?.methods?.[exp]
    node.addEventListener(eventName, fn.bind(this.$vm))
}

isEvent(str) {
    return str.indexOf('@') === 0
}

编译器 v-model实现

    modelUpdater(node, val) {
        node.value = val
    }
    model(node, exp) {
        // update方法只完成赋值和更新
        this.update(node, exp, 'model')
        // 事件监听
        node.addEventListener('input', e => {
            this.$vm[exp] = e.target.value
        })
    }


总结

完整代码

/* eslint-disable */
const originalProto = Array.prototype

const arrayProto = Object.create(originalProto)
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
methodsToPatch.forEach(method => {
  arrayProto[method] = function () {
    originalProto[method].apply(this, arguments)
  }
})

function defineReactive (obj, key, val) {
  observe(val)
  const dep = new Dep()
  Object.defineProperty(obj, key, {
    get () {
      if (Dep.target) {
        dep.addDep(Dep.target)
      }
      return val
    },
    set (newVal) {
      if (newVal !== val) {
        observe(newVal)
        val = newVal
        dep.notify()
      }
    }

  })
}


function observe (obj) {
  if (typeof obj !== 'object' || obj === null) {
    return
  }
  if (Array.isArray(obj)) {
    obj.__proto__ = arrayProto
    for (let i = 0; i < obj.length; i++) {
      const item = obj[i]
      observe(item)
    }
  } else {
    Object.keys(obj).forEach(key => {
      defineReactive(obj, key, obj[key])
    })
  }
}

function set (obj, key, val) {
  defineReactive(obj, key, val)
}

function proxy (vm) {
  Object.keys(vm.$data).forEach(key => {
    Object.defineProperty(vm, key, {
      get () {
        return vm.$data[key]
      },
      set (v) {
        vm.$data[key] = v
      }
    })
  })
}

class Vue {
  constructor (options) {
    this.$options = options
    this.$data = options.data
    observe(this.$data)
    proxy(this)
    new Compile(options.el, this)
  }
}

class Compile {
  constructor (elSelector, vm) {
    this.$vm = vm
    const element = document.querySelector(elSelector)
    this.compile(element)
  }

  compile (el) {
    const childNodes = el.childNodes
    childNodes.forEach(node => {
      if (this.isElement(node)) {
        const attrs = node.attributes;
        [...attrs].forEach(({ name: attrName, value: exp }) => {
          if (this.isDir(attrName)) {
            const dirName = attrName.slice(2)
            this[dirName]?.(node, exp)
          }
          if (this.isEvent(attrName)) {
            const eventName = attrName.slice(1)
            this.eventHandler(node, eventName, exp)
          }
        })
        if (node.childNodes.length) {
          this.compile(node)
        }
      }
      if (this.isInter(node)) {
        this.compileText(node)
      }
    })
  }

  update (node, exp, dir) {
    this[dir + 'Updater']?.(node, this.$vm[exp])
    new Watcher(this.$vm, exp, (val) => {
      this[dir + 'Updater']?.(node, val)
    })
  }

  textUpdater (node, val) {
    node.textContent = val
  }

  text (node, exp) {
    this.update(node, exp, 'text')
  }

  htmlUpdater (node, val) {
    node.innerHTML = val
  }

  html (node, exp) {
    this.update(node, exp, 'html')
  }

  modelUpdater (node, val) {
    node.value = val
  }

  model (node, exp) {
    this.update(node, exp, 'model')
    node.addEventListener('input', e => {
      this.$vm[exp] = e.target.value
    })
  }

  compileText (node) {
    this.update(node, RegExp.$1.trim(), 'text')
  }

  eventHandler (node, eventName, exp) {
    const fn = this.$vm.$options?.methods?.[exp]
    node.addEventListener(eventName, fn.bind(this.$vm))
  }

  isEvent (str) {
    return str.indexOf('@') === 0
  }

  isElement (node) {
    return node.nodeType === 1
  }

  isInter (node) {
    return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)
  }

  isDir (str) {
    return str.startsWith('v-')
  }
}

class Watcher {
  constructor (vm, key, updater) {
    this.vm = vm
    this.key = key
    this.updater = updater
    Dep.target = this // 赋值
    this.vm[this.key] // 会触发get
    Dep.target = null // 置空
  }

  update () {
    const val = this.vm[this.key]
    this.updater.call(this.vm, val)
  }
}

class Dep {
  constructor () {
    this.deps = []
  }

  addDep (dep) {
    this.deps.push(dep)
  }

  notify () {
    this.deps.forEach(dep => dep.update())
  }
}

简易Vue分析

23-分析开局的图.jpg

vue1的实现基本就是这样,vue2用虚拟dom是因为vue1带来的问题,这次实现,模板中每有一个变量就有一个watcher,明显不合理! 所以vue2后面一个组件一个watcher,但他怎么知道要更新具体哪个呢,所以有了虚拟dom的概念,这也是引进来虚拟dom的必要性

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYZGlwCg' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片