vue原理手写

86 阅读3分钟

VUE

把你了解的vue原理阐述一下

首先了解vue中的三个核心类

  1. Observer: 给对象的属性添加getter和setter,用于依赖收集和派发更新
  2. Dep: 用于收集当前响应式对象的依赖关系,每个响应式对象都有一个dep实例。dep.subs = watcher[]。当数据发生变更的时候,会通过dep.notify()通知🧍‍♂各个watcher
  3. Watcher: 观察者对象, render watcher,computed watcher, user watcher
  • 依赖收集
  1. initState,对computed属性初始化时,会触发computed watcher依赖收集
  2. initState, 对监听属性初始化的时候,触发的user watcher依赖收集
  3. render,触发render watcher的收集
  • 派发更新 Object.defineProperty
  1. 组件中响应的数据进行了修改,会触发setter逻辑
  2. dep.notify()
  3. 遍历所有的subs,调用每个watcher的update方法 总结原理 当创建vue实例时,vue会遍历data里的属性,object.defineProperty 为属性添加getter和setter对数据的读取进行劫持, getter: 依赖收集 setter: 派发更新 每个组建的实例都会有响应的watcher实例

计算属性的实现原理

computed watcher,计算属性的监听器 computed watcher 持有一个dep实例,通过dirty属性标记计算属性是否需要重新求值 当computed的依赖值改变后,就会通知订阅的watcher进行更新,对于computed watcher 会将dirty属性设置为true,并且进行计算属性的调用

  1. computed 所谓的缓存是指什么? 计算属性是基于他的响应式依赖进行缓存的,只有依赖发生改变的时候才会重新求值。
  2. 那computed缓存存在的意义是什么,或者你经常在什么时候使用 比如计算属性方法内部操作非常耗时,遍历一个极大的数组,计算一次可能需要耗时1s。
 const largeArray = [
     {...},
     {...}
     ....
 ]//10w
 data: {
     id:1
 }
 computed: {
     currentItem: function(){
         return largeArray.find(item=>item.id===this.id)
     }
     stringId: function(){
         return String(this.id)
     }
 }
  1. 以下情况,computed可以监听到数据的变化吗=====不能(只有data经过vue初始化监听,创建了observer的才会变化
template
    {{storageMsg}}
computed: {
    storageMsg: function(){
        return sessionStorage.getItem('xxx')
    },
    time: function(){
        return Date.now()
    }
}
created(){
    sessionStorage.setItem('xxx',111)
}
onClick(){
    sessionStorage.setItem('xxx',Math.random())
}

watch和computed的区别

Vue.nextTick的原理

Vue.nextTick(()=>{

})

Vue是异步执行dom更新的,一旦观察到数据的变化,会把同一个event loop中观察数据变化的watcher推送紧这个队列。

在下一次事件循环时,Vue会清空异步队列,进行dom更新,所以是在下一次时间循环中更新的 优先级: Promise.then > MutationObserver => setImmediate => setTimeout vm.somData = 'new value ',

dom并不会马上更新,而是在异步队列被清除时才会更新dom 宏任务=> 微任务队列 =》 UI render 一般什么时候用到nextTick呢 在数据变化后要执行某个操作,而这个操作印尼的数据改变而改变的dom,这个操作应该放到nextTick回调中

手写一个简单的vue,实现响应式的更新

  1. 首先新建一个目录
  • index.html主页面
  • vue.js vue 主文件
  • compiler.js 编译模版,解析指令,v-model,v-html
  • observer.js 数据劫持
  • watcher.js 观察者对象类
  1. index.html
<!DOCTYPE html>
<html lang="cn">
    <head>
        <title>My Vue</title>
    </head>
    <body>
        <div id="app">
            // 验证 
            <h1>模版表达式</h1>
            <h3>{{msg}}</h3>
            <br />

            <h1>v-text测试</h1>
            <h3 v-text="msg"></h3>
            <br />    

            <h1>v-html测试</h1>
            <div v-html="testHtml"></div>
            <br /> 

            <h1>v-model测试</h1>
            <input type = "text" v-model="msg">
            <input type = "text" v-model="count">
            <button v-on:click="handler">按钮</button>
            <br /> 
        </div>
        <script src="./index.js" type="module"></script>
    </body>
</html>
  1. vue.js
/**
 * 包括Vue的构造函数,接收各种配置参数等等
*/
import Observer from './observer.js'
import Compiler fom './compiler.js'
export default class Vue{
    constructor(options={}){
        this.$options = options
        this.$data = options.data
        this.$methods = options.methods
        this.initRootElement(options)
        // 利用Object.defineProperty将data里的属性注入到vue实例中
        this._proxyData(this.$data)
        // 实例化observer对象
        new Observer(this.$data)
        // 实例化Compiler对象,解析指令和模版表达式
        new Compiler(this)
    }
    /**
     * 获取根元素,并存储到vue实例,简单检查以下传入的el是否合规
    */
    initRootElement(options){
        if(typeof options.el === 'string'){
            this.$el = document.querySelector(options.el)
        }else if(options.el instanceof HTMLElement){
            this.$el = options.el
        }
        if(!options.el){
            throw new Error('传入的el不喝大,请传入css selector 或者htmlelement')
        }
    }
    // data中的属性挂载到this实例上
    _proxyData(data){
        Object.keys(data).forEach(key=>{
            Object.defineProperty(this,key,{
                enumerable: true,
                configurable: true,
                get(){
                    return data[key]
                }
                set(newValue){
                    if(data[key] === newValue){
                        return
                    }
                    data[key] = newValue
                }
            })
        })
    }
}
  1. 验证一下,新建index.js
import Vue from './myvue/vue.js'
const vm = new Vue({
    el: '#app',
    data: {
        msg: 'hello world',
        testHtml: '<h2 style="color: red">html</h2>'
    },
    methods: {
        handler(){
            alert(111)
        }
    }
})
console.log(vm)
  1. vue里可以通过this来获取到data的属性
 // data中的属性挂载到this实例上
    _proxyData(data){
        Object.keys(data).forEach(key=>{
            Object.defineProperty(this,key,{
                enumerable: true,
                configurable: true,
                get(){
                    return data[key]
                }
                set(newValue){
                    if(data[key] === newValue){
                        return
                    }
                    data[key] = newValue
                }
            })
        })
    }
  1. 接下来,先把几个核心类声明好,先忽略具体的实现
// dep.js
/**
* 发布订阅的模式
* 存储所有的观察者,watcher,
* 每个watcher都有一个update
* 通知subs里的每个watcher实例,触发update方法
*/

export default class Dep{
    constructor(){
        // 存储素有的观察者
        this.subs = []
    }
    /** 添加观察者 */
    addSub(watcher){
        if(watcher && watcher.update) {
            this.subs.push(watcher)
        }
    }
    /** 发送通知 */
    notify(){
        this.subs.forEach(watcher=>{
            watcher.update()
        ]})
    }

// Dep在哪里实例化,哪里addsub
// Dep notify 在哪里调用
}
// observer.js
export default class Observer{
    constructor(data){
        this.traverse(data)
    }
    /** 递归遍历data里的所有属性 */
    traverse(data){
        if(!data || typeof data !== 'object'){
            return 
        }
        Object.keys(data).forEach(key=>{
            this.defineReactive(data,key,data[key])
        })
    }
    /** 给传入的数据设置 getter/setter */
    defineReactive(obj, key, val){
        this.traverse(val)
        const dep = new Dep()
        const that = this
        Object.defineProperty(obj.key,{
            configurable: true,
            enumberable: true,
            get(){
                Dep.target && dep.addSub(Dep.target);
                return value
            },
            set(newValue){
                if(newValue === val){
                    return 
                }
                val = newValue
                that.traverse(newValue)
                dep.notify()
            }
        })
    }
}
// watcher.js
import Dep from './Dep.js'
export default class Watcher{
    /**
    * @param vm vue实例
    * @param key data中的属性名
    * @param cb 负责更新的回调函数
    */
    constructor(vm,key,cb){
        this.vm = vm
        this.key = key
        this.cb = cb
        Dep.target = this
        // 触发get方法,在get方法中会做一些操作?
        this.oldValue = vm[key]
        Dep.target = null
    }
    /** 当数据变化的时候,更新视图 */
    update(){
        let newValue = this.vm[this.key]
        if(this.oldValue === newValue){
            return
        }
        this.cb(newValue)
    }
}
// watcher初始化获取oldValue的时候,会去做一些什么操作
// 通过vm[key]获取oldValue前,为什么要将实例挂载DEP上,获取之后为什么又要置为null
// update方法是什么时候执行的
// compiler.js 编译模版
import Watcher fom './watcher.js'
export default class Compiler{
    constructor(vm){
        this.el = vm.$el
        this.vm = vm
        this.methods = vm.$methods
        this.compile(vm.$el)
    }
    /** 编译模版 */
    compile(el){
        const childNodes = el.childNodes
        Array.from(childNodes).forEach(node=>{
            if(this.isTextNode(node)){
                this.compileText(node)
            }else if(this.isElementNode(node)){
                this.compileElement(node)
            }
            if(node.childNodes&& node.childNodes.legnth>0){
                this.compile(node)
            }
        })
    }
    compileText(node){
        //{{}}
        const reg = /|{\{(.*)\}\}/g
        const value = node.textContent
        if(reg.test(value)){
            const key = RegExp.$1.trim()
            node.textContent = value.replace(reg,this.vm[key])
            new Watcher(this.vm,key,(newValue)=>{
                node.textContent = newValue
            })
        }
    }
    compileElement(node){
        if(node.attributes.length){
            Array.from(node.attributes).forEach(attr=>{
                const attrName = attr.name
                if(this.isDirective(attrName)){
                    let directiveName = attrName.inedexOf(':')>-1?attrName.substr(5):attrName.substr(2)
                    let key = attr.value
                    // 更新元素节点
                    this.update(node,key,directiveName)
                }
            })
        }
    }
    update(node,key,directiveName){
        // v-model,v-text,v-html,v-on:click
        const updateFn = this[directiveName + 'Updater']
        updateFn && updateFn.call(this,node, this.vm[key],key, directiveName)
    }
    textUpdater(node, value, key){
        node.textContent = value
        new Watcher(this.vm,key,(newValue)=>{
            node.textContent = newValue
        })
    }
    modelUpdater(node, value, key){
        node.value = value
        new Watcher(this.vm,key,(newValue)=>{
            node.value = newValue
        })
        node.addEventListener('input',()=>{
            this.vm[key] = node.value
        })
    }
    htmlUpdater(node, value, key){
        node.innerHTML = value
        new Watcher(this.vm,key,(newValue)=>{
            node.innerHTML = newValue
        })
    }
    clickUpdater(node, value, key,directiveName){
        node.addEventListener(directiveName,this.methods[key])
    }
    /**判断是否是文本节点*/
    isTextNode(node){
        return node.nodeType === 3
    }
     /**判断是否是元素节点*/
    isElementNode(node){
        return node.nodeType === 1
    }
    isDirective(attrName){
        return attrName.startWith('v-')
    }
}