Vue2核心原理(简易) - 视图更新(初次渲染)笔记

906 阅读3分钟

前言

  • 本章项目地址
  • 当数据发生变化 视图也在发生变化
  • 每个组件都有一个渲染watchernew Watch()(观察者模式)
  • new Watche()组件开始渲染组件,在render函数生成vnode时,调用了劫持数据getter
  • 这个时候每个属性可以订阅一个渲染watcher, 在数据发生变化时就会调用劫持数据的setter
  • setter里去通过一个notify去调用渲染watcher中的updateComponent方法去更新视图

html 和 javascript模板

<div id="app">{{ message }} - {{ arr }}</div>

<script>
var vm = new Vue({
    data: {
        message: 'Hello Vue!',
        arr: [[0]]
    }
})

vm.$mount('#app')

setTimeout(() => {
    vm.message = 'vue updater'
    vm.arr.push(100)
}, 2000)
</script>

render函数生成vnode 方法

/**
 * @description 创建标签vnode
 */
export function createElement(vm, tag, data = {}, ...children) {
    return vnode(vm, tag, data, data.key, children, undefined);
}

/**
 * @description 创建文本的vnode
 */
export function createTextElement(vm, text) {
    return vnode(vm, undefined, undefined, undefined, undefined, text);
}

/** 核心方法 */
/**
 * @description 套装vnode
 */
function vnode(vm, tag, data, key, children, text) {
    return {
        vm,
        tag,
        data,
        key,
        children,
        text,
        // .....
    }
}
export function renderMixin(Vue){
    Vue.prototype._c = function() {
        return createElement(this,...arguments)
    }

    Vue.prototype._v = function(text) {
        return createTextElement(this,text)
    }

    Vue.prototype._s = function(val) {
        if(typeof val == 'object') return JSON.stringify(val)
        return val;
    }

    Vue.prototype._render = function(){
       const vm = this
       let render = vm.$options.render
       let vnode = render.call(vm)
       return vnode
    }
}

初始化

function Vue (options) {
    /** 初始化 */
    this._init(options)
}
/**
 * @description 扩展原型
 */
initMixin(Vue)
renderMixin(Vue)
export function initMixin (Vue) {    
    Vue.prototype._init = function (options) {
        vm.$options = options

        /** 数据初始化 */
        initState(vm)

        /** compile */
        if(vm.$options.el){
            vm.$mount(vm.$options.el);
        } 
    }

    Vue.prototype.$mount = function (el) {
        const vm = this;
        const options = vm.$options
        el = document.querySelector(el);
        vm.$el = el;

        if(!options.render) {
            let template = options.template;
            if(!template && el) {
                template = el.outerHTML;
            }
            // 生成的render函数
            let render = compileToFunction(template);
            options.render = render;
        }

        /** 组件挂载 */
        mountComponent(vm,el)
    }
}

组件挂载,初始化渲染watcher

mountComponent文件

import { patch } from './vdom/patch'
import Watcher from './observer/watcher'

/** 生命周期 */
export function lifecycleMixin(Vue) {
  Vue.prototype._update = function(vnode) {
        const vm = this
        // patch是将老的vnode和新的vnode做比对 然后生成真实的dom
        vm.$el = patch(vm.$el, vnode)
 	 }
}


export function mountComponent(vm, el) {
    // 更新函数 数据变化后 会再次调用此函数
    let updateComponent = () => {
        vm._update(vm._render())
    }

    new Watcher(vm, updateComponent, () => {
        console.log('视图更新了')
    }, true)
}

patch文件 将vnode生成真实dom

/** patch 文件 这里只是简单处理 直接生成真实的dom */
export function patch(oldVnode, vnode) {
    if (oldVnode.nodeType == 1) {
        const parentElm = oldVnode.parentNode
        let elm = createElm(vnode)

        // 在第一次渲染后 删除掉节点
        parentElm.insertBefore(elm, oldVnode.nextSibling)
        parentElm.removeChild(oldVnode)

        return elm
    }
}

function createElm(vnode) {
    let { tag, data, children, text, vm } = vnode
    if (typeof tag === 'string') {
        vnode.el = document.createElement(tag)
        children.forEach(child => {
            vnode.el.appendChild(createElm(child))
        })
    } else {
        vnode.el = document.createTextNode(text)
    }
    return vnode.el
}

Watcher 和 Dep (重点)

Watcher 与 Dep 关联起来 是靠当组件渲染时 会走数据 数据已劫持 在getter中 每个属性 都有Dep 在Dep上加类属性 Dep.target 在渲染时将Dep.target指向这个渲染Watcher(神来之笔)

Watcher 类

import { popTarget, pushTarget } from './dep'
import { queueWatcher } from './scheduler'

let id = 0
class Watcher {
    constructor(vm,exprOrFn,cb,options){
        this.vm = vm
        this.exprOrFn = exprOrFn
        this.cb = cb
        this.options = options
        this.id = id++
		
        // 视图更新 就是上面的updateComponent方法
        this.getter = exprOrFn 
        this.deps = [] 
        this.depsId = new Set()

        this.get()
    }

