Vue 进阶 [六] 简单易懂的手写 Vue

人生当自勉,学习需坚持

代码地址

https://gitee.com/xiaozhidayu/vue-study-zvue

https://gitee.com/xiaozhidayu/vue-study-zvue.git

前面看了一篇关于Vue响应式的原理分析文章收益匪浅,进行了转载,今天看了一节课成,敲了自己的Vue,整体思想等都收获很多,只是小demo 中没有涉及到虚拟DOM,因此类似是1.0 Vue。

Vue 的设计思想

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

数据响应式:监听数据变化并在视图中更新
 
Object.defifineProperty()
Proxy
 
模版引擎:提供描述视图的模版语法
 
插值: {{}}
指令: v-bind v-on v-model v-for v-if
 
渲染:如何将模板转换为 html
模板 => vdom => dom
 

数据响应式原理

所谓数据响应式 ,就是数据变更能够响应在视图中,vue2.0 中使用 Object.defineProperty()实现变更监测,实例代码不再赘述,完整代码中有各种实验。

Vue数据响应化实现

1. new Vue() 首先执行初始化,对data执行响应化处理,这个过程发生在Observer

2. 同时对模板执行编译,找到其中动态绑定的数据,从data中获取并初始化视图,这个过程发生在

Compile

3. 同时定义一个更新函数和Watcher,将来对应数据变化时Watcher会调用更新函数

4. 由于data的某个key在一个视图中可能出现多次,所以每个key都需要一个管家Dep来管理多个

Watcher

5. 将来data中数据一旦发生变化,会首先找到对应的Dep,通知所有Watcher执行更新函数

超啰嗦的图解:

MVVM 代表我们要创建的Vue 即 ZVue

当进行创建的时候 要执行两个操作:

  1. 数据响应式 Observer 数据劫持
  2. 编译 compile 主要作用是解析模板当中非HTML 的部分 ,比如差值表达式 以及 一些特殊的指令 v-if v-model v-on @click 等指令,编译会做两件事情① 初始化视图 ,就是从劫持的数据中心get 到数据,设置为初始值。② 设置数据变化的时候可以更新视图 怎么做呢?引入了几个新的角色,观察者 Watcher ,她的作用是在界面中如果出现一个绑定就创建一个观察者Watcher ,每个watcher 会保存一个更新函数,更新函数做对应的dom 元素的更新操作。以上Observer 和 Watcher 之间还没有出现闭环 ,闭环的关键是 角色Dep(依赖)的主要作用是管理多个watcher,因为很多数据在页面中不止出现一次,因此对应的会有多个watcher,那么watcher 和 拦截器Observer 之间就产生了一个一对多的关系,此时就需要一个管家把多个watcher 管理起来,就是Dep,但是在拦截器Observer 中每一个key 和 Dep 之间是一定有一个一对一的关系。这样整体形成了一个闭环关系。

     代码中的类型介绍:

      1、ZVue:框架构造函数

      2、Observer:执行数据响应化(需要分析是对象还是数组)

      3、Compile:编译模板,初始化视图,收集依赖(更新函数、watcher)

      4、Watcher:执行更新函数(更新都没)

      5、Dep:管理多个Watcher ,批量更新

附上代码

zvue.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>zvue</title>
</head>

<body>
    <div id="app">
        <!-- 验证差值 -->
        <p>{{counter}}</p>
        <!-- 验证指令 -->
        <p z-text="counter"></p>
        <p z-html="desc"></p>
    </div>
    <script src="zvue.js"></script>
    <script src="compile.js"></script>
    <script>
        const app = new ZVue({
            el: '#app',
            data: {
                counter: 1,
                desc: '<span style="color:red">zvue 可还行? </span>'
            },
        })
        setInterval(() => {
            // app.$data.counter++
            app.counter++
            // app.$data 这种访问数据的方式在实际中不应用,我们在vue中都是直接访问的,因此要做数据代理 即直接访问实例的某个属性,就像访问$data 中的数据一样
        }, 1000)
    </script>
