vue响应式和依赖收集
2021-01-08 本文已影响0人
一蓑烟雨任平生_cui
看了vue源码后实现的一个很简单很简单的vue😂
目的主要是串一下new Vue()之后到组件挂载的流程,及数据更新时视图的更新流程。
源码主要流程如下:
- new Vue()
- this._init()
- initLifecycle(vm)
- initEvents(vm)
- initRender(vm)
- callHook(vm, 'beforeCreate')
- initInjections(vm)
- initState(vm)
- initProps(vm)
- initMethods(vm)
- vm.XXX = this.methods[XXX]
- initData(vm)
- observe(value) // 开启响应式
- initComputed(vm)
- initWatch(vm)
- vm.$watch(expOrFn, cb, option)
- initProvide(vm)
- callHook(vm, 'created')
- vm.mount(vm.mount(el)
- vm.$mount(el)
- mountComponent(vm)
- callHook(vm, 'beforeMount')
- new Watcher()
- callHook(vm, 'mounted')
具体包括对象和数组的响应式原理、发布订阅、观察者、单例模式、依赖收集、模版编译
// index.js
import { initState, initMethods } from './init.js'
import initLifecycle from './initLifecycle.js'
import mounted from './mounted.js'
import Compiler from './compiler.js'
class Vue {
constructor(options) {
this.vm = this
this.$options = options
this.init(this)
}
// 初始化操作
init(vm) {
initState(vm)
initMethods(vm)
initLifecycle(vm)
mounted(vm)
}
$mount(el) {
Compiler.getInstance(this, el)
}
}
export default Vue
// init.js
import { def, observe, proxy } from './utils.js'
function initData(vm) {
let data = vm.$options.data
// 保存_data 主要是因为data可能是函数
data = vm._data = typeof data === 'function' ? data.call(vm, vm) : data || {}
Object.keys(data).forEach(key => {
proxy(vm, '_data', key)
})
observe(data)
}
function initProps() {}
function initComputed() {}
function initWatch() {}
export function initState(vm) {
initData(vm)
initProps(vm)
initComputed(vm)
initWatch(vm)
}
export function initMethods(vm) {
const methods = vm.$options.methods
if (methods) {
for (const key in methods) {
if (methods.hasOwnProperty(key)) {
def(vm, key, methods[key])
}
}
}
}
// initLifecycle.js
export default function initLifecycle(vm) {
const created = vm.$options.created
if (created) {
created.call(vm)
}
}
// mounted.js
import Compiler from './compiler.js'
// 挂载
export default function mounted(vm) {
const el = vm.$options.el
// 是否提供 el 选项,如果没有则调用实例的 $mount() 方法
if (el) {
Compiler.getInstance(vm, el)
}
}
// utils.js
import Observer from './observer.js'
export function def(target, key, value, enumerable = false) {
Object.defineProperty(target, key, {
value,
enumerable,
writable: true,
configurable: true
})
}
// 将属性代理到实例上 以便可以通过 this.XXX 方式访问
export function proxy(target, sourceKey, key) {
Object.defineProperty(target, key, {
get() {
return target[sourceKey][key]
},
set(newValue) {
target[sourceKey][key] = newValue
}
})
}
export function observe(data) {
if (typeof data !== 'object' || data === null) return
return new Observer(data)
}
// array.js
const arrayPrototype = Array.prototype
const arrayMethods = Object.create(arrayPrototype)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
// 重写数组方法
methodsToPatch.forEach(method => {
// 缓存原方法
const originalMethod = arrayPrototype[method]
arrayMethods[method] = function (...args) {
const result = originalMethod.apply(this, args)
const 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) {
// this 为修改后的数组
// 在一开始初始化数组时已经给它添加了属性 __ob__ 指向 Observer 的实例
ob.observeArray(this)
}
return result
}
})
export default arrayMethods
// defineReactive.js
import Dep from './dep.js'
import { observe } from './utils.js'
// 响应式
export default function defineReactive(target, key, value) {
observe(value)
const dep = new Dep()
Object.defineProperty(target, key, {
get() {
Dep.target && dep.addSub(Dep.target)
return value
},
set(newValue) {
if (value !== newValue) {
value = newValue
observe(newValue)
// 通知依赖 更新视图
dep.notify()
}
},
enumerable: true,
configurable: true
})
}
// dep.js
// 订阅发布
export default class Dep {
constructor() {
this.subs = []
}
addSub(watcher) {
this.subs.push(watcher)
}
notify() {
this.subs.forEach(watcher => {
watcher.update()
})
}
}
// watcher.js
import Dep from './dep.js'
// 观察者
export default class Watcher {
constructor(vm, exp, cb) {
this.vm = vm
this.exp = exp
this.cb = cb
Dep.target = this
// 取值
this.get()
Dep.target = null // 避免重复添加Watcher
}
get() {
this.value = this.vm[this.exp]
this.cb(this.value)
}
update() {
const value = this.vm[this.exp]
this.cb.call(this.vm, value, this.value)
this.value = value
}
}
// observer.js
import arrayMethods from './array.js'
import defineReactive from './defineReactive.js'
import { observe, def } from './utils.js'
export default class Observer {
constructor(data) {
this.value = data
// 便于在其他处操作data时,可以找到该data对应的Observer实例
def(data, '__ob__', this) // 必须定义成不可枚举的,否则会陷入死循环。仔细品一下为啥😄
if (Array.isArray(data)) {
// 数组
data.__proto__ = arrayMethods
this.observeArray(data)
} else {
// 对象
this.walk(data)
}
}
walk(obj) {
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
observeArray(array) {
for (let i = 0; i < array.length; i++) {
observe(array[i])
}
}
}
// compiler.js
import Watcher from './watcher.js'
// 插值正则
const REG = /\{\{(.*)\}\}/
class Compiler {
constructor(vm, el) {
this.$el = document.querySelector(el)
this.$vm = vm
if (this.$el) {
// 将节点转成文档片段
this.nodeToFragment(this.$el)
// 编译
this.compile(this.$fragment)
// 将编译后的片段插入html
this.$el.appendChild(this.$fragment)
}
}
// 采用单例模式 避免同时存在传入 el 选项 和 调用了 $mount(el)
static getInstance(vm, el) {
if (!this.instance) {
this.instance = new Compiler(vm, el)
}
return this.instance
}
nodeToFragment(el) {
const fragment = document.createDocumentFragment()
let child
while ((child = el.firstChild)) {
fragment.appendChild(child)
}
this.$fragment = fragment
}
compile(fragment) {
const childNodes = fragment.childNodes
Array.from(childNodes).forEach(node => {
if (this.isElement(node)) {
// console.log('编译元素', node.nodeName)
const attrs = node.attributes
Array.from(attrs).forEach(attr => {
const attrName = attr.name
const attrValue = attr.value
if (this.isDirective(attrName)) {
// 指令
const dirName = attrName.slice(2) + 'Dir'
console.log(dirName)
this[dirName] && this[dirName](node, this.$vm, attrValue)
}
// 事件
const eventName = this.isEvent(attrName)
if (eventName) {
this.eventHandler(node, eventName, attrValue)
}
})
} else if (this.isInterpolation(node)) {
// console.log('编译插值表达式', node.nodeValue)
this.update(node, this.$vm, RegExp.$1, 'text')
}
if (node.childNodes?.length) {
this.compile(node)
}
})
}
isElement({ nodeType }) {
return nodeType === 1
}
// 含有插值表达式的文本节点
isInterpolation({ nodeType, nodeValue }) {
return nodeType === 3 && REG.test(nodeValue)
}
textUpdate(node, value) {
node.textContent = value
}
// 更新函数
update(node, vm, exp, dir) {
const updateFn = this[`${dir}Update`]
new Watcher(vm, exp, function (newValue) {
updateFn && updateFn(node, newValue)
})
}
isDirective(name) {
return /^v-(.*)/.test(name)
}
isEvent(name) {
return /^v-on:|@(.*)/.test(name) && RegExp.$1
}
textDir(node, vm, exp) {
this.commonWatcher(node, vm, 'textContent', exp)
}
modelDir(node, vm, exp) {
this.commonWatcher(node, vm, 'value', exp)
node.addEventListener('input', e => {
vm[exp] = e.target.value
})
}
htmlDir(node, vm, exp) {
this.commonWatcher(node, vm, 'innerHTML', exp)
}
commonWatcher(node, vm, prop, exp) {
new Watcher(vm, exp, function (value) {
node[prop] = value
})
}
eventHandler(node, eventName, exp) {
node.addEventListener(eventName, this.$vm[exp].bind(this.$vm))
}
}
export default Compiler
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<h1>{{title}}</h1>
<h2>{{age}}</h2>
<h3>
<span>{{age}}</span>
</h3>
<hr>
<input type="text" v-model="tel">
<h3>{{tel}}</h3>
<hr>
<div v-text="desc"></div>
<br>
<div>这是静态文本</div>
<hr>
<button @click="add">add</button>
<h2>{{count}}</h2>
<div v-html="html"></div>
</div>
</body>
</html>
<script type="module">
import Vue from './index.js'
new Vue({
el: '#app',
data() {
return {
title: 'hello',
tel: 1571121,
desc: '这是v-text文本',
count: 2,
age: 12,
html: '<p>这是html片段</p>',
info: {
name: 'mike',
age: 23
},
skill: ['eat', 'song', {
foo: 'bar'
}]
}
},
methods: {
add(e) {
this.count = this.count + 1
this.html = '<h2>修改html片段</h2>'
this.tel = 565744
this.desc = '3453534'
}
},
created() {
setTimeout(() => {
this.age = 34
}, 2000)
}
}).$mount('#app') // 如果没有传el选项,则调用$mount方法实现挂载
// OR
// const vm = new Vue()
// vm.$mount('#app')
</script>