    get(){
        // 将渲染wather指向给dep 在数据getter时 依赖起来 将dep与watcher关联起来(神来之笔)
        pushTarget(this)
        this.getter()
        // 页面渲染后将 Dep.target = null
        popTarget()
    }

    update(){
      // 异步更新操作 就是将更新缓存起来(做一些去重, 防抖)然后一起调用,最后还是调用下方run方法
       queueWatcher(this)
    }
    run(){
        this.get()
    }
    
    /**
     * @description 将watcher 存储dep dep也存储watcher实现双向双向存储, 并做去重处理
     * @description 给每个属性都加了个dep属性,用于存储这个渲染watcher (同一个watcher会对应多个dep)
     * @description 每个属性可能对应多个视图(多个视图肯定是多个watcher) 一个属性也要对应多个watcher
     */
    addDep(dep){
        let id = dep.id;
        if(!this.depsId.has(id)){
            this.depsId.add(id);
            this.deps.push(dep);
            dep.addSub(this)
        }
    }
}

export default Watcher

Dep 类

/** 每个劫持的属性 加上唯一的标识 */
let id = 0

/**
 * @description 每个劫持的属性 new Dep
 */
class Dep {
    constructor() {
        this.id = id++
        this.subs = []
    }

    /** dep传给watcher */
    depend() {
        if (Dep.target) {
            Dep.target.addDep(this)
        }
    }

    addSub(watcher) {
        this.subs.push(watcher)
    }

    notify() {
        this.subs.forEach(watcher => watcher.update())
    }

}

Dep.target = null
let stack = []

export function pushTarget(watcher) {
    Dep.target = watcher
    stack.push(watcher)
}

export function popTarget() {
    stack.pop()
    Dep.target = stack[stack.length - 1]
}

export default Dep

数据劫持

  • 是普通的对象, 直接dep.append()
  • 是数组或者对象,再起上单独增加一个Dep
  • 多层数组要进行递归操作

Observer 类(主文件)

import { isObject } from '../utils'
import { arrayMethods } from './array'
import Dep from './dep'

class Observer {
    constructor(data) {
		
        // 看这里 数据 加了个Dep
        this.dep = new Dep()

        Object.defineProperty(data, '__ob__', {
            value: this,
            enumerable: false
        })
        
        /** 数据是数组 */
        if (Array.isArray(data)) {
            // 针对数组中使用的方法 如push splice... 修改原数组增加的元素(是对象)进行劫持
            data.__proto__ = arrayMethods

            // 初始化 劫持数组中的每个元素 如果是对象进行劫持
            this.observeArray(data)
            return
        }

        /** 数据是对象 */
        this.walk(data)
    }

    walk(data) {
        Object.keys(data).forEach(key => {
            defineReactive(data, key, data[key])
        })
    }

    observeArray(data) {
        data.forEach(item => observe(item))
    }
}

/**
 * @description 看这里 劫持数据只劫持对象 不劫持数组 通过current.__ob__.dep依赖watcehr
 * @description 多层数组 依赖收集 watcher
 */
function dependArray(value) {
    for (let i = 0; i < value.length; i++) {
        let current = value[i]
        current.__ob__ && current.__ob__.dep.depend()
        if (Array.isArray(current)) {
            dependArray(current)
        }
    }
}

/** 核心方法 */
/**
 * @description 劫持对象数据
 */
function defineReactive(data, key, value) {
    let childOb = observe(value)

    let dep = new Dep()

    Object.defineProperty(data, key, {
        get() {
            
            if (Dep.target) {
                dep.depend()

                // 看这里 数组进行依赖收集watcher
                if (childOb) {
                    childOb.dep.depend()

                    // 看这里 多层数组[[[]]] 
                    if (Array.isArray(value)) { dependArray(value) }

                }

            }


            return value
        },
        set(newValue) {
            if (newValue !== value) {
                observe(newValue)
                value = newValue
                dep.notify()
            }
        }
    })
}

export function observe(data) {
    if (!isObject(data)) return
    
    if (data.__ob__) return data.__ob__

    return new Observer(data)
}

arrayMethods调用数组的七个方法时 直接notify()

const oldArrayPrototype = Array.prototype

export let arrayMethods = Object.create(oldArrayPrototype)

/**
 * @description 改变原数组的方法
 */
const methods = [
    'push',
    'pop',
    'unshift',
    'shift',
    'reverse',
    'sort',
    'splice'
]

methods.forEach(method => {
    arrayMethods[method] = function (...args) {
        oldArrayPrototype[method].call(this, ...args)
        
        let ob = this.__ob__
        let inserted
        switch (method) {
            case 'push':
            case 'unshift':
                inserted = args
                break;
            case 'splice':
                inserted = args.slice(2)
                break;
            default:
                break;
        }

        if (inserted) ob.observeArray(inserted)
		
        // 看这里
        ob.dep.notify()

    }
})

下一章 vue2核心原理(简易)-异步更新(Vue.nextTick)笔记