Vue核心源码实现
环境配置、基础代码编写
npm init
npm i webpack webpack-cli webpack-dev-server --save-dev
npm i html-webpack-plugin --save-dev
对象劫持、访问属性代理
数组劫持
vue中数组劫持的缺点
1. 直接通过数组索引改变值,是不会被观察到的。
[1, 3, 4] => vm.nums[2] = 5;
2. 数组长度修改,不会对其观察
编译文本
根据数据进行页面渲染,区分元素节点和文本节点,并进行编译。
依赖收集
数据驱动视图更改。
解决订阅事件重复的问题,使dep和watcher相互依赖。
批量异步更新策略
Promise、MutationObserver 微任务 setImmediate、setTimeout 宏任务
微任务比宏任务消耗的时间短,支持Promise的话,可以更快的执行程序。
/**
- @description 执行异步函数
- @param {function} cb - 回调函数
- @return {void} */ function nextTick (cb) { callbacks.push(cb);
let timerFunction = () => {
flushCallbacks();
}
if (Promise) {
return Promise.resolve().then(timerFunction);
}
if (MutationObserver) {
// html5中的API
let observe = new MutationObserver(timerFunction);
let textNode = document.createElement(10);
observe.observe(textNode, { characterData: true });
textNode.textContent = 20;
return;
}
if (setImmediate) {
// 性能比setTimeout好,老版本不支持
return setImmediate(timerFunction);
}
// 必选项
setTimeout(timerFunction);
}
数组的依赖收集
数组和多维数组需要单独处理依赖收集。
定义watch方法
每一个watch都是一个watcher。
vue原型上挂载watch方法,实例化一个watcher
Vue.prototype.$watch = function (exp, handler) { let vm = this;
// exp - watch
// watch属性的watcher
new Watcher(vm, exp, handler, {
user: true
});
}
在watcher里,获取到改变前与改变后的值,执行回调函数。
这里的watcher属于用户自定义watcher。
计算属性的实现
每一个计算属性都是一个watcher。
function initComputed (vm, computed) {
let watchers = vm._watchersComputed = Object.create(null);
for (let key in computed) {
let useDef = computed[key];
// 计算属性的watcher
watchers[key] = new Watcher(vm, useDef, () => {}, {
lazy: true
});
// 设置getter方法
Object.defineProperty(vm, key, {
get: createComputedGetter(vm, key)
});
}
}
/**
* @description 计算属性的getter方法
* @param {object} vm - vue实例
* @param {string} key - 键
*/
function createComputedGetter (vm, key) {
let watcher = vm._watchersComputed[key];
// dirty -> 计算属性缓存问题
return function () {
if (watcher) {
if (watcher.dirty) {
watcher.evaluate();
}
return watcher.value;
}
}
}
计算属性更新问题
为计算属性添加渲染watcher、
/**
- @description 计算属性的getter方法
- @param {object} vm - vue实例
- @param {string} key - 键 */ function createComputedGetter (vm, key) { let watcher = vm._watchersComputed[key];
// dirty -> 计算属性缓存问题
return function () {
if (watcher) {
if (watcher.dirty) {
watcher.evaluate();
}
if (Dep.target) {
watcher.depend();
}
return watcher.value;
}
}
}
/**
- @description computed属性更新
- 将渲染watcher添加到计算属性中的deps
- 用于计算属性watcher出栈后的数据更新操作 */ depend () { let i = this.deps.length;
while (i--) {
this.deps[i].depend();
}
}
/**
- @description 更新
- @return {void} */ update () { if (this.lazy) { // 计算属性watcher,重新取值 this.dirty = true; } else { // this.get(); // 立即更新 -> 更新队列处理 queryWatcher(this); } }
计算属性更新相对比较复杂。
了解虚拟DOM
虚拟DOM就是一个对象。
{
tag: div,
props: {},
children: [
{
tag: undefined,
props: undefined,
children: undefined,
text: 'yueluo'
}
]
}
使用虚拟DOM,可以有效减少性能消耗。
vue中,虚拟DOM的创建都是依靠h函数。
```js
new Vue({
render (h) {
return h('div', {}, 'yueluo');
}
})
```
h类似于react中的createElement方法。
1. 创建虚拟节点
2. 将虚拟节点转换为真实节点
3. 放入到DOM中
将虚拟DOM转换为真实DOM
虚拟DOM创建、首次渲染
需要对新节点和旧节点进行比对,然后在更新页面。
操作虚拟DOM,比直接操作DOM性能好得多。
节点更新时,虚拟DOM处理
实现patch方法。
针对 老节点有子节点,新节点也有子节点 的情况进行特殊处理。
老节点有子节点,新节点也有子节点的情况
updateChildren DOM Diff 核心逻辑
处理追加元素的逻辑。
a b c
a b c d
```js
/**
* @description 更新子节点
* @param {object} parent 父节点
* @param {object} oldChildren 旧子节点
* @param {object} newChildren 新子节点
* @return {void}
*/
function updateChildren (parent, oldChildren, newChildren) {
let oldStartIndex = 0,
oldStartVnode = oldChildren[0],
oldEndIndex = oldChildren.length - 1,
oldEndVnode = oldChildren[oldEndIndex];
let newStartIndex = 0,
newStartVnode = newChildren[0],
newEndIndex = newChildren.length - 1,
newEndVnode = newChildren[newEndIndex];
while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
if (isSameVnode(oldStartVnode, newStartVnode)) {
patch(oldStartVnode, newStartVnode);
oldStartVnode = oldChildren[++oldStartIndex];
newStartVnode = newChildren[++newStartIndex];
}
}
if (newStartIndex <= newEndIndex) {
for (let i = newStartIndex; i <= newEndIndex; i++) {
parent.appendChild(createElm(newChildren[i]));
}
}
}
```
```js
/**
* @description 判断是不是同一个节点
* @param {object} oldVnode 老节点
* @param {object} newVnode 新节点
* @return {void}
*/
function isSameVnode (oldVnode, newVnode) {
return (oldVnode.tag === newVnode.tag) && (oldVnode.key === newVnode.key);
}
```
处理新元素在头部或元素倒序问题
1.
a b c
e d a b c
2.
a b c
c b a
```js
/**
* @description 更新子节点
* @param {object} parent 父节点
* @param {object} oldChildren 旧子节点
* @param {object} newChildren 新子节点
* @return {void}
*/
function updateChildren (parent, oldChildren, newChildren) {
let oldStartIndex = 0,
oldStartVnode = oldChildren[0],
oldEndIndex = oldChildren.length - 1,
oldEndVnode = oldChildren[oldEndIndex];
let newStartIndex = 0,
newStartVnode = newChildren[0],
newEndIndex = newChildren.length - 1,
newEndVnode = newChildren[newEndIndex];
while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
if (isSameVnode(oldStartVnode, newStartVnode)) {
patch(oldStartVnode, newStartVnode);
oldStartVnode = oldChildren[++oldStartIndex];
newStartVnode = newChildren[++newStartIndex];
} else if (isSameVnode(oldEndVnode, newEndVnode)) {
patch(oldEndVnode, newEndVnode);
oldEndVnode = oldChildren[--oldEndIndex];
newEndVnode = newChildren[--newEndIndex];
} else if (isSameVnode(oldStartVnode, newEndVnode)) {
patch(oldStartVnode, newEndVnode);
parent.insertBefore(oldStartVnode.el, oldEndVnode.el.nextSibling);
oldStartVnode = oldChildren[++oldStartIndex];
newEndVnode = newChildren[--newEndIndex];
}
}
if (newStartIndex <= newEndIndex) {
for (let i = newStartIndex; i <= newEndIndex; i++) {
let refElm = newChildren[newEndIndex + 1].el;
parent.insertBefore(createElm(newChildren[i]), refElm);
}
}
}
```
为什么经常说给属性设置key值时,不要用索引?
a b c d
d c b a
如果元素首尾颠倒时,索引是不变的,diff算法比较时,会根据索引去查询元素。
导致会被判断n次,如果不使用索引,只会虚拟DOM元素的移动。
如果不使用索引作为key,只需要3次DOM移动,倒序的时候可以减少性能消耗。
如果使用索引作为key,会进行4次重新渲染才形成最终的结果。
虚拟DOM代码合并到原有vue逻辑中(合并Vue代码)
将虚拟DOM合并到原有逻辑中。
Vue源码
package.json
```js
"main": "dist/vue.runtime.common.js" => require('vue'); common.js
"module": "dist/vue.runtime.esm.js" => import Vue from 'vue'; es module
```
```js
"scipts": {
"dev": "rollup -w -c scripts/config.js --environment TAGHET:web-full-dev",
"build": "node script/build.js",
"build:ssr": "npm run build -- web-runtime-cjs,web-server-renderer",
"build:weex": "npm run build -- weex"
}
```
scripts/build.js
...