长文警告!阅读时长5-10m。以代码为主,你将了解Vue响应式原理和运行机制。前端
原理:vue
<body>
<main>
<input type="text" id="input">
<br/>
<label>值:<span id="span"></span></label>
</main>
<script src="./main.js"></script>
</body>
复制代码
const obj = {};
const inputDom = document.querySelector('#input');
const spanDom = document.querySelector('#span');
Object.defineProperty(obj, 'txt', {
get() {},
set(newVal) {
inputDom.value = newVal;
spanDom.innerHTML = newVal;
}
})
inputDom.addEventListener('input', (e) => {
obj.txt = e.target.value
})
复制代码
看看效果node
原理:git
let uid = 0;
class Dep {
constructor() {
this.id = uid++;
this.subs = [];
}
// 添加订阅者
addSub(sub) {
this.subs.push(sub)
}
// 通知订阅者更新
notify() {
this.subs.forEach(sub => sub.update())
}
//
depend() {
Dep.target.addDep(this)
// 如果新Dep,则会触发addSub从新添加订阅
}
}
// 当指向当前活跃的Watcher => 执行get 便于收集依赖时(排除没必要要的依赖)
Dep.target = null;
复制代码
import Dep from './Dep'
class Watcher {
constructor(vm, expOrFn, cb) {
this.depIds = {}; // 存储订阅者的id
this.vm = vm; // vue实例
this.expOrFn = expOrFn; // 订阅数据的key
this.cb = cb; // 数据更新回调
this.val = this.get(); // 首次实例,触发get,收集依赖
}
get() {
// 当前订阅者(Watcher)读取被订阅数据的值时,通知订阅者管理员收集当前订阅者
Dep.target = this;
// 执行一次get
const val = this.vm._data[this.expOrFn];
Dep.target = null;
return val
}
update() {
this.run()
}
run () {
const val = this.get();
if (val !== this.val || isObject(val)) {
this.val = val;
this.cb.call(this.vm, val);
}
}
addDep(dep) {
if (!this.depIds.hasOwnProperty(dep.id)) {
dep.addSub(this)
this.depIds[dep.id] = dep;
}
}
}
function isObject (obj) {
return obj !== null && typeof obj === 'object'
}
export default Watcher;
复制代码
class Observer {
constructor(value) {
this.value = value
this.dep = new Dep()
def(value, '__ob__', this)
if (Array.isArray(value)) {
/// 数组,包装数组响应式方法
protoAugment(value, arrayMethods)
this.observeArray(value)
} else {
// 对象,遍历属性,劫持数据
this.walk(value)
}
}
walk(value) {
Object.keys(value).forEach(key => this.convert(key, value[key]))
}
convert(key, val) {
defineReactive(this.value, key, val)
}
observeArray (items) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
function defineReactive(obj, key, val) {
const dep = new Dep();
// 递归添加数据劫持
let chlidOb = observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
if (Dep.target) {
dep.depend();
if (chlidOb) {
chlidOb.dep.depend()
if (Array.isArray(val)) {
dependArray(val)
}
}
}
return val
},
set(newVal) {
if (newVal === val) return;
val = newVal;
chlidOb = observe(newVal);
dep.notify()
}
})
}
复制代码
值得一提的是,defineProperty没法监听数组变化,这也是咱们在使用vue初期,困扰的this.arr[index] = xxx不会更新页面的问题,必须在使用array的方法(经vue包装过)才能达到预期效果,下面试着改造下Array的方法。github
import { def } from './util'
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
// 会修改原数组的方法
const methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ]
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.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
}
if (inserted) ob.observeArray(inserted)
ob.dep.notify()
return result
})
})
复制代码
实际使用面试
const vm = new Vue({
data: {
txt: '',
arr: []
},
});
inputDom.addEventListener('input', e => vm.txt = e.target.value);
buttonDom.addEventListener('click', e => vm.arr.push(1));
vm.$watch('txt', txt => spanDom.innerHTML = txt);
vm.$watch('arr', arr => span1Dom.innerHTML = arr);
复制代码
看看效果算法
v2须要开发者操做dom,这一点也不mvvm。向vue看齐,实现一个简单的模版compiler,处理模版;绑定数据;挂载dom;达到隔离dom操做的效果。小程序
原理:segmentfault
export default function parseHTML(template) {
const box = document.createElement('div')
box.innerHTML = template
const fragment = nodeToFragment(box);
return fragment
}
export function nodeToFragment(el) {
const fragment = document.createDocumentFragment();
let child = el.firstChild;
while (child) {
fragment.appendChild(child);
child = el.firstChild
}
return fragment;
}
复制代码
export default function patch(el, vm) {
const childNodes = el.childNodes;
[].slice.call(childNodes).forEach(function(node) {
const text = node.textContent;
if (node.nodeType == 1) {
// 元素节点
patchElement(node, vm);
} else if (node.nodeType == 3) {
// 文本节点
patchText(node, vm, text);
}
if (node.childNodes && node.childNodes.length) {
patch(node, vm);
}
});
return el
}
<!--patchElement-->
export default function patchElement(node, vm) {
const nodeAttrs = node.attributes;
const nodeAttrsArr = Array.from(nodeAttrs)
nodeAttrsArr.forEach((attr) => {
const { name, value } = attr;
// 默认指令
if (dirRE.test(name)) {
if (bindRE.test(name)) { // v-bind
const dir = name.replace(bindRE, '')
handleBind(node, vm, value, dir)
} else if (modelRE.test(name)) { // v-model
const dir = name.replace(modelRE, '')
handleModel(node, vm, value, dir)
} else if (onRE.test(name)) { // v-on/@
const dir = name.replace(onRE, '')
handleEvent(node, vm, value, dir)
} else if (ifArr.includes(name)) { // v-if
handleIf(node, vm, value, name)
} else if (forRE.test(name)) { // v-for
handleFor(node, vm, value)
}
node.removeAttribute(name);
}
})
return node
};
<!--patchText-->
const defaultTagRE = /\{\{(.*)\}\}/
export default function patchText(node, vm, text) {
if (defaultTagRE.test(text)) {
const exp = defaultTagRE.exec(text)[1]
const initText = vm[exp];
updateText(node, initText);
new Watcher(vm, exp, (value) => updateText(node, value));
}
}
function updateText(node, value) {
node.textContent = isUndef(value) ? '' : value;
}
复制代码
<!--v-bind-->
export function handleBind (node, vm, exp, dir) {
const val = vm[exp];
updateAttr(node, val);
new Watcher(vm, exp, (value) => updateAttr(node, value));
}
const updateAttr = (node, attr, value) => node.setAttribute(attr, isUndef(value) ? '' : value);
<!--v-model-->
export function handleModel (node, vm, exp, dir) {
let val = vm[exp];
updateModel(node, val);
new Watcher(vm, exp, (value) => updateModel(node, value));
handleEvent(node, vm, (e) => {
const newValue = e.target.value;
if (val === newValue) return;
vm[exp] = newValue;
val = newValue;
}, 'input')
}
export function handleEvent (node, vm, exp, dir) {
const eventType = dir;
const cb = isFun(exp) ? exp : vm[exp].bind(vm);
if (eventType && cb) {
node.addEventListener(eventType, e => cb(e), false);
}
}
const updateModel = (node, value) => node.value = isUndef(value) ? '' : value;
<!--v-for-->
export function handleFor (node, vm, exp) {
const inMatch = exp.match(forAliasRE)
if (!inMatch) return;
exp = inMatch[2].trim();
const alias = inMatch[1].trim();
const val = vm[exp];
const oldIndex = getIndex(node);
const parentNode = node.parentNode;
parentNode.removeChild(node);
node.removeAttribute('v-for');
const templateNode = node.cloneNode(true);
appendForNode(parentNode, templateNode, val, alias, oldIndex);
new Watcher(vm, exp, (value) => appendForNode(parentNode, templateNode, val, alias, oldIndex));
}
function appendForNode(parentNode, node, arr, alias, oldIndex) {
removeOldNode(parentNode, oldIndex)
for (const key in arr) {
const templateNode = node.cloneNode(true)
const patchNode = patch(templateNode, {[alias]: arr[key]})
patchNode.setAttribute('data-for', true)
parentNode.appendChild(patchNode)
}
}
复制代码
如今,咱们用模版试下效果数组
let vm = new Vue({
el: '#app',
template:
`<div>
<input v-model="txt" type="text"/>
<input @input="input" type="text"/>
<br />
<label>值:<span>{{txt}}</span></label>
<br />
<button @click="addArr">数组+1</button>
<br />
<label>数组:<span v-for="item in arr">{{item}}</span></label>
<br />
<label v-if="txt">是:<span>{{txt}}</span></label>
<label v-else="txt">否</label>
</div>`,
data: {
txt: '',
arr: [1, 2, 3]
},
methods: {
input(e) {
const newValue = e.target.value;
if (this.txt === newValue) return;
this.txt = newValue;
},
addArr() {
this.arr.push(this.arr.length + 1)
}
}
});
复制代码
做为消费级的框架而不是玩具(呵呵!依然是玩具。。。),固然是但愿在保证可开发维护同时,咱们的性能要过得去。
显然,由于数据变化而频繁地更新dom,不是咱们想要。vue给的方案是VNode(对象的方式描述dom)
原理:
例:
export function generate (ast) {
const code = ast ? genElement(ast) : '_c("div")'
return {
render: `with(this){return ${code}}`
}
}
export function genElement (el) {
if (el.for && !el.forProcessed) {
return genFor(el)
} else if (el.if && !el.ifProcessed) {
return genIf(el)
} else {
let code
let data
if (!el.plain) {
data = genData(el)
}
const children = genChildren(el, true)
code = `_c('${el.tag}'${
data ? `,${data}` : '' // data
}${
children ? `,${children}` : '' // children
})`
return code
}
}
....
复制代码
function updateChildren (parentElm, oldCh, newCh) {
let oldStartIdx = 0
let newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
let oldKeyToIdx, idxInOld, vnodeToMove, refElm
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) { // oldStart == newStart 更新节点
patchVnode(oldStartVnode, newStartVnode)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) { // oldEnd == newEnd 更新节点
patchVnode(oldEndVnode, newEndVnode)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) { // oldStart == newEnd 更新节点 节点右移
patchVnode(oldStartVnode, newEndVnode)
nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) { // oldStart == newEnd 更新节点 节点左移
patchVnode(oldEndVnode, newStartVnode)
nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
if (isUndef(idxInOld)) { // New element
createElm(newStartVnode, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
vnodeToMove = oldCh[idxInOld]
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode)
oldCh[idxInOld] = undefined
nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
} else {
// 相同的键,但不一样的元素。看成新元素对待
createElm(newStartVnode, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}
}
newStartVnode = newCh[++newStartIdx]
}
}
if (oldStartIdx > oldEndIdx) { // 须要新增节点
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx)
} else if (newStartIdx > newEndIdx) { // 须要移除节点
removeVnodes(oldCh, oldStartIdx, oldEndIdx)
}
}
复制代码
看一下分解动做:
从sameVnode判断上不难看出,在v-for循环出的列表的场景中,对元素设置key,直接指导diff是否复用DOM。
敲黑板,这里指出两个咱们编写时的问题
写到这里一步,完成了vue的基本操纵,剩下扩展component/filter/mixin/生命周期等等特性就不一一分解了。 以上代码主要为了描述vue运行过程,部分借鉴vue源码,但丢失了不少细节,有兴趣的同窗能够参考vue源码分析。
事实上,虚拟dom的意义远非提升性能这么简单。咱们有了描述UI的规则后,单从vue来说,不依赖常规宿主环境,能够是浏览器,是weex,或者node跑ssr;从大环境来说,这为原生级跨端提供可能,好比RN;固然也有从编译上阶段实现跨平台的,好比Taro/uniapp。
关于下一版,参考vue3.x,实现一些新特性。
前面提到Vue2.x采用defineProperty劫持数据,这个作法有两个问题。
一是须要初始化时,遍历递归一必定义OB;
二是没法劫持数组的变化,倒不是没有方案劫持数组,基于性能考量,Vue采用了改造数组方法的方式;
复制代码
Vue3.0采用了新的劫持方案Proxy,一次性解决上述问题。但就目前国内环境而言,依然存在大量低版本ie用户,兼容版还会沿用2.x的机制
✅ 解决的问题:
将任意个组件特征(属性和方法)拷贝到须要的组件中,达到复用的目的
复制代码
❌ 形成的困扰:
当多个mixins配合时,会出现数据源不清晰和命名可能冲突的问题
复制代码
✅ 解决的问题:
让组件通用功能获得封装,而不一样逻辑经过插槽分发
复制代码
❌ 形成的困扰:
多层组件嵌套时,没法清晰的体现具体是哪一个组件在模板中提供哪一个变量。
须要额外实例组件,形成额外性能开销
复制代码
✅ 解决的问题:
秉承分层的思想,能够处理和分发传入的参数和方法
复制代码
❌ 形成的困扰:
来自民间的用法,相较与React,Vue的HOC使用起来尤其鸡肋。
由于原来的父子组件关系被分割,产生了属性和方法以及真实ref传递问题,好比v-model之类的,都须要高阶组件手动处理。
与slot-scope相似,由于须要额外的实例组件而形成性能开销
复制代码
✅ 解决的问题(案例)
从官方给出的案例来看,确实不存在上述方案形成的反作用。
至因而否会像社区所反应的,基于函数的 API 会形成大量面条代码产生,这就须要你们实践了才知道了。
复制代码
如下内容,纯属我的YY,不喜轻喷。
关于下一代,React已经指明了一个小方向---Fiber。且先不谈它的出现会不会像vdom同样为前端带来革命性的性能提高,单单循环任务调度的思路就很契合js的开发思路,Vue会不会借鉴暂时还不清楚,但至少会有适合Vue的方案出现。
在编译阶段作更多文章,在开发者和机器之间作更多,一方面能让开发者更加专一逻辑而不是代码组织;另外一方面提升运行时的效率,借鉴一个现下很热门的例子---WebAssembly,固然编译成机器更易于理解和执行的代码,势必让框架编写更多的判断来解决适配以及线上调试难以定位等等问题。合理分割compiler和runtime的代码也是框架必须思考的问题。
而后是Service Worker,目前看真正获得普遍应用的仍是PWA方面,相信在Google的进一步推广下(Apple依然会从中做梗),成为标准也将会在各大框架中获得应用,好比把diff放到WebWorker中去。这远比小程序的思路---双线程要来得有意思的多,固然我仍是尊重小程序做为平台向的做用。只是各家小程序接口和质量不一,没有标准,要坐等小程序消费大户---JD继续探索。。。