</body>

</html>

compile.js

// 编译器
// 递归遍历dom树
//判断节点类型 如果是文本则判断是否是差值绑定
//如果是元素,则遍历其属性判断是否是指令或事件,然后递归子元素
class Compiler {
    // el是宿主元素
    // vm是 ZVue 实例
    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
        //childNodes 本身不是一个数组,所以用Array转换一下
        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) {
        // console.log(RegExp.$1)
        // node.textContent = this.$vm[RegExp.$1]
        this.update(node, RegExp.$1, 'text')
    }
    compileElement(node) {
        // 节点是元素
        // 遍历其属性列表
        const nodeAttrs = node.attributes
        Array.from(nodeAttrs).forEach(attr => {
            //规定:指令以z-xx = 'oo'定义
            const attrName = attr.name // z-xx
            const exp = attr.value // oo
            if (this.isDirective(attrName)) {
                const dir = attrName.substring(2) //xx
                // 执行指令
                this[dir] && this[dir](node, exp)
            }
        })
    }
    isDirective(attr) {
        return attr.indexOf('z-') === 0
    }

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



    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)
        })

    }
    //z-text
    text(node, exp) {
        // node.textContent = this.$vm[exp]
        this.update(node, exp, 'text')
    }

    // z-html 
    html(node, exp) {
        // node.innerHTML = this.$vm[exp]
        this.update(node, exp, 'html')
    }

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

zvue.js

function defineReactive(obj, key, val) {
    // 递归  如果val 还是对象的话 进行遍历响应式
    observe(val)

    // 创建一个Dep 和 当前的可以 一一对应

    const dep = new Dep()
    //对传入obj 进行访问拦截
    // 在每次执行defineReactive 时候,其实形成了一个闭包,因为在内部保留了一个内部作用域的变量 就是value
    Object.defineProperty(obj, key, {
        get() {
            console.log('get', key)

            // 核心的依赖手机过程 发生在get 中 
            Dep.target && dep.addDep(Dep.target)

            return val;
        },
        set(newVal) {
            if (newVal !== val) {
                console.log('set::' + key + ":" + newVal)
                // 如果newVal 是对象,应该做响应化的处理
                observe(newVal)
                val = newVal

                // 执行更新函数遍历watchers
                // watchers.forEach(w => {
                //     w.update()
                // })
                dep.notify()
            }
        }
    })
}

function observe(obj) {
    if (typeof obj !== 'object' || obj == null) {
        // 希望传入的是obj
        return
    }
    // 遍历 相当于所有的可以 都定义了响应式
    // Object.keys(obj).forEach(key => {
    //     defineReactive(obj, key, obj[key])
    // })
    //此处不再是直接循环遍历,而是要引入一个新的角色Observer 一个主要作用是分析数据是对象还是数组。
    // 创建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
            }
        })
    })
}
// 创建zvue的构造函数
class ZVue {
    constructor(options) {
        // 保存选项
        this.$options = options
        this.$data = options.data

        // 响应化处理
        observe(this.$data)

        // 代理
        proxy(this, '$data')

        // 创建编译器的实例
        new Compiler(options.el, this)
    }
}
// 根据对象类型决定如何做响应化
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])
        })
    }
    // 数组数据的响应化 待补充
}

// 创建观察者:保存更新函数,值发生变化 调用更新函数
// 在没有Dep 的情况下 先自己把每一个watcher 保存起来
// 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] // 读取触发get 因此可以在get 中 将其target 追加进去
        Dep.target = null // 收集完后置空

    }
    update() {
        this.updateFn.call(this.vm, this.vm[this.key])
    }

}

//Dep:依赖,管理某个key 相关所有Watcher实例 
class Dep {
    constructor() {
        this.deps = []
    }

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

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

代码中有数据代理的实现 核心就是 把data 中的每个数据拿出来 放在组件实例上 。

学习过程很烧脑,每天收获很开心!

